Skip to content

Commit 558c1eb

Browse files
committed
fix: respect resource_id for coder_metadata
1 parent be4f5ef commit 558c1eb

File tree

2 files changed

+238
-26
lines changed

2 files changed

+238
-26
lines changed

provisioner/terraform/resources.go

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -646,41 +646,77 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
646646
if err != nil {
647647
return nil, xerrors.Errorf("decode metadata attributes: %w", err)
648648
}
649-
resourceLabel := convertAddressToLabel(resource.Address)
650649

651-
var attachedNode *gographviz.Node
652-
for _, node := range graph.Nodes.Lookup {
653-
// The node attributes surround the label with quotes.
654-
if strings.Trim(node.Attrs["label"], `"`) != resourceLabel {
655-
continue
650+
var targetLabel string
651+
652+
// First, check if ResourceID is provided and try to find the resource by ID
653+
if attrs.ResourceID != "" {
654+
// Look for a resource with matching ID
655+
foundByID := false
656+
for label, tfResources := range tfResourcesByLabel {
657+
for _, tfResource := range tfResources {
658+
// Check if this resource's ID matches the ResourceID
659+
idAttr, hasID := tfResource.AttributeValues["id"]
660+
if hasID {
661+
idStr, ok := idAttr.(string)
662+
if ok && idStr == attrs.ResourceID {
663+
targetLabel = label
664+
foundByID = true
665+
break
666+
}
667+
}
668+
}
669+
if foundByID {
670+
break
671+
}
672+
}
673+
674+
// If we couldn't find by ID, fall back to graph traversal
675+
if !foundByID {
676+
logger.Warn(ctx, "coder_metadata resource_id not found, falling back to graph traversal",
677+
slog.F("resource_id", attrs.ResourceID),
678+
slog.F("metadata_address", resource.Address))
656679
}
657-
attachedNode = node
658-
break
659-
}
660-
if attachedNode == nil {
661-
continue
662680
}
663-
var attachedResource *graphResource
664-
for _, resource := range findResourcesInGraph(graph, tfResourcesByLabel, attachedNode.Name, 0, false) {
665-
if attachedResource == nil {
666-
// Default to the first resource because we have nothing to compare!
667-
attachedResource = resource
668-
continue
681+
682+
// If ResourceID wasn't provided or wasn't found, use graph traversal
683+
if targetLabel == "" {
684+
resourceLabel := convertAddressToLabel(resource.Address)
685+
686+
var attachedNode *gographviz.Node
687+
for _, node := range graph.Nodes.Lookup {
688+
// The node attributes surround the label with quotes.
689+
if strings.Trim(node.Attrs["label"], `"`) != resourceLabel {
690+
continue
691+
}
692+
attachedNode = node
693+
break
669694
}
670-
if resource.Depth < attachedResource.Depth {
671-
// There's a closer resource!
672-
attachedResource = resource
695+
if attachedNode == nil {
673696
continue
674697
}
675-
if resource.Depth == attachedResource.Depth && resource.Label < attachedResource.Label {
676-
attachedResource = resource
698+
var attachedResource *graphResource
699+
for _, resource := range findResourcesInGraph(graph, tfResourcesByLabel, attachedNode.Name, 0, false) {
700+
if attachedResource == nil {
701+
// Default to the first resource because we have nothing to compare!
702+
attachedResource = resource
703+
continue
704+
}
705+
if resource.Depth < attachedResource.Depth {
706+
// There's a closer resource!
707+
attachedResource = resource
708+
continue
709+
}
710+
if resource.Depth == attachedResource.Depth && resource.Label < attachedResource.Label {
711+
attachedResource = resource
712+
continue
713+
}
714+
}
715+
if attachedResource == nil {
677716
continue
678717
}
718+
targetLabel = attachedResource.Label
679719
}
680-
if attachedResource == nil {
681-
continue
682-
}
683-
targetLabel := attachedResource.Label
684720

685721
if metadataTargetLabels[targetLabel] {
686722
return nil, xerrors.Errorf("duplicate metadata resource: %s", targetLabel)

provisioner/terraform/resources_test.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1469,3 +1469,179 @@ func sortExternalAuthProviders(providers []*proto.ExternalAuthProviderResource)
14691469
return strings.Compare(providers[i].Id, providers[j].Id) == -1
14701470
})
14711471
}
1472+
1473+
func TestMetadataResourceID(t *testing.T) {
1474+
t.Parallel()
1475+
1476+
t.Run("UsesResourceIDWhenProvided", func(t *testing.T) {
1477+
t.Parallel()
1478+
ctx, logger := ctxAndLogger(t)
1479+
1480+
// Create a state with two resources and metadata that references the second one via resource_id
1481+
state, err := terraform.ConvertState(ctx, []*tfjson.StateModule{{
1482+
Resources: []*tfjson.StateResource{{
1483+
Address: "null_resource.first",
1484+
Type: "null_resource",
1485+
Name: "first",
1486+
Mode: tfjson.ManagedResourceMode,
1487+
AttributeValues: map[string]interface{}{
1488+
"id": "first-resource-id",
1489+
},
1490+
}, {
1491+
Address: "null_resource.second",
1492+
Type: "null_resource",
1493+
Name: "second",
1494+
Mode: tfjson.ManagedResourceMode,
1495+
AttributeValues: map[string]interface{}{
1496+
"id": "second-resource-id",
1497+
},
1498+
}, {
1499+
Address: "coder_metadata.example",
1500+
Type: "coder_metadata",
1501+
Name: "example",
1502+
Mode: tfjson.ManagedResourceMode,
1503+
DependsOn: []string{"null_resource.first"},
1504+
AttributeValues: map[string]interface{}{
1505+
"resource_id": "second-resource-id",
1506+
"item": []interface{}{
1507+
map[string]interface{}{
1508+
"key": "test",
1509+
"value": "value",
1510+
},
1511+
},
1512+
},
1513+
}},
1514+
}}, `digraph {
1515+
compound = "true"
1516+
newrank = "true"
1517+
subgraph "root" {
1518+
"[root] null_resource.first" [label = "null_resource.first", shape = "box"]
1519+
"[root] null_resource.second" [label = "null_resource.second", shape = "box"]
1520+
"[root] coder_metadata.example" [label = "coder_metadata.example", shape = "box"]
1521+
"[root] coder_metadata.example" -> "[root] null_resource.first"
1522+
}
1523+
}`, logger)
1524+
require.NoError(t, err)
1525+
require.Len(t, state.Resources, 2)
1526+
1527+
// Find the resources
1528+
var firstResource, secondResource *proto.Resource
1529+
for _, res := range state.Resources {
1530+
if res.Name == "first" && res.Type == "null_resource" {
1531+
firstResource = res
1532+
} else if res.Name == "second" && res.Type == "null_resource" {
1533+
secondResource = res
1534+
}
1535+
}
1536+
1537+
require.NotNil(t, firstResource)
1538+
require.NotNil(t, secondResource)
1539+
1540+
// The metadata should be on the second resource (as specified by resource_id),
1541+
// not the first one (which is the closest in the graph)
1542+
require.Len(t, firstResource.Metadata, 0, "first resource should have no metadata")
1543+
require.Len(t, secondResource.Metadata, 1, "second resource should have metadata")
1544+
require.Equal(t, "test", secondResource.Metadata[0].Key)
1545+
require.Equal(t, "value", secondResource.Metadata[0].Value)
1546+
})
1547+
1548+
t.Run("FallsBackToGraphWhenResourceIDNotFound", func(t *testing.T) {
1549+
t.Parallel()
1550+
ctx, logger := ctxAndLogger(t)
1551+
1552+
// Create a state where resource_id references a non-existent ID
1553+
state, err := terraform.ConvertState(ctx, []*tfjson.StateModule{{
1554+
Resources: []*tfjson.StateResource{{
1555+
Address: "null_resource.example",
1556+
Type: "null_resource",
1557+
Name: "example",
1558+
Mode: tfjson.ManagedResourceMode,
1559+
AttributeValues: map[string]interface{}{
1560+
"id": "example-resource-id",
1561+
},
1562+
}, {
1563+
Address: "coder_metadata.example",
1564+
Type: "coder_metadata",
1565+
Name: "example",
1566+
Mode: tfjson.ManagedResourceMode,
1567+
DependsOn: []string{"null_resource.example"},
1568+
AttributeValues: map[string]interface{}{
1569+
"resource_id": "non-existent-id",
1570+
"item": []interface{}{
1571+
map[string]interface{}{
1572+
"key": "test",
1573+
"value": "value",
1574+
},
1575+
},
1576+
},
1577+
}},
1578+
}}, `digraph {
1579+
compound = "true"
1580+
newrank = "true"
1581+
subgraph "root" {
1582+
"[root] null_resource.example" [label = "null_resource.example", shape = "box"]
1583+
"[root] coder_metadata.example" [label = "coder_metadata.example", shape = "box"]
1584+
"[root] coder_metadata.example" -> "[root] null_resource.example"
1585+
}
1586+
}`, logger)
1587+
require.NoError(t, err)
1588+
require.Len(t, state.Resources, 1)
1589+
1590+
// The metadata should still be applied via graph traversal
1591+
require.Equal(t, "example", state.Resources[0].Name)
1592+
require.Len(t, state.Resources[0].Metadata, 1)
1593+
require.Equal(t, "test", state.Resources[0].Metadata[0].Key)
1594+
require.Equal(t, "value", state.Resources[0].Metadata[0].Value)
1595+
1596+
// When resource_id is not found, it falls back to graph traversal
1597+
// We can't easily verify the warning was logged without access to the log capture API
1598+
})
1599+
1600+
t.Run("UsesGraphWhenResourceIDNotProvided", func(t *testing.T) {
1601+
t.Parallel()
1602+
ctx, logger := ctxAndLogger(t)
1603+
1604+
// Create a state without resource_id
1605+
state, err := terraform.ConvertState(ctx, []*tfjson.StateModule{{
1606+
Resources: []*tfjson.StateResource{{
1607+
Address: "null_resource.example",
1608+
Type: "null_resource",
1609+
Name: "example",
1610+
Mode: tfjson.ManagedResourceMode,
1611+
AttributeValues: map[string]interface{}{
1612+
"id": "example-resource-id",
1613+
},
1614+
}, {
1615+
Address: "coder_metadata.example",
1616+
Type: "coder_metadata",
1617+
Name: "example",
1618+
Mode: tfjson.ManagedResourceMode,
1619+
DependsOn: []string{"null_resource.example"},
1620+
AttributeValues: map[string]interface{}{
1621+
"item": []interface{}{
1622+
map[string]interface{}{
1623+
"key": "test",
1624+
"value": "value",
1625+
},
1626+
},
1627+
},
1628+
}},
1629+
}}, `digraph {
1630+
compound = "true"
1631+
newrank = "true"
1632+
subgraph "root" {
1633+
"[root] null_resource.example" [label = "null_resource.example", shape = "box"]
1634+
"[root] coder_metadata.example" [label = "coder_metadata.example", shape = "box"]
1635+
"[root] coder_metadata.example" -> "[root] null_resource.example"
1636+
}
1637+
}`, logger)
1638+
require.NoError(t, err)
1639+
require.Len(t, state.Resources, 1)
1640+
1641+
// The metadata should be applied via graph traversal
1642+
require.Equal(t, "example", state.Resources[0].Name)
1643+
require.Len(t, state.Resources[0].Metadata, 1)
1644+
require.Equal(t, "test", state.Resources[0].Metadata[0].Key)
1645+
require.Equal(t, "value", state.Resources[0].Metadata[0].Value)
1646+
})
1647+
}

0 commit comments

Comments
 (0)
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