From 9d60f471bda07ac196ef986e57e7093cd72d68cf Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Tue, 10 Jun 2025 03:40:35 +0000 Subject: [PATCH 1/9] fix: respect resource_id for coder_metadata --- provisioner/terraform/resources.go | 88 ++++++++---- provisioner/terraform/resources_test.go | 176 ++++++++++++++++++++++++ 2 files changed, 238 insertions(+), 26 deletions(-) diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index 9642751e7466a..4412d4ee8aee0 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -684,41 +684,77 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s if err != nil { return nil, xerrors.Errorf("decode metadata attributes: %w", err) } - resourceLabel := convertAddressToLabel(resource.Address) - var attachedNode *gographviz.Node - for _, node := range graph.Nodes.Lookup { - // The node attributes surround the label with quotes. - if strings.Trim(node.Attrs["label"], `"`) != resourceLabel { - continue + var targetLabel string + + // First, check if ResourceID is provided and try to find the resource by ID + if attrs.ResourceID != "" { + // Look for a resource with matching ID + foundByID := false + for label, tfResources := range tfResourcesByLabel { + for _, tfResource := range tfResources { + // Check if this resource's ID matches the ResourceID + idAttr, hasID := tfResource.AttributeValues["id"] + if hasID { + idStr, ok := idAttr.(string) + if ok && idStr == attrs.ResourceID { + targetLabel = label + foundByID = true + break + } + } + } + if foundByID { + break + } + } + + // If we couldn't find by ID, fall back to graph traversal + if !foundByID { + logger.Warn(ctx, "coder_metadata resource_id not found, falling back to graph traversal", + slog.F("resource_id", attrs.ResourceID), + slog.F("metadata_address", resource.Address)) } - attachedNode = node - break - } - if attachedNode == nil { - continue } - var attachedResource *graphResource - for _, resource := range findResourcesInGraph(graph, tfResourcesByLabel, attachedNode.Name, 0, false) { - if attachedResource == nil { - // Default to the first resource because we have nothing to compare! - attachedResource = resource - continue + + // If ResourceID wasn't provided or wasn't found, use graph traversal + if targetLabel == "" { + resourceLabel := convertAddressToLabel(resource.Address) + + var attachedNode *gographviz.Node + for _, node := range graph.Nodes.Lookup { + // The node attributes surround the label with quotes. + if strings.Trim(node.Attrs["label"], `"`) != resourceLabel { + continue + } + attachedNode = node + break } - if resource.Depth < attachedResource.Depth { - // There's a closer resource! - attachedResource = resource + if attachedNode == nil { continue } - if resource.Depth == attachedResource.Depth && resource.Label < attachedResource.Label { - attachedResource = resource + 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 { continue } + targetLabel = attachedResource.Label } - if attachedResource == nil { - continue - } - targetLabel := attachedResource.Label if metadataTargetLabels[targetLabel] { return nil, xerrors.Errorf("duplicate metadata resource: %s", targetLabel) diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go index 1575c6c9c159e..8df413bab99f0 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -1608,3 +1608,179 @@ func sortExternalAuthProviders(providers []*proto.ExternalAuthProviderResource) return strings.Compare(providers[i].Id, providers[j].Id) == -1 }) } + +func TestMetadataResourceID(t *testing.T) { + t.Parallel() + + t.Run("UsesResourceIDWhenProvided", func(t *testing.T) { + t.Parallel() + ctx, logger := ctxAndLogger(t) + + // Create a state with two resources and metadata that references the second one via resource_id + state, err := terraform.ConvertState(ctx, []*tfjson.StateModule{{ + Resources: []*tfjson.StateResource{{ + Address: "null_resource.first", + Type: "null_resource", + Name: "first", + Mode: tfjson.ManagedResourceMode, + AttributeValues: map[string]interface{}{ + "id": "first-resource-id", + }, + }, { + Address: "null_resource.second", + Type: "null_resource", + Name: "second", + Mode: tfjson.ManagedResourceMode, + AttributeValues: map[string]interface{}{ + "id": "second-resource-id", + }, + }, { + Address: "coder_metadata.example", + Type: "coder_metadata", + Name: "example", + Mode: tfjson.ManagedResourceMode, + DependsOn: []string{"null_resource.first"}, + AttributeValues: map[string]interface{}{ + "resource_id": "second-resource-id", + "item": []interface{}{ + map[string]interface{}{ + "key": "test", + "value": "value", + }, + }, + }, + }}, + }}, `digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] null_resource.first" [label = "null_resource.first", shape = "box"] + "[root] null_resource.second" [label = "null_resource.second", shape = "box"] + "[root] coder_metadata.example" [label = "coder_metadata.example", shape = "box"] + "[root] coder_metadata.example" -> "[root] null_resource.first" + } +}`, logger) + require.NoError(t, err) + require.Len(t, state.Resources, 2) + + // Find the resources + var firstResource, secondResource *proto.Resource + for _, res := range state.Resources { + if res.Name == "first" && res.Type == "null_resource" { + firstResource = res + } else if res.Name == "second" && res.Type == "null_resource" { + secondResource = res + } + } + + require.NotNil(t, firstResource) + require.NotNil(t, secondResource) + + // The metadata should be on the second resource (as specified by resource_id), + // not the first one (which is the closest in the graph) + require.Len(t, firstResource.Metadata, 0, "first resource should have no metadata") + require.Len(t, secondResource.Metadata, 1, "second resource should have metadata") + require.Equal(t, "test", secondResource.Metadata[0].Key) + require.Equal(t, "value", secondResource.Metadata[0].Value) + }) + + t.Run("FallsBackToGraphWhenResourceIDNotFound", func(t *testing.T) { + t.Parallel() + ctx, logger := ctxAndLogger(t) + + // Create a state where resource_id references a non-existent ID + state, err := terraform.ConvertState(ctx, []*tfjson.StateModule{{ + Resources: []*tfjson.StateResource{{ + Address: "null_resource.example", + Type: "null_resource", + Name: "example", + Mode: tfjson.ManagedResourceMode, + AttributeValues: map[string]interface{}{ + "id": "example-resource-id", + }, + }, { + Address: "coder_metadata.example", + Type: "coder_metadata", + Name: "example", + Mode: tfjson.ManagedResourceMode, + DependsOn: []string{"null_resource.example"}, + AttributeValues: map[string]interface{}{ + "resource_id": "non-existent-id", + "item": []interface{}{ + map[string]interface{}{ + "key": "test", + "value": "value", + }, + }, + }, + }}, + }}, `digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] null_resource.example" [label = "null_resource.example", shape = "box"] + "[root] coder_metadata.example" [label = "coder_metadata.example", shape = "box"] + "[root] coder_metadata.example" -> "[root] null_resource.example" + } +}`, logger) + require.NoError(t, err) + require.Len(t, state.Resources, 1) + + // The metadata should still be applied via graph traversal + require.Equal(t, "example", state.Resources[0].Name) + require.Len(t, state.Resources[0].Metadata, 1) + require.Equal(t, "test", state.Resources[0].Metadata[0].Key) + require.Equal(t, "value", state.Resources[0].Metadata[0].Value) + + // When resource_id is not found, it falls back to graph traversal + // We can't easily verify the warning was logged without access to the log capture API + }) + + t.Run("UsesGraphWhenResourceIDNotProvided", func(t *testing.T) { + t.Parallel() + ctx, logger := ctxAndLogger(t) + + // Create a state without resource_id + state, err := terraform.ConvertState(ctx, []*tfjson.StateModule{{ + Resources: []*tfjson.StateResource{{ + Address: "null_resource.example", + Type: "null_resource", + Name: "example", + Mode: tfjson.ManagedResourceMode, + AttributeValues: map[string]interface{}{ + "id": "example-resource-id", + }, + }, { + Address: "coder_metadata.example", + Type: "coder_metadata", + Name: "example", + Mode: tfjson.ManagedResourceMode, + DependsOn: []string{"null_resource.example"}, + AttributeValues: map[string]interface{}{ + "item": []interface{}{ + map[string]interface{}{ + "key": "test", + "value": "value", + }, + }, + }, + }}, + }}, `digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] null_resource.example" [label = "null_resource.example", shape = "box"] + "[root] coder_metadata.example" [label = "coder_metadata.example", shape = "box"] + "[root] coder_metadata.example" -> "[root] null_resource.example" + } +}`, logger) + require.NoError(t, err) + require.Len(t, state.Resources, 1) + + // The metadata should be applied via graph traversal + require.Equal(t, "example", state.Resources[0].Name) + require.Len(t, state.Resources[0].Metadata, 1) + require.Equal(t, "test", state.Resources[0].Metadata[0].Key) + require.Equal(t, "value", state.Resources[0].Metadata[0].Value) + }) +} From 1232eaac72a3f03d33dd34eb9711034a4d5b8bfe Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Tue, 10 Jun 2025 06:18:11 +0000 Subject: [PATCH 2/9] cache id --- provisioner/terraform/resources.go | 36 +++++++++++++----------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index 4412d4ee8aee0..c46696ac80131 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -208,6 +208,9 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s // The label is what "terraform graph" uses to reference nodes. tfResourcesByLabel := map[string]map[string]*tfjson.StateResource{} + // Map resource IDs to labels for efficient lookup when processing metadata + labelByResourceID := map[string]string{} + // Extra array to preserve the order of rich parameters. tfResourcesRichParameters := make([]*tfjson.StateResource, 0) tfResourcesPresets := make([]*tfjson.StateResource, 0) @@ -233,6 +236,13 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s tfResourcesByLabel[label] = map[string]*tfjson.StateResource{} } tfResourcesByLabel[label][resource.Address] = resource + + // Build the ID to label map + if idAttr, hasID := resource.AttributeValues["id"]; hasID { + if idStr, ok := idAttr.(string); ok && idStr != "" { + labelByResourceID[idStr] = label + } + } } } for _, module := range modules { @@ -690,27 +700,11 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s // First, check if ResourceID is provided and try to find the resource by ID if attrs.ResourceID != "" { // Look for a resource with matching ID - foundByID := false - for label, tfResources := range tfResourcesByLabel { - for _, tfResource := range tfResources { - // Check if this resource's ID matches the ResourceID - idAttr, hasID := tfResource.AttributeValues["id"] - if hasID { - idStr, ok := idAttr.(string) - if ok && idStr == attrs.ResourceID { - targetLabel = label - foundByID = true - break - } - } - } - if foundByID { - break - } - } - - // If we couldn't find by ID, fall back to graph traversal - if !foundByID { + foundLabel, foundByID := labelByResourceID[attrs.ResourceID] + if foundByID { + targetLabel = foundLabel + } else { + // If we couldn't find by ID, fall back to graph traversal logger.Warn(ctx, "coder_metadata resource_id not found, falling back to graph traversal", slog.F("resource_id", attrs.ResourceID), slog.F("metadata_address", resource.Address)) From 6182af413e11d5b79d9cc1c821beaea88617da81 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Thu, 12 Jun 2025 01:50:15 +0000 Subject: [PATCH 3/9] update tests --- provisioner/terraform/resources_test.go | 302 +++++++----------- .../resource-id-not-found.tf | 19 ++ .../resource-id-not-found.tfplan.dot | 9 + .../resource-id-not-found.tfplan.json | 1 + .../resource-id-not-found.tfstate.dot | 9 + .../resource-id-not-found.tfstate.json | 49 +++ .../resource-id-not-provided.tf | 18 ++ .../resource-id-not-provided.tfplan.dot | 9 + .../resource-id-not-provided.tfplan.json | 1 + .../resource-id-not-provided.tfstate.dot | 9 + .../resource-id-not-provided.tfstate.json | 48 +++ .../resource-id-provided.tf | 21 ++ .../resource-id-provided.tfplan.dot | 21 ++ .../resource-id-provided.tfplan.json | 217 +++++++++++++ .../resource-id-provided.tfstate.dot | 21 ++ .../resource-id-provided.tfstate.json | 68 ++++ 16 files changed, 631 insertions(+), 191 deletions(-) create mode 100644 provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tf create mode 100644 provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfplan.dot create mode 100644 provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfplan.json create mode 100644 provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfstate.dot create mode 100644 provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfstate.json create mode 100644 provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tf create mode 100644 provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfplan.dot create mode 100644 provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfplan.json create mode 100644 provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfstate.dot create mode 100644 provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfstate.json create mode 100644 provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tf create mode 100644 provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tfplan.dot create mode 100644 provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tfplan.json create mode 100644 provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tfstate.dot create mode 100644 provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tfstate.json diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go index 8df413bab99f0..a38400818af40 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -1249,24 +1249,120 @@ func TestAgentNameDuplicate(t *testing.T) { require.ErrorContains(t, err, "duplicate agent name") } -func TestMetadataResourceDuplicate(t *testing.T) { +func TestMetadata(t *testing.T) { t.Parallel() - ctx, logger := ctxAndLogger(t) - // Load the multiple-apps state file and edit it. - dir := filepath.Join("testdata", "resources", "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) + t.Run("Duplicate", func(t *testing.T) { + t.Parallel() + ctx, logger := ctxAndLogger(t) + // Load the multiple-apps state file and edit it. + dir := filepath.Join("testdata", "resources", "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(ctx, []*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph), logger) - require.Nil(t, state) - require.Error(t, err) - require.ErrorContains(t, err, "duplicate metadata resource: null_resource.about") + state, err := terraform.ConvertState(ctx, []*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph), logger) + require.Nil(t, state) + require.Error(t, err) + require.ErrorContains(t, err, "duplicate metadata resource: null_resource.about") + }) + + t.Run("ResourceID", func(t *testing.T) { + t.Parallel() + + t.Run("ResourceIDProvided", func(t *testing.T) { + t.Parallel() + ctx, logger := ctxAndLogger(t) + + dir := filepath.Join("testdata", "resources", "resource-id-provided") + tfStateRaw, err := os.ReadFile(filepath.Join(dir, "resource-id-provided.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, "resource-id-provided.tfstate.dot")) + require.NoError(t, err) + + state, err := terraform.ConvertState(ctx, []*tfjson.StateModule{tfState.Values.RootModule}, string(tfStateGraph), logger) + require.NoError(t, err) + require.Len(t, state.Resources, 2) + + // Find the resources + var firstResource, secondResource *proto.Resource + for _, res := range state.Resources { + if res.Name == "first" && res.Type == "null_resource" { + firstResource = res + } else if res.Name == "second" && res.Type == "null_resource" { + secondResource = res + } + } + + require.NotNil(t, firstResource) + require.NotNil(t, secondResource) + + // The metadata should be on the second resource (as specified by resource_id), + // not the first one (which is the closest in the graph) + require.Len(t, firstResource.Metadata, 0, "first resource should have no metadata") + require.Len(t, secondResource.Metadata, 1, "second resource should have metadata") + require.Equal(t, "test", secondResource.Metadata[0].Key) + require.Equal(t, "value", secondResource.Metadata[0].Value) + }) + + t.Run("ResourceIDNotFound", func(t *testing.T) { + t.Parallel() + ctx, logger := ctxAndLogger(t) + + dir := filepath.Join("testdata", "resources", "resource-id-not-found") + tfStateRaw, err := os.ReadFile(filepath.Join(dir, "resource-id-not-found.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, "resource-id-not-found.tfstate.dot")) + require.NoError(t, err) + + state, err := terraform.ConvertState(ctx, []*tfjson.StateModule{tfState.Values.RootModule}, string(tfStateGraph), logger) + require.NoError(t, err) + require.Len(t, state.Resources, 1) + + // The metadata should still be applied via graph traversal + require.Equal(t, "example", state.Resources[0].Name) + require.Len(t, state.Resources[0].Metadata, 1) + require.Equal(t, "test", state.Resources[0].Metadata[0].Key) + require.Equal(t, "value", state.Resources[0].Metadata[0].Value) + + // When resource_id is not found, it falls back to graph traversal + // We can't easily verify the warning was logged without access to the log capture API + }) + + t.Run("ResourceIDNotProvided", func(t *testing.T) { + t.Parallel() + ctx, logger := ctxAndLogger(t) + + dir := filepath.Join("testdata", "resources", "resource-id-not-provided") + tfStateRaw, err := os.ReadFile(filepath.Join(dir, "resource-id-not-provided.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, "resource-id-not-provided.tfstate.dot")) + require.NoError(t, err) + + state, err := terraform.ConvertState(ctx, []*tfjson.StateModule{tfState.Values.RootModule}, string(tfStateGraph), logger) + require.NoError(t, err) + require.Len(t, state.Resources, 1) + + // The metadata should be applied via graph traversal + require.Equal(t, "example", state.Resources[0].Name) + require.Len(t, state.Resources[0].Metadata, 1) + require.Equal(t, "test", state.Resources[0].Metadata[0].Key) + require.Equal(t, "value", state.Resources[0].Metadata[0].Value) + }) + }) } func TestParameterValidation(t *testing.T) { @@ -1608,179 +1704,3 @@ func sortExternalAuthProviders(providers []*proto.ExternalAuthProviderResource) return strings.Compare(providers[i].Id, providers[j].Id) == -1 }) } - -func TestMetadataResourceID(t *testing.T) { - t.Parallel() - - t.Run("UsesResourceIDWhenProvided", func(t *testing.T) { - t.Parallel() - ctx, logger := ctxAndLogger(t) - - // Create a state with two resources and metadata that references the second one via resource_id - state, err := terraform.ConvertState(ctx, []*tfjson.StateModule{{ - Resources: []*tfjson.StateResource{{ - Address: "null_resource.first", - Type: "null_resource", - Name: "first", - Mode: tfjson.ManagedResourceMode, - AttributeValues: map[string]interface{}{ - "id": "first-resource-id", - }, - }, { - Address: "null_resource.second", - Type: "null_resource", - Name: "second", - Mode: tfjson.ManagedResourceMode, - AttributeValues: map[string]interface{}{ - "id": "second-resource-id", - }, - }, { - Address: "coder_metadata.example", - Type: "coder_metadata", - Name: "example", - Mode: tfjson.ManagedResourceMode, - DependsOn: []string{"null_resource.first"}, - AttributeValues: map[string]interface{}{ - "resource_id": "second-resource-id", - "item": []interface{}{ - map[string]interface{}{ - "key": "test", - "value": "value", - }, - }, - }, - }}, - }}, `digraph { - compound = "true" - newrank = "true" - subgraph "root" { - "[root] null_resource.first" [label = "null_resource.first", shape = "box"] - "[root] null_resource.second" [label = "null_resource.second", shape = "box"] - "[root] coder_metadata.example" [label = "coder_metadata.example", shape = "box"] - "[root] coder_metadata.example" -> "[root] null_resource.first" - } -}`, logger) - require.NoError(t, err) - require.Len(t, state.Resources, 2) - - // Find the resources - var firstResource, secondResource *proto.Resource - for _, res := range state.Resources { - if res.Name == "first" && res.Type == "null_resource" { - firstResource = res - } else if res.Name == "second" && res.Type == "null_resource" { - secondResource = res - } - } - - require.NotNil(t, firstResource) - require.NotNil(t, secondResource) - - // The metadata should be on the second resource (as specified by resource_id), - // not the first one (which is the closest in the graph) - require.Len(t, firstResource.Metadata, 0, "first resource should have no metadata") - require.Len(t, secondResource.Metadata, 1, "second resource should have metadata") - require.Equal(t, "test", secondResource.Metadata[0].Key) - require.Equal(t, "value", secondResource.Metadata[0].Value) - }) - - t.Run("FallsBackToGraphWhenResourceIDNotFound", func(t *testing.T) { - t.Parallel() - ctx, logger := ctxAndLogger(t) - - // Create a state where resource_id references a non-existent ID - state, err := terraform.ConvertState(ctx, []*tfjson.StateModule{{ - Resources: []*tfjson.StateResource{{ - Address: "null_resource.example", - Type: "null_resource", - Name: "example", - Mode: tfjson.ManagedResourceMode, - AttributeValues: map[string]interface{}{ - "id": "example-resource-id", - }, - }, { - Address: "coder_metadata.example", - Type: "coder_metadata", - Name: "example", - Mode: tfjson.ManagedResourceMode, - DependsOn: []string{"null_resource.example"}, - AttributeValues: map[string]interface{}{ - "resource_id": "non-existent-id", - "item": []interface{}{ - map[string]interface{}{ - "key": "test", - "value": "value", - }, - }, - }, - }}, - }}, `digraph { - compound = "true" - newrank = "true" - subgraph "root" { - "[root] null_resource.example" [label = "null_resource.example", shape = "box"] - "[root] coder_metadata.example" [label = "coder_metadata.example", shape = "box"] - "[root] coder_metadata.example" -> "[root] null_resource.example" - } -}`, logger) - require.NoError(t, err) - require.Len(t, state.Resources, 1) - - // The metadata should still be applied via graph traversal - require.Equal(t, "example", state.Resources[0].Name) - require.Len(t, state.Resources[0].Metadata, 1) - require.Equal(t, "test", state.Resources[0].Metadata[0].Key) - require.Equal(t, "value", state.Resources[0].Metadata[0].Value) - - // When resource_id is not found, it falls back to graph traversal - // We can't easily verify the warning was logged without access to the log capture API - }) - - t.Run("UsesGraphWhenResourceIDNotProvided", func(t *testing.T) { - t.Parallel() - ctx, logger := ctxAndLogger(t) - - // Create a state without resource_id - state, err := terraform.ConvertState(ctx, []*tfjson.StateModule{{ - Resources: []*tfjson.StateResource{{ - Address: "null_resource.example", - Type: "null_resource", - Name: "example", - Mode: tfjson.ManagedResourceMode, - AttributeValues: map[string]interface{}{ - "id": "example-resource-id", - }, - }, { - Address: "coder_metadata.example", - Type: "coder_metadata", - Name: "example", - Mode: tfjson.ManagedResourceMode, - DependsOn: []string{"null_resource.example"}, - AttributeValues: map[string]interface{}{ - "item": []interface{}{ - map[string]interface{}{ - "key": "test", - "value": "value", - }, - }, - }, - }}, - }}, `digraph { - compound = "true" - newrank = "true" - subgraph "root" { - "[root] null_resource.example" [label = "null_resource.example", shape = "box"] - "[root] coder_metadata.example" [label = "coder_metadata.example", shape = "box"] - "[root] coder_metadata.example" -> "[root] null_resource.example" - } -}`, logger) - require.NoError(t, err) - require.Len(t, state.Resources, 1) - - // The metadata should be applied via graph traversal - require.Equal(t, "example", state.Resources[0].Name) - require.Len(t, state.Resources[0].Metadata, 1) - require.Equal(t, "test", state.Resources[0].Metadata[0].Key) - require.Equal(t, "value", state.Resources[0].Metadata[0].Value) - }) -} diff --git a/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tf b/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tf new file mode 100644 index 0000000000000..6ccd83d6233a9 --- /dev/null +++ b/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = ">=2.0.0" + } + } +} + +resource "null_resource" "example" {} + +resource "coder_metadata" "example" { + resource_id = "non-existent-id" + depends_on = [null_resource.example] + item { + key = "test" + value = "value" + } +} diff --git a/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfplan.dot b/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfplan.dot new file mode 100644 index 0000000000000..67d7f022f5f60 --- /dev/null +++ b/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfplan.dot @@ -0,0 +1,9 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] null_resource.example" [label = "null_resource.example", shape = "box"] + "[root] coder_metadata.example" [label = "coder_metadata.example", shape = "box"] + "[root] coder_metadata.example" -> "[root] null_resource.example" + } +} diff --git a/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfplan.json b/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfplan.json new file mode 100644 index 0000000000000..0967ef424bce6 --- /dev/null +++ b/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfplan.json @@ -0,0 +1 @@ +{} diff --git a/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfstate.dot b/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfstate.dot new file mode 100644 index 0000000000000..67d7f022f5f60 --- /dev/null +++ b/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfstate.dot @@ -0,0 +1,9 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] null_resource.example" [label = "null_resource.example", shape = "box"] + "[root] coder_metadata.example" [label = "coder_metadata.example", shape = "box"] + "[root] coder_metadata.example" -> "[root] null_resource.example" + } +} diff --git a/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfstate.json b/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfstate.json new file mode 100644 index 0000000000000..86d36fe5d19d8 --- /dev/null +++ b/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfstate.json @@ -0,0 +1,49 @@ +{ + "format_version": "1.0", + "terraform_version": "1.11.0", + "values": { + "root_module": { + "resources": [ + { + "address": "null_resource.example", + "mode": "managed", + "type": "null_resource", + "name": "example", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "id": "example-resource-id", + "triggers": null + }, + "sensitive_values": {} + }, + { + "address": "coder_metadata.example", + "mode": "managed", + "type": "coder_metadata", + "name": "example", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "id": "metadata-id", + "resource_id": "non-existent-id", + "item": [ + { + "key": "test", + "value": "value", + "sensitive": false, + "is_null": false + } + ] + }, + "sensitive_values": { + "item": [{}] + }, + "depends_on": [ + "null_resource.example" + ] + } + ] + } + } +} diff --git a/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tf b/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tf new file mode 100644 index 0000000000000..2330b6cf638b5 --- /dev/null +++ b/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tf @@ -0,0 +1,18 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = ">=2.0.0" + } + } +} + +resource "null_resource" "example" {} + +resource "coder_metadata" "example" { + depends_on = [null_resource.example] + item { + key = "test" + value = "value" + } +} diff --git a/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfplan.dot b/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfplan.dot new file mode 100644 index 0000000000000..67d7f022f5f60 --- /dev/null +++ b/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfplan.dot @@ -0,0 +1,9 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] null_resource.example" [label = "null_resource.example", shape = "box"] + "[root] coder_metadata.example" [label = "coder_metadata.example", shape = "box"] + "[root] coder_metadata.example" -> "[root] null_resource.example" + } +} diff --git a/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfplan.json b/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfplan.json new file mode 100644 index 0000000000000..0967ef424bce6 --- /dev/null +++ b/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfplan.json @@ -0,0 +1 @@ +{} diff --git a/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfstate.dot b/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfstate.dot new file mode 100644 index 0000000000000..67d7f022f5f60 --- /dev/null +++ b/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfstate.dot @@ -0,0 +1,9 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] null_resource.example" [label = "null_resource.example", shape = "box"] + "[root] coder_metadata.example" [label = "coder_metadata.example", shape = "box"] + "[root] coder_metadata.example" -> "[root] null_resource.example" + } +} diff --git a/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfstate.json b/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfstate.json new file mode 100644 index 0000000000000..83b83977ff272 --- /dev/null +++ b/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfstate.json @@ -0,0 +1,48 @@ +{ + "format_version": "1.0", + "terraform_version": "1.11.0", + "values": { + "root_module": { + "resources": [ + { + "address": "null_resource.example", + "mode": "managed", + "type": "null_resource", + "name": "example", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "id": "example-resource-id", + "triggers": null + }, + "sensitive_values": {} + }, + { + "address": "coder_metadata.example", + "mode": "managed", + "type": "coder_metadata", + "name": "example", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "id": "metadata-id", + "item": [ + { + "key": "test", + "value": "value", + "sensitive": false, + "is_null": false + } + ] + }, + "sensitive_values": { + "item": [{}] + }, + "depends_on": [ + "null_resource.example" + ] + } + ] + } + } +} diff --git a/provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tf b/provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tf new file mode 100644 index 0000000000000..ba44bf172ce7f --- /dev/null +++ b/provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tf @@ -0,0 +1,21 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = ">=2.0.0" + } + } +} + +resource "null_resource" "first" {} + +resource "null_resource" "second" {} + +resource "coder_metadata" "example" { + resource_id = null_resource.second.id + depends_on = [null_resource.first] + item { + key = "test" + value = "value" + } +} diff --git a/provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tfplan.dot b/provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tfplan.dot new file mode 100644 index 0000000000000..937a312445a06 --- /dev/null +++ b/provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tfplan.dot @@ -0,0 +1,21 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] coder_metadata.example (expand)" [label = "coder_metadata.example", 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_metadata.example (expand)" -> "[root] null_resource.first (expand)" + "[root] coder_metadata.example (expand)" -> "[root] null_resource.second (expand)" + "[root] coder_metadata.example (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] null_resource.first (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" + "[root] null_resource.second (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_metadata.example (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.first (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/resources/resource-id-provided/resource-id-provided.tfplan.json b/provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tfplan.json new file mode 100644 index 0000000000000..2a3f445d1e017 --- /dev/null +++ b/provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tfplan.json @@ -0,0 +1,217 @@ +{ + "format_version": "1.2", + "terraform_version": "1.11.4", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "coder_metadata.example", + "mode": "managed", + "type": "coder_metadata", + "name": "example", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "daily_cost": null, + "hide": null, + "icon": null, + "item": [ + { + "key": "test", + "sensitive": false, + "value": "value" + } + ] + }, + "sensitive_values": { + "item": [ + {} + ] + } + }, + { + "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_metadata.example", + "mode": "managed", + "type": "coder_metadata", + "name": "example", + "provider_name": "registry.terraform.io/coder/coder", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "daily_cost": null, + "hide": null, + "icon": null, + "item": [ + { + "key": "test", + "sensitive": false, + "value": "value" + } + ] + }, + "after_unknown": { + "id": true, + "item": [ + { + "is_null": true + } + ], + "resource_id": true + }, + "before_sensitive": false, + "after_sensitive": { + "item": [ + {} + ] + } + } + }, + { + "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": ">= 2.0.0" + }, + "null": { + "name": "null", + "full_name": "registry.terraform.io/hashicorp/null" + } + }, + "root_module": { + "resources": [ + { + "address": "coder_metadata.example", + "mode": "managed", + "type": "coder_metadata", + "name": "example", + "provider_config_key": "coder", + "expressions": { + "item": [ + { + "key": { + "constant_value": "test" + }, + "value": { + "constant_value": "value" + } + } + ], + "resource_id": { + "references": [ + "null_resource.second.id", + "null_resource.second" + ] + } + }, + "schema_version": 1, + "depends_on": [ + "null_resource.first" + ] + }, + { + "address": "null_resource.first", + "mode": "managed", + "type": "null_resource", + "name": "first", + "provider_config_key": "null", + "schema_version": 0 + }, + { + "address": "null_resource.second", + "mode": "managed", + "type": "null_resource", + "name": "second", + "provider_config_key": "null", + "schema_version": 0 + } + ] + } + }, + "relevant_attributes": [ + { + "resource": "null_resource.second", + "attribute": [ + "id" + ] + } + ], + "timestamp": "2025-06-11T20:34:28Z", + "applyable": true, + "complete": true, + "errored": false +} diff --git a/provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tfstate.dot b/provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tfstate.dot new file mode 100644 index 0000000000000..937a312445a06 --- /dev/null +++ b/provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tfstate.dot @@ -0,0 +1,21 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] coder_metadata.example (expand)" [label = "coder_metadata.example", 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_metadata.example (expand)" -> "[root] null_resource.first (expand)" + "[root] coder_metadata.example (expand)" -> "[root] null_resource.second (expand)" + "[root] coder_metadata.example (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] null_resource.first (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" + "[root] null_resource.second (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_metadata.example (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.first (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/resources/resource-id-provided/resource-id-provided.tfstate.json b/provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tfstate.json new file mode 100644 index 0000000000000..ed30a9d927d08 --- /dev/null +++ b/provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tfstate.json @@ -0,0 +1,68 @@ +{ + "format_version": "1.0", + "terraform_version": "1.11.4", + "values": { + "root_module": { + "resources": [ + { + "address": "coder_metadata.example", + "mode": "managed", + "type": "coder_metadata", + "name": "example", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "daily_cost": null, + "hide": null, + "icon": null, + "id": "79e8977e-3277-4856-93a5-6200d9b8b156", + "item": [ + { + "is_null": false, + "key": "test", + "sensitive": false, + "value": "value" + } + ], + "resource_id": "6187209037590702578" + }, + "sensitive_values": { + "item": [ + {} + ] + }, + "depends_on": [ + "null_resource.first", + "null_resource.second" + ] + }, + { + "address": "null_resource.first", + "mode": "managed", + "type": "null_resource", + "name": "first", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "id": "529239707770856465", + "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": { + "id": "6187209037590702578", + "triggers": null + }, + "sensitive_values": {} + } + ] + } + } +} From 49a3a2bede55a410f9f055fa8a3fe9b1f01399dc Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Thu, 12 Jun 2025 02:30:30 +0000 Subject: [PATCH 4/9] fix tests --- provisioner/terraform/resources_test.go | 27 +- .../resource-id-not-found.tfplan.dot | 14 +- .../resource-id-not-found.tfplan.json | 1 - .../resource-id-not-found.tfstate.dot | 14 +- .../resource-id-not-found.tfstate.json | 41 +- .../resource-id-not-provided.tf | 18 - .../resource-id-not-provided.tfplan.dot | 9 - .../resource-id-not-provided.tfplan.json | 1 - .../resource-id-not-provided.tfstate.dot | 9 - .../resource-id-not-provided.tfstate.json | 48 -- .../resource-id-provided.tfplan.json | 217 --------- .../resource-metadata-duplicate.tfplan.json | 440 ------------------ 12 files changed, 47 insertions(+), 792 deletions(-) delete mode 100644 provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tf delete mode 100644 provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfplan.dot delete mode 100644 provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfplan.json delete mode 100644 provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfstate.dot delete mode 100644 provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfstate.json diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go index a38400818af40..000bad03a1d7a 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -1274,7 +1274,7 @@ func TestMetadata(t *testing.T) { t.Run("ResourceID", func(t *testing.T) { t.Parallel() - t.Run("ResourceIDProvided", func(t *testing.T) { + t.Run("OK", func(t *testing.T) { t.Parallel() ctx, logger := ctxAndLogger(t) @@ -1312,7 +1312,7 @@ func TestMetadata(t *testing.T) { require.Equal(t, "value", secondResource.Metadata[0].Value) }) - t.Run("ResourceIDNotFound", func(t *testing.T) { + t.Run("NotFound", func(t *testing.T) { t.Parallel() ctx, logger := ctxAndLogger(t) @@ -1339,29 +1339,6 @@ func TestMetadata(t *testing.T) { // We can't easily verify the warning was logged without access to the log capture API }) - t.Run("ResourceIDNotProvided", func(t *testing.T) { - t.Parallel() - ctx, logger := ctxAndLogger(t) - - dir := filepath.Join("testdata", "resources", "resource-id-not-provided") - tfStateRaw, err := os.ReadFile(filepath.Join(dir, "resource-id-not-provided.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, "resource-id-not-provided.tfstate.dot")) - require.NoError(t, err) - - state, err := terraform.ConvertState(ctx, []*tfjson.StateModule{tfState.Values.RootModule}, string(tfStateGraph), logger) - require.NoError(t, err) - require.Len(t, state.Resources, 1) - - // The metadata should be applied via graph traversal - require.Equal(t, "example", state.Resources[0].Name) - require.Len(t, state.Resources[0].Metadata, 1) - require.Equal(t, "test", state.Resources[0].Metadata[0].Key) - require.Equal(t, "value", state.Resources[0].Metadata[0].Value) - }) }) } diff --git a/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfplan.dot b/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfplan.dot index 67d7f022f5f60..9a77a06ec3737 100644 --- a/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfplan.dot +++ b/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfplan.dot @@ -2,8 +2,16 @@ digraph { compound = "true" newrank = "true" subgraph "root" { - "[root] null_resource.example" [label = "null_resource.example", shape = "box"] - "[root] coder_metadata.example" [label = "coder_metadata.example", shape = "box"] - "[root] coder_metadata.example" -> "[root] null_resource.example" + "[root] coder_metadata.example (expand)" [label = "coder_metadata.example", shape = "box"] + "[root] null_resource.example (expand)" [label = "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_metadata.example (expand)" -> "[root] null_resource.example (expand)" + "[root] coder_metadata.example (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] null_resource.example (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_metadata.example (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.example (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/resources/resource-id-not-found/resource-id-not-found.tfplan.json b/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfplan.json index 0967ef424bce6..e69de29bb2d1d 100644 --- a/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfplan.json +++ b/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfplan.json @@ -1 +0,0 @@ -{} diff --git a/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfstate.dot b/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfstate.dot index 67d7f022f5f60..9a77a06ec3737 100644 --- a/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfstate.dot +++ b/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfstate.dot @@ -2,8 +2,16 @@ digraph { compound = "true" newrank = "true" subgraph "root" { - "[root] null_resource.example" [label = "null_resource.example", shape = "box"] - "[root] coder_metadata.example" [label = "coder_metadata.example", shape = "box"] - "[root] coder_metadata.example" -> "[root] null_resource.example" + "[root] coder_metadata.example (expand)" [label = "coder_metadata.example", shape = "box"] + "[root] null_resource.example (expand)" [label = "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_metadata.example (expand)" -> "[root] null_resource.example (expand)" + "[root] coder_metadata.example (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] null_resource.example (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_metadata.example (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.example (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/resources/resource-id-not-found/resource-id-not-found.tfstate.json b/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfstate.json index 86d36fe5d19d8..971a980fd975b 100644 --- a/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfstate.json +++ b/provisioner/terraform/testdata/resources/resource-id-not-found/resource-id-not-found.tfstate.json @@ -4,19 +4,6 @@ "values": { "root_module": { "resources": [ - { - "address": "null_resource.example", - "mode": "managed", - "type": "null_resource", - "name": "example", - "provider_name": "registry.terraform.io/hashicorp/null", - "schema_version": 0, - "values": { - "id": "example-resource-id", - "triggers": null - }, - "sensitive_values": {} - }, { "address": "coder_metadata.example", "mode": "managed", @@ -25,23 +12,41 @@ "provider_name": "registry.terraform.io/coder/coder", "schema_version": 1, "values": { + "daily_cost": null, + "hide": null, + "icon": null, "id": "metadata-id", - "resource_id": "non-existent-id", "item": [ { + "is_null": false, "key": "test", - "value": "value", "sensitive": false, - "is_null": false + "value": "value" } - ] + ], + "resource_id": "non-existent-id" }, "sensitive_values": { - "item": [{}] + "item": [ + {} + ] }, "depends_on": [ "null_resource.example" ] + }, + { + "address": "null_resource.example", + "mode": "managed", + "type": "null_resource", + "name": "example", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "id": "7576374697426687487", + "triggers": null + }, + "sensitive_values": {} } ] } diff --git a/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tf b/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tf deleted file mode 100644 index 2330b6cf638b5..0000000000000 --- a/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tf +++ /dev/null @@ -1,18 +0,0 @@ -terraform { - required_providers { - coder = { - source = "coder/coder" - version = ">=2.0.0" - } - } -} - -resource "null_resource" "example" {} - -resource "coder_metadata" "example" { - depends_on = [null_resource.example] - item { - key = "test" - value = "value" - } -} diff --git a/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfplan.dot b/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfplan.dot deleted file mode 100644 index 67d7f022f5f60..0000000000000 --- a/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfplan.dot +++ /dev/null @@ -1,9 +0,0 @@ -digraph { - compound = "true" - newrank = "true" - subgraph "root" { - "[root] null_resource.example" [label = "null_resource.example", shape = "box"] - "[root] coder_metadata.example" [label = "coder_metadata.example", shape = "box"] - "[root] coder_metadata.example" -> "[root] null_resource.example" - } -} diff --git a/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfplan.json b/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfplan.json deleted file mode 100644 index 0967ef424bce6..0000000000000 --- a/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfplan.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfstate.dot b/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfstate.dot deleted file mode 100644 index 67d7f022f5f60..0000000000000 --- a/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfstate.dot +++ /dev/null @@ -1,9 +0,0 @@ -digraph { - compound = "true" - newrank = "true" - subgraph "root" { - "[root] null_resource.example" [label = "null_resource.example", shape = "box"] - "[root] coder_metadata.example" [label = "coder_metadata.example", shape = "box"] - "[root] coder_metadata.example" -> "[root] null_resource.example" - } -} diff --git a/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfstate.json b/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfstate.json deleted file mode 100644 index 83b83977ff272..0000000000000 --- a/provisioner/terraform/testdata/resources/resource-id-not-provided/resource-id-not-provided.tfstate.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "format_version": "1.0", - "terraform_version": "1.11.0", - "values": { - "root_module": { - "resources": [ - { - "address": "null_resource.example", - "mode": "managed", - "type": "null_resource", - "name": "example", - "provider_name": "registry.terraform.io/hashicorp/null", - "schema_version": 0, - "values": { - "id": "example-resource-id", - "triggers": null - }, - "sensitive_values": {} - }, - { - "address": "coder_metadata.example", - "mode": "managed", - "type": "coder_metadata", - "name": "example", - "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 1, - "values": { - "id": "metadata-id", - "item": [ - { - "key": "test", - "value": "value", - "sensitive": false, - "is_null": false - } - ] - }, - "sensitive_values": { - "item": [{}] - }, - "depends_on": [ - "null_resource.example" - ] - } - ] - } - } -} diff --git a/provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tfplan.json b/provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tfplan.json index 2a3f445d1e017..e69de29bb2d1d 100644 --- a/provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tfplan.json +++ b/provisioner/terraform/testdata/resources/resource-id-provided/resource-id-provided.tfplan.json @@ -1,217 +0,0 @@ -{ - "format_version": "1.2", - "terraform_version": "1.11.4", - "planned_values": { - "root_module": { - "resources": [ - { - "address": "coder_metadata.example", - "mode": "managed", - "type": "coder_metadata", - "name": "example", - "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 1, - "values": { - "daily_cost": null, - "hide": null, - "icon": null, - "item": [ - { - "key": "test", - "sensitive": false, - "value": "value" - } - ] - }, - "sensitive_values": { - "item": [ - {} - ] - } - }, - { - "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_metadata.example", - "mode": "managed", - "type": "coder_metadata", - "name": "example", - "provider_name": "registry.terraform.io/coder/coder", - "change": { - "actions": [ - "create" - ], - "before": null, - "after": { - "daily_cost": null, - "hide": null, - "icon": null, - "item": [ - { - "key": "test", - "sensitive": false, - "value": "value" - } - ] - }, - "after_unknown": { - "id": true, - "item": [ - { - "is_null": true - } - ], - "resource_id": true - }, - "before_sensitive": false, - "after_sensitive": { - "item": [ - {} - ] - } - } - }, - { - "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": ">= 2.0.0" - }, - "null": { - "name": "null", - "full_name": "registry.terraform.io/hashicorp/null" - } - }, - "root_module": { - "resources": [ - { - "address": "coder_metadata.example", - "mode": "managed", - "type": "coder_metadata", - "name": "example", - "provider_config_key": "coder", - "expressions": { - "item": [ - { - "key": { - "constant_value": "test" - }, - "value": { - "constant_value": "value" - } - } - ], - "resource_id": { - "references": [ - "null_resource.second.id", - "null_resource.second" - ] - } - }, - "schema_version": 1, - "depends_on": [ - "null_resource.first" - ] - }, - { - "address": "null_resource.first", - "mode": "managed", - "type": "null_resource", - "name": "first", - "provider_config_key": "null", - "schema_version": 0 - }, - { - "address": "null_resource.second", - "mode": "managed", - "type": "null_resource", - "name": "second", - "provider_config_key": "null", - "schema_version": 0 - } - ] - } - }, - "relevant_attributes": [ - { - "resource": "null_resource.second", - "attribute": [ - "id" - ] - } - ], - "timestamp": "2025-06-11T20:34:28Z", - "applyable": true, - "complete": true, - "errored": false -} diff --git a/provisioner/terraform/testdata/resources/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.json b/provisioner/terraform/testdata/resources/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.json index ae38a9f3571d2..e69de29bb2d1d 100644 --- a/provisioner/terraform/testdata/resources/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.json +++ b/provisioner/terraform/testdata/resources/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.json @@ -1,440 +0,0 @@ -{ - "format_version": "1.2", - "terraform_version": "1.11.0", - "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": 1, - "values": { - "api_key_scope": "all", - "arch": "amd64", - "auth": "token", - "connection_timeout": 120, - "dir": null, - "env": null, - "metadata": [ - { - "display_name": "Process Count", - "interval": 5, - "key": "process_count", - "order": null, - "script": "ps -ef | wc -l", - "timeout": 1 - } - ], - "motd_file": null, - "order": null, - "os": "linux", - "resources_monitoring": [], - "shutdown_script": null, - "startup_script": null, - "startup_script_behavior": "non-blocking", - "troubleshooting_url": null - }, - "sensitive_values": { - "display_apps": [], - "metadata": [ - {} - ], - "resources_monitoring": [], - "token": true - } - }, - { - "address": "coder_metadata.about_info", - "mode": "managed", - "type": "coder_metadata", - "name": "about_info", - "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 1, - "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": 1, - "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": { - "api_key_scope": "all", - "arch": "amd64", - "auth": "token", - "connection_timeout": 120, - "dir": null, - "env": null, - "metadata": [ - { - "display_name": "Process Count", - "interval": 5, - "key": "process_count", - "order": null, - "script": "ps -ef | wc -l", - "timeout": 1 - } - ], - "motd_file": null, - "order": null, - "os": "linux", - "resources_monitoring": [], - "shutdown_script": null, - "startup_script": null, - "startup_script_behavior": "non-blocking", - "troubleshooting_url": null - }, - "after_unknown": { - "display_apps": true, - "id": true, - "init_script": true, - "metadata": [ - {} - ], - "resources_monitoring": [], - "token": true - }, - "before_sensitive": false, - "after_sensitive": { - "display_apps": [], - "metadata": [ - {} - ], - "resources_monitoring": [], - "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": ">= 2.0.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": 1 - }, - { - "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": 1 - }, - { - "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": 1 - }, - { - "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": "2025-03-03T20:39:59Z", - "applyable": true, - "complete": true, - "errored": false -} From f1dabf7618f44211b6acb92b61054345a737a3f0 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Thu, 12 Jun 2025 02:34:30 +0000 Subject: [PATCH 5/9] make fmt --- provisioner/terraform/resources_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go index 000bad03a1d7a..8a0c8b6b0baac 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -1338,7 +1338,6 @@ func TestMetadata(t *testing.T) { // When resource_id is not found, it falls back to graph traversal // We can't easily verify the warning was logged without access to the log capture API }) - }) } From cfb02c7e43e68238497f0325460ae989c88ed072 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Thu, 12 Jun 2025 02:38:08 +0000 Subject: [PATCH 6/9] remove comment --- provisioner/terraform/resources_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go index 8a0c8b6b0baac..d4987e2effa5f 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -1334,9 +1334,6 @@ func TestMetadata(t *testing.T) { require.Len(t, state.Resources[0].Metadata, 1) require.Equal(t, "test", state.Resources[0].Metadata[0].Key) require.Equal(t, "value", state.Resources[0].Metadata[0].Value) - - // When resource_id is not found, it falls back to graph traversal - // We can't easily verify the warning was logged without access to the log capture API }) }) } From 3d92225133d12e1ca2172f7999d2e094e00f813c Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Thu, 12 Jun 2025 03:06:29 +0000 Subject: [PATCH 7/9] handle duplicate TF IDs --- provisioner/terraform/resources.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index c46696ac80131..e10fa5254237e 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -209,7 +209,8 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s tfResourcesByLabel := map[string]map[string]*tfjson.StateResource{} // Map resource IDs to labels for efficient lookup when processing metadata - labelByResourceID := map[string]string{} + // Multiple resources can have the same ID, so we store a slice of labels + labelsByResourceID := map[string][]string{} // Extra array to preserve the order of rich parameters. tfResourcesRichParameters := make([]*tfjson.StateResource, 0) @@ -237,10 +238,10 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s } tfResourcesByLabel[label][resource.Address] = resource - // Build the ID to label map + // Build the ID to labels map - multiple resources can have the same ID if idAttr, hasID := resource.AttributeValues["id"]; hasID { if idStr, ok := idAttr.(string); ok && idStr != "" { - labelByResourceID[idStr] = label + labelsByResourceID[idStr] = append(labelsByResourceID[idStr], label) } } } @@ -700,9 +701,16 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s // First, check if ResourceID is provided and try to find the resource by ID if attrs.ResourceID != "" { // Look for a resource with matching ID - foundLabel, foundByID := labelByResourceID[attrs.ResourceID] - if foundByID { - targetLabel = foundLabel + foundLabels := labelsByResourceID[attrs.ResourceID] + if len(foundLabels) == 1 { + // Single match - use it + targetLabel = foundLabels[0] + } else if len(foundLabels) > 1 { + // Multiple resources with same ID - this creates ambiguity + logger.Warn(ctx, "multiple resources found with same resource_id, falling back to graph traversal", + slog.F("resource_id", attrs.ResourceID), + slog.F("metadata_address", resource.Address), + slog.F("matching_labels", foundLabels)) } else { // If we couldn't find by ID, fall back to graph traversal logger.Warn(ctx, "coder_metadata resource_id not found, falling back to graph traversal", From a2afc35e7088ba79dfccc24987d19ea78affb34c Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Thu, 12 Jun 2025 03:07:28 +0000 Subject: [PATCH 8/9] undo deleted file --- .../resource-metadata-duplicate.tfplan.json | 440 ++++++++++++++++++ 1 file changed, 440 insertions(+) diff --git a/provisioner/terraform/testdata/resources/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.json b/provisioner/terraform/testdata/resources/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.json index e69de29bb2d1d..ae38a9f3571d2 100644 --- a/provisioner/terraform/testdata/resources/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.json +++ b/provisioner/terraform/testdata/resources/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.json @@ -0,0 +1,440 @@ +{ + "format_version": "1.2", + "terraform_version": "1.11.0", + "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": 1, + "values": { + "api_key_scope": "all", + "arch": "amd64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "env": null, + "metadata": [ + { + "display_name": "Process Count", + "interval": 5, + "key": "process_count", + "order": null, + "script": "ps -ef | wc -l", + "timeout": 1 + } + ], + "motd_file": null, + "order": null, + "os": "linux", + "resources_monitoring": [], + "shutdown_script": null, + "startup_script": null, + "startup_script_behavior": "non-blocking", + "troubleshooting_url": null + }, + "sensitive_values": { + "display_apps": [], + "metadata": [ + {} + ], + "resources_monitoring": [], + "token": true + } + }, + { + "address": "coder_metadata.about_info", + "mode": "managed", + "type": "coder_metadata", + "name": "about_info", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "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": 1, + "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": { + "api_key_scope": "all", + "arch": "amd64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "env": null, + "metadata": [ + { + "display_name": "Process Count", + "interval": 5, + "key": "process_count", + "order": null, + "script": "ps -ef | wc -l", + "timeout": 1 + } + ], + "motd_file": null, + "order": null, + "os": "linux", + "resources_monitoring": [], + "shutdown_script": null, + "startup_script": null, + "startup_script_behavior": "non-blocking", + "troubleshooting_url": null + }, + "after_unknown": { + "display_apps": true, + "id": true, + "init_script": true, + "metadata": [ + {} + ], + "resources_monitoring": [], + "token": true + }, + "before_sensitive": false, + "after_sensitive": { + "display_apps": [], + "metadata": [ + {} + ], + "resources_monitoring": [], + "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": ">= 2.0.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": 1 + }, + { + "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": 1 + }, + { + "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": 1 + }, + { + "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": "2025-03-03T20:39:59Z", + "applyable": true, + "complete": true, + "errored": false +} From 7e0c0a2775db95d91282bd7ea9fb2726d8764444 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Fri, 8 Aug 2025 12:19:46 +0200 Subject: [PATCH 9/9] ci: fix linter errors Change-Id: Ibda8f39393b6df90b98bc82e2a005a506830ce00 Signed-off-by: Thomas Kosiewski --- cli/vpndaemon_darwin.go | 2 +- provisioner/terraform/resources.go | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/cli/vpndaemon_darwin.go b/cli/vpndaemon_darwin.go index a1b836dd6b0c3..0e019a728ac71 100644 --- a/cli/vpndaemon_darwin.go +++ b/cli/vpndaemon_darwin.go @@ -10,7 +10,7 @@ import ( "github.com/coder/serpent" ) -func (r *RootCmd) vpnDaemonRun() *serpent.Command { +func (*RootCmd) vpnDaemonRun() *serpent.Command { var ( rpcReadFD int64 rpcWriteFD int64 diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index e10fa5254237e..a47c450dd6fed 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -702,20 +702,21 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s if attrs.ResourceID != "" { // Look for a resource with matching ID foundLabels := labelsByResourceID[attrs.ResourceID] - if len(foundLabels) == 1 { + switch len(foundLabels) { + case 0: + // If we couldn't find by ID, fall back to graph traversal + logger.Warn(ctx, "coder_metadata resource_id not found, falling back to graph traversal", + slog.F("resource_id", attrs.ResourceID), + slog.F("metadata_address", resource.Address)) + case 1: // Single match - use it targetLabel = foundLabels[0] - } else if len(foundLabels) > 1 { + default: // Multiple resources with same ID - this creates ambiguity logger.Warn(ctx, "multiple resources found with same resource_id, falling back to graph traversal", slog.F("resource_id", attrs.ResourceID), slog.F("metadata_address", resource.Address), slog.F("matching_labels", foundLabels)) - } else { - // If we couldn't find by ID, fall back to graph traversal - logger.Warn(ctx, "coder_metadata resource_id not found, falling back to graph traversal", - slog.F("resource_id", attrs.ResourceID), - slog.F("metadata_address", resource.Address)) } }