Documentation
¶
Overview ¶
Package transport implements remote command execution — the low-level primitives an orchestration tool's task/module logic runs commands and moves files through, whether the target is the control node itself (Local), a Unix-like host over SSH (SSH), or a Windows host over WinRM (WinRM) — plus privilege escalation (Become: sudo/su/doas) as a decorator over any of them.
It is deliberately scoped to these primitives and nothing tool-specific: no playbooks, no catalogs, no task manifests. Ansible's modules and Puppet Bolt's tasks both reduce, in the end, to "run this command" and "put this file there" — this package is that reduction, shared instead of reimplemented per tool.
Index ¶
- type BecomeConfig
- type BecomeMethod
- type Connection
- type HTTPDoer
- type Local
- func (l *Local) Close() error
- func (l *Local) Exec(ctx context.Context, cmd string, stdin io.Reader) (Result, error)
- func (l *Local) Fetch(ctx context.Context, remotePath, localPath string) error
- func (l *Local) NewSession(ctx context.Context) (Session, error)
- func (l *Local) Put(ctx context.Context, localPath, remotePath string, opts PutOptions) error
- func (l *Local) Remove(ctx context.Context, remotePath string) error
- func (l *Local) TempPath(base string) string
- type PutOptions
- type Result
- type SSH
- func (s *SSH) Close() error
- func (s *SSH) Exec(ctx context.Context, cmd string, stdin io.Reader) (Result, error)
- func (s *SSH) Fetch(ctx context.Context, remotePath, localPath string) error
- func (s *SSH) NewSession(ctx context.Context) (Session, error)
- func (s *SSH) Put(ctx context.Context, localPath, remotePath string, opts PutOptions) error
- func (s *SSH) Remove(ctx context.Context, remotePath string) error
- func (s *SSH) TempPath(base string) string
- type SSHConfig
- type Session
- type Streamer
- type WinRM
- func (w *WinRM) Close() error
- func (w *WinRM) Exec(ctx context.Context, cmd string, stdin io.Reader) (Result, error)
- func (w *WinRM) ExecArgv(ctx context.Context, command string, args []string, stdin io.Reader) (Result, error)
- func (w *WinRM) Fetch(ctx context.Context, remotePath, localPath string) error
- func (w *WinRM) Put(ctx context.Context, localPath, remotePath string, opts PutOptions) error
- func (w *WinRM) Remove(ctx context.Context, remotePath string) error
- func (w *WinRM) TempPath(base string) string
- type WinRMConfig
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type BecomeConfig ¶
type BecomeConfig struct {
Method BecomeMethod // default BecomeSudo
User string // default "root"
Password string // optional; empty assumes passwordless (NOPASSWD sudoers, or doas persist)
}
BecomeConfig configures privilege escalation, Ansible's `become:`.
type BecomeMethod ¶
type BecomeMethod string
BecomeMethod names a privilege-escalation program.
const ( BecomeSudo BecomeMethod = "sudo" BecomeSu BecomeMethod = "su" BecomeDoas BecomeMethod = "doas" )
type Connection ¶
type Connection interface {
// Exec runs cmd through the target's shell (one command line, not an
// argv — matching how both Ansible and Bolt build task invocations)
// and returns its captured output and exit code. A non-zero exit is
// not itself an error; err is only for a failure to run the command
// at all. stdin, if non-nil, is streamed to the command's standard
// input — how a become/run-as password reaches sudo/su without ever
// appearing in argv or an environment variable.
Exec(ctx context.Context, cmd string, stdin io.Reader) (Result, error)
// Put copies the local file at localPath to remotePath on the
// target.
Put(ctx context.Context, localPath, remotePath string, opts PutOptions) error
// Fetch copies the file at remotePath on the target to the local
// file at localPath.
Fetch(ctx context.Context, remotePath, localPath string) error
// Remove deletes remotePath on the target. Removing a path that
// does not exist is not an error.
Remove(ctx context.Context, remotePath string) error
// TempPath returns a fresh path under the target's temp directory
// ending in base, using the target's own path syntax (forward
// slashes for Local/SSH, backslashes under WinRM).
TempPath(base string) string
// Close releases any resources (an SSH/WinRM connection, an open
// shell).
Close() error
}
Connection is how a caller reaches its target: run a shell command, move files in either direction, and clean up after itself. Every task/module operation is expressed in terms of these primitives so the same higher-level logic runs unchanged whether the connection is local, SSH, or WinRM.
func Become ¶
func Become(conn Connection, cfg BecomeConfig) Connection
Become wraps conn so every Exec runs as BecomeConfig.User via the configured escalation method. Put/Fetch/Close pass through unchanged (Ansible's own become plumbing only affects command execution, not file transfer, since the transferred file is written by the connection's own user and then chmod/chowned by an escalated task).
Known gap: the wrapper returned here does not implement Streamer, even when conn does. Exec's become handling works by printing a random marker after escalation succeeds and slicing everything before it out of the buffered result (see successMarker below) — doing the equivalent safely on a live stream means scanning the stdout stream for that marker in real time and only starting to hand bytes to the caller once it has been seen, while still correctly forwarding a become password to the escalation program's stdin ahead of the wrapped command's own stdin. That is buildable, but not something to get subtly wrong under time pressure, so it is deliberately left undone rather than shipped half-right: conn.(transport.Streamer) on a Become-wrapped connection fails the type assertion, same as WinRM. Callers needing both become and a live interactive session need to wait for a follow-up that implements this deliberately, or run their interactive session unprivileged.
type HTTPDoer ¶ added in v0.1.3
HTTPDoer is the seam through which WinRM performs HTTP requests. The default implementation is an *http.Client; tests inject a fake.
type Local ¶
type Local struct {
// Shell is the interpreter commands are run through. Defaults to
// "sh" resolved via PATH.
Shell string
// TempDir is where TempPath builds paths under. Defaults to
// os.TempDir().
TempDir string
}
Local is the "local" connection: the target IS the control node. Running the command through a real shell (rather than an argv exec.Command) is not a shortcut — it's what a local task/module runner needs, since a command's args are shell syntax (pipes, redirects, globs) that only a shell interprets.
func NewLocal ¶
func NewLocal() *Local
NewLocal returns a Local connection using "sh" resolved via PATH.
This is deliberately the bare name "sh", not the absolute path "/bin/sh": os/exec's Windows LookPath only recognizes "\" and ":" as path separators, not "/", so an absolute-looking POSIX path like "/bin/sh" is instead treated as a literal bare command name and searched for on %PATH% — where it can never exist — failing every local exec on Windows with "executable file not found in %PATH%". A bare "sh" resolves correctly on both POSIX (finds /bin/sh or /usr/bin/sh via PATH) and Windows (finds Git for Windows' sh.exe, which GitHub's windows-latest runners — and most developer machines with Git installed — already have on PATH).
func (*Local) NewSession ¶ added in v0.1.4
NewSession opens a live streaming session for a local command, implementing Streamer. The command itself is not known until Start, so this builds the *exec.Cmd now with an empty placeholder "-c" argument, which Start fills in later — letting StdinPipe/StdoutPipe/StderrPipe use os/exec's own Cmd.StdinPipe/StdoutPipe/StderrPipe (real OS pipes) instead of a manually bridged io.Pipe.
That distinction matters, and cost a real deadlock to learn: assigning an arbitrary io.Reader/io.Writer to Cmd.Stdin/Stdout/Stderr makes os/exec spawn its own internal copy goroutine, and Cmd.Wait blocks until every one of those finishes — including the stdin one, which only sees io.EOF once the write end is closed. A caller that requests StdinPipe (as any real interactive session does, to be able to forward keystrokes) but has no need to write anything — a command that doesn't read stdin, or a caller closing the session on an unrelated event before ever touching stdin — would then hang in Wait forever, since nothing else ever closes that write end. Cmd's own *Pipe methods don't have this problem: they hand back a real OS pipe end directly, with no bridging goroutine for Wait to wait on.
type PutOptions ¶
type PutOptions struct {
// Executable chmods the remote file +x after writing (POSIX targets
// only; WinRM implementations ignore it — Windows has no exec bit).
Executable bool
// MkdirParents creates the destination's parent directory first.
MkdirParents bool
}
PutOptions configures a file upload.
type SSH ¶
type SSH struct {
// contains filtered or unexported fields
}
SSH is a live SSH connection to one target host. Every Exec opens its own session, matching how SSH sessions work (one command per session).
func DialSSH ¶
DialSSH connects and authenticates to cfg.Host, trying (in order) explicit password, private key, and ssh-agent — whichever of those cfg populates.
func (*SSH) NewSession ¶ added in v0.1.4
NewSession opens a live streaming session over a fresh *ssh.Session on the already-dialed client, implementing Streamer. TTY is honored the same best-effort way Exec honors it: a RequestPty before Start, ignored if the server refuses it.
type SSHConfig ¶
type SSHConfig struct {
Host string
Port int // default 22
User string
Password string // optional
PrivateKeyFile string // optional, e.g. ~/.ssh/id_ed25519
PrivateKeyBytes []byte // optional, an already-loaded key (e.g. from inventory)
PrivateKeyPassphrase string
UseAgent bool // authenticate via SSH_AUTH_SOCK
// HostKeyCheck: when true (the default), the remote host key is
// verified against KnownHostsFile (default ~/.ssh/known_hosts) and
// the connection is refused on any mismatch or unknown host.
HostKeyCheck bool
KnownHostsFile string
// TempDir is the remote directory TempPath builds paths under.
// Defaults to /tmp.
TempDir string
// TTY requests a pseudo-terminal for every Exec (best-effort: a
// server that refuses it does not fail the command).
TTY bool
Timeout time.Duration // default 30s
// Dialer establishes the transport-level TCP connection. Defaults
// to net.Dialer honoring Timeout. Tests inject a dialer that reaches
// an in-process server.
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
}
SSHConfig configures an SSH connection to a Unix-like remote host.
type Session ¶ added in v0.1.4
type Session interface {
// StdinPipe returns a writer for the session's standard input. Call
// before Start.
StdinPipe() (io.WriteCloser, error)
// StdoutPipe returns a reader streaming standard output as it is
// produced. Call before Start.
StdoutPipe() (io.Reader, error)
// StderrPipe returns a reader streaming standard error as it is
// produced. Call before Start.
StderrPipe() (io.Reader, error)
// Start begins running cmd. Non-blocking.
Start(cmd string) error
// Wait blocks until the command exits and returns its exit code. A
// non-zero exit is not itself an error; err is only for a failure to
// wait on the command at all.
Wait() (int, error)
// Close releases the session's resources, terminating the remote
// process if Wait has not yet returned. Safe to call after Wait, and
// safe to call more than once.
Close() error
}
Session is a live, streaming command execution — the interactive counterpart to Connection.Exec's single buffered call. Where Exec hands back one Result after the command has already finished, a Session lets the caller drive its stdin/stdout/stderr directly as plain pipes while the command runs: an interactive multi-host shell needs to forward keystrokes to a remote process and show its output as it is produced, not after the fact. Nothing is buffered by the library — the caller owns pacing and framing.
The usual sequence is: request the pipes you need, Start the command, pump stdin/stdout/stderr concurrently, then Wait for the exit code. Close releases the session's resources at any point, terminating the remote process if Wait has not yet returned.
type Streamer ¶ added in v0.1.4
Streamer is implemented by a Connection that can also open a live Session for callers needing real-time interactivity instead of Exec's buffered result. Local and SSH implement it.
WinRM does not: WS-Management's Command/Send/Receive shell protocol is poll-based (Receive returns whatever output has accumulated since the last poll, not a continuous stream), so it does not map onto the continuous-pipe shape Session assumes without either fabricating fake liveness or accepting periodic stalls that would silently degrade what "streaming" promises. Left unimplemented rather than shipped half-true.
Become-wrapped connections also do not implement Streamer, for a narrower reason: see Become's doc comment.
Callers should type-assert (conn.(transport.Streamer)) and handle absence explicitly, not assume every Connection supports it.
type WinRM ¶
type WinRM struct {
// contains filtered or unexported fields
}
WinRM is a live WinRM connection to one target host: one WS-Man shell, opened at Dial time and reused across every Exec/Put/Fetch/Remove until Close.
func DialWinRM ¶
func DialWinRM(ctx context.Context, cfg WinRMConfig) (*WinRM, error)
DialWinRM opens a WS-Man shell on cfg.Host.
func (*WinRM) Exec ¶
Exec runs cmd through cmd.exe /c on the target's open shell. cmd is parsed by cmd.exe itself, with its own quoting rules; a caller that already has a program and a clean argv (no cmd.exe requoting wanted — e.g. running an uploaded executable with arguments that may contain spaces or quotes) should use WinRM.ExecArgv instead.
func (*WinRM) ExecArgv ¶ added in v0.1.2
func (w *WinRM) ExecArgv(ctx context.Context, command string, args []string, stdin io.Reader) (Result, error)
ExecArgv runs command with args as the WS-Man protocol's own Command and Arguments elements — no cmd.exe requoting layer, so each argument reaches the target byte for byte regardless of embedded spaces or quotes. Prefer this over Exec whenever the caller already has a program and a clean argv.
func (*WinRM) Fetch ¶
Fetch reads remotePath by streaming its base64 encoding to stdout via PowerShell.
type WinRMConfig ¶
type WinRMConfig struct {
Host string
Port int // default 5986 (SSL) or 5985
User string
Password string
SSL bool // default true
SSLVerify bool // default true
CACert string // path to a CA certificate PEM (custom trust root)
// Transport selects the auth scheme: "negotiate" (default), "basic",
// or "ssl" (TLS client certificate — set ClientCert/ClientKey).
Transport string
ClientCert string
ClientKey string
ConnectTimeout time.Duration // default 60s
TempDir string // default `C:\Windows\Temp`
Path string // WS-Man endpoint path, default "/wsman"
// Environment is set on the shell at creation time (the WS-Man
// protocol's own Environment block), so every Exec/ExecArgv on this
// connection sees these variables — e.g. Bolt's PT_-prefixed task
// parameters for the "environment" input method.
Environment map[string]string
// NewDoer builds the HTTP client used to reach the target. Defaults
// to buildWinRMClient(cfg). Tests inject a doer that reaches an
// in-process WS-Man server.
NewDoer func(WinRMConfig) (HTTPDoer, error)
}
WinRMConfig configures a WinRM connection to a Windows remote host. It speaks WS-Management (SOAP 1.2 with WS-Addressing over HTTP/HTTPS) driving the MS-WSMV shell protocol: Create shell -> Command -> Send (stdin) -> Receive (stdout/stderr/CommandState/ExitCode) -> Signal (terminate) -> Delete shell.
Authentication is selected by Transport: "basic" sends HTTP Basic credentials, "negotiate" (the default) performs NTLM via the pure-Go github.com/Azure/go-ntlmssp round-tripper, and "ssl" uses TLS client-certificate authentication. Kerberos is not implemented.