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