internal/depsfile: Loading locks from HCL files on disk

This is the initial implementation of the parser/decoder portion of the
new dependency lock file handler. It's currently dead code because the
caller isn't written yet. We'll continue to build out this functionality
here until we have the basic level of both load and save functionality
before introducing this into the provider installer codepath.
This commit is contained in:
Martin Atkins 2020-09-03 18:35:31 -07:00
parent 6993ecb0a6
commit 92723661d0
11 changed files with 784 additions and 1 deletions

22
internal/depsfile/doc.go Normal file
View File

@ -0,0 +1,22 @@
// Package depsfile contains the logic for reading and writing Terraform's
// dependency lock and development override configuration files.
//
// These files are separate from the main Terraform configuration files (.tf)
// for a number of reasons. The first is to help establish a distinction
// where .tf files configure a particular module while these configure
// a whole configuration tree. Another, more practical consideration is that
// we intend both of these files to be primarily maintained automatically by
// Terraform itself, rather than by human-originated edits, and so keeping
// them separate means that it's easier to distinguish the files that Terraform
// will change automatically during normal workflow from the files that
// Terraform only edits on direct request.
//
// Both files use HCL syntax, for consistency with other files in Terraform
// that we expect humans to (in this case, only occasionally) edit directly.
// A dependency lock file tracks the most recently selected upstream versions
// of each dependency, and is intended for checkin to version control.
// A development override file allows for temporarily overriding upstream
// dependencies with local files/directories on disk as an aid to testing
// a cross-codebase change during development, and should not be saved in
// version control.
package depsfile

160
internal/depsfile/locks.go Normal file
View File

@ -0,0 +1,160 @@
package depsfile
import (
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/internal/getproviders"
)
// Locks is the top-level type representing the information retained in a
// dependency lock file.
//
// Locks and the other types used within it are mutable via various setter
// methods, but they are not safe for concurrent modifications, so it's the
// caller's responsibility to prevent concurrent writes and writes concurrent
// with reads.
type Locks struct {
providers map[addrs.Provider]*ProviderLock
// TODO: In future we'll also have module locks, but the design of that
// still needs some more work and we're deferring that to get the
// provider locking capability out sooner, because it's more common to
// directly depend on providers maintained outside your organization than
// modules maintained outside your organization.
// sources is a copy of the map of source buffers produced by the HCL
// parser during loading, which we retain only so that the caller can
// use it to produce source code snippets in error messages.
sources map[string][]byte
}
// NewLocks constructs and returns a new Locks object that initially contains
// no locks at all.
func NewLocks() *Locks {
return &Locks{
providers: make(map[addrs.Provider]*ProviderLock),
// no "sources" here, because that's only for locks objects loaded
// from files.
}
}
// Provider returns the stored lock for the given provider, or nil if that
// provider currently has no lock.
func (l *Locks) Provider(addr addrs.Provider) *ProviderLock {
return l.providers[addr]
}
// SetProvider creates a new lock or replaces the existing lock for the given
// provider.
//
// SetProvider returns the newly-created provider lock object, which
// invalidates any ProviderLock object previously returned from Provider or
// SetProvider for the given provider address.
func (l *Locks) SetProvider(addr addrs.Provider, version getproviders.Version, constraints getproviders.VersionConstraints, hashes map[getproviders.Platform][]string) *ProviderLock {
new := &ProviderLock{
addr: addr,
version: version,
versionConstraints: constraints,
hashes: hashes,
}
l.providers[addr] = new
return new
}
// Sources returns the source code of the file the receiver was generated from,
// or an empty map if the receiver wasn't generated from a file.
//
// This return type matches the one expected by HCL diagnostics printers to
// produce source code snapshots, which is the only intended use for this
// method.
func (l *Locks) Sources() map[string][]byte {
return l.sources
}
// ProviderLock represents lock information for a specific provider.
type ProviderLock struct {
// addr is the address of the provider this lock applies to.
addr addrs.Provider
// version is the specific version that was previously selected, while
// versionConstraints is the constraint that was used to make that
// selection, which we can potentially use to hint to run
// e.g. terraform init -upgrade if a user has changed a version
// constraint but the previous selection still remains valid.
// "version" is therefore authoritative, while "versionConstraints" is
// just for a UI hint and not used to make any real decisions.
version getproviders.Version
versionConstraints getproviders.VersionConstraints
// hashes contains one or more hashes of packages or package contents
// for the package associated with the selected version on each supported
// architecture.
//
// hashes can contain a mixture of hashes in different formats to support
// changes over time. The new-style hash format is to have a string
// starting with "h" followed by a version number and then a colon, like
// "h1:" for the first hash format version. Other hash versions following
// this scheme may come later. These versioned hash schemes are implemented
// in the getproviders package; for example, "h1:" is implemented in
// getproviders.HashV1 .
//
// There is also a legacy hash format which is just a lowercase-hex-encoded
// SHA256 hash of the official upstream .zip file for the selected version.
// We'll allow as that a stop-gap until we can upgrade Terraform Registry
// to support the new scheme, but is non-ideal because we can verify it only
// when we have the original .zip file exactly; we can't verify a local
// directory containing the unpacked contents of that .zip file.
//
// We ideally want to populate hashes for all available architectures at
// once, by referring to the signed checksums file in the upstream
// registry. In that ideal case it's possible to later work with the same
// configuration on a different platform while still verifying the hashes.
// However, installation from any method other than an origin registry
// means we can only populate the hash for the current platform, and so
// it won't be possible to verify a subsequent installation of the same
// provider on a different platform.
hashes map[getproviders.Platform][]string
}
// Provider returns the address of the provider this lock applies to.
func (l *ProviderLock) Provider() addrs.Provider {
return l.addr
}
// Version returns the currently-selected version for the corresponding provider.
func (l *ProviderLock) Version() getproviders.Version {
return l.version
}
// VersionConstraints returns the version constraints that were recorded as
// being used to choose the version returned by Version.
//
// These version constraints are not authoritative for future selections and
// are included only so Terraform can detect if the constraints in
// configuration have changed since a selection was made, and thus hint to the
// user that they may need to run terraform init -upgrade to apply the new
// constraints.
func (l *ProviderLock) VersionConstraints() getproviders.VersionConstraints {
return l.versionConstraints
}
// HashesForPlatform returns all of the package hashes that were recorded for
// the given platform when this lock was created. If no hashes were recorded
// for that platform, the result is a zero-length slice.
//
// If your intent is to verify a package against the recorded hashes, use
// PreferredHashForPlatform to get a single hash which the current version
// of Terraform considers the strongest of the available hashes, which is
// the one that must pass for verification to be considered successful.
//
// Do not modify the backing array of the returned slice.
func (l *ProviderLock) HashesForPlatform(platform getproviders.Platform) []string {
return l.hashes[platform]
}
// PreferredHashForPlatform returns a single hash which must match for a package
// for the given platform to be considered valid, or an empty string if there
// are no acceptable hashes recorded for the given platform.
func (l *ProviderLock) PreferredHashForPlatform(platform getproviders.Platform) string {
return getproviders.PreferredHash(l.hashes[platform])
}

View File

@ -0,0 +1,322 @@
package depsfile
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/tfdiags"
"github.com/hashicorp/terraform/version"
)
// LoadLocksFromFile reads locks from the given file, expecting it to be a
// valid dependency lock file, or returns error diagnostics explaining why
// that was not possible.
//
// The returned locks are a snapshot of what was present on disk at the time
// the method was called. It does not take into account any subsequent writes
// to the file, whether through this package's functions or by external
// writers.
//
// If the returned diagnostics contains errors then the returned Locks may
// be incomplete or invalid.
func LoadLocksFromFile(filename string) (*Locks, tfdiags.Diagnostics) {
ret := NewLocks()
var diags tfdiags.Diagnostics
parser := hclparse.NewParser()
f, hclDiags := parser.ParseHCLFile(filename)
ret.sources = parser.Sources()
diags = diags.Append(hclDiags)
moreDiags := decodeLocksFromHCL(ret, f.Body)
diags = diags.Append(moreDiags)
return ret, diags
}
// SaveLocksToFile writes the given locks object to the given file,
// entirely replacing any content already in that file, or returns error
// diagnostics explaining why that was not possible.
//
// SaveLocksToFile attempts an atomic replacement of the file, as an aid
// to external tools such as text editor integrations that might be monitoring
// the file as a signal to invalidate cached metadata. Consequently, other
// temporary files may be temporarily created in the same directory as the
// given filename during the operation.
func SaveLocksToFile(locks *Locks, filename string) tfdiags.Diagnostics {
panic("SaveLocksToFile is not implemented yet")
}
func decodeLocksFromHCL(locks *Locks, body hcl.Body) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
content, hclDiags := body.Content(&hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{
Type: "provider",
LabelNames: []string{"source_addr"},
},
// "module" is just a placeholder for future enhancement, so we
// can mostly-ignore the this block type we intend to add in
// future, but warn in case someone tries to use one e.g. if they
// downgraded to an earlier version of Terraform.
{
Type: "module",
LabelNames: []string{"path"},
},
},
})
diags = diags.Append(hclDiags)
seenProviders := make(map[addrs.Provider]hcl.Range)
seenModule := false
for _, block := range content.Blocks {
switch block.Type {
case "provider":
lock, moreDiags := decodeProviderLockFromHCL(block)
diags = diags.Append(moreDiags)
if lock == nil {
continue
}
if previousRng, exists := seenProviders[lock.addr]; exists {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate provider lock",
Detail: fmt.Sprintf("This lockfile already declared a lock for provider %s at %s.", lock.addr.String(), previousRng.String()),
Subject: block.TypeRange.Ptr(),
})
continue
}
locks.providers[lock.addr] = lock
seenProviders[lock.addr] = block.DefRange
case "module":
// We'll just take the first module block to use for a single warning,
// because that's sufficient to get the point across without swamping
// the output with warning noise.
if !seenModule {
currentVersion := version.SemVer.String()
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Dependency locks for modules are not yet supported",
Detail: fmt.Sprintf("Terraform v%s only supports dependency locks for providers, not for modules. This configuration may be intended for a later version of Terraform that also supports dependency locks for modules.", currentVersion),
Subject: block.TypeRange.Ptr(),
})
seenModule = true
}
default:
// Shouldn't get here because this should be exhaustive for
// all of the block types in the schema above.
}
}
return diags
}
func decodeProviderLockFromHCL(block *hcl.Block) (*ProviderLock, tfdiags.Diagnostics) {
ret := &ProviderLock{}
var diags tfdiags.Diagnostics
rawAddr := block.Labels[0]
addr, moreDiags := addrs.ParseProviderSourceString(rawAddr)
if moreDiags.HasErrors() {
// The diagnostics from ParseProviderSourceString are, as the name
// suggests, written with an intended audience of someone who is
// writing a "source" attribute in a provider requirement, not
// our lock file. Therefore we're using a less helpful, fixed error
// here, which is non-ideal but hopefully okay for now because we
// don't intend end-users to typically be hand-editing these anyway.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid provider source address",
Detail: "The provider source address for a provider lock must be a valid, fully-qualified address of the form \"hostname/namespace/type\".",
Subject: block.LabelRanges[0].Ptr(),
})
return nil, diags
}
if canonAddr := addr.String(); canonAddr != rawAddr {
// We also require the provider addresses in the lock file to be
// written in fully-qualified canonical form, so that it's totally
// clear to a reader which provider each block relates to. Again,
// we expect hand-editing of these to be atypical so it's reasonable
// to be stricter in parsing these than we would be in the main
// configuration.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Non-normalized provider source address",
Detail: fmt.Sprintf("The provider source address for this provider lock must be written as %q, the fully-qualified and normalized form.", canonAddr),
Subject: block.LabelRanges[0].Ptr(),
})
return nil, diags
}
ret.addr = addr
// We'll decode the block body using gohcl, because we don't have any
// special structural validation to do other than what gohcl will naturally
// do for us here.
type RawHashes struct {
// We'll consume all of the attributes and process them dynamically.
Hashes hcl.Attributes `hcl:",remain"`
}
type Provider struct {
Version hcl.Expression `hcl:"version,attr"`
VersionConstraints hcl.Expression `hcl:"constraints,attr"`
HashesBlock *RawHashes `hcl:"hashes,block"`
}
var raw Provider
hclDiags := gohcl.DecodeBody(block.Body, nil, &raw)
diags = diags.Append(hclDiags)
if hclDiags.HasErrors() {
return ret, diags
}
version, moreDiags := decodeProviderVersionArgument(addr, raw.Version)
ret.version = version
diags = diags.Append(moreDiags)
constraints, moreDiags := decodeProviderVersionConstraintsArgument(addr, raw.VersionConstraints)
ret.versionConstraints = constraints
diags = diags.Append(moreDiags)
if raw.HashesBlock != nil {
hashes, moreDiags := decodeProviderHashesArgument(addr, raw.HashesBlock.Hashes)
ret.hashes = hashes
diags = diags.Append(moreDiags)
}
return ret, diags
}
func decodeProviderVersionArgument(provider addrs.Provider, expr hcl.Expression) (getproviders.Version, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
var raw *string
hclDiags := gohcl.DecodeExpression(expr, nil, &raw)
diags = diags.Append(hclDiags)
if hclDiags.HasErrors() {
return getproviders.UnspecifiedVersion, diags
}
if raw == nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing required argument",
Detail: "A provider lock block must contain a \"version\" argument.",
Subject: expr.Range().Ptr(), // the range for a missing argument's expression is the body's missing item range
})
return getproviders.UnspecifiedVersion, diags
}
version, err := getproviders.ParseVersion(*raw)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid version number",
Detail: fmt.Sprintf("The selected version number for provider %s is invalid: %s.", provider, err),
Subject: expr.Range().Ptr(),
})
}
if canon := version.String(); canon != *raw {
// Canonical forms are required in the lock file, to reduce the risk
// that a file diff will show changes that are entirely cosmetic.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid version number",
Detail: fmt.Sprintf("The selected version number for provider %s must be written in normalized form: %q.", provider, canon),
Subject: expr.Range().Ptr(),
})
}
return version, diags
}
func decodeProviderVersionConstraintsArgument(provider addrs.Provider, expr hcl.Expression) (getproviders.VersionConstraints, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
var raw *string
hclDiags := gohcl.DecodeExpression(expr, nil, &raw)
diags = diags.Append(hclDiags)
if hclDiags.HasErrors() {
return nil, diags
}
if raw == nil {
// It's okay to omit this argument.
return nil, diags
}
constraints, err := getproviders.ParseVersionConstraints(*raw)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid version constraints",
Detail: fmt.Sprintf("The recorded version constraints for provider %s are invalid: %s.", provider, err),
Subject: expr.Range().Ptr(),
})
}
if canon := getproviders.VersionConstraintsString(constraints); canon != *raw {
// Canonical forms are required in the lock file, to reduce the risk
// that a file diff will show changes that are entirely cosmetic.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid version constraints",
Detail: fmt.Sprintf("The recorded version constraints for provider %s must be written in normalized form: %q.", provider, canon),
Subject: expr.Range().Ptr(),
})
}
return constraints, diags
}
func decodeProviderHashesArgument(provider addrs.Provider, attrs hcl.Attributes) (map[getproviders.Platform][]string, tfdiags.Diagnostics) {
if len(attrs) == 0 {
return nil, nil
}
ret := make(map[getproviders.Platform][]string, len(attrs))
var diags tfdiags.Diagnostics
for platformStr, attr := range attrs {
platform, err := getproviders.ParsePlatform(platformStr)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid provider hash platform",
Detail: fmt.Sprintf("The string %q is not a valid platform specification: %s.", platformStr, err),
Subject: attr.NameRange.Ptr(),
})
continue
}
if canon := platform.String(); canon != platformStr {
// Canonical forms are required in the lock file, to reduce the risk
// that a file diff will show changes that are entirely cosmetic.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid provider hash platform",
Detail: fmt.Sprintf("The platform specification %q must be written in the normalized form %q.", platformStr, canon),
Subject: attr.NameRange.Ptr(),
})
continue
}
var hashes []string
hclDiags := gohcl.DecodeExpression(attr.Expr, nil, &hashes)
diags = diags.Append(hclDiags)
if hclDiags.HasErrors() {
continue
}
// We don't validate the hashes, because we expect to support different
// hash formats over time and so we'll assume any that are in formats
// we don't understand are from later Terraform versions, or perhaps
// from an origin registry that is offering hashes aimed at a later
// Terraform version.
ret[platform] = hashes
}
return ret, diags
}

View File

@ -0,0 +1,164 @@
package depsfile
import (
"bufio"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/tfdiags"
)
func TestLoadLocksFromFile(t *testing.T) {
// For ease of test maintenance we treat every file under
// test-data/locks-files as a test case which is subject
// at least to testing that it produces an expected set
// of diagnostics represented via specially-formatted comments
// in the fixture files (which might be the empty set, if
// there are no such comments).
//
// Some of the files also have additional assertions that
// are encoded in the test code below. These must pass
// in addition to the standard diagnostics tests, if present.
files, err := ioutil.ReadDir("testdata/locks-files")
if err != nil {
t.Fatal(err.Error())
}
for _, info := range files {
testName := filepath.Base(info.Name())
filename := filepath.Join("testdata/locks-files", testName)
t.Run(testName, func(t *testing.T) {
f, err := os.Open(filename)
if err != nil {
t.Fatal(err.Error())
}
defer f.Close()
const errorPrefix = "# ERROR: "
const warningPrefix = "# WARNING: "
wantErrors := map[int]string{}
wantWarnings := map[int]string{}
sc := bufio.NewScanner(f)
lineNum := 1
for sc.Scan() {
l := sc.Text()
if pos := strings.Index(l, errorPrefix); pos != -1 {
wantSummary := l[pos+len(errorPrefix):]
wantErrors[lineNum] = wantSummary
}
if pos := strings.Index(l, warningPrefix); pos != -1 {
wantSummary := l[pos+len(warningPrefix):]
wantWarnings[lineNum] = wantSummary
}
lineNum++
}
if err := sc.Err(); err != nil {
t.Fatal(err.Error())
}
locks, diags := LoadLocksFromFile(filename)
gotErrors := map[int]string{}
gotWarnings := map[int]string{}
for _, diag := range diags {
summary := diag.Description().Summary
if diag.Source().Subject == nil {
// We don't expect any sourceless diagnostics here.
t.Errorf("unexpected sourceless diagnostic: %s", summary)
continue
}
lineNum := diag.Source().Subject.Start.Line
switch sev := diag.Severity(); sev {
case tfdiags.Error:
gotErrors[lineNum] = summary
case tfdiags.Warning:
gotWarnings[lineNum] = summary
default:
t.Errorf("unexpected diagnostic severity %s", sev)
}
}
if diff := cmp.Diff(wantErrors, gotErrors); diff != "" {
t.Errorf("wrong errors\n%s", diff)
}
if diff := cmp.Diff(wantWarnings, gotWarnings); diff != "" {
t.Errorf("wrong warnings\n%s", diff)
}
switch testName {
// These are the file-specific test assertions. Not all files
// need custom test assertions in addition to the standard
// diagnostics assertions implemented above, so the cases here
// don't need to be exhaustive for all files.
//
// Please keep these in alphabetical order so the list is easy
// to scan!
case "empty.hcl":
if got, want := len(locks.providers), 0; got != want {
t.Errorf("wrong number of providers %d; want %d", got, want)
}
case "valid-provider-locks.hcl":
if got, want := len(locks.providers), 3; got != want {
t.Errorf("wrong number of providers %d; want %d", got, want)
}
t.Run("version-only", func(t *testing.T) {
if lock := locks.Provider(addrs.MustParseProviderSourceString("terraform.io/test/version-only")); lock != nil {
if got, want := lock.Version().String(), "1.0.0"; got != want {
t.Errorf("wrong version\ngot: %s\nwant: %s", got, want)
}
if got, want := getproviders.VersionConstraintsString(lock.VersionConstraints()), ""; got != want {
t.Errorf("wrong version constraints\ngot: %s\nwant: %s", got, want)
}
if got, want := len(lock.hashes), 0; got != want {
t.Errorf("wrong number of hashes %d; want %d", got, want)
}
}
})
t.Run("version-and-constraints", func(t *testing.T) {
if lock := locks.Provider(addrs.MustParseProviderSourceString("terraform.io/test/version-and-constraints")); lock != nil {
if got, want := lock.Version().String(), "1.2.0"; got != want {
t.Errorf("wrong version\ngot: %s\nwant: %s", got, want)
}
if got, want := getproviders.VersionConstraintsString(lock.VersionConstraints()), "~> 1.2"; got != want {
t.Errorf("wrong version constraints\ngot: %s\nwant: %s", got, want)
}
if got, want := len(lock.hashes), 0; got != want {
t.Errorf("wrong number of hashes %d; want %d", got, want)
}
}
})
t.Run("all-the-things", func(t *testing.T) {
if lock := locks.Provider(addrs.MustParseProviderSourceString("terraform.io/test/all-the-things")); lock != nil {
if got, want := lock.Version().String(), "3.0.10"; got != want {
t.Errorf("wrong version\ngot: %s\nwant: %s", got, want)
}
if got, want := getproviders.VersionConstraintsString(lock.VersionConstraints()), ">= 3.0.2"; got != want {
t.Errorf("wrong version constraints\ngot: %s\nwant: %s", got, want)
}
wantHashes := map[getproviders.Platform][]string{
{OS: "amigaos", Arch: "m68k"}: {
"placeholder-hash-1",
},
{OS: "tos", Arch: "m68k"}: {
"placeholder-hash-2",
"placeholder-hash-3",
},
}
if diff := cmp.Diff(wantHashes, lock.hashes); diff != "" {
t.Errorf("wrong hashes\n%s", diff)
}
}
})
}
})
}
}

View File

@ -0,0 +1,18 @@
package depsfile
// LockFilePath is the path, relative to a configuration's root module
// directory, where Terraform expects to find the dependency lock file for
// that configuration.
//
// This file is intended to be kept in version control, so it lives directly
// in the root module directory. The ".terraform" prefix is intended to
// suggest that it's metadata about several types of objects that ultimately
// end up in the .terraform directory after running "terraform init".
const LockFilePath = ".terraform.lock.hcl"
// DevOverrideFilePath is the path, relative to a configuration's root module
// directory, where Terraform will look to find a possible override file that
// represents a request to temporarily (within a single working directory only)
// use specific local directories in place of packages that would normally
// need to be installed from a remote location.
const DevOverrideFilePath = ".terraform/dev-overrides.hcl"

View File

@ -0,0 +1 @@
# An empty locks file is a bit of a degenerate case, but it is valid.

View File

@ -0,0 +1,34 @@
provider "" { # ERROR: Invalid provider source address
}
provider "hashicorp/aws" { # ERROR: Non-normalized provider source address
}
provider "aws" { # ERROR: Non-normalized provider source address
}
provider "too/many/parts/here" { # ERROR: Invalid provider source address
}
provider "Registry.terraform.io/example/example" { # ERROR: Non-normalized provider source address
}
provider "registry.terraform.io/eXample/example" { # ERROR: Non-normalized provider source address
}
provider "registry.terraform.io/example/Example" { # ERROR: Non-normalized provider source address
}
provider "this/one/okay" {
version = "1.0.0"
}
provider "this/one/okay" { # ERROR: Duplicate provider lock
}

View File

@ -0,0 +1,30 @@
provider "terraform.io/test/foo" {
version = "" # ERROR: Invalid version number
}
provider "terraform.io/test/bar" {
# The "v" prefix is not expected here
version = "v1.0.0" # ERROR: Invalid version number
}
provider "terraform.io/test/baz" {
# Must be written in the canonical form, with three parts
version = "1.0" # ERROR: Invalid version number
}
provider "terraform.io/test/boop" {
# Must be written in the canonical form, with three parts
version = "1" # ERROR: Invalid version number
}
provider "terraform.io/test/blep" {
# Mustn't use redundant extra zero padding
version = "1.02" # ERROR: Invalid version number
}
provider "terraform.io/test/huzzah" { # ERROR: Missing required argument
}
provider "terraform.io/test/huzznot" {
version = null # ERROR: Missing required argument
}

View File

@ -0,0 +1,2 @@
doodad "blah" { # ERROR: Unsupported block type
}

View File

@ -0,0 +1,24 @@
provider "terraform.io/test/version-only" {
version = "1.0.0"
}
provider "terraform.io/test/version-and-constraints" {
version = "1.2.0"
constraints = "~> 1.2"
}
provider "terraform.io/test/all-the-things" {
version = "3.0.10"
constraints = ">= 3.0.2"
hashes {
amigaos_m68k = [
"placeholder-hash-1",
]
tos_m68k = [
"placeholder-hash-2",
"placeholder-hash-3",
]
}
}

View File

@ -346,7 +346,7 @@ func (l PackageMetaList) FilterProviderPlatformExactVersion(provider addrs.Provi
return ret
}
// VersionConstraintsString returns a UI-oriented string representation of
// VersionConstraintsString returns a canonical string representation of
// a VersionConstraints value.
func VersionConstraintsString(spec VersionConstraints) string {
// (we have our own function for this because the upstream versions
@ -355,6 +355,12 @@ func VersionConstraintsString(spec VersionConstraints) string {
// function to do this later, but having this in here avoids blocking on
// that and this is the sort of thing that is unlikely to need ongoing
// maintenance because the version constraint syntax is unlikely to change.)
//
// ParseVersionConstraints allows some variations for convenience, but the
// return value from this function serves as the normalized form of a
// particular version constraint, which is the form we require in dependency
// lock files. Therefore the canonical forms produced here are a compatibility
// constraint for the dependency lock file parser.
var b strings.Builder
for i, sel := range spec {