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