diff --git a/.gitattributes b/.gitattributes index 13cf2256a019d..03d8ab8d02c77 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,7 @@ coderd/database/dump.sql linguist-generated=true peerbroker/proto/*.go linguist-generated=true provisionerd/proto/*.go linguist-generated=true provisionersdk/proto/*.go linguist-generated=true +*.tfplan.json linguist-generated=true +*.tfstate.json linguist-generated=true +*.tfstate.dot linguist-generated=true +*.tfplan.dot linguist-generated=true diff --git a/.vscode/settings.json b/.vscode/settings.json index a7c4e8086ffdc..61f0037e8e4b9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -38,6 +38,7 @@ "kirsle", "ldflags", "manifoldco", + "mapstructure", "mattn", "mitchellh", "moby", @@ -67,9 +68,11 @@ "tcpip", "TCSETS", "templateversions", + "testdata", "testid", "tfexec", "tfjson", + "tfplan", "tfstate", "trimprefix", "turnconn", diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index 61e426467dc37..ec42f4b28cead 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -13,10 +13,8 @@ import ( "runtime" "strings" - "github.com/awalterschulze/gographviz" "github.com/hashicorp/terraform-exec/tfexec" tfjson "github.com/hashicorp/terraform-json" - "github.com/mitchellh/mapstructure" "golang.org/x/xerrors" "github.com/coder/coder/provisionersdk" @@ -25,7 +23,7 @@ import ( var ( // noStateRegex is matched against the output from `terraform state show` - noStateRegex = regexp.MustCompile(`(?i)State read error.*no state`) + noStateRegex = regexp.MustCompile(`no state`) ) // Provision executes `terraform apply`. @@ -202,7 +200,7 @@ func (t *terraform) Provision(stream proto.DRPCProvisioner_ProvisionStream) erro // contingency, in the future we will try harder to prevent workspaces being // broken this hard. if start.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY { - _, err := getTerraformState(shutdown, terraform, statefilePath) + _, err := pullTerraformState(shutdown, terraform, statefilePath) if xerrors.Is(err, os.ErrNotExist) { _ = stream.Send(&proto.Provision_Response{ Type: &proto.Provision_Response_Log{ @@ -325,86 +323,9 @@ func parseTerraformPlan(ctx context.Context, terraform *tfexec.Terraform, planfi if err != nil { return nil, xerrors.Errorf("graph: %w", err) } - resourceDependencies, err := findDirectDependencies(rawGraph) + resources, err := ConvertResources(plan.PlannedValues.RootModule, rawGraph) if err != nil { - return nil, xerrors.Errorf("find dependencies: %w", err) - } - - resources := make([]*proto.Resource, 0) - agents := map[string]*proto.Agent{} - - tfResources := make([]*tfjson.ConfigResource, 0) - var appendResources func(mod *tfjson.ConfigModule) - appendResources = func(mod *tfjson.ConfigModule) { - for _, module := range mod.ModuleCalls { - appendResources(module.Module) - } - tfResources = append(tfResources, mod.Resources...) - } - appendResources(plan.Config.RootModule) - - // Store all agents inside the maps! - for _, resource := range tfResources { - if resource.Type != "coder_agent" { - continue - } - agent := &proto.Agent{ - Name: resource.Name, - Auth: &proto.Agent_Token{}, - } - if operatingSystemRaw, has := resource.Expressions["os"]; has { - operatingSystem, ok := operatingSystemRaw.ConstantValue.(string) - if ok { - agent.OperatingSystem = operatingSystem - } - } - if archRaw, has := resource.Expressions["arch"]; has { - arch, ok := archRaw.ConstantValue.(string) - if ok { - agent.Architecture = arch - } - } - if envRaw, has := resource.Expressions["env"]; has { - env, ok := envRaw.ConstantValue.(map[string]interface{}) - if ok { - agent.Env = map[string]string{} - for key, valueRaw := range env { - value, valid := valueRaw.(string) - if !valid { - continue - } - agent.Env[key] = value - } - } - } - if startupScriptRaw, has := resource.Expressions["startup_script"]; has { - startupScript, ok := startupScriptRaw.ConstantValue.(string) - if ok { - agent.StartupScript = startupScript - } - } - if directoryRaw, has := resource.Expressions["dir"]; has { - dir, ok := directoryRaw.ConstantValue.(string) - if ok { - agent.Directory = dir - } - } - - agents[convertAddressToLabel(resource.Address)] = agent - } - - for _, resource := range tfResources { - if resource.Mode == tfjson.DataResourceMode { - continue - } - if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" || resource.Type == "coder_app" { - continue - } - resources = append(resources, &proto.Resource{ - Name: resource.Name, - Type: resource.Type, - Agents: findAgents(resourceDependencies, agents, convertAddressToLabel(resource.Address)), - }) + return nil, err } return &proto.Provision_Response{ @@ -420,185 +341,19 @@ func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, state _, err := os.Stat(statefilePath) statefileExisted := err == nil - state, err := getTerraformState(ctx, terraform, statefilePath) + state, err := pullTerraformState(ctx, terraform, statefilePath) if err != nil { return nil, xerrors.Errorf("get terraform state: %w", err) } - - resources := make([]*proto.Resource, 0) + rawGraph, err := terraform.Graph(ctx) + if err != nil { + return nil, xerrors.Errorf("get terraform graph: %w", err) + } + var resources []*proto.Resource if state.Values != nil { - rawGraph, err := terraform.Graph(ctx) - if err != nil { - return nil, xerrors.Errorf("graph: %w", err) - } - resourceDependencies, err := findDirectDependencies(rawGraph) + resources, err = ConvertResources(state.Values.RootModule, rawGraph) if err != nil { - return nil, xerrors.Errorf("find dependencies: %w", err) - } - type agentAttributes struct { - Auth string `mapstructure:"auth"` - OperatingSystem string `mapstructure:"os"` - Architecture string `mapstructure:"arch"` - Directory string `mapstructure:"dir"` - ID string `mapstructure:"id"` - Token string `mapstructure:"token"` - Env map[string]string `mapstructure:"env"` - StartupScript string `mapstructure:"startup_script"` - } - agents := map[string]*proto.Agent{} - - tfResources := make([]*tfjson.StateResource, 0) - var appendResources func(resource *tfjson.StateModule) - appendResources = func(mod *tfjson.StateModule) { - for _, module := range mod.ChildModules { - appendResources(module) - } - tfResources = append(tfResources, mod.Resources...) - } - appendResources(state.Values.RootModule) - - // Store all agents inside the maps! - for _, resource := range tfResources { - if resource.Type != "coder_agent" { - continue - } - var attrs agentAttributes - err = mapstructure.Decode(resource.AttributeValues, &attrs) - if err != nil { - return nil, xerrors.Errorf("decode agent attributes: %w", err) - } - agent := &proto.Agent{ - Name: resource.Name, - Id: attrs.ID, - Env: attrs.Env, - StartupScript: attrs.StartupScript, - OperatingSystem: attrs.OperatingSystem, - Architecture: attrs.Architecture, - Directory: attrs.Directory, - } - switch attrs.Auth { - case "token": - agent.Auth = &proto.Agent_Token{ - Token: attrs.Token, - } - default: - agent.Auth = &proto.Agent_InstanceId{} - } - agents[convertAddressToLabel(resource.Address)] = agent - } - - // Manually associate agents with instance IDs. - for _, resource := range tfResources { - 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 _, agent := range agents { - if agent.Id != agentID { - continue - } - agent.Auth = &proto.Agent_InstanceId{ - InstanceId: instanceID, - } - break - } - } - - type appAttributes struct { - AgentID string `mapstructure:"agent_id"` - Name string `mapstructure:"name"` - Icon string `mapstructure:"icon"` - URL string `mapstructure:"url"` - Command string `mapstructure:"command"` - RelativePath bool `mapstructure:"relative_path"` - } - // Associate Apps with agents. - for _, resource := range state.Values.RootModule.Resources { - if resource.Type != "coder_app" { - continue - } - var attrs appAttributes - err = mapstructure.Decode(resource.AttributeValues, &attrs) - if err != nil { - return nil, xerrors.Errorf("decode app attributes: %w", err) - } - if attrs.Name == "" { - // Default to the resource name if none is set! - attrs.Name = resource.Name - } - for _, agent := range agents { - if agent.Id != attrs.AgentID { - continue - } - agent.Apps = append(agent.Apps, &proto.App{ - Name: attrs.Name, - Command: attrs.Command, - Url: attrs.URL, - Icon: attrs.Icon, - RelativePath: attrs.RelativePath, - }) - } - } - - for _, resource := range tfResources { - if resource.Mode == tfjson.DataResourceMode { - continue - } - if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" || resource.Type == "coder_app" { - continue - } - resourceAgents := findAgents(resourceDependencies, agents, convertAddressToLabel(resource.Address)) - for _, agent := range resourceAgents { - // Didn't use instance identity. - if agent.GetToken() != "" { - continue - } - - key, isValid := map[string]string{ - "google_compute_instance": "instance_id", - "aws_instance": "id", - "azurerm_linux_virtual_machine": "id", - "azurerm_windows_virtual_machine": "id", - }[resource.Type] - if !isValid { - // The resource type doesn't support - // automatically setting the instance ID. - continue - } - instanceIDRaw, valid := resource.AttributeValues[key] - if !valid { - continue - } - instanceID, valid := instanceIDRaw.(string) - if !valid { - continue - } - agent.Auth = &proto.Agent_InstanceId{ - InstanceId: instanceID, - } - } - - resources = append(resources, &proto.Resource{ - Name: resource.Name, - Type: resource.Type, - Agents: resourceAgents, - }) + return nil, err } } @@ -621,10 +376,10 @@ func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, state }, nil } -// getTerraformState pulls and merges any remote terraform state into the given +// pullTerraformState pulls and merges any remote terraform state into the given // path and reads the merged state. If there is no state, `os.ErrNotExist` will // be returned. -func getTerraformState(ctx context.Context, terraform *tfexec.Terraform, statefilePath string) (*tfjson.State, error) { +func pullTerraformState(ctx context.Context, terraform *tfexec.Terraform, statefilePath string) (*tfjson.State, error) { statefile, err := os.OpenFile(statefilePath, os.O_CREATE|os.O_RDWR, 0600) if err != nil { return nil, xerrors.Errorf("open statefile %q: %w", statefilePath, err) @@ -681,75 +436,3 @@ func convertTerraformLogLevel(logLevel string) (proto.LogLevel, error) { return proto.LogLevel(0), xerrors.Errorf("invalid log level %q", logLevel) } } - -// findDirectDependencies maps Terraform resources to their children nodes. -// This parses GraphViz output from Terraform which isn't ideal, but seems reliable. -func findDirectDependencies(rawGraph string) (map[string][]string, error) { - parsedGraph, err := gographviz.ParseString(rawGraph) - if err != nil { - return nil, xerrors.Errorf("parse graph: %w", err) - } - graph, err := gographviz.NewAnalysedGraph(parsedGraph) - if err != nil { - return nil, xerrors.Errorf("analyze graph: %w", err) - } - direct := map[string][]string{} - for _, node := range graph.Nodes.Nodes { - label, exists := node.Attrs["label"] - if !exists { - continue - } - label = strings.Trim(label, `"`) - direct[label] = findDependenciesWithLabels(graph, node.Name) - } - - return direct, nil -} - -// findDependenciesWithLabels recursively finds nodes with labels (resource and data nodes) -// to build a dependency tree. -func findDependenciesWithLabels(graph *gographviz.Graph, nodeName string) []string { - dependencies := make([]string, 0) - for destination := range graph.Edges.SrcToDsts[nodeName] { - dependencyNode, exists := graph.Nodes.Lookup[destination] - if !exists { - continue - } - label, exists := dependencyNode.Attrs["label"] - if !exists { - dependencies = append(dependencies, findDependenciesWithLabels(graph, dependencyNode.Name)...) - continue - } - label = strings.Trim(label, `"`) - dependencies = append(dependencies, label) - } - return dependencies -} - -// findAgents recursively searches through resource dependencies -// to find associated agents. Nested is required for indirect -// dependency matching. -func findAgents(resourceDependencies map[string][]string, agents map[string]*proto.Agent, resourceLabel string) []*proto.Agent { - resourceNode, exists := resourceDependencies[resourceLabel] - if !exists { - return []*proto.Agent{} - } - // Associate resources that depend on an agent. - resourceAgents := make([]*proto.Agent, 0) - for _, dep := range resourceNode { - var has bool - agent, has := agents[dep] - if !has { - resourceAgents = append(resourceAgents, findAgents(resourceDependencies, agents, dep)...) - continue - } - resourceAgents = append(resourceAgents, agent) - } - return resourceAgents -} - -// convertAddressToLabel returns the Terraform address without the count -// specifier. eg. "module.ec2_dev.ec2_instance.dev[0]" becomes "module.ec2_dev.ec2_instance.dev" -func convertAddressToLabel(address string) string { - return strings.Split(address, "[")[0] -} diff --git a/provisioner/terraform/provision_test.go b/provisioner/terraform/provision_test.go index 294a70d3deb8c..9e381c63dab6e 100644 --- a/provisioner/terraform/provision_test.go +++ b/provisioner/terraform/provision_test.go @@ -121,369 +121,6 @@ provider "coder" { "main.tf": `a`, }, Error: true, - }, { - Name: "dryrun-single-resource", - Files: map[string]string{ - "main.tf": `resource "null_resource" "A" {}`, - }, - Request: &proto.Provision_Request{ - Type: &proto.Provision_Request_Start{ - Start: &proto.Provision_Start{ - DryRun: true, - }, - }, - }, - Response: &proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "A", - Type: "null_resource", - }}, - }, - }, - }, - }, { - Name: "dryrun-conditional-single-resource", - Files: map[string]string{ - "main.tf": ` - variable "test" { - default = "no" - } - resource "null_resource" "A" { - count = var.test == "yes" ? 1 : 0 - }`, - }, - Response: &proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: nil, - }, - }, - }, - }, { - Name: "resource-associated-with-agent", - Files: map[string]string{ - "main.tf": provider + ` - resource "coder_agent" "A" { - os = "windows" - arch = "arm64" - dir = "C:\\System32" - } - resource "null_resource" "A" { - depends_on = [ - coder_agent.A - ] - }`, - }, - Request: &proto.Provision_Request{ - Type: &proto.Provision_Request_Start{ - Start: &proto.Provision_Start{ - Metadata: &proto.Provision_Metadata{}, - }, - }, - }, - Response: &proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "A", - Type: "null_resource", - Agents: []*proto.Agent{{ - Name: "A", - OperatingSystem: "windows", - Architecture: "arm64", - Directory: "C:\\System32", - Auth: &proto.Agent_Token{ - Token: "", - }, - }}, - }}, - }, - }, - }, - }, { - Name: "dryrun-resource-associated-with-agent", - Files: map[string]string{ - "main.tf": provider + ` - data "coder_workspace" "me" {} - resource "coder_agent" "A" { - count = 1 - os = "linux" - arch = "amd64" - env = { - test: "example" - } - startup_script = "code-server" - } - resource "null_resource" "A" { - depends_on = [ - coder_agent.A[0] - ] - count = data.coder_workspace.me.start_count - }`, - }, - Request: &proto.Provision_Request{ - Type: &proto.Provision_Request_Start{ - Start: &proto.Provision_Start{ - DryRun: true, - Metadata: &proto.Provision_Metadata{}, - }, - }, - }, - Response: &proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "A", - Type: "null_resource", - Agents: []*proto.Agent{{ - Name: "A", - OperatingSystem: "linux", - Architecture: "amd64", - Auth: &proto.Agent_Token{}, - Env: map[string]string{ - "test": "example", - }, - StartupScript: "code-server", - }}, - }}, - }, - }, - }, - }, { - Name: "resource-manually-associated-with-agent", - Files: map[string]string{ - "main.tf": provider + ` - resource "coder_agent" "A" { - os = "darwin" - arch = "amd64" - } - resource "null_resource" "A" { - depends_on = [ - coder_agent.A - ] - } - resource "coder_agent_instance" "A" { - agent_id = coder_agent.A.id - instance_id = "bananas" - } - `, - }, - Request: &proto.Provision_Request{ - Type: &proto.Provision_Request_Start{ - Start: &proto.Provision_Start{ - Metadata: &proto.Provision_Metadata{}, - }, - }, - }, - Response: &proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "A", - Type: "null_resource", - Agents: []*proto.Agent{{ - Name: "A", - OperatingSystem: "darwin", - Architecture: "amd64", - Auth: &proto.Agent_InstanceId{ - InstanceId: "bananas", - }, - }}, - }}, - }, - }, - }, - }, { - Name: "resource-manually-associated-with-multiple-agents", - Files: map[string]string{ - "main.tf": provider + ` - resource "coder_agent" "A" { - os = "darwin" - arch = "amd64" - } - resource "coder_agent" "B" { - os = "linux" - arch = "amd64" - } - resource "null_resource" "A" { - depends_on = [ - coder_agent.A, - coder_agent.B - ] - } - resource "coder_agent_instance" "A" { - agent_id = coder_agent.A.id - instance_id = "bananas" - } - `, - }, - Request: &proto.Provision_Request{ - Type: &proto.Provision_Request_Start{ - Start: &proto.Provision_Start{ - Metadata: &proto.Provision_Metadata{}, - }, - }, - }, - Response: &proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "A", - Type: "null_resource", - Agents: []*proto.Agent{{ - Name: "A", - OperatingSystem: "darwin", - Architecture: "amd64", - Auth: &proto.Agent_InstanceId{ - InstanceId: "bananas", - }, - }, { - Name: "B", - OperatingSystem: "linux", - Architecture: "amd64", - Auth: &proto.Agent_Token{ - Token: "", - }, - }}, - }}, - }, - }, - }, - }, { - Name: "dryrun-resource-separated-from-agent", - Files: map[string]string{ - "main.tf": provider + ` - resource "coder_agent" "A" { - os = "darwin" - arch = "amd64" - } - data "null_data_source" "values" { - inputs = { - script = coder_agent.A.init_script - } - } - resource "null_resource" "A" { - depends_on = [ - data.null_data_source.values - ] - } - `, - }, - Request: &proto.Provision_Request{ - Type: &proto.Provision_Request_Start{ - Start: &proto.Provision_Start{ - Metadata: &proto.Provision_Metadata{}, - DryRun: true, - }, - }, - }, - Response: &proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "A", - Type: "null_resource", - Agents: []*proto.Agent{{ - Name: "A", - OperatingSystem: "darwin", - Architecture: "amd64", - Auth: &proto.Agent_Token{}, - }}, - }}, - }, - }, - }, - }, { - Name: "resource-separated-from-agent", - Files: map[string]string{ - "main.tf": provider + ` - resource "coder_agent" "A" { - os = "darwin" - arch = "amd64" - } - data "null_data_source" "values" { - inputs = { - script = coder_agent.A.init_script - } - } - resource "null_resource" "A" { - depends_on = [ - data.null_data_source.values - ] - } - `, - }, - Request: &proto.Provision_Request{ - Type: &proto.Provision_Request_Start{ - Start: &proto.Provision_Start{ - Metadata: &proto.Provision_Metadata{}, - }, - }, - }, - Response: &proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "A", - Type: "null_resource", - Agents: []*proto.Agent{{ - Name: "A", - OperatingSystem: "darwin", - Architecture: "amd64", - Auth: &proto.Agent_Token{}, - }}, - }}, - }, - }, - }, - }, { - Name: "agent-with-app", - Files: map[string]string{ - "main.tf": provider + ` - resource "coder_agent" "A" { - os = "darwin" - arch = "amd64" - } - resource "null_resource" "A" { - depends_on = [ - coder_agent.A - ] - } - resource "coder_app" "A" { - agent_id = coder_agent.A.id - command = "vim" - } - `, - }, - Request: &proto.Provision_Request{ - Type: &proto.Provision_Request_Start{ - Start: &proto.Provision_Start{ - Metadata: &proto.Provision_Metadata{}, - }, - }, - }, - Response: &proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "A", - Type: "null_resource", - Agents: []*proto.Agent{{ - Name: "A", - OperatingSystem: "darwin", - Architecture: "amd64", - Auth: &proto.Agent_Token{}, - Apps: []*proto.App{{ - Name: "A", - Command: "vim", - }}, - }}, - }}, - }, - }, - }, }} { testCase := testCase t.Run(testCase.Name, func(t *testing.T) { diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go new file mode 100644 index 0000000000000..c6d1e99ab86f6 --- /dev/null +++ b/provisioner/terraform/resources.go @@ -0,0 +1,188 @@ +package terraform + +import ( + "strings" + + "github.com/awalterschulze/gographviz" + tfjson "github.com/hashicorp/terraform-json" + "github.com/mitchellh/mapstructure" + "golang.org/x/xerrors" + + "github.com/coder/coder/provisionersdk/proto" +) + +// ConvertResources consumes Terraform state and a GraphViz representation produced by +// `terraform graph` to produce resources consumable by Coder. +func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Resource, error) { + parsedGraph, err := gographviz.ParseString(rawGraph) + if err != nil { + return nil, xerrors.Errorf("parse graph: %w", err) + } + graph, err := gographviz.NewAnalysedGraph(parsedGraph) + if err != nil { + return nil, xerrors.Errorf("analyze graph: %w", err) + } + resourceDependencies := map[string][]string{} + for _, node := range graph.Nodes.Nodes { + label, exists := node.Attrs["label"] + if !exists { + continue + } + label = strings.Trim(label, `"`) + resourceDependencies[label] = findDependenciesWithLabels(graph, node.Name) + } + + resources := make([]*proto.Resource, 0) + agents := map[string]*proto.Agent{} + + tfResources := make([]*tfjson.StateResource, 0) + var appendResources func(mod *tfjson.StateModule) + appendResources = func(mod *tfjson.StateModule) { + for _, module := range mod.ChildModules { + appendResources(module) + } + tfResources = append(tfResources, mod.Resources...) + } + appendResources(module) + + type agentAttributes struct { + Auth string `mapstructure:"auth"` + OperatingSystem string `mapstructure:"os"` + Architecture string `mapstructure:"arch"` + Directory string `mapstructure:"dir"` + ID string `mapstructure:"id"` + Token string `mapstructure:"token"` + Env map[string]string `mapstructure:"env"` + StartupScript string `mapstructure:"startup_script"` + } + + // Store all agents inside the maps! + for _, resource := range tfResources { + if resource.Type != "coder_agent" { + continue + } + var attrs agentAttributes + err = mapstructure.Decode(resource.AttributeValues, &attrs) + if err != nil { + return nil, xerrors.Errorf("decode agent attributes: %w", err) + } + agent := &proto.Agent{ + Name: resource.Name, + Id: attrs.ID, + Env: attrs.Env, + StartupScript: attrs.StartupScript, + OperatingSystem: attrs.OperatingSystem, + Architecture: attrs.Architecture, + Directory: attrs.Directory, + } + switch attrs.Auth { + case "token": + agent.Auth = &proto.Agent_Token{ + Token: attrs.Token, + } + default: + agent.Auth = &proto.Agent_InstanceId{} + } + + agents[convertAddressToLabel(resource.Address)] = agent + } + + // Manually associate agents with instance IDs. + for _, resource := range tfResources { + 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 _, agent := range agents { + if agent.Id != agentID { + continue + } + agent.Auth = &proto.Agent_InstanceId{ + InstanceId: instanceID, + } + break + } + } + + for _, resource := range tfResources { + if resource.Mode == tfjson.DataResourceMode { + continue + } + if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" || resource.Type == "coder_app" { + continue + } + resources = append(resources, &proto.Resource{ + Name: resource.Name, + Type: resource.Type, + Agents: findAgents(resourceDependencies, agents, convertAddressToLabel(resource.Address)), + }) + } + + return resources, nil +} + +// convertAddressToLabel returns the Terraform address without the count +// specifier. eg. "module.ec2_dev.ec2_instance.dev[0]" becomes "module.ec2_dev.ec2_instance.dev" +func convertAddressToLabel(address string) string { + return strings.Split(address, "[")[0] +} + +// findAgents recursively searches through resource dependencies +// to find associated agents. Nested is required for indirect +// dependency matching. +func findAgents(resourceDependencies map[string][]string, agents map[string]*proto.Agent, resourceLabel string) []*proto.Agent { + resourceNode, exists := resourceDependencies[resourceLabel] + if !exists { + return []*proto.Agent{} + } + // Associate resources that depend on an agent. + resourceAgents := make([]*proto.Agent, 0) + for _, dep := range resourceNode { + var has bool + agent, has := agents[dep] + if !has { + resourceAgents = append(resourceAgents, findAgents(resourceDependencies, agents, dep)...) + continue + } + // An agent must be deleted after being assigned so it isn't referenced twice. + delete(agents, dep) + resourceAgents = append(resourceAgents, agent) + } + return resourceAgents +} + +// findDependenciesWithLabels recursively finds nodes with labels (resource and data nodes) +// to build a dependency tree. +func findDependenciesWithLabels(graph *gographviz.Graph, nodeName string) []string { + dependencies := make([]string, 0) + for destination := range graph.Edges.SrcToDsts[nodeName] { + dependencyNode, exists := graph.Nodes.Lookup[destination] + if !exists { + continue + } + label, exists := dependencyNode.Attrs["label"] + if !exists { + dependencies = append(dependencies, findDependenciesWithLabels(graph, dependencyNode.Name)...) + continue + } + label = strings.Trim(label, `"`) + dependencies = append(dependencies, label) + } + return dependencies +} diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go new file mode 100644 index 0000000000000..ded477a83208a --- /dev/null +++ b/provisioner/terraform/resources_test.go @@ -0,0 +1,142 @@ +package terraform_test + +import ( + "encoding/json" + "os" + "path/filepath" + "runtime" + "sort" + "testing" + + tfjson "github.com/hashicorp/terraform-json" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/provisioner/terraform" + "github.com/coder/coder/provisionersdk/proto" +) + +func TestConvertResources(t *testing.T) { + t.Parallel() + // nolint:dogsled + _, filename, _, _ := runtime.Caller(0) + // nolint:paralleltest + for folderName, expected := range map[string][]*proto.Resource{ + "chaining-resources": {{ + Name: "first", + Type: "null_resource", + Agents: []*proto.Agent{{ + Name: "dev1", + OperatingSystem: "linux", + Architecture: "amd64", + Auth: &proto.Agent_Token{}, + }}, + }, { + Name: "second", + Type: "null_resource", + }}, + "instance-id": {{ + Name: "dev", + Type: "null_resource", + Agents: []*proto.Agent{{ + Name: "dev", + OperatingSystem: "linux", + Architecture: "amd64", + Auth: &proto.Agent_InstanceId{}, + }}, + }}, + "calling-module": {{ + Name: "example", + Type: "null_resource", + Agents: []*proto.Agent{{ + Name: "dev", + OperatingSystem: "linux", + Architecture: "amd64", + Auth: &proto.Agent_Token{}, + }}, + }}, + "multiple-agents": {{ + Name: "dev", + Type: "null_resource", + Agents: []*proto.Agent{{ + Name: "dev1", + OperatingSystem: "linux", + Architecture: "amd64", + Auth: &proto.Agent_Token{}, + }, { + Name: "dev2", + OperatingSystem: "darwin", + Architecture: "amd64", + Auth: &proto.Agent_Token{}, + }, { + Name: "dev3", + OperatingSystem: "windows", + Architecture: "arm64", + Auth: &proto.Agent_Token{}, + }}, + }}, + } { + folderName := folderName + expected := expected + t.Run(folderName, func(t *testing.T) { + t.Parallel() + dir := filepath.Join(filepath.Dir(filename), "testdata", folderName) + t.Run("Plan", func(t *testing.T) { + t.Parallel() + + tfPlanRaw, err := os.ReadFile(filepath.Join(dir, folderName+".tfplan.json")) + require.NoError(t, err) + var tfPlan tfjson.Plan + err = json.Unmarshal(tfPlanRaw, &tfPlan) + require.NoError(t, err) + tfPlanGraph, err := os.ReadFile(filepath.Join(dir, folderName+".tfplan.dot")) + require.NoError(t, err) + + resources, err := terraform.ConvertResources(tfPlan.PlannedValues.RootModule, string(tfPlanGraph)) + require.NoError(t, err) + for _, resource := range resources { + sort.Slice(resource.Agents, func(i, j int) bool { + return resource.Agents[i].Name < resource.Agents[j].Name + }) + } + resourcesWant, err := json.Marshal(expected) + require.NoError(t, err) + resourcesGot, err := json.Marshal(resources) + require.NoError(t, err) + require.Equal(t, string(resourcesWant), string(resourcesGot)) + }) + t.Run("Provision", func(t *testing.T) { + t.Parallel() + tfStateRaw, err := os.ReadFile(filepath.Join(dir, folderName+".tfstate.json")) + require.NoError(t, err) + var tfState tfjson.State + err = json.Unmarshal(tfStateRaw, &tfState) + require.NoError(t, err) + tfStateGraph, err := os.ReadFile(filepath.Join(dir, folderName+".tfstate.dot")) + require.NoError(t, err) + + resources, err := terraform.ConvertResources(tfState.Values.RootModule, string(tfStateGraph)) + require.NoError(t, err) + for _, resource := range resources { + sort.Slice(resource.Agents, func(i, j int) bool { + return resource.Agents[i].Name < resource.Agents[j].Name + }) + for _, agent := range resource.Agents { + agent.Id = "" + if agent.GetToken() != "" { + agent.Auth = &proto.Agent_Token{} + } + if agent.GetInstanceId() != "" { + agent.Auth = &proto.Agent_InstanceId{} + } + } + } + resourcesWant, err := json.Marshal(expected) + require.NoError(t, err) + resourcesGot, err := json.Marshal(resources) + require.NoError(t, err) + + require.Equal(t, string(resourcesWant), string(resourcesGot)) + }) + }) + } +} diff --git a/provisioner/terraform/testdata/calling-module/calling-module.tfplan.dot b/provisioner/terraform/testdata/calling-module/calling-module.tfplan.dot new file mode 100644 index 0000000000000..0d5447f4bb153 --- /dev/null +++ b/provisioner/terraform/testdata/calling-module/calling-module.tfplan.dot @@ -0,0 +1,22 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] coder_agent.dev (expand)" [label = "coder_agent.dev", shape = "box"] + "[root] module.module.null_resource.example (expand)" [label = "module.module.null_resource.example", 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] module.module (close)" -> "[root] module.module.null_resource.example (expand)" + "[root] module.module.null_resource.example (expand)" -> "[root] module.module.var.script (expand)" + "[root] module.module.null_resource.example (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" + "[root] module.module.var.script (expand)" -> "[root] coder_agent.dev (expand)" + "[root] module.module.var.script (expand)" -> "[root] module.module (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_agent.dev (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] module.module.null_resource.example (expand)" + "[root] root" -> "[root] module.module (close)" + "[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/calling-module/calling-module.tfplan.json b/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json new file mode 100644 index 0000000000000..57b0ec2bbdaac --- /dev/null +++ b/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json @@ -0,0 +1,163 @@ +{ + "format_version": "1.1", + "terraform_version": "1.2.2", + "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", + "dir": null, + "env": null, + "os": "linux", + "startup_script": null + }, + "sensitive_values": {} + } + ], + "child_modules": [ + { + "resources": [ + { + "address": "module.module.null_resource.example", + "mode": "managed", + "type": "null_resource", + "name": "example", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "triggers": null + }, + "sensitive_values": {} + } + ], + "address": "module.module" + } + ] + } + }, + "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", + "dir": null, + "env": null, + "os": "linux", + "startup_script": null + }, + "after_unknown": { + "id": true, + "init_script": true, + "token": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + }, + { + "address": "module.module.null_resource.example", + "module_address": "module.module", + "mode": "managed", + "type": "null_resource", + "name": "example", + "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.4.2" + }, + "module.module:null": { + "name": "null", + "full_name": "registry.terraform.io/hashicorp/null", + "module_address": "module.module" + } + }, + "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 + } + ], + "module_calls": { + "module": { + "source": "./module", + "expressions": { + "script": { + "references": [ + "coder_agent.dev.init_script", + "coder_agent.dev" + ] + } + }, + "module": { + "resources": [ + { + "address": "null_resource.example", + "mode": "managed", + "type": "null_resource", + "name": "example", + "provider_config_key": "module.module:null", + "schema_version": 0, + "depends_on": [ + "var.script" + ] + } + ], + "variables": { + "script": {} + } + } + } + } + } + } +} diff --git a/provisioner/terraform/testdata/calling-module/calling-module.tfstate.dot b/provisioner/terraform/testdata/calling-module/calling-module.tfstate.dot new file mode 100644 index 0000000000000..0d5447f4bb153 --- /dev/null +++ b/provisioner/terraform/testdata/calling-module/calling-module.tfstate.dot @@ -0,0 +1,22 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] coder_agent.dev (expand)" [label = "coder_agent.dev", shape = "box"] + "[root] module.module.null_resource.example (expand)" [label = "module.module.null_resource.example", 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] module.module (close)" -> "[root] module.module.null_resource.example (expand)" + "[root] module.module.null_resource.example (expand)" -> "[root] module.module.var.script (expand)" + "[root] module.module.null_resource.example (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" + "[root] module.module.var.script (expand)" -> "[root] coder_agent.dev (expand)" + "[root] module.module.var.script (expand)" -> "[root] module.module (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_agent.dev (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] module.module.null_resource.example (expand)" + "[root] root" -> "[root] module.module (close)" + "[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/calling-module/calling-module.tfstate.json b/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json new file mode 100644 index 0000000000000..3f07070815e19 --- /dev/null +++ b/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json @@ -0,0 +1,53 @@ +{ + "format_version": "1.0", + "terraform_version": "1.2.2", + "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", + "dir": null, + "env": null, + "id": "2f83ed29-c32e-401c-9019-f6cd00ed2b31", + "init_script": "", + "os": "linux", + "startup_script": null, + "token": "e3cb764c-a792-4d9b-8962-b9218715beef" + }, + "sensitive_values": {} + } + ], + "child_modules": [ + { + "resources": [ + { + "address": "module.module.null_resource.example", + "mode": "managed", + "type": "null_resource", + "name": "example", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "id": "7454863731787788813", + "triggers": null + }, + "sensitive_values": {}, + "depends_on": [ + "coder_agent.dev" + ] + } + ], + "address": "module.module" + } + ] + } + } +} diff --git a/provisioner/terraform/testdata/calling-module/module/module.tf b/provisioner/terraform/testdata/calling-module/module/module.tf new file mode 100644 index 0000000000000..deb3f71f5a3cf --- /dev/null +++ b/provisioner/terraform/testdata/calling-module/module/module.tf @@ -0,0 +1,9 @@ +variable "script" { + type = string +} + +resource "null_resource" "example" { + depends_on = [ + var.script + ] +} diff --git a/provisioner/terraform/testdata/calling-module/with-module.tf b/provisioner/terraform/testdata/calling-module/with-module.tf new file mode 100644 index 0000000000000..9c1938de28e9e --- /dev/null +++ b/provisioner/terraform/testdata/calling-module/with-module.tf @@ -0,0 +1,18 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "0.4.2" + } + } +} + +resource "coder_agent" "dev" { + os = "linux" + arch = "amd64" +} + +module "module" { + source = "./module" + script = coder_agent.dev.init_script +} diff --git a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf new file mode 100644 index 0000000000000..cc5a43218beac --- /dev/null +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf @@ -0,0 +1,25 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "0.4.2" + } + } +} + +resource "coder_agent" "dev1" { + os = "linux" + arch = "amd64" +} + +resource "null_resource" "first" { + depends_on = [ + coder_agent.dev1 + ] +} + +resource "null_resource" "second" { + depends_on = [ + null_resource.first + ] +} diff --git a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.dot b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.dot new file mode 100644 index 0000000000000..b590846e3828e --- /dev/null +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.dot @@ -0,0 +1,20 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] coder_agent.dev1 (expand)" [label = "coder_agent.dev1", shape = "box"] + "[root] null_resource.first (expand)" [label = "null_resource.first", shape = "box"] + "[root] null_resource.second (expand)" [label = "null_resource.second", 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.dev1 (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] null_resource.first (expand)" -> "[root] coder_agent.dev1 (expand)" + "[root] null_resource.first (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" + "[root] null_resource.second (expand)" -> "[root] null_resource.first (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_agent.dev1 (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.second (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/chaining-resources/chaining-resources.tfplan.json b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json new file mode 100644 index 0000000000000..9e338c184cc9f --- /dev/null +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json @@ -0,0 +1,178 @@ +{ + "format_version": "1.1", + "terraform_version": "1.2.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "coder_agent.dev1", + "mode": "managed", + "type": "coder_agent", + "name": "dev1", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "arch": "amd64", + "auth": "token", + "dir": null, + "env": null, + "os": "linux", + "startup_script": null + }, + "sensitive_values": {} + }, + { + "address": "null_resource.first", + "mode": "managed", + "type": "null_resource", + "name": "first", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "triggers": null + }, + "sensitive_values": {} + }, + { + "address": "null_resource.second", + "mode": "managed", + "type": "null_resource", + "name": "second", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "triggers": null + }, + "sensitive_values": {} + } + ] + } + }, + "resource_changes": [ + { + "address": "coder_agent.dev1", + "mode": "managed", + "type": "coder_agent", + "name": "dev1", + "provider_name": "registry.terraform.io/coder/coder", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "arch": "amd64", + "auth": "token", + "dir": null, + "env": null, + "os": "linux", + "startup_script": null + }, + "after_unknown": { + "id": true, + "init_script": true, + "token": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + }, + { + "address": "null_resource.first", + "mode": "managed", + "type": "null_resource", + "name": "first", + "provider_name": "registry.terraform.io/hashicorp/null", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "triggers": null + }, + "after_unknown": { + "id": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + }, + { + "address": "null_resource.second", + "mode": "managed", + "type": "null_resource", + "name": "second", + "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.4.2" + }, + "null": { + "name": "null", + "full_name": "registry.terraform.io/hashicorp/null" + } + }, + "root_module": { + "resources": [ + { + "address": "coder_agent.dev1", + "mode": "managed", + "type": "coder_agent", + "name": "dev1", + "provider_config_key": "coder", + "expressions": { + "arch": { + "constant_value": "amd64" + }, + "os": { + "constant_value": "linux" + } + }, + "schema_version": 0 + }, + { + "address": "null_resource.first", + "mode": "managed", + "type": "null_resource", + "name": "first", + "provider_config_key": "null", + "schema_version": 0, + "depends_on": [ + "coder_agent.dev1" + ] + }, + { + "address": "null_resource.second", + "mode": "managed", + "type": "null_resource", + "name": "second", + "provider_config_key": "null", + "schema_version": 0, + "depends_on": [ + "null_resource.first" + ] + } + ] + } + } +} diff --git a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.dot b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.dot new file mode 100644 index 0000000000000..b590846e3828e --- /dev/null +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.dot @@ -0,0 +1,20 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] coder_agent.dev1 (expand)" [label = "coder_agent.dev1", shape = "box"] + "[root] null_resource.first (expand)" [label = "null_resource.first", shape = "box"] + "[root] null_resource.second (expand)" [label = "null_resource.second", 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.dev1 (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] null_resource.first (expand)" -> "[root] coder_agent.dev1 (expand)" + "[root] null_resource.first (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" + "[root] null_resource.second (expand)" -> "[root] null_resource.first (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_agent.dev1 (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.second (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/chaining-resources/chaining-resources.tfstate.json b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json new file mode 100644 index 0000000000000..17e1cab4852b7 --- /dev/null +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json @@ -0,0 +1,63 @@ +{ + "format_version": "1.0", + "terraform_version": "1.2.2", + "values": { + "root_module": { + "resources": [ + { + "address": "coder_agent.dev1", + "mode": "managed", + "type": "coder_agent", + "name": "dev1", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "arch": "amd64", + "auth": "token", + "dir": null, + "env": null, + "id": "13a37216-e26b-4cb3-9c32-b56173f1d7b4", + "init_script": "", + "os": "linux", + "startup_script": null, + "token": "863b5c19-069d-4873-90fa-8a99d17e60b5" + }, + "sensitive_values": {} + }, + { + "address": "null_resource.first", + "mode": "managed", + "type": "null_resource", + "name": "first", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "id": "9217427594333257339", + "triggers": null + }, + "sensitive_values": {}, + "depends_on": [ + "coder_agent.dev1" + ] + }, + { + "address": "null_resource.second", + "mode": "managed", + "type": "null_resource", + "name": "second", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "id": "2139093808191769892", + "triggers": null + }, + "sensitive_values": {}, + "depends_on": [ + "coder_agent.dev1", + "null_resource.first" + ] + } + ] + } + } +} diff --git a/provisioner/terraform/testdata/generate.sh b/provisioner/terraform/testdata/generate.sh new file mode 100755 index 0000000000000..95cf6fde2b19e --- /dev/null +++ b/provisioner/terraform/testdata/generate.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" + +for d in */; do + pushd "$d" + name=$(basename "$(pwd)") + terraform init + terraform plan -out terraform.tfplan + terraform show -json ./terraform.tfplan | jq >"$name".tfplan.json + terraform graph >"$name".tfplan.dot + rm terraform.tfplan + terraform apply -auto-approve + terraform show -json ./terraform.tfstate | jq >"$name".tfstate.json + rm terraform.tfstate + terraform graph >"$name".tfstate.dot + popd +done diff --git a/provisioner/terraform/testdata/instance-id/instance-id.tfplan.dot b/provisioner/terraform/testdata/instance-id/instance-id.tfplan.dot new file mode 100644 index 0000000000000..b732ca64b5202 --- /dev/null +++ b/provisioner/terraform/testdata/instance-id/instance-id.tfplan.dot @@ -0,0 +1,20 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] coder_agent.dev (expand)" [label = "coder_agent.dev", shape = "box"] + "[root] coder_agent_instance.dev (expand)" [label = "coder_agent_instance.dev", 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_agent_instance.dev (expand)" -> "[root] coder_agent.dev (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_agent_instance.dev (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/instance-id/instance-id.tfplan.json b/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json new file mode 100644 index 0000000000000..2eaad7588292b --- /dev/null +++ b/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json @@ -0,0 +1,198 @@ +{ + "format_version": "1.1", + "terraform_version": "1.2.2", + "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": "google-instance-identity", + "dir": null, + "env": null, + "os": "linux", + "startup_script": null + }, + "sensitive_values": {} + }, + { + "address": "coder_agent_instance.dev", + "mode": "managed", + "type": "coder_agent_instance", + "name": "dev", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "instance_id": "example" + }, + "sensitive_values": {} + }, + { + "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": "google-instance-identity", + "dir": null, + "env": null, + "os": "linux", + "startup_script": null + }, + "after_unknown": { + "id": true, + "init_script": true, + "token": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + }, + { + "address": "coder_agent_instance.dev", + "mode": "managed", + "type": "coder_agent_instance", + "name": "dev", + "provider_name": "registry.terraform.io/coder/coder", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "instance_id": "example" + }, + "after_unknown": { + "agent_id": true, + "id": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + }, + { + "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.4.2" + }, + "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" + }, + "auth": { + "constant_value": "google-instance-identity" + }, + "os": { + "constant_value": "linux" + } + }, + "schema_version": 0 + }, + { + "address": "coder_agent_instance.dev", + "mode": "managed", + "type": "coder_agent_instance", + "name": "dev", + "provider_config_key": "coder", + "expressions": { + "agent_id": { + "references": [ + "coder_agent.dev.id", + "coder_agent.dev" + ] + }, + "instance_id": { + "constant_value": "example" + } + }, + "schema_version": 0 + }, + { + "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/instance-id/instance-id.tfstate.dot b/provisioner/terraform/testdata/instance-id/instance-id.tfstate.dot new file mode 100644 index 0000000000000..b732ca64b5202 --- /dev/null +++ b/provisioner/terraform/testdata/instance-id/instance-id.tfstate.dot @@ -0,0 +1,20 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] coder_agent.dev (expand)" [label = "coder_agent.dev", shape = "box"] + "[root] coder_agent_instance.dev (expand)" [label = "coder_agent_instance.dev", 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_agent_instance.dev (expand)" -> "[root] coder_agent.dev (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_agent_instance.dev (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/instance-id/instance-id.tfstate.json b/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json new file mode 100644 index 0000000000000..ee302d39daa5b --- /dev/null +++ b/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json @@ -0,0 +1,63 @@ +{ + "format_version": "1.0", + "terraform_version": "1.2.2", + "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": "google-instance-identity", + "dir": null, + "env": null, + "id": "9ac1de0f-b30c-44df-9f10-7c3e4a2f86b9", + "init_script": "", + "os": "linux", + "startup_script": null, + "token": "e1a5d4d0-479b-4d20-9bcf-8309ed6a030f" + }, + "sensitive_values": {} + }, + { + "address": "coder_agent_instance.dev", + "mode": "managed", + "type": "coder_agent_instance", + "name": "dev", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "agent_id": "9ac1de0f-b30c-44df-9f10-7c3e4a2f86b9", + "id": "1bb18780-24f5-452d-a863-b4f3e15ff7bc", + "instance_id": "example" + }, + "sensitive_values": {}, + "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": "3056446686368085729", + "triggers": null + }, + "sensitive_values": {}, + "depends_on": [ + "coder_agent.dev" + ] + } + ] + } + } +} diff --git a/provisioner/terraform/testdata/instance-id/with-instance-id.tf b/provisioner/terraform/testdata/instance-id/with-instance-id.tf new file mode 100644 index 0000000000000..637f654d9ec13 --- /dev/null +++ b/provisioner/terraform/testdata/instance-id/with-instance-id.tf @@ -0,0 +1,25 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "0.4.2" + } + } +} + +resource "coder_agent" "dev" { + os = "linux" + arch = "amd64" + auth = "google-instance-identity" +} + +resource "null_resource" "dev" { + depends_on = [ + coder_agent.dev + ] +} + +resource "coder_agent_instance" "dev" { + agent_id = coder_agent.dev.id + instance_id = "example" +} diff --git a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.dot b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.dot new file mode 100644 index 0000000000000..feeca3e9493f6 --- /dev/null +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.dot @@ -0,0 +1,26 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] coder_agent.dev1 (expand)" [label = "coder_agent.dev1", shape = "box"] + "[root] coder_agent.dev2 (expand)" [label = "coder_agent.dev2", shape = "box"] + "[root] coder_agent.dev3 (expand)" [label = "coder_agent.dev3", 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.dev1 (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] coder_agent.dev2 (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] coder_agent.dev3 (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] null_resource.dev (expand)" -> "[root] coder_agent.dev1 (expand)" + "[root] null_resource.dev (expand)" -> "[root] coder_agent.dev2 (expand)" + "[root] null_resource.dev (expand)" -> "[root] coder_agent.dev3 (expand)" + "[root] null_resource.dev (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_agent.dev1 (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_agent.dev2 (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_agent.dev3 (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/multiple-agents/multiple-agents.tfplan.json b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json new file mode 100644 index 0000000000000..f78dd1cd475df --- /dev/null +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json @@ -0,0 +1,258 @@ +{ + "format_version": "1.1", + "terraform_version": "1.2.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "coder_agent.dev1", + "mode": "managed", + "type": "coder_agent", + "name": "dev1", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "arch": "amd64", + "auth": "token", + "dir": null, + "env": null, + "os": "linux", + "startup_script": null + }, + "sensitive_values": {} + }, + { + "address": "coder_agent.dev2", + "mode": "managed", + "type": "coder_agent", + "name": "dev2", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "arch": "amd64", + "auth": "token", + "dir": null, + "env": null, + "os": "darwin", + "startup_script": null + }, + "sensitive_values": {} + }, + { + "address": "coder_agent.dev3", + "mode": "managed", + "type": "coder_agent", + "name": "dev3", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "arch": "arm64", + "auth": "token", + "dir": null, + "env": null, + "os": "windows", + "startup_script": null + }, + "sensitive_values": {} + }, + { + "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.dev1", + "mode": "managed", + "type": "coder_agent", + "name": "dev1", + "provider_name": "registry.terraform.io/coder/coder", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "arch": "amd64", + "auth": "token", + "dir": null, + "env": null, + "os": "linux", + "startup_script": null + }, + "after_unknown": { + "id": true, + "init_script": true, + "token": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + }, + { + "address": "coder_agent.dev2", + "mode": "managed", + "type": "coder_agent", + "name": "dev2", + "provider_name": "registry.terraform.io/coder/coder", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "arch": "amd64", + "auth": "token", + "dir": null, + "env": null, + "os": "darwin", + "startup_script": null + }, + "after_unknown": { + "id": true, + "init_script": true, + "token": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + }, + { + "address": "coder_agent.dev3", + "mode": "managed", + "type": "coder_agent", + "name": "dev3", + "provider_name": "registry.terraform.io/coder/coder", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "arch": "arm64", + "auth": "token", + "dir": null, + "env": null, + "os": "windows", + "startup_script": null + }, + "after_unknown": { + "id": true, + "init_script": true, + "token": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + }, + { + "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.4.2" + }, + "null": { + "name": "null", + "full_name": "registry.terraform.io/hashicorp/null" + } + }, + "root_module": { + "resources": [ + { + "address": "coder_agent.dev1", + "mode": "managed", + "type": "coder_agent", + "name": "dev1", + "provider_config_key": "coder", + "expressions": { + "arch": { + "constant_value": "amd64" + }, + "os": { + "constant_value": "linux" + } + }, + "schema_version": 0 + }, + { + "address": "coder_agent.dev2", + "mode": "managed", + "type": "coder_agent", + "name": "dev2", + "provider_config_key": "coder", + "expressions": { + "arch": { + "constant_value": "amd64" + }, + "os": { + "constant_value": "darwin" + } + }, + "schema_version": 0 + }, + { + "address": "coder_agent.dev3", + "mode": "managed", + "type": "coder_agent", + "name": "dev3", + "provider_config_key": "coder", + "expressions": { + "arch": { + "constant_value": "arm64" + }, + "os": { + "constant_value": "windows" + } + }, + "schema_version": 0 + }, + { + "address": "null_resource.dev", + "mode": "managed", + "type": "null_resource", + "name": "dev", + "provider_config_key": "null", + "schema_version": 0, + "depends_on": [ + "coder_agent.dev1", + "coder_agent.dev2", + "coder_agent.dev3" + ] + } + ] + } + } +} diff --git a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.dot b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.dot new file mode 100644 index 0000000000000..feeca3e9493f6 --- /dev/null +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.dot @@ -0,0 +1,26 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] coder_agent.dev1 (expand)" [label = "coder_agent.dev1", shape = "box"] + "[root] coder_agent.dev2 (expand)" [label = "coder_agent.dev2", shape = "box"] + "[root] coder_agent.dev3 (expand)" [label = "coder_agent.dev3", 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.dev1 (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] coder_agent.dev2 (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] coder_agent.dev3 (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] null_resource.dev (expand)" -> "[root] coder_agent.dev1 (expand)" + "[root] null_resource.dev (expand)" -> "[root] coder_agent.dev2 (expand)" + "[root] null_resource.dev (expand)" -> "[root] coder_agent.dev3 (expand)" + "[root] null_resource.dev (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_agent.dev1 (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_agent.dev2 (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_agent.dev3 (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/multiple-agents/multiple-agents.tfstate.json b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json new file mode 100644 index 0000000000000..fd5c9d5585c1c --- /dev/null +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json @@ -0,0 +1,88 @@ +{ + "format_version": "1.0", + "terraform_version": "1.2.2", + "values": { + "root_module": { + "resources": [ + { + "address": "coder_agent.dev1", + "mode": "managed", + "type": "coder_agent", + "name": "dev1", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "arch": "amd64", + "auth": "token", + "dir": null, + "env": null, + "id": "03857334-bbeb-4ad0-9562-faf04890191e", + "init_script": "", + "os": "linux", + "startup_script": null, + "token": "aebe14d8-06b9-49e2-8bd3-156612c5575e" + }, + "sensitive_values": {} + }, + { + "address": "coder_agent.dev2", + "mode": "managed", + "type": "coder_agent", + "name": "dev2", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "arch": "amd64", + "auth": "token", + "dir": null, + "env": null, + "id": "e677717c-8f42-422b-b5d5-4f7f2da0af87", + "init_script": "", + "os": "darwin", + "startup_script": null, + "token": "15c8bb04-48c3-40d5-9fc4-21d172890bf5" + }, + "sensitive_values": {} + }, + { + "address": "coder_agent.dev3", + "mode": "managed", + "type": "coder_agent", + "name": "dev3", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "arch": "arm64", + "auth": "token", + "dir": null, + "env": null, + "id": "c6f4a383-e827-4404-8210-97b7c331d2fe", + "init_script": "", + "os": "windows", + "startup_script": null, + "token": "78c8ce5a-677d-429b-b1f6-dc32c03cee5c" + }, + "sensitive_values": {} + }, + { + "address": "null_resource.dev", + "mode": "managed", + "type": "null_resource", + "name": "dev", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "id": "4861314035639495631", + "triggers": null + }, + "sensitive_values": {}, + "depends_on": [ + "coder_agent.dev1", + "coder_agent.dev2", + "coder_agent.dev3" + ] + } + ] + } + } +} diff --git a/provisioner/terraform/testdata/multiple-agents/with-multiple-agents.tf b/provisioner/terraform/testdata/multiple-agents/with-multiple-agents.tf new file mode 100644 index 0000000000000..820708859a0af --- /dev/null +++ b/provisioner/terraform/testdata/multiple-agents/with-multiple-agents.tf @@ -0,0 +1,31 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "0.4.2" + } + } +} + +resource "coder_agent" "dev1" { + os = "linux" + arch = "amd64" +} + +resource "coder_agent" "dev2" { + os = "darwin" + arch = "amd64" +} + +resource "coder_agent" "dev3" { + os = "windows" + arch = "arm64" +} + +resource "null_resource" "dev" { + depends_on = [ + coder_agent.dev1, + coder_agent.dev2, + coder_agent.dev3 + ] +}