Thanks to visit codestin.com
Credit goes to github.com

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)