Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

terraform: Compare locks and provider requirements #26761

Merged
merged 1 commit into from
Nov 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

// 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": {
alisdair marked this conversation as resolved.
Show resolved Hide resolved
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