Skip to content

Commit

Permalink
Merge pull request #26761 from hashicorp/alisdair/compare-locks-and-p…
Browse files Browse the repository at this point in the history
…rovider-requirements

terraform: Compare locks and provider requirements
  • Loading branch information
alisdair authored Nov 6, 2020
2 parents e38e8e2 + 10cc25f commit 60e01f5
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 0 deletions.
22 changes: 22 additions & 0 deletions command/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
56 changes: 56 additions & 0 deletions terraform/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
)

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand Down
177 changes: 177 additions & 0 deletions terraform/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()

Expand Down

0 comments on commit 60e01f5

Please sign in to comment.