diff --git a/.changes/v1.16/BUG FIXES-20260804-172309.yaml b/.changes/v1.16/BUG FIXES-20260804-172309.yaml new file mode 100644 index 000000000000..c24b0074f321 --- /dev/null +++ b/.changes/v1.16/BUG FIXES-20260804-172309.yaml @@ -0,0 +1,5 @@ +kind: BUG FIXES +body: 'stacks: Fixed validation to ensure the provider versions in the lock file and configuration are compatible.' +time: 2026-08-04T17:23:09.662989-04:00 +custom: + Issue: "38829" diff --git a/internal/rpcapi/stacks_test.go b/internal/rpcapi/stacks_test.go index 37ec72b8077d..b34c4cb21bb1 100644 --- a/internal/rpcapi/stacks_test.go +++ b/internal/rpcapi/stacks_test.go @@ -505,8 +505,8 @@ func TestStacksPlanStackChanges_noPolicies(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) stacksServer.providerDependencyLockOverride = lock @@ -580,8 +580,8 @@ func TestStacksPlanStackChanges_withPolicies(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) stacksServer.providerDependencyLockOverride = lock @@ -661,8 +661,8 @@ func TestStacksApplyStackChanges_noPolicies(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) stacksServer.providerDependencyLockOverride = lock @@ -781,8 +781,8 @@ func TestStacksApplyStackChanges_withPolicies(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) stacksServer.providerDependencyLockOverride = lock @@ -1424,8 +1424,8 @@ func TestStackChangeProgressDuringPlanNormal(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) stacksServer.providerDependencyLockOverride = lock @@ -1651,8 +1651,8 @@ func TestStackChangeProgressDuringPlanDestroy(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) stacksServer.providerDependencyLockOverride = lock @@ -1905,8 +1905,8 @@ func TestStackChangeProgressDuringApply(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) stacksServer.providerDependencyLockOverride = lock @@ -2392,8 +2392,8 @@ func TestStacksMigrateTerraformState(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) lockHandle := handles.NewDependencyLocks(lock) @@ -2610,7 +2610,7 @@ func policyEvaluationTestClient(t *testing.T) policy.Client { Alias: "default", Namespace: "hashicorp", Source: "registry.terraform.io/hashicorp/testing", - Version: "0.0.0", + Version: "0.1.0", } if diff := cmp.Diff(req.Meta, expectedMeta, protocmp.Transform()); diff != "" { t.Fatalf("unexpected provider metadata\n%s", diff) diff --git a/internal/stacks/stackconfig/provider_requirements.go b/internal/stacks/stackconfig/provider_requirements.go index 04bd3197eed0..d0d5c4999078 100644 --- a/internal/stacks/stackconfig/provider_requirements.go +++ b/internal/stacks/stackconfig/provider_requirements.go @@ -246,3 +246,15 @@ func (pr *ProviderRequirements) LocalNameForProvider(providerAddr addrs.Provider } return "", false } + +func (pr *ProviderRequirements) VersionConstraintsForProvider(providerAddr addrs.Provider) (constraints.IntersectionSpec, bool) { + if pr == nil { + return nil, false + } + for _, obj := range pr.Requirements { + if obj.Provider == providerAddr { + return obj.VersionConstraints, true + } + } + return nil, false +} diff --git a/internal/stacks/stackruntime/apply_destroy_test.go b/internal/stacks/stackruntime/apply_destroy_test.go index 16c7b6ab67bc..dcf0dfa08ed2 100644 --- a/internal/stacks/stackruntime/apply_destroy_test.go +++ b/internal/stacks/stackruntime/apply_destroy_test.go @@ -1762,8 +1762,8 @@ func TestApplyDestroy(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) diff --git a/internal/stacks/stackruntime/apply_test.go b/internal/stacks/stackruntime/apply_test.go index ff018ea79ee8..ad2591243b4c 100644 --- a/internal/stacks/stackruntime/apply_test.go +++ b/internal/stacks/stackruntime/apply_test.go @@ -2309,8 +2309,8 @@ After applying this plan, Terraform will no longer manage these objects. You wil lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -2518,8 +2518,8 @@ func TestApplyWithMovedResource(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -2696,8 +2696,8 @@ func TestApplyWithSensitivePropagation(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ @@ -3078,8 +3078,8 @@ func TestApplyWithFailedComponent(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ @@ -3200,8 +3200,8 @@ func TestApplyWithFailedProviderLinkedComponent(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ @@ -3317,8 +3317,8 @@ func TestApplyWithStateManipulation(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -3849,8 +3849,8 @@ func TestApplyWithChangedInputValues(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ @@ -3977,8 +3977,8 @@ func TestApplyAutomaticInputConversion(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ @@ -4146,8 +4146,8 @@ func TestApply_DependsOnComponentWithNoInstances(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -4243,8 +4243,8 @@ func TestApply_WithProviderFunctions(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -4478,8 +4478,8 @@ func TestApplyFailedDependencyWithResourceInState(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -4648,8 +4648,8 @@ func TestApplyManuallyRemovedResource(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -4809,8 +4809,8 @@ func TestApply_WithPolicyResults(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -4858,8 +4858,8 @@ func TestApply_WithPolicyResults_EmbeddedStack(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -4907,8 +4907,8 @@ func TestApply_WithPolicyResultsOnRefresh(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -4979,8 +4979,8 @@ func TestApply_WithPolicyResultsOnDestroy(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -5030,8 +5030,8 @@ func TestApply_WithPolicyResultsOnRemovedComponent(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) diff --git a/internal/stacks/stackruntime/helper_test.go b/internal/stacks/stackruntime/helper_test.go index 15e91a7f3ae8..34fb4b91116d 100644 --- a/internal/stacks/stackruntime/helper_test.go +++ b/internal/stacks/stackruntime/helper_test.go @@ -21,6 +21,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" @@ -618,3 +619,26 @@ func providerFunctionHashResult(value cty.Value) []byte { bytes := sha256.Sum256([]byte(value.GoString())) return bytes[:] } + +// buildVersionMismatchLock returns a Locks value with provider "testing" +// locked at 0.2.0, used to exercise version-mismatch diagnostics in tests. +func buildVersionMismatchLock() depsfile.Locks { + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.2.0"), + providerreqs.MustParseVersionConstraints("0.2.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + return *lock +} + +// hasDiagSummary reports whether any diagnostic in diags has the given summary. +func hasDiagSummary(diags tfdiags.Diagnostics, summary string) bool { + for _, diag := range diags { + if diag.Description().Summary == summary { + return true + } + } + return false +} diff --git a/internal/stacks/stackruntime/internal/stackeval/diagnostics_test.go b/internal/stacks/stackruntime/internal/stackeval/diagnostics_test.go index fbdb845a5b77..51c15aeddf27 100644 --- a/internal/stacks/stackruntime/internal/stackeval/diagnostics_test.go +++ b/internal/stacks/stackruntime/internal/stackeval/diagnostics_test.go @@ -44,8 +44,8 @@ func TestNamedPromisesPlan(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( providerAddrs, - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("1.0.0"), + providerreqs.MustParseVersionConstraints("1.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) diff --git a/internal/stacks/stackruntime/internal/stackeval/planning_test.go b/internal/stacks/stackruntime/internal/stackeval/planning_test.go index 12e0f8b7981e..b9476e0bef65 100644 --- a/internal/stacks/stackruntime/internal/stackeval/planning_test.go +++ b/internal/stacks/stackruntime/internal/stackeval/planning_test.go @@ -941,8 +941,8 @@ func TestPlanning_LocalsDataSource(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.0.1"), + providerreqs.MustParseVersionConstraints("0.0.1"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) diff --git a/internal/stacks/stackruntime/internal/stackeval/provider_config.go b/internal/stacks/stackruntime/internal/stackeval/provider_config.go index 33c49ade4e4d..47cf91c25a4a 100644 --- a/internal/stacks/stackruntime/internal/stackeval/provider_config.go +++ b/internal/stacks/stackruntime/internal/stackeval/provider_config.go @@ -13,8 +13,10 @@ import ( "github.com/hashicorp/hcl/v2/hcldec" "github.com/zclconf/go-cty/cty" + "github.com/apparentlymart/go-versions/versions/constraints" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/promising" @@ -76,12 +78,13 @@ func (p *ProviderConfig) ProviderArgs(ctx context.Context, phase EvalPhase) cty. return v } -func CheckProviderInLockfile(locks depsfile.Locks, providerType *ProviderType, declRange *hcl.Range) (diags tfdiags.Diagnostics) { +func CheckProviderInLockfile(locks depsfile.Locks, providerType *ProviderType, versionConstraints constraints.IntersectionSpec, declRange *hcl.Range) (diags tfdiags.Diagnostics) { if !depsfile.ProviderIsLockable(providerType.Addr()) { return diags } - if p := locks.Provider(providerType.Addr()); p == nil { + lock := locks.Provider(providerType.Addr()) + if lock == nil { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Provider missing from lockfile", @@ -91,7 +94,26 @@ func CheckProviderInLockfile(locks depsfile.Locks, providerType *ProviderType, d ), Subject: declRange, }) + return diags } + + if len(versionConstraints) > 0 { + selectedVersion := lock.Version() + allowedVersions := providerreqs.MeetingConstraints(versionConstraints) + if !allowedVersions.Has(selectedVersion) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Provider version doesn't match the lockfile", + Detail: fmt.Sprintf( + "Provider %q is locked at version %s in the dependency lockfile, but the configuration's version constraints (%s) do not allow that version. This usually means the version constraints were changed after the lockfile was generated. Please run `terraform stacks providers lock` to update the lockfile and run this operation again with an updated configuration.", + providerType.Addr(), selectedVersion.String(), + providerreqs.VersionConstraintsString(versionConstraints), + ), + Subject: declRange, + }) + } + } + return diags } @@ -106,9 +128,11 @@ func (p *ProviderConfig) CheckProviderArgs(ctx context.Context, phase EvalPhase) depLocks := p.main.DependencyLocks(phase) if depLocks != nil { - // Check if the provider is in the lockfile, - // if it is not we can not read the provider schema - lockfileDiags := CheckProviderInLockfile(*depLocks, providerType, decl.DeclRange.ToHCL().Ptr()) + // Check that the provider is in the lockfile (we can't read + // the provider schema otherwise) and that the version recorded + // there still satisfies the configuration's version constraints. + versionConstraints, _ := p.stack.config.Stack.RequiredProviders.VersionConstraintsForProvider(providerType.Addr()) + lockfileDiags := CheckProviderInLockfile(*depLocks, providerType, versionConstraints, decl.DeclRange.ToHCL().Ptr()) if lockfileDiags.HasErrors() { return cty.DynamicVal, lockfileDiags } diff --git a/internal/stacks/stackruntime/internal/stackeval/provider_config_test.go b/internal/stacks/stackruntime/internal/stackeval/provider_config_test.go index 17caea609c32..e51dfe97baf9 100644 --- a/internal/stacks/stackruntime/internal/stackeval/provider_config_test.go +++ b/internal/stacks/stackruntime/internal/stackeval/provider_config_test.go @@ -12,8 +12,11 @@ import ( "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" + "github.com/apparentlymart/go-versions/versions/constraints" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" "github.com/hashicorp/terraform/internal/providers" testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" @@ -218,3 +221,79 @@ func TestProviderConfig_CheckProviderArgs(t *testing.T) { } }) } + +func TestCheckProviderInLockfile(t *testing.T) { + providerAddr := addrs.NewDefaultProvider("testing") + providerType := newProviderType(nil, providerAddr) + + mustConstraints := func(s string) constraints.IntersectionSpec { + spec, err := constraints.ParseRubyStyleMulti(s) + if err != nil { + t.Fatalf("invalid constraint %q: %s", s, err) + } + return spec + } + + // A lock file that records version 1.2.0 for the testing provider. + lockedLocks := depsfile.NewLocks() + lockedLocks.SetProvider( + providerAddr, + providerreqs.MustParseVersion("1.2.0"), + providerreqs.MustParseVersionConstraints("~> 1.2.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + tests := map[string]struct { + locks *depsfile.Locks + constraints constraints.IntersectionSpec + wantSummary string + }{ + "present and satisfies constraints": { + locks: lockedLocks, + constraints: mustConstraints("~> 1.2"), + wantSummary: "", + }, + "present with no constraints": { + locks: lockedLocks, + constraints: nil, + wantSummary: "", + }, + "present but constraints no longer allow locked version": { + locks: lockedLocks, + constraints: mustConstraints(">= 2.0.0"), + wantSummary: "Provider version doesn't match the lockfile", + }, + "missing from lockfile": { + locks: depsfile.NewLocks(), + constraints: mustConstraints("~> 1.2"), + wantSummary: "Provider missing from lockfile", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + diags := CheckProviderInLockfile(*tc.locks, providerType, tc.constraints, nil) + + if tc.wantSummary == "" { + if diags.HasErrors() { + t.Fatalf("unexpected diagnostics:\n%s", diags.Err()) + } + return + } + + if !diags.HasErrors() { + t.Fatalf("expected error %q, got none", tc.wantSummary) + } + found := false + for _, diag := range diags { + if diag.Description().Summary == tc.wantSummary { + found = true + break + } + } + if !found { + t.Fatalf("expected diagnostic %q, got:\n%s", tc.wantSummary, diags.Err()) + } + }) + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/provider_expressions.go b/internal/stacks/stackruntime/internal/stackeval/provider_expressions.go index ff25e95157fc..e56253106073 100644 --- a/internal/stacks/stackruntime/internal/stackeval/provider_expressions.go +++ b/internal/stacks/stackruntime/internal/stackeval/provider_expressions.go @@ -426,8 +426,11 @@ func neededProviderSchemas[Addr any](ctx context.Context, main *Main, phase Eval depLocks := main.DependencyLocks(phase) if depLocks != nil { // Check if the provider is in the lockfile, - // if it is not we can not read the provider schema - providerLockfileDiags := CheckProviderInLockfile(*depLocks, pTy, scope.DeclRange()) + // if it is not we can not read the provider schema. The + // version-constraint check is handled where the stack-level + // required_providers constraints are available (see + // ProviderConfig.CheckProviderArgs), so we pass nil here. + providerLockfileDiags := CheckProviderInLockfile(*depLocks, pTy, nil, scope.DeclRange()) // We report these diagnostics in a different place if providerLockfileDiags.HasErrors() { diff --git a/internal/stacks/stackruntime/internal/stackeval/stack_config.go b/internal/stacks/stackruntime/internal/stackeval/stack_config.go index 5390e9771bb9..888990d2f888 100644 --- a/internal/stacks/stackruntime/internal/stackeval/stack_config.go +++ b/internal/stacks/stackruntime/internal/stackeval/stack_config.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackplan" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -51,6 +52,7 @@ type StackConfig struct { var ( _ ExpressionScope = (*StackConfig)(nil) + _ StaticEvaler = (*StackConfig)(nil) ) func newStackConfig(main *Main, addr stackaddrs.Stack, parent *StackConfig, config *stackconfig.ConfigNode) *StackConfig { @@ -614,3 +616,71 @@ func (s *StackConfig) ExternalFunctions(ctx context.Context) (lang.ExternalFuncs func (s *StackConfig) PlanTimestamp() time.Time { return s.main.PlanTimestamp() } + +// checkRequiredProviders validates that every provider declared in the +// required_providers block that does NOT have an explicit "provider" block +// in this stack is still present in the lock file and that the locked version +// satisfies the declared version constraints. +// +// Providers that DO have an explicit "provider" block are validated separately +// by ProviderConfig.checkValid, so we skip them here to avoid double-reporting. +func (s *StackConfig) checkRequiredProviders(phase EvalPhase) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + depLocks := s.main.DependencyLocks(phase) + if depLocks == nil { + return diags + } + + reqs := s.config.Stack.RequiredProviders + if reqs == nil { + return diags + } + + for _, req := range reqs.Requirements { + providerAddr := req.Provider + + // Skip providers that already have an explicit "provider" block — those + // are checked by ProviderConfig.checkValid which runs separately. + // A stack can have multiple "provider" blocks for the same type (using + // different names/aliases), so we just need to know if any exist. + localName, ok := reqs.LocalNameForProvider(providerAddr) + if !ok { + continue + } + hasProviderBlock := false + for cfgAddr := range s.config.Stack.ProviderConfigs { + if cfgAddr.LocalName == localName { + hasProviderBlock = true + break + } + } + if hasProviderBlock { + continue + } + + pTy := s.main.ProviderType(providerAddr) + lockfileDiags := CheckProviderInLockfile(*depLocks, pTy, req.VersionConstraints, nil) + diags = diags.Append(lockfileDiags) + } + + return diags +} + +// tracingName implements StaticEvaler. +func (s *StackConfig) tracingName() string { + if s.addr.IsRoot() { + return "root stack" + } + return s.addr.String() +} + +// Validate implements Validatable. +func (s *StackConfig) Validate(_ context.Context) tfdiags.Diagnostics { + return s.checkRequiredProviders(ValidatePhase) +} + +// PlanChanges implements Plannable. +func (s *StackConfig) PlanChanges(_ context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + return nil, s.checkRequiredProviders(PlanPhase) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/walk_static.go b/internal/stacks/stackruntime/internal/stackeval/walk_static.go index 23a1bbbf94d7..33fa8904320c 100644 --- a/internal/stacks/stackruntime/internal/stackeval/walk_static.go +++ b/internal/stacks/stackruntime/internal/stackeval/walk_static.go @@ -43,6 +43,11 @@ func walkStaticObjectsInStackConfig[Output any]( stackConfig *StackConfig, visit func(ctx context.Context, walk *walkWithOutput[Output], obj StaticEvaler), ) { + // Visit the StackConfig itself so it can validate required_providers entries + // that don't have an explicit "provider" block (those are checked by + // ProviderConfig, but pass-through providers are only declared here). + visit(ctx, walk, stackConfig) + for _, obj := range stackConfig.InputVariables() { visit(ctx, walk, obj) } diff --git a/internal/stacks/stackruntime/plan_refresh_test.go b/internal/stacks/stackruntime/plan_refresh_test.go index a7c02fd65ea6..13a4822ce44c 100644 --- a/internal/stacks/stackruntime/plan_refresh_test.go +++ b/internal/stacks/stackruntime/plan_refresh_test.go @@ -290,8 +290,8 @@ func TestRefreshPlan(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) diff --git a/internal/stacks/stackruntime/plan_test.go b/internal/stacks/stackruntime/plan_test.go index 9ff111aa4230..19e3693623ea 100644 --- a/internal/stacks/stackruntime/plan_test.go +++ b/internal/stacks/stackruntime/plan_test.go @@ -64,14 +64,14 @@ func TestPlan_valid(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) lock.SetProvider( addrs.NewDefaultProvider("other"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -133,8 +133,8 @@ func TestPlan_invalid(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -1422,8 +1422,8 @@ func TestPlan(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -1660,8 +1660,8 @@ func TestPlanWithComplexVariableDefaults(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ @@ -2542,8 +2542,8 @@ func TestPlanWithProviderConfig(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( providerAddr, - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("1.0.0"), + providerreqs.MustParseVersionConstraints("1.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -2730,8 +2730,8 @@ func TestPlanWithSensitivePropagation(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ @@ -2895,8 +2895,8 @@ func TestPlanWithSensitivePropagationNested(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ @@ -3056,8 +3056,8 @@ func TestPlanWithForEach(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ @@ -3106,8 +3106,8 @@ func TestPlanWithCheckableObjects(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ @@ -3339,8 +3339,8 @@ func TestPlanWithDeferredResource(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ @@ -3493,8 +3493,8 @@ func TestPlanWithDeferredComponentForEach(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ @@ -3732,8 +3732,8 @@ func TestPlanWithDeferredComponentReferences(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ @@ -3990,8 +3990,8 @@ func TestPlanWithDeferredComponentForEachOfInvalidType(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ @@ -4052,8 +4052,8 @@ func TestPlanWithDeferredProviderForEach(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ @@ -4267,8 +4267,8 @@ func TestPlanInvalidProvidersFailGracefully(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ @@ -4333,8 +4333,8 @@ func TestPlanWithStateManipulation(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -4958,8 +4958,8 @@ func TestPlan_DependsOnUpdatesRequirements(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -5201,8 +5201,8 @@ func TestPlan_RemovedBlocks(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -6148,8 +6148,8 @@ func TestPlanWithResourceIdentities(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -6248,8 +6248,8 @@ func TestPlanInvalidLocalValue(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -6499,8 +6499,8 @@ func TestPlanWithDeferredActionInvocation(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ @@ -6562,8 +6562,8 @@ func TestPlan_variableValidationAdvanced(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -6919,8 +6919,8 @@ func TestPlan_WithPolicyResults(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -6955,8 +6955,8 @@ func TestPlan_WithPolicyResults_EmbeddedStack(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -6991,8 +6991,8 @@ func TestPlan_WithPolicyResultsOnRefresh(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -7028,8 +7028,8 @@ func TestPlan_WithPolicyResultsOnDestroy(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -7067,8 +7067,8 @@ func TestPlan_WithPolicyResultsOnRemovedComponent(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -7144,8 +7144,8 @@ func TestPlan_NoPolicyResultsOnDeferredResource(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -7410,7 +7410,7 @@ func policyEvaluationTestClient(t *testing.T) *policy.MockClient { Alias: "default", Namespace: "hashicorp", Source: "registry.terraform.io/hashicorp/testing", - Version: "0.0.0", + Version: "0.1.0", } if diff := cmp.Diff(req.Meta, expectedMeta, protocmp.Transform()); diff != "" { t.Fatalf("unexpected provider metadata\n%s", diff) @@ -7652,3 +7652,69 @@ func planAndCollectPolicyResults(t *testing.T, ctx context.Context, req PlanRequ return gotPolicyResults } + +// TestPlan_versionMismatch verifies that a version mismatch between the lock +// file and required_providers is reported as an error. Two scenarios are +// tested: a stack with an explicit "provider" block, and one where the provider +// is only declared in required_providers and passed through to an embedded stack. +func TestPlan_versionMismatch(t *testing.T) { + cases := []struct { + name string + configDir string + fatalMsg string + providerFactory providers.Factory + }{ + { + // "with-single-input/valid" has both required_providers and a + // provider block (direct code path through ProviderConfig.checkValid). + name: "withProviderBlock", + configDir: "with-single-input/valid", + fatalMsg: "expected version mismatch error, got none", + providerFactory: func() (providers.Interface, error) { + return &default_testing_provider.MockProvider{}, nil + }, + }, + { + // "policy-evaluation-embedded-stack" has required_providers in the + // root stack config but the provider block lives only in the + // embedded stack. + name: "passThroughProvider", + configDir: "policy-evaluation-embedded-stack", + fatalMsg: "expected version mismatch error for pass-through provider, got none", + providerFactory: func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, tc.configDir) + // Lock says 0.2.0, but configs say version = "0.1.0". + lock := buildVersionMismatchLock() + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + req := PlanRequest{ + Config: cfg, + DependencyLocks: lock, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): tc.providerFactory, + }, + InputValues: make(map[stackaddrs.InputVariable]ExternalInputValue), + } + resp := PlanResponse{PlannedChanges: changesCh, Diagnostics: diagsCh} + + go Plan(ctx, &req, &resp) + _, gotDiags := collectPlanOutput(changesCh, diagsCh) + + if !gotDiags.HasErrors() { + t.Fatal(tc.fatalMsg) + } + if !hasDiagSummary(gotDiags, "Provider version doesn't match the lockfile") { + t.Fatalf("expected 'Provider version doesn't match the lockfile', got:\n%s", gotDiags.Err()) + } + }) + } +} diff --git a/internal/stacks/stackruntime/validate_test.go b/internal/stacks/stackruntime/validate_test.go index 71a1fc09c4e1..6d8fff6b2d2a 100644 --- a/internal/stacks/stackruntime/validate_test.go +++ b/internal/stacks/stackruntime/validate_test.go @@ -349,14 +349,14 @@ func TestValidate_valid(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) lock.SetProvider( addrs.NewDefaultProvider("other"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -396,14 +396,14 @@ func TestValidate_invalid(t *testing.T) { lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) lock.SetProvider( addrs.NewDefaultProvider("other"), - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) @@ -535,8 +535,8 @@ Terraform uses references to decide a suitable order for performing operations, for addr := range tc.providers { locks.SetProvider( addr, - providerreqs.MustParseVersion("0.0.0"), - providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.MustParseVersion("0.1.0"), + providerreqs.MustParseVersionConstraints("0.1.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) } @@ -553,3 +553,59 @@ Terraform uses references to decide a suitable order for performing operations, }) } } + +// TestValidate_versionMismatch verifies that a version mismatch between the +// lock file and required_providers is reported as an error during validation. +// Two scenarios are tested: a stack with an explicit "provider" block, and one +// where the provider is only declared in required_providers and passed through +// to an embedded stack. +func TestValidate_versionMismatch(t *testing.T) { + cases := []struct { + name string + configDir string + fatalMsg string + }{ + { + // "with-single-input/valid" has both required_providers and a + // provider block (direct code path through ProviderConfig.checkValid). + name: "withProviderBlock", + configDir: "with-single-input/valid", + fatalMsg: "expected version mismatch error, got none", + }, + { + // "policy-evaluation-embedded-stack" has required_providers in the + // root stack config but the provider block lives only in the + // embedded stack. + name: "passThroughProvider", + configDir: "policy-evaluation-embedded-stack", + fatalMsg: "expected version mismatch error for pass-through provider, got none", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, tc.configDir) + // Lock says 0.2.0, but configs say version = "0.1.0". + lock := buildVersionMismatchLock() + + gotDiags := Validate(ctx, &ValidateRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: lock, + ExperimentsAllowed: true, + }) + + if !gotDiags.HasErrors() { + t.Fatal(tc.fatalMsg) + } + if !hasDiagSummary(gotDiags, "Provider version doesn't match the lockfile") { + t.Fatalf("expected 'Provider version doesn't match the lockfile', got:\n%s", gotDiags.Err()) + } + }) + } +}