From d9c3b0736d9b0883c52b8e8dbab8ec37940774e9 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 16 Feb 2023 18:01:35 +0000 Subject: [PATCH] fix: allow mapped resources in our terraform provider Fixes #6234. --- provisioner/terraform/resources.go | 606 +++++++++--------- provisioner/terraform/resources_test.go | 24 + .../testdata/mapped-apps/mapped-apps.tf | 38 ++ .../mapped-apps/mapped-apps.tfplan.dot | 21 + .../mapped-apps/mapped-apps.tfplan.json | 298 +++++++++ .../mapped-apps/mapped-apps.tfstate.dot | 21 + .../mapped-apps/mapped-apps.tfstate.json | 106 +++ 7 files changed, 819 insertions(+), 295 deletions(-) create mode 100644 provisioner/terraform/testdata/mapped-apps/mapped-apps.tf create mode 100644 provisioner/terraform/testdata/mapped-apps/mapped-apps.tfplan.dot create mode 100644 provisioner/terraform/testdata/mapped-apps/mapped-apps.tfplan.json create mode 100644 provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.dot create mode 100644 provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.json diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index 25ea6e1503128..b3b23005e1a65 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -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) - } + for _, tfResources := range tfResourcesByLabel { + for _, tfResource := range tfResources { + 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{}{} + 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 - } + // 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{} - } + 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) + // 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 + 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 } - 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, tfResourceByLabel, agentNode.Name, 0, true) { - if agentResource == nil { - // Default to the first resource because we have nothing to compare! - agentResource = resource - continue + if agentNode == nil { + return nil, nil, xerrors.Errorf("couldn't find node on graph: %q", agentLabel) } - if resource.Depth < agentResource.Depth { - // There's a closer resource! - agentResource = resource - continue + + 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 + } } - if resource.Depth == agentResource.Depth && resource.Label < agentResource.Label { - agentResource = resource + + if agentResource == nil { continue } - } - if agentResource == nil { - continue - } - - agents, exists := resourceAgents[agentResource.Label] - if !exists { - agents = make([]*proto.Agent, 0) + agents, exists := resourceAgents[agentResource.Label] + if !exists { + agents = make([]*proto.Agent, 0) + } + agents = append(agents, agent) + resourceAgents[agentResource.Label] = agents } - 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) - } + for _, resources := range tfResourcesByLabel { + for _, resource := range resources { + if resource.Type != "coder_app" { + 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 + var attrs agentAppAttributes + err = mapstructure.Decode(resource.AttributeValues, &attrs) + if err != nil { + return nil, nil, xerrors.Errorf("decode app attributes: %w", err) } - } - 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) - } + // 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 _, 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{}{} + 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) + } - 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, + 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 - } + 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 + 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 - } + for _, resources := range tfResourcesByLabel { + for _, resource := range resources { + 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) - } + 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) + 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 { + 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 } - attachedNode = node - break - } - if attachedNode == nil { - continue - } - var attachedResource *graphResource - for _, resource := range findResourcesInGraph(graph, tfResourceByLabel, attachedNode.Name, 0, false) { + 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 + 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, + }) + } + } + } + + for _, tfResources := range tfResourcesByLabel { + for _, resource := range tfResources { + if resource.Mode == tfjson.DataResourceMode { continue } - if resource.Depth == attachedResource.Depth && resource.Label < attachedResource.Label { - attachedResource = resource + if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" || resource.Type == "coder_app" || resource.Type == "coder_metadata" { continue } - } - if attachedResource == nil { - continue - } - 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, - }) - } - } + label := convertAddressToLabel(resource.Address) - 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) + 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 - } - // Don't associate Coder resources with other Coder resources! - if strings.HasPrefix(resource.Type, "coder_") { - 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, + }) } - graphResources = append(graphResources, &graphResource{ - Label: destinationLabel, - Depth: currentDepth, - }) } return graphResources diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go index 36ca43811bc02..8a93302068bda 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -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{{ diff --git a/provisioner/terraform/testdata/mapped-apps/mapped-apps.tf b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tf new file mode 100644 index 0000000000000..6ed5f0d18276b --- /dev/null +++ b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tf @@ -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 + ] +} diff --git a/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfplan.dot b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfplan.dot new file mode 100644 index 0000000000000..a54bed2003cc0 --- /dev/null +++ b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfplan.dot @@ -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)" + } +} + diff --git a/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfplan.json b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfplan.json new file mode 100644 index 0000000000000..41e69f1cc351f --- /dev/null +++ b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfplan.json @@ -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" + ] + } + ] +} diff --git a/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.dot b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.dot new file mode 100644 index 0000000000000..a54bed2003cc0 --- /dev/null +++ b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.dot @@ -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)" + } +} + diff --git a/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.json b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.json new file mode 100644 index 0000000000000..fccec50dcac43 --- /dev/null +++ b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.json @@ -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" + ] + } + ] + } + } +}