Merge pull request #26761 from hashicorp/alisdair/compare-locks-and-provider-requirements
terraform: Compare locks and provider requirements
This commit is contained in:
commit
60e01f595c
|
@ -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