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.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()
|
||||
|
|
|
@ -5,8 +5,10 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/apparentlymart/go-versions/versions"
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
"github.com/hashicorp/terraform/configs"
|
||||
"github.com/hashicorp/terraform/instances"
|
||||
|
@ -19,6 +21,8 @@ import (
|
|||
"github.com/hashicorp/terraform/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/depsfile"
|
||||
"github.com/hashicorp/terraform/internal/getproviders"
|
||||
_ "github.com/hashicorp/terraform/internal/logging"
|
||||
)
|
||||
|
||||
|
@ -67,6 +71,14 @@ type ContextOpts struct {
|
|||
// plugins that will be requested from the provider resolver.
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -212,6 +224,50 @@ func NewContext(opts *ContextOpts) (*Context, tfdiags.Diagnostics) {
|
|||
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")
|
||||
|
||||
// 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/cmpopts"
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
"github.com/hashicorp/terraform/configs"
|
||||
"github.com/hashicorp/terraform/configs/configload"
|
||||
"github.com/hashicorp/terraform/configs/configschema"
|
||||
"github.com/hashicorp/terraform/configs/hcl2shim"
|
||||
"github.com/hashicorp/terraform/internal/depsfile"
|
||||
"github.com/hashicorp/terraform/plans"
|
||||
"github.com/hashicorp/terraform/plans/planfile"
|
||||
"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 {
|
||||
t.Helper()
|
||||
|
||||
|
|
Loading…
Reference in New Issue