Skip to content

Commit 2c4787c

Browse files
jpogran-hashimjyocca
authored andcommitted
Add component registry source resolution support to Terraform Stacks
This change implements the missing component source resolution case in the stack configuration loader, enabling Terraform Stacks to properly handle component registry sources from HCP Terraform and other component registries. The implementation mirrors the existing module registry resolution workflow, where component sources are first resolved to their versioned form using the source bundle's component metadata, then converted to final source addresses that can be used to locate the actual component code. This completes the integration between the terraform-registry-address component parsing capabilities and the go-slug sourcebundle component resolution APIs.
1 parent fe70f0a commit 2c4787c

File tree

6 files changed

+147
-0
lines changed

6 files changed

+147
-0
lines changed

internal/stacks/stackconfig/config.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,17 @@ func resolveFinalSourceAddr(base sourceaddrs.FinalSource, rel sourceaddrs.Source
391391
}
392392
finalRel := rel.Versioned(selectedVersion)
393393
return sourceaddrs.ResolveRelativeFinalSource(base, finalRel)
394+
395+
case sourceaddrs.ComponentSource:
396+
// Component registry sources work similar to module registry sources
397+
allowedVersions := versions.MeetingConstraints(versionConstraints)
398+
availableVersions := sources.ComponentPackageVersions(rel.Package())
399+
selectedVersion := availableVersions.NewestInSet(allowedVersions)
400+
if selectedVersion == versions.Unspecified {
401+
return nil, fmt.Errorf("no cached versions of %s match the given version constraints", rel.Package())
402+
}
403+
finalRel := rel.Versioned(selectedVersion)
404+
return sourceaddrs.ResolveRelativeFinalSource(base, finalRel)
394405
default:
395406
// Should not get here because the above cases should be exhaustive
396407
// for all implementations of sourceaddrs.Source.

internal/stacks/stackconfig/config_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,3 +284,85 @@ func TestOmittingBuiltInProviders(t *testing.T) {
284284
})
285285
})
286286
}
287+
288+
func TestComponentSourceResolution(t *testing.T) {
289+
bundle, err := sourcebundle.OpenDir("testdata/basics-bundle")
290+
if err != nil {
291+
t.Fatal(err)
292+
}
293+
294+
rootAddr := sourceaddrs.MustParseSource("git::https://example.com/component-test.git").(sourceaddrs.RemoteSource)
295+
config, diags := LoadConfigDir(rootAddr, bundle)
296+
if len(diags) != 0 {
297+
t.Fatalf("unexpected diagnostics:\n%s", diags.NonFatalErr().Error())
298+
}
299+
300+
t.Run("component source resolution", func(t *testing.T) {
301+
// Verify that the component was loaded
302+
if got, want := len(config.Root.Stack.Components), 1; got != want {
303+
t.Errorf("wrong number of components %d; want %d", got, want)
304+
}
305+
306+
t.Run("pet-nulls component", func(t *testing.T) {
307+
cmpn, ok := config.Root.Stack.Components["pet-nulls"]
308+
if !ok {
309+
t.Fatal("Root stack config has no component named \"pet-nulls\".")
310+
}
311+
312+
// Verify component name
313+
if got, want := cmpn.Name, "pet-nulls"; got != want {
314+
t.Errorf("wrong component name\ngot: %s\nwant: %s", got, want)
315+
}
316+
317+
// Verify that the source address was parsed correctly
318+
componentSource, ok := cmpn.SourceAddr.(sourceaddrs.ComponentSource)
319+
if !ok {
320+
t.Fatalf("expected ComponentSource, got %T", cmpn.SourceAddr)
321+
}
322+
323+
expectedSourceStr := "app.staging.terraform.io/component-configurations/pet-nulls"
324+
if got := componentSource.String(); got != expectedSourceStr {
325+
t.Errorf("wrong source address\ngot: %s\nwant: %s", got, expectedSourceStr)
326+
}
327+
328+
// Verify that version constraints were parsed
329+
if cmpn.VersionConstraints == nil {
330+
t.Fatal("component has no version constraints")
331+
}
332+
333+
// Verify that the final source address was resolved
334+
if cmpn.FinalSourceAddr == nil {
335+
t.Fatal("component FinalSourceAddr was not resolved")
336+
}
337+
338+
// The final source should be a ComponentSourceFinal
339+
componentSourceFinal, ok := cmpn.FinalSourceAddr.(sourceaddrs.ComponentSourceFinal)
340+
if !ok {
341+
t.Fatalf("expected ComponentSourceFinal for FinalSourceAddr, got %T", cmpn.FinalSourceAddr)
342+
}
343+
344+
// Verify it resolved to the correct version (0.0.2)
345+
expectedVersion := "0.0.2"
346+
if got := componentSourceFinal.SelectedVersion().String(); got != expectedVersion {
347+
t.Errorf("wrong selected version\ngot: %s\nwant: %s", got, expectedVersion)
348+
}
349+
350+
// Verify the unversioned component source matches
351+
if got := componentSourceFinal.Unversioned().String(); got != expectedSourceStr {
352+
t.Errorf("wrong unversioned source in final address\ngot: %s\nwant: %s", got, expectedSourceStr)
353+
}
354+
355+
// Verify we can get the local path from the bundle
356+
localPath, err := bundle.LocalPathForSource(cmpn.FinalSourceAddr)
357+
if err != nil {
358+
t.Fatalf("failed to get local path for component source: %s", err)
359+
}
360+
361+
// The local path should point to the pet-nulls directory
362+
if localPath == "" {
363+
t.Error("local path is empty")
364+
}
365+
t.Logf("Component resolved to local path: %s", localPath)
366+
})
367+
})
368+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
component "pet-nulls" {
2+
source = "app.staging.terraform.io/component-configurations/pet-nulls"
3+
version = "0.0.2"
4+
5+
inputs = {
6+
instances = var.instances
7+
prefix = var.prefix
8+
}
9+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
variable "instances" {
2+
type = number
3+
}
4+
5+
variable "prefix" {
6+
type = string
7+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
variable "instances" {
2+
type = number
3+
}
4+
5+
variable "prefix" {
6+
type = string
7+
}
8+
9+
resource "null_resource" "pet" {
10+
count = var.instances
11+
}
12+
13+
output "pet_ids" {
14+
value = null_resource.pet[*].id
15+
}

internal/stacks/stackconfig/testdata/basics-bundle/terraform-sources.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@
3030
"source": "git::https://example.com/builtin.git",
3131
"local": "builtin",
3232
"meta": {}
33+
},
34+
{
35+
"source": "git::https://example.com/component-test.git",
36+
"local": "component-test",
37+
"meta": {}
38+
},
39+
{
40+
"source": "git::https://example.com/pet-nulls.git?ref=v0.0.2",
41+
"local": "pet-nulls",
42+
"meta": {}
3343
}
3444
],
3545
"registry": [
@@ -44,5 +54,18 @@
4454
}
4555
}
4656
}
57+
],
58+
"components": [
59+
{
60+
"source": "app.staging.terraform.io/component-configurations/pet-nulls",
61+
"versions": {
62+
"0.0.1": {
63+
"source": "git::https://example.com/pet-nulls.git?ref=v0.0.1"
64+
},
65+
"0.0.2": {
66+
"source": "git::https://example.com/pet-nulls.git?ref=v0.0.2"
67+
}
68+
}
69+
}
4770
]
4871
}

0 commit comments

Comments
 (0)