package terraform import ( "fmt" "log" "github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/providers" "github.com/hashicorp/terraform/provisioners" "github.com/hashicorp/terraform/tfdiags" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" "github.com/zclconf/go-cty/cty/gocty" ) // EvalValidateCount is an EvalNode implementation that validates // the count of a resource. type EvalValidateCount struct { Resource *configs.Resource } // TODO: test func (n *EvalValidateCount) Eval(ctx EvalContext) (interface{}, error) { var diags tfdiags.Diagnostics var count int var err error val, valDiags := ctx.EvaluateExpr(n.Resource.Count, cty.Number, nil) diags = diags.Append(valDiags) if valDiags.HasErrors() { goto RETURN } if val.IsNull() || !val.IsKnown() { goto RETURN } err = gocty.FromCtyValue(val, &count) if err != nil { // The EvaluateExpr call above already guaranteed us a number value, // so if we end up here then we have something that is out of range // for an int, and the error message will include a description of // the valid range. rawVal := val.AsBigFloat() diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid count value", Detail: fmt.Sprintf("The number %s is not a valid count value: %s.", rawVal, err), Subject: n.Resource.Count.Range().Ptr(), }) } else if count < 0 { rawVal := val.AsBigFloat() diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid count value", Detail: fmt.Sprintf("The number %s is not a valid count value: count must not be negative.", rawVal), Subject: n.Resource.Count.Range().Ptr(), }) } RETURN: return nil, diags.NonFatalErr() } // EvalValidateProvider is an EvalNode implementation that validates // a provider configuration. type EvalValidateProvider struct { Addr addrs.ProviderConfig Provider *providers.Interface Config *configs.Provider } func (n *EvalValidateProvider) Eval(ctx EvalContext) (interface{}, error) { var diags tfdiags.Diagnostics provider := *n.Provider configBody := buildProviderConfig(ctx, n.Addr, n.Config) resp := provider.GetSchema() diags = diags.Append(resp.Diagnostics) if diags.HasErrors() { return nil, diags.NonFatalErr() } configSchema := resp.Provider.Block if configSchema == nil { // Should never happen in real code, but often comes up in tests where // mock schemas are being used that tend to be incomplete. log.Printf("[WARN] EvalValidateProvider: no config schema is available for %s, so using empty schema", n.Addr) configSchema = &configschema.Block{} } configVal, configBody, evalDiags := ctx.EvaluateBlock(configBody, configSchema, nil, EvalDataForNoInstanceKey) diags = diags.Append(evalDiags) if evalDiags.HasErrors() { return nil, diags.NonFatalErr() } req := providers.PrepareProviderConfigRequest{ Config: configVal, } validateResp := provider.PrepareProviderConfig(req) diags = diags.Append(validateResp.Diagnostics) return nil, diags.NonFatalErr() } // EvalValidateProvisioner is an EvalNode implementation that validates // the configuration of a provisioner belonging to a resource. The provisioner // config is expected to contain the merged connection configurations. type EvalValidateProvisioner struct { ResourceAddr addrs.Resource Provisioner *provisioners.Interface Schema **configschema.Block Config *configs.Provisioner ResourceHasCount bool ResourceHasForEach bool } func (n *EvalValidateProvisioner) Eval(ctx EvalContext) (interface{}, error) { provisioner := *n.Provisioner config := *n.Config schema := *n.Schema var diags tfdiags.Diagnostics { // Validate the provisioner's own config first configVal, _, configDiags := n.evaluateBlock(ctx, config.Config, schema) diags = diags.Append(configDiags) if configDiags.HasErrors() { return nil, diags.Err() } if configVal == cty.NilVal { // Should never happen for a well-behaved EvaluateBlock implementation return nil, fmt.Errorf("EvaluateBlock returned nil value") } req := provisioners.ValidateProvisionerConfigRequest{ Config: configVal, } resp := provisioner.ValidateProvisionerConfig(req) diags = diags.Append(resp.Diagnostics) } { // Now validate the connection config, which contains the merged bodies // of the resource and provisioner connection blocks. connDiags := n.validateConnConfig(ctx, config.Connection, n.ResourceAddr) diags = diags.Append(connDiags) } return nil, diags.NonFatalErr() } func (n *EvalValidateProvisioner) validateConnConfig(ctx EvalContext, config *configs.Connection, self addrs.Referenceable) tfdiags.Diagnostics { // We can't comprehensively validate the connection config since its // final structure is decided by the communicator and we can't instantiate // that until we have a complete instance state. However, we *can* catch // configuration keys that are not valid for *any* communicator, catching // typos early rather than waiting until we actually try to run one of // the resource's provisioners. var diags tfdiags.Diagnostics if config == nil || config.Config == nil { // No block to validate return diags } // We evaluate here just by evaluating the block and returning any // diagnostics we get, since evaluation alone is enough to check for // extraneous arguments and incorrectly-typed arguments. _, _, configDiags := n.evaluateBlock(ctx, config.Config, connectionBlockSupersetSchema) diags = diags.Append(configDiags) return diags } func (n *EvalValidateProvisioner) evaluateBlock(ctx EvalContext, body hcl.Body, schema *configschema.Block) (cty.Value, hcl.Body, tfdiags.Diagnostics) { keyData := EvalDataForNoInstanceKey selfAddr := n.ResourceAddr.Instance(addrs.NoKey) if n.ResourceHasCount { // For a resource that has count, we allow count.index but don't // know at this stage what it will return. keyData = InstanceKeyEvalData{ CountIndex: cty.UnknownVal(cty.Number), } // "self" can't point to an unknown key, but we'll force it to be // key 0 here, which should return an unknown value of the // expected type since none of these elements are known at this // point anyway. selfAddr = n.ResourceAddr.Instance(addrs.IntKey(0)) } else if n.ResourceHasForEach { // For a resource that has for_each, we allow each.value and each.key // but don't know at this stage what it will return. keyData = InstanceKeyEvalData{ EachKey: cty.UnknownVal(cty.String), EachValue: cty.DynamicVal, } // "self" can't point to an unknown key, but we'll force it to be // key "" here, which should return an unknown value of the // expected type since none of these elements are known at // this point anyway. selfAddr = n.ResourceAddr.Instance(addrs.StringKey("")) } return ctx.EvaluateBlock(body, schema, selfAddr, keyData) } // connectionBlockSupersetSchema is a schema representing the superset of all // possible arguments for "connection" blocks across all supported connection // types. // // This currently lives here because we've not yet updated our communicator // subsystem to be aware of schema itself. Once that is done, we can remove // this and use a type-specific schema from the communicator to validate // exactly what is expected for a given connection type. var connectionBlockSupersetSchema = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ // NOTE: "type" is not included here because it's treated special // by the config loader and stored away in a separate field. // Common attributes for both connection types "host": { Type: cty.String, Required: true, }, "type": { Type: cty.String, Optional: true, }, "user": { Type: cty.String, Optional: true, }, "password": { Type: cty.String, Optional: true, }, "port": { Type: cty.String, Optional: true, }, "timeout": { Type: cty.String, Optional: true, }, "script_path": { Type: cty.String, Optional: true, }, // For type=ssh only (enforced in ssh communicator) "private_key": { Type: cty.String, Optional: true, }, "certificate": { Type: cty.String, Optional: true, }, "host_key": { Type: cty.String, Optional: true, }, "agent": { Type: cty.Bool, Optional: true, }, "agent_identity": { Type: cty.String, Optional: true, }, "bastion_host": { Type: cty.String, Optional: true, }, "bastion_host_key": { Type: cty.String, Optional: true, }, "bastion_port": { Type: cty.Number, Optional: true, }, "bastion_user": { Type: cty.String, Optional: true, }, "bastion_password": { Type: cty.String, Optional: true, }, "bastion_private_key": { Type: cty.String, Optional: true, }, "bastion_certificate": { Type: cty.String, Optional: true, }, // For type=winrm only (enforced in winrm communicator) "https": { Type: cty.Bool, Optional: true, }, "insecure": { Type: cty.Bool, Optional: true, }, "cacert": { Type: cty.String, Optional: true, }, "use_ntlm": { Type: cty.Bool, Optional: true, }, }, } // connectionBlockSupersetSchema is a schema representing the superset of all // possible arguments for "connection" blocks across all supported connection // types. // // This currently lives here because we've not yet updated our communicator // subsystem to be aware of schema itself. It's exported only for use in the // configs/configupgrade package and should not be used from anywhere else. // The caller may not modify any part of the returned schema data structure. func ConnectionBlockSupersetSchema() *configschema.Block { return connectionBlockSupersetSchema } // EvalValidateResource is an EvalNode implementation that validates // the configuration of a resource. type EvalValidateResource struct { Addr addrs.Resource Provider *providers.Interface ProviderSchema **ProviderSchema Config *configs.Resource // IgnoreWarnings means that warnings will not be passed through. This allows // "just-in-time" passes of validation to continue execution through warnings. IgnoreWarnings bool // ConfigVal, if non-nil, will be updated with the value resulting from // evaluating the given configuration body. Since validation is performed // very early, this value is likely to contain lots of unknown values, // but its type will conform to the schema of the resource type associated // with the resource instance being validated. ConfigVal *cty.Value } func (n *EvalValidateResource) Eval(ctx EvalContext) (interface{}, error) { if n.ProviderSchema == nil || *n.ProviderSchema == nil { return nil, fmt.Errorf("EvalValidateResource has nil schema for %s", n.Addr) } var diags tfdiags.Diagnostics provider := *n.Provider cfg := *n.Config schema := *n.ProviderSchema mode := cfg.Mode keyData := EvalDataForNoInstanceKey if n.Config.Count != nil { // If the config block has count, we'll evaluate with an unknown // number as count.index so we can still type check even though // we won't expand count until the plan phase. keyData = InstanceKeyEvalData{ CountIndex: cty.UnknownVal(cty.Number), } // Basic type-checking of the count argument. More complete validation // of this will happen when we DynamicExpand during the plan walk. countDiags := n.validateCount(ctx, n.Config.Count) diags = diags.Append(countDiags) } if n.Config.ForEach != nil { keyData = InstanceKeyEvalData{ EachKey: cty.UnknownVal(cty.String), EachValue: cty.UnknownVal(cty.DynamicPseudoType), } // Evaluate the for_each expression here so we can expose the diagnostics forEachDiags := n.validateForEach(ctx, n.Config.ForEach) diags = diags.Append(forEachDiags) } for _, traversal := range n.Config.DependsOn { ref, refDiags := addrs.ParseRef(traversal) diags = diags.Append(refDiags) if !refDiags.HasErrors() && len(ref.Remaining) != 0 { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid depends_on reference", Detail: "References in depends_on must be to a whole object (resource, etc), not to an attribute of an object.", Subject: ref.Remaining.SourceRange().Ptr(), }) } // The ref must also refer to something that exists. To test that, // we'll just eval it and count on the fact that our evaluator will // detect references to non-existent objects. if !diags.HasErrors() { scope := ctx.EvaluationScope(nil, EvalDataForNoInstanceKey) if scope != nil { // sometimes nil in tests, due to incomplete mocks _, refDiags = scope.EvalReference(ref, cty.DynamicPseudoType) diags = diags.Append(refDiags) } } } // Provider entry point varies depending on resource mode, because // managed resources and data resources are two distinct concepts // in the provider abstraction. switch mode { case addrs.ManagedResourceMode: schema, _ := schema.SchemaForResourceType(mode, cfg.Type) if schema == nil { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid resource type", Detail: fmt.Sprintf("The provider %s does not support resource type %q.", cfg.ProviderConfigAddr(), cfg.Type), Subject: &cfg.TypeRange, }) return nil, diags.Err() } configVal, _, valDiags := ctx.EvaluateBlock(cfg.Config, schema, nil, keyData) diags = diags.Append(valDiags) if valDiags.HasErrors() { return nil, diags.Err() } if cfg.Managed != nil { // can be nil only in tests with poorly-configured mocks for _, traversal := range cfg.Managed.IgnoreChanges { moreDiags := schema.StaticValidateTraversal(traversal) diags = diags.Append(moreDiags) } } req := providers.ValidateResourceTypeConfigRequest{ TypeName: cfg.Type, Config: configVal, } resp := provider.ValidateResourceTypeConfig(req) diags = diags.Append(resp.Diagnostics.InConfigBody(cfg.Config)) if n.ConfigVal != nil { *n.ConfigVal = configVal } case addrs.DataResourceMode: schema, _ := schema.SchemaForResourceType(mode, cfg.Type) if schema == nil { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid data source", Detail: fmt.Sprintf("The provider %s does not support data source %q.", cfg.ProviderConfigAddr(), cfg.Type), Subject: &cfg.TypeRange, }) return nil, diags.Err() } configVal, _, valDiags := ctx.EvaluateBlock(cfg.Config, schema, nil, keyData) diags = diags.Append(valDiags) if valDiags.HasErrors() { return nil, diags.Err() } req := providers.ValidateDataSourceConfigRequest{ TypeName: cfg.Type, Config: configVal, } resp := provider.ValidateDataSourceConfig(req) diags = diags.Append(resp.Diagnostics.InConfigBody(cfg.Config)) } if n.IgnoreWarnings { // If we _only_ have warnings then we'll return nil. if diags.HasErrors() { return nil, diags.NonFatalErr() } return nil, nil } else { // We'll return an error if there are any diagnostics at all, even if // some of them are warnings. return nil, diags.NonFatalErr() } } func (n *EvalValidateResource) validateCount(ctx EvalContext, expr hcl.Expression) tfdiags.Diagnostics { if expr == nil { return nil } var diags tfdiags.Diagnostics countVal, countDiags := ctx.EvaluateExpr(expr, cty.Number, nil) diags = diags.Append(countDiags) if diags.HasErrors() { return diags } if countVal.IsNull() { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid count argument", Detail: `The given "count" argument value is null. An integer is required.`, Subject: expr.Range().Ptr(), }) return diags } var err error countVal, err = convert.Convert(countVal, cty.Number) if err != nil { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid count argument", Detail: fmt.Sprintf(`The given "count" argument value is unsuitable: %s.`, err), Subject: expr.Range().Ptr(), }) return diags } // If the value isn't known then that's the best we can do for now, but // we'll check more thoroughly during the plan walk. if !countVal.IsKnown() { return diags } // If we _do_ know the value, then we can do a few more checks here. var count int err = gocty.FromCtyValue(countVal, &count) if err != nil { // Isn't a whole number, etc. diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid count argument", Detail: fmt.Sprintf(`The given "count" argument value is unsuitable: %s.`, err), Subject: expr.Range().Ptr(), }) return diags } if count < 0 { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid count argument", Detail: `The given "count" argument value is unsuitable: count cannot be negative.`, Subject: expr.Range().Ptr(), }) return diags } return diags } func (n *EvalValidateResource) validateForEach(ctx EvalContext, expr hcl.Expression) (diags tfdiags.Diagnostics) { _, known, forEachDiags := evaluateResourceForEachExpressionKnown(expr, ctx) // If the value isn't known then that's the best we can do for now, but // we'll check more thoroughly during the plan walk if !known { return diags } if forEachDiags.HasErrors() { diags = diags.Append(forEachDiags) } return diags }