terraform: Compare locks and provider requirements
When building a context, we read the dependency locks and ensure that the provider requirements from the configuration can be satisfied. If the configured requirements change such that the locks need to be updated, we explain this and recommend running "terraform init". This check is ignored for any providers which are locally marked as in development. This includes unmanaged providers and those listed in the provider installation `dev_overrides` block.
This commit is contained in:
parent
e38e8e2e61
commit
10cc25fc21
|
@ -421,6 +421,28 @@ func (m *Meta) contextOpts() (*terraform.ContextOpts, error) {
|
||||||
}
|
}
|
||||||
opts.Providers = providerFactories
|
opts.Providers = providerFactories
|
||||||
opts.Provisioners = m.provisionerFactories()
|
opts.Provisioners = m.provisionerFactories()
|
||||||
|
|
||||||
|
// Read the dependency locks so that they can be verified against the
|
||||||
|
// provider requirements in the configuration
|
||||||
|
lockedDependencies, diags := m.lockedDependencies()
|
||||||
|
|
||||||
|
// If the locks file is invalid, we should fail early rather than
|
||||||
|
// ignore it. A missing locks file will return no error.
|
||||||
|
if diags.HasErrors() {
|
||||||
|
return nil, diags.Err()
|
||||||
|
}
|
||||||
|
opts.LockedDependencies = lockedDependencies
|
||||||
|
|
||||||
|
// If any unmanaged providers or dev overrides are enabled, they must
|
||||||
|
// be listed in the context so that they can be ignored when verifying
|
||||||
|
// the locks against the configuration
|
||||||
|
opts.ProvidersInDevelopment = make(map[addrs.Provider]struct{})
|
||||||
|
for provider := range m.UnmanagedProviders {
|
||||||
|
opts.ProvidersInDevelopment[provider] = struct{}{}
|
||||||
|
}
|
||||||
|
for provider := range m.ProviderDevOverrides {
|
||||||
|
opts.ProvidersInDevelopment[provider] = struct{}{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
opts.ProviderSHA256s = m.providerPluginsLock().Read()
|
opts.ProviderSHA256s = m.providerPluginsLock().Read()
|
||||||
|
|
|
@ -5,8 +5,10 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/apparentlymart/go-versions/versions"
|
||||||
"github.com/hashicorp/terraform/addrs"
|
"github.com/hashicorp/terraform/addrs"
|
||||||
"github.com/hashicorp/terraform/configs"
|
"github.com/hashicorp/terraform/configs"
|
||||||
"github.com/hashicorp/terraform/instances"
|
"github.com/hashicorp/terraform/instances"
|
||||||
|
@ -19,6 +21,8 @@ import (
|
||||||
"github.com/hashicorp/terraform/tfdiags"
|
"github.com/hashicorp/terraform/tfdiags"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/internal/depsfile"
|
||||||
|
"github.com/hashicorp/terraform/internal/getproviders"
|
||||||
_ "github.com/hashicorp/terraform/internal/logging"
|
_ "github.com/hashicorp/terraform/internal/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -67,6 +71,14 @@ type ContextOpts struct {
|
||||||
// plugins that will be requested from the provider resolver.
|
// plugins that will be requested from the provider resolver.
|
||||||
ProviderSHA256s map[string][]byte
|
ProviderSHA256s map[string][]byte
|
||||||
|
|
||||||
|
// If non-nil, will be verified to ensure that provider requirements from
|
||||||
|
// configuration can be satisfied by the set of locked dependencies.
|
||||||
|
LockedDependencies *depsfile.Locks
|
||||||
|
|
||||||
|
// Set of providers to exclude from the requirements check process, as they
|
||||||
|
// are marked as in local development.
|
||||||
|
ProvidersInDevelopment map[addrs.Provider]struct{}
|
||||||
|
|
||||||
UIInput UIInput
|
UIInput UIInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,6 +224,50 @@ func NewContext(opts *ContextOpts) (*Context, tfdiags.Diagnostics) {
|
||||||
config = configs.NewEmptyConfig()
|
config = configs.NewEmptyConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we have a configuration and a set of locked dependencies, verify that
|
||||||
|
// the provider requirements from the configuration can be satisfied by the
|
||||||
|
// locked dependencies.
|
||||||
|
if opts.LockedDependencies != nil {
|
||||||
|
reqs, providerDiags := config.ProviderRequirements()
|
||||||
|
diags = diags.Append(providerDiags)
|
||||||
|
|
||||||
|
locked := opts.LockedDependencies.AllProviders()
|
||||||
|
unmetReqs := make(getproviders.Requirements)
|
||||||
|
for provider, versionConstraints := range reqs {
|
||||||
|
// Builtin providers are not listed in the locks file
|
||||||
|
if provider.IsBuiltIn() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Development providers must be excluded from this check
|
||||||
|
if _, ok := opts.ProvidersInDevelopment[provider]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// If the required provider doesn't exist in the lock, or the
|
||||||
|
// locked version doesn't meet the constraints, mark the
|
||||||
|
// requirement unmet
|
||||||
|
acceptable := versions.MeetingConstraints(versionConstraints)
|
||||||
|
if lock, ok := locked[provider]; !ok || !acceptable.Has(lock.Version()) {
|
||||||
|
unmetReqs[provider] = versionConstraints
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(unmetReqs) > 0 {
|
||||||
|
var buf strings.Builder
|
||||||
|
for provider, versionConstraints := range unmetReqs {
|
||||||
|
fmt.Fprintf(&buf, "\n- %s", provider)
|
||||||
|
if len(versionConstraints) > 0 {
|
||||||
|
fmt.Fprintf(&buf, " (%s)", getproviders.VersionConstraintsString(versionConstraints))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
|
tfdiags.Error,
|
||||||
|
"Provider requirements cannot be satisfied by locked dependencies",
|
||||||
|
fmt.Sprintf("The following required providers are not installed:\n%s\n\nPlease run \"terraform init\".", buf.String()),
|
||||||
|
))
|
||||||
|
return nil, diags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf("[TRACE] terraform.NewContext: complete")
|
log.Printf("[TRACE] terraform.NewContext: complete")
|
||||||
|
|
||||||
// By the time we get here, we should have values defined for all of
|
// By the time we get here, we should have values defined for all of
|
||||||
|
|
|
@ -15,10 +15,12 @@ import (
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
"github.com/hashicorp/go-version"
|
"github.com/hashicorp/go-version"
|
||||||
|
"github.com/hashicorp/terraform/addrs"
|
||||||
"github.com/hashicorp/terraform/configs"
|
"github.com/hashicorp/terraform/configs"
|
||||||
"github.com/hashicorp/terraform/configs/configload"
|
"github.com/hashicorp/terraform/configs/configload"
|
||||||
"github.com/hashicorp/terraform/configs/configschema"
|
"github.com/hashicorp/terraform/configs/configschema"
|
||||||
"github.com/hashicorp/terraform/configs/hcl2shim"
|
"github.com/hashicorp/terraform/configs/hcl2shim"
|
||||||
|
"github.com/hashicorp/terraform/internal/depsfile"
|
||||||
"github.com/hashicorp/terraform/plans"
|
"github.com/hashicorp/terraform/plans"
|
||||||
"github.com/hashicorp/terraform/plans/planfile"
|
"github.com/hashicorp/terraform/plans/planfile"
|
||||||
"github.com/hashicorp/terraform/providers"
|
"github.com/hashicorp/terraform/providers"
|
||||||
|
@ -117,6 +119,181 @@ func TestNewContextRequiredVersion(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewContext_lockedDependencies(t *testing.T) {
|
||||||
|
configBeepGreaterThanOne := `
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
beep = {
|
||||||
|
source = "example.com/foo/beep"
|
||||||
|
version = ">= 1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
configBeepLessThanOne := `
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
beep = {
|
||||||
|
source = "example.com/foo/beep"
|
||||||
|
version = "< 1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
configBuiltin := `
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
terraform = {
|
||||||
|
source = "terraform.io/builtin/terraform"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
locksBeepGreaterThanOne := `
|
||||||
|
provider "example.com/foo/beep" {
|
||||||
|
version = "1.0.0"
|
||||||
|
constraints = ">= 1.0.0"
|
||||||
|
hashes = [
|
||||||
|
"h1:does-not-match",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
configBeepBoop := `
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
beep = {
|
||||||
|
source = "example.com/foo/beep"
|
||||||
|
version = "< 1.0.0" # different from locks
|
||||||
|
}
|
||||||
|
boop = {
|
||||||
|
source = "example.com/foo/boop"
|
||||||
|
version = ">= 2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
locksBeepBoop := `
|
||||||
|
provider "example.com/foo/beep" {
|
||||||
|
version = "1.0.0"
|
||||||
|
constraints = ">= 1.0.0"
|
||||||
|
hashes = [
|
||||||
|
"h1:does-not-match",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
provider "example.com/foo/boop" {
|
||||||
|
version = "2.3.4"
|
||||||
|
constraints = ">= 2.0.0"
|
||||||
|
hashes = [
|
||||||
|
"h1:does-not-match",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
beepAddr := addrs.MustParseProviderSourceString("example.com/foo/beep")
|
||||||
|
boopAddr := addrs.MustParseProviderSourceString("example.com/foo/boop")
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
Config string
|
||||||
|
LockFile string
|
||||||
|
DevProviders []addrs.Provider
|
||||||
|
WantErr string
|
||||||
|
}{
|
||||||
|
"dependencies met": {
|
||||||
|
Config: configBeepGreaterThanOne,
|
||||||
|
LockFile: locksBeepGreaterThanOne,
|
||||||
|
},
|
||||||
|
"no locks given": {
|
||||||
|
Config: configBeepGreaterThanOne,
|
||||||
|
},
|
||||||
|
"builtin provider with empty locks": {
|
||||||
|
Config: configBuiltin,
|
||||||
|
LockFile: `# This file is maintained automatically by "terraform init".`,
|
||||||
|
},
|
||||||
|
"multiple providers, one in development": {
|
||||||
|
Config: configBeepBoop,
|
||||||
|
LockFile: locksBeepBoop,
|
||||||
|
DevProviders: []addrs.Provider{beepAddr},
|
||||||
|
},
|
||||||
|
"development provider with empty locks": {
|
||||||
|
Config: configBeepGreaterThanOne,
|
||||||
|
LockFile: `# This file is maintained automatically by "terraform init".`,
|
||||||
|
DevProviders: []addrs.Provider{beepAddr},
|
||||||
|
},
|
||||||
|
"multiple providers, one in development, one missing": {
|
||||||
|
Config: configBeepBoop,
|
||||||
|
LockFile: locksBeepGreaterThanOne,
|
||||||
|
DevProviders: []addrs.Provider{beepAddr},
|
||||||
|
WantErr: `Provider requirements cannot be satisfied by locked dependencies: The following required providers are not installed:
|
||||||
|
|
||||||
|
- example.com/foo/boop (>= 2.0.0)
|
||||||
|
|
||||||
|
Please run "terraform init".`,
|
||||||
|
},
|
||||||
|
"wrong provider version": {
|
||||||
|
Config: configBeepLessThanOne,
|
||||||
|
LockFile: locksBeepGreaterThanOne,
|
||||||
|
WantErr: `Provider requirements cannot be satisfied by locked dependencies: The following required providers are not installed:
|
||||||
|
|
||||||
|
- example.com/foo/beep (< 1.0.0)
|
||||||
|
|
||||||
|
Please run "terraform init".`,
|
||||||
|
},
|
||||||
|
"empty locks": {
|
||||||
|
Config: configBeepGreaterThanOne,
|
||||||
|
LockFile: `# This file is maintained automatically by "terraform init".`,
|
||||||
|
WantErr: `Provider requirements cannot be satisfied by locked dependencies: The following required providers are not installed:
|
||||||
|
|
||||||
|
- example.com/foo/beep (>= 1.0.0)
|
||||||
|
|
||||||
|
Please run "terraform init".`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
var locks *depsfile.Locks
|
||||||
|
if tc.LockFile != "" {
|
||||||
|
var diags tfdiags.Diagnostics
|
||||||
|
locks, diags = depsfile.LoadLocksFromBytes([]byte(tc.LockFile), "test.lock.hcl")
|
||||||
|
if len(diags) > 0 {
|
||||||
|
t.Fatalf("unexpected error loading locks file: %s", diags.Err())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
devProviders := make(map[addrs.Provider]struct{})
|
||||||
|
for _, provider := range tc.DevProviders {
|
||||||
|
devProviders[provider] = struct{}{}
|
||||||
|
}
|
||||||
|
opts := &ContextOpts{
|
||||||
|
Config: testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": tc.Config,
|
||||||
|
}),
|
||||||
|
LockedDependencies: locks,
|
||||||
|
ProvidersInDevelopment: devProviders,
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
beepAddr: testProviderFuncFixed(testProvider("beep")),
|
||||||
|
boopAddr: testProviderFuncFixed(testProvider("boop")),
|
||||||
|
addrs.NewBuiltInProvider("terraform"): testProviderFuncFixed(testProvider("terraform")),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, diags := NewContext(opts)
|
||||||
|
if tc.WantErr != "" {
|
||||||
|
if len(diags) == 0 {
|
||||||
|
t.Fatal("expected diags but none returned")
|
||||||
|
}
|
||||||
|
if got, want := diags.Err().Error(), tc.WantErr; got != want {
|
||||||
|
t.Errorf("wrong diags\n got: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(diags) > 0 {
|
||||||
|
t.Errorf("unexpected diags: %s", diags.Err())
|
||||||
|
}
|
||||||
|
if ctx == nil {
|
||||||
|
t.Error("ctx is nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testContext2(t *testing.T, opts *ContextOpts) *Context {
|
func testContext2(t *testing.T, opts *ContextOpts) *Context {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue