github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/depsfile/locks_file.go (about)

     1  package depsfile
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  
     7  	"github.com/hashicorp/hcl/v2"
     8  	"github.com/hashicorp/hcl/v2/gohcl"
     9  	"github.com/hashicorp/hcl/v2/hclparse"
    10  	"github.com/hashicorp/hcl/v2/hclsyntax"
    11  	"github.com/hashicorp/hcl/v2/hclwrite"
    12  	"github.com/zclconf/go-cty/cty"
    13  
    14  	"github.com/hashicorp/terraform/internal/addrs"
    15  	"github.com/hashicorp/terraform/internal/getproviders"
    16  	"github.com/hashicorp/terraform/internal/replacefile"
    17  	"github.com/hashicorp/terraform/internal/tfdiags"
    18  	"github.com/hashicorp/terraform/version"
    19  )
    20  
    21  // LoadLocksFromFile reads locks from the given file, expecting it to be a
    22  // valid dependency lock file, or returns error diagnostics explaining why
    23  // that was not possible.
    24  //
    25  // The returned locks are a snapshot of what was present on disk at the time
    26  // the method was called. It does not take into account any subsequent writes
    27  // to the file, whether through this package's functions or by external
    28  // writers.
    29  //
    30  // If the returned diagnostics contains errors then the returned Locks may
    31  // be incomplete or invalid.
    32  func LoadLocksFromFile(filename string) (*Locks, tfdiags.Diagnostics) {
    33  	return loadLocks(func(parser *hclparse.Parser) (*hcl.File, hcl.Diagnostics) {
    34  		return parser.ParseHCLFile(filename)
    35  	})
    36  }
    37  
    38  // LoadLocksFromBytes reads locks from the given byte array, pretending that
    39  // it was read from the given filename.
    40  //
    41  // The constraints and behaviors are otherwise the same as for
    42  // LoadLocksFromFile. LoadLocksFromBytes is primarily to allow more convenient
    43  // integration testing (avoiding creating temporary files on disk); if you
    44  // are writing non-test code, consider whether LoadLocksFromFile might be
    45  // more appropriate to call.
    46  func LoadLocksFromBytes(src []byte, filename string) (*Locks, tfdiags.Diagnostics) {
    47  	return loadLocks(func(parser *hclparse.Parser) (*hcl.File, hcl.Diagnostics) {
    48  		return parser.ParseHCL(src, filename)
    49  	})
    50  }
    51  
    52  func loadLocks(loadParse func(*hclparse.Parser) (*hcl.File, hcl.Diagnostics)) (*Locks, tfdiags.Diagnostics) {
    53  	ret := NewLocks()
    54  
    55  	var diags tfdiags.Diagnostics
    56  
    57  	parser := hclparse.NewParser()
    58  	f, hclDiags := loadParse(parser)
    59  	ret.sources = parser.Sources()
    60  	diags = diags.Append(hclDiags)
    61  	if f == nil {
    62  		// If we encountered an error loading the file then those errors
    63  		// should already be in diags from the above, but the file might
    64  		// also be nil itself and so we can't decode from it.
    65  		return ret, diags
    66  	}
    67  
    68  	moreDiags := decodeLocksFromHCL(ret, f.Body)
    69  	diags = diags.Append(moreDiags)
    70  	return ret, diags
    71  }
    72  
    73  // SaveLocksToFile writes the given locks object to the given file,
    74  // entirely replacing any content already in that file, or returns error
    75  // diagnostics explaining why that was not possible.
    76  //
    77  // SaveLocksToFile attempts an atomic replacement of the file, as an aid
    78  // to external tools such as text editor integrations that might be monitoring
    79  // the file as a signal to invalidate cached metadata. Consequently, other
    80  // temporary files may be temporarily created in the same directory as the
    81  // given filename during the operation.
    82  func SaveLocksToFile(locks *Locks, filename string) tfdiags.Diagnostics {
    83  	var diags tfdiags.Diagnostics
    84  
    85  	// In other uses of the "hclwrite" package we typically try to make
    86  	// surgical updates to the author's existing files, preserving their
    87  	// block ordering, comments, etc. We intentionally don't do that here
    88  	// to reinforce the fact that this file primarily belongs to Terraform,
    89  	// and to help ensure that VCS diffs of the file primarily reflect
    90  	// changes that actually affect functionality rather than just cosmetic
    91  	// changes, by maintaining it in a highly-normalized form.
    92  
    93  	f := hclwrite.NewEmptyFile()
    94  	rootBody := f.Body()
    95  
    96  	// End-users _may_ edit the lock file in exceptional situations, like
    97  	// working around potential dependency selection bugs, but we intend it
    98  	// to be primarily maintained automatically by the "terraform init"
    99  	// command.
   100  	rootBody.AppendUnstructuredTokens(hclwrite.Tokens{
   101  		{
   102  			Type:  hclsyntax.TokenComment,
   103  			Bytes: []byte("# This file is maintained automatically by \"terraform init\".\n"),
   104  		},
   105  		{
   106  			Type:  hclsyntax.TokenComment,
   107  			Bytes: []byte("# Manual edits may be lost in future updates.\n"),
   108  		},
   109  	})
   110  
   111  	providers := make([]addrs.Provider, 0, len(locks.providers))
   112  	for provider := range locks.providers {
   113  		providers = append(providers, provider)
   114  	}
   115  	sort.Slice(providers, func(i, j int) bool {
   116  		return providers[i].LessThan(providers[j])
   117  	})
   118  
   119  	for _, provider := range providers {
   120  		lock := locks.providers[provider]
   121  		rootBody.AppendNewline()
   122  		block := rootBody.AppendNewBlock("provider", []string{lock.addr.String()})
   123  		body := block.Body()
   124  		body.SetAttributeValue("version", cty.StringVal(lock.version.String()))
   125  		if constraintsStr := getproviders.VersionConstraintsString(lock.versionConstraints); constraintsStr != "" {
   126  			body.SetAttributeValue("constraints", cty.StringVal(constraintsStr))
   127  		}
   128  		if len(lock.hashes) != 0 {
   129  			hashToks := encodeHashSetTokens(lock.hashes)
   130  			body.SetAttributeRaw("hashes", hashToks)
   131  		}
   132  	}
   133  
   134  	newContent := f.Bytes()
   135  
   136  	err := replacefile.AtomicWriteFile(filename, newContent, 0644)
   137  	if err != nil {
   138  		diags = diags.Append(tfdiags.Sourceless(
   139  			tfdiags.Error,
   140  			"Failed to update dependency lock file",
   141  			fmt.Sprintf("Error while writing new dependency lock information to %s: %s.", filename, err),
   142  		))
   143  		return diags
   144  	}
   145  
   146  	return diags
   147  }
   148  
   149  func decodeLocksFromHCL(locks *Locks, body hcl.Body) tfdiags.Diagnostics {
   150  	var diags tfdiags.Diagnostics
   151  
   152  	content, hclDiags := body.Content(&hcl.BodySchema{
   153  		Blocks: []hcl.BlockHeaderSchema{
   154  			{
   155  				Type:       "provider",
   156  				LabelNames: []string{"source_addr"},
   157  			},
   158  
   159  			// "module" is just a placeholder for future enhancement, so we
   160  			// can mostly-ignore the this block type we intend to add in
   161  			// future, but warn in case someone tries to use one e.g. if they
   162  			// downgraded to an earlier version of Terraform.
   163  			{
   164  				Type:       "module",
   165  				LabelNames: []string{"path"},
   166  			},
   167  		},
   168  	})
   169  	diags = diags.Append(hclDiags)
   170  
   171  	seenProviders := make(map[addrs.Provider]hcl.Range)
   172  	seenModule := false
   173  	for _, block := range content.Blocks {
   174  
   175  		switch block.Type {
   176  		case "provider":
   177  			lock, moreDiags := decodeProviderLockFromHCL(block)
   178  			diags = diags.Append(moreDiags)
   179  			if lock == nil {
   180  				continue
   181  			}
   182  			if previousRng, exists := seenProviders[lock.addr]; exists {
   183  				diags = diags.Append(&hcl.Diagnostic{
   184  					Severity: hcl.DiagError,
   185  					Summary:  "Duplicate provider lock",
   186  					Detail:   fmt.Sprintf("This lockfile already declared a lock for provider %s at %s.", lock.addr.String(), previousRng.String()),
   187  					Subject:  block.TypeRange.Ptr(),
   188  				})
   189  				continue
   190  			}
   191  			locks.providers[lock.addr] = lock
   192  			seenProviders[lock.addr] = block.DefRange
   193  
   194  		case "module":
   195  			// We'll just take the first module block to use for a single warning,
   196  			// because that's sufficient to get the point across without swamping
   197  			// the output with warning noise.
   198  			if !seenModule {
   199  				currentVersion := version.SemVer.String()
   200  				diags = diags.Append(&hcl.Diagnostic{
   201  					Severity: hcl.DiagWarning,
   202  					Summary:  "Dependency locks for modules are not yet supported",
   203  					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),
   204  					Subject:  block.TypeRange.Ptr(),
   205  				})
   206  				seenModule = true
   207  			}
   208  
   209  		default:
   210  			// Shouldn't get here because this should be exhaustive for
   211  			// all of the block types in the schema above.
   212  		}
   213  
   214  	}
   215  
   216  	return diags
   217  }
   218  
   219  func decodeProviderLockFromHCL(block *hcl.Block) (*ProviderLock, tfdiags.Diagnostics) {
   220  	ret := &ProviderLock{}
   221  	var diags tfdiags.Diagnostics
   222  
   223  	rawAddr := block.Labels[0]
   224  	addr, moreDiags := addrs.ParseProviderSourceString(rawAddr)
   225  	if moreDiags.HasErrors() {
   226  		// The diagnostics from ParseProviderSourceString are, as the name
   227  		// suggests, written with an intended audience of someone who is
   228  		// writing a "source" attribute in a provider requirement, not
   229  		// our lock file. Therefore we're using a less helpful, fixed error
   230  		// here, which is non-ideal but hopefully okay for now because we
   231  		// don't intend end-users to typically be hand-editing these anyway.
   232  		diags = diags.Append(&hcl.Diagnostic{
   233  			Severity: hcl.DiagError,
   234  			Summary:  "Invalid provider source address",
   235  			Detail:   "The provider source address for a provider lock must be a valid, fully-qualified address of the form \"hostname/namespace/type\".",
   236  			Subject:  block.LabelRanges[0].Ptr(),
   237  		})
   238  		return nil, diags
   239  	}
   240  	if !ProviderIsLockable(addr) {
   241  		if addr.IsBuiltIn() {
   242  			// A specialized error for built-in providers, because we have an
   243  			// explicit explanation for why those are not allowed.
   244  			diags = diags.Append(&hcl.Diagnostic{
   245  				Severity: hcl.DiagError,
   246  				Summary:  "Invalid provider source address",
   247  				Detail:   fmt.Sprintf("Cannot lock a version for built-in provider %s. Built-in providers are bundled inside Terraform itself, so you can't select a version for them independently of the Terraform release you are currently running.", addr),
   248  				Subject:  block.LabelRanges[0].Ptr(),
   249  			})
   250  			return nil, diags
   251  		}
   252  		// Otherwise, we'll use a generic error message.
   253  		diags = diags.Append(&hcl.Diagnostic{
   254  			Severity: hcl.DiagError,
   255  			Summary:  "Invalid provider source address",
   256  			Detail:   fmt.Sprintf("Provider source address %s is a special provider that is not eligible for dependency locking.", addr),
   257  			Subject:  block.LabelRanges[0].Ptr(),
   258  		})
   259  		return nil, diags
   260  	}
   261  	if canonAddr := addr.String(); canonAddr != rawAddr {
   262  		// We also require the provider addresses in the lock file to be
   263  		// written in fully-qualified canonical form, so that it's totally
   264  		// clear to a reader which provider each block relates to. Again,
   265  		// we expect hand-editing of these to be atypical so it's reasonable
   266  		// to be stricter in parsing these than we would be in the main
   267  		// configuration.
   268  		diags = diags.Append(&hcl.Diagnostic{
   269  			Severity: hcl.DiagError,
   270  			Summary:  "Non-normalized provider source address",
   271  			Detail:   fmt.Sprintf("The provider source address for this provider lock must be written as %q, the fully-qualified and normalized form.", canonAddr),
   272  			Subject:  block.LabelRanges[0].Ptr(),
   273  		})
   274  		return nil, diags
   275  	}
   276  
   277  	ret.addr = addr
   278  
   279  	content, hclDiags := block.Body.Content(&hcl.BodySchema{
   280  		Attributes: []hcl.AttributeSchema{
   281  			{Name: "version", Required: true},
   282  			{Name: "constraints"},
   283  			{Name: "hashes"},
   284  		},
   285  	})
   286  	diags = diags.Append(hclDiags)
   287  
   288  	version, moreDiags := decodeProviderVersionArgument(addr, content.Attributes["version"])
   289  	ret.version = version
   290  	diags = diags.Append(moreDiags)
   291  
   292  	constraints, moreDiags := decodeProviderVersionConstraintsArgument(addr, content.Attributes["constraints"])
   293  	ret.versionConstraints = constraints
   294  	diags = diags.Append(moreDiags)
   295  
   296  	hashes, moreDiags := decodeProviderHashesArgument(addr, content.Attributes["hashes"])
   297  	ret.hashes = hashes
   298  	diags = diags.Append(moreDiags)
   299  
   300  	return ret, diags
   301  }
   302  
   303  func decodeProviderVersionArgument(provider addrs.Provider, attr *hcl.Attribute) (getproviders.Version, tfdiags.Diagnostics) {
   304  	var diags tfdiags.Diagnostics
   305  	if attr == nil {
   306  		// It's not okay to omit this argument, but the caller should already
   307  		// have generated diagnostics about that.
   308  		return getproviders.UnspecifiedVersion, diags
   309  	}
   310  	expr := attr.Expr
   311  
   312  	var raw *string
   313  	hclDiags := gohcl.DecodeExpression(expr, nil, &raw)
   314  	diags = diags.Append(hclDiags)
   315  	if hclDiags.HasErrors() {
   316  		return getproviders.UnspecifiedVersion, diags
   317  	}
   318  	if raw == nil {
   319  		diags = diags.Append(&hcl.Diagnostic{
   320  			Severity: hcl.DiagError,
   321  			Summary:  "Missing required argument",
   322  			Detail:   "A provider lock block must contain a \"version\" argument.",
   323  			Subject:  expr.Range().Ptr(), // the range for a missing argument's expression is the body's missing item range
   324  		})
   325  		return getproviders.UnspecifiedVersion, diags
   326  	}
   327  	version, err := getproviders.ParseVersion(*raw)
   328  	if err != nil {
   329  		diags = diags.Append(&hcl.Diagnostic{
   330  			Severity: hcl.DiagError,
   331  			Summary:  "Invalid provider version number",
   332  			Detail:   fmt.Sprintf("The selected version number for provider %s is invalid: %s.", provider, err),
   333  			Subject:  expr.Range().Ptr(),
   334  		})
   335  	}
   336  	if canon := version.String(); canon != *raw {
   337  		// Canonical forms are required in the lock file, to reduce the risk
   338  		// that a file diff will show changes that are entirely cosmetic.
   339  		diags = diags.Append(&hcl.Diagnostic{
   340  			Severity: hcl.DiagError,
   341  			Summary:  "Invalid provider version number",
   342  			Detail:   fmt.Sprintf("The selected version number for provider %s must be written in normalized form: %q.", provider, canon),
   343  			Subject:  expr.Range().Ptr(),
   344  		})
   345  	}
   346  	return version, diags
   347  }
   348  
   349  func decodeProviderVersionConstraintsArgument(provider addrs.Provider, attr *hcl.Attribute) (getproviders.VersionConstraints, tfdiags.Diagnostics) {
   350  	var diags tfdiags.Diagnostics
   351  	if attr == nil {
   352  		// It's okay to omit this argument.
   353  		return nil, diags
   354  	}
   355  	expr := attr.Expr
   356  
   357  	var raw string
   358  	hclDiags := gohcl.DecodeExpression(expr, nil, &raw)
   359  	diags = diags.Append(hclDiags)
   360  	if hclDiags.HasErrors() {
   361  		return nil, diags
   362  	}
   363  	constraints, err := getproviders.ParseVersionConstraints(raw)
   364  	if err != nil {
   365  		diags = diags.Append(&hcl.Diagnostic{
   366  			Severity: hcl.DiagError,
   367  			Summary:  "Invalid provider version constraints",
   368  			Detail:   fmt.Sprintf("The recorded version constraints for provider %s are invalid: %s.", provider, err),
   369  			Subject:  expr.Range().Ptr(),
   370  		})
   371  	}
   372  	if canon := getproviders.VersionConstraintsString(constraints); canon != raw {
   373  		// Canonical forms are required in the lock file, to reduce the risk
   374  		// that a file diff will show changes that are entirely cosmetic.
   375  		diags = diags.Append(&hcl.Diagnostic{
   376  			Severity: hcl.DiagError,
   377  			Summary:  "Invalid provider version constraints",
   378  			Detail:   fmt.Sprintf("The recorded version constraints for provider %s must be written in normalized form: %q.", provider, canon),
   379  			Subject:  expr.Range().Ptr(),
   380  		})
   381  	}
   382  
   383  	return constraints, diags
   384  }
   385  
   386  func decodeProviderHashesArgument(provider addrs.Provider, attr *hcl.Attribute) ([]getproviders.Hash, tfdiags.Diagnostics) {
   387  	var diags tfdiags.Diagnostics
   388  	if attr == nil {
   389  		// It's okay to omit this argument.
   390  		return nil, diags
   391  	}
   392  	expr := attr.Expr
   393  
   394  	// We'll decode this argument using the HCL static analysis mode, because
   395  	// there's no reason for the hashes list to be dynamic and this way we can
   396  	// give more precise feedback on individual elements that are invalid,
   397  	// with direct source locations.
   398  	hashExprs, hclDiags := hcl.ExprList(expr)
   399  	diags = diags.Append(hclDiags)
   400  	if hclDiags.HasErrors() {
   401  		return nil, diags
   402  	}
   403  	if len(hashExprs) == 0 {
   404  		diags = diags.Append(&hcl.Diagnostic{
   405  			Severity: hcl.DiagError,
   406  			Summary:  "Invalid provider hash set",
   407  			Detail:   "The \"hashes\" argument must either be omitted or contain at least one hash value.",
   408  			Subject:  expr.Range().Ptr(),
   409  		})
   410  		return nil, diags
   411  	}
   412  
   413  	ret := make([]getproviders.Hash, 0, len(hashExprs))
   414  	for _, hashExpr := range hashExprs {
   415  		var raw string
   416  		hclDiags := gohcl.DecodeExpression(hashExpr, nil, &raw)
   417  		diags = diags.Append(hclDiags)
   418  		if hclDiags.HasErrors() {
   419  			continue
   420  		}
   421  
   422  		hash, err := getproviders.ParseHash(raw)
   423  		if err != nil {
   424  			diags = diags.Append(&hcl.Diagnostic{
   425  				Severity: hcl.DiagError,
   426  				Summary:  "Invalid provider hash string",
   427  				Detail:   fmt.Sprintf("Cannot interpret %q as a provider hash: %s.", raw, err),
   428  				Subject:  expr.Range().Ptr(),
   429  			})
   430  			continue
   431  		}
   432  
   433  		ret = append(ret, hash)
   434  	}
   435  
   436  	return ret, diags
   437  }
   438  
   439  func encodeHashSetTokens(hashes []getproviders.Hash) hclwrite.Tokens {
   440  	// We'll generate the source code in a low-level way here (direct
   441  	// token manipulation) because it's desirable to maintain exactly
   442  	// the layout implemented here so that diffs against the locks
   443  	// file are easy to read; we don't want potential future changes to
   444  	// hclwrite to inadvertently introduce whitespace changes here.
   445  	ret := hclwrite.Tokens{
   446  		{
   447  			Type:  hclsyntax.TokenOBrack,
   448  			Bytes: []byte{'['},
   449  		},
   450  		{
   451  			Type:  hclsyntax.TokenNewline,
   452  			Bytes: []byte{'\n'},
   453  		},
   454  	}
   455  
   456  	// Although lock.hashes is a slice, we de-dupe and sort it on
   457  	// initialization so it's normalized for interpretation as a logical
   458  	// set, and so we can just trust it's already in a good order here.
   459  	for _, hash := range hashes {
   460  		hashVal := cty.StringVal(hash.String())
   461  		ret = append(ret, hclwrite.TokensForValue(hashVal)...)
   462  		ret = append(ret, hclwrite.Tokens{
   463  			{
   464  				Type:  hclsyntax.TokenComma,
   465  				Bytes: []byte{','},
   466  			},
   467  			{
   468  				Type:  hclsyntax.TokenNewline,
   469  				Bytes: []byte{'\n'},
   470  			},
   471  		}...)
   472  	}
   473  	ret = append(ret, &hclwrite.Token{
   474  		Type:  hclsyntax.TokenCBrack,
   475  		Bytes: []byte{']'},
   476  	})
   477  
   478  	return ret
   479  }