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 }