mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-30 10:47:14 -06:00
1a68fdb4f6
Add `host_key` and `bastion_host_key` fields to the ssh communicator config for strict host key checking. Both fields expect the contents of an openssh formated public key. This key can either be the remote host's public key, or the public key of the CA which signed the remote host certificate. Support for signed certificates is limited, because the provisioner usually connects to a remote host by ip address rather than hostname, so the certificate would need to be signed appropriately. Connecting via a hostname needs to currently be done through a secondary provisioner, like one attached to a null_resource.
231 lines
6.6 KiB
Go
231 lines
6.6 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"`
|
|
HostKey interface{} `mapstructure:"host_key"`
|
|
Agent interface{} `mapstructure:"agent"`
|
|
BastionHost interface{} `mapstructure:"bastion_host"`
|
|
BastionHostKey interface{} `mapstructure:"bastion_host_key"`
|
|
BastionPort interface{} `mapstructure:"bastion_port"`
|
|
BastionUser interface{} `mapstructure:"bastion_user"`
|
|
BastionPassword interface{} `mapstructure:"bastion_password"`
|
|
BastionPrivateKey interface{} `mapstructure:"bastion_private_key"`
|
|
AgentIdentity interface{} `mapstructure:"agent_identity"`
|
|
|
|
// 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,
|
|
}
|
|
}
|