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 }