github.com/hashicorp/terraform-plugin-sdk@v1.17.2/terraform/eval_validate.go (about) 1 package terraform 2 3 import ( 4 "fmt" 5 "log" 6 7 "github.com/hashicorp/hcl/v2" 8 "github.com/hashicorp/terraform-plugin-sdk/internal/addrs" 9 "github.com/hashicorp/terraform-plugin-sdk/internal/configs" 10 "github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema" 11 "github.com/hashicorp/terraform-plugin-sdk/internal/providers" 12 "github.com/hashicorp/terraform-plugin-sdk/internal/provisioners" 13 "github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags" 14 "github.com/zclconf/go-cty/cty" 15 "github.com/zclconf/go-cty/cty/convert" 16 "github.com/zclconf/go-cty/cty/gocty" 17 ) 18 19 // EvalValidateCount is an EvalNode implementation that validates 20 // the count of a resource. 21 type EvalValidateCount struct { 22 Resource *configs.Resource 23 } 24 25 // TODO: test 26 func (n *EvalValidateCount) Eval(ctx EvalContext) (interface{}, error) { 27 var diags tfdiags.Diagnostics 28 var count int 29 var err error 30 31 val, valDiags := ctx.EvaluateExpr(n.Resource.Count, cty.Number, nil) 32 diags = diags.Append(valDiags) 33 if valDiags.HasErrors() { 34 goto RETURN 35 } 36 if val.IsNull() || !val.IsKnown() { 37 goto RETURN 38 } 39 40 err = gocty.FromCtyValue(val, &count) 41 if err != nil { 42 // The EvaluateExpr call above already guaranteed us a number value, 43 // so if we end up here then we have something that is out of range 44 // for an int, and the error message will include a description of 45 // the valid range. 46 rawVal := val.AsBigFloat() 47 diags = diags.Append(&hcl.Diagnostic{ 48 Severity: hcl.DiagError, 49 Summary: "Invalid count value", 50 Detail: fmt.Sprintf("The number %s is not a valid count value: %s.", rawVal, err), 51 Subject: n.Resource.Count.Range().Ptr(), 52 }) 53 } else if count < 0 { 54 rawVal := val.AsBigFloat() 55 diags = diags.Append(&hcl.Diagnostic{ 56 Severity: hcl.DiagError, 57 Summary: "Invalid count value", 58 Detail: fmt.Sprintf("The number %s is not a valid count value: count must not be negative.", rawVal), 59 Subject: n.Resource.Count.Range().Ptr(), 60 }) 61 } 62 63 RETURN: 64 return nil, diags.NonFatalErr() 65 } 66 67 // EvalValidateProvider is an EvalNode implementation that validates 68 // a provider configuration. 69 type EvalValidateProvider struct { 70 Addr addrs.ProviderConfig 71 Provider *providers.Interface 72 Config *configs.Provider 73 } 74 75 func (n *EvalValidateProvider) Eval(ctx EvalContext) (interface{}, error) { 76 var diags tfdiags.Diagnostics 77 provider := *n.Provider 78 79 configBody := buildProviderConfig(ctx, n.Addr, n.Config) 80 81 resp := provider.GetSchema() 82 diags = diags.Append(resp.Diagnostics) 83 if diags.HasErrors() { 84 return nil, diags.NonFatalErr() 85 } 86 87 configSchema := resp.Provider.Block 88 if configSchema == nil { 89 // Should never happen in real code, but often comes up in tests where 90 // mock schemas are being used that tend to be incomplete. 91 log.Printf("[WARN] EvalValidateProvider: no config schema is available for %s, so using empty schema", n.Addr) 92 configSchema = &configschema.Block{} 93 } 94 95 configVal, configBody, evalDiags := ctx.EvaluateBlock(configBody, configSchema, nil, EvalDataForNoInstanceKey) 96 diags = diags.Append(evalDiags) 97 if evalDiags.HasErrors() { 98 return nil, diags.NonFatalErr() 99 } 100 101 req := providers.PrepareProviderConfigRequest{ 102 Config: configVal, 103 } 104 105 validateResp := provider.PrepareProviderConfig(req) 106 diags = diags.Append(validateResp.Diagnostics) 107 108 return nil, diags.NonFatalErr() 109 } 110 111 // EvalValidateProvisioner is an EvalNode implementation that validates 112 // the configuration of a provisioner belonging to a resource. The provisioner 113 // config is expected to contain the merged connection configurations. 114 type EvalValidateProvisioner struct { 115 ResourceAddr addrs.Resource 116 Provisioner *provisioners.Interface 117 Schema **configschema.Block 118 Config *configs.Provisioner 119 ResourceHasCount bool 120 ResourceHasForEach bool 121 } 122 123 func (n *EvalValidateProvisioner) Eval(ctx EvalContext) (interface{}, error) { 124 provisioner := *n.Provisioner 125 config := *n.Config 126 schema := *n.Schema 127 128 var diags tfdiags.Diagnostics 129 130 { 131 // Validate the provisioner's own config first 132 133 configVal, _, configDiags := n.evaluateBlock(ctx, config.Config, schema) 134 diags = diags.Append(configDiags) 135 if configDiags.HasErrors() { 136 return nil, diags.Err() 137 } 138 139 if configVal == cty.NilVal { 140 // Should never happen for a well-behaved EvaluateBlock implementation 141 return nil, fmt.Errorf("EvaluateBlock returned nil value") 142 } 143 144 req := provisioners.ValidateProvisionerConfigRequest{ 145 Config: configVal, 146 } 147 148 resp := provisioner.ValidateProvisionerConfig(req) 149 diags = diags.Append(resp.Diagnostics) 150 } 151 152 { 153 // Now validate the connection config, which contains the merged bodies 154 // of the resource and provisioner connection blocks. 155 connDiags := n.validateConnConfig(ctx, config.Connection, n.ResourceAddr) 156 diags = diags.Append(connDiags) 157 } 158 159 return nil, diags.NonFatalErr() 160 } 161 162 func (n *EvalValidateProvisioner) validateConnConfig(ctx EvalContext, config *configs.Connection, self addrs.Referenceable) tfdiags.Diagnostics { 163 // We can't comprehensively validate the connection config since its 164 // final structure is decided by the communicator and we can't instantiate 165 // that until we have a complete instance state. However, we *can* catch 166 // configuration keys that are not valid for *any* communicator, catching 167 // typos early rather than waiting until we actually try to run one of 168 // the resource's provisioners. 169 170 var diags tfdiags.Diagnostics 171 172 if config == nil || config.Config == nil { 173 // No block to validate 174 return diags 175 } 176 177 // We evaluate here just by evaluating the block and returning any 178 // diagnostics we get, since evaluation alone is enough to check for 179 // extraneous arguments and incorrectly-typed arguments. 180 _, _, configDiags := n.evaluateBlock(ctx, config.Config, connectionBlockSupersetSchema) 181 diags = diags.Append(configDiags) 182 183 return diags 184 } 185 186 func (n *EvalValidateProvisioner) evaluateBlock(ctx EvalContext, body hcl.Body, schema *configschema.Block) (cty.Value, hcl.Body, tfdiags.Diagnostics) { 187 keyData := EvalDataForNoInstanceKey 188 selfAddr := n.ResourceAddr.Instance(addrs.NoKey) 189 190 if n.ResourceHasCount { 191 // For a resource that has count, we allow count.index but don't 192 // know at this stage what it will return. 193 keyData = InstanceKeyEvalData{ 194 CountIndex: cty.UnknownVal(cty.Number), 195 } 196 197 // "self" can't point to an unknown key, but we'll force it to be 198 // key 0 here, which should return an unknown value of the 199 // expected type since none of these elements are known at this 200 // point anyway. 201 selfAddr = n.ResourceAddr.Instance(addrs.IntKey(0)) 202 } else if n.ResourceHasForEach { 203 // For a resource that has for_each, we allow each.value and each.key 204 // but don't know at this stage what it will return. 205 keyData = InstanceKeyEvalData{ 206 EachKey: cty.UnknownVal(cty.String), 207 EachValue: cty.DynamicVal, 208 } 209 210 // "self" can't point to an unknown key, but we'll force it to be 211 // key "" here, which should return an unknown value of the 212 // expected type since none of these elements are known at 213 // this point anyway. 214 selfAddr = n.ResourceAddr.Instance(addrs.StringKey("")) 215 } 216 217 return ctx.EvaluateBlock(body, schema, selfAddr, keyData) 218 } 219 220 // connectionBlockSupersetSchema is a schema representing the superset of all 221 // possible arguments for "connection" blocks across all supported connection 222 // types. 223 // 224 // This currently lives here because we've not yet updated our communicator 225 // subsystem to be aware of schema itself. Once that is done, we can remove 226 // this and use a type-specific schema from the communicator to validate 227 // exactly what is expected for a given connection type. 228 var connectionBlockSupersetSchema = &configschema.Block{ 229 Attributes: map[string]*configschema.Attribute{ 230 // NOTE: "type" is not included here because it's treated special 231 // by the config loader and stored away in a separate field. 232 233 // Common attributes for both connection types 234 "host": { 235 Type: cty.String, 236 Required: true, 237 }, 238 "type": { 239 Type: cty.String, 240 Optional: true, 241 }, 242 "user": { 243 Type: cty.String, 244 Optional: true, 245 }, 246 "password": { 247 Type: cty.String, 248 Optional: true, 249 }, 250 "port": { 251 Type: cty.String, 252 Optional: true, 253 }, 254 "timeout": { 255 Type: cty.String, 256 Optional: true, 257 }, 258 "script_path": { 259 Type: cty.String, 260 Optional: true, 261 }, 262 263 // For type=ssh only (enforced in ssh communicator) 264 "private_key": { 265 Type: cty.String, 266 Optional: true, 267 }, 268 "certificate": { 269 Type: cty.String, 270 Optional: true, 271 }, 272 "host_key": { 273 Type: cty.String, 274 Optional: true, 275 }, 276 "agent": { 277 Type: cty.Bool, 278 Optional: true, 279 }, 280 "agent_identity": { 281 Type: cty.String, 282 Optional: true, 283 }, 284 "bastion_host": { 285 Type: cty.String, 286 Optional: true, 287 }, 288 "bastion_host_key": { 289 Type: cty.String, 290 Optional: true, 291 }, 292 "bastion_port": { 293 Type: cty.Number, 294 Optional: true, 295 }, 296 "bastion_user": { 297 Type: cty.String, 298 Optional: true, 299 }, 300 "bastion_password": { 301 Type: cty.String, 302 Optional: true, 303 }, 304 "bastion_private_key": { 305 Type: cty.String, 306 Optional: true, 307 }, 308 "bastion_certificate": { 309 Type: cty.String, 310 Optional: true, 311 }, 312 313 // For type=winrm only (enforced in winrm communicator) 314 "https": { 315 Type: cty.Bool, 316 Optional: true, 317 }, 318 "insecure": { 319 Type: cty.Bool, 320 Optional: true, 321 }, 322 "cacert": { 323 Type: cty.String, 324 Optional: true, 325 }, 326 "use_ntlm": { 327 Type: cty.Bool, 328 Optional: true, 329 }, 330 }, 331 } 332 333 // connectionBlockSupersetSchema is a schema representing the superset of all 334 // possible arguments for "connection" blocks across all supported connection 335 // types. 336 // 337 // This currently lives here because we've not yet updated our communicator 338 // subsystem to be aware of schema itself. It's exported only for use in the 339 // configs/configupgrade package and should not be used from anywhere else. 340 // The caller may not modify any part of the returned schema data structure. 341 func ConnectionBlockSupersetSchema() *configschema.Block { 342 return connectionBlockSupersetSchema 343 } 344 345 // EvalValidateResource is an EvalNode implementation that validates 346 // the configuration of a resource. 347 type EvalValidateResource struct { 348 Addr addrs.Resource 349 Provider *providers.Interface 350 ProviderSchema **ProviderSchema 351 Config *configs.Resource 352 353 // IgnoreWarnings means that warnings will not be passed through. This allows 354 // "just-in-time" passes of validation to continue execution through warnings. 355 IgnoreWarnings bool 356 357 // ConfigVal, if non-nil, will be updated with the value resulting from 358 // evaluating the given configuration body. Since validation is performed 359 // very early, this value is likely to contain lots of unknown values, 360 // but its type will conform to the schema of the resource type associated 361 // with the resource instance being validated. 362 ConfigVal *cty.Value 363 } 364 365 func (n *EvalValidateResource) Eval(ctx EvalContext) (interface{}, error) { 366 if n.ProviderSchema == nil || *n.ProviderSchema == nil { 367 return nil, fmt.Errorf("EvalValidateResource has nil schema for %s", n.Addr) 368 } 369 370 var diags tfdiags.Diagnostics 371 provider := *n.Provider 372 cfg := *n.Config 373 schema := *n.ProviderSchema 374 mode := cfg.Mode 375 376 keyData := EvalDataForNoInstanceKey 377 if n.Config.Count != nil { 378 // If the config block has count, we'll evaluate with an unknown 379 // number as count.index so we can still type check even though 380 // we won't expand count until the plan phase. 381 keyData = InstanceKeyEvalData{ 382 CountIndex: cty.UnknownVal(cty.Number), 383 } 384 385 // Basic type-checking of the count argument. More complete validation 386 // of this will happen when we DynamicExpand during the plan walk. 387 countDiags := n.validateCount(ctx, n.Config.Count) 388 diags = diags.Append(countDiags) 389 } 390 391 if n.Config.ForEach != nil { 392 keyData = InstanceKeyEvalData{ 393 EachKey: cty.UnknownVal(cty.String), 394 EachValue: cty.UnknownVal(cty.DynamicPseudoType), 395 } 396 397 // Evaluate the for_each expression here so we can expose the diagnostics 398 forEachDiags := n.validateForEach(ctx, n.Config.ForEach) 399 diags = diags.Append(forEachDiags) 400 } 401 402 for _, traversal := range n.Config.DependsOn { 403 ref, refDiags := addrs.ParseRef(traversal) 404 diags = diags.Append(refDiags) 405 if !refDiags.HasErrors() && len(ref.Remaining) != 0 { 406 diags = diags.Append(&hcl.Diagnostic{ 407 Severity: hcl.DiagError, 408 Summary: "Invalid depends_on reference", 409 Detail: "References in depends_on must be to a whole object (resource, etc), not to an attribute of an object.", 410 Subject: ref.Remaining.SourceRange().Ptr(), 411 }) 412 } 413 414 // The ref must also refer to something that exists. To test that, 415 // we'll just eval it and count on the fact that our evaluator will 416 // detect references to non-existent objects. 417 if !diags.HasErrors() { 418 scope := ctx.EvaluationScope(nil, EvalDataForNoInstanceKey) 419 if scope != nil { // sometimes nil in tests, due to incomplete mocks 420 _, refDiags = scope.EvalReference(ref, cty.DynamicPseudoType) 421 diags = diags.Append(refDiags) 422 } 423 } 424 } 425 426 // Provider entry point varies depending on resource mode, because 427 // managed resources and data resources are two distinct concepts 428 // in the provider abstraction. 429 switch mode { 430 case addrs.ManagedResourceMode: 431 schema, _ := schema.SchemaForResourceType(mode, cfg.Type) 432 if schema == nil { 433 diags = diags.Append(&hcl.Diagnostic{ 434 Severity: hcl.DiagError, 435 Summary: "Invalid resource type", 436 Detail: fmt.Sprintf("The provider %s does not support resource type %q.", cfg.ProviderConfigAddr(), cfg.Type), 437 Subject: &cfg.TypeRange, 438 }) 439 return nil, diags.Err() 440 } 441 442 configVal, _, valDiags := ctx.EvaluateBlock(cfg.Config, schema, nil, keyData) 443 diags = diags.Append(valDiags) 444 if valDiags.HasErrors() { 445 return nil, diags.Err() 446 } 447 448 if cfg.Managed != nil { // can be nil only in tests with poorly-configured mocks 449 for _, traversal := range cfg.Managed.IgnoreChanges { 450 moreDiags := schema.StaticValidateTraversal(traversal) 451 diags = diags.Append(moreDiags) 452 } 453 } 454 455 req := providers.ValidateResourceTypeConfigRequest{ 456 TypeName: cfg.Type, 457 Config: configVal, 458 } 459 460 resp := provider.ValidateResourceTypeConfig(req) 461 diags = diags.Append(resp.Diagnostics.InConfigBody(cfg.Config)) 462 463 if n.ConfigVal != nil { 464 *n.ConfigVal = configVal 465 } 466 467 case addrs.DataResourceMode: 468 schema, _ := schema.SchemaForResourceType(mode, cfg.Type) 469 if schema == nil { 470 diags = diags.Append(&hcl.Diagnostic{ 471 Severity: hcl.DiagError, 472 Summary: "Invalid data source", 473 Detail: fmt.Sprintf("The provider %s does not support data source %q.", cfg.ProviderConfigAddr(), cfg.Type), 474 Subject: &cfg.TypeRange, 475 }) 476 return nil, diags.Err() 477 } 478 479 configVal, _, valDiags := ctx.EvaluateBlock(cfg.Config, schema, nil, keyData) 480 diags = diags.Append(valDiags) 481 if valDiags.HasErrors() { 482 return nil, diags.Err() 483 } 484 485 req := providers.ValidateDataSourceConfigRequest{ 486 TypeName: cfg.Type, 487 Config: configVal, 488 } 489 490 resp := provider.ValidateDataSourceConfig(req) 491 diags = diags.Append(resp.Diagnostics.InConfigBody(cfg.Config)) 492 } 493 494 if n.IgnoreWarnings { 495 // If we _only_ have warnings then we'll return nil. 496 if diags.HasErrors() { 497 return nil, diags.NonFatalErr() 498 } 499 return nil, nil 500 } else { 501 // We'll return an error if there are any diagnostics at all, even if 502 // some of them are warnings. 503 return nil, diags.NonFatalErr() 504 } 505 } 506 507 func (n *EvalValidateResource) validateCount(ctx EvalContext, expr hcl.Expression) tfdiags.Diagnostics { 508 if expr == nil { 509 return nil 510 } 511 512 var diags tfdiags.Diagnostics 513 514 countVal, countDiags := ctx.EvaluateExpr(expr, cty.Number, nil) 515 diags = diags.Append(countDiags) 516 if diags.HasErrors() { 517 return diags 518 } 519 520 if countVal.IsNull() { 521 diags = diags.Append(&hcl.Diagnostic{ 522 Severity: hcl.DiagError, 523 Summary: "Invalid count argument", 524 Detail: `The given "count" argument value is null. An integer is required.`, 525 Subject: expr.Range().Ptr(), 526 }) 527 return diags 528 } 529 530 var err error 531 countVal, err = convert.Convert(countVal, cty.Number) 532 if err != nil { 533 diags = diags.Append(&hcl.Diagnostic{ 534 Severity: hcl.DiagError, 535 Summary: "Invalid count argument", 536 Detail: fmt.Sprintf(`The given "count" argument value is unsuitable: %s.`, err), 537 Subject: expr.Range().Ptr(), 538 }) 539 return diags 540 } 541 542 // If the value isn't known then that's the best we can do for now, but 543 // we'll check more thoroughly during the plan walk. 544 if !countVal.IsKnown() { 545 return diags 546 } 547 548 // If we _do_ know the value, then we can do a few more checks here. 549 var count int 550 err = gocty.FromCtyValue(countVal, &count) 551 if err != nil { 552 // Isn't a whole number, etc. 553 diags = diags.Append(&hcl.Diagnostic{ 554 Severity: hcl.DiagError, 555 Summary: "Invalid count argument", 556 Detail: fmt.Sprintf(`The given "count" argument value is unsuitable: %s.`, err), 557 Subject: expr.Range().Ptr(), 558 }) 559 return diags 560 } 561 562 if count < 0 { 563 diags = diags.Append(&hcl.Diagnostic{ 564 Severity: hcl.DiagError, 565 Summary: "Invalid count argument", 566 Detail: `The given "count" argument value is unsuitable: count cannot be negative.`, 567 Subject: expr.Range().Ptr(), 568 }) 569 return diags 570 } 571 572 return diags 573 } 574 575 func (n *EvalValidateResource) validateForEach(ctx EvalContext, expr hcl.Expression) (diags tfdiags.Diagnostics) { 576 _, known, forEachDiags := evaluateResourceForEachExpressionKnown(expr, ctx) 577 // If the value isn't known then that's the best we can do for now, but 578 // we'll check more thoroughly during the plan walk 579 if !known { 580 return diags 581 } 582 583 if forEachDiags.HasErrors() { 584 diags = diags.Append(forEachDiags) 585 } 586 587 return diags 588 }