github.com/opentofu/opentofu@v1.7.1/internal/configs/test_file.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package configs 7 8 import ( 9 "fmt" 10 11 "github.com/hashicorp/hcl/v2" 12 "github.com/hashicorp/hcl/v2/gohcl" 13 "github.com/hashicorp/hcl/v2/hclsyntax" 14 15 "github.com/opentofu/opentofu/internal/addrs" 16 "github.com/opentofu/opentofu/internal/getmodules" 17 "github.com/opentofu/opentofu/internal/tfdiags" 18 ) 19 20 // TestCommand represents the OpenTofu a given run block will execute, plan 21 // or apply. Defaults to apply. 22 type TestCommand rune 23 24 // TestMode represents the plan mode that OpenTofu will use for a given run 25 // block, normal or refresh-only. Defaults to normal. 26 type TestMode rune 27 28 const ( 29 // ApplyTestCommand causes the run block to execute a OpenTofu apply 30 // operation. 31 ApplyTestCommand TestCommand = 0 32 33 // PlanTestCommand causes the run block to execute a OpenTofu plan 34 // operation. 35 PlanTestCommand TestCommand = 'P' 36 37 // NormalTestMode causes the run block to execute in plans.NormalMode. 38 NormalTestMode TestMode = 0 39 40 // RefreshOnlyTestMode causes the run block to execute in 41 // plans.RefreshOnlyMode. 42 RefreshOnlyTestMode TestMode = 'R' 43 ) 44 45 // TestFile represents a single test file within a `tofu test` execution. 46 // 47 // A test file is made up of a sequential list of run blocks, each designating 48 // a command to execute and a series of validations to check after the command. 49 type TestFile struct { 50 // Variables defines a set of global variable definitions that should be set 51 // for every run block within the test file. 52 Variables map[string]hcl.Expression 53 54 // Providers defines a set of providers that are available to run blocks 55 // within this test file. 56 // 57 // If empty, tests should use the default providers for the module under 58 // test. 59 Providers map[string]*Provider 60 61 // Runs defines the sequential list of run blocks that should be executed in 62 // order. 63 Runs []*TestRun 64 65 VariablesDeclRange hcl.Range 66 } 67 68 // TestRun represents a single run block within a test file. 69 // 70 // Each run block represents a single OpenTofu command to be executed and a set 71 // of validations to run after the command. 72 type TestRun struct { 73 Name string 74 75 // Command is the OpenTofu command to execute. 76 // 77 // One of ['apply', 'plan']. 78 Command TestCommand 79 80 // Options contains the embedded plan options that will affect the given 81 // Command. These should map to the options documented here: 82 // - https://opentofu.org/docs/cli/commands/plan/#planning-options 83 // 84 // Note, that the Variables are a top level concept and not embedded within 85 // the options despite being listed as plan options in the documentation. 86 Options *TestRunOptions 87 88 // Variables defines a set of variable definitions for this command. 89 // 90 // Any variables specified locally that clash with the global variables will 91 // take precedence over the global definition. 92 Variables map[string]hcl.Expression 93 94 // Providers specifies the set of providers that should be loaded into the 95 // module for this run block. 96 // 97 // Providers specified here must be configured in one of the provider blocks 98 // for this file. If empty, the run block will load the default providers 99 // for the module under test. 100 Providers []PassedProviderConfig 101 102 // CheckRules defines the list of assertions/validations that should be 103 // checked by this run block. 104 CheckRules []*CheckRule 105 106 // Module defines an address of another module that should be loaded and 107 // executed as part of this run block instead of the module under test. 108 // 109 // In the initial version of the testing framework we will only support 110 // loading alternate modules from local directories or the registry. 111 Module *TestRunModuleCall 112 113 // ConfigUnderTest describes the configuration this run block should execute 114 // against. 115 // 116 // In typical cases, this will be null and the config under test is the 117 // configuration within the directory the tofu test command is 118 // executing within. However, when Module is set the config under test is 119 // whichever config is defined by Module. This field is then set during the 120 // configuration load process and should be used when the test is executed. 121 ConfigUnderTest *Config 122 123 // ExpectFailures should be a list of checkable objects that are expected 124 // to report a failure from their custom conditions as part of this test 125 // run. 126 ExpectFailures []hcl.Traversal 127 128 NameDeclRange hcl.Range 129 VariablesDeclRange hcl.Range 130 DeclRange hcl.Range 131 } 132 133 // Validate does a very simple and cursory check across the run block to look 134 // for simple issues we can highlight early on. 135 func (run *TestRun) Validate() tfdiags.Diagnostics { 136 var diags tfdiags.Diagnostics 137 138 // For now, we only want to make sure all the ExpectFailure references are 139 // the correct kind of reference. 140 for _, traversal := range run.ExpectFailures { 141 142 reference, refDiags := addrs.ParseRefFromTestingScope(traversal) 143 diags = diags.Append(refDiags) 144 if refDiags.HasErrors() { 145 continue 146 } 147 148 switch reference.Subject.(type) { 149 // You can only reference outputs, inputs, checks, and resources. 150 case addrs.OutputValue, addrs.InputVariable, addrs.Check, addrs.ResourceInstance, addrs.Resource: 151 // Do nothing, these are okay! 152 default: 153 diags = diags.Append(&hcl.Diagnostic{ 154 Severity: hcl.DiagError, 155 Summary: "Invalid `expect_failures` reference", 156 Detail: fmt.Sprintf("You cannot expect failures from %s. You can only expect failures from checkable objects such as input variables, output values, check blocks, managed resources and data sources.", reference.Subject.String()), 157 Subject: reference.SourceRange.ToHCL().Ptr(), 158 }) 159 } 160 161 } 162 163 return diags 164 } 165 166 // TestRunModuleCall specifies which module should be executed by a given run 167 // block. 168 type TestRunModuleCall struct { 169 // Source is the source of the module to test. 170 Source addrs.ModuleSource 171 172 // Version is the version of the module to load from the registry. 173 Version VersionConstraint 174 175 DeclRange hcl.Range 176 SourceDeclRange hcl.Range 177 } 178 179 // TestRunOptions contains the plan options for a given run block. 180 type TestRunOptions struct { 181 // Mode is the planning mode to run in. One of ['normal', 'refresh-only']. 182 Mode TestMode 183 184 // Refresh is analogous to the -refresh=false OpenTofu plan option. 185 Refresh bool 186 187 // Replace is analogous to the -refresh=ADDRESS OpenTofu plan option. 188 Replace []hcl.Traversal 189 190 // Target is analogous to the -target=ADDRESS OpenTofu plan option. 191 Target []hcl.Traversal 192 193 DeclRange hcl.Range 194 } 195 196 func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) { 197 var diags hcl.Diagnostics 198 199 content, contentDiags := body.Content(testFileSchema) 200 diags = append(diags, contentDiags...) 201 202 tf := TestFile{ 203 Providers: make(map[string]*Provider), 204 } 205 206 for _, block := range content.Blocks { 207 switch block.Type { 208 case "run": 209 run, runDiags := decodeTestRunBlock(block) 210 diags = append(diags, runDiags...) 211 if !runDiags.HasErrors() { 212 tf.Runs = append(tf.Runs, run) 213 } 214 case "variables": 215 if tf.Variables != nil { 216 diags = append(diags, &hcl.Diagnostic{ 217 Severity: hcl.DiagError, 218 Summary: "Multiple \"variables\" blocks", 219 Detail: fmt.Sprintf("This test file already has a variables block defined at %s.", tf.VariablesDeclRange), 220 Subject: block.DefRange.Ptr(), 221 }) 222 continue 223 } 224 225 tf.Variables = make(map[string]hcl.Expression) 226 tf.VariablesDeclRange = block.DefRange 227 228 vars, varsDiags := block.Body.JustAttributes() 229 diags = append(diags, varsDiags...) 230 for _, v := range vars { 231 tf.Variables[v.Name] = v.Expr 232 } 233 case "provider": 234 provider, providerDiags := decodeProviderBlock(block) 235 diags = append(diags, providerDiags...) 236 if provider != nil { 237 tf.Providers[provider.moduleUniqueKey()] = provider 238 } 239 } 240 } 241 242 return &tf, diags 243 } 244 245 func decodeTestRunBlock(block *hcl.Block) (*TestRun, hcl.Diagnostics) { 246 var diags hcl.Diagnostics 247 248 content, contentDiags := block.Body.Content(testRunBlockSchema) 249 diags = append(diags, contentDiags...) 250 251 r := TestRun{ 252 Name: block.Labels[0], 253 NameDeclRange: block.LabelRanges[0], 254 DeclRange: block.DefRange, 255 } 256 257 if !hclsyntax.ValidIdentifier(r.Name) { 258 diags = append(diags, &hcl.Diagnostic{ 259 Severity: hcl.DiagError, 260 Summary: "Invalid run block name", 261 Detail: badIdentifierDetail, 262 Subject: &block.LabelRanges[0], 263 }) 264 } 265 266 for _, block := range content.Blocks { 267 switch block.Type { 268 case "assert": 269 cr, crDiags := decodeCheckRuleBlock(block, false) 270 diags = append(diags, crDiags...) 271 if !crDiags.HasErrors() { 272 r.CheckRules = append(r.CheckRules, cr) 273 } 274 case "plan_options": 275 if r.Options != nil { 276 diags = append(diags, &hcl.Diagnostic{ 277 Severity: hcl.DiagError, 278 Summary: "Multiple \"plan_options\" blocks", 279 Detail: fmt.Sprintf("This run block already has a plan_options block defined at %s.", r.Options.DeclRange), 280 Subject: block.DefRange.Ptr(), 281 }) 282 continue 283 } 284 285 opts, optsDiags := decodeTestRunOptionsBlock(block) 286 diags = append(diags, optsDiags...) 287 if !optsDiags.HasErrors() { 288 r.Options = opts 289 } 290 case "variables": 291 if r.Variables != nil { 292 diags = append(diags, &hcl.Diagnostic{ 293 Severity: hcl.DiagError, 294 Summary: "Multiple \"variables\" blocks", 295 Detail: fmt.Sprintf("This run block already has a variables block defined at %s.", r.VariablesDeclRange), 296 Subject: block.DefRange.Ptr(), 297 }) 298 continue 299 } 300 301 r.Variables = make(map[string]hcl.Expression) 302 r.VariablesDeclRange = block.DefRange 303 304 vars, varsDiags := block.Body.JustAttributes() 305 diags = append(diags, varsDiags...) 306 for _, v := range vars { 307 r.Variables[v.Name] = v.Expr 308 } 309 case "module": 310 if r.Module != nil { 311 diags = append(diags, &hcl.Diagnostic{ 312 Severity: hcl.DiagError, 313 Summary: "Multiple \"module\" blocks", 314 Detail: fmt.Sprintf("This run block already has a module block defined at %s.", r.Module.DeclRange), 315 Subject: block.DefRange.Ptr(), 316 }) 317 } 318 319 module, moduleDiags := decodeTestRunModuleBlock(block) 320 diags = append(diags, moduleDiags...) 321 if !moduleDiags.HasErrors() { 322 r.Module = module 323 } 324 } 325 } 326 327 if r.Variables == nil { 328 // There is no distinction between a nil map of variables or an empty 329 // map, but we can avoid any potential nil pointer exceptions by just 330 // creating an empty map. 331 r.Variables = make(map[string]hcl.Expression) 332 } 333 334 if r.Options == nil { 335 // Create an options with default values if the user didn't specify 336 // anything. 337 r.Options = &TestRunOptions{ 338 Mode: NormalTestMode, 339 Refresh: true, 340 } 341 } 342 343 if attr, exists := content.Attributes["command"]; exists { 344 switch hcl.ExprAsKeyword(attr.Expr) { 345 case "apply": 346 r.Command = ApplyTestCommand 347 case "plan": 348 r.Command = PlanTestCommand 349 default: 350 diags = append(diags, &hcl.Diagnostic{ 351 Severity: hcl.DiagError, 352 Summary: "Invalid \"command\" keyword", 353 Detail: "The \"command\" argument requires one of the following keywords without quotes: apply or plan.", 354 Subject: attr.Expr.Range().Ptr(), 355 }) 356 } 357 } else { 358 r.Command = ApplyTestCommand // Default to apply 359 } 360 361 if attr, exists := content.Attributes["providers"]; exists { 362 providers, providerDiags := decodePassedProviderConfigs(attr) 363 diags = append(diags, providerDiags...) 364 r.Providers = append(r.Providers, providers...) 365 } 366 367 if attr, exists := content.Attributes["expect_failures"]; exists { 368 failures, failDiags := decodeDependsOn(attr) 369 diags = append(diags, failDiags...) 370 r.ExpectFailures = failures 371 } 372 373 return &r, diags 374 } 375 376 func decodeTestRunModuleBlock(block *hcl.Block) (*TestRunModuleCall, hcl.Diagnostics) { 377 var diags hcl.Diagnostics 378 379 content, contentDiags := block.Body.Content(testRunModuleBlockSchema) 380 diags = append(diags, contentDiags...) 381 382 module := TestRunModuleCall{ 383 DeclRange: block.DefRange, 384 } 385 386 haveVersionArg := false 387 if attr, exists := content.Attributes["version"]; exists { 388 var versionDiags hcl.Diagnostics 389 module.Version, versionDiags = decodeVersionConstraint(attr) 390 diags = append(diags, versionDiags...) 391 haveVersionArg = true 392 } 393 394 if attr, exists := content.Attributes["source"]; exists { 395 module.SourceDeclRange = attr.Range 396 397 var raw string 398 rawDiags := gohcl.DecodeExpression(attr.Expr, nil, &raw) 399 diags = append(diags, rawDiags...) 400 if !rawDiags.HasErrors() { 401 var err error 402 if haveVersionArg { 403 module.Source, err = addrs.ParseModuleSourceRegistry(raw) 404 } else { 405 module.Source, err = addrs.ParseModuleSource(raw) 406 } 407 if err != nil { 408 // NOTE: We leave mc.SourceAddr as nil for any situation where the 409 // source attribute is invalid, so any code which tries to carefully 410 // use the partial result of a failed config decode must be 411 // resilient to that. 412 module.Source = nil 413 414 // NOTE: In practice it's actually very unlikely to end up here, 415 // because our source address parser can turn just about any string 416 // into some sort of remote package address, and so for most errors 417 // we'll detect them only during module installation. There are 418 // still a _few_ purely-syntax errors we can catch at parsing time, 419 // though, mostly related to remote package sub-paths and local 420 // paths. 421 switch err := err.(type) { 422 case *getmodules.MaybeRelativePathErr: 423 diags = append(diags, &hcl.Diagnostic{ 424 Severity: hcl.DiagError, 425 Summary: "Invalid module source address", 426 Detail: fmt.Sprintf( 427 "OpenTofu failed to determine your intended installation method for remote module package %q.\n\nIf you intended this as a path relative to the current module, use \"./%s\" instead. The \"./\" prefix indicates that the address is a relative filesystem path.", 428 err.Addr, err.Addr, 429 ), 430 Subject: module.SourceDeclRange.Ptr(), 431 }) 432 default: 433 if haveVersionArg { 434 // In this case we'll include some extra context that 435 // we assumed a registry source address due to the 436 // version argument. 437 diags = append(diags, &hcl.Diagnostic{ 438 Severity: hcl.DiagError, 439 Summary: "Invalid registry module source address", 440 Detail: fmt.Sprintf("Failed to parse module registry address: %s.\n\nOpenTofu assumed that you intended a module registry source address because you also set the argument \"version\", which applies only to registry modules.", err), 441 Subject: module.SourceDeclRange.Ptr(), 442 }) 443 } else { 444 diags = append(diags, &hcl.Diagnostic{ 445 Severity: hcl.DiagError, 446 Summary: "Invalid module source address", 447 Detail: fmt.Sprintf("Failed to parse module source address: %s.", err), 448 Subject: module.SourceDeclRange.Ptr(), 449 }) 450 } 451 } 452 } 453 454 switch module.Source.(type) { 455 case addrs.ModuleSourceRemote: 456 // We only support local or registry modules when loading 457 // modules directly from alternate sources during a test 458 // execution. 459 diags = append(diags, &hcl.Diagnostic{ 460 Severity: hcl.DiagError, 461 Summary: "Invalid module source address", 462 Detail: "Only local or registry module sources are currently supported from within test run blocks.", 463 Subject: module.SourceDeclRange.Ptr(), 464 }) 465 } 466 } 467 } else { 468 // Must have a source attribute. 469 diags = append(diags, &hcl.Diagnostic{ 470 Severity: hcl.DiagError, 471 Summary: "Missing \"source\" attribute for module block", 472 Detail: "You must specify a source attribute when executing alternate modules during test executions.", 473 Subject: module.DeclRange.Ptr(), 474 }) 475 } 476 477 return &module, diags 478 } 479 480 func decodeTestRunOptionsBlock(block *hcl.Block) (*TestRunOptions, hcl.Diagnostics) { 481 var diags hcl.Diagnostics 482 483 content, contentDiags := block.Body.Content(testRunOptionsBlockSchema) 484 diags = append(diags, contentDiags...) 485 486 opts := TestRunOptions{ 487 DeclRange: block.DefRange, 488 } 489 490 if attr, exists := content.Attributes["mode"]; exists { 491 switch hcl.ExprAsKeyword(attr.Expr) { 492 case "refresh-only": 493 opts.Mode = RefreshOnlyTestMode 494 case "normal": 495 opts.Mode = NormalTestMode 496 default: 497 diags = append(diags, &hcl.Diagnostic{ 498 Severity: hcl.DiagError, 499 Summary: "Invalid \"mode\" keyword", 500 Detail: "The \"mode\" argument requires one of the following keywords without quotes: normal or refresh-only", 501 Subject: attr.Expr.Range().Ptr(), 502 }) 503 } 504 } else { 505 opts.Mode = NormalTestMode // Default to normal 506 } 507 508 if attr, exists := content.Attributes["refresh"]; exists { 509 diags = append(diags, gohcl.DecodeExpression(attr.Expr, nil, &opts.Refresh)...) 510 } else { 511 // Defaults to true. 512 opts.Refresh = true 513 } 514 515 if attr, exists := content.Attributes["replace"]; exists { 516 reps, repsDiags := decodeDependsOn(attr) 517 diags = append(diags, repsDiags...) 518 opts.Replace = reps 519 } 520 521 if attr, exists := content.Attributes["target"]; exists { 522 tars, tarsDiags := decodeDependsOn(attr) 523 diags = append(diags, tarsDiags...) 524 opts.Target = tars 525 } 526 527 if !opts.Refresh && opts.Mode == RefreshOnlyTestMode { 528 // These options are incompatible. 529 diags = append(diags, &hcl.Diagnostic{ 530 Severity: hcl.DiagError, 531 Summary: "Incompatible plan options", 532 Detail: "The \"refresh\" option cannot be set to false when running a test in \"refresh-only\" mode.", 533 Subject: content.Attributes["refresh"].Range.Ptr(), 534 }) 535 } 536 537 return &opts, diags 538 } 539 540 var testFileSchema = &hcl.BodySchema{ 541 Blocks: []hcl.BlockHeaderSchema{ 542 { 543 Type: "run", 544 LabelNames: []string{"name"}, 545 }, 546 { 547 Type: "provider", 548 LabelNames: []string{"name"}, 549 }, 550 { 551 Type: "variables", 552 }, 553 }, 554 } 555 556 var testRunBlockSchema = &hcl.BodySchema{ 557 Attributes: []hcl.AttributeSchema{ 558 {Name: "command"}, 559 {Name: "providers"}, 560 {Name: "expect_failures"}, 561 }, 562 Blocks: []hcl.BlockHeaderSchema{ 563 { 564 Type: "plan_options", 565 }, 566 { 567 Type: "assert", 568 }, 569 { 570 Type: "variables", 571 }, 572 { 573 Type: "module", 574 }, 575 }, 576 } 577 578 var testRunOptionsBlockSchema = &hcl.BodySchema{ 579 Attributes: []hcl.AttributeSchema{ 580 {Name: "mode"}, 581 {Name: "refresh"}, 582 {Name: "replace"}, 583 {Name: "target"}, 584 }, 585 } 586 587 var testRunModuleBlockSchema = &hcl.BodySchema{ 588 Attributes: []hcl.AttributeSchema{ 589 {Name: "source"}, 590 {Name: "version"}, 591 }, 592 }