diff --git a/builtin/provisioners/remote-exec/resource_provisioner.go b/builtin/provisioners/remote-exec/resource_provisioner.go index c70d78691..4d9150f4d 100644 --- a/builtin/provisioners/remote-exec/resource_provisioner.go +++ b/builtin/provisioners/remote-exec/resource_provisioner.go @@ -2,35 +2,148 @@ package remoteexec import ( "fmt" + "strings" "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/mapstructure" +) + +const ( + // DefaultUser is used if there is no default user given + DefaultUser = "root" + + // DefaultPort is used if there is no port given + DefaultPort = 22 + + // DefaultScriptPath is used as the path to copy the file to + // for remote execution if not provided otherwise. + DefaultScriptPath = "/tmp/script.sh" + + // DefaultTimeout is used if there is no timeout given + DefaultTimeout = "5m" + + // DefaultShebang is added at the top of the script file + DefaultShebang = "#!/bin/sh" ) type ResourceProvisioner struct{} -func (p *ResourceProvisioner) Apply( - s *terraform.ResourceState, +// SSHConfig is decoded from the ConnInfo of the resource. These +// are the only keys we look at. If a KeyFile is given, that is used +// instead of a password. +type SSHConfig struct { + User string + Password string + KeyFile string `mapstructure:"key_file"` + Host string + Port int + Timeout string + ScriptPath string `mapstructure:"script_path"` +} + +func (p *ResourceProvisioner) Apply(s *terraform.ResourceState, c *terraform.ResourceConfig) (*terraform.ResourceState, error) { + // Ensure the connection type is SSH + if err := p.verifySSH(s); err != nil { + return s, err + } + + // Get the SSH configuration + _, err := p.sshConfig(s) + if err != nil { + return s, err + } + panic("not implemented") return s, nil } func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) { - var hasCommand, hasInline bool + num := 0 for name := range c.Raw { switch name { - case "command": - hasCommand = true + case "scripts": + fallthrough + case "script": + fallthrough case "inline": - hasInline = true + num++ default: es = append(es, fmt.Errorf("Unknown configuration '%s'", name)) } } - if hasInline && hasCommand { - es = append(es, fmt.Errorf("Cannot provide both 'command' and 'inline' to remote-exec")) - } else if !hasInline && !hasCommand { - es = append(es, fmt.Errorf("Must provide 'command' or 'inline' to remote-exec")) + if num != 1 { + es = append(es, fmt.Errorf("Must provide one of 'scripts', 'script' or 'inline' to remote-exec")) } return } + +// verifySSH is used to verify the ConnInfo is usable by remote-exec +func (p *ResourceProvisioner) verifySSH(s *terraform.ResourceState) error { + connType := s.ConnInfo.Raw["type"] + switch connType { + case "": + case "ssh": + default: + return fmt.Errorf("Connection type '%s' not supported", connType) + } + return nil +} + +// sshConfig is used to convert the ConnInfo of the ResourceState into +// a SSHConfig struct +func (p *ResourceProvisioner) sshConfig(s *terraform.ResourceState) (*SSHConfig, error) { + sshConf := &SSHConfig{} + decConf := &mapstructure.DecoderConfig{ + WeaklyTypedInput: true, + Result: sshConf, + } + dec, err := mapstructure.NewDecoder(decConf) + if err != nil { + return nil, err + } + if err := dec.Decode(s.ConnInfo.Raw); err != nil { + return nil, err + } + if sshConf.User == "" { + sshConf.User = DefaultUser + } + if sshConf.Port == 0 { + sshConf.Port = DefaultPort + } + if sshConf.ScriptPath == "" { + sshConf.ScriptPath = DefaultScriptPath + } + if sshConf.Timeout == "" { + sshConf.Timeout = DefaultTimeout + } + return sshConf, nil +} + +// generateScript takes the configuration and creates a script to be executed +// from the inline configs +func (p *ResourceProvisioner) generateScript(c *terraform.ResourceConfig) (string, error) { + lines := []string{DefaultShebang} + command, ok := c.Config["inline"] + if ok { + switch cmd := command.(type) { + case string: + lines = append(lines, cmd) + case []string: + lines = append(lines, cmd...) + case []interface{}: + for _, l := range cmd { + lStr, ok := l.(string) + if ok { + lines = append(lines, lStr) + } else { + return "", fmt.Errorf("Unsupported 'inline' type! Must be string, or list of strings.") + } + } + default: + return "", fmt.Errorf("Unsupported 'inline' type! Must be string, or list of strings.") + } + } + lines = append(lines, "") + return strings.Join(lines, "\n"), nil +} diff --git a/builtin/provisioners/remote-exec/resource_provisioner_test.go b/builtin/provisioners/remote-exec/resource_provisioner_test.go index 2d09fea0b..27112f42b 100644 --- a/builtin/provisioners/remote-exec/resource_provisioner_test.go +++ b/builtin/provisioners/remote-exec/resource_provisioner_test.go @@ -13,7 +13,7 @@ func TestResourceProvisioner_impl(t *testing.T) { func TestResourceProvider_Validate_good(t *testing.T) { c := testConfig(t, map[string]interface{}{ - "command": "echo foo", + "inline": "echo foo", }) p := new(ResourceProvisioner) warn, errs := p.Validate(c) @@ -39,6 +39,93 @@ func TestResourceProvider_Validate_bad(t *testing.T) { } } +func TestResourceProvider_verifySSH(t *testing.T) { + p := new(ResourceProvisioner) + r := &terraform.ResourceState{ + ConnInfo: &terraform.ResourceConnectionInfo{ + Raw: map[string]interface{}{ + "type": "telnet", + }, + }, + } + if err := p.verifySSH(r); err == nil { + t.Fatalf("expected error with telnet") + } + r.ConnInfo.Raw["type"] = "ssh" + if err := p.verifySSH(r); err != nil { + t.Fatalf("err: %v", err) + } +} + +func TestResourceProvider_sshConfig(t *testing.T) { + p := new(ResourceProvisioner) + r := &terraform.ResourceState{ + ConnInfo: &terraform.ResourceConnectionInfo{ + Raw: map[string]interface{}{ + "type": "ssh", + "user": "root", + "password": "supersecret", + "key_file": "/my/key/file.pem", + "host": "127.0.0.1", + "port": "22", + "timeout": "30s", + }, + }, + } + + conf, err := p.sshConfig(r) + if err != nil { + t.Fatalf("err: %v", err) + } + + if conf.User != "root" { + t.Fatalf("bad: %v", conf) + } + if conf.Password != "supersecret" { + t.Fatalf("bad: %v", conf) + } + if conf.KeyFile != "/my/key/file.pem" { + t.Fatalf("bad: %v", conf) + } + if conf.Host != "127.0.0.1" { + t.Fatalf("bad: %v", conf) + } + if conf.Port != 22 { + t.Fatalf("bad: %v", conf) + } + if conf.Timeout != "30s" { + t.Fatalf("bad: %v", conf) + } + if conf.ScriptPath != DefaultScriptPath { + t.Fatalf("bad: %v", conf) + } +} + +func TestResourceProvider_generateScript(t *testing.T) { + p := new(ResourceProvisioner) + conf := testConfig(t, map[string]interface{}{ + "inline": []string{ + "cd /tmp", + "wget http://foobar", + "exit 0", + }, + }) + out, err := p.generateScript(conf) + if err != nil { + t.Fatalf("err: %v", err) + } + + if out != expectedScriptOut { + t.Fatalf("bad: %v", out) + } +} + +var expectedScriptOut = `#!/bin/sh +cd /tmp +wget http://foobar +exit 0 +` + func testConfig( t *testing.T, c map[string]interface{}) *terraform.ResourceConfig {