drone-ssh/plugin.go

307 lines
6.9 KiB
Go
Raw Permalink Normal View History

2016-07-11 19:47:15 +02:00
package main
import (
"errors"
2017-01-23 14:33:49 +08:00
"fmt"
"io"
"os"
2016-07-11 19:47:15 +02:00
"strconv"
"strings"
"sync"
2016-07-11 19:47:15 +02:00
"time"
easyssh "github.com/appleboy/easyssh-proxy"
2016-07-11 19:47:15 +02:00
)
var (
errMissingHost = errors.New("error: missing server host")
errMissingPasswordOrKey = errors.New(
"error: can't connect without a private SSH key or password",
)
errCommandTimeOut = errors.New("error: command timeout")
envsFormat = "export {NAME}={VALUE}"
2017-01-23 14:24:27 +08:00
)
2016-07-11 19:47:15 +02:00
type (
2017-01-23 10:03:54 +08:00
// Config for the plugin.
2016-07-11 19:47:15 +02:00
Config struct {
Key string
Passphrase string
KeyPath string
Username string
Password string
Host []string
Port int
Protocol easyssh.Protocol
Fingerprint string
Timeout time.Duration
CommandTimeout time.Duration
Script []string
ScriptStop bool
Envs []string
Proxy easyssh.DefaultConfig
Debug bool
Sync bool
Ciphers []string
UseInsecureCipher bool
EnvsFormat string
AllEnvs bool
RequireTty bool
2016-07-11 19:47:15 +02:00
}
2017-01-23 10:03:54 +08:00
// Plugin structure
2016-07-11 19:47:15 +02:00
Plugin struct {
Config Config
Writer io.Writer
2016-07-11 19:47:15 +02:00
}
)
func escapeArg(arg string) string {
return "'" + strings.ReplaceAll(arg, "'", `'\''`) + "'"
}
func (p Plugin) hostPort(host string) (string, string) {
hosts := strings.Split(host, ":")
port := strconv.Itoa(p.Config.Port)
chore(protocol): improve IPv6 address. (#268) * docs: improve documentation and configuration handling - Clarify valid values for the IP protocol in usage messages for both main application and proxy settings Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com> * test: improve IPv6 command execution tests - Add a new test function `TestCommandWithIPv6` to check command execution with an IPv6 address - Initialize test variables and expected output for the IPv6 command test - Set up a `Plugin` struct with IPv6 host, user, port, key path, script, and command timeout for testing - Verify that `plugin.Exec()` returns `nil` (no error) in the IPv6 test - Assert that the output of the command execution matches the expected output in the IPv6 test Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com> * test: enhance test suite and CI robustness - Add support for IPv6 protocol in `TestCommandWithIPv6` test case in `plugin_test.go` Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com> * update Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com> * update Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com> * update Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com> * update Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com> * update2 Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com> * update3 Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com> * update4 Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com> * update5 Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com> * update5 Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com> * update5 Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com> --------- Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2024-01-21 09:27:49 +08:00
if len(hosts) > 1 &&
(p.Config.Protocol == easyssh.PROTOCOL_TCP ||
p.Config.Protocol == easyssh.PROTOCOL_TCP4) {
host = hosts[0]
port = hosts[1]
}
return host, port
}
func (p Plugin) exec(host string, wg *sync.WaitGroup, errChannel chan error) {
defer wg.Done()
host, port := p.hostPort(host)
// Create MakeConfig instance with remote username, server address and path to private key.
ssh := &easyssh.MakeConfig{
Server: host,
User: p.Config.Username,
Password: p.Config.Password,
Port: port,
Protocol: p.Config.Protocol,
Key: p.Config.Key,
KeyPath: p.Config.KeyPath,
Passphrase: p.Config.Passphrase,
Timeout: p.Config.Timeout,
Ciphers: p.Config.Ciphers,
Fingerprint: p.Config.Fingerprint,
UseInsecureCipher: p.Config.UseInsecureCipher,
RequestPty: p.Config.RequireTty,
Proxy: easyssh.DefaultConfig{
Server: p.Config.Proxy.Server,
User: p.Config.Proxy.User,
Password: p.Config.Proxy.Password,
Port: p.Config.Proxy.Port,
Protocol: p.Config.Proxy.Protocol,
Key: p.Config.Proxy.Key,
KeyPath: p.Config.Proxy.KeyPath,
Passphrase: p.Config.Proxy.Passphrase,
Timeout: p.Config.Proxy.Timeout,
Ciphers: p.Config.Proxy.Ciphers,
Fingerprint: p.Config.Proxy.Fingerprint,
UseInsecureCipher: p.Config.Proxy.UseInsecureCipher,
},
}
if p.Config.Debug {
p.log(host, "======CMD======")
p.log(host, strings.Join(p.Config.Script, "\n"))
p.log(host, "======END======")
}
env := []string{}
if p.Config.AllEnvs {
allenvs := findEnvs("DRONE_", "PLUGIN_", "INPUT_", "GITHUB_")
p.Config.Envs = append(p.Config.Envs, allenvs...)
}
for _, key := range p.Config.Envs {
key = strings.ToUpper(key)
if val, found := os.LookupEnv(key); found {
env = append(
env,
p.format(p.Config.EnvsFormat, "{NAME}", key, "{VALUE}", escapeArg(val)),
)
}
}
p.Config.Script = append(env, p.scriptCommands()...)
if p.Config.Debug && len(env) > 0 {
p.log(host, "======ENV======")
p.log(host, strings.Join(env, "\n"))
p.log(host, "======END======")
}
stdoutChan, stderrChan, doneChan, errChan, err := ssh.Stream(
strings.Join(p.Config.Script, "\n"),
p.Config.CommandTimeout,
)
if err != nil {
errChannel <- err
return
}
// read from the output channel until the done signal is passed
var isTimeout bool
loop:
for {
select {
case isTimeout = <-doneChan:
break loop
case outline := <-stdoutChan:
if outline != "" {
p.log(host, outline)
}
case errline := <-stderrChan:
if errline != "" {
p.log(host, errline)
}
case err = <-errChan:
}
}
// get exit code or command error.
if err != nil {
errChannel <- err
}
// command time out
if !isTimeout {
errChannel <- errCommandTimeOut
}
}
// format string
func (p Plugin) format(format string, args ...string) string {
r := strings.NewReplacer(args...)
return r.Replace(format)
}
// log output to console
func (p Plugin) log(host string, message ...interface{}) {
if p.Writer == nil {
p.Writer = os.Stdout
}
if count := len(p.Config.Host); count == 1 {
fmt.Fprintf(p.Writer, "%s", fmt.Sprintln(message...))
return
}
fmt.Fprintf(p.Writer, "%s: %s", host, fmt.Sprintln(message...))
}
2017-01-23 10:03:54 +08:00
// Exec executes the plugin.
2016-07-11 19:47:15 +02:00
func (p Plugin) Exec() error {
p.Config.Host = trimValues(p.Config.Host)
if len(p.Config.Host) == 0 {
return errMissingHost
2017-01-23 14:24:27 +08:00
}
if len(p.Config.Key) == 0 && len(p.Config.Password) == 0 && len(p.Config.KeyPath) == 0 {
return errMissingPasswordOrKey
2016-07-11 19:47:15 +02:00
}
if p.Config.EnvsFormat == "" {
p.Config.EnvsFormat = envsFormat
}
wg := sync.WaitGroup{}
wg.Add(len(p.Config.Host))
errChannel := make(chan error)
finished := make(chan struct{})
if p.Config.Sync {
go func() {
for _, host := range p.Config.Host {
p.exec(host, &wg, errChannel)
}
}()
} else {
for _, host := range p.Config.Host {
go p.exec(host, &wg, errChannel)
}
}
2016-07-11 19:47:15 +02:00
go func() {
wg.Wait()
close(finished)
}()
2016-07-11 19:47:15 +02:00
select {
case <-finished:
case err := <-errChannel:
2016-07-11 19:47:15 +02:00
if err != nil {
return err
}
}
fmt.Println("===============================================")
fmt.Println("✅ Successfully executed commands to all hosts.")
fmt.Println("===============================================")
2016-07-11 19:47:15 +02:00
return nil
}
func (p Plugin) scriptCommands() []string {
scripts := []string{}
for _, cmd := range p.Config.Script {
if p.Config.ScriptStop {
scripts = append(scripts, strings.Split(cmd, "\n")...)
} else {
scripts = append(scripts, cmd)
}
}
commands := make([]string, 0)
for _, cmd := range scripts {
cmd = strings.TrimSpace(cmd)
if strings.TrimSpace(cmd) == "" {
continue
}
commands = append(commands, cmd)
if p.Config.ScriptStop && cmd[(len(cmd)-1):] != "\\" {
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;",
)
}
}
return commands
}
func trimValues(keys []string) []string {
var newKeys []string
for _, value := range keys {
value = strings.TrimSpace(value)
if len(value) == 0 {
continue
}
newKeys = append(newKeys, value)
}
return newKeys
}
// Find all envs from specified prefix
func findEnvs(prefix ...string) []string {
envs := []string{}
for _, e := range os.Environ() {
for _, p := range prefix {
if strings.HasPrefix(e, p) {
e = strings.Split(e, "=")[0]
envs = append(envs, e)
break
}
}
}
return envs
}