From 80b0875ef37757083a381ee8c2b799958f02d869 Mon Sep 17 00:00:00 2001 From: Arun Ramakrishnan Date: Mon, 6 Jul 2026 14:51:02 -0400 Subject: [PATCH 1/9] Validate provider versions in config match the lock file --- .../stackconfig/provider_requirements.go | 17 ++++ .../internal/stackeval/provider_config.go | 40 ++++++++-- .../stackeval/provider_config_test.go | 79 +++++++++++++++++++ .../stackeval/provider_expressions.go | 7 +- 4 files changed, 136 insertions(+), 7 deletions(-) diff --git a/internal/stacks/stackconfig/provider_requirements.go b/internal/stacks/stackconfig/provider_requirements.go index 04bd3197eed0..9e748d70fc8c 100644 --- a/internal/stacks/stackconfig/provider_requirements.go +++ b/internal/stacks/stackconfig/provider_requirements.go @@ -246,3 +246,20 @@ func (pr *ProviderRequirements) LocalNameForProvider(providerAddr addrs.Provider } return "", false } + +// VersionConstraintsForProvider returns the version constraints declared for +// the given provider in this required_providers block. +// +// The second return value is false if the provider is not declared in this +// required_providers block, in which case the first return value is nil. +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/internal/stackeval/provider_config.go b/internal/stacks/stackruntime/internal/stackeval/provider_config.go index 33c49ade4e4d..1ce0607859d9 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,32 @@ func CheckProviderInLockfile(locks depsfile.Locks, providerType *ProviderType, d ), Subject: declRange, }) + return diags } + + // If the configuration declares version constraints for this provider, + // make sure the version recorded in the lockfile still satisfies them. + // A mismatch here typically means the version constraints in the + // configuration were changed after the lockfile was generated, leaving the + // lockfile inconsistent with the configuration. We must catch this so that + // we don't proceed with an out-of-date set of provider selections. + 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 +134,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() { From b9d1ffe463c9cd7c2ba9cf98a0ea7ad2e0f60144 Mon Sep 17 00:00:00 2001 From: Arun Ramakrishnan Date: Mon, 6 Jul 2026 14:51:24 -0400 Subject: [PATCH 2/9] Update tests to support provider version validations --- internal/rpcapi/stacks_test.go | 34 ++--- .../stacks/stackruntime/apply_destroy_test.go | 4 +- internal/stacks/stackruntime/apply_test.go | 68 +++++----- .../internal/stackeval/diagnostics_test.go | 4 +- .../internal/stackeval/planning_test.go | 4 +- .../stacks/stackruntime/plan_refresh_test.go | 4 +- internal/stacks/stackruntime/plan_test.go | 118 +++++++++--------- internal/stacks/stackruntime/validate_test.go | 20 +-- 8 files changed, 128 insertions(+), 128 deletions(-) diff --git a/internal/rpcapi/stacks_test.go b/internal/rpcapi/stacks_test.go index fb9443674894..8feb080a60a9 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/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 a8ee9f019a4b..2906b482ebae 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/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/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 c6545318467e..4e74c5bf9faa 100644 --- a/internal/stacks/stackruntime/plan_test.go +++ b/internal/stacks/stackruntime/plan_test.go @@ -65,14 +65,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{}), ) @@ -134,8 +134,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{}), ) @@ -1423,8 +1423,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{}), ) @@ -1661,8 +1661,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{ @@ -2543,8 +2543,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{}), ) @@ -2731,8 +2731,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{ @@ -2896,8 +2896,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{ @@ -3057,8 +3057,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{ @@ -3107,8 +3107,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{ @@ -3340,8 +3340,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{ @@ -3494,8 +3494,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{ @@ -3733,8 +3733,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{ @@ -3991,8 +3991,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{ @@ -4053,8 +4053,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{ @@ -4268,8 +4268,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{ @@ -4334,8 +4334,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{}), ) @@ -4959,8 +4959,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{}), ) @@ -5202,8 +5202,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{}), ) @@ -6149,8 +6149,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{}), ) @@ -6249,8 +6249,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{}), ) @@ -6500,8 +6500,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{ @@ -6563,8 +6563,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{}), ) @@ -6920,8 +6920,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{}), ) @@ -6956,8 +6956,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{}), ) @@ -6992,8 +6992,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{}), ) @@ -7034,8 +7034,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{}), ) @@ -7078,8 +7078,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{}), ) @@ -7155,8 +7155,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{}), ) @@ -7424,7 +7424,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) diff --git a/internal/stacks/stackruntime/validate_test.go b/internal/stacks/stackruntime/validate_test.go index 71a1fc09c4e1..0fdbe6a8d121 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{}), ) } From 3638b175a41de827d003b64b9453994893e1606d Mon Sep 17 00:00:00 2001 From: Arun Ramakrishnan Date: Mon, 27 Jul 2026 11:50:59 -0400 Subject: [PATCH 3/9] validate provider version constraints for pass-through providers --- .../internal/stackeval/stack_config.go | 70 ++++++++++++++ .../internal/stackeval/walk_static.go | 5 + internal/stacks/stackruntime/plan_test.go | 95 +++++++++++++++++++ 3 files changed, 170 insertions(+) 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_test.go b/internal/stacks/stackruntime/plan_test.go index 37a3dcac52b4..ba4d11f7e6b5 100644 --- a/internal/stacks/stackruntime/plan_test.go +++ b/internal/stacks/stackruntime/plan_test.go @@ -7652,3 +7652,98 @@ func planAndCollectPolicyResults(t *testing.T, ctx context.Context, req PlanRequ return gotPolicyResults } + +// TestPlan_versionMismatch_withProviderBlock verifies that a version mismatch +// between the lock file and required_providers is reported as an error when the +// stack has an explicit "provider" block (the direct code path through +// ProviderConfig.checkValid). +func TestPlan_versionMismatch_withProviderBlock(t *testing.T) { + ctx := context.Background() + // "with-single-input/valid" has both required_providers and a provider block. + cfg := loadMainBundleConfigForTest(t, "with-single-input/valid") + + // Lock says 0.2.0, but config says version = "0.1.0" (exact match constraint). + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.2.0"), + providerreqs.MustParseVersionConstraints("0.2.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + 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"): func() (providers.Interface, error) { + return &default_testing_provider.MockProvider{}, nil + }, + }, + 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("expected version mismatch error, got none") + } + 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()) + } +} + +// TestPlan_versionMismatch_passThroughProvider verifies that a version mismatch +// between the lock file and required_providers is reported even when the root +// stack has no explicit "provider" block — it only declares the provider in +// required_providers and passes it through to an embedded stack. +func TestPlan_versionMismatch_passThroughProvider(t *testing.T) { + ctx := context.Background() + // "policy-evaluation-embedded-stack" has required_providers in the root + // stack config but the provider block lives only in the embedded stack. + cfg := loadMainBundleConfigForTest(t, "policy-evaluation-embedded-stack") + + // Lock says 0.2.0, but both the root and embedded configs say version = "0.1.0". + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.2.0"), + providerreqs.MustParseVersionConstraints("0.2.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + 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"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + } + resp := PlanResponse{PlannedChanges: changesCh, Diagnostics: diagsCh} + + go Plan(ctx, &req, &resp) + _, gotDiags := collectPlanOutput(changesCh, diagsCh) + + if !gotDiags.HasErrors() { + t.Fatal("expected version mismatch error for pass-through provider, got none") + } + 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()) + } +} + +func hasDiagSummary(diags tfdiags.Diagnostics, summary string) bool { + for _, diag := range diags { + if diag.Description().Summary == summary { + return true + } + } + return false +} From ae3a506cafad24661f7abfab28522cd7b8344434 Mon Sep 17 00:00:00 2001 From: Arun Ramakrishnan Date: Tue, 28 Jul 2026 10:51:28 -0400 Subject: [PATCH 4/9] Add changelog --- .changes/v1.17/BUG FIXES-20260728-105047.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changes/v1.17/BUG FIXES-20260728-105047.yaml diff --git a/.changes/v1.17/BUG FIXES-20260728-105047.yaml b/.changes/v1.17/BUG FIXES-20260728-105047.yaml new file mode 100644 index 000000000000..6db11972c1b8 --- /dev/null +++ b/.changes/v1.17/BUG FIXES-20260728-105047.yaml @@ -0,0 +1,6 @@ +kind: BUG FIXES +body: Add a validation to check the provider versions in the lock file and configuration + are compatible for a Stack +time: 2026-07-28T10:50:47.221088-04:00 +custom: + Issue: "38829" From 81e27bda2212f455bfc8fc19fa3f5d82b1181252 Mon Sep 17 00:00:00 2001 From: Arun Ramakrishnan Date: Tue, 4 Aug 2026 15:28:03 -0400 Subject: [PATCH 5/9] Removed unnecessary comments and reworded CHANGELOG --- .changes/v1.17/BUG FIXES-20260728-105047.yaml | 4 ++-- internal/stacks/stackconfig/provider_requirements.go | 5 ----- .../stackruntime/internal/stackeval/provider_config.go | 6 ------ 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/.changes/v1.17/BUG FIXES-20260728-105047.yaml b/.changes/v1.17/BUG FIXES-20260728-105047.yaml index 6db11972c1b8..8e8cc2f12c1f 100644 --- a/.changes/v1.17/BUG FIXES-20260728-105047.yaml +++ b/.changes/v1.17/BUG FIXES-20260728-105047.yaml @@ -1,6 +1,6 @@ kind: BUG FIXES -body: Add a validation to check the provider versions in the lock file and configuration - are compatible for a Stack +body: stacks: Fixed validation to ensure the provider versions in the lock file and + configuration are compatible. time: 2026-07-28T10:50:47.221088-04:00 custom: Issue: "38829" diff --git a/internal/stacks/stackconfig/provider_requirements.go b/internal/stacks/stackconfig/provider_requirements.go index 9e748d70fc8c..d0d5c4999078 100644 --- a/internal/stacks/stackconfig/provider_requirements.go +++ b/internal/stacks/stackconfig/provider_requirements.go @@ -247,11 +247,6 @@ func (pr *ProviderRequirements) LocalNameForProvider(providerAddr addrs.Provider return "", false } -// VersionConstraintsForProvider returns the version constraints declared for -// the given provider in this required_providers block. -// -// The second return value is false if the provider is not declared in this -// required_providers block, in which case the first return value is nil. func (pr *ProviderRequirements) VersionConstraintsForProvider(providerAddr addrs.Provider) (constraints.IntersectionSpec, bool) { if pr == nil { return nil, false diff --git a/internal/stacks/stackruntime/internal/stackeval/provider_config.go b/internal/stacks/stackruntime/internal/stackeval/provider_config.go index 1ce0607859d9..47cf91c25a4a 100644 --- a/internal/stacks/stackruntime/internal/stackeval/provider_config.go +++ b/internal/stacks/stackruntime/internal/stackeval/provider_config.go @@ -97,12 +97,6 @@ func CheckProviderInLockfile(locks depsfile.Locks, providerType *ProviderType, v return diags } - // If the configuration declares version constraints for this provider, - // make sure the version recorded in the lockfile still satisfies them. - // A mismatch here typically means the version constraints in the - // configuration were changed after the lockfile was generated, leaving the - // lockfile inconsistent with the configuration. We must catch this so that - // we don't proceed with an out-of-date set of provider selections. if len(versionConstraints) > 0 { selectedVersion := lock.Version() allowedVersions := providerreqs.MeetingConstraints(versionConstraints) From 6477d859363d2306207777a513e13e1b52773832 Mon Sep 17 00:00:00 2001 From: Arun Ramakrishnan Date: Tue, 4 Aug 2026 16:23:02 -0400 Subject: [PATCH 6/9] Add provider version validation unit tests for Validate --- internal/stacks/stackruntime/validate_test.go | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/internal/stacks/stackruntime/validate_test.go b/internal/stacks/stackruntime/validate_test.go index 0fdbe6a8d121..ddd46bd1584d 100644 --- a/internal/stacks/stackruntime/validate_test.go +++ b/internal/stacks/stackruntime/validate_test.go @@ -553,3 +553,79 @@ Terraform uses references to decide a suitable order for performing operations, }) } } + +// TestValidate_versionMismatch_withProviderBlock verifies that a version +// mismatch between the lock file and required_providers is reported as an +// error during validation when the stack has an explicit "provider" block +// (the direct code path through ProviderConfig.checkValid). +func TestValidate_versionMismatch_withProviderBlock(t *testing.T) { + ctx := context.Background() + // "with-single-input/valid" has both required_providers and a provider block. + cfg := loadMainBundleConfigForTest(t, "with-single-input/valid") + + // Lock says 0.2.0, but config says version = "0.1.0" (exact match constraint). + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.2.0"), + providerreqs.MustParseVersionConstraints("0.2.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + 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("expected version mismatch error, got none") + } + 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()) + } +} + +// TestValidate_versionMismatch_passThroughProvider verifies that a version +// mismatch between the lock file and required_providers is reported during +// validation even when the root stack has no explicit "provider" block — it +// only declares the provider in required_providers and passes it through to +// an embedded stack. +func TestValidate_versionMismatch_passThroughProvider(t *testing.T) { + ctx := context.Background() + // "policy-evaluation-embedded-stack" has required_providers in the root + // stack config but the provider block lives only in the embedded stack. + cfg := loadMainBundleConfigForTest(t, "policy-evaluation-embedded-stack") + + // Lock says 0.2.0, but both the root and embedded configs say version = "0.1.0". + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.2.0"), + providerreqs.MustParseVersionConstraints("0.2.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + 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("expected version mismatch error for pass-through provider, got none") + } + 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()) + } +} From d5f083d65bf70eaaa6fe0e0bb5493c3e2c2b8ad9 Mon Sep 17 00:00:00 2001 From: Arun Ramakrishnan Date: Tue, 4 Aug 2026 16:58:02 -0400 Subject: [PATCH 7/9] Refactored new unit tests and helper code to reduce duplication --- internal/stacks/stackruntime/helper_test.go | 24 ++++ internal/stacks/stackruntime/plan_test.go | 135 +++++++----------- internal/stacks/stackruntime/validate_test.go | 116 +++++++-------- 3 files changed, 125 insertions(+), 150 deletions(-) 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/plan_test.go b/internal/stacks/stackruntime/plan_test.go index ba4d11f7e6b5..19e3693623ea 100644 --- a/internal/stacks/stackruntime/plan_test.go +++ b/internal/stacks/stackruntime/plan_test.go @@ -7653,97 +7653,68 @@ func planAndCollectPolicyResults(t *testing.T, ctx context.Context, req PlanRequ return gotPolicyResults } -// TestPlan_versionMismatch_withProviderBlock verifies that a version mismatch -// between the lock file and required_providers is reported as an error when the -// stack has an explicit "provider" block (the direct code path through -// ProviderConfig.checkValid). -func TestPlan_versionMismatch_withProviderBlock(t *testing.T) { - ctx := context.Background() - // "with-single-input/valid" has both required_providers and a provider block. - cfg := loadMainBundleConfigForTest(t, "with-single-input/valid") - - // Lock says 0.2.0, but config says version = "0.1.0" (exact match constraint). - lock := depsfile.NewLocks() - lock.SetProvider( - addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.2.0"), - providerreqs.MustParseVersionConstraints("0.2.0"), - providerreqs.PreferredHashes([]providerreqs.Hash{}), - ) - - 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"): func() (providers.Interface, error) { +// 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 }, }, - 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("expected version mismatch error, got none") - } - 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()) - } -} - -// TestPlan_versionMismatch_passThroughProvider verifies that a version mismatch -// between the lock file and required_providers is reported even when the root -// stack has no explicit "provider" block — it only declares the provider in -// required_providers and passes it through to an embedded stack. -func TestPlan_versionMismatch_passThroughProvider(t *testing.T) { - ctx := context.Background() - // "policy-evaluation-embedded-stack" has required_providers in the root - // stack config but the provider block lives only in the embedded stack. - cfg := loadMainBundleConfigForTest(t, "policy-evaluation-embedded-stack") - - // Lock says 0.2.0, but both the root and embedded configs say version = "0.1.0". - lock := depsfile.NewLocks() - lock.SetProvider( - addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.2.0"), - providerreqs.MustParseVersionConstraints("0.2.0"), - providerreqs.PreferredHashes([]providerreqs.Hash{}), - ) - - 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"): func() (providers.Interface, error) { + { + // "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 }, }, } - resp := PlanResponse{PlannedChanges: changesCh, Diagnostics: diagsCh} - go Plan(ctx, &req, &resp) - _, gotDiags := collectPlanOutput(changesCh, diagsCh) + 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() - if !gotDiags.HasErrors() { - t.Fatal("expected version mismatch error for pass-through provider, got none") - } - 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()) - } -} + 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} -func hasDiagSummary(diags tfdiags.Diagnostics, summary string) bool { - for _, diag := range diags { - if diag.Description().Summary == summary { - return true - } + 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()) + } + }) } - return false } diff --git a/internal/stacks/stackruntime/validate_test.go b/internal/stacks/stackruntime/validate_test.go index ddd46bd1584d..6d8fff6b2d2a 100644 --- a/internal/stacks/stackruntime/validate_test.go +++ b/internal/stacks/stackruntime/validate_test.go @@ -554,78 +554,58 @@ Terraform uses references to decide a suitable order for performing operations, } } -// TestValidate_versionMismatch_withProviderBlock verifies that a version -// mismatch between the lock file and required_providers is reported as an -// error during validation when the stack has an explicit "provider" block -// (the direct code path through ProviderConfig.checkValid). -func TestValidate_versionMismatch_withProviderBlock(t *testing.T) { - ctx := context.Background() - // "with-single-input/valid" has both required_providers and a provider block. - cfg := loadMainBundleConfigForTest(t, "with-single-input/valid") - - // Lock says 0.2.0, but config says version = "0.1.0" (exact match constraint). - lock := depsfile.NewLocks() - lock.SetProvider( - addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.2.0"), - providerreqs.MustParseVersionConstraints("0.2.0"), - providerreqs.PreferredHashes([]providerreqs.Hash{}), - ) - - 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 - }, +// 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", }, - DependencyLocks: *lock, - ExperimentsAllowed: true, - }) - - if !gotDiags.HasErrors() { - t.Fatal("expected version mismatch error, got none") - } - 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()) } -} - -// TestValidate_versionMismatch_passThroughProvider verifies that a version -// mismatch between the lock file and required_providers is reported during -// validation even when the root stack has no explicit "provider" block — it -// only declares the provider in required_providers and passes it through to -// an embedded stack. -func TestValidate_versionMismatch_passThroughProvider(t *testing.T) { - ctx := context.Background() - // "policy-evaluation-embedded-stack" has required_providers in the root - // stack config but the provider block lives only in the embedded stack. - cfg := loadMainBundleConfigForTest(t, "policy-evaluation-embedded-stack") - // Lock says 0.2.0, but both the root and embedded configs say version = "0.1.0". - lock := depsfile.NewLocks() - lock.SetProvider( - addrs.NewDefaultProvider("testing"), - providerreqs.MustParseVersion("0.2.0"), - providerreqs.MustParseVersionConstraints("0.2.0"), - providerreqs.PreferredHashes([]providerreqs.Hash{}), - ) + 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, - }) + 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("expected version mismatch error for pass-through provider, got none") - } - 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()) + 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()) + } + }) } } From 1afdd83fc8d75ee4f35179b160c5d5f87b2d413f Mon Sep 17 00:00:00 2001 From: Arun Ramakrishnan Date: Tue, 4 Aug 2026 17:23:54 -0400 Subject: [PATCH 8/9] Regenerate changelog with tool --- .changes/v1.17/BUG FIXES-20260728-105047.yaml | 6 ------ .changes/v1.17/BUG FIXES-20260804-172309.yaml | 5 +++++ 2 files changed, 5 insertions(+), 6 deletions(-) delete mode 100644 .changes/v1.17/BUG FIXES-20260728-105047.yaml create mode 100644 .changes/v1.17/BUG FIXES-20260804-172309.yaml diff --git a/.changes/v1.17/BUG FIXES-20260728-105047.yaml b/.changes/v1.17/BUG FIXES-20260728-105047.yaml deleted file mode 100644 index 8e8cc2f12c1f..000000000000 --- a/.changes/v1.17/BUG FIXES-20260728-105047.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: BUG FIXES -body: stacks: Fixed validation to ensure the provider versions in the lock file and - configuration are compatible. -time: 2026-07-28T10:50:47.221088-04:00 -custom: - Issue: "38829" diff --git a/.changes/v1.17/BUG FIXES-20260804-172309.yaml b/.changes/v1.17/BUG FIXES-20260804-172309.yaml new file mode 100644 index 000000000000..c24b0074f321 --- /dev/null +++ b/.changes/v1.17/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" From 850f03154fbad4f4030a39341357696fd7137d2d Mon Sep 17 00:00:00 2001 From: Arun Ramakrishnan Date: Wed, 5 Aug 2026 10:05:26 -0400 Subject: [PATCH 9/9] Move the changelog to 1.16 to push the fix to 1.16 --- .changes/{v1.17 => v1.16}/BUG FIXES-20260804-172309.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changes/{v1.17 => v1.16}/BUG FIXES-20260804-172309.yaml (100%) diff --git a/.changes/v1.17/BUG FIXES-20260804-172309.yaml b/.changes/v1.16/BUG FIXES-20260804-172309.yaml similarity index 100% rename from .changes/v1.17/BUG FIXES-20260804-172309.yaml rename to .changes/v1.16/BUG FIXES-20260804-172309.yaml