opentofu/terraform/eval_validate.go
Martin Atkins 0e963db2c5 Detect and reject unknown attributes in "connection" blocks
Since the validation of connection blocks is delegated to the communicator
selected by "type", we were not previously doing any validation of the
attribute names in these blocks until running provisioners during apply.

Proper validation here requires us to already have the instance state,
since the final connection info is a merge of values provided in config
with values assigned automatically by the resource. However, we can do
some basic name validation to catch typos during the validation pass, even
though semantic validation and checking for missing attributes will still
wait until the provisioner is instantiated.

This fixes #6582 as much as we reasonably can.
2017-04-06 13:46:43 -07:00

228 lines
6.4 KiB
Go

package terraform
import (
"fmt"
"github.com/hashicorp/terraform/config"
"github.com/mitchellh/mapstructure"
)
// EvalValidateError is the error structure returned if there were
// validation errors.
type EvalValidateError struct {
Warnings []string
Errors []error
}
func (e *EvalValidateError) Error() string {
return fmt.Sprintf("Warnings: %s. Errors: %s", e.Warnings, e.Errors)
}
// EvalValidateCount is an EvalNode implementation that validates
// the count of a resource.
type EvalValidateCount struct {
Resource *config.Resource
}
// TODO: test
func (n *EvalValidateCount) Eval(ctx EvalContext) (interface{}, error) {
var count int
var errs []error
var err error
if _, err := ctx.Interpolate(n.Resource.RawCount, nil); err != nil {
errs = append(errs, fmt.Errorf(
"Failed to interpolate count: %s", err))
goto RETURN
}
count, err = n.Resource.Count()
if err != nil {
// If we can't get the count during validation, then
// just replace it with the number 1.
c := n.Resource.RawCount.Config()
c[n.Resource.RawCount.Key] = "1"
count = 1
}
err = nil
if count < 0 {
errs = append(errs, fmt.Errorf(
"Count is less than zero: %d", count))
}
RETURN:
if len(errs) != 0 {
err = &EvalValidateError{
Errors: errs,
}
}
return nil, err
}
// EvalValidateProvider is an EvalNode implementation that validates
// the configuration of a resource.
type EvalValidateProvider struct {
Provider *ResourceProvider
Config **ResourceConfig
}
func (n *EvalValidateProvider) Eval(ctx EvalContext) (interface{}, error) {
provider := *n.Provider
config := *n.Config
warns, errs := provider.Validate(config)
if len(warns) == 0 && len(errs) == 0 {
return nil, nil
}
return nil, &EvalValidateError{
Warnings: warns,
Errors: errs,
}
}
// EvalValidateProvisioner is an EvalNode implementation that validates
// the configuration of a resource.
type EvalValidateProvisioner struct {
Provisioner *ResourceProvisioner
Config **ResourceConfig
ConnConfig **ResourceConfig
}
func (n *EvalValidateProvisioner) Eval(ctx EvalContext) (interface{}, error) {
provisioner := *n.Provisioner
config := *n.Config
var warns []string
var errs []error
{
// Validate the provisioner's own config first
w, e := provisioner.Validate(config)
warns = append(warns, w...)
errs = append(errs, e...)
}
{
// Now validate the connection config, which might either be from
// the provisioner block itself or inherited from the resource's
// shared connection info.
w, e := n.validateConnConfig(*n.ConnConfig)
warns = append(warns, w...)
errs = append(errs, e...)
}
if len(warns) == 0 && len(errs) == 0 {
return nil, nil
}
return nil, &EvalValidateError{
Warnings: warns,
Errors: errs,
}
}
func (n *EvalValidateProvisioner) validateConnConfig(connConfig *ResourceConfig) (warns []string, errs []error) {
// 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.
type connConfigSuperset struct {
// All attribute types are interface{} here because at this point we
// may still have unresolved interpolation expressions, which will
// appear as strings regardless of the final goal type.
Type interface{} `mapstructure:"type"`
User interface{} `mapstructure:"user"`
Password interface{} `mapstructure:"password"`
Host interface{} `mapstructure:"host"`
Port interface{} `mapstructure:"port"`
Timeout interface{} `mapstructure:"timeout"`
ScriptPath interface{} `mapstructure:"script_path"`
// For type=ssh only (enforced in ssh communicator)
PrivateKey interface{} `mapstructure:"private_key"`
Agent interface{} `mapstructure:"agent"`
BastionHost interface{} `mapstructure:"bastion_host"`
BastionPort interface{} `mapstructure:"bastion_port"`
BastionUser interface{} `mapstructure:"bastion_user"`
BastionPassword interface{} `mapstructure:"bastion_password"`
BastionPrivateKey interface{} `mapstructure:"bastion_private_key"`
// For type=winrm only (enforced in winrm communicator)
HTTPS interface{} `mapstructure:"https"`
Insecure interface{} `mapstructure:"insecure"`
CACert interface{} `mapstructure:"cacert"`
}
var metadata mapstructure.Metadata
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Metadata: &metadata,
Result: &connConfigSuperset{}, // result is disregarded; we only care about unused keys
})
if err != nil {
// should never happen
errs = append(errs, err)
return
}
if err := decoder.Decode(connConfig.Config); err != nil {
errs = append(errs, err)
return
}
for _, attrName := range metadata.Unused {
errs = append(errs, fmt.Errorf("unknown 'connection' argument %q", attrName))
}
return
}
// EvalValidateResource is an EvalNode implementation that validates
// the configuration of a resource.
type EvalValidateResource struct {
Provider *ResourceProvider
Config **ResourceConfig
ResourceName string
ResourceType string
ResourceMode config.ResourceMode
// IgnoreWarnings means that warnings will not be passed through. This allows
// "just-in-time" passes of validation to continue execution through warnings.
IgnoreWarnings bool
}
func (n *EvalValidateResource) Eval(ctx EvalContext) (interface{}, error) {
provider := *n.Provider
cfg := *n.Config
var warns []string
var errs []error
// Provider entry point varies depending on resource mode, because
// managed resources and data resources are two distinct concepts
// in the provider abstraction.
switch n.ResourceMode {
case config.ManagedResourceMode:
warns, errs = provider.ValidateResource(n.ResourceType, cfg)
case config.DataResourceMode:
warns, errs = provider.ValidateDataSource(n.ResourceType, cfg)
}
// If the resource name doesn't match the name regular
// expression, show an error.
if !config.NameRegexp.Match([]byte(n.ResourceName)) {
errs = append(errs, fmt.Errorf(
"%s: resource name can only contain letters, numbers, "+
"dashes, and underscores.", n.ResourceName))
}
if (len(warns) == 0 || n.IgnoreWarnings) && len(errs) == 0 {
return nil, nil
}
return nil, &EvalValidateError{
Warnings: warns,
Errors: errs,
}
}