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