From d42de4b35b65a79d8c86793972557d3ae80de8d9 Mon Sep 17 00:00:00 2001 From: Vladislav Rassokhin Date: Tue, 29 Nov 2016 23:00:26 +0300 Subject: [PATCH 1/2] Various built-in provisioners improvements: 1. Migrate `chef` provisioner to `schema.Provisioner`: * `chef.Provisioner` structure was renamed to `ProvisionerS`and now it's decoded from `schema.ResourceData` instead of `terraform.ResourceConfig` using simple copy-paste-based solution; * Added simple schema without any validation yet. 2. Support `ValidateFunc` validate function : implemented in `file` and `chef` provisioners. --- builtin/bins/provisioner-chef/main.go | 5 +- .../provisioners/chef/linux_provisioner.go | 6 +- .../chef/linux_provisioner_test.go | 6 +- .../provisioners/chef/resource_provisioner.go | 369 +++++++++++++----- .../chef/resource_provisioner_test.go | 28 +- .../provisioners/chef/windows_provisioner.go | 4 +- .../chef/windows_provisioner_test.go | 6 +- .../provisioners/file/resource_provisioner.go | 18 +- .../file/resource_provisioner_test.go | 25 ++ .../local-exec/resource_provisioner_test.go | 11 + .../remote-exec/resource_provisioner_test.go | 10 + helper/schema/provisioner.go | 31 +- helper/schema/provisioner_test.go | 78 +++- helper/schema/testing.go | 7 + 14 files changed, 466 insertions(+), 138 deletions(-) diff --git a/builtin/bins/provisioner-chef/main.go b/builtin/bins/provisioner-chef/main.go index a12c65cf7607..6e81fde49816 100644 --- a/builtin/bins/provisioner-chef/main.go +++ b/builtin/bins/provisioner-chef/main.go @@ -3,13 +3,10 @@ package main import ( "github.com/hashicorp/terraform/builtin/provisioners/chef" "github.com/hashicorp/terraform/plugin" - "github.com/hashicorp/terraform/terraform" ) func main() { plugin.Serve(&plugin.ServeOpts{ - ProvisionerFunc: func() terraform.ResourceProvisioner { - return new(chef.ResourceProvisioner) - }, + ProvisionerFunc: chef.Provisioner, }) } diff --git a/builtin/provisioners/chef/linux_provisioner.go b/builtin/provisioners/chef/linux_provisioner.go index ebfe729795ba..a9ad806a06c9 100644 --- a/builtin/provisioners/chef/linux_provisioner.go +++ b/builtin/provisioners/chef/linux_provisioner.go @@ -14,7 +14,7 @@ const ( installURL = "https://www.chef.io/chef/install.sh" ) -func (p *Provisioner) linuxInstallChefClient( +func (p *ProvisionerS) linuxInstallChefClient( o terraform.UIOutput, comm communicator.Communicator) error { @@ -26,7 +26,7 @@ func (p *Provisioner) linuxInstallChefClient( if p.HTTPSProxy != "" { prefix += fmt.Sprintf("https_proxy='%s' ", p.HTTPSProxy) } - if p.NOProxy != nil { + if p.NOProxy != nil && len(p.NOProxy) > 0 { prefix += fmt.Sprintf("no_proxy='%s' ", strings.Join(p.NOProxy, ",")) } @@ -46,7 +46,7 @@ func (p *Provisioner) linuxInstallChefClient( return p.runCommand(o, comm, fmt.Sprintf("%srm -f install.sh", prefix)) } -func (p *Provisioner) linuxCreateConfigFiles( +func (p *ProvisionerS) linuxCreateConfigFiles( o terraform.UIOutput, comm communicator.Communicator) error { // Make sure the config directory exists diff --git a/builtin/provisioners/chef/linux_provisioner_test.go b/builtin/provisioners/chef/linux_provisioner_test.go index 7d2339e04f67..444a0f39ac3a 100644 --- a/builtin/provisioners/chef/linux_provisioner_test.go +++ b/builtin/provisioners/chef/linux_provisioner_test.go @@ -125,14 +125,13 @@ func TestResourceProvider_linuxInstallChefClient(t *testing.T) { }, } - r := new(ResourceProvisioner) o := new(terraform.MockUIOutput) c := new(communicator.MockCommunicator) for k, tc := range cases { c.Commands = tc.Commands - p, err := r.decodeConfig(tc.Config) + p, err := decodeConfig(getTestResourceData(tc.Config)) if err != nil { t.Fatalf("Error: %v", err) } @@ -264,7 +263,6 @@ func TestResourceProvider_linuxCreateConfigFiles(t *testing.T) { }, } - r := new(ResourceProvisioner) o := new(terraform.MockUIOutput) c := new(communicator.MockCommunicator) @@ -272,7 +270,7 @@ func TestResourceProvider_linuxCreateConfigFiles(t *testing.T) { c.Commands = tc.Commands c.Uploads = tc.Uploads - p, err := r.decodeConfig(tc.Config) + p, err := decodeConfig(getTestResourceData(tc.Config)) if err != nil { t.Fatalf("Error: %v", err) } diff --git a/builtin/provisioners/chef/resource_provisioner.go b/builtin/provisioners/chef/resource_provisioner.go index 22f300c44b55..c4b37320771f 100644 --- a/builtin/provisioners/chef/resource_provisioner.go +++ b/builtin/provisioners/chef/resource_provisioner.go @@ -15,12 +15,13 @@ import ( "text/template" "time" + "context" "github.com/hashicorp/terraform/communicator" "github.com/hashicorp/terraform/communicator/remote" + "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/go-homedir" "github.com/mitchellh/go-linereader" - "github.com/mitchellh/mapstructure" ) const ( @@ -81,36 +82,36 @@ enable_reporting false {{ end }} ` -// Provisioner represents a Chef provisioner -type Provisioner struct { - AttributesJSON string `mapstructure:"attributes_json"` - ClientOptions []string `mapstructure:"client_options"` - DisableReporting bool `mapstructure:"disable_reporting"` - Environment string `mapstructure:"environment"` - FetchChefCertificates bool `mapstructure:"fetch_chef_certificates"` - LogToFile bool `mapstructure:"log_to_file"` - UsePolicyfile bool `mapstructure:"use_policyfile"` - PolicyGroup string `mapstructure:"policy_group"` - PolicyName string `mapstructure:"policy_name"` - HTTPProxy string `mapstructure:"http_proxy"` - HTTPSProxy string `mapstructure:"https_proxy"` - NamedRunList string `mapstructure:"named_run_list"` - NOProxy []string `mapstructure:"no_proxy"` - NodeName string `mapstructure:"node_name"` - OhaiHints []string `mapstructure:"ohai_hints"` - OSType string `mapstructure:"os_type"` - RecreateClient bool `mapstructure:"recreate_client"` - PreventSudo bool `mapstructure:"prevent_sudo"` - RunList []string `mapstructure:"run_list"` - SecretKey string `mapstructure:"secret_key"` - ServerURL string `mapstructure:"server_url"` - SkipInstall bool `mapstructure:"skip_install"` - SkipRegister bool `mapstructure:"skip_register"` - SSLVerifyMode string `mapstructure:"ssl_verify_mode"` - UserName string `mapstructure:"user_name"` - UserKey string `mapstructure:"user_key"` - VaultJSON string `mapstructure:"vault_json"` - Version string `mapstructure:"version"` +// ProvisionerS represents a Chef provisioner +type ProvisionerS struct { + AttributesJSON string + ClientOptions []string + DisableReporting bool + Environment string + FetchChefCertificates bool + LogToFile bool + UsePolicyfile bool + PolicyGroup string + PolicyName string + HTTPProxy string + HTTPSProxy string + NamedRunList string + NOProxy []string + NodeName string + OhaiHints []string + OSType string + RecreateClient bool + PreventSudo bool + RunList []string + SecretKey string + ServerURL string + SkipInstall bool + SkipRegister bool + SSLVerifyMode string + UserName string + UserKey string + VaultJSON string + Version string attributes map[string]interface{} vaults map[string][]string @@ -125,37 +126,179 @@ type Provisioner struct { useSudo bool // Deprecated Fields - ValidationClientName string `mapstructure:"validation_client_name"` - ValidationKey string `mapstructure:"validation_key"` + ValidationClientName string + ValidationKey string } -// ResourceProvisioner represents a generic chef provisioner -type ResourceProvisioner struct{} - -func (r *ResourceProvisioner) Stop() error { - // Noop for now. TODO in the future. - return nil +func Provisioner() terraform.ResourceProvisioner { + return &schema.Provisioner{ + Schema: map[string]*schema.Schema{ + "attributes_json": { + Type: schema.TypeString, + Optional: true, + }, + "client_options": { + Type: schema.TypeList, + Elem: schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "disable_reporting": { + Type: schema.TypeBool, + Optional: true, + }, + "environment": { + Type: schema.TypeString, + Optional: true, + }, + "fetch_chef_certificates": { + Type: schema.TypeBool, + Optional: true, + }, + "log_to_file": { + Type: schema.TypeBool, + Optional: true, + }, + "use_policyfile": { + Type: schema.TypeBool, + Optional: true, + }, + "policy_group": { + Type: schema.TypeString, + Optional: true, + }, + "policy_name": { + Type: schema.TypeString, + Optional: true, + }, + "http_proxy": { + Type: schema.TypeString, + Optional: true, + }, + "https_proxy": { + Type: schema.TypeString, + Optional: true, + }, + "no_proxy": { + Type: schema.TypeList, + Elem: schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + Optional: true, + }, + "named_run_list": { + Type: schema.TypeString, + Optional: true, + }, + "node_name": { + Type: schema.TypeString, + Required: true, + }, + "ohai_hints": { + Type: schema.TypeList, + Elem: schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + Optional: true, + }, + "os_type": { + Type: schema.TypeString, + Optional: true, + }, + "recreate_client": { + Type: schema.TypeBool, + Optional: true, + }, + "prevent_sudo": { + Type: schema.TypeBool, + Optional: true, + }, + "run_list": { + Type: schema.TypeList, + Elem: schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + Optional: true, + }, + "secret_key": { + Type: schema.TypeString, + Optional: true, + }, + "server_url": { + Type: schema.TypeString, + Required: true, + }, + "skip_install": { + Type: schema.TypeBool, + Optional: true, + }, + "skip_register": { + Type: schema.TypeBool, + Optional: true, + }, + "ssl_verify_mode": { + Type: schema.TypeString, + Optional: true, + }, + "user_name": { + Type: schema.TypeString, + Optional: true, + }, + "user_key": { + Type: schema.TypeString, + Optional: true, + }, + "vault_json": { + Type: schema.TypeString, + Optional: true, + }, + "version": { + Type: schema.TypeString, + Optional: true, + }, + + // Deprecated + "validation_client_name": { + Type: schema.TypeString, + Deprecated: "Please use user_name instead", + Optional: true, + }, + + "validation_key": { + Type: schema.TypeString, + Deprecated: "Please use user_key instead", + Optional: true, + }, + }, + ApplyFunc: Apply, + ValidateFunc: Validate, + } } +// TODO: Support context cancelling (Provisioner Stop) // Apply executes the file provisioner -func (r *ResourceProvisioner) Apply( - o terraform.UIOutput, - s *terraform.InstanceState, - c *terraform.ResourceConfig) error { +func Apply(ctx context.Context) error { + o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput) + d := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData) // Decode the raw config for this provisioner - p, err := r.decodeConfig(c) + p, err := decodeConfig(d) if err != nil { return err } if p.OSType == "" { - switch s.Ephemeral.ConnInfo["type"] { + t := d.State().Ephemeral.ConnInfo["type"] + switch t { case "ssh", "": // The default connection type is ssh, so if the type is empty assume ssh p.OSType = "linux" case "winrm": p.OSType = "windows" default: - return fmt.Errorf("Unsupported connection type: %s", s.Ephemeral.ConnInfo["type"]) + return fmt.Errorf("Unsupported connection type: %s", t) } } @@ -169,7 +312,7 @@ func (r *ResourceProvisioner) Apply( p.generateClientKey = p.generateClientKeyFunc(linuxKnifeCmd, linuxConfDir, linuxNoOutput) p.configureVaults = p.configureVaultsFunc(linuxGemCmd, linuxKnifeCmd, linuxConfDir) p.runChefClient = p.runChefClientFunc(linuxChefCmd, linuxConfDir) - p.useSudo = !p.PreventSudo && s.Ephemeral.ConnInfo["user"] != "root" + p.useSudo = !p.PreventSudo && d.State().Ephemeral.ConnInfo["user"] != "root" case "windows": p.cleanupUserKeyCmd = fmt.Sprintf("cd %s && del /F /Q %s", windowsConfDir, p.UserName+".pem") p.createConfigFiles = p.windowsCreateConfigFiles @@ -184,7 +327,7 @@ func (r *ResourceProvisioner) Apply( } // Get a new communicator - comm, err := communicator.New(s) + comm, err := communicator.New(d.State()) if err != nil { return err } @@ -254,22 +397,16 @@ func (r *ResourceProvisioner) Apply( } // Validate checks if the required arguments are configured -func (r *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) { - p, err := r.decodeConfig(c) +func Validate(d *schema.ResourceData) (ws []string, es []error) { + p, err := decodeConfig(d) if err != nil { es = append(es, err) return ws, es } - if p.NodeName == "" { - es = append(es, errors.New("Key not found: node_name")) - } if !p.UsePolicyfile && p.RunList == nil { es = append(es, errors.New("Key not found: run_list")) } - if p.ServerURL == "" { - es = append(es, errors.New("Key not found: server_url")) - } if p.UsePolicyfile && p.PolicyName == "" { es = append(es, errors.New("Policyfile enabled but key not found: policy_name")) } @@ -284,12 +421,7 @@ func (r *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string es = append(es, errors.New( "One of user_key or the deprecated validation_key must be provided")) } - if p.ValidationClientName != "" { - ws = append(ws, "validation_client_name is deprecated, please use user_name instead") - } if p.ValidationKey != "" { - ws = append(ws, "validation_key is deprecated, please use user_key instead") - if p.RecreateClient { es = append(es, errors.New( "Cannot use recreate_client=true with the deprecated validation_key, please provide a user_key")) @@ -303,38 +435,8 @@ func (r *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string return ws, es } -func (r *ResourceProvisioner) decodeConfig(c *terraform.ResourceConfig) (*Provisioner, error) { - p := new(Provisioner) - - decConf := &mapstructure.DecoderConfig{ - ErrorUnused: true, - WeaklyTypedInput: true, - Result: p, - } - dec, err := mapstructure.NewDecoder(decConf) - if err != nil { - return nil, err - } - - // We need to merge both configs into a single map first. Order is - // important as we need to make sure interpolated values are used - // over raw values. This makes sure that all values are there even - // if some still need to be interpolated later on. Without this - // the validation will fail when using a variable for a required - // parameter (the node_name for example). - m := make(map[string]interface{}) - - for k, v := range c.Raw { - m[k] = v - } - - for k, v := range c.Config { - m[k] = v - } - - if err := dec.Decode(m); err != nil { - return nil, err - } +func decodeConfig(d *schema.ResourceData) (*ProvisionerS, error) { + p := decodeDataToProvisioner(d) // Make sure the supplied URL has a trailing slash p.ServerURL = strings.TrimSuffix(p.ServerURL, "/") + "/" @@ -359,17 +461,17 @@ func (r *ResourceProvisioner) decodeConfig(c *terraform.ResourceConfig) (*Provis p.UserKey = p.ValidationKey } - if attrs, ok := c.Config["attributes_json"].(string); ok && !c.IsComputed("attributes_json") { + if attrs, ok := d.GetOk("attributes_json"); ok { var m map[string]interface{} - if err := json.Unmarshal([]byte(attrs), &m); err != nil { + if err := json.Unmarshal([]byte(attrs.(string)), &m); err != nil { return nil, fmt.Errorf("Error parsing attributes_json: %v", err) } p.attributes = m } - if vaults, ok := c.Config["vault_json"].(string); ok && !c.IsComputed("vault_json") { + if vaults, ok := d.GetOk("vault_json"); ok { var m map[string]interface{} - if err := json.Unmarshal([]byte(vaults), &m); err != nil { + if err := json.Unmarshal([]byte(vaults.(string)), &m); err != nil { return nil, fmt.Errorf("Error parsing vault_json: %v", err) } @@ -395,7 +497,7 @@ func (r *ResourceProvisioner) decodeConfig(c *terraform.ResourceConfig) (*Provis return p, nil } -func (p *Provisioner) deployConfigFiles( +func (p *ProvisionerS) deployConfigFiles( o terraform.UIOutput, comm communicator.Communicator, confDir string) error { @@ -469,7 +571,7 @@ func (p *Provisioner) deployConfigFiles( return nil } -func (p *Provisioner) deployOhaiHints( +func (p *ProvisionerS) deployOhaiHints( o terraform.UIOutput, comm communicator.Communicator, hintDir string) error { @@ -490,7 +592,7 @@ func (p *Provisioner) deployOhaiHints( return nil } -func (p *Provisioner) fetchChefCertificatesFunc( +func (p *ProvisionerS) fetchChefCertificatesFunc( knifeCmd string, confDir string) func(terraform.UIOutput, communicator.Communicator) error { return func(o terraform.UIOutput, comm communicator.Communicator) error { @@ -501,7 +603,7 @@ func (p *Provisioner) fetchChefCertificatesFunc( } } -func (p *Provisioner) generateClientKeyFunc( +func (p *ProvisionerS) generateClientKeyFunc( knifeCmd string, confDir string, noOutput string) func(terraform.UIOutput, communicator.Communicator) error { @@ -562,7 +664,7 @@ func (p *Provisioner) generateClientKeyFunc( } } -func (p *Provisioner) configureVaultsFunc( +func (p *ProvisionerS) configureVaultsFunc( gemCmd string, knifeCmd string, confDir string) func(terraform.UIOutput, communicator.Communicator) error { @@ -596,7 +698,7 @@ func (p *Provisioner) configureVaultsFunc( } } -func (p *Provisioner) runChefClientFunc( +func (p *ProvisionerS) runChefClientFunc( chefCmd string, confDir string) func(terraform.UIOutput, communicator.Communicator) error { return func(o terraform.UIOutput, comm communicator.Communicator) error { @@ -634,7 +736,7 @@ func (p *Provisioner) runChefClientFunc( } // Output implementation of terraform.UIOutput interface -func (p *Provisioner) Output(output string) { +func (p *ProvisionerS) Output(output string) { logFile := path.Join(logfileDir, p.NodeName) f, err := os.OpenFile(logFile, os.O_APPEND|os.O_WRONLY, 0666) if err != nil { @@ -660,7 +762,7 @@ func (p *Provisioner) Output(output string) { } // runCommand is used to run already prepared commands -func (p *Provisioner) runCommand( +func (p *ProvisionerS) runCommand( o terraform.UIOutput, comm communicator.Communicator, command string) error { @@ -702,7 +804,7 @@ func (p *Provisioner) runCommand( return err } -func (p *Provisioner) copyOutput(o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) { +func (p *ProvisionerS) copyOutput(o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) { defer close(doneCh) lr := linereader.New(r) for line := range lr.Ch { @@ -727,3 +829,58 @@ func retryFunc(timeout time.Duration, f func() error) error { } } } + +func decodeDataToProvisioner(d *schema.ResourceData) *ProvisionerS { + return &ProvisionerS{ + AttributesJSON: d.Get("attributes_json").(string), + ClientOptions: getStringList(d.Get("client_options")), + DisableReporting: d.Get("disable_reporting").(bool), + Environment: d.Get("environment").(string), + FetchChefCertificates: d.Get("fetch_chef_certificates").(bool), + LogToFile: d.Get("log_to_file").(bool), + UsePolicyfile: d.Get("use_policyfile").(bool), + PolicyGroup: d.Get("policy_group").(string), + PolicyName: d.Get("policy_name").(string), + HTTPProxy: d.Get("http_proxy").(string), + HTTPSProxy: d.Get("https_proxy").(string), + NOProxy: getStringList(d.Get("no_proxy")), + NamedRunList: d.Get("named_run_list").(string), + NodeName: d.Get("node_name").(string), + OhaiHints: getStringList(d.Get("ohai_hints")), + OSType: d.Get("os_type").(string), + RecreateClient: d.Get("recreate_client").(bool), + PreventSudo: d.Get("prevent_sudo").(bool), + RunList: getStringList(d.Get("run_list")), + SecretKey: d.Get("secret_key").(string), + ServerURL: d.Get("server_url").(string), + SkipInstall: d.Get("skip_install").(bool), + SkipRegister: d.Get("skip_register").(bool), + SSLVerifyMode: d.Get("ssl_verify_mode").(string), + UserName: d.Get("user_name").(string), + UserKey: d.Get("user_key").(string), + VaultJSON: d.Get("vault_json").(string), + Version: d.Get("version").(string), + + // Deprecated + ValidationClientName: d.Get("validation_client_name").(string), + ValidationKey: d.Get("validation_key").(string), + } +} + +func getStringList(v interface{}) []string { + if v == nil { + return nil + } + switch l := v.(type) { + case []string: + return l + case []interface{}: + arr := make([]string, len(l)) + for i, x := range l { + arr[i] = x.(string) + } + return arr + default: + panic(fmt.Sprintf("Unsupported type: %T", v)) + } +} diff --git a/builtin/provisioners/chef/resource_provisioner_test.go b/builtin/provisioners/chef/resource_provisioner_test.go index ef7861572b7a..8e6f0deb2cf2 100644 --- a/builtin/provisioners/chef/resource_provisioner_test.go +++ b/builtin/provisioners/chef/resource_provisioner_test.go @@ -7,11 +7,18 @@ import ( "github.com/hashicorp/terraform/communicator" "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" ) func TestResourceProvisioner_impl(t *testing.T) { - var _ terraform.ResourceProvisioner = new(ResourceProvisioner) + var _ terraform.ResourceProvisioner = Provisioner() +} + +func TestProvisioner(t *testing.T) { + if err := Provisioner().(*schema.Provisioner).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } } func TestResourceProvider_Validate_good(t *testing.T) { @@ -23,7 +30,7 @@ func TestResourceProvider_Validate_good(t *testing.T) { "user_name": "bob", "user_key": "USER-KEY", }) - r := new(ResourceProvisioner) + r := Provisioner() warn, errs := r.Validate(c) if len(warn) > 0 { t.Fatalf("Warnings: %v", warn) @@ -37,7 +44,7 @@ func TestResourceProvider_Validate_bad(t *testing.T) { c := testConfig(t, map[string]interface{}{ "invalid": "nope", }) - p := new(ResourceProvisioner) + p := Provisioner() warn, errs := p.Validate(c) if len(warn) > 0 { t.Fatalf("Warnings: %v", warn) @@ -59,7 +66,7 @@ func TestResourceProvider_Validate_computedValues(t *testing.T) { "user_key": "USER-KEY", "attributes_json": config.UnknownVariableValue, }) - r := new(ResourceProvisioner) + r := Provisioner() warn, errs := r.Validate(c) if len(warn) > 0 { t.Fatalf("Warnings: %v", warn) @@ -149,14 +156,13 @@ func TestResourceProvider_runChefClient(t *testing.T) { }, } - r := new(ResourceProvisioner) o := new(terraform.MockUIOutput) c := new(communicator.MockCommunicator) for k, tc := range cases { c.Commands = tc.Commands - p, err := r.decodeConfig(tc.Config) + p, err := decodeConfig(getTestResourceData(tc.Config)) if err != nil { t.Fatalf("Error: %v", err) } @@ -222,14 +228,13 @@ func TestResourceProvider_fetchChefCertificates(t *testing.T) { }, } - r := new(ResourceProvisioner) o := new(terraform.MockUIOutput) c := new(communicator.MockCommunicator) for k, tc := range cases { c.Commands = tc.Commands - p, err := r.decodeConfig(tc.Config) + p, err := decodeConfig(getTestResourceData(tc.Config)) if err != nil { t.Fatalf("Error: %v", err) } @@ -347,14 +352,13 @@ func TestResourceProvider_configureVaults(t *testing.T) { }, } - r := new(ResourceProvisioner) o := new(terraform.MockUIOutput) c := new(communicator.MockCommunicator) for k, tc := range cases { c.Commands = tc.Commands - p, err := r.decodeConfig(tc.Config) + p, err := decodeConfig(getTestResourceData(tc.Config)) if err != nil { t.Fatalf("Error: %v", err) } @@ -368,3 +372,7 @@ func TestResourceProvider_configureVaults(t *testing.T) { } } } + +func getTestResourceData(c *terraform.ResourceConfig) *schema.ResourceData { + return schema.TestResourceDataConfig(Provisioner().(*schema.Provisioner).Schema, c) +} diff --git a/builtin/provisioners/chef/windows_provisioner.go b/builtin/provisioners/chef/windows_provisioner.go index 95373402101f..773d1b0f2b8c 100644 --- a/builtin/provisioners/chef/windows_provisioner.go +++ b/builtin/provisioners/chef/windows_provisioner.go @@ -46,7 +46,7 @@ Write-Host 'Installing Chef Client...' Start-Process -FilePath msiexec -ArgumentList /qn, /i, $dest -Wait ` -func (p *Provisioner) windowsInstallChefClient( +func (p *ProvisionerS) windowsInstallChefClient( o terraform.UIOutput, comm communicator.Communicator) error { script := path.Join(path.Dir(comm.ScriptPath()), "ChefClient.ps1") @@ -62,7 +62,7 @@ func (p *Provisioner) windowsInstallChefClient( return p.runCommand(o, comm, installCmd) } -func (p *Provisioner) windowsCreateConfigFiles( +func (p *ProvisionerS) windowsCreateConfigFiles( o terraform.UIOutput, comm communicator.Communicator) error { // Make sure the config directory exists diff --git a/builtin/provisioners/chef/windows_provisioner_test.go b/builtin/provisioners/chef/windows_provisioner_test.go index 659953d978b6..0bb0dc126aa4 100644 --- a/builtin/provisioners/chef/windows_provisioner_test.go +++ b/builtin/provisioners/chef/windows_provisioner_test.go @@ -73,7 +73,6 @@ func TestResourceProvider_windowsInstallChefClient(t *testing.T) { }, } - r := new(ResourceProvisioner) o := new(terraform.MockUIOutput) c := new(communicator.MockCommunicator) @@ -81,7 +80,7 @@ func TestResourceProvider_windowsInstallChefClient(t *testing.T) { c.Commands = tc.Commands c.UploadScripts = tc.UploadScripts - p, err := r.decodeConfig(tc.Config) + p, err := decodeConfig(getTestResourceData(tc.Config)) if err != nil { t.Fatalf("Error: %v", err) } @@ -180,7 +179,6 @@ func TestResourceProvider_windowsCreateConfigFiles(t *testing.T) { }, } - r := new(ResourceProvisioner) o := new(terraform.MockUIOutput) c := new(communicator.MockCommunicator) @@ -188,7 +186,7 @@ func TestResourceProvider_windowsCreateConfigFiles(t *testing.T) { c.Commands = tc.Commands c.Uploads = tc.Uploads - p, err := r.decodeConfig(tc.Config) + p, err := decodeConfig(getTestResourceData(tc.Config)) if err != nil { t.Fatalf("Error: %v", err) } diff --git a/builtin/provisioners/file/resource_provisioner.go b/builtin/provisioners/file/resource_provisioner.go index 001e78af5201..eb29424b4a17 100644 --- a/builtin/provisioners/file/resource_provisioner.go +++ b/builtin/provisioners/file/resource_provisioner.go @@ -35,7 +35,8 @@ func Provisioner() terraform.ResourceProvisioner { }, }, - ApplyFunc: applyFn, + ApplyFunc: applyFn, + ValidateFunc: Validate, } } @@ -77,6 +78,21 @@ func applyFn(ctx context.Context) error { } } +// Validate checks if the required arguments are configured +func Validate(d *schema.ResourceData) (ws []string, es []error) { + numSrc := 0 + if _, ok := d.GetOk("source"); ok == true { + numSrc++ + } + if _, ok := d.GetOk("content"); ok == true { + numSrc++ + } + if numSrc != 1 { + es = append(es, fmt.Errorf("Must provide one of 'content' or 'source'")) + } + return +} + // getSrc returns the file to use as source func getSrc(data *schema.ResourceData) (string, bool, error) { src := data.Get("source").(string) diff --git a/builtin/provisioners/file/resource_provisioner_test.go b/builtin/provisioners/file/resource_provisioner_test.go index d57dbbaa5853..96afc519fab2 100644 --- a/builtin/provisioners/file/resource_provisioner_test.go +++ b/builtin/provisioners/file/resource_provisioner_test.go @@ -4,9 +4,20 @@ import ( "testing" "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" ) +func TestResourceProvisioner_impl(t *testing.T) { + var _ terraform.ResourceProvisioner = Provisioner() +} + +func TestProvisioner(t *testing.T) { + if err := Provisioner().(*schema.Provisioner).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + func TestResourceProvider_Validate_good_source(t *testing.T) { c := testConfig(t, map[string]interface{}{ "source": "/tmp/foo", @@ -51,6 +62,20 @@ func TestResourceProvider_Validate_bad_not_destination(t *testing.T) { } } +func TestResourceProvider_Validate_bad_no_source(t *testing.T) { + c := testConfig(t, map[string]interface{}{ + "destination": "/tmp/bar", + }) + p := Provisioner() + warn, errs := p.Validate(c) + if len(warn) > 0 { + t.Fatalf("Warnings: %v", warn) + } + if len(errs) == 0 { + t.Fatalf("Should have errors") + } +} + func TestResourceProvider_Validate_bad_to_many_src(t *testing.T) { c := testConfig(t, map[string]interface{}{ "source": "nope", diff --git a/builtin/provisioners/local-exec/resource_provisioner_test.go b/builtin/provisioners/local-exec/resource_provisioner_test.go index 2dff4a85de7e..85fb6abcf8ea 100644 --- a/builtin/provisioners/local-exec/resource_provisioner_test.go +++ b/builtin/provisioners/local-exec/resource_provisioner_test.go @@ -8,9 +8,20 @@ import ( "time" "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" ) +func TestResourceProvisioner_impl(t *testing.T) { + var _ terraform.ResourceProvisioner = Provisioner() +} + +func TestProvisioner(t *testing.T) { + if err := Provisioner().(*schema.Provisioner).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + func TestResourceProvider_Apply(t *testing.T) { defer os.Remove("test_out") c := testConfig(t, map[string]interface{}{ diff --git a/builtin/provisioners/remote-exec/resource_provisioner_test.go b/builtin/provisioners/remote-exec/resource_provisioner_test.go index 69e5e9cdfee5..80bc669165bc 100644 --- a/builtin/provisioners/remote-exec/resource_provisioner_test.go +++ b/builtin/provisioners/remote-exec/resource_provisioner_test.go @@ -14,6 +14,16 @@ import ( "github.com/hashicorp/terraform/terraform" ) +func TestResourceProvisioner_impl(t *testing.T) { + var _ terraform.ResourceProvisioner = Provisioner() +} + +func TestProvisioner(t *testing.T) { + if err := Provisioner().(*schema.Provisioner).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + func TestResourceProvider_Validate_good(t *testing.T) { c := testConfig(t, map[string]interface{}{ "inline": "echo foo", diff --git a/helper/schema/provisioner.go b/helper/schema/provisioner.go index 6ac3fc1bf34b..4700bb5e6d41 100644 --- a/helper/schema/provisioner.go +++ b/helper/schema/provisioner.go @@ -41,6 +41,11 @@ type Provisioner struct { // information. ApplyFunc func(ctx context.Context) error + // ValidateFunc is the function for extended validation. This is optional. + // It is given a resource data. + // Should be provided when Scheme is not enough. + ValidateFunc func(*ResourceData) ([]string, []error) + stopCtx context.Context stopCtxCancel context.CancelFunc stopOnce sync.Once @@ -117,8 +122,30 @@ func (p *Provisioner) Stop() error { return nil } -func (p *Provisioner) Validate(c *terraform.ResourceConfig) ([]string, []error) { - return schemaMap(p.Schema).Validate(c) +func (p *Provisioner) Validate(config *terraform.ResourceConfig) ([]string, []error) { + if err := p.InternalValidate(); err != nil { + return nil, []error{fmt.Errorf( + "Internal validation of the provisioner failed! This is always a bug\n"+ + "with the provisioner itself, and not a user issue. Please report\n"+ + "this bug:\n\n%s", err)} + } + w := []string{} + e := []error{} + if p.Schema != nil { + w2, e2 := schemaMap(p.Schema).Validate(config) + w = append(w, w2...) + e = append(e, e2...) + } + if p.ValidateFunc != nil { + data := &ResourceData{ + schema: p.Schema, + config: config, + } + w2, e2 := p.ValidateFunc(data) + w = append(w, w2...) + e = append(e, e2...) + } + return w, e } // Apply implementation of terraform.ResourceProvisioner interface. diff --git a/helper/schema/provisioner_test.go b/helper/schema/provisioner_test.go index d8448acef9e8..585586b7df53 100644 --- a/helper/schema/provisioner_test.go +++ b/helper/schema/provisioner_test.go @@ -3,6 +3,7 @@ package schema import ( "context" "fmt" + "reflect" "testing" "time" @@ -14,13 +15,35 @@ func TestProvisioner_impl(t *testing.T) { var _ terraform.ResourceProvisioner = new(Provisioner) } +func noopApply(ctx context.Context) error { + return nil +} + func TestProvisionerValidate(t *testing.T) { cases := []struct { Name string P *Provisioner Config map[string]interface{} Err bool + Warns []string }{ + { + Name: "No ApplyFunc", + P: &Provisioner{}, + Config: nil, + Err: true, + }, + { + Name: "Incorrect schema", + P: &Provisioner{ + Schema: map[string]*Schema{ + "foo": {}, + }, + ApplyFunc: noopApply, + }, + Config: nil, + Err: true, + }, { "Basic required field", &Provisioner{ @@ -30,9 +53,11 @@ func TestProvisionerValidate(t *testing.T) { Type: TypeString, }, }, + ApplyFunc: noopApply, }, nil, true, + nil, }, { @@ -44,11 +69,57 @@ func TestProvisionerValidate(t *testing.T) { Type: TypeString, }, }, + ApplyFunc: noopApply, }, map[string]interface{}{ "foo": "bar", }, false, + nil, + }, + { + Name: "Warning from property validation", + P: &Provisioner{ + Schema: map[string]*Schema{ + "foo": { + Type: TypeString, + Optional: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + ws = append(ws, "Simple warning from property validation") + return + }, + }, + }, + ApplyFunc: noopApply, + }, + Config: map[string]interface{}{ + "foo": "", + }, + Err: false, + Warns: []string{"Simple warning from property validation"}, + }, + { + Name: "No schema", + P: &Provisioner{ + Schema: nil, + ApplyFunc: noopApply, + }, + Config: nil, + Err: false, + }, + { + Name: "Warning from provisioner ValidateFunc", + P: &Provisioner{ + Schema: nil, + ApplyFunc: noopApply, + ValidateFunc: func(*ResourceData) (ws []string, errors []error) { + ws = append(ws, "Simple warning from provisioner ValidateFunc") + return + }, + }, + Config: nil, + Err: false, + Warns: []string{"Simple warning from provisioner ValidateFunc"}, }, } @@ -59,9 +130,12 @@ func TestProvisionerValidate(t *testing.T) { t.Fatalf("err: %s", err) } - _, es := tc.P.Validate(terraform.NewResourceConfig(c)) + ws, es := tc.P.Validate(terraform.NewResourceConfig(c)) if len(es) > 0 != tc.Err { - t.Fatalf("%d: %#v", i, es) + t.Fatalf("%d: %#v %s", i, es, es) + } + if (tc.Warns != nil || len(ws) != 0) && !reflect.DeepEqual(ws, tc.Warns) { + t.Fatalf("%d: warnings mismatch, actual: %#v", i, ws) } }) } diff --git a/helper/schema/testing.go b/helper/schema/testing.go index 9765bdbc6dcf..bbc7b0cb86f5 100644 --- a/helper/schema/testing.go +++ b/helper/schema/testing.go @@ -28,3 +28,10 @@ func TestResourceDataRaw( return result } + +func TestResourceDataConfig(schema map[string]*Schema, config *terraform.ResourceConfig) *ResourceData { + return &ResourceData{ + schema: schema, + config: config, + } +} From 9c6f6c6c30f05b76cda81a2058d762788ce9ab07 Mon Sep 17 00:00:00 2001 From: Vladislav Rassokhin Date: Tue, 29 Nov 2016 23:03:21 +0300 Subject: [PATCH 2/2] Regenerate plugin list since provisioners were changed in previous commits --- command/internal_plugin_list.go | 12 ++---------- scripts/generate-plugins.go | 19 ++++--------------- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/command/internal_plugin_list.go b/command/internal_plugin_list.go index 2f48908c7d43..296bc7099ee0 100644 --- a/command/internal_plugin_list.go +++ b/command/internal_plugin_list.go @@ -70,15 +70,12 @@ import ( vaultprovider "github.com/hashicorp/terraform/builtin/providers/vault" vcdprovider "github.com/hashicorp/terraform/builtin/providers/vcd" vsphereprovider "github.com/hashicorp/terraform/builtin/providers/vsphere" + chefprovisioner "github.com/hashicorp/terraform/builtin/provisioners/chef" fileprovisioner "github.com/hashicorp/terraform/builtin/provisioners/file" localexecprovisioner "github.com/hashicorp/terraform/builtin/provisioners/local-exec" remoteexecprovisioner "github.com/hashicorp/terraform/builtin/provisioners/remote-exec" "github.com/hashicorp/terraform/plugin" - "github.com/hashicorp/terraform/terraform" - - // Legacy, will remove once it conforms with new structure - chefprovisioner "github.com/hashicorp/terraform/builtin/provisioners/chef" ) var InternalProviders = map[string]plugin.ProviderFunc{ @@ -149,13 +146,8 @@ var InternalProviders = map[string]plugin.ProviderFunc{ } var InternalProvisioners = map[string]plugin.ProvisionerFunc{ + "chef": chefprovisioner.Provisioner, "file": fileprovisioner.Provisioner, "local-exec": localexecprovisioner.Provisioner, "remote-exec": remoteexecprovisioner.Provisioner, } - -func init() { - // Legacy provisioners that don't match our heuristics for auto-finding - // built-in provisioners. - InternalProvisioners["chef"] = func() terraform.ResourceProvisioner { return new(chefprovisioner.ResourceProvisioner) } -} diff --git a/scripts/generate-plugins.go b/scripts/generate-plugins.go index 07bf33be7770..3bf257c2681f 100644 --- a/scripts/generate-plugins.go +++ b/scripts/generate-plugins.go @@ -82,12 +82,11 @@ func makeProviderMap(items []plugin) string { // makeProvisionerMap creates a map of provisioners like this: // -// "file": func() terraform.ResourceProvisioner { return new(file.ResourceProvisioner) }, -// "local-exec": func() terraform.ResourceProvisioner { return new(localexec.ResourceProvisioner) }, -// "remote-exec": func() terraform.ResourceProvisioner { return new(remoteexec.ResourceProvisioner) }, +// "chef": chefprovisioner.Provisioner, +// "file": fileprovisioner.Provisioner, +// "local-exec": localexecprovisioner.Provisioner, +// "remote-exec": remoteexecprovisioner.Provisioner, // -// This is more verbose than the Provider case because there is no corresponding -// Provisioner function. func makeProvisionerMap(items []plugin) string { output := "" for _, item := range items { @@ -269,10 +268,6 @@ package command import ( IMPORTS "github.com/hashicorp/terraform/plugin" - "github.com/hashicorp/terraform/terraform" - - // Legacy, will remove once it conforms with new structure - chefprovisioner "github.com/hashicorp/terraform/builtin/provisioners/chef" ) var InternalProviders = map[string]plugin.ProviderFunc{ @@ -283,10 +278,4 @@ var InternalProvisioners = map[string]plugin.ProvisionerFunc{ PROVISIONERS } -func init() { - // Legacy provisioners that don't match our heuristics for auto-finding - // built-in provisioners. - InternalProvisioners["chef"] = func() terraform.ResourceProvisioner { return new(chefprovisioner.ResourceProvisioner) } -} - `