| 1 | package legatus |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "io" |
| 6 | "io/fs" |
| 7 | ) |
| 8 | |
| 9 | // FileMode is the permission mode used when writing remote files. |
| 10 | type FileMode = fs.FileMode |
| 11 | |
| 12 | // Cmd is a remote command. Args are passed structurally: a transport must |
| 13 | // quote each element so the remote shell can never re-split or re-evaluate it. |
| 14 | type Cmd struct { |
| 15 | Path string |
| 16 | Args []string |
| 17 | Env map[string]string |
| 18 | Sudo bool |
| 19 | Stdin io.Reader |
| 20 | } |
| 21 | |
| 22 | // ExecResult is the outcome of a remote command. |
| 23 | type ExecResult struct { |
| 24 | Stdout []byte |
| 25 | Stderr []byte |
| 26 | ExitCode int |
| 27 | } |
| 28 | |
| 29 | // Combined returns stdout followed by stderr. |
| 30 | func (r ExecResult) Combined() []byte { |
| 31 | out := make([]byte, 0, len(r.Stdout)+len(r.Stderr)) |
| 32 | out = append(out, r.Stdout...) |
| 33 | out = append(out, r.Stderr...) |
| 34 | return out |
| 35 | } |
| 36 | |
| 37 | // PutOptions configures a file upload. |
| 38 | type PutOptions struct { |
| 39 | Sudo bool |
| 40 | Owner string |
| 41 | } |
| 42 | |
| 43 | // PutOption mutates PutOptions. |
| 44 | type PutOption func(*PutOptions) |
| 45 | |
| 46 | // WithPutSudo stages the upload and moves it into place with sudo. |
| 47 | func WithPutSudo() PutOption { return func(o *PutOptions) { o.Sudo = true } } |
| 48 | |
| 49 | // WithPutOwner chowns the uploaded file to owner. |
| 50 | func WithPutOwner(owner string) PutOption { return func(o *PutOptions) { o.Owner = owner } } |
| 51 | |
| 52 | // Conn is a live connection to one host. Transports implement it; the core |
| 53 | // engine depends only on this interface and never imports SSH (IV1, PC1). |
| 54 | type Conn interface { |
| 55 | // Exec runs cmd to completion. A command that runs is not an error even on a |
| 56 | // non-zero exit: the status is reported via ExecResult.ExitCode and err is |
| 57 | // nil. err is reserved for failures to run the command at all (no session, |
| 58 | // IO error, context cancellation). Callers decide whether a non-zero exit is |
| 59 | // a failure (a step's Apply) or a signal (a probe like `test -e`). |
| 60 | Exec(ctx context.Context, cmd Cmd) (ExecResult, error) |
| 61 | // ExecStream runs cmd like Exec but streams output: a non-nil stdout/stderr |
| 62 | // writer receives that stream live and the matching ExecResult field is left |
| 63 | // nil; a nil writer buffers that stream into ExecResult exactly as Exec does, |
| 64 | // so a caller can stream one stream while capturing the other. The exit-code |
| 65 | // contract is identical to Exec — a clean run with a non-zero exit returns |
| 66 | // err==nil with ExecResult.ExitCode set; ctx cancellation tears down the |
| 67 | // process and returns ctx.Err() with bytes-so-far already written to the sink. |
| 68 | ExecStream(ctx context.Context, cmd Cmd, stdout, stderr io.Writer) (ExecResult, error) |
| 69 | Put(ctx context.Context, content io.Reader, remote string, mode FileMode, opts ...PutOption) error |
| 70 | Get(ctx context.Context, remote string, w io.Writer) error |
| 71 | Close() error |
| 72 | } |
| 73 | |
| 74 | // Dialer opens a Conn to a host. transport provides the SSH implementation. |
| 75 | type Dialer interface { |
| 76 | Dial(ctx context.Context, h *Host) (Conn, error) |
| 77 | } |
| 78 | |