github.com/opentofu/opentofu@v1.7.1/internal/tofu/node_resource_validate.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 tofu 7 8 import ( 9 "fmt" 10 "strings" 11 12 "github.com/hashicorp/hcl/v2" 13 "github.com/zclconf/go-cty/cty" 14 15 "github.com/opentofu/opentofu/internal/addrs" 16 "github.com/opentofu/opentofu/internal/configs" 17 "github.com/opentofu/opentofu/internal/configs/configschema" 18 "github.com/opentofu/opentofu/internal/didyoumean" 19 "github.com/opentofu/opentofu/internal/instances" 20 "github.com/opentofu/opentofu/internal/lang" 21 "github.com/opentofu/opentofu/internal/providers" 22 "github.com/opentofu/opentofu/internal/provisioners" 23 "github.com/opentofu/opentofu/internal/tfdiags" 24 ) 25 26 // NodeValidatableResource represents a resource that is used for validation 27 // only. 28 type NodeValidatableResource struct { 29 *NodeAbstractResource 30 } 31 32 var ( 33 _ GraphNodeModuleInstance = (*NodeValidatableResource)(nil) 34 _ GraphNodeExecutable = (*NodeValidatableResource)(nil) 35 _ GraphNodeReferenceable = (*NodeValidatableResource)(nil) 36 _ GraphNodeReferencer = (*NodeValidatableResource)(nil) 37 _ GraphNodeConfigResource = (*NodeValidatableResource)(nil) 38 _ GraphNodeAttachResourceConfig = (*NodeValidatableResource)(nil) 39 _ GraphNodeAttachProviderMetaConfigs = (*NodeValidatableResource)(nil) 40 ) 41 42 func (n *NodeValidatableResource) Path() addrs.ModuleInstance { 43 // There is no expansion during validation, so we evaluate everything as 44 // single module instances. 45 return n.Addr.Module.UnkeyedInstanceShim() 46 } 47 48 // GraphNodeEvalable 49 func (n *NodeValidatableResource) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) { 50 if n.Config == nil { 51 return diags 52 } 53 54 diags = diags.Append(n.validateResource(ctx)) 55 56 diags = diags.Append(n.validateCheckRules(ctx, n.Config)) 57 58 if managed := n.Config.Managed; managed != nil { 59 // Validate all the provisioners 60 for _, p := range managed.Provisioners { 61 // Create a local shallow copy of the provisioner 62 provisioner := *p 63 64 if p.Connection == nil { 65 provisioner.Connection = n.Config.Managed.Connection 66 } else if n.Config.Managed.Connection != nil { 67 // Merge the connection with n.Config.Managed.Connection, but only in 68 // our local provisioner, as it will only be used by 69 // "validateProvisioner" 70 connection := &configs.Connection{} 71 *connection = *p.Connection 72 connection.Config = configs.MergeBodies(n.Config.Managed.Connection.Config, connection.Config) 73 provisioner.Connection = connection 74 } 75 76 // Validate Provisioner Config 77 diags = diags.Append(n.validateProvisioner(ctx, &provisioner)) 78 if diags.HasErrors() { 79 return diags 80 } 81 } 82 } 83 return diags 84 } 85 86 // validateProvisioner validates the configuration of a provisioner belonging to 87 // a resource. The provisioner config is expected to contain the merged 88 // connection configurations. 89 func (n *NodeValidatableResource) validateProvisioner(ctx EvalContext, p *configs.Provisioner) tfdiags.Diagnostics { 90 var diags tfdiags.Diagnostics 91 92 provisioner, err := ctx.Provisioner(p.Type) 93 if err != nil { 94 diags = diags.Append(err) 95 return diags 96 } 97 98 if provisioner == nil { 99 return diags.Append(fmt.Errorf("provisioner %s not initialized", p.Type)) 100 } 101 provisionerSchema, err := ctx.ProvisionerSchema(p.Type) 102 if err != nil { 103 return diags.Append(fmt.Errorf("failed to read schema for provisioner %s: %w", p.Type, err)) 104 } 105 if provisionerSchema == nil { 106 return diags.Append(fmt.Errorf("provisioner %s has no schema", p.Type)) 107 } 108 109 // Validate the provisioner's own config first 110 configVal, _, configDiags := n.evaluateBlock(ctx, p.Config, provisionerSchema) 111 diags = diags.Append(configDiags) 112 113 if configVal == cty.NilVal { 114 // Should never happen for a well-behaved EvaluateBlock implementation 115 return diags.Append(fmt.Errorf("EvaluateBlock returned nil value")) 116 } 117 118 // Use unmarked value for validate request 119 unmarkedConfigVal, _ := configVal.UnmarkDeep() 120 req := provisioners.ValidateProvisionerConfigRequest{ 121 Config: unmarkedConfigVal, 122 } 123 124 resp := provisioner.ValidateProvisionerConfig(req) 125 diags = diags.Append(resp.Diagnostics) 126 127 if p.Connection != nil { 128 // We can't comprehensively validate the connection config since its 129 // final structure is decided by the communicator and we can't instantiate 130 // that until we have a complete instance state. However, we *can* catch 131 // configuration keys that are not valid for *any* communicator, catching 132 // typos early rather than waiting until we actually try to run one of 133 // the resource's provisioners. 134 _, _, connDiags := n.evaluateBlock(ctx, p.Connection.Config, connectionBlockSupersetSchema) 135 diags = diags.Append(connDiags) 136 } 137 return diags 138 } 139 140 func (n *NodeValidatableResource) evaluateBlock(ctx EvalContext, body hcl.Body, schema *configschema.Block) (cty.Value, hcl.Body, tfdiags.Diagnostics) { 141 keyData, selfAddr := n.stubRepetitionData(n.Config.Count != nil, n.Config.ForEach != nil) 142 143 return ctx.EvaluateBlock(body, schema, selfAddr, keyData) 144 } 145 146 // connectionBlockSupersetSchema is a schema representing the superset of all 147 // possible arguments for "connection" blocks across all supported connection 148 // types. 149 // 150 // This currently lives here because we've not yet updated our communicator 151 // subsystem to be aware of schema itself. Once that is done, we can remove 152 // this and use a type-specific schema from the communicator to validate 153 // exactly what is expected for a given connection type. 154 var connectionBlockSupersetSchema = &configschema.Block{ 155 Attributes: map[string]*configschema.Attribute{ 156 // NOTE: "type" is not included here because it's treated special 157 // by the config loader and stored away in a separate field. 158 159 // Common attributes for both connection types 160 "host": { 161 Type: cty.String, 162 Required: true, 163 }, 164 "type": { 165 Type: cty.String, 166 Optional: true, 167 }, 168 "user": { 169 Type: cty.String, 170 Optional: true, 171 }, 172 "password": { 173 Type: cty.String, 174 Optional: true, 175 }, 176 "port": { 177 Type: cty.Number, 178 Optional: true, 179 }, 180 "timeout": { 181 Type: cty.String, 182 Optional: true, 183 }, 184 "script_path": { 185 Type: cty.String, 186 Optional: true, 187 }, 188 // For type=ssh only (enforced in ssh communicator) 189 "target_platform": { 190 Type: cty.String, 191 Optional: true, 192 }, 193 "private_key": { 194 Type: cty.String, 195 Optional: true, 196 }, 197 "certificate": { 198 Type: cty.String, 199 Optional: true, 200 }, 201 "host_key": { 202 Type: cty.String, 203 Optional: true, 204 }, 205 "agent": { 206 Type: cty.Bool, 207 Optional: true, 208 }, 209 "agent_identity": { 210 Type: cty.String, 211 Optional: true, 212 }, 213 "proxy_scheme": { 214 Type: cty.String, 215 Optional: true, 216 }, 217 "proxy_host": { 218 Type: cty.String, 219 Optional: true, 220 }, 221 "proxy_port": { 222 Type: cty.Number, 223 Optional: true, 224 }, 225 "proxy_user_name": { 226 Type: cty.String, 227 Optional: true, 228 }, 229 "proxy_user_password": { 230 Type: cty.String, 231 Optional: true, 232 }, 233 "bastion_host": { 234 Type: cty.String, 235 Optional: true, 236 }, 237 "bastion_host_key": { 238 Type: cty.String, 239 Optional: true, 240 }, 241 "bastion_port": { 242 Type: cty.Number, 243 Optional: true, 244 }, 245 "bastion_user": { 246 Type: cty.String, 247 Optional: true, 248 }, 249 "bastion_password": { 250 Type: cty.String, 251 Optional: true, 252 }, 253 "bastion_private_key": { 254 Type: cty.String, 255 Optional: true, 256 }, 257 "bastion_certificate": { 258 Type: cty.String, 259 Optional: true, 260 }, 261 262 // For type=winrm only (enforced in winrm communicator) 263 "https": { 264 Type: cty.Bool, 265 Optional: true, 266 }, 267 "insecure": { 268 Type: cty.Bool, 269 Optional: true, 270 }, 271 "cacert": { 272 Type: cty.String, 273 Optional: true, 274 }, 275 "use_ntlm": { 276 Type: cty.Bool, 277 Optional: true, 278 }, 279 }, 280 } 281 282 func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diagnostics { 283 var diags tfdiags.Diagnostics 284 285 provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider) 286 diags = diags.Append(err) 287 if diags.HasErrors() { 288 return diags 289 } 290 291 keyData := EvalDataForNoInstanceKey 292 293 switch { 294 case n.Config.Count != nil: 295 // If the config block has count, we'll evaluate with an unknown 296 // number as count.index so we can still type check even though 297 // we won't expand count until the plan phase. 298 keyData = InstanceKeyEvalData{ 299 CountIndex: cty.UnknownVal(cty.Number), 300 } 301 302 // Basic type-checking of the count argument. More complete validation 303 // of this will happen when we DynamicExpand during the plan walk. 304 countDiags := validateCount(ctx, n.Config.Count) 305 diags = diags.Append(countDiags) 306 307 case n.Config.ForEach != nil: 308 keyData = InstanceKeyEvalData{ 309 EachKey: cty.UnknownVal(cty.String), 310 EachValue: cty.UnknownVal(cty.DynamicPseudoType), 311 } 312 313 // Evaluate the for_each expression here so we can expose the diagnostics 314 forEachDiags := validateForEach(ctx, n.Config.ForEach) 315 diags = diags.Append(forEachDiags) 316 } 317 318 diags = diags.Append(validateDependsOn(ctx, n.Config.DependsOn)) 319 320 // Validate the provider_meta block for the provider this resource 321 // belongs to, if there is one. 322 // 323 // Note: this will return an error for every resource a provider 324 // uses in a module, if the provider_meta for that module is 325 // incorrect. The only way to solve this that we've found is to 326 // insert a new ProviderMeta graph node in the graph, and make all 327 // that provider's resources in the module depend on the node. That's 328 // an awful heavy hammer to swing for this feature, which should be 329 // used only in limited cases with heavy coordination with the 330 // OpenTofu team, so we're going to defer that solution for a future 331 // enhancement to this functionality. 332 /* 333 if n.ProviderMetas != nil { 334 if m, ok := n.ProviderMetas[n.ProviderAddr.ProviderConfig.Type]; ok && m != nil { 335 // if the provider doesn't support this feature, throw an error 336 if (*n.ProviderSchema).ProviderMeta == nil { 337 diags = diags.Append(&hcl.Diagnostic{ 338 Severity: hcl.DiagError, 339 Summary: fmt.Sprintf("Provider %s doesn't support provider_meta", cfg.ProviderConfigAddr()), 340 Detail: fmt.Sprintf("The resource %s belongs to a provider that doesn't support provider_meta blocks", n.Addr), 341 Subject: &m.ProviderRange, 342 }) 343 } else { 344 _, _, metaDiags := ctx.EvaluateBlock(m.Config, (*n.ProviderSchema).ProviderMeta, nil, EvalDataForNoInstanceKey) 345 diags = diags.Append(metaDiags) 346 } 347 } 348 } 349 */ 350 // BUG(paddy): we're not validating provider_meta blocks on EvalValidate right now 351 // because the ProviderAddr for the resource isn't available on the EvalValidate 352 // struct. 353 354 // Provider entry point varies depending on resource mode, because 355 // managed resources and data resources are two distinct concepts 356 // in the provider abstraction. 357 switch n.Config.Mode { 358 case addrs.ManagedResourceMode: 359 schema, _ := providerSchema.SchemaForResourceType(n.Config.Mode, n.Config.Type) 360 if schema == nil { 361 var suggestion string 362 if dSchema, _ := providerSchema.SchemaForResourceType(addrs.DataResourceMode, n.Config.Type); dSchema != nil { 363 suggestion = fmt.Sprintf("\n\nDid you intend to use the data source %q? If so, declare this using a \"data\" block instead of a \"resource\" block.", n.Config.Type) 364 } else if len(providerSchema.ResourceTypes) > 0 { 365 suggestions := make([]string, 0, len(providerSchema.ResourceTypes)) 366 for name := range providerSchema.ResourceTypes { 367 suggestions = append(suggestions, name) 368 } 369 if suggestion = didyoumean.NameSuggestion(n.Config.Type, suggestions); suggestion != "" { 370 suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) 371 } 372 } 373 374 diags = diags.Append(&hcl.Diagnostic{ 375 Severity: hcl.DiagError, 376 Summary: "Invalid resource type", 377 Detail: fmt.Sprintf("The provider %s does not support resource type %q.%s", n.Provider().ForDisplay(), n.Config.Type, suggestion), 378 Subject: &n.Config.TypeRange, 379 }) 380 return diags 381 } 382 383 configVal, _, valDiags := ctx.EvaluateBlock(n.Config.Config, schema, nil, keyData) 384 diags = diags.Append(valDiags) 385 if valDiags.HasErrors() { 386 return diags 387 } 388 389 if n.Config.Managed != nil { // can be nil only in tests with poorly-configured mocks 390 for _, traversal := range n.Config.Managed.IgnoreChanges { 391 // validate the ignore_changes traversals apply. 392 moreDiags := schema.StaticValidateTraversal(traversal) 393 diags = diags.Append(moreDiags) 394 395 // ignore_changes cannot be used for Computed attributes, 396 // unless they are also Optional. 397 // If the traversal was valid, convert it to a cty.Path and 398 // use that to check whether the Attribute is Computed and 399 // non-Optional. 400 if !diags.HasErrors() { 401 path := traversalToPath(traversal) 402 403 attrSchema := schema.AttributeByPath(path) 404 405 if attrSchema != nil && !attrSchema.Optional && attrSchema.Computed { 406 // ignore_changes uses absolute traversal syntax in config despite 407 // using relative traversals, so we strip the leading "." added by 408 // FormatCtyPath for a better error message. 409 attrDisplayPath := strings.TrimPrefix(tfdiags.FormatCtyPath(path), ".") 410 411 diags = diags.Append(&hcl.Diagnostic{ 412 Severity: hcl.DiagWarning, 413 Summary: "Redundant ignore_changes element", 414 Detail: fmt.Sprintf("Adding an attribute name to ignore_changes tells OpenTofu to ignore future changes to the argument in configuration after the object has been created, retaining the value originally configured.\n\nThe attribute %s is decided by the provider alone and therefore there can be no configured value to compare with. Including this attribute in ignore_changes has no effect. Remove the attribute from ignore_changes to quiet this warning.", attrDisplayPath), 415 Subject: &n.Config.TypeRange, 416 }) 417 } 418 } 419 } 420 } 421 422 // Use unmarked value for validate request 423 unmarkedConfigVal, _ := configVal.UnmarkDeep() 424 req := providers.ValidateResourceConfigRequest{ 425 TypeName: n.Config.Type, 426 Config: unmarkedConfigVal, 427 } 428 429 resp := provider.ValidateResourceConfig(req) 430 diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config, n.Addr.String())) 431 432 case addrs.DataResourceMode: 433 schema, _ := providerSchema.SchemaForResourceType(n.Config.Mode, n.Config.Type) 434 if schema == nil { 435 var suggestion string 436 if dSchema, _ := providerSchema.SchemaForResourceType(addrs.ManagedResourceMode, n.Config.Type); dSchema != nil { 437 suggestion = fmt.Sprintf("\n\nDid you intend to use the managed resource type %q? If so, declare this using a \"resource\" block instead of a \"data\" block.", n.Config.Type) 438 } else if len(providerSchema.DataSources) > 0 { 439 suggestions := make([]string, 0, len(providerSchema.DataSources)) 440 for name := range providerSchema.DataSources { 441 suggestions = append(suggestions, name) 442 } 443 if suggestion = didyoumean.NameSuggestion(n.Config.Type, suggestions); suggestion != "" { 444 suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) 445 } 446 } 447 448 diags = diags.Append(&hcl.Diagnostic{ 449 Severity: hcl.DiagError, 450 Summary: "Invalid data source", 451 Detail: fmt.Sprintf("The provider %s does not support data source %q.%s", n.Provider().ForDisplay(), n.Config.Type, suggestion), 452 Subject: &n.Config.TypeRange, 453 }) 454 return diags 455 } 456 457 configVal, _, valDiags := ctx.EvaluateBlock(n.Config.Config, schema, nil, keyData) 458 diags = diags.Append(valDiags) 459 if valDiags.HasErrors() { 460 return diags 461 } 462 463 // Use unmarked value for validate request 464 unmarkedConfigVal, _ := configVal.UnmarkDeep() 465 req := providers.ValidateDataResourceConfigRequest{ 466 TypeName: n.Config.Type, 467 Config: unmarkedConfigVal, 468 } 469 470 resp := provider.ValidateDataResourceConfig(req) 471 diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config, n.Addr.String())) 472 } 473 474 return diags 475 } 476 477 func (n *NodeValidatableResource) evaluateExpr(ctx EvalContext, expr hcl.Expression, wantTy cty.Type, self addrs.Referenceable, keyData instances.RepetitionData) (cty.Value, tfdiags.Diagnostics) { 478 var diags tfdiags.Diagnostics 479 480 refs, refDiags := lang.ReferencesInExpr(addrs.ParseRef, expr) 481 diags = diags.Append(refDiags) 482 483 scope := ctx.EvaluationScope(self, nil, keyData) 484 485 hclCtx, moreDiags := scope.EvalContext(refs) 486 diags = diags.Append(moreDiags) 487 488 result, hclDiags := expr.Value(hclCtx) 489 diags = diags.Append(hclDiags) 490 491 return result, diags 492 } 493 494 func (n *NodeValidatableResource) stubRepetitionData(hasCount, hasForEach bool) (instances.RepetitionData, addrs.Referenceable) { 495 keyData := EvalDataForNoInstanceKey 496 selfAddr := n.ResourceAddr().Resource.Instance(addrs.NoKey) 497 498 if n.Config.Count != nil { 499 // For a resource that has count, we allow count.index but don't 500 // know at this stage what it will return. 501 keyData = InstanceKeyEvalData{ 502 CountIndex: cty.UnknownVal(cty.Number), 503 } 504 505 // "self" can't point to an unknown key, but we'll force it to be 506 // key 0 here, which should return an unknown value of the 507 // expected type since none of these elements are known at this 508 // point anyway. 509 selfAddr = n.ResourceAddr().Resource.Instance(addrs.IntKey(0)) 510 } else if n.Config.ForEach != nil { 511 // For a resource that has for_each, we allow each.value and each.key 512 // but don't know at this stage what it will return. 513 keyData = InstanceKeyEvalData{ 514 EachKey: cty.UnknownVal(cty.String), 515 EachValue: cty.DynamicVal, 516 } 517 518 // "self" can't point to an unknown key, but we'll force it to be 519 // key "" here, which should return an unknown value of the 520 // expected type since none of these elements are known at 521 // this point anyway. 522 selfAddr = n.ResourceAddr().Resource.Instance(addrs.StringKey("")) 523 } 524 525 return keyData, selfAddr 526 } 527 528 func (n *NodeValidatableResource) validateCheckRules(ctx EvalContext, config *configs.Resource) tfdiags.Diagnostics { 529 var diags tfdiags.Diagnostics 530 531 keyData, selfAddr := n.stubRepetitionData(n.Config.Count != nil, n.Config.ForEach != nil) 532 533 for _, cr := range config.Preconditions { 534 _, conditionDiags := n.evaluateExpr(ctx, cr.Condition, cty.Bool, nil, keyData) 535 diags = diags.Append(conditionDiags) 536 537 _, errorMessageDiags := n.evaluateExpr(ctx, cr.ErrorMessage, cty.Bool, nil, keyData) 538 diags = diags.Append(errorMessageDiags) 539 } 540 541 for _, cr := range config.Postconditions { 542 _, conditionDiags := n.evaluateExpr(ctx, cr.Condition, cty.Bool, selfAddr, keyData) 543 diags = diags.Append(conditionDiags) 544 545 _, errorMessageDiags := n.evaluateExpr(ctx, cr.ErrorMessage, cty.Bool, selfAddr, keyData) 546 diags = diags.Append(errorMessageDiags) 547 } 548 549 return diags 550 } 551 552 func validateCount(ctx EvalContext, expr hcl.Expression) (diags tfdiags.Diagnostics) { 553 val, countDiags := evaluateCountExpressionValue(expr, ctx) 554 // If the value isn't known then that's the best we can do for now, but 555 // we'll check more thoroughly during the plan walk 556 if !val.IsKnown() { 557 return diags 558 } 559 560 if countDiags.HasErrors() { 561 diags = diags.Append(countDiags) 562 } 563 564 return diags 565 } 566 567 func validateForEach(ctx EvalContext, expr hcl.Expression) (diags tfdiags.Diagnostics) { 568 val, forEachDiags := evaluateForEachExpressionValue(expr, ctx, true, false) 569 // If the value isn't known then that's the best we can do for now, but 570 // we'll check more thoroughly during the plan walk 571 if !val.IsKnown() { 572 return diags 573 } 574 575 if forEachDiags.HasErrors() { 576 diags = diags.Append(forEachDiags) 577 } 578 579 return diags 580 } 581 582 func validateDependsOn(ctx EvalContext, dependsOn []hcl.Traversal) (diags tfdiags.Diagnostics) { 583 for _, traversal := range dependsOn { 584 ref, refDiags := addrs.ParseRef(traversal) 585 diags = diags.Append(refDiags) 586 if !refDiags.HasErrors() && len(ref.Remaining) != 0 { 587 diags = diags.Append(&hcl.Diagnostic{ 588 Severity: hcl.DiagError, 589 Summary: "Invalid depends_on reference", 590 Detail: "References in depends_on must be to a whole object (resource, etc), not to an attribute of an object.", 591 Subject: ref.Remaining.SourceRange().Ptr(), 592 }) 593 } 594 595 // The ref must also refer to something that exists. To test that, 596 // we'll just eval it and count on the fact that our evaluator will 597 // detect references to non-existent objects. 598 if !diags.HasErrors() { 599 scope := ctx.EvaluationScope(nil, nil, EvalDataForNoInstanceKey) 600 if scope != nil { // sometimes nil in tests, due to incomplete mocks 601 _, refDiags = scope.EvalReference(ref, cty.DynamicPseudoType) 602 diags = diags.Append(refDiags) 603 } 604 } 605 } 606 return diags 607 }