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