Compare commits

..

10 Commits

Author SHA1 Message Date
Bo-Yi Wu f9cc37282c update golang to 1.11
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2018-09-26 15:42:19 +08:00
Bo-Yi Wu 6e431b0c53 test: add TestCommandScriptStop (#124) 2018-09-26 15:35:57 +08:00
Marco Vito Moscaritolo 3499506089 exit after first error (#123)
Closes #121 

WIP
2018-09-26 15:23:31 +08:00
Josh Komoroske 6c0b475c15 Customization of logger output destination (#117)
* Customization of logger output destination

* Tests to verify output correctness
2018-02-28 14:52:15 +08:00
Josh Komoroske 60993a71e2 Preservation of forwarded environment (#113)
* Tests for omitting unset variables

* Preservation of forwarded environment
2018-02-28 13:44:44 +08:00
Bo-Yi Wu 8bfc58f9d0 test: Add escapeArg testing (#116) 2018-02-27 14:58:43 +08:00
Damian Kaczmarek 7f4cb1c1d0 improve: shell escaping, allow for whitespace and single quotes (#108) 2018-02-27 14:48:38 +08:00
Josh Komoroske f92f762c9d Load PLUGIN_ENV_FILE before app is run (#112) 2018-02-24 19:47:07 +08:00
Bo-Yi Wu 84cb184039 refactor(drone): replace facebook with discord. 2017-12-18 09:34:36 +08:00
Bo-Yi Wu 31c084fd3e remove unused doc.
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-12-18 09:30:13 +08:00
5 changed files with 310 additions and 57 deletions
+8 -9
View File
@@ -10,7 +10,7 @@ clone:
pipeline: pipeline:
lint: lint:
image: appleboy/golang-testing image: golang:1.11
pull: true pull: true
group: golang group: golang
commands: commands:
@@ -19,21 +19,21 @@ pipeline:
- make test-vendor - make test-vendor
linux_amd64: linux_amd64:
image: appleboy/golang-testing image: golang:1.11
pull: true pull: true
group: golang group: golang
commands: commands:
- make linux_amd64 - make linux_amd64
linux_arm64: linux_arm64:
image: appleboy/golang-testing image: golang:1.11
pull: true pull: true
group: golang group: golang
commands: commands:
- make linux_arm64 - make linux_arm64
linux_arm: linux_arm:
image: appleboy/golang-testing image: golang:1.11
pull: true pull: true
group: golang group: golang
commands: commands:
@@ -49,7 +49,7 @@ pipeline:
- make coverage - make coverage
release: release:
image: appleboy/golang-testing image: golang:1.11
pull: true pull: true
commands: commands:
- make release - make release
@@ -88,10 +88,9 @@ pipeline:
event: [ tag ] event: [ tag ]
local: false local: false
facebook: discord:
image: appleboy/drone-facebook image: appleboy/drone-discord
secrets: [ fb_page_token, fb_verify_token ]
pull: true pull: true
to: 1234973386524610 secrets: [ discord_webhook_id, discord_webhook_token ]
when: when:
status: [ changed, failure ] status: [ changed, failure ]
-34
View File
@@ -75,40 +75,6 @@ pipeline:
+ proxy_password: 1234 + proxy_password: 1234
``` ```
Example configuration for `master` branch:
```diff
pipeline:
ssh:
image: appleboy/drone-ssh
host: foo.com
username: root
password: 1234
port: 22
script:
- echo hello
- echo world
+ when:
+ branch: master
```
Example configuration for `tag` event:
```diff
pipeline:
ssh:
image: appleboy/drone-ssh
host: foo.com
username: root
password: 1234
port: 22
script:
- echo hello
- echo world
+ when:
+ event: tag
```
Example configuration using password from secrets: Example configuration using password from secrets:
```diff ```diff
+11 -7
View File
@@ -21,6 +21,11 @@ func main() {
Version = fmt.Sprintf("1.3.1+%s", build) Version = fmt.Sprintf("1.3.1+%s", build)
} }
// Load env-file if it exists first
if filename, found := os.LookupEnv("PLUGIN_ENV_FILE"); found {
_ = godotenv.Load(filename)
}
app := cli.NewApp() app := cli.NewApp()
app.Name = "Drone SSH" app.Name = "Drone SSH"
app.Usage = "Executing remote ssh commands" app.Usage = "Executing remote ssh commands"
@@ -87,9 +92,10 @@ func main() {
Usage: "execute commands", Usage: "execute commands",
EnvVar: "PLUGIN_SCRIPT,SSH_SCRIPT", EnvVar: "PLUGIN_SCRIPT,SSH_SCRIPT",
}, },
cli.StringFlag{ cli.BoolFlag{
Name: "env-file", Name: "script.stop",
Usage: "source env file", Usage: "stop script after first failure",
EnvVar: "PLUGIN_SCRIPT_STOP",
}, },
cli.StringFlag{ cli.StringFlag{
Name: "proxy.ssh-key", Name: "proxy.ssh-key",
@@ -185,10 +191,6 @@ REPOSITORY:
} }
func run(c *cli.Context) error { func run(c *cli.Context) error {
if c.String("env-file") != "" {
_ = godotenv.Load(c.String("env-file"))
}
plugin := Plugin{ plugin := Plugin{
Config: Config{ Config: Config{
Key: c.String("ssh-key"), Key: c.String("ssh-key"),
@@ -200,6 +202,7 @@ func run(c *cli.Context) error {
Timeout: c.Duration("timeout"), Timeout: c.Duration("timeout"),
CommandTimeout: c.Int("command.timeout"), CommandTimeout: c.Int("command.timeout"),
Script: c.StringSlice("script"), Script: c.StringSlice("script"),
ScriptStop: c.Bool("script.stop"),
Secrets: c.StringSlice("secrets"), Secrets: c.StringSlice("secrets"),
Envs: c.StringSlice("envs"), Envs: c.StringSlice("envs"),
Debug: c.Bool("debug"), Debug: c.Bool("debug"),
@@ -214,6 +217,7 @@ func run(c *cli.Context) error {
Timeout: c.Duration("proxy.timeout"), Timeout: c.Duration("proxy.timeout"),
}, },
}, },
Writer: os.Stdout,
} }
return plugin.Exec() return plugin.Exec()
+35 -6
View File
@@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"io"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@@ -30,6 +31,7 @@ type (
Timeout time.Duration Timeout time.Duration
CommandTimeout int CommandTimeout int
Script []string Script []string
ScriptStop bool
Secrets []string Secrets []string
Envs []string Envs []string
Proxy easyssh.DefaultConfig Proxy easyssh.DefaultConfig
@@ -40,9 +42,14 @@ type (
// Plugin structure // Plugin structure
Plugin struct { Plugin struct {
Config Config Config Config
Writer io.Writer
} }
) )
func escapeArg(arg string) string {
return "'" + strings.Replace(arg, "'", `'\''`, -1) + "'"
}
func (p Plugin) exec(host string, wg *sync.WaitGroup, errChannel chan error) { func (p Plugin) exec(host string, wg *sync.WaitGroup, errChannel chan error) {
// Create MakeConfig instance with remote username, server address and path to private key. // Create MakeConfig instance with remote username, server address and path to private key.
ssh := &easyssh.MakeConfig{ ssh := &easyssh.MakeConfig{
@@ -71,12 +78,12 @@ func (p Plugin) exec(host string, wg *sync.WaitGroup, errChannel chan error) {
env := []string{} env := []string{}
for _, key := range p.Config.Envs { for _, key := range p.Config.Envs {
key = strings.ToUpper(key) key = strings.ToUpper(key)
val := os.Getenv(key) if val, found := os.LookupEnv(key); found {
val = strings.Replace(val, " ", "", -1) env = append(env, key+"="+escapeArg(val))
env = append(env, key+"='"+val+"'") }
} }
p.Config.Script = append(env, p.Config.Script...) p.Config.Script = append(env, p.scriptCommands()...)
if p.Config.Debug { if p.Config.Debug {
p.log(host, "======ENV======") p.log(host, "======ENV======")
@@ -118,10 +125,13 @@ func (p Plugin) exec(host string, wg *sync.WaitGroup, errChannel chan error) {
} }
func (p Plugin) log(host string, message ...interface{}) { func (p Plugin) log(host string, message ...interface{}) {
if p.Writer == nil {
p.Writer = os.Stdout
}
if count := len(p.Config.Host); count == 1 { if count := len(p.Config.Host); count == 1 {
fmt.Printf("%s", fmt.Sprintln(message...)) fmt.Fprintf(p.Writer, "%s", fmt.Sprintln(message...))
} else { } else {
fmt.Printf("%s: %s", host, fmt.Sprintln(message...)) fmt.Fprintf(p.Writer, "%s: %s", host, fmt.Sprintln(message...))
} }
} }
@@ -170,3 +180,22 @@ func (p Plugin) Exec() error {
return nil return nil
} }
func (p Plugin) scriptCommands() []string {
numCommands := len(p.Config.Script)
if p.Config.ScriptStop {
numCommands *= 2
}
commands := make([]string, numCommands)
for _, cmd := range p.Config.Script {
if p.Config.ScriptStop {
commands = append(commands, "DRONE_SSH_PREV_COMMAND_EXIT_CODE=$? ; if [ $DRONE_SSH_PREV_COMMAND_EXIT_CODE -ne 0 ]; then exit $DRONE_SSH_PREV_COMMAND_EXIT_CODE; fi;")
}
commands = append(commands, cmd)
}
return commands
}
+256 -1
View File
@@ -1,7 +1,9 @@
package main package main
import ( import (
"bytes"
"os" "os"
"strings"
"testing" "testing"
"github.com/appleboy/easyssh-proxy" "github.com/appleboy/easyssh-proxy"
@@ -23,6 +25,7 @@ func TestMissingKeyOrPassword(t *testing.T) {
Host: []string{"localhost"}, Host: []string{"localhost"},
UserName: "ubuntu", UserName: "ubuntu",
}, },
os.Stdout,
} }
err := plugin.Exec() err := plugin.Exec()
@@ -39,6 +42,7 @@ func TestSetPasswordAndKey(t *testing.T) {
Password: "1234", Password: "1234",
Key: "1234", Key: "1234",
}, },
os.Stdout,
} }
err := plugin.Exec() err := plugin.Exec()
@@ -232,7 +236,7 @@ func TestSSHCommandExitCodeError(t *testing.T) {
} }
func TestSetENV(t *testing.T) { func TestSetENV(t *testing.T) {
os.Setenv("FOO", "1)") os.Setenv("FOO", `' 1) '`)
plugin := Plugin{ plugin := Plugin{
Config: Config{ Config: Config{
Host: []string{"localhost"}, Host: []string{"localhost"},
@@ -257,6 +261,33 @@ func TestSetENV(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
} }
func TestSetExistingENV(t *testing.T) {
os.Setenv("FOO", "Value for foo")
os.Setenv("BAR", "")
plugin := Plugin{
Config: Config{
Host: []string{"localhost"},
UserName: "drone-scp",
Port: 22,
KeyPath: "./tests/.ssh/id_rsa",
Secrets: []string{"FOO"},
Envs: []string{"foo", "bar", "baz"},
Debug: true,
Script: []string{"export FOO", "export BAR", "export BAZ", "env | grep -q '^FOO=Value for foo$'", "env | grep -q '^BAR=$'", "if env | grep -q BAZ; then false; else true; fi"},
CommandTimeout: 1,
Proxy: easyssh.DefaultConfig{
Server: "localhost",
User: "drone-scp",
Port: "22",
KeyPath: "./tests/.ssh/id_rsa",
},
},
}
err := plugin.Exec()
assert.Nil(t, err)
}
func TestSyncMode(t *testing.T) { func TestSyncMode(t *testing.T) {
plugin := Plugin{ plugin := Plugin{
Config: Config{ Config: Config{
@@ -273,3 +304,227 @@ func TestSyncMode(t *testing.T) {
err := plugin.Exec() err := plugin.Exec()
assert.Nil(t, err) assert.Nil(t, err)
} }
func Test_escapeArg(t *testing.T) {
type args struct {
arg string
}
tests := []struct {
name string
args args
want string
}{
{
name: "escape nothing",
args: args{
arg: "Hi I am appleboy",
},
want: `'Hi I am appleboy'`,
},
{
name: "escape single quote",
args: args{
arg: "Hi I am 'appleboy'",
},
want: `'Hi I am '\''appleboy'\'''`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := escapeArg(tt.args.arg)
assert.Equal(t, tt.want, got)
})
}
}
func TestCommandOutput(t *testing.T) {
var (
buffer bytes.Buffer
expected = `
localhost: ======CMD======
localhost: pwd
whoami
uname
localhost: ======END======
localhost: out: /home/drone-scp
localhost: out: drone-scp
localhost: out: Linux
127.0.0.1: ======CMD======
127.0.0.1: pwd
whoami
uname
127.0.0.1: ======END======
127.0.0.1: out: /home/drone-scp
127.0.0.1: out: drone-scp
127.0.0.1: out: Linux
`
)
plugin := Plugin{
Config: Config{
Host: []string{"localhost", "127.0.0.1"},
UserName: "drone-scp",
Port: 22,
KeyPath: "./tests/.ssh/id_rsa",
Script: []string{
"pwd",
"whoami",
"uname",
},
CommandTimeout: 60,
Sync: true,
},
Writer: &buffer,
}
err := plugin.Exec()
assert.Nil(t, err)
assert.Equal(t, unindent(expected), unindent(buffer.String()))
}
func TestScriptStop(t *testing.T) {
var (
buffer bytes.Buffer
expected = `
======CMD======
mkdir a/b/c
mkdir d/e/f
======END======
err: mkdir: can't create directory 'a/b/c': No such file or directory
`
)
plugin := Plugin{
Config: Config{
Host: []string{"localhost"},
UserName: "drone-scp",
Port: 22,
KeyPath: "./tests/.ssh/id_rsa",
Script: []string{
"mkdir a/b/c",
"mkdir d/e/f",
},
CommandTimeout: 10,
ScriptStop: true,
},
Writer: &buffer,
}
err := plugin.Exec()
assert.NotNil(t, err)
assert.Equal(t, unindent(expected), unindent(buffer.String()))
}
func TestNoneScriptStop(t *testing.T) {
var (
buffer bytes.Buffer
expected = `
======CMD======
mkdir a/b/c
mkdir d/e/f
======END======
err: mkdir: can't create directory 'a/b/c': No such file or directory
err: mkdir: can't create directory 'd/e/f': No such file or directory
`
)
plugin := Plugin{
Config: Config{
Host: []string{"localhost"},
UserName: "drone-scp",
Port: 22,
KeyPath: "./tests/.ssh/id_rsa",
Script: []string{
"mkdir a/b/c",
"mkdir d/e/f",
},
CommandTimeout: 10,
},
Writer: &buffer,
}
err := plugin.Exec()
assert.NotNil(t, err)
assert.Equal(t, unindent(expected), unindent(buffer.String()))
}
func TestEnvOutput(t *testing.T) {
var (
buffer bytes.Buffer
expected = `
======CMD======
echo "[${ENV_1}]"
echo "[${ENV_2}]"
echo "[${ENV_3}]"
echo "[${ENV_4}]"
echo "[${ENV_5}]"
echo "[${ENV_6}]"
echo "[${ENV_7}]"
======END======
======ENV======
ENV_1='test'
ENV_2='test test'
ENV_3='test '
ENV_4=' test test '
ENV_5='test'\'''
ENV_6='test"'
ENV_7='test,!#;?.@$~'\''"'
======END======
out: [test]
out: [test test]
out: [test ]
out: [ test test ]
out: [test']
out: [test"]
out: [test,!#;?.@$~'"]
`
)
os.Setenv("ENV_1", `test`)
os.Setenv("ENV_2", `test test`)
os.Setenv("ENV_3", `test `)
os.Setenv("ENV_4", ` test test `)
os.Setenv("ENV_5", `test'`)
os.Setenv("ENV_6", `test"`)
os.Setenv("ENV_7", `test,!#;?.@$~'"`)
plugin := Plugin{
Config: Config{
Host: []string{"localhost"},
UserName: "drone-scp",
Port: 22,
KeyPath: "./tests/.ssh/id_rsa",
Envs: []string{"env_1", "env_2", "env_3", "env_4", "env_5", "env_6", "env_7"},
Debug: true,
Script: []string{
`echo "[${ENV_1}]"`,
`echo "[${ENV_2}]"`,
`echo "[${ENV_3}]"`,
`echo "[${ENV_4}]"`,
`echo "[${ENV_5}]"`,
`echo "[${ENV_6}]"`,
`echo "[${ENV_7}]"`,
},
CommandTimeout: 10,
Proxy: easyssh.DefaultConfig{
Server: "localhost",
User: "drone-scp",
Port: "22",
KeyPath: "./tests/.ssh/id_rsa",
},
},
Writer: &buffer,
}
err := plugin.Exec()
assert.Nil(t, err)
assert.Equal(t, unindent(expected), unindent(buffer.String()))
}
func unindent(text string) string {
return strings.TrimSpace(strings.Replace(text, "\t", "", -1))
}