diff --git a/builtin/bins/provisioner-chef/main.go b/builtin/bins/provisioner-chef/main.go index a12c65cf7..6e81fde49 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 ebfe72979..6d87cfe8a 100644 --- a/builtin/provisioners/chef/linux_provisioner.go +++ b/builtin/provisioners/chef/linux_provisioner.go @@ -14,10 +14,7 @@ const ( installURL = "https://www.chef.io/chef/install.sh" ) -func (p *Provisioner) linuxInstallChefClient( - o terraform.UIOutput, - comm communicator.Communicator) error { - +func (p *provisioner) linuxInstallChefClient(o terraform.UIOutput, comm communicator.Communicator) error { // Build up the command prefix prefix := "" if p.HTTPProxy != "" { @@ -26,7 +23,7 @@ func (p *Provisioner) linuxInstallChefClient( if p.HTTPSProxy != "" { prefix += fmt.Sprintf("https_proxy='%s' ", p.HTTPSProxy) } - if p.NOProxy != nil { + if len(p.NOProxy) > 0 { prefix += fmt.Sprintf("no_proxy='%s' ", strings.Join(p.NOProxy, ",")) } @@ -46,9 +43,7 @@ func (p *Provisioner) linuxInstallChefClient( return p.runCommand(o, comm, fmt.Sprintf("%srm -f install.sh", prefix)) } -func (p *Provisioner) linuxCreateConfigFiles( - o terraform.UIOutput, - comm communicator.Communicator) error { +func (p *provisioner) linuxCreateConfigFiles(o terraform.UIOutput, comm communicator.Communicator) error { // Make sure the config directory exists if err := p.runCommand(o, comm, "mkdir -p "+linuxConfDir); err != nil { return err diff --git a/builtin/provisioners/chef/linux_provisioner_test.go b/builtin/provisioners/chef/linux_provisioner_test.go index 7d2339e04..578bb69ff 100644 --- a/builtin/provisioners/chef/linux_provisioner_test.go +++ b/builtin/provisioners/chef/linux_provisioner_test.go @@ -6,22 +6,23 @@ import ( "testing" "github.com/hashicorp/terraform/communicator" + "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" ) func TestResourceProvider_linuxInstallChefClient(t *testing.T) { cases := map[string]struct { - Config *terraform.ResourceConfig + Config map[string]interface{} Commands map[string]bool }{ "Sudo": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "node_name": "nodename1", "run_list": []interface{}{"cookbook::recipe"}, "server_url": "https://chef.local", "user_name": "bob", "user_key": "USER-KEY", - }), + }, Commands: map[string]bool{ "sudo curl -LO https://www.chef.io/chef/install.sh": true, @@ -31,7 +32,7 @@ func TestResourceProvider_linuxInstallChefClient(t *testing.T) { }, "NoSudo": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "node_name": "nodename1", "prevent_sudo": true, "run_list": []interface{}{"cookbook::recipe"}, @@ -39,7 +40,7 @@ func TestResourceProvider_linuxInstallChefClient(t *testing.T) { "server_url": "https://chef.local", "user_name": "bob", "user_key": "USER-KEY", - }), + }, Commands: map[string]bool{ "curl -LO https://www.chef.io/chef/install.sh": true, @@ -49,7 +50,7 @@ func TestResourceProvider_linuxInstallChefClient(t *testing.T) { }, "HTTPProxy": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "http_proxy": "http://proxy.local", "node_name": "nodename1", "prevent_sudo": true, @@ -57,7 +58,7 @@ func TestResourceProvider_linuxInstallChefClient(t *testing.T) { "server_url": "https://chef.local", "user_name": "bob", "user_key": "USER-KEY", - }), + }, Commands: map[string]bool{ "http_proxy='http://proxy.local' curl -LO https://www.chef.io/chef/install.sh": true, @@ -67,7 +68,7 @@ func TestResourceProvider_linuxInstallChefClient(t *testing.T) { }, "HTTPSProxy": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "https_proxy": "https://proxy.local", "node_name": "nodename1", "prevent_sudo": true, @@ -75,7 +76,7 @@ func TestResourceProvider_linuxInstallChefClient(t *testing.T) { "server_url": "https://chef.local", "user_name": "bob", "user_key": "USER-KEY", - }), + }, Commands: map[string]bool{ "https_proxy='https://proxy.local' curl -LO https://www.chef.io/chef/install.sh": true, @@ -85,7 +86,7 @@ func TestResourceProvider_linuxInstallChefClient(t *testing.T) { }, "NoProxy": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "http_proxy": "http://proxy.local", "no_proxy": []interface{}{"http://local.local", "http://local.org"}, "node_name": "nodename1", @@ -94,7 +95,7 @@ func TestResourceProvider_linuxInstallChefClient(t *testing.T) { "server_url": "https://chef.local", "user_name": "bob", "user_key": "USER-KEY", - }), + }, Commands: map[string]bool{ "http_proxy='http://proxy.local' no_proxy='http://local.local,http://local.org' " + @@ -107,7 +108,7 @@ func TestResourceProvider_linuxInstallChefClient(t *testing.T) { }, "Version": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "node_name": "nodename1", "prevent_sudo": true, "run_list": []interface{}{"cookbook::recipe"}, @@ -115,7 +116,7 @@ func TestResourceProvider_linuxInstallChefClient(t *testing.T) { "user_name": "bob", "user_key": "USER-KEY", "version": "11.18.6", - }), + }, Commands: map[string]bool{ "curl -LO https://www.chef.io/chef/install.sh": true, @@ -125,14 +126,15 @@ 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( + schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config), + ) if err != nil { t.Fatalf("Error: %v", err) } @@ -148,12 +150,12 @@ func TestResourceProvider_linuxInstallChefClient(t *testing.T) { func TestResourceProvider_linuxCreateConfigFiles(t *testing.T) { cases := map[string]struct { - Config *terraform.ResourceConfig + Config map[string]interface{} Commands map[string]bool Uploads map[string]string }{ "Sudo": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "ohai_hints": []interface{}{"test-fixtures/ohaihint.json"}, "node_name": "nodename1", "run_list": []interface{}{"cookbook::recipe"}, @@ -161,7 +163,7 @@ func TestResourceProvider_linuxCreateConfigFiles(t *testing.T) { "server_url": "https://chef.local", "user_name": "bob", "user_key": "USER-KEY", - }), + }, Commands: map[string]bool{ "sudo mkdir -p " + linuxConfDir: true, @@ -188,7 +190,7 @@ func TestResourceProvider_linuxCreateConfigFiles(t *testing.T) { }, "NoSudo": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "node_name": "nodename1", "prevent_sudo": true, "run_list": []interface{}{"cookbook::recipe"}, @@ -196,7 +198,7 @@ func TestResourceProvider_linuxCreateConfigFiles(t *testing.T) { "server_url": "https://chef.local", "user_name": "bob", "user_key": "USER-KEY", - }), + }, Commands: map[string]bool{ "mkdir -p " + linuxConfDir: true, @@ -211,7 +213,7 @@ func TestResourceProvider_linuxCreateConfigFiles(t *testing.T) { }, "Proxy": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "http_proxy": "http://proxy.local", "https_proxy": "https://proxy.local", "no_proxy": []interface{}{"http://local.local", "https://local.local"}, @@ -223,7 +225,7 @@ func TestResourceProvider_linuxCreateConfigFiles(t *testing.T) { "ssl_verify_mode": "verify_none", "user_name": "bob", "user_key": "USER-KEY", - }), + }, Commands: map[string]bool{ "mkdir -p " + linuxConfDir: true, @@ -238,7 +240,7 @@ func TestResourceProvider_linuxCreateConfigFiles(t *testing.T) { }, "Attributes JSON": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "attributes_json": `{"key1":{"subkey1":{"subkey2a":["val1","val2","val3"],` + `"subkey2b":{"subkey3":"value3"}}},"key2":"value2"}`, "node_name": "nodename1", @@ -248,7 +250,7 @@ func TestResourceProvider_linuxCreateConfigFiles(t *testing.T) { "server_url": "https://chef.local", "user_name": "bob", "user_key": "USER-KEY", - }), + }, Commands: map[string]bool{ "mkdir -p " + linuxConfDir: true, @@ -264,7 +266,6 @@ func TestResourceProvider_linuxCreateConfigFiles(t *testing.T) { }, } - r := new(ResourceProvisioner) o := new(terraform.MockUIOutput) c := new(communicator.MockCommunicator) @@ -272,7 +273,9 @@ func TestResourceProvider_linuxCreateConfigFiles(t *testing.T) { c.Commands = tc.Commands c.Uploads = tc.Uploads - p, err := r.decodeConfig(tc.Config) + p, err := decodeConfig( + schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, 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 a5482ea38..7df7e93ca 100644 --- a/builtin/provisioners/chef/resource_provisioner.go +++ b/builtin/provisioners/chef/resource_provisioner.go @@ -2,6 +2,7 @@ package chef import ( "bytes" + "context" "encoding/json" "errors" "fmt" @@ -17,10 +18,10 @@ import ( "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,81 +82,196 @@ 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"` +type provisionFn func(terraform.UIOutput, communicator.Communicator) error - attributes map[string]interface{} - vaults map[string][]string +type provisioner struct { + Attributes map[string]interface{} + 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 + Vaults map[string][]string + Version string cleanupUserKeyCmd string - createConfigFiles func(terraform.UIOutput, communicator.Communicator) error - installChefClient func(terraform.UIOutput, communicator.Communicator) error - fetchChefCertificates func(terraform.UIOutput, communicator.Communicator) error - generateClientKey func(terraform.UIOutput, communicator.Communicator) error - configureVaults func(terraform.UIOutput, communicator.Communicator) error - runChefClient func(terraform.UIOutput, communicator.Communicator) error + createConfigFiles provisionFn + installChefClient provisionFn + fetchChefCertificates provisionFn + generateClientKey provisionFn + configureVaults provisionFn + runChefClient provisionFn useSudo bool - - // Deprecated Fields - ValidationClientName string `mapstructure:"validation_client_name"` - ValidationKey string `mapstructure:"validation_key"` } -// ResourceProvisioner represents a generic chef provisioner -type ResourceProvisioner struct{} +// Provisioner returns a Chef provisioner +func Provisioner() terraform.ResourceProvisioner { + return &schema.Provisioner{ + Schema: map[string]*schema.Schema{ + "node_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "server_url": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "user_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "user_key": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, -func (r *ResourceProvisioner) Stop() error { - // Noop for now. TODO in the future. - return nil + "attributes_json": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "client_options": &schema.Schema{ + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + "disable_reporting": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "environment": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: defaultEnv, + }, + "fetch_chef_certificates": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "log_to_file": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "use_policyfile": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "policy_group": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "policy_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "http_proxy": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "https_proxy": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "no_proxy": &schema.Schema{ + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + "named_run_list": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "ohai_hints": &schema.Schema{ + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + "os_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "recreate_client": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "prevent_sudo": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "run_list": &schema.Schema{ + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + "secret_key": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "skip_install": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "skip_register": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "ssl_verify_mode": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "vault_json": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "version": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + + ApplyFunc: applyFn, + ValidateFunc: validateFn, + } } -// Apply executes the file provisioner -func (r *ResourceProvisioner) Apply( - o terraform.UIOutput, - s *terraform.InstanceState, - c *terraform.ResourceConfig) error { +// TODO: Support context cancelling (Provisioner Stop) +func applyFn(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"] { + switch t := d.State().Ephemeral.ConnInfo["type"]; 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 +285,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,15 +300,14 @@ func (r *ResourceProvisioner) Apply( } // Get a new communicator - comm, err := communicator.New(s) + comm, err := communicator.New(d.State()) if err != nil { return err } // Wait and retry until we establish the connection err = retryFunc(comm.Timeout(), func() error { - err := comm.Connect(o) - return err + return comm.Connect(o) }) if err != nil { return err @@ -234,7 +349,7 @@ func (r *ResourceProvisioner) Apply( } } - if p.VaultJSON != "" { + if p.Vaults != nil { o.Output("Configure Chef vaults...") if err := p.configureVaults(o, comm); err != nil { return err @@ -253,152 +368,27 @@ func (r *ResourceProvisioner) Apply( return nil } -// 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 validateFn(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")) } if p.UsePolicyfile && p.PolicyGroup == "" { es = append(es, errors.New("Policyfile enabled but key not found: policy_group")) } - if p.UserName == "" && p.ValidationClientName == "" { - es = append(es, errors.New( - "One of user_name or the deprecated validation_client_name must be provided")) - } - if p.UserKey == "" && p.ValidationKey == "" { - 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")) - } - if p.VaultJSON != "" { - es = append(es, errors.New( - "Cannot configure chef vaults using the deprecated validation_key, please provide a user_key")) - } - } 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 - } - - // Make sure the supplied URL has a trailing slash - p.ServerURL = strings.TrimSuffix(p.ServerURL, "/") + "/" - - if p.Environment == "" { - p.Environment = defaultEnv - } - - for i, hint := range p.OhaiHints { - hintPath, err := homedir.Expand(hint) - if err != nil { - return nil, fmt.Errorf("Error expanding the path %s: %v", hint, err) - } - p.OhaiHints[i] = hintPath - } - - if p.UserName == "" && p.ValidationClientName != "" { - p.UserName = p.ValidationClientName - } - - if p.UserKey == "" && p.ValidationKey != "" { - p.UserKey = p.ValidationKey - } - - if attrs, ok := c.Config["attributes_json"].(string); ok && !c.IsComputed("attributes_json") { - var m map[string]interface{} - if err := json.Unmarshal([]byte(attrs), &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") { - var m map[string]interface{} - if err := json.Unmarshal([]byte(vaults), &m); err != nil { - return nil, fmt.Errorf("Error parsing vault_json: %v", err) - } - - v := make(map[string][]string) - for vault, items := range m { - switch items := items.(type) { - case []interface{}: - for _, item := range items { - if item, ok := item.(string); ok { - v[vault] = append(v[vault], item) - } - } - case interface{}: - if item, ok := items.(string); ok { - v[vault] = append(v[vault], item) - } - } - } - - p.vaults = v - } - - return p, nil -} - -func (p *Provisioner) deployConfigFiles( - o terraform.UIOutput, - comm communicator.Communicator, - confDir string) error { +func (p *provisioner) deployConfigFiles(o terraform.UIOutput, comm communicator.Communicator, confDir string) error { // Copy the user key to the new instance pk := strings.NewReader(p.UserKey) if err := comm.Upload(path.Join(confDir, p.UserName+".pem"), pk); err != nil { @@ -433,14 +423,14 @@ func (p *Provisioner) deployConfigFiles( } // Copy the client config to the new instance - if err := comm.Upload(path.Join(confDir, clienrb), &buf); err != nil { + if err = comm.Upload(path.Join(confDir, clienrb), &buf); err != nil { return fmt.Errorf("Uploading %s failed: %v", clienrb, err) } // Create a map with first boot settings fb := make(map[string]interface{}) - if p.attributes != nil { - fb = p.attributes + if p.Attributes != nil { + fb = p.Attributes } // Check if the run_list was also in the attributes and if so log a warning @@ -469,10 +459,7 @@ func (p *Provisioner) deployConfigFiles( return nil } -func (p *Provisioner) deployOhaiHints( - o terraform.UIOutput, - comm communicator.Communicator, - hintDir string) error { +func (p *provisioner) deployOhaiHints(o terraform.UIOutput, comm communicator.Communicator, hintDir string) error { for _, hint := range p.OhaiHints { // Open the hint file f, err := os.Open(hint) @@ -490,7 +477,7 @@ func (p *Provisioner) deployOhaiHints( return nil } -func (p *Provisioner) fetchChefCertificatesFunc( +func (p *provisioner) fetchChefCertificatesFunc( knifeCmd string, confDir string) func(terraform.UIOutput, communicator.Communicator) error { return func(o terraform.UIOutput, comm communicator.Communicator) error { @@ -501,10 +488,7 @@ func (p *Provisioner) fetchChefCertificatesFunc( } } -func (p *Provisioner) generateClientKeyFunc( - knifeCmd string, - confDir string, - noOutput string) func(terraform.UIOutput, communicator.Communicator) error { +func (p *provisioner) generateClientKeyFunc(knifeCmd string, confDir string, noOutput string) provisionFn { return func(o terraform.UIOutput, comm communicator.Communicator) error { options := fmt.Sprintf("-c %s -u %s --key %s", path.Join(confDir, clienrb), @@ -562,10 +546,7 @@ func (p *Provisioner) generateClientKeyFunc( } } -func (p *Provisioner) configureVaultsFunc( - gemCmd string, - knifeCmd string, - confDir string) func(terraform.UIOutput, communicator.Communicator) error { +func (p *provisioner) configureVaultsFunc(gemCmd string, knifeCmd string, confDir string) provisionFn { return func(o terraform.UIOutput, comm communicator.Communicator) error { if err := p.runCommand(o, comm, fmt.Sprintf("%s install chef-vault", gemCmd)); err != nil { return err @@ -577,7 +558,7 @@ func (p *Provisioner) configureVaultsFunc( path.Join(confDir, p.UserName+".pem"), ) - for vault, items := range p.vaults { + for vault, items := range p.Vaults { for _, item := range items { updateCmd := fmt.Sprintf("%s vault update %s %s -C %s -M client %s", knifeCmd, @@ -596,9 +577,7 @@ func (p *Provisioner) configureVaultsFunc( } } -func (p *Provisioner) runChefClientFunc( - chefCmd string, - confDir string) func(terraform.UIOutput, communicator.Communicator) error { +func (p *provisioner) runChefClientFunc(chefCmd string, confDir string) provisionFn { return func(o terraform.UIOutput, comm communicator.Communicator) error { fb := path.Join(confDir, firstBoot) var cmd string @@ -634,7 +613,7 @@ func (p *Provisioner) runChefClientFunc( } // Output implementation of terraform.UIOutput interface -func (p *Provisioner) Output(output string) { +func (p *provisioner) 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,10 +639,7 @@ func (p *Provisioner) Output(output string) { } // runCommand is used to run already prepared commands -func (p *Provisioner) runCommand( - o terraform.UIOutput, - comm communicator.Communicator, - command string) error { +func (p *provisioner) runCommand(o terraform.UIOutput, comm communicator.Communicator, command string) error { // Unless prevented, prefix the command with sudo if p.useSudo { command = "sudo " + command @@ -702,7 +678,7 @@ func (p *Provisioner) runCommand( return err } -func (p *Provisioner) copyOutput(o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) { +func (p *provisioner) copyOutput(o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) { defer close(doneCh) lr := linereader.New(r) for line := range lr.Ch { @@ -727,3 +703,98 @@ func retryFunc(timeout time.Duration, f func() error) error { } } } + +func decodeConfig(d *schema.ResourceData) (*provisioner, error) { + p := &provisioner{ + 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), + Version: d.Get("version").(string), + } + + // Make sure the supplied URL has a trailing slash + p.ServerURL = strings.TrimSuffix(p.ServerURL, "/") + "/" + + for i, hint := range p.OhaiHints { + hintPath, err := homedir.Expand(hint) + if err != nil { + return nil, fmt.Errorf("Error expanding the path %s: %v", hint, err) + } + p.OhaiHints[i] = hintPath + } + + if attrs, ok := d.GetOk("attributes_json"); ok { + var m map[string]interface{} + 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 := d.GetOk("vault_json"); ok { + var m map[string]interface{} + if err := json.Unmarshal([]byte(vaults.(string)), &m); err != nil { + return nil, fmt.Errorf("Error parsing vault_json: %v", err) + } + + v := make(map[string][]string) + for vault, items := range m { + switch items := items.(type) { + case []interface{}: + for _, item := range items { + if item, ok := item.(string); ok { + v[vault] = append(v[vault], item) + } + } + case interface{}: + if item, ok := items.(string); ok { + v[vault] = append(v[vault], item) + } + } + } + + p.Vaults = v + } + + return p, nil +} + +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 7c2dc3a2e..f9e9f33fe 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,8 +30,8 @@ func TestResourceProvider_Validate_good(t *testing.T) { "user_name": "bob", "user_key": "USER-KEY", }) - r := new(ResourceProvisioner) - warn, errs := r.Validate(c) + + warn, errs := Provisioner().Validate(c) if len(warn) > 0 { t.Fatalf("Warnings: %v", warn) } @@ -37,8 +44,8 @@ func TestResourceProvider_Validate_bad(t *testing.T) { c := testConfig(t, map[string]interface{}{ "invalid": "nope", }) - p := new(ResourceProvisioner) - warn, errs := p.Validate(c) + + warn, errs := Provisioner().Validate(c) if len(warn) > 0 { t.Fatalf("Warnings: %v", warn) } @@ -59,8 +66,8 @@ func TestResourceProvider_Validate_computedValues(t *testing.T) { "user_key": "USER-KEY", "attributes_json": config.UnknownVariableValue, }) - r := new(ResourceProvisioner) - warn, errs := r.Validate(c) + + warn, errs := Provisioner().Validate(c) if len(warn) > 0 { t.Fatalf("Warnings: %v", warn) } @@ -69,30 +76,21 @@ func TestResourceProvider_Validate_computedValues(t *testing.T) { } } -func testConfig(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig { - r, err := config.NewRawConfig(c) - if err != nil { - t.Fatalf("bad: %s", err) - } - - return terraform.NewResourceConfig(r) -} - func TestResourceProvider_runChefClient(t *testing.T) { cases := map[string]struct { - Config *terraform.ResourceConfig + Config map[string]interface{} ChefCmd string ConfDir string Commands map[string]bool }{ "Sudo": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "node_name": "nodename1", "run_list": []interface{}{"cookbook::recipe"}, "server_url": "https://chef.local", "user_name": "bob", "user_key": "USER-KEY", - }), + }, ChefCmd: linuxChefCmd, @@ -106,14 +104,14 @@ func TestResourceProvider_runChefClient(t *testing.T) { }, "NoSudo": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "node_name": "nodename1", "prevent_sudo": true, "run_list": []interface{}{"cookbook::recipe"}, "server_url": "https://chef.local", "user_name": "bob", "user_key": "USER-KEY", - }), + }, ChefCmd: linuxChefCmd, @@ -127,7 +125,7 @@ func TestResourceProvider_runChefClient(t *testing.T) { }, "Environment": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "environment": "production", "node_name": "nodename1", "prevent_sudo": true, @@ -135,7 +133,7 @@ func TestResourceProvider_runChefClient(t *testing.T) { "server_url": "https://chef.local", "user_name": "bob", "user_key": "USER-KEY", - }), + }, ChefCmd: windowsChefCmd, @@ -149,14 +147,15 @@ 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( + schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config), + ) if err != nil { t.Fatalf("Error: %v", err) } @@ -173,20 +172,20 @@ func TestResourceProvider_runChefClient(t *testing.T) { func TestResourceProvider_fetchChefCertificates(t *testing.T) { cases := map[string]struct { - Config *terraform.ResourceConfig + Config map[string]interface{} KnifeCmd string ConfDir string Commands map[string]bool }{ "Sudo": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "fetch_chef_certificates": true, "node_name": "nodename1", "run_list": []interface{}{"cookbook::recipe"}, "server_url": "https://chef.local", "user_name": "bob", "user_key": "USER-KEY", - }), + }, KnifeCmd: linuxKnifeCmd, @@ -200,7 +199,7 @@ func TestResourceProvider_fetchChefCertificates(t *testing.T) { }, "NoSudo": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "fetch_chef_certificates": true, "node_name": "nodename1", "prevent_sudo": true, @@ -208,7 +207,7 @@ func TestResourceProvider_fetchChefCertificates(t *testing.T) { "server_url": "https://chef.local", "user_name": "bob", "user_key": "USER-KEY", - }), + }, KnifeCmd: windowsKnifeCmd, @@ -222,14 +221,15 @@ 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( + schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config), + ) if err != nil { t.Fatalf("Error: %v", err) } @@ -246,14 +246,14 @@ func TestResourceProvider_fetchChefCertificates(t *testing.T) { func TestResourceProvider_configureVaults(t *testing.T) { cases := map[string]struct { - Config *terraform.ResourceConfig + Config map[string]interface{} GemCmd string KnifeCmd string ConfDir string Commands map[string]bool }{ "Linux Vault string": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "node_name": "nodename1", "prevent_sudo": true, "run_list": []interface{}{"cookbook::recipe"}, @@ -261,7 +261,7 @@ func TestResourceProvider_configureVaults(t *testing.T) { "user_name": "bob", "user_key": "USER-KEY", "vault_json": `{"vault1": "item1"}`, - }), + }, GemCmd: linuxGemCmd, KnifeCmd: linuxKnifeCmd, @@ -275,7 +275,7 @@ func TestResourceProvider_configureVaults(t *testing.T) { }, "Linux Vault []string": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "fetch_chef_certificates": true, "node_name": "nodename1", "prevent_sudo": true, @@ -284,7 +284,7 @@ func TestResourceProvider_configureVaults(t *testing.T) { "user_name": "bob", "user_key": "USER-KEY", "vault_json": `{"vault1": ["item1", "item2"]}`, - }), + }, GemCmd: linuxGemCmd, KnifeCmd: linuxKnifeCmd, @@ -300,7 +300,7 @@ func TestResourceProvider_configureVaults(t *testing.T) { }, "Windows Vault string": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "node_name": "nodename1", "prevent_sudo": true, "run_list": []interface{}{"cookbook::recipe"}, @@ -308,7 +308,7 @@ func TestResourceProvider_configureVaults(t *testing.T) { "user_name": "bob", "user_key": "USER-KEY", "vault_json": `{"vault1": "item1"}`, - }), + }, GemCmd: windowsGemCmd, KnifeCmd: windowsKnifeCmd, @@ -322,7 +322,7 @@ func TestResourceProvider_configureVaults(t *testing.T) { }, "Windows Vault []string": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "fetch_chef_certificates": true, "node_name": "nodename1", "prevent_sudo": true, @@ -331,7 +331,7 @@ func TestResourceProvider_configureVaults(t *testing.T) { "user_name": "bob", "user_key": "USER-KEY", "vault_json": `{"vault1": ["item1", "item2"]}`, - }), + }, GemCmd: windowsGemCmd, KnifeCmd: windowsKnifeCmd, @@ -347,14 +347,15 @@ 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( + schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config), + ) if err != nil { t.Fatalf("Error: %v", err) } @@ -368,3 +369,12 @@ func TestResourceProvider_configureVaults(t *testing.T) { } } } + +func testConfig(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig { + r, err := config.NewRawConfig(c) + if err != nil { + t.Fatalf("bad: %s", err) + } + + return terraform.NewResourceConfig(r) +} diff --git a/builtin/provisioners/chef/windows_provisioner.go b/builtin/provisioners/chef/windows_provisioner.go index 953734021..8713affcd 100644 --- a/builtin/provisioners/chef/windows_provisioner.go +++ b/builtin/provisioners/chef/windows_provisioner.go @@ -46,9 +46,7 @@ Write-Host 'Installing Chef Client...' Start-Process -FilePath msiexec -ArgumentList /qn, /i, $dest -Wait ` -func (p *Provisioner) windowsInstallChefClient( - o terraform.UIOutput, - comm communicator.Communicator) error { +func (p *provisioner) windowsInstallChefClient(o terraform.UIOutput, comm communicator.Communicator) error { script := path.Join(path.Dir(comm.ScriptPath()), "ChefClient.ps1") content := fmt.Sprintf(installScript, p.Version, p.HTTPProxy, strings.Join(p.NOProxy, ",")) @@ -62,9 +60,7 @@ func (p *Provisioner) windowsInstallChefClient( return p.runCommand(o, comm, installCmd) } -func (p *Provisioner) windowsCreateConfigFiles( - o terraform.UIOutput, - comm communicator.Communicator) error { +func (p *provisioner) windowsCreateConfigFiles(o terraform.UIOutput, comm communicator.Communicator) error { // Make sure the config directory exists cmd := fmt.Sprintf("cmd /c if not exist %q mkdir %q", windowsConfDir, windowsConfDir) if err := p.runCommand(o, comm, cmd); err != nil { diff --git a/builtin/provisioners/chef/windows_provisioner_test.go b/builtin/provisioners/chef/windows_provisioner_test.go index 659953d97..566f8c457 100644 --- a/builtin/provisioners/chef/windows_provisioner_test.go +++ b/builtin/provisioners/chef/windows_provisioner_test.go @@ -6,23 +6,24 @@ import ( "testing" "github.com/hashicorp/terraform/communicator" + "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" ) func TestResourceProvider_windowsInstallChefClient(t *testing.T) { cases := map[string]struct { - Config *terraform.ResourceConfig + Config map[string]interface{} Commands map[string]bool UploadScripts map[string]string }{ "Default": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "node_name": "nodename1", "run_list": []interface{}{"cookbook::recipe"}, "server_url": "https://chef.local", "user_name": "bob", "user_key": "USER-KEY", - }), + }, Commands: map[string]bool{ "powershell -NoProfile -ExecutionPolicy Bypass -File ChefClient.ps1": true, @@ -34,7 +35,7 @@ func TestResourceProvider_windowsInstallChefClient(t *testing.T) { }, "Proxy": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "http_proxy": "http://proxy.local", "no_proxy": []interface{}{"http://local.local", "http://local.org"}, "node_name": "nodename1", @@ -42,7 +43,7 @@ func TestResourceProvider_windowsInstallChefClient(t *testing.T) { "server_url": "https://chef.local", "user_name": "bob", "user_key": "USER-KEY", - }), + }, Commands: map[string]bool{ "powershell -NoProfile -ExecutionPolicy Bypass -File ChefClient.ps1": true, @@ -54,14 +55,14 @@ func TestResourceProvider_windowsInstallChefClient(t *testing.T) { }, "Version": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "node_name": "nodename1", "run_list": []interface{}{"cookbook::recipe"}, "server_url": "https://chef.local", "user_name": "bob", "user_key": "USER-KEY", "version": "11.18.6", - }), + }, Commands: map[string]bool{ "powershell -NoProfile -ExecutionPolicy Bypass -File ChefClient.ps1": true, @@ -73,7 +74,6 @@ func TestResourceProvider_windowsInstallChefClient(t *testing.T) { }, } - r := new(ResourceProvisioner) o := new(terraform.MockUIOutput) c := new(communicator.MockCommunicator) @@ -81,7 +81,9 @@ func TestResourceProvider_windowsInstallChefClient(t *testing.T) { c.Commands = tc.Commands c.UploadScripts = tc.UploadScripts - p, err := r.decodeConfig(tc.Config) + p, err := decodeConfig( + schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config), + ) if err != nil { t.Fatalf("Error: %v", err) } @@ -97,12 +99,12 @@ func TestResourceProvider_windowsInstallChefClient(t *testing.T) { func TestResourceProvider_windowsCreateConfigFiles(t *testing.T) { cases := map[string]struct { - Config *terraform.ResourceConfig + Config map[string]interface{} Commands map[string]bool Uploads map[string]string }{ "Default": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "ohai_hints": []interface{}{"test-fixtures/ohaihint.json"}, "node_name": "nodename1", "run_list": []interface{}{"cookbook::recipe"}, @@ -110,7 +112,7 @@ func TestResourceProvider_windowsCreateConfigFiles(t *testing.T) { "server_url": "https://chef.local", "user_name": "bob", "user_key": "USER-KEY", - }), + }, Commands: map[string]bool{ fmt.Sprintf("cmd /c if not exist %q mkdir %q", windowsConfDir, windowsConfDir): true, @@ -129,7 +131,7 @@ func TestResourceProvider_windowsCreateConfigFiles(t *testing.T) { }, "Proxy": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "http_proxy": "http://proxy.local", "https_proxy": "https://proxy.local", "no_proxy": []interface{}{"http://local.local", "https://local.local"}, @@ -140,7 +142,7 @@ func TestResourceProvider_windowsCreateConfigFiles(t *testing.T) { "ssl_verify_mode": "verify_none", "user_name": "bob", "user_key": "USER-KEY", - }), + }, Commands: map[string]bool{ fmt.Sprintf("cmd /c if not exist %q mkdir %q", windowsConfDir, windowsConfDir): true, @@ -155,7 +157,7 @@ func TestResourceProvider_windowsCreateConfigFiles(t *testing.T) { }, "Attributes JSON": { - Config: testConfig(t, map[string]interface{}{ + Config: map[string]interface{}{ "attributes_json": `{"key1":{"subkey1":{"subkey2a":["val1","val2","val3"],` + `"subkey2b":{"subkey3":"value3"}}},"key2":"value2"}`, "node_name": "nodename1", @@ -164,7 +166,7 @@ func TestResourceProvider_windowsCreateConfigFiles(t *testing.T) { "server_url": "https://chef.local", "user_name": "bob", "user_key": "USER-KEY", - }), + }, Commands: map[string]bool{ fmt.Sprintf("cmd /c if not exist %q mkdir %q", windowsConfDir, windowsConfDir): true, @@ -180,7 +182,6 @@ func TestResourceProvider_windowsCreateConfigFiles(t *testing.T) { }, } - r := new(ResourceProvisioner) o := new(terraform.MockUIOutput) c := new(communicator.MockCommunicator) @@ -188,7 +189,9 @@ func TestResourceProvider_windowsCreateConfigFiles(t *testing.T) { c.Commands = tc.Commands c.Uploads = tc.Uploads - p, err := r.decodeConfig(tc.Config) + p, err := decodeConfig( + schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, 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 001e78af5..2c9c7298a 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: validateFn, } } @@ -77,6 +78,20 @@ func applyFn(ctx context.Context) error { } } +func validateFn(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 d57dbbaa5..2dacab35e 100644 --- a/builtin/provisioners/file/resource_provisioner_test.go +++ b/builtin/provisioners/file/resource_provisioner_test.go @@ -4,16 +4,27 @@ 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", "destination": "/tmp/bar", }) - p := Provisioner() - warn, errs := p.Validate(c) + + warn, errs := Provisioner().Validate(c) if len(warn) > 0 { t.Fatalf("Warnings: %v", warn) } @@ -27,8 +38,8 @@ func TestResourceProvider_Validate_good_content(t *testing.T) { "content": "value to copy", "destination": "/tmp/bar", }) - p := Provisioner() - warn, errs := p.Validate(c) + + warn, errs := Provisioner().Validate(c) if len(warn) > 0 { t.Fatalf("Warnings: %v", warn) } @@ -41,8 +52,22 @@ func TestResourceProvider_Validate_bad_not_destination(t *testing.T) { c := testConfig(t, map[string]interface{}{ "source": "nope", }) - p := Provisioner() - warn, errs := p.Validate(c) + + warn, errs := Provisioner().Validate(c) + if len(warn) > 0 { + t.Fatalf("Warnings: %v", warn) + } + if len(errs) == 0 { + t.Fatalf("Should have errors") + } +} + +func TestResourceProvider_Validate_bad_no_source(t *testing.T) { + c := testConfig(t, map[string]interface{}{ + "destination": "/tmp/bar", + }) + + warn, errs := Provisioner().Validate(c) if len(warn) > 0 { t.Fatalf("Warnings: %v", warn) } @@ -57,8 +82,8 @@ func TestResourceProvider_Validate_bad_to_many_src(t *testing.T) { "content": "value to copy", "destination": "/tmp/bar", }) - p := Provisioner() - warn, errs := p.Validate(c) + + warn, errs := Provisioner().Validate(c) if len(warn) > 0 { t.Fatalf("Warnings: %v", warn) } @@ -67,12 +92,11 @@ func TestResourceProvider_Validate_bad_to_many_src(t *testing.T) { } } -func testConfig( - t *testing.T, - c map[string]interface{}) *terraform.ResourceConfig { +func testConfig(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig { r, err := config.NewRawConfig(c) if err != nil { t.Fatalf("bad: %s", err) } + return terraform.NewResourceConfig(r) } diff --git a/builtin/provisioners/local-exec/resource_provisioner_test.go b/builtin/provisioners/local-exec/resource_provisioner_test.go index 2dff4a85d..2fa17efc5 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{}{ @@ -19,6 +30,7 @@ func TestResourceProvider_Apply(t *testing.T) { output := new(terraform.MockUIOutput) p := Provisioner() + if err := p.Apply(output, nil, c); err != nil { t.Fatalf("err: %v", err) } @@ -73,8 +85,8 @@ func TestResourceProvider_Validate_good(t *testing.T) { c := testConfig(t, map[string]interface{}{ "command": "echo foo", }) - p := Provisioner() - warn, errs := p.Validate(c) + + warn, errs := Provisioner().Validate(c) if len(warn) > 0 { t.Fatalf("Warnings: %v", warn) } @@ -85,8 +97,8 @@ func TestResourceProvider_Validate_good(t *testing.T) { func TestResourceProvider_Validate_missing(t *testing.T) { c := testConfig(t, map[string]interface{}{}) - p := Provisioner() - warn, errs := p.Validate(c) + + warn, errs := Provisioner().Validate(c) if len(warn) > 0 { t.Fatalf("Warnings: %v", warn) } @@ -95,9 +107,7 @@ func TestResourceProvider_Validate_missing(t *testing.T) { } } -func testConfig( - t *testing.T, - c map[string]interface{}) *terraform.ResourceConfig { +func testConfig(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig { r, err := config.NewRawConfig(c) if err != nil { t.Fatalf("bad: %s", err) diff --git a/builtin/provisioners/remote-exec/resource_provisioner_test.go b/builtin/provisioners/remote-exec/resource_provisioner_test.go index aa69cad61..67faf1fe4 100644 --- a/builtin/provisioners/remote-exec/resource_provisioner_test.go +++ b/builtin/provisioners/remote-exec/resource_provisioner_test.go @@ -16,12 +16,22 @@ 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", }) - p := Provisioner() - warn, errs := p.Validate(c) + + warn, errs := Provisioner().Validate(c) if len(warn) > 0 { t.Fatalf("Warnings: %v", warn) } @@ -34,8 +44,8 @@ func TestResourceProvider_Validate_bad(t *testing.T) { c := testConfig(t, map[string]interface{}{ "invalid": "nope", }) - p := Provisioner() - warn, errs := p.Validate(c) + + warn, errs := Provisioner().Validate(c) if len(warn) > 0 { t.Fatalf("Warnings: %v", warn) } @@ -50,7 +60,6 @@ exit 0 ` func TestResourceProvider_generateScript(t *testing.T) { - p := Provisioner().(*schema.Provisioner) conf := map[string]interface{}{ "inline": []interface{}{ "cd /tmp", @@ -58,8 +67,10 @@ func TestResourceProvider_generateScript(t *testing.T) { "exit 0", }, } - out, err := generateScripts(schema.TestResourceDataRaw( - t, p.Schema, conf)) + + out, err := generateScripts( + schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, conf), + ) if err != nil { t.Fatalf("err: %v", err) } @@ -91,7 +102,6 @@ func TestResourceProvider_generateScriptEmptyInline(t *testing.T) { } func TestResourceProvider_CollectScripts_inline(t *testing.T) { - p := Provisioner().(*schema.Provisioner) conf := map[string]interface{}{ "inline": []interface{}{ "cd /tmp", @@ -100,8 +110,9 @@ func TestResourceProvider_CollectScripts_inline(t *testing.T) { }, } - scripts, err := collectScripts(schema.TestResourceDataRaw( - t, p.Schema, conf)) + scripts, err := collectScripts( + schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, conf), + ) if err != nil { t.Fatalf("err: %v", err) } @@ -122,13 +133,13 @@ func TestResourceProvider_CollectScripts_inline(t *testing.T) { } func TestResourceProvider_CollectScripts_script(t *testing.T) { - p := Provisioner().(*schema.Provisioner) conf := map[string]interface{}{ "script": "test-fixtures/script1.sh", } - scripts, err := collectScripts(schema.TestResourceDataRaw( - t, p.Schema, conf)) + scripts, err := collectScripts( + schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, conf), + ) if err != nil { t.Fatalf("err: %v", err) } @@ -149,7 +160,6 @@ func TestResourceProvider_CollectScripts_script(t *testing.T) { } func TestResourceProvider_CollectScripts_scripts(t *testing.T) { - p := Provisioner().(*schema.Provisioner) conf := map[string]interface{}{ "scripts": []interface{}{ "test-fixtures/script1.sh", @@ -158,8 +168,9 @@ func TestResourceProvider_CollectScripts_scripts(t *testing.T) { }, } - scripts, err := collectScripts(schema.TestResourceDataRaw( - t, p.Schema, conf)) + scripts, err := collectScripts( + schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, conf), + ) if err != nil { t.Fatalf("err: %v", err) } @@ -224,9 +235,7 @@ func TestRetryFunc(t *testing.T) { } } -func testConfig( - t *testing.T, - c map[string]interface{}) *terraform.ResourceConfig { +func testConfig(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig { r, err := config.NewRawConfig(c) if err != nil { t.Fatalf("bad: %s", err) diff --git a/command/internal_plugin_list.go b/command/internal_plugin_list.go index 0ac40837b..271bc8f6c 100644 --- a/command/internal_plugin_list.go +++ b/command/internal_plugin_list.go @@ -75,6 +75,7 @@ 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" @@ -84,9 +85,6 @@ import ( //New Provider Builds opcprovider "github.com/hashicorp/terraform-provider-opc/opc" - - // Legacy, will remove once it conforms with new structure - chefprovisioner "github.com/hashicorp/terraform/builtin/provisioners/chef" ) var InternalProviders = map[string]plugin.ProviderFunc{ @@ -162,16 +160,13 @@ 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) } - // New Provider Layouts InternalProviders["opc"] = func() terraform.ResourceProvider { return opcprovider.Provider() } } diff --git a/helper/schema/provisioner.go b/helper/schema/provisioner.go index c1564a215..856c6758a 100644 --- a/helper/schema/provisioner.go +++ b/helper/schema/provisioner.go @@ -41,6 +41,10 @@ type Provisioner struct { // information. ApplyFunc func(ctx context.Context) error + // ValidateFunc is a function for extended validation. This is optional + // and should be used when individual field validation is not enough. + ValidateFunc func(*ResourceData) ([]string, []error) + stopCtx context.Context stopCtxCancel context.CancelFunc stopOnce sync.Once @@ -117,8 +121,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 d8448acef..585586b7d 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/scripts/generate-plugins.go b/scripts/generate-plugins.go index b4a0bc9a4..415123891 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 { @@ -273,9 +272,6 @@ IMPORTS //New Provider Builds opcprovider "github.com/hashicorp/terraform-provider-opc/opc" - - // Legacy, will remove once it conforms with new structure - chefprovisioner "github.com/hashicorp/terraform/builtin/provisioners/chef" ) var InternalProviders = map[string]plugin.ProviderFunc{ @@ -287,12 +283,7 @@ 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) } - // New Provider Layouts InternalProviders["opc"] = func() terraform.ResourceProvider { return opcprovider.Provider() } } - ` diff --git a/website/source/docs/provisioners/chef.html.markdown b/website/source/docs/provisioners/chef.html.markdown index 8b9557f81..34fff2cd2 100644 --- a/website/source/docs/provisioners/chef.html.markdown +++ b/website/source/docs/provisioners/chef.html.markdown @@ -157,9 +157,3 @@ The following arguments are supported: * `version (string)` - (Optional) The Chef Client version to install on the remote machine. If not set, the latest available version will be installed. - -These options are supported for backwards compatibility and may be removed in a -future version: - -* `validation_client_name (string)` - __Deprecated: please use `user_name` instead__. -* `validation_key (string)` - __Deprecated: please use `user_key` instead__.