mirror of https://github.com/coder/coder.git
fix: allow mapped resources in our terraform provider (#6242)
Fixes #6234.
This commit is contained in:
parent
2c309194e9
commit
d5af536ea2
|
@ -90,7 +90,7 @@ func ConvertResourcesAndParameters(modules []*tfjson.StateModule, rawGraph strin
|
|||
|
||||
// Indexes Terraform resources by their label.
|
||||
// The label is what "terraform graph" uses to reference nodes.
|
||||
tfResourceByLabel := map[string]*tfjson.StateResource{}
|
||||
tfResourcesByLabel := map[string][]*tfjson.StateResource{}
|
||||
var findTerraformResources func(mod *tfjson.StateModule)
|
||||
findTerraformResources = func(mod *tfjson.StateModule) {
|
||||
for _, module := range mod.ChildModules {
|
||||
|
@ -98,8 +98,10 @@ func ConvertResourcesAndParameters(modules []*tfjson.StateModule, rawGraph strin
|
|||
}
|
||||
for _, resource := range mod.Resources {
|
||||
label := convertAddressToLabel(resource.Address)
|
||||
// index by label
|
||||
tfResourceByLabel[label] = resource
|
||||
if tfResourcesByLabel[label] == nil {
|
||||
tfResourcesByLabel[label] = []*tfjson.StateResource{}
|
||||
}
|
||||
tfResourcesByLabel[label] = append(tfResourcesByLabel[label], resource)
|
||||
}
|
||||
}
|
||||
for _, module := range modules {
|
||||
|
@ -108,212 +110,218 @@ func ConvertResourcesAndParameters(modules []*tfjson.StateModule, rawGraph strin
|
|||
|
||||
// Find all agents!
|
||||
agentNames := map[string]struct{}{}
|
||||
for _, tfResource := range tfResourceByLabel {
|
||||
if tfResource.Type != "coder_agent" {
|
||||
continue
|
||||
}
|
||||
var attrs agentAttributes
|
||||
err = mapstructure.Decode(tfResource.AttributeValues, &attrs)
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("decode agent attributes: %w", err)
|
||||
}
|
||||
|
||||
if _, ok := agentNames[tfResource.Name]; ok {
|
||||
return nil, nil, xerrors.Errorf("duplicate agent name: %s", tfResource.Name)
|
||||
}
|
||||
agentNames[tfResource.Name] = struct{}{}
|
||||
|
||||
// Handling for provider pre-v0.6.10.
|
||||
loginBeforeReady := true
|
||||
if _, ok := tfResource.AttributeValues["login_before_ready"]; ok {
|
||||
loginBeforeReady = attrs.LoginBeforeReady
|
||||
}
|
||||
|
||||
agent := &proto.Agent{
|
||||
Name: tfResource.Name,
|
||||
Id: attrs.ID,
|
||||
Env: attrs.Env,
|
||||
StartupScript: attrs.StartupScript,
|
||||
OperatingSystem: attrs.OperatingSystem,
|
||||
Architecture: attrs.Architecture,
|
||||
Directory: attrs.Directory,
|
||||
ConnectionTimeoutSeconds: attrs.ConnectionTimeoutSeconds,
|
||||
TroubleshootingUrl: attrs.TroubleshootingURL,
|
||||
MotdFile: attrs.MOTDFile,
|
||||
LoginBeforeReady: loginBeforeReady,
|
||||
StartupScriptTimeoutSeconds: attrs.StartupScriptTimeoutSeconds,
|
||||
}
|
||||
switch attrs.Auth {
|
||||
case "token":
|
||||
agent.Auth = &proto.Agent_Token{
|
||||
Token: attrs.Token,
|
||||
}
|
||||
default:
|
||||
// If token authentication isn't specified,
|
||||
// assume instance auth. It's our only other
|
||||
// authentication type!
|
||||
agent.Auth = &proto.Agent_InstanceId{}
|
||||
}
|
||||
|
||||
// The label is used to find the graph node!
|
||||
agentLabel := convertAddressToLabel(tfResource.Address)
|
||||
|
||||
var agentNode *gographviz.Node
|
||||
for _, node := range graph.Nodes.Lookup {
|
||||
// The node attributes surround the label with quotes.
|
||||
if strings.Trim(node.Attrs["label"], `"`) != agentLabel {
|
||||
for _, tfResources := range tfResourcesByLabel {
|
||||
for _, tfResource := range tfResources {
|
||||
if tfResource.Type != "coder_agent" {
|
||||
continue
|
||||
}
|
||||
agentNode = node
|
||||
break
|
||||
}
|
||||
if agentNode == nil {
|
||||
return nil, nil, xerrors.Errorf("couldn't find node on graph: %q", agentLabel)
|
||||
}
|
||||
var attrs agentAttributes
|
||||
err = mapstructure.Decode(tfResource.AttributeValues, &attrs)
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("decode agent attributes: %w", err)
|
||||
}
|
||||
|
||||
if _, ok := agentNames[tfResource.Name]; ok {
|
||||
return nil, nil, xerrors.Errorf("duplicate agent name: %s", tfResource.Name)
|
||||
}
|
||||
agentNames[tfResource.Name] = struct{}{}
|
||||
|
||||
// Handling for provider pre-v0.6.10.
|
||||
loginBeforeReady := true
|
||||
if _, ok := tfResource.AttributeValues["login_before_ready"]; ok {
|
||||
loginBeforeReady = attrs.LoginBeforeReady
|
||||
}
|
||||
|
||||
agent := &proto.Agent{
|
||||
Name: tfResource.Name,
|
||||
Id: attrs.ID,
|
||||
Env: attrs.Env,
|
||||
StartupScript: attrs.StartupScript,
|
||||
OperatingSystem: attrs.OperatingSystem,
|
||||
Architecture: attrs.Architecture,
|
||||
Directory: attrs.Directory,
|
||||
ConnectionTimeoutSeconds: attrs.ConnectionTimeoutSeconds,
|
||||
TroubleshootingUrl: attrs.TroubleshootingURL,
|
||||
MotdFile: attrs.MOTDFile,
|
||||
LoginBeforeReady: loginBeforeReady,
|
||||
StartupScriptTimeoutSeconds: attrs.StartupScriptTimeoutSeconds,
|
||||
}
|
||||
switch attrs.Auth {
|
||||
case "token":
|
||||
agent.Auth = &proto.Agent_Token{
|
||||
Token: attrs.Token,
|
||||
}
|
||||
default:
|
||||
// If token authentication isn't specified,
|
||||
// assume instance auth. It's our only other
|
||||
// authentication type!
|
||||
agent.Auth = &proto.Agent_InstanceId{}
|
||||
}
|
||||
|
||||
// The label is used to find the graph node!
|
||||
agentLabel := convertAddressToLabel(tfResource.Address)
|
||||
|
||||
var agentNode *gographviz.Node
|
||||
for _, node := range graph.Nodes.Lookup {
|
||||
// The node attributes surround the label with quotes.
|
||||
if strings.Trim(node.Attrs["label"], `"`) != agentLabel {
|
||||
continue
|
||||
}
|
||||
agentNode = node
|
||||
break
|
||||
}
|
||||
if agentNode == nil {
|
||||
return nil, nil, xerrors.Errorf("couldn't find node on graph: %q", agentLabel)
|
||||
}
|
||||
|
||||
var agentResource *graphResource
|
||||
for _, resource := range findResourcesInGraph(graph, tfResourcesByLabel, agentNode.Name, 0, true) {
|
||||
if agentResource == nil {
|
||||
// Default to the first resource because we have nothing to compare!
|
||||
agentResource = resource
|
||||
continue
|
||||
}
|
||||
if resource.Depth < agentResource.Depth {
|
||||
// There's a closer resource!
|
||||
agentResource = resource
|
||||
continue
|
||||
}
|
||||
if resource.Depth == agentResource.Depth && resource.Label < agentResource.Label {
|
||||
agentResource = resource
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var agentResource *graphResource
|
||||
for _, resource := range findResourcesInGraph(graph, tfResourceByLabel, agentNode.Name, 0, true) {
|
||||
if agentResource == nil {
|
||||
// Default to the first resource because we have nothing to compare!
|
||||
agentResource = resource
|
||||
continue
|
||||
}
|
||||
if resource.Depth < agentResource.Depth {
|
||||
// There's a closer resource!
|
||||
agentResource = resource
|
||||
continue
|
||||
}
|
||||
if resource.Depth == agentResource.Depth && resource.Label < agentResource.Label {
|
||||
agentResource = resource
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if agentResource == nil {
|
||||
continue
|
||||
agents, exists := resourceAgents[agentResource.Label]
|
||||
if !exists {
|
||||
agents = make([]*proto.Agent, 0)
|
||||
}
|
||||
agents = append(agents, agent)
|
||||
resourceAgents[agentResource.Label] = agents
|
||||
}
|
||||
|
||||
agents, exists := resourceAgents[agentResource.Label]
|
||||
if !exists {
|
||||
agents = make([]*proto.Agent, 0)
|
||||
}
|
||||
agents = append(agents, agent)
|
||||
resourceAgents[agentResource.Label] = agents
|
||||
}
|
||||
|
||||
// Manually associate agents with instance IDs.
|
||||
for _, resource := range tfResourceByLabel {
|
||||
if resource.Type != "coder_agent_instance" {
|
||||
continue
|
||||
}
|
||||
agentIDRaw, valid := resource.AttributeValues["agent_id"]
|
||||
if !valid {
|
||||
continue
|
||||
}
|
||||
agentID, valid := agentIDRaw.(string)
|
||||
if !valid {
|
||||
continue
|
||||
}
|
||||
instanceIDRaw, valid := resource.AttributeValues["instance_id"]
|
||||
if !valid {
|
||||
continue
|
||||
}
|
||||
instanceID, valid := instanceIDRaw.(string)
|
||||
if !valid {
|
||||
continue
|
||||
}
|
||||
for _, resources := range tfResourcesByLabel {
|
||||
for _, resource := range resources {
|
||||
if resource.Type != "coder_agent_instance" {
|
||||
continue
|
||||
}
|
||||
agentIDRaw, valid := resource.AttributeValues["agent_id"]
|
||||
if !valid {
|
||||
continue
|
||||
}
|
||||
agentID, valid := agentIDRaw.(string)
|
||||
if !valid {
|
||||
continue
|
||||
}
|
||||
instanceIDRaw, valid := resource.AttributeValues["instance_id"]
|
||||
if !valid {
|
||||
continue
|
||||
}
|
||||
instanceID, valid := instanceIDRaw.(string)
|
||||
if !valid {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, agents := range resourceAgents {
|
||||
for _, agent := range agents {
|
||||
if agent.Id != agentID {
|
||||
continue
|
||||
for _, agents := range resourceAgents {
|
||||
for _, agent := range agents {
|
||||
if agent.Id != agentID {
|
||||
continue
|
||||
}
|
||||
// Only apply the instance ID if the agent authentication
|
||||
// type is set to do so. A user ran into a bug where they
|
||||
// had the instance ID block, but auth was set to "token". See:
|
||||
// https://github.com/coder/coder/issues/4551#issuecomment-1336293468
|
||||
switch t := agent.Auth.(type) {
|
||||
case *proto.Agent_Token:
|
||||
continue
|
||||
case *proto.Agent_InstanceId:
|
||||
t.InstanceId = instanceID
|
||||
}
|
||||
break
|
||||
}
|
||||
// Only apply the instance ID if the agent authentication
|
||||
// type is set to do so. A user ran into a bug where they
|
||||
// had the instance ID block, but auth was set to "token". See:
|
||||
// https://github.com/coder/coder/issues/4551#issuecomment-1336293468
|
||||
switch t := agent.Auth.(type) {
|
||||
case *proto.Agent_Token:
|
||||
continue
|
||||
case *proto.Agent_InstanceId:
|
||||
t.InstanceId = instanceID
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Associate Apps with agents.
|
||||
appSlugs := make(map[string]struct{})
|
||||
for _, resource := range tfResourceByLabel {
|
||||
if resource.Type != "coder_app" {
|
||||
continue
|
||||
}
|
||||
|
||||
var attrs agentAppAttributes
|
||||
err = mapstructure.Decode(resource.AttributeValues, &attrs)
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("decode app attributes: %w", err)
|
||||
}
|
||||
|
||||
// Default to the resource name if none is set!
|
||||
if attrs.Slug == "" {
|
||||
attrs.Slug = resource.Name
|
||||
}
|
||||
if attrs.DisplayName == "" {
|
||||
if attrs.Name != "" {
|
||||
// Name is deprecated but still accepted.
|
||||
attrs.DisplayName = attrs.Name
|
||||
} else {
|
||||
attrs.DisplayName = attrs.Slug
|
||||
for _, resources := range tfResourcesByLabel {
|
||||
for _, resource := range resources {
|
||||
if resource.Type != "coder_app" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if !provisioner.AppSlugRegex.MatchString(attrs.Slug) {
|
||||
return nil, nil, xerrors.Errorf("invalid app slug %q, please update your coder/coder provider to the latest version and specify the slug property on each coder_app", attrs.Slug)
|
||||
}
|
||||
|
||||
if _, exists := appSlugs[attrs.Slug]; exists {
|
||||
return nil, nil, xerrors.Errorf("duplicate app slug, they must be unique per template: %q", attrs.Slug)
|
||||
}
|
||||
appSlugs[attrs.Slug] = struct{}{}
|
||||
|
||||
var healthcheck *proto.Healthcheck
|
||||
if len(attrs.Healthcheck) != 0 {
|
||||
healthcheck = &proto.Healthcheck{
|
||||
Url: attrs.Healthcheck[0].URL,
|
||||
Interval: attrs.Healthcheck[0].Interval,
|
||||
Threshold: attrs.Healthcheck[0].Threshold,
|
||||
var attrs agentAppAttributes
|
||||
err = mapstructure.Decode(resource.AttributeValues, &attrs)
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("decode app attributes: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
sharingLevel := proto.AppSharingLevel_OWNER
|
||||
switch strings.ToLower(attrs.Share) {
|
||||
case "owner":
|
||||
sharingLevel = proto.AppSharingLevel_OWNER
|
||||
case "authenticated":
|
||||
sharingLevel = proto.AppSharingLevel_AUTHENTICATED
|
||||
case "public":
|
||||
sharingLevel = proto.AppSharingLevel_PUBLIC
|
||||
}
|
||||
|
||||
for _, agents := range resourceAgents {
|
||||
for _, agent := range agents {
|
||||
// Find agents with the matching ID and associate them!
|
||||
if agent.Id != attrs.AgentID {
|
||||
continue
|
||||
// Default to the resource name if none is set!
|
||||
if attrs.Slug == "" {
|
||||
attrs.Slug = resource.Name
|
||||
}
|
||||
if attrs.DisplayName == "" {
|
||||
if attrs.Name != "" {
|
||||
// Name is deprecated but still accepted.
|
||||
attrs.DisplayName = attrs.Name
|
||||
} else {
|
||||
attrs.DisplayName = attrs.Slug
|
||||
}
|
||||
}
|
||||
|
||||
if !provisioner.AppSlugRegex.MatchString(attrs.Slug) {
|
||||
return nil, nil, xerrors.Errorf("invalid app slug %q, please update your coder/coder provider to the latest version and specify the slug property on each coder_app", attrs.Slug)
|
||||
}
|
||||
|
||||
if _, exists := appSlugs[attrs.Slug]; exists {
|
||||
return nil, nil, xerrors.Errorf("duplicate app slug, they must be unique per template: %q", attrs.Slug)
|
||||
}
|
||||
appSlugs[attrs.Slug] = struct{}{}
|
||||
|
||||
var healthcheck *proto.Healthcheck
|
||||
if len(attrs.Healthcheck) != 0 {
|
||||
healthcheck = &proto.Healthcheck{
|
||||
Url: attrs.Healthcheck[0].URL,
|
||||
Interval: attrs.Healthcheck[0].Interval,
|
||||
Threshold: attrs.Healthcheck[0].Threshold,
|
||||
}
|
||||
}
|
||||
|
||||
sharingLevel := proto.AppSharingLevel_OWNER
|
||||
switch strings.ToLower(attrs.Share) {
|
||||
case "owner":
|
||||
sharingLevel = proto.AppSharingLevel_OWNER
|
||||
case "authenticated":
|
||||
sharingLevel = proto.AppSharingLevel_AUTHENTICATED
|
||||
case "public":
|
||||
sharingLevel = proto.AppSharingLevel_PUBLIC
|
||||
}
|
||||
|
||||
for _, agents := range resourceAgents {
|
||||
for _, agent := range agents {
|
||||
// Find agents with the matching ID and associate them!
|
||||
if agent.Id != attrs.AgentID {
|
||||
continue
|
||||
}
|
||||
agent.Apps = append(agent.Apps, &proto.App{
|
||||
Slug: attrs.Slug,
|
||||
DisplayName: attrs.DisplayName,
|
||||
Command: attrs.Command,
|
||||
External: attrs.External,
|
||||
Url: attrs.URL,
|
||||
Icon: attrs.Icon,
|
||||
Subdomain: attrs.Subdomain,
|
||||
SharingLevel: sharingLevel,
|
||||
Healthcheck: healthcheck,
|
||||
})
|
||||
}
|
||||
agent.Apps = append(agent.Apps, &proto.App{
|
||||
Slug: attrs.Slug,
|
||||
DisplayName: attrs.DisplayName,
|
||||
Command: attrs.Command,
|
||||
External: attrs.External,
|
||||
Url: attrs.URL,
|
||||
Icon: attrs.Icon,
|
||||
Subdomain: attrs.Subdomain,
|
||||
SharingLevel: sharingLevel,
|
||||
Healthcheck: healthcheck,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -324,130 +332,136 @@ func ConvertResourcesAndParameters(modules []*tfjson.StateModule, rawGraph strin
|
|||
resourceIcon := map[string]string{}
|
||||
resourceCost := map[string]int32{}
|
||||
|
||||
for _, resource := range tfResourceByLabel {
|
||||
if resource.Type != "coder_metadata" {
|
||||
continue
|
||||
}
|
||||
|
||||
var attrs metadataAttributes
|
||||
err = mapstructure.Decode(resource.AttributeValues, &attrs)
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("decode metadata attributes: %w", err)
|
||||
}
|
||||
|
||||
resourceLabel := convertAddressToLabel(resource.Address)
|
||||
|
||||
var attachedNode *gographviz.Node
|
||||
for _, node := range graph.Nodes.Lookup {
|
||||
// The node attributes surround the label with quotes.
|
||||
if strings.Trim(node.Attrs["label"], `"`) != resourceLabel {
|
||||
for _, resources := range tfResourcesByLabel {
|
||||
for _, resource := range resources {
|
||||
if resource.Type != "coder_metadata" {
|
||||
continue
|
||||
}
|
||||
attachedNode = node
|
||||
break
|
||||
}
|
||||
if attachedNode == nil {
|
||||
continue
|
||||
}
|
||||
var attachedResource *graphResource
|
||||
for _, resource := range findResourcesInGraph(graph, tfResourceByLabel, attachedNode.Name, 0, false) {
|
||||
|
||||
var attrs metadataAttributes
|
||||
err = mapstructure.Decode(resource.AttributeValues, &attrs)
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("decode metadata attributes: %w", err)
|
||||
}
|
||||
|
||||
resourceLabel := convertAddressToLabel(resource.Address)
|
||||
|
||||
var attachedNode *gographviz.Node
|
||||
for _, node := range graph.Nodes.Lookup {
|
||||
// The node attributes surround the label with quotes.
|
||||
if strings.Trim(node.Attrs["label"], `"`) != resourceLabel {
|
||||
continue
|
||||
}
|
||||
attachedNode = node
|
||||
break
|
||||
}
|
||||
if attachedNode == nil {
|
||||
continue
|
||||
}
|
||||
var attachedResource *graphResource
|
||||
for _, resource := range findResourcesInGraph(graph, tfResourcesByLabel, attachedNode.Name, 0, false) {
|
||||
if attachedResource == nil {
|
||||
// Default to the first resource because we have nothing to compare!
|
||||
attachedResource = resource
|
||||
continue
|
||||
}
|
||||
if resource.Depth < attachedResource.Depth {
|
||||
// There's a closer resource!
|
||||
attachedResource = resource
|
||||
continue
|
||||
}
|
||||
if resource.Depth == attachedResource.Depth && resource.Label < attachedResource.Label {
|
||||
attachedResource = resource
|
||||
continue
|
||||
}
|
||||
}
|
||||
if attachedResource == nil {
|
||||
// Default to the first resource because we have nothing to compare!
|
||||
attachedResource = resource
|
||||
continue
|
||||
}
|
||||
if resource.Depth < attachedResource.Depth {
|
||||
// There's a closer resource!
|
||||
attachedResource = resource
|
||||
continue
|
||||
}
|
||||
if resource.Depth == attachedResource.Depth && resource.Label < attachedResource.Label {
|
||||
attachedResource = resource
|
||||
continue
|
||||
}
|
||||
}
|
||||
if attachedResource == nil {
|
||||
continue
|
||||
}
|
||||
targetLabel := attachedResource.Label
|
||||
targetLabel := attachedResource.Label
|
||||
|
||||
resourceHidden[targetLabel] = attrs.Hide
|
||||
resourceIcon[targetLabel] = attrs.Icon
|
||||
resourceCost[targetLabel] = attrs.DailyCost
|
||||
for _, item := range attrs.Items {
|
||||
resourceMetadata[targetLabel] = append(resourceMetadata[targetLabel],
|
||||
&proto.Resource_Metadata{
|
||||
Key: item.Key,
|
||||
Value: item.Value,
|
||||
Sensitive: item.Sensitive,
|
||||
IsNull: item.IsNull,
|
||||
})
|
||||
resourceHidden[targetLabel] = attrs.Hide
|
||||
resourceIcon[targetLabel] = attrs.Icon
|
||||
resourceCost[targetLabel] = attrs.DailyCost
|
||||
for _, item := range attrs.Items {
|
||||
resourceMetadata[targetLabel] = append(resourceMetadata[targetLabel],
|
||||
&proto.Resource_Metadata{
|
||||
Key: item.Key,
|
||||
Value: item.Value,
|
||||
Sensitive: item.Sensitive,
|
||||
IsNull: item.IsNull,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, resource := range tfResourceByLabel {
|
||||
if resource.Mode == tfjson.DataResourceMode {
|
||||
continue
|
||||
}
|
||||
if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" || resource.Type == "coder_app" || resource.Type == "coder_metadata" {
|
||||
continue
|
||||
}
|
||||
label := convertAddressToLabel(resource.Address)
|
||||
for _, tfResources := range tfResourcesByLabel {
|
||||
for _, resource := range tfResources {
|
||||
if resource.Mode == tfjson.DataResourceMode {
|
||||
continue
|
||||
}
|
||||
if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" || resource.Type == "coder_app" || resource.Type == "coder_metadata" {
|
||||
continue
|
||||
}
|
||||
label := convertAddressToLabel(resource.Address)
|
||||
|
||||
agents, exists := resourceAgents[label]
|
||||
if exists {
|
||||
applyAutomaticInstanceID(resource, agents)
|
||||
}
|
||||
agents, exists := resourceAgents[label]
|
||||
if exists {
|
||||
applyAutomaticInstanceID(resource, agents)
|
||||
}
|
||||
|
||||
resources = append(resources, &proto.Resource{
|
||||
Name: resource.Name,
|
||||
Type: resource.Type,
|
||||
Agents: agents,
|
||||
Metadata: resourceMetadata[label],
|
||||
Hide: resourceHidden[label],
|
||||
Icon: resourceIcon[label],
|
||||
DailyCost: resourceCost[label],
|
||||
InstanceType: applyInstanceType(resource),
|
||||
})
|
||||
resources = append(resources, &proto.Resource{
|
||||
Name: resource.Name,
|
||||
Type: resource.Type,
|
||||
Agents: agents,
|
||||
Metadata: resourceMetadata[label],
|
||||
Hide: resourceHidden[label],
|
||||
Icon: resourceIcon[label],
|
||||
DailyCost: resourceCost[label],
|
||||
InstanceType: applyInstanceType(resource),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
parameters := make([]*proto.RichParameter, 0)
|
||||
for _, resource := range tfResourceByLabel {
|
||||
if resource.Type != "coder_parameter" {
|
||||
continue
|
||||
}
|
||||
var param provider.Parameter
|
||||
err = mapstructure.Decode(resource.AttributeValues, ¶m)
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("decode map values for coder_parameter.%s: %w", resource.Name, err)
|
||||
}
|
||||
protoParam := &proto.RichParameter{
|
||||
Name: param.Name,
|
||||
Description: param.Description,
|
||||
Type: param.Type,
|
||||
Mutable: param.Mutable,
|
||||
DefaultValue: param.Default,
|
||||
Icon: param.Icon,
|
||||
}
|
||||
if len(param.Validation) == 1 {
|
||||
protoParam.ValidationRegex = param.Validation[0].Regex
|
||||
protoParam.ValidationError = param.Validation[0].Error
|
||||
protoParam.ValidationMax = int32(param.Validation[0].Max)
|
||||
protoParam.ValidationMin = int32(param.Validation[0].Min)
|
||||
protoParam.ValidationMonotonic = param.Validation[0].Monotonic
|
||||
}
|
||||
if len(param.Option) > 0 {
|
||||
protoParam.Options = make([]*proto.RichParameterOption, 0, len(param.Option))
|
||||
for _, option := range param.Option {
|
||||
protoParam.Options = append(protoParam.Options, &proto.RichParameterOption{
|
||||
Name: option.Name,
|
||||
Description: option.Description,
|
||||
Value: option.Value,
|
||||
Icon: option.Icon,
|
||||
})
|
||||
for _, tfResources := range tfResourcesByLabel {
|
||||
for _, resource := range tfResources {
|
||||
if resource.Type != "coder_parameter" {
|
||||
continue
|
||||
}
|
||||
var param provider.Parameter
|
||||
err = mapstructure.Decode(resource.AttributeValues, ¶m)
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("decode map values for coder_parameter.%s: %w", resource.Name, err)
|
||||
}
|
||||
protoParam := &proto.RichParameter{
|
||||
Name: param.Name,
|
||||
Description: param.Description,
|
||||
Type: param.Type,
|
||||
Mutable: param.Mutable,
|
||||
DefaultValue: param.Default,
|
||||
Icon: param.Icon,
|
||||
}
|
||||
if len(param.Validation) == 1 {
|
||||
protoParam.ValidationRegex = param.Validation[0].Regex
|
||||
protoParam.ValidationError = param.Validation[0].Error
|
||||
protoParam.ValidationMax = int32(param.Validation[0].Max)
|
||||
protoParam.ValidationMin = int32(param.Validation[0].Min)
|
||||
protoParam.ValidationMonotonic = param.Validation[0].Monotonic
|
||||
}
|
||||
if len(param.Option) > 0 {
|
||||
protoParam.Options = make([]*proto.RichParameterOption, 0, len(param.Option))
|
||||
for _, option := range param.Option {
|
||||
protoParam.Options = append(protoParam.Options, &proto.RichParameterOption{
|
||||
Name: option.Name,
|
||||
Description: option.Description,
|
||||
Value: option.Value,
|
||||
Icon: option.Icon,
|
||||
})
|
||||
}
|
||||
}
|
||||
parameters = append(parameters, protoParam)
|
||||
}
|
||||
parameters = append(parameters, protoParam)
|
||||
}
|
||||
|
||||
return resources, parameters, nil
|
||||
|
@ -536,7 +550,7 @@ func applyAutomaticInstanceID(resource *tfjson.StateResource, agents []*proto.Ag
|
|||
// findResourcesInGraph traverses directionally in a graph until a resource is found,
|
||||
// then it stores the depth it was found at, and continues working up the tree.
|
||||
// nolint:revive
|
||||
func findResourcesInGraph(graph *gographviz.Graph, tfResourceByLabel map[string]*tfjson.StateResource, nodeName string, currentDepth uint, up bool) []*graphResource {
|
||||
func findResourcesInGraph(graph *gographviz.Graph, tfResourcesByLabel map[string][]*tfjson.StateResource, nodeName string, currentDepth uint, up bool) []*graphResource {
|
||||
graphResources := make([]*graphResource, 0)
|
||||
mapping := graph.Edges.DstToSrcs
|
||||
if !up {
|
||||
|
@ -545,29 +559,31 @@ func findResourcesInGraph(graph *gographviz.Graph, tfResourceByLabel map[string]
|
|||
for destination := range mapping[nodeName] {
|
||||
destinationNode := graph.Nodes.Lookup[destination]
|
||||
// Work our way up the tree!
|
||||
graphResources = append(graphResources, findResourcesInGraph(graph, tfResourceByLabel, destinationNode.Name, currentDepth+1, up)...)
|
||||
graphResources = append(graphResources, findResourcesInGraph(graph, tfResourcesByLabel, destinationNode.Name, currentDepth+1, up)...)
|
||||
|
||||
destinationLabel, exists := destinationNode.Attrs["label"]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
destinationLabel = strings.Trim(destinationLabel, `"`)
|
||||
resource, exists := tfResourceByLabel[destinationLabel]
|
||||
resources, exists := tfResourcesByLabel[destinationLabel]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
// Data sources cannot be associated with agents for now!
|
||||
if resource.Mode != tfjson.ManagedResourceMode {
|
||||
continue
|
||||
for _, resource := range resources {
|
||||
// Data sources cannot be associated with agents for now!
|
||||
if resource.Mode != tfjson.ManagedResourceMode {
|
||||
continue
|
||||
}
|
||||
// Don't associate Coder resources with other Coder resources!
|
||||
if strings.HasPrefix(resource.Type, "coder_") {
|
||||
continue
|
||||
}
|
||||
graphResources = append(graphResources, &graphResource{
|
||||
Label: destinationLabel,
|
||||
Depth: currentDepth,
|
||||
})
|
||||
}
|
||||
// Don't associate Coder resources with other Coder resources!
|
||||
if strings.HasPrefix(resource.Type, "coder_") {
|
||||
continue
|
||||
}
|
||||
graphResources = append(graphResources, &graphResource{
|
||||
Label: destinationLabel,
|
||||
Depth: currentDepth,
|
||||
})
|
||||
}
|
||||
|
||||
return graphResources
|
||||
|
|
|
@ -170,6 +170,30 @@ func TestConvertResources(t *testing.T) {
|
|||
}},
|
||||
}},
|
||||
},
|
||||
"mapped-apps": {
|
||||
resources: []*proto.Resource{{
|
||||
Name: "dev",
|
||||
Type: "null_resource",
|
||||
Agents: []*proto.Agent{{
|
||||
Name: "dev",
|
||||
OperatingSystem: "linux",
|
||||
Architecture: "amd64",
|
||||
Apps: []*proto.App{
|
||||
{
|
||||
Slug: "app1",
|
||||
DisplayName: "app1",
|
||||
},
|
||||
{
|
||||
Slug: "app2",
|
||||
DisplayName: "app2",
|
||||
},
|
||||
},
|
||||
Auth: &proto.Agent_Token{},
|
||||
LoginBeforeReady: true,
|
||||
ConnectionTimeoutSeconds: 120,
|
||||
}},
|
||||
}},
|
||||
},
|
||||
// Tests fetching metadata about workspace resources.
|
||||
"resource-metadata": {
|
||||
resources: []*proto.Resource{{
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.6.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_agent" "dev" {
|
||||
os = "linux"
|
||||
arch = "amd64"
|
||||
}
|
||||
|
||||
locals {
|
||||
apps_map = {
|
||||
"app1" = {
|
||||
name = "app1"
|
||||
}
|
||||
"app2" = {
|
||||
name = "app2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_app" "apps" {
|
||||
for_each = local.apps_map
|
||||
|
||||
agent_id = coder_agent.dev.id
|
||||
slug = each.key
|
||||
display_name = each.value.name
|
||||
}
|
||||
|
||||
resource "null_resource" "dev" {
|
||||
depends_on = [
|
||||
coder_agent.dev
|
||||
]
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
digraph {
|
||||
compound = "true"
|
||||
newrank = "true"
|
||||
subgraph "root" {
|
||||
"[root] coder_agent.dev (expand)" [label = "coder_agent.dev", shape = "box"]
|
||||
"[root] coder_app.apps (expand)" [label = "coder_app.apps", shape = "box"]
|
||||
"[root] null_resource.dev (expand)" [label = "null_resource.dev", shape = "box"]
|
||||
"[root] provider[\"registry.terraform.io/coder/coder\"]" [label = "provider[\"registry.terraform.io/coder/coder\"]", shape = "diamond"]
|
||||
"[root] provider[\"registry.terraform.io/hashicorp/null\"]" [label = "provider[\"registry.terraform.io/hashicorp/null\"]", shape = "diamond"]
|
||||
"[root] coder_agent.dev (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
|
||||
"[root] coder_app.apps (expand)" -> "[root] coder_agent.dev (expand)"
|
||||
"[root] coder_app.apps (expand)" -> "[root] local.apps_map (expand)"
|
||||
"[root] null_resource.dev (expand)" -> "[root] coder_agent.dev (expand)"
|
||||
"[root] null_resource.dev (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]"
|
||||
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_app.apps (expand)"
|
||||
"[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.dev (expand)"
|
||||
"[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)"
|
||||
"[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)"
|
||||
}
|
||||
}
|
||||
|
298
provisioner/terraform/testdata/mapped-apps/mapped-apps.tfplan.json
generated
vendored
Normal file
298
provisioner/terraform/testdata/mapped-apps/mapped-apps.tfplan.json
generated
vendored
Normal file
|
@ -0,0 +1,298 @@
|
|||
{
|
||||
"format_version": "1.1",
|
||||
"terraform_version": "1.3.7",
|
||||
"planned_values": {
|
||||
"root_module": {
|
||||
"resources": [
|
||||
{
|
||||
"address": "coder_agent.dev",
|
||||
"mode": "managed",
|
||||
"type": "coder_agent",
|
||||
"name": "dev",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"schema_version": 0,
|
||||
"values": {
|
||||
"arch": "amd64",
|
||||
"auth": "token",
|
||||
"connection_timeout": 120,
|
||||
"dir": null,
|
||||
"env": null,
|
||||
"os": "linux",
|
||||
"startup_script": null,
|
||||
"troubleshooting_url": null
|
||||
},
|
||||
"sensitive_values": {}
|
||||
},
|
||||
{
|
||||
"address": "coder_app.apps[\"app1\"]",
|
||||
"mode": "managed",
|
||||
"type": "coder_app",
|
||||
"name": "apps",
|
||||
"index": "app1",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"schema_version": 0,
|
||||
"values": {
|
||||
"command": null,
|
||||
"display_name": "app1",
|
||||
"healthcheck": [],
|
||||
"icon": null,
|
||||
"name": null,
|
||||
"relative_path": null,
|
||||
"share": "owner",
|
||||
"slug": "app1",
|
||||
"subdomain": null,
|
||||
"url": null
|
||||
},
|
||||
"sensitive_values": {
|
||||
"healthcheck": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "coder_app.apps[\"app2\"]",
|
||||
"mode": "managed",
|
||||
"type": "coder_app",
|
||||
"name": "apps",
|
||||
"index": "app2",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"schema_version": 0,
|
||||
"values": {
|
||||
"command": null,
|
||||
"display_name": "app2",
|
||||
"healthcheck": [],
|
||||
"icon": null,
|
||||
"name": null,
|
||||
"relative_path": null,
|
||||
"share": "owner",
|
||||
"slug": "app2",
|
||||
"subdomain": null,
|
||||
"url": null
|
||||
},
|
||||
"sensitive_values": {
|
||||
"healthcheck": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "null_resource.dev",
|
||||
"mode": "managed",
|
||||
"type": "null_resource",
|
||||
"name": "dev",
|
||||
"provider_name": "registry.terraform.io/hashicorp/null",
|
||||
"schema_version": 0,
|
||||
"values": {
|
||||
"triggers": null
|
||||
},
|
||||
"sensitive_values": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"resource_changes": [
|
||||
{
|
||||
"address": "coder_agent.dev",
|
||||
"mode": "managed",
|
||||
"type": "coder_agent",
|
||||
"name": "dev",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"change": {
|
||||
"actions": [
|
||||
"create"
|
||||
],
|
||||
"before": null,
|
||||
"after": {
|
||||
"arch": "amd64",
|
||||
"auth": "token",
|
||||
"connection_timeout": 120,
|
||||
"dir": null,
|
||||
"env": null,
|
||||
"os": "linux",
|
||||
"startup_script": null,
|
||||
"troubleshooting_url": null
|
||||
},
|
||||
"after_unknown": {
|
||||
"id": true,
|
||||
"init_script": true,
|
||||
"token": true
|
||||
},
|
||||
"before_sensitive": false,
|
||||
"after_sensitive": {
|
||||
"token": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "coder_app.apps[\"app1\"]",
|
||||
"mode": "managed",
|
||||
"type": "coder_app",
|
||||
"name": "apps",
|
||||
"index": "app1",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"change": {
|
||||
"actions": [
|
||||
"create"
|
||||
],
|
||||
"before": null,
|
||||
"after": {
|
||||
"command": null,
|
||||
"display_name": "app1",
|
||||
"healthcheck": [],
|
||||
"icon": null,
|
||||
"name": null,
|
||||
"relative_path": null,
|
||||
"share": "owner",
|
||||
"slug": "app1",
|
||||
"subdomain": null,
|
||||
"url": null
|
||||
},
|
||||
"after_unknown": {
|
||||
"agent_id": true,
|
||||
"healthcheck": [],
|
||||
"id": true
|
||||
},
|
||||
"before_sensitive": false,
|
||||
"after_sensitive": {
|
||||
"healthcheck": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "coder_app.apps[\"app2\"]",
|
||||
"mode": "managed",
|
||||
"type": "coder_app",
|
||||
"name": "apps",
|
||||
"index": "app2",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"change": {
|
||||
"actions": [
|
||||
"create"
|
||||
],
|
||||
"before": null,
|
||||
"after": {
|
||||
"command": null,
|
||||
"display_name": "app2",
|
||||
"healthcheck": [],
|
||||
"icon": null,
|
||||
"name": null,
|
||||
"relative_path": null,
|
||||
"share": "owner",
|
||||
"slug": "app2",
|
||||
"subdomain": null,
|
||||
"url": null
|
||||
},
|
||||
"after_unknown": {
|
||||
"agent_id": true,
|
||||
"healthcheck": [],
|
||||
"id": true
|
||||
},
|
||||
"before_sensitive": false,
|
||||
"after_sensitive": {
|
||||
"healthcheck": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "null_resource.dev",
|
||||
"mode": "managed",
|
||||
"type": "null_resource",
|
||||
"name": "dev",
|
||||
"provider_name": "registry.terraform.io/hashicorp/null",
|
||||
"change": {
|
||||
"actions": [
|
||||
"create"
|
||||
],
|
||||
"before": null,
|
||||
"after": {
|
||||
"triggers": null
|
||||
},
|
||||
"after_unknown": {
|
||||
"id": true
|
||||
},
|
||||
"before_sensitive": false,
|
||||
"after_sensitive": {}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"provider_config": {
|
||||
"coder": {
|
||||
"name": "coder",
|
||||
"full_name": "registry.terraform.io/coder/coder",
|
||||
"version_constraint": "0.6.1"
|
||||
},
|
||||
"null": {
|
||||
"name": "null",
|
||||
"full_name": "registry.terraform.io/hashicorp/null"
|
||||
}
|
||||
},
|
||||
"root_module": {
|
||||
"resources": [
|
||||
{
|
||||
"address": "coder_agent.dev",
|
||||
"mode": "managed",
|
||||
"type": "coder_agent",
|
||||
"name": "dev",
|
||||
"provider_config_key": "coder",
|
||||
"expressions": {
|
||||
"arch": {
|
||||
"constant_value": "amd64"
|
||||
},
|
||||
"os": {
|
||||
"constant_value": "linux"
|
||||
}
|
||||
},
|
||||
"schema_version": 0
|
||||
},
|
||||
{
|
||||
"address": "coder_app.apps",
|
||||
"mode": "managed",
|
||||
"type": "coder_app",
|
||||
"name": "apps",
|
||||
"provider_config_key": "coder",
|
||||
"expressions": {
|
||||
"agent_id": {
|
||||
"references": [
|
||||
"coder_agent.dev.id",
|
||||
"coder_agent.dev"
|
||||
]
|
||||
},
|
||||
"display_name": {
|
||||
"references": [
|
||||
"each.value.name",
|
||||
"each.value"
|
||||
]
|
||||
},
|
||||
"slug": {
|
||||
"references": [
|
||||
"each.key"
|
||||
]
|
||||
}
|
||||
},
|
||||
"schema_version": 0,
|
||||
"for_each_expression": {
|
||||
"references": [
|
||||
"local.apps_map"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "null_resource.dev",
|
||||
"mode": "managed",
|
||||
"type": "null_resource",
|
||||
"name": "dev",
|
||||
"provider_config_key": "null",
|
||||
"schema_version": 0,
|
||||
"depends_on": [
|
||||
"coder_agent.dev"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"relevant_attributes": [
|
||||
{
|
||||
"resource": "coder_agent.dev",
|
||||
"attribute": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
21
provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.dot
generated
vendored
Normal file
21
provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.dot
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
digraph {
|
||||
compound = "true"
|
||||
newrank = "true"
|
||||
subgraph "root" {
|
||||
"[root] coder_agent.dev (expand)" [label = "coder_agent.dev", shape = "box"]
|
||||
"[root] coder_app.apps (expand)" [label = "coder_app.apps", shape = "box"]
|
||||
"[root] null_resource.dev (expand)" [label = "null_resource.dev", shape = "box"]
|
||||
"[root] provider[\"registry.terraform.io/coder/coder\"]" [label = "provider[\"registry.terraform.io/coder/coder\"]", shape = "diamond"]
|
||||
"[root] provider[\"registry.terraform.io/hashicorp/null\"]" [label = "provider[\"registry.terraform.io/hashicorp/null\"]", shape = "diamond"]
|
||||
"[root] coder_agent.dev (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
|
||||
"[root] coder_app.apps (expand)" -> "[root] coder_agent.dev (expand)"
|
||||
"[root] coder_app.apps (expand)" -> "[root] local.apps_map (expand)"
|
||||
"[root] null_resource.dev (expand)" -> "[root] coder_agent.dev (expand)"
|
||||
"[root] null_resource.dev (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]"
|
||||
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_app.apps (expand)"
|
||||
"[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.dev (expand)"
|
||||
"[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)"
|
||||
"[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)"
|
||||
}
|
||||
}
|
||||
|
106
provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.json
generated
vendored
Normal file
106
provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.json
generated
vendored
Normal file
|
@ -0,0 +1,106 @@
|
|||
{
|
||||
"format_version": "1.0",
|
||||
"terraform_version": "1.3.7",
|
||||
"values": {
|
||||
"root_module": {
|
||||
"resources": [
|
||||
{
|
||||
"address": "coder_agent.dev",
|
||||
"mode": "managed",
|
||||
"type": "coder_agent",
|
||||
"name": "dev",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"schema_version": 0,
|
||||
"values": {
|
||||
"arch": "amd64",
|
||||
"auth": "token",
|
||||
"connection_timeout": 120,
|
||||
"dir": null,
|
||||
"env": null,
|
||||
"id": "9607dca1-bb3e-4606-849c-69d073b3b7d1",
|
||||
"init_script": "",
|
||||
"os": "linux",
|
||||
"startup_script": null,
|
||||
"token": "47b62747-82ad-4792-960a-e120a88d2bac",
|
||||
"troubleshooting_url": null
|
||||
},
|
||||
"sensitive_values": {}
|
||||
},
|
||||
{
|
||||
"address": "coder_app.apps[\"app1\"]",
|
||||
"mode": "managed",
|
||||
"type": "coder_app",
|
||||
"name": "apps",
|
||||
"index": "app1",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"schema_version": 0,
|
||||
"values": {
|
||||
"agent_id": "9607dca1-bb3e-4606-849c-69d073b3b7d1",
|
||||
"command": null,
|
||||
"display_name": "app1",
|
||||
"healthcheck": [],
|
||||
"icon": null,
|
||||
"id": "75b7232a-320f-462f-91c0-5542e655dce9",
|
||||
"name": null,
|
||||
"relative_path": null,
|
||||
"share": "owner",
|
||||
"slug": "app1",
|
||||
"subdomain": null,
|
||||
"url": null
|
||||
},
|
||||
"sensitive_values": {
|
||||
"healthcheck": []
|
||||
},
|
||||
"depends_on": [
|
||||
"coder_agent.dev"
|
||||
]
|
||||
},
|
||||
{
|
||||
"address": "coder_app.apps[\"app2\"]",
|
||||
"mode": "managed",
|
||||
"type": "coder_app",
|
||||
"name": "apps",
|
||||
"index": "app2",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"schema_version": 0,
|
||||
"values": {
|
||||
"agent_id": "9607dca1-bb3e-4606-849c-69d073b3b7d1",
|
||||
"command": null,
|
||||
"display_name": "app2",
|
||||
"healthcheck": [],
|
||||
"icon": null,
|
||||
"id": "30e8a682-a440-48a8-847d-525baa05783f",
|
||||
"name": null,
|
||||
"relative_path": null,
|
||||
"share": "owner",
|
||||
"slug": "app2",
|
||||
"subdomain": null,
|
||||
"url": null
|
||||
},
|
||||
"sensitive_values": {
|
||||
"healthcheck": []
|
||||
},
|
||||
"depends_on": [
|
||||
"coder_agent.dev"
|
||||
]
|
||||
},
|
||||
{
|
||||
"address": "null_resource.dev",
|
||||
"mode": "managed",
|
||||
"type": "null_resource",
|
||||
"name": "dev",
|
||||
"provider_name": "registry.terraform.io/hashicorp/null",
|
||||
"schema_version": 0,
|
||||
"values": {
|
||||
"id": "6276937382685771643",
|
||||
"triggers": null
|
||||
},
|
||||
"sensitive_values": {},
|
||||
"depends_on": [
|
||||
"coder_agent.dev"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue