package ssh import ( "context" "fmt" "io" "os" "strings" "github.com/melbahja/goph" "github.com/woodpecker-ci/woodpecker/pipeline/backend/common" "github.com/woodpecker-ci/woodpecker/pipeline/backend/types" "github.com/woodpecker-ci/woodpecker/shared/constant" ) type ssh struct { cmd *goph.Cmd output io.ReadCloser client *goph.Client workingdir string } type readCloser struct { io.Reader } func (c readCloser) Close() error { return nil } // make sure local implements Engine var _ types.Engine = &ssh{} // New returns a new ssh Engine. func New() types.Engine { return &ssh{} } func (e *ssh) Name() string { return "ssh" } func (e *ssh) IsAvailable() bool { return os.Getenv("WOODPECKER_BACKEND_SSH_KEY") != "" || os.Getenv("WOODPECKER_BACKEND_SSH_PASSWORD") != "" } func (e *ssh) Load() error { cmd, err := e.client.Command("/bin/env", "mktemp", "-d", "-p", "/tmp", "woodpecker-ssh-XXXXXXXXXX") if err != nil { return err } dir, err := cmd.Output() if err != nil { return err } e.workingdir = string(dir) address := os.Getenv("WOODPECKER_BACKEND_SSH_ADDRESS") if address == "" { return fmt.Errorf("missing SSH address") } user := os.Getenv("WOODPECKER_BACKEND_SSH_USER") if user == "" { return fmt.Errorf("missing SSH user") } var auth goph.Auth if file, has := os.LookupEnv("WOODPECKER_BACKEND_SSH_KEY"); has { var err error auth, err = goph.Key(file, os.Getenv("WOODPECKER_BACKEND_SSH_KEY_PASSWORD")) if err != nil { return err } } else { auth = goph.Password(os.Getenv("WOODPECKER_BACKEND_SSH_PASSWORD")) } client, err := goph.New(user, address, auth) if err != nil { return err } e.client = client return nil } // Setup the pipeline environment. func (e *ssh) Setup(ctx context.Context, config *types.Config) error { return nil } // Exec the pipeline step. func (e *ssh) Exec(ctx context.Context, step *types.Step) error { // Get environment variables command := []string{} for a, b := range step.Environment { if a != "HOME" && a != "SHELL" { // Don't override $HOME and $SHELL command = append(command, a+"="+b) } } if step.Image == constant.DefaultCloneImage { // Default clone step command = append(command, "CI_WORKSPACE="+e.workingdir+"/"+step.Environment["CI_REPO"]) command = append(command, "plugin-git") } else { // Use "image name" as run command command = append(command, step.Image) command = append(command, "-c") // TODO: use commands directly script := common.GenerateScript(step.Commands) // Deleting the initial lines removes netrc support but adds compatibility for more shells like fish command = append(command, "cd "+e.workingdir+"/"+step.Environment["CI_REPO"]+" && "+script[strings.Index(script, "\n\n")+2:]) } // Prepare command var err error e.cmd, err = e.client.CommandContext(ctx, "/bin/env", command...) if err != nil { return err } // Get output and redirect Stderr to Stdout std, _ := e.cmd.StdoutPipe() e.output = readCloser{std} e.cmd.Stderr = e.cmd.Stdout return e.cmd.Start() } // Wait for the pipeline step to complete and returns // the completion results. func (e *ssh) Wait(context.Context, *types.Step) (*types.State, error) { return &types.State{ Exited: true, }, e.cmd.Wait() } // Tail the pipeline step logs. func (e *ssh) Tail(context.Context, *types.Step) (io.ReadCloser, error) { return e.output, nil } // Destroy the pipeline environment. func (e *ssh) Destroy(context.Context, *types.Config) error { e.client.Close() sftp, err := e.client.NewSftp() if err != nil { return err } return sftp.RemoveDirectory(e.workingdir) }