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  }