diff --git a/internal/stacks/stackconfig/config.go b/internal/stacks/stackconfig/config.go index 883b52f40e8f..f72082436396 100644 --- a/internal/stacks/stackconfig/config.go +++ b/internal/stacks/stackconfig/config.go @@ -391,6 +391,17 @@ func resolveFinalSourceAddr(base sourceaddrs.FinalSource, rel sourceaddrs.Source } finalRel := rel.Versioned(selectedVersion) return sourceaddrs.ResolveRelativeFinalSource(base, finalRel) + + case sourceaddrs.ComponentSource: + // Component registry sources work similar to module registry sources + allowedVersions := versions.MeetingConstraints(versionConstraints) + availableVersions := sources.ComponentPackageVersions(rel.Package()) + selectedVersion := availableVersions.NewestInSet(allowedVersions) + if selectedVersion == versions.Unspecified { + return nil, fmt.Errorf("no cached versions of %s match the given version constraints", rel.Package()) + } + finalRel := rel.Versioned(selectedVersion) + return sourceaddrs.ResolveRelativeFinalSource(base, finalRel) default: // Should not get here because the above cases should be exhaustive // for all implementations of sourceaddrs.Source. diff --git a/internal/stacks/stackconfig/config_test.go b/internal/stacks/stackconfig/config_test.go index 6af08c8be0fc..8875711fbf6c 100644 --- a/internal/stacks/stackconfig/config_test.go +++ b/internal/stacks/stackconfig/config_test.go @@ -284,3 +284,85 @@ func TestOmittingBuiltInProviders(t *testing.T) { }) }) } + +func TestComponentSourceResolution(t *testing.T) { + bundle, err := sourcebundle.OpenDir("testdata/basics-bundle") + if err != nil { + t.Fatal(err) + } + + rootAddr := sourceaddrs.MustParseSource("git::https://example.com/component-test.git").(sourceaddrs.RemoteSource) + config, diags := LoadConfigDir(rootAddr, bundle) + if len(diags) != 0 { + t.Fatalf("unexpected diagnostics:\n%s", diags.NonFatalErr().Error()) + } + + t.Run("component source resolution", func(t *testing.T) { + // Verify that the component was loaded + if got, want := len(config.Root.Stack.Components), 1; got != want { + t.Errorf("wrong number of components %d; want %d", got, want) + } + + t.Run("pet-nulls component", func(t *testing.T) { + cmpn, ok := config.Root.Stack.Components["pet-nulls"] + if !ok { + t.Fatal("Root stack config has no component named \"pet-nulls\".") + } + + // Verify component name + if got, want := cmpn.Name, "pet-nulls"; got != want { + t.Errorf("wrong component name\ngot: %s\nwant: %s", got, want) + } + + // Verify that the source address was parsed correctly + componentSource, ok := cmpn.SourceAddr.(sourceaddrs.ComponentSource) + if !ok { + t.Fatalf("expected ComponentSource, got %T", cmpn.SourceAddr) + } + + expectedSourceStr := "app.staging.terraform.io/component-configurations/pet-nulls" + if got := componentSource.String(); got != expectedSourceStr { + t.Errorf("wrong source address\ngot: %s\nwant: %s", got, expectedSourceStr) + } + + // Verify that version constraints were parsed + if cmpn.VersionConstraints == nil { + t.Fatal("component has no version constraints") + } + + // Verify that the final source address was resolved + if cmpn.FinalSourceAddr == nil { + t.Fatal("component FinalSourceAddr was not resolved") + } + + // The final source should be a ComponentSourceFinal + componentSourceFinal, ok := cmpn.FinalSourceAddr.(sourceaddrs.ComponentSourceFinal) + if !ok { + t.Fatalf("expected ComponentSourceFinal for FinalSourceAddr, got %T", cmpn.FinalSourceAddr) + } + + // Verify it resolved to the correct version (0.0.2) + expectedVersion := "0.0.2" + if got := componentSourceFinal.SelectedVersion().String(); got != expectedVersion { + t.Errorf("wrong selected version\ngot: %s\nwant: %s", got, expectedVersion) + } + + // Verify the unversioned component source matches + if got := componentSourceFinal.Unversioned().String(); got != expectedSourceStr { + t.Errorf("wrong unversioned source in final address\ngot: %s\nwant: %s", got, expectedSourceStr) + } + + // Verify we can get the local path from the bundle + localPath, err := bundle.LocalPathForSource(cmpn.FinalSourceAddr) + if err != nil { + t.Fatalf("failed to get local path for component source: %s", err) + } + + // The local path should point to the pet-nulls directory + if localPath == "" { + t.Error("local path is empty") + } + t.Logf("Component resolved to local path: %s", localPath) + }) + }) +} diff --git a/internal/stacks/stackconfig/testdata/basics-bundle/component-test/main.tfcomponent.hcl b/internal/stacks/stackconfig/testdata/basics-bundle/component-test/main.tfcomponent.hcl new file mode 100644 index 000000000000..e7c05608b4b0 --- /dev/null +++ b/internal/stacks/stackconfig/testdata/basics-bundle/component-test/main.tfcomponent.hcl @@ -0,0 +1,9 @@ +component "pet-nulls" { + source = "app.staging.terraform.io/component-configurations/pet-nulls" + version = "0.0.2" + + inputs = { + instances = var.instances + prefix = var.prefix + } +} diff --git a/internal/stacks/stackconfig/testdata/basics-bundle/component-test/variables.tfcomponent.hcl b/internal/stacks/stackconfig/testdata/basics-bundle/component-test/variables.tfcomponent.hcl new file mode 100644 index 000000000000..1b6fb4a69643 --- /dev/null +++ b/internal/stacks/stackconfig/testdata/basics-bundle/component-test/variables.tfcomponent.hcl @@ -0,0 +1,7 @@ +variable "instances" { + type = number +} + +variable "prefix" { + type = string +} diff --git a/internal/stacks/stackconfig/testdata/basics-bundle/pet-nulls/main.tf b/internal/stacks/stackconfig/testdata/basics-bundle/pet-nulls/main.tf new file mode 100644 index 000000000000..22a8feabf6d3 --- /dev/null +++ b/internal/stacks/stackconfig/testdata/basics-bundle/pet-nulls/main.tf @@ -0,0 +1,15 @@ +variable "instances" { + type = number +} + +variable "prefix" { + type = string +} + +resource "null_resource" "pet" { + count = var.instances +} + +output "pet_ids" { + value = null_resource.pet[*].id +} diff --git a/internal/stacks/stackconfig/testdata/basics-bundle/terraform-sources.json b/internal/stacks/stackconfig/testdata/basics-bundle/terraform-sources.json index 4d79b35dd0be..fbe5268dad24 100644 --- a/internal/stacks/stackconfig/testdata/basics-bundle/terraform-sources.json +++ b/internal/stacks/stackconfig/testdata/basics-bundle/terraform-sources.json @@ -30,6 +30,16 @@ "source": "git::https://example.com/builtin.git", "local": "builtin", "meta": {} + }, + { + "source": "git::https://example.com/component-test.git", + "local": "component-test", + "meta": {} + }, + { + "source": "git::https://example.com/pet-nulls.git?ref=v0.0.2", + "local": "pet-nulls", + "meta": {} } ], "registry": [ @@ -44,5 +54,18 @@ } } } + ], + "components": [ + { + "source": "app.staging.terraform.io/component-configurations/pet-nulls", + "versions": { + "0.0.1": { + "source": "git::https://example.com/pet-nulls.git?ref=v0.0.1" + }, + "0.0.2": { + "source": "git::https://example.com/pet-nulls.git?ref=v0.0.2" + } + } + } ] } \ No newline at end of file