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)) } } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy