terraform/internal/command/meta_dependencies_test.go
Sarah French da76dba3dc
PSS: Add alternative, experimental version of init command that downloads providers in two stages (#37350)
* Add forked version of `run` logic that's only used if experiments are enabled

* Reorder actions in experimental init - load in full config before configuring the backend.

* Add getProvidersFromConfig method, initially as an exact copy of getProviders

* Make getProvidersFromConfig not use state to get providers

* Add `appendLockedDependencies` method to `Meta` to allow multi-phase saving to the dep locks file

* Update experimental init to use new getProvidersFromConfig method

* Add new getProvidersFromState method that only accepts state information as input for getting providers. Use in experimental init and append values to existing deps lock file

* Update messages sent to view about provider download phases

* Change init to save updates to the deps lock file only once

* Make Terraform output report that a lock file _will_ be made after providers are determined from config

* Remove use of `ProviderDownloadOutcome`s

* Move repeated code into separate method

* Change provider download approach: determine if locks changed at point of attempting to update the lockfile, keep record of incomplete providers inside init command struct

* Refactor `mergeLockedDependencies` and update test

* Add comments to provider download methods

* Fix issue where incorrect message ouput to view when downloading providers

* Update `mergeLockedDependencies` method to be more generic

* Update `getProvidersFromState` method to receive in-progress config locks and merge those with any locks on file. This allows re-use of providers downloaded by `getProvidersFromConfig` in the same init command

* Fix config for `TestInit_stateStoreBlockIsExperimental`

* Improve testing of mergeLockedDependencies; state locks are always missing version constraints

* Add tests for 2 phase provider download

* Add test case to cover use of the `-upgrade` flag

* Change the message shown when a provider is reused during the second provider download step.

When downloading providers described only in the state then the provider may already be downloaded from a previous init (i.e. is recorded in the deps lock file) or downloaded during step 1 of provider download. The message here needs to cover both potential scenarios.

* Update mergeLockedDependencies comment

* fix: completely remove use of upgrade flag in getProvidersFromState

* Fix: avoid nil pointer errors by returning an empty collection of locks when there is no state

* Fix: use state store data only in diagnostic

* Change how we make PSS experimental - avoid relying on a package level variable that causes tests to interact.

* Remove full-stop in view message, update tests

* Update span names to be unique

* Re-add lost early returns

* Remove unused view messages

* Add comments to new view messages
2025-08-18 11:20:18 +01:00

151 lines
5.7 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"testing"
"github.com/apparentlymart/go-versions/versions"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/cli"
tfaddr "github.com/hashicorp/terraform-registry-address"
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
)
// This tests combining locks from config and state. Locks derived from state are always unconstrained, i.e. no version constraint data,
// so this test
func Test_mergeLockedDependencies_config_and_state(t *testing.T) {
providerA := tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "my-org", "providerA")
providerB := tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "my-org", "providerB")
v1_0_0 := providerreqs.MustParseVersion("1.0.0")
versionConstraintv1, _ := providerreqs.ParseVersionConstraints("1.0.0")
hashesProviderA := []providerreqs.Hash{providerreqs.MustParseHash("providerA:this-is-providerA")}
hashesProviderB := []providerreqs.Hash{providerreqs.MustParseHash("providerB:this-is-providerB")}
var versionUnconstrained providerreqs.VersionConstraints = nil
noVersion := versions.Version{}
cases := map[string]struct {
configLocks *depsfile.Locks
stateLocks *depsfile.Locks
expectedLocks *depsfile.Locks
}{
"no locks when all inputs empty": {
configLocks: depsfile.NewLocks(),
stateLocks: depsfile.NewLocks(),
expectedLocks: depsfile.NewLocks(),
},
"when provider only described in config, output locks have matching constraints": {
configLocks: func() *depsfile.Locks {
configLocks := depsfile.NewLocks()
configLocks.SetProvider(providerA, v1_0_0, versionConstraintv1, hashesProviderA)
return configLocks
}(),
stateLocks: depsfile.NewLocks(),
expectedLocks: func() *depsfile.Locks {
combinedLocks := depsfile.NewLocks()
combinedLocks.SetProvider(providerA, v1_0_0, versionConstraintv1, hashesProviderA)
return combinedLocks
}(),
},
"when provider only described in state, output locks are unconstrained": {
configLocks: depsfile.NewLocks(),
stateLocks: func() *depsfile.Locks {
stateLocks := depsfile.NewLocks()
stateLocks.SetProvider(providerA, noVersion, versionUnconstrained, hashesProviderA)
return stateLocks
}(),
expectedLocks: func() *depsfile.Locks {
combinedLocks := depsfile.NewLocks()
combinedLocks.SetProvider(providerA, noVersion, versionUnconstrained, hashesProviderA)
return combinedLocks
}(),
},
"different providers present in state and config are combined, with version constraints kept on config providers": {
configLocks: func() *depsfile.Locks {
configLocks := depsfile.NewLocks()
configLocks.SetProvider(providerA, v1_0_0, versionConstraintv1, hashesProviderA)
return configLocks
}(),
stateLocks: func() *depsfile.Locks {
stateLocks := depsfile.NewLocks()
// Imagine that the state locks contain:
// 1) provider for resources in the config
stateLocks.SetProvider(providerA, noVersion, versionUnconstrained, hashesProviderA)
// 2) also, a provider that's deleted from the config and only present in state
stateLocks.SetProvider(providerB, noVersion, versionUnconstrained, hashesProviderB)
return stateLocks
}(),
expectedLocks: func() *depsfile.Locks {
combinedLocks := depsfile.NewLocks()
combinedLocks.SetProvider(providerA, v1_0_0, versionConstraintv1, hashesProviderA) // version constraint preserved
combinedLocks.SetProvider(providerB, noVersion, versionUnconstrained, hashesProviderB) // sourced from state only
return combinedLocks
}(),
},
}
for tn, tc := range cases {
t.Run(tn, func(t *testing.T) {
// Use tmp dir as we're creating lock files in the test
td := t.TempDir()
t.Chdir(td)
ui := new(cli.MockUi)
view, _ := testView(t)
m := Meta{
Ui: ui,
View: view,
}
// Code under test - combine deps from state with prior deps from config
// mergedLocks := m.mergeLockedDependencies(tc.configLocks, tc.stateLocks)
mergedLocks := m.mergeLockedDependencies(tc.configLocks, tc.stateLocks)
// We cannot use (l *depsfile.Locks) Equal here as it doesn't compare version constraints
// Instead, inspect entries directly
if len(mergedLocks.AllProviders()) != len(tc.expectedLocks.AllProviders()) {
t.Fatalf("expected merged dependencies to include %d providers, but got %d:\n %#v",
len(tc.expectedLocks.AllProviders()),
len(mergedLocks.AllProviders()),
mergedLocks.AllProviders(),
)
}
for _, lock := range tc.expectedLocks.AllProviders() {
match := mergedLocks.Provider(lock.Provider())
if match == nil {
t.Fatalf("expected merged dependencies to include provider %s, but it's missing", lock.Provider())
}
if len(match.VersionConstraints()) != len(lock.VersionConstraints()) {
t.Fatalf("detected a problem with version constraints for provider %s, got: %d, want %d",
lock.Provider(),
len(match.VersionConstraints()),
len(lock.VersionConstraints()),
)
}
if len(match.VersionConstraints()) > 0 && len(lock.VersionConstraints()) > 0 {
gotConstraints := match.VersionConstraints()[0]
wantConstraints := lock.VersionConstraints()[0]
if gotConstraints.Boundary.String() != wantConstraints.Boundary.String() {
t.Fatalf("expected merged dependencies to include provider %s with version constraint %v, but instead got %v",
lock.Provider(),
gotConstraints.Boundary.String(),
wantConstraints.Boundary.String(),
)
}
}
}
if diff := cmp.Diff(tc.expectedLocks, mergedLocks); diff != "" {
t.Errorf("difference in file contents detected\n%s", diff)
}
})
}
}