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  }