mirror of
				https://github.com/appleboy/drone-ssh.git
				synced 2025-10-29 00:51:15 +08:00 
			
		
		
		
	feat: add easyssh and go routine.
This commit is contained in:
		
							parent
							
								
									c086c6a226
								
							
						
					
					
						commit
						763c78af52
					
				
							
								
								
									
										193
									
								
								easyssh/easyssh.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								easyssh/easyssh.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,193 @@ | |||||||
|  | // Package easyssh provides a simple implementation of some SSH protocol
 | ||||||
|  | // features in Go. You can simply run a command on a remote server or get a file
 | ||||||
|  | // even simpler than native console SSH client. You don't need to think about
 | ||||||
|  | // Dials, sessions, defers, or public keys... Let easyssh think about it!
 | ||||||
|  | package easyssh | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bufio" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"golang.org/x/crypto/ssh" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // MakeConfig Contains main authority information.
 | ||||||
|  | // User field should be a name of user on remote server (ex. john in ssh john@example.com).
 | ||||||
|  | // Server field should be a remote machine address (ex. example.com in ssh john@example.com)
 | ||||||
|  | // Key is a path to private key on your local machine.
 | ||||||
|  | // Port is SSH server port on remote machine.
 | ||||||
|  | // Note: easyssh looking for private key in user's home directory (ex. /home/john + Key).
 | ||||||
|  | // Then ensure your Key begins from '/' (ex. /.ssh/id_rsa)
 | ||||||
|  | type MakeConfig struct { | ||||||
|  | 	User     string | ||||||
|  | 	Server   string | ||||||
|  | 	Key      string | ||||||
|  | 	KeyPath  string | ||||||
|  | 	Port     string | ||||||
|  | 	Password string | ||||||
|  | 	Timeout  time.Duration | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // returns ssh.Signer from user you running app home path + cutted key path.
 | ||||||
|  | // (ex. pubkey,err := getKeyFile("/.ssh/id_rsa") )
 | ||||||
|  | func getKeyFile(keypath string) (ssh.Signer, error) { | ||||||
|  | 	buf, err := ioutil.ReadFile(keypath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	pubkey, err := ssh.ParsePrivateKey(buf) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return pubkey, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // connects to remote server using MakeConfig struct and returns *ssh.Session
 | ||||||
|  | func (ssh_conf *MakeConfig) connect() (*ssh.Session, error) { | ||||||
|  | 	// auths holds the detected ssh auth methods
 | ||||||
|  | 	auths := []ssh.AuthMethod{} | ||||||
|  | 
 | ||||||
|  | 	// figure out what auths are requested, what is supported
 | ||||||
|  | 	if ssh_conf.Password != "" { | ||||||
|  | 		auths = append(auths, ssh.Password(ssh_conf.Password)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if ssh_conf.KeyPath != "" { | ||||||
|  | 		if pubkey, err := getKeyFile(ssh_conf.KeyPath); err == nil { | ||||||
|  | 			auths = append(auths, ssh.PublicKeys(pubkey)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if ssh_conf.Key != "" { | ||||||
|  | 		signer, _ := ssh.ParsePrivateKey([]byte(ssh_conf.Key)) | ||||||
|  | 		auths = append(auths, ssh.PublicKeys(signer)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	config := &ssh.ClientConfig{ | ||||||
|  | 		Timeout: ssh_conf.Timeout, | ||||||
|  | 		User:    ssh_conf.User, | ||||||
|  | 		Auth:    auths, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	client, err := ssh.Dial("tcp", ssh_conf.Server+":"+ssh_conf.Port, config) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	session, err := client.NewSession() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return session, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Stream returns one channel that combines the stdout and stderr of the command
 | ||||||
|  | // as it is run on the remote machine, and another that sends true when the
 | ||||||
|  | // command is done. The sessions and channels will then be closed.
 | ||||||
|  | func (ssh_conf *MakeConfig) Stream(command string) (output chan string, done chan bool, err error) { | ||||||
|  | 	// connect to remote host
 | ||||||
|  | 	session, err := ssh_conf.connect() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return output, done, err | ||||||
|  | 	} | ||||||
|  | 	// connect to both outputs (they are of type io.Reader)
 | ||||||
|  | 	outReader, err := session.StdoutPipe() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return output, done, err | ||||||
|  | 	} | ||||||
|  | 	errReader, err := session.StderrPipe() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return output, done, err | ||||||
|  | 	} | ||||||
|  | 	// combine outputs, create a line-by-line scanner
 | ||||||
|  | 	outputReader := io.MultiReader(outReader, errReader) | ||||||
|  | 	err = session.Start(command) | ||||||
|  | 	scanner := bufio.NewScanner(outputReader) | ||||||
|  | 	// continuously send the command's output over the channel
 | ||||||
|  | 	outputChan := make(chan string) | ||||||
|  | 	done = make(chan bool) | ||||||
|  | 	go func(scanner *bufio.Scanner, out chan string, done chan bool) { | ||||||
|  | 		defer close(outputChan) | ||||||
|  | 		defer close(done) | ||||||
|  | 		for scanner.Scan() { | ||||||
|  | 			outputChan <- scanner.Text() | ||||||
|  | 		} | ||||||
|  | 		// close all of our open resources
 | ||||||
|  | 		done <- true | ||||||
|  | 		session.Close() | ||||||
|  | 	}(scanner, outputChan, done) | ||||||
|  | 	return outputChan, done, err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Run command on remote machine and returns its stdout as a string
 | ||||||
|  | func (ssh_conf *MakeConfig) Run(command string) (outStr string, err error) { | ||||||
|  | 	outChan, doneChan, err := ssh_conf.Stream(command) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return outStr, err | ||||||
|  | 	} | ||||||
|  | 	// read from the output channel until the done signal is passed
 | ||||||
|  | 	stillGoing := true | ||||||
|  | 	for stillGoing { | ||||||
|  | 		select { | ||||||
|  | 		case <-doneChan: | ||||||
|  | 			stillGoing = false | ||||||
|  | 		case line := <-outChan: | ||||||
|  | 			outStr += line + "\n" | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	// return the concatenation of all signals from the output channel
 | ||||||
|  | 	return outStr, err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Scp uploads sourceFile to remote machine like native scp console app.
 | ||||||
|  | func (ssh_conf *MakeConfig) Scp(sourceFile string) error { | ||||||
|  | 	session, err := ssh_conf.connect() | ||||||
|  | 
 | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer session.Close() | ||||||
|  | 
 | ||||||
|  | 	targetFile := filepath.Base(sourceFile) | ||||||
|  | 
 | ||||||
|  | 	src, srcErr := os.Open(sourceFile) | ||||||
|  | 
 | ||||||
|  | 	if srcErr != nil { | ||||||
|  | 		return srcErr | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	srcStat, statErr := src.Stat() | ||||||
|  | 
 | ||||||
|  | 	if statErr != nil { | ||||||
|  | 		return statErr | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	go func() { | ||||||
|  | 		w, _ := session.StdinPipe() | ||||||
|  | 
 | ||||||
|  | 		fmt.Fprintln(w, "C0644", srcStat.Size(), targetFile) | ||||||
|  | 
 | ||||||
|  | 		if srcStat.Size() > 0 { | ||||||
|  | 			io.Copy(w, src) | ||||||
|  | 			fmt.Fprint(w, "\x00") | ||||||
|  | 			w.Close() | ||||||
|  | 		} else { | ||||||
|  | 			fmt.Fprint(w, "\x00") | ||||||
|  | 			w.Close() | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  | 
 | ||||||
|  | 	if err := session.Run(fmt.Sprintf("scp -t %s", targetFile)); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										6
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								main.go
									
									
									
									
									
								
							| @ -57,11 +57,6 @@ func main() { | |||||||
| 			EnvVar: "PLUGIN_PORT,SSH_PORT", | 			EnvVar: "PLUGIN_PORT,SSH_PORT", | ||||||
| 			Value:  22, | 			Value:  22, | ||||||
| 		}, | 		}, | ||||||
| 		cli.IntFlag{ |  | ||||||
| 			Name:   "sleep", |  | ||||||
| 			Usage:  "sleep between hosts", |  | ||||||
| 			EnvVar: "PLUGIN_SLEEP,SSH_SLEEP", |  | ||||||
| 		}, |  | ||||||
| 		cli.DurationFlag{ | 		cli.DurationFlag{ | ||||||
| 			Name:   "timeout,t", | 			Name:   "timeout,t", | ||||||
| 			Usage:  "connection timeout", | 			Usage:  "connection timeout", | ||||||
| @ -127,7 +122,6 @@ func run(c *cli.Context) error { | |||||||
| 			Password: c.String("password"), | 			Password: c.String("password"), | ||||||
| 			Host:     c.StringSlice("host"), | 			Host:     c.StringSlice("host"), | ||||||
| 			Port:     c.Int("port"), | 			Port:     c.Int("port"), | ||||||
| 			Sleep:    c.Int("sleep"), |  | ||||||
| 			Timeout:  c.Duration("timeout"), | 			Timeout:  c.Duration("timeout"), | ||||||
| 			Script:   c.StringSlice("script"), | 			Script:   c.StringSlice("script"), | ||||||
| 		}, | 		}, | ||||||
|  | |||||||
							
								
								
									
										109
									
								
								plugin.go
									
									
									
									
									
								
							
							
						
						
									
										109
									
								
								plugin.go
									
									
									
									
									
								
							| @ -2,23 +2,20 @@ package main | |||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/ioutil" |  | ||||||
| 	"log" | 	"log" | ||||||
| 	"net" |  | ||||||
| 	"os" |  | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"golang.org/x/crypto/ssh" | 	"github.com/appleboy/drone-ssh/easyssh" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | var wg sync.WaitGroup | ||||||
|  | 
 | ||||||
| const ( | const ( | ||||||
| 	missingHostOrUser    = "Error: missing server host or user" | 	missingHostOrUser    = "Error: missing server host or user" | ||||||
| 	missingPasswordOrKey = "Error: can't connect without a private SSH key or password" | 	missingPasswordOrKey = "Error: can't connect without a private SSH key or password" | ||||||
| 	unableConnectServer  = "Error: Failed to start a SSH session" |  | ||||||
| 	failParsePrivateKey  = "Error: Failed to parse private key" |  | ||||||
| 	sshKeyNotFound       = "ssh: no key found" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type ( | type ( | ||||||
| @ -30,7 +27,6 @@ type ( | |||||||
| 		Password string | 		Password string | ||||||
| 		Host     []string | 		Host     []string | ||||||
| 		Port     int | 		Port     int | ||||||
| 		Sleep    int |  | ||||||
| 		Timeout  time.Duration | 		Timeout  time.Duration | ||||||
| 		Script   []string | 		Script   []string | ||||||
| 	} | 	} | ||||||
| @ -41,20 +37,8 @@ type ( | |||||||
| 	} | 	} | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // returns ssh.Signer from user you running app home path + cutted key path.
 | func (p Plugin) log(host string, message ...interface{}) { | ||||||
| // (ex. pubkey,err := getKeyFile("/.ssh/id_rsa") )
 | 	log.Printf("%s: %s", host, fmt.Sprintln(message...)) | ||||||
| func getKeyFile(keypath string) (ssh.Signer, error) { |  | ||||||
| 	buf, err := ioutil.ReadFile(keypath) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	pubkey, err := ssh.ParsePrivateKey(buf) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return pubkey, nil |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Exec executes the plugin.
 | // Exec executes the plugin.
 | ||||||
| @ -67,68 +51,49 @@ func (p Plugin) Exec() error { | |||||||
| 		return fmt.Errorf(missingPasswordOrKey) | 		return fmt.Errorf(missingPasswordOrKey) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for i, host := range p.Config.Host { | 	wg.Add(len(p.Config.Host)) | ||||||
| 		addr := net.JoinHostPort( | 	errChannel := make(chan error, 1) | ||||||
| 			host, | 	finished := make(chan bool, 1) | ||||||
| 			strconv.Itoa(p.Config.Port), | 	for _, host := range p.Config.Host { | ||||||
| 		) | 		go func(host string) { | ||||||
| 
 | 			// Create MakeConfig instance with remote username, server address and path to private key.
 | ||||||
| 		// auths holds the detected ssh auth methods
 | 			ssh := &easyssh.MakeConfig{ | ||||||
| 		auths := []ssh.AuthMethod{} | 				Server:   host, | ||||||
| 
 |  | ||||||
| 		if p.Config.KeyPath != "" { |  | ||||||
| 			pubkey, err := getKeyFile(p.Config.KeyPath) |  | ||||||
| 
 |  | ||||||
| 			if err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			auths = append(auths, ssh.PublicKeys(pubkey)) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if p.Config.Key != "" { |  | ||||||
| 			signer, err := ssh.ParsePrivateKey([]byte(p.Config.Key)) |  | ||||||
| 
 |  | ||||||
| 			if err != nil { |  | ||||||
| 				return fmt.Errorf(failParsePrivateKey) |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			auths = append(auths, ssh.PublicKeys(signer)) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// figure out what auths are requested, what is supported
 |  | ||||||
| 		if p.Config.Password != "" { |  | ||||||
| 			auths = append(auths, ssh.Password(p.Config.Password)) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		config := &ssh.ClientConfig{ |  | ||||||
| 			Timeout: p.Config.Timeout, |  | ||||||
| 				User:     p.Config.User, | 				User:     p.Config.User, | ||||||
| 			Auth:    auths, | 				Password: p.Config.Password, | ||||||
|  | 				Port:     strconv.Itoa(p.Config.Port), | ||||||
|  | 				Key:      p.Config.Key, | ||||||
|  | 				KeyPath:  p.Config.KeyPath, | ||||||
|  | 				Timeout:  p.Config.Timeout, | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 		log.Printf("+ ssh %s@%s \n", p.Config.User, addr) | 			p.log(host, "commands: ", strings.Join(p.Config.Script, "\n")) | ||||||
| 		client, err := ssh.Dial("tcp", addr, config) | 			response, err := ssh.Run(strings.Join(p.Config.Script, "\n")) | ||||||
|  | 			p.log(host, "outputs:", response) | ||||||
| 
 | 
 | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 			return fmt.Errorf(unableConnectServer) | 				errChannel <- err | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 		session, _ := client.NewSession() | 			wg.Done() | ||||||
| 		defer session.Close() | 		}(host) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 		session.Stdout = os.Stdout | 	go func() { | ||||||
| 		session.Stderr = os.Stderr | 		wg.Wait() | ||||||
|  | 		close(finished) | ||||||
|  | 	}() | ||||||
| 
 | 
 | ||||||
| 		if err := session.Run(strings.Join(p.Config.Script, "\n")); err != nil { | 	select { | ||||||
|  | 	case <-finished: | ||||||
|  | 	case err := <-errChannel: | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println("drone-ssh error: ", err) | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 		if p.Config.Sleep != 0 && i != len(p.Config.Host)-1 { | 	log.Println("Successfully executed commnads to all host.") | ||||||
| 			log.Printf("+ sleep %d\n", p.Config.Sleep) |  | ||||||
| 			time.Sleep(time.Duration(p.Config.Sleep) * time.Second) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  | |||||||
| @ -42,23 +42,6 @@ func TestIncorrectPassword(t *testing.T) { | |||||||
| 
 | 
 | ||||||
| 	err := plugin.Exec() | 	err := plugin.Exec() | ||||||
| 	assert.NotNil(t, err) | 	assert.NotNil(t, err) | ||||||
| 	assert.Equal(t, unableConnectServer, err.Error()) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestFailParsePrivateKey(t *testing.T) { |  | ||||||
| 	plugin := Plugin{ |  | ||||||
| 		Config: Config{ |  | ||||||
| 			Host:   []string{"localhost"}, |  | ||||||
| 			User:   "drone-scp", |  | ||||||
| 			Port:   22, |  | ||||||
| 			Key:    "123456", |  | ||||||
| 			Script: []string{"whoami"}, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	err := plugin.Exec() |  | ||||||
| 	assert.NotNil(t, err) |  | ||||||
| 	assert.Equal(t, failParsePrivateKey, err.Error()) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestSSHScriptFromRawKey(t *testing.T) { | func TestSSHScriptFromRawKey(t *testing.T) { | ||||||
| @ -103,38 +86,6 @@ ib4KbP5ovZlrjL++akMQ7V2fHzuQIFWnCkDA5c2ZAqzlM+ZN+HRG7gWur7Bt4XH1 | |||||||
| 	assert.Nil(t, err) | 	assert.Nil(t, err) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestWrongKeyPath(t *testing.T) { |  | ||||||
| 	plugin := Plugin{ |  | ||||||
| 		Config: Config{ |  | ||||||
| 			Host:    []string{"localhost"}, |  | ||||||
| 			User:    "drone-scp", |  | ||||||
| 			Port:    22, |  | ||||||
| 			KeyPath: "/appleboy", |  | ||||||
| 			Script:  []string{"whoami"}, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	err := plugin.Exec() |  | ||||||
| 	assert.NotNil(t, err) |  | ||||||
| 	assert.Equal(t, "open /appleboy: no such file or directory", err.Error()) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestWrongKeyFormat(t *testing.T) { |  | ||||||
| 	plugin := Plugin{ |  | ||||||
| 		Config: Config{ |  | ||||||
| 			Host:    []string{"localhost"}, |  | ||||||
| 			User:    "drone-scp", |  | ||||||
| 			Port:    22, |  | ||||||
| 			KeyPath: "./tests/.ssh/id_rsa.pub", |  | ||||||
| 			Script:  []string{"whoami"}, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	err := plugin.Exec() |  | ||||||
| 	assert.NotNil(t, err) |  | ||||||
| 	assert.Equal(t, sshKeyNotFound, err.Error()) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestSSHScriptFromKeyFile(t *testing.T) { | func TestSSHScriptFromKeyFile(t *testing.T) { | ||||||
| 	plugin := Plugin{ | 	plugin := Plugin{ | ||||||
| 		Config: Config{ | 		Config: Config{ | ||||||
| @ -142,26 +93,10 @@ func TestSSHScriptFromKeyFile(t *testing.T) { | |||||||
| 			User:    "drone-scp", | 			User:    "drone-scp", | ||||||
| 			Port:    22, | 			Port:    22, | ||||||
| 			KeyPath: "./tests/.ssh/id_rsa", | 			KeyPath: "./tests/.ssh/id_rsa", | ||||||
| 			Script:  []string{"whoami"}, | 			Script:  []string{"whoami", "ls -al"}, | ||||||
| 			Sleep:   1, |  | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	err := plugin.Exec() | 	err := plugin.Exec() | ||||||
| 	assert.Nil(t, err) | 	assert.Nil(t, err) | ||||||
| } | } | ||||||
| 
 |  | ||||||
| func TestSSHScriptRunError(t *testing.T) { |  | ||||||
| 	plugin := Plugin{ |  | ||||||
| 		Config: Config{ |  | ||||||
| 			Host:    []string{"localhost"}, |  | ||||||
| 			User:    "drone-scp", |  | ||||||
| 			Port:    22, |  | ||||||
| 			KeyPath: "./tests/.ssh/id_rsa", |  | ||||||
| 			Script:  []string{"whoami", "whoam"}, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	err := plugin.Exec() |  | ||||||
| 	assert.NotNil(t, err) |  | ||||||
| } |  | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Bo-Yi Wu
						Bo-Yi Wu