diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index 4606dfff133fc..df377c69b0874 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -385,6 +385,7 @@ func ConvertState(modules []*tfjson.StateModule, rawGraph string) (*State, error resourceIcon := map[string]string{} resourceCost := map[string]int32{} + metadataTargetLabels := map[string]bool{} for _, resources := range tfResourcesByLabel { for _, resource := range resources { if resource.Type != "coder_metadata" { @@ -396,7 +397,6 @@ func ConvertState(modules []*tfjson.StateModule, rawGraph string) (*State, error if err != nil { return nil, xerrors.Errorf("decode metadata attributes: %w", err) } - resourceLabel := convertAddressToLabel(resource.Address) var attachedNode *gographviz.Node @@ -433,6 +433,11 @@ func ConvertState(modules []*tfjson.StateModule, rawGraph string) (*State, error } targetLabel := attachedResource.Label + if metadataTargetLabels[targetLabel] { + return nil, xerrors.Errorf("duplicate metadata resource: %s", targetLabel) + } + metadataTargetLabels[targetLabel] = true + resourceHidden[targetLabel] = attrs.Hide resourceIcon[targetLabel] = attrs.Icon resourceCost[targetLabel] = attrs.DailyCost diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go index 86c65d97801dd..49c5f4cde82df 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -636,6 +636,25 @@ func TestAppSlugValidation(t *testing.T) { require.ErrorContains(t, err, "duplicate app slug") } +func TestMetadataResourceDuplicate(t *testing.T) { + t.Parallel() + + // Load the multiple-apps state file and edit it. + dir := filepath.Join("testdata", "resource-metadata-duplicate") + tfPlanRaw, err := os.ReadFile(filepath.Join(dir, "resource-metadata-duplicate.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, "resource-metadata-duplicate.tfplan.dot")) + require.NoError(t, err) + + state, err := terraform.ConvertState([]*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph)) + require.Nil(t, state) + require.Error(t, err) + require.ErrorContains(t, err, "duplicate metadata resource: null_resource.about") +} + func TestParameterValidation(t *testing.T) { t.Parallel() diff --git a/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tf b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tf new file mode 100644 index 0000000000000..21e6f4206499c --- /dev/null +++ b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tf @@ -0,0 +1,51 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "0.9.0" + } + } +} + +resource "coder_agent" "main" { + os = "linux" + arch = "amd64" + metadata { + key = "process_count" + display_name = "Process Count" + script = "ps -ef | wc -l" + interval = 5 + timeout = 1 + } +} + +resource "null_resource" "about" { + depends_on = [ + coder_agent.main, + ] +} + +resource "coder_metadata" "about_info" { + resource_id = null_resource.about.id + hide = true + icon = "/icon/server.svg" + daily_cost = 29 + item { + key = "hello" + value = "world" + } + item { + key = "null" + } +} + +resource "coder_metadata" "other_info" { + resource_id = null_resource.about.id + hide = true + icon = "/icon/server.svg" + daily_cost = 20 + item { + key = "hello" + value = "world" + } +} diff --git a/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.dot b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.dot new file mode 100644 index 0000000000000..34f1ea8f3cb29 --- /dev/null +++ b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.dot @@ -0,0 +1,23 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] coder_agent.main (expand)" [label = "coder_agent.main", shape = "box"] + "[root] coder_metadata.about_info (expand)" [label = "coder_metadata.about_info", shape = "box"] + "[root] coder_metadata.other_info (expand)" [label = "coder_metadata.other_info", shape = "box"] + "[root] null_resource.about (expand)" [label = "null_resource.about", 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.main (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] coder_metadata.about_info (expand)" -> "[root] null_resource.about (expand)" + "[root] coder_metadata.other_info (expand)" -> "[root] null_resource.about (expand)" + "[root] null_resource.about (expand)" -> "[root] coder_agent.main (expand)" + "[root] null_resource.about (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_metadata.about_info (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_metadata.other_info (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.about (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/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.json b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.json new file mode 100644 index 0000000000000..7b707c26d89e4 --- /dev/null +++ b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.json @@ -0,0 +1,428 @@ +{ + "format_version": "1.2", + "terraform_version": "1.5.1", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "coder_agent.main", + "mode": "managed", + "type": "coder_agent", + "name": "main", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "arch": "amd64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "env": null, + "login_before_ready": true, + "metadata": [ + { + "display_name": "Process Count", + "interval": 5, + "key": "process_count", + "script": "ps -ef | wc -l", + "timeout": 1 + } + ], + "motd_file": null, + "os": "linux", + "shutdown_script": null, + "shutdown_script_timeout": 300, + "startup_script": null, + "startup_script_behavior": null, + "startup_script_timeout": 300, + "troubleshooting_url": null + }, + "sensitive_values": { + "metadata": [ + {} + ] + } + }, + { + "address": "coder_metadata.about_info", + "mode": "managed", + "type": "coder_metadata", + "name": "about_info", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "daily_cost": 29, + "hide": true, + "icon": "/icon/server.svg", + "item": [ + { + "key": "hello", + "sensitive": false, + "value": "world" + }, + { + "key": "null", + "sensitive": false, + "value": null + } + ] + }, + "sensitive_values": { + "item": [ + {}, + {} + ] + } + }, + { + "address": "coder_metadata.other_info", + "mode": "managed", + "type": "coder_metadata", + "name": "other_info", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "daily_cost": 20, + "hide": true, + "icon": "/icon/server.svg", + "item": [ + { + "key": "hello", + "sensitive": false, + "value": "world" + } + ] + }, + "sensitive_values": { + "item": [ + {} + ] + } + }, + { + "address": "null_resource.about", + "mode": "managed", + "type": "null_resource", + "name": "about", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "triggers": null + }, + "sensitive_values": {} + } + ] + } + }, + "resource_changes": [ + { + "address": "coder_agent.main", + "mode": "managed", + "type": "coder_agent", + "name": "main", + "provider_name": "registry.terraform.io/coder/coder", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "arch": "amd64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "env": null, + "login_before_ready": true, + "metadata": [ + { + "display_name": "Process Count", + "interval": 5, + "key": "process_count", + "script": "ps -ef | wc -l", + "timeout": 1 + } + ], + "motd_file": null, + "os": "linux", + "shutdown_script": null, + "shutdown_script_timeout": 300, + "startup_script": null, + "startup_script_behavior": null, + "startup_script_timeout": 300, + "troubleshooting_url": null + }, + "after_unknown": { + "id": true, + "init_script": true, + "metadata": [ + {} + ], + "token": true + }, + "before_sensitive": false, + "after_sensitive": { + "metadata": [ + {} + ], + "token": true + } + } + }, + { + "address": "coder_metadata.about_info", + "mode": "managed", + "type": "coder_metadata", + "name": "about_info", + "provider_name": "registry.terraform.io/coder/coder", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "daily_cost": 29, + "hide": true, + "icon": "/icon/server.svg", + "item": [ + { + "key": "hello", + "sensitive": false, + "value": "world" + }, + { + "key": "null", + "sensitive": false, + "value": null + } + ] + }, + "after_unknown": { + "id": true, + "item": [ + { + "is_null": true + }, + { + "is_null": true + } + ], + "resource_id": true + }, + "before_sensitive": false, + "after_sensitive": { + "item": [ + {}, + {} + ] + } + } + }, + { + "address": "coder_metadata.other_info", + "mode": "managed", + "type": "coder_metadata", + "name": "other_info", + "provider_name": "registry.terraform.io/coder/coder", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "daily_cost": 20, + "hide": true, + "icon": "/icon/server.svg", + "item": [ + { + "key": "hello", + "sensitive": false, + "value": "world" + } + ] + }, + "after_unknown": { + "id": true, + "item": [ + { + "is_null": true + } + ], + "resource_id": true + }, + "before_sensitive": false, + "after_sensitive": { + "item": [ + {} + ] + } + } + }, + { + "address": "null_resource.about", + "mode": "managed", + "type": "null_resource", + "name": "about", + "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.9.0" + }, + "null": { + "name": "null", + "full_name": "registry.terraform.io/hashicorp/null" + } + }, + "root_module": { + "resources": [ + { + "address": "coder_agent.main", + "mode": "managed", + "type": "coder_agent", + "name": "main", + "provider_config_key": "coder", + "expressions": { + "arch": { + "constant_value": "amd64" + }, + "metadata": [ + { + "display_name": { + "constant_value": "Process Count" + }, + "interval": { + "constant_value": 5 + }, + "key": { + "constant_value": "process_count" + }, + "script": { + "constant_value": "ps -ef | wc -l" + }, + "timeout": { + "constant_value": 1 + } + } + ], + "os": { + "constant_value": "linux" + } + }, + "schema_version": 0 + }, + { + "address": "coder_metadata.about_info", + "mode": "managed", + "type": "coder_metadata", + "name": "about_info", + "provider_config_key": "coder", + "expressions": { + "daily_cost": { + "constant_value": 29 + }, + "hide": { + "constant_value": true + }, + "icon": { + "constant_value": "/icon/server.svg" + }, + "item": [ + { + "key": { + "constant_value": "hello" + }, + "value": { + "constant_value": "world" + } + }, + { + "key": { + "constant_value": "null" + } + } + ], + "resource_id": { + "references": [ + "null_resource.about.id", + "null_resource.about" + ] + } + }, + "schema_version": 0 + }, + { + "address": "coder_metadata.other_info", + "mode": "managed", + "type": "coder_metadata", + "name": "other_info", + "provider_config_key": "coder", + "expressions": { + "daily_cost": { + "constant_value": 20 + }, + "hide": { + "constant_value": true + }, + "icon": { + "constant_value": "/icon/server.svg" + }, + "item": [ + { + "key": { + "constant_value": "hello" + }, + "value": { + "constant_value": "world" + } + } + ], + "resource_id": { + "references": [ + "null_resource.about.id", + "null_resource.about" + ] + } + }, + "schema_version": 0 + }, + { + "address": "null_resource.about", + "mode": "managed", + "type": "null_resource", + "name": "about", + "provider_config_key": "null", + "schema_version": 0, + "depends_on": [ + "coder_agent.main" + ] + } + ] + } + }, + "relevant_attributes": [ + { + "resource": "null_resource.about", + "attribute": [ + "id" + ] + } + ], + "timestamp": "2023-07-05T10:28:42Z" +} diff --git a/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfstate.dot b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfstate.dot new file mode 100644 index 0000000000000..34f1ea8f3cb29 --- /dev/null +++ b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfstate.dot @@ -0,0 +1,23 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] coder_agent.main (expand)" [label = "coder_agent.main", shape = "box"] + "[root] coder_metadata.about_info (expand)" [label = "coder_metadata.about_info", shape = "box"] + "[root] coder_metadata.other_info (expand)" [label = "coder_metadata.other_info", shape = "box"] + "[root] null_resource.about (expand)" [label = "null_resource.about", 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.main (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] coder_metadata.about_info (expand)" -> "[root] null_resource.about (expand)" + "[root] coder_metadata.other_info (expand)" -> "[root] null_resource.about (expand)" + "[root] null_resource.about (expand)" -> "[root] coder_agent.main (expand)" + "[root] null_resource.about (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_metadata.about_info (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_metadata.other_info (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.about (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/resource-metadata-duplicate/resource-metadata-duplicate.tfstate.json b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfstate.json new file mode 100644 index 0000000000000..cd0c4afd775c7 --- /dev/null +++ b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfstate.json @@ -0,0 +1,139 @@ +{ + "format_version": "1.0", + "terraform_version": "1.5.1", + "values": { + "root_module": { + "resources": [ + { + "address": "coder_agent.main", + "mode": "managed", + "type": "coder_agent", + "name": "main", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "arch": "amd64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "env": null, + "id": "d211806f-511a-4ced-aa68-c70ca3eeab60", + "init_script": "", + "login_before_ready": true, + "metadata": [ + { + "display_name": "Process Count", + "interval": 5, + "key": "process_count", + "script": "ps -ef | wc -l", + "timeout": 1 + } + ], + "motd_file": null, + "os": "linux", + "shutdown_script": null, + "shutdown_script_timeout": 300, + "startup_script": null, + "startup_script_behavior": null, + "startup_script_timeout": 300, + "token": "746a2a79-b575-4eab-9e03-ec95d53a810c", + "troubleshooting_url": null + }, + "sensitive_values": { + "metadata": [ + {} + ], + "token": true + } + }, + { + "address": "coder_metadata.about_info", + "mode": "managed", + "type": "coder_metadata", + "name": "about_info", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "daily_cost": 29, + "hide": true, + "icon": "/icon/server.svg", + "id": "0ddb64e0-ed2f-493a-a305-bd617837faad", + "item": [ + { + "is_null": false, + "key": "hello", + "sensitive": false, + "value": "world" + }, + { + "is_null": true, + "key": "null", + "sensitive": false, + "value": "" + } + ], + "resource_id": "799423861826055007" + }, + "sensitive_values": { + "item": [ + {}, + {} + ] + }, + "depends_on": [ + "coder_agent.main", + "null_resource.about" + ] + }, + { + "address": "coder_metadata.other_info", + "mode": "managed", + "type": "coder_metadata", + "name": "other_info", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "daily_cost": 20, + "hide": true, + "icon": "/icon/server.svg", + "id": "305c001c-28d4-4d90-adc7-655a3a89eb2e", + "item": [ + { + "is_null": false, + "key": "hello", + "sensitive": false, + "value": "world" + } + ], + "resource_id": "799423861826055007" + }, + "sensitive_values": { + "item": [ + {} + ] + }, + "depends_on": [ + "coder_agent.main", + "null_resource.about" + ] + }, + { + "address": "null_resource.about", + "mode": "managed", + "type": "null_resource", + "name": "about", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "id": "799423861826055007", + "triggers": null + }, + "sensitive_values": {}, + "depends_on": [ + "coder_agent.main" + ] + } + ] + } + } +} diff --git a/provisionerd/runner/runner.go b/provisionerd/runner/runner.go index 7ca5b7f2c4e2c..99eb7133a6b80 100644 --- a/provisionerd/runner/runner.go +++ b/provisionerd/runner/runner.go @@ -232,6 +232,10 @@ func (r *Runner) Run() { err := r.sender.CompleteJob(ctx, r.completedJob) if err != nil { r.logger.Error(ctx, "sending CompletedJob failed", slog.Error(err)) + err = r.sender.FailJob(ctx, r.failedJobf("internal provisionerserver error")) + if err != nil { + r.logger.Error(ctx, "sending FailJob failed (while CompletedJob)", slog.Error(err)) + } } else { r.logger.Debug(ctx, "sent CompletedJob") }