connection

package
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 21, 2026 License: MPL-2.0 Imports: 23 Imported by: 0

Documentation

Overview

Package connection defines the transport abstraction the typed Hyper-V client (internal/hyperv) uses to ship PowerShell scripts to a Windows host and read their results back. Three backends are implemented: local (exec pwsh.exe directly), ssh (golang.org/x/crypto/ssh), and winrm (github.com/masterzen/winrm).

Contract highlights: script body via -EncodedCommand, stdin for data, stderr has CLIXML progress noise stripped before reaching the Result. Per-call cost is dominated by PowerShell startup, not transport.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnreachable means the transport could not reach the host (DNS,
	// TCP, TLS handshake, auth failure). Resource code typically retries.
	ErrUnreachable = errors.New("transport unreachable")

	// ErrTimeout means the call exceeded its context deadline. Distinct
	// from ErrUnreachable so callers can decide whether to retry vs.
	// surface as `timeouts.Diagnostics`.
	ErrTimeout = errors.New("transport timeout")

	// ErrSessionDropped means the remote command's session ended without
	// signalling an exit status -- typically because the SSH/WinRM
	// channel was torn down mid-command. The wrapped underlying error
	// is `*ssh.ExitMissingError` ("wait: remote command exited without
	// exit status or exit signal") on the SSH backend.
	//
	// Distinct from ErrUnreachable (which fires on connection setup)
	// and ErrTimeout (which fires on the operator's deadline). This
	// signals the cmdlet may have completed on the host and the
	// response got stranded; typed-client methods that know their
	// cmdlet is idempotent (Remove-VMSwitch on a switch already gone
	// is a no-op) can verify post-drop with a follow-up Get rather
	// than surface a false failure. The canonical case is destroying
	// an External hyperv_virtual_switch over an SSH connection that
	// traverses the switch's vEthernet -- the cmdlet succeeds, the
	// host re-binds the NIC, and the SSH session blinks long enough
	// that session.Wait() returns ExitMissingError before the exit
	// status reaches the runner.
	ErrSessionDropped = errors.New("transport session dropped before exit status")

	// ErrUnsupportedBackend is returned by the backend selector when the
	// requested backend identifier is not yet implemented. Removed once
	// SSH/WinRM backends ship.
	ErrUnsupportedBackend = errors.New("backend not implemented")
)

Functions

This section is empty.

Types

type Connection

type Connection interface {
	Runner

	// Open establishes any persistent state the backend needs (an SSH
	// client, a pooled HTTP transport, etc.). Local is stateless and
	// returns nil.
	Open(ctx context.Context) error

	// Close releases the backend's persistent state. Idempotent. Local
	// is a no-op.
	Close() error

	// Healthcheck returns nil if the backend can reach the host and run
	// a trivial command. Used at provider Configure time to fail fast on
	// misconfiguration.
	Healthcheck(ctx context.Context) error

	// Backend returns the lowercase identifier of the implementation —
	// "local" | "ssh" | "winrm". Used for tflog field decoration; the
	// schema's `backend` attribute is the user-facing form.
	Backend() string
}

Connection is the abstract transport. Each provider instance holds a single Connection per (backend, host, user) tuple. Resources never reach for a new one; they share the configured Connection via the typed client in internal/hyperv.

func NewLocal

func NewLocal(opts LocalOptions) (Connection, error)

NewLocal returns a Connection backed by a local pwsh / powershell.exe process per call. Opens nothing — the local backend is stateless.

func NewSSH

func NewSSH(opts SSHOptions) (Connection, error)

NewSSH builds a Connection backed by ssh.Client. It resolves authentication methods and the known_hosts callback up-front so misconfiguration surfaces before Open dials. The client itself is established lazily by Open.

func NewWinRM added in v0.2.0

func NewWinRM(opts WinRMOptions) (Connection, error)

NewWinRM builds a Connection backed by masterzen/winrm. Validates required fields and applies defaults so callers get a fully-resolved backend; the actual HTTP client is constructed lazily by Open so a unit test that only exercises NewWinRM doesn't pay the construction cost.

Auth methods supported: ntlm (default), basic, kerberos.

Kerberos uses jcmturner/gokrb5 under the hood (pure-Go MIT Kerberos, no GSSAPI library on the runner) and supports two credential modes: password (inline AS-REQ) or ccache (re-use an existing TGT). The caller picks the mode by setting Password or KrbCCachePath; setting both, or neither, is rejected.

type LocalOptions

type LocalOptions struct {
	// PwshPath, if non-empty, is used as-is. Set via the provider's
	// `local.pwsh_path` attribute or the HYPERV_PWSH_PATH env var.
	PwshPath string
}

LocalOptions configures the local backend. Empty values mean "discover from PATH": prefer pwsh (faster cold start), fall back to powershell.exe.

type Result

type Result struct {
	Stdout   []byte
	Stderr   []byte
	ExitCode int
	Duration time.Duration
}

Result is what every script invocation returns. The transport layer captures four pieces of information; the typed Hyper-V client maps them into typed Go errors.

`Stderr` has CLIXML progress noise stripped before reaching this struct. Real PS errors arrive as a JSON envelope on stderr per the Write-HypervError contract.

`error` from RunScript is reserved for transport failures (connection refused, auth failed, ctx canceled). PS-level failures come back via `ExitCode != 0` plus the structured envelope on `Stderr`.

type Runner

type Runner interface {
	RunScript(ctx context.Context, script string, stdinJSON []byte) (Result, error)

	// StreamFile copies the bytes at localPath on the runner to
	// remotePath on the host. Both paths are absolute. Backends create
	// (or truncate) the destination file and write streamed bytes —
	// nothing is buffered in memory beyond the backend's transport
	// chunk size.
	//
	// No SHA-256 verification is performed at this layer: the typed
	// client (internal/hyperv) computes the local hash before calling
	// StreamFile and verifies the remote hash via the existing
	// image_file/get.ps1 path after the destination rename. Keeping
	// the primitive a pure bytes-copy lets each backend pick its own
	// optimal transport without dragging hashing into the protocol.
	//
	// Cancellation through ctx interrupts the stream; partial files at
	// the remote path are the caller's problem to clean up. Resources
	// that need atomicity stage to a `.part` sibling and rename, the
	// same pattern image_file's url-mode uses.
	StreamFile(ctx context.Context, localPath, remotePath string) error
}

Runner is the narrowest useful interface — just "run a script, get a result." Connection composes Runner with lifecycle methods (Open/Close/ Healthcheck) that backends with persistent state need.

The split exists so unit tests can implement just Runner via the fake in internal/testutil, without faking lifecycle calls that don't matter for the typed-client tests.

Calling convention:

  • `script` is the full PowerShell body. The caller has already concatenated common/preamble.ps1 to the top. Backends transmit it as UTF-16LE base64 via -EncodedCommand. **Never** via stdin or as a command-line argument — multi-line scripts get mis-parsed otherwise.

  • `stdinJSON` is structured input. Empty for scripts that don't need input. Backends pipe these bytes to the PS process's stdin. Scripts read with `$input_json = $Input | ConvertFrom-Json -Compress`.

  • The returned `Result` carries the four useful streams. The error return is reserved for transport-level failures (connection refused, ctx canceled). Non-zero `ExitCode` is the application-level signal.

type SSHOptions

type SSHOptions struct {
	Host     string // required
	Port     int    // default 22
	Username string // required

	// Auth methods, in priority order:
	//
	//   1. PrivateKey       (raw key contents -- wins if both PrivateKey and PrivateKeyPath are set)
	//   2. PrivateKeyPath   (path read at Open time)
	//   3. Password         (fallback only -- key auth is preferred)
	//
	// Passphrase decrypts the key when set.
	//
	// All three sensitive fields use []byte so Close() can zero the
	// long-lived copy held by the backend. Libraries we hand the value
	// to (golang.org/x/crypto/ssh) make their own copies; zeroing here
	// covers the provider's own state, not theirs.
	PrivateKey     []byte
	PrivateKeyPath string
	Passphrase     []byte
	Password       []byte

	// KnownHostsPath is the file used for host key verification. Default:
	// ~/.ssh/known_hosts. Empty path falls back to the default. A missing
	// file is a hard error -- silently disabling host-key checking would be
	// a security regression.
	KnownHostsPath string

	// Timeout is the dial timeout. Default 30s.
	Timeout time.Duration

	// CommandTimeout bounds an individual RunScript call. Default
	// 5m. A wedged remote cmdlet surfaces as ErrTimeout instead of
	// blocking the whole apply. Set to 0 to disable.
	CommandTimeout time.Duration

	// KeepaliveInterval is how often the backend sends an SSH
	// keepalive request while the persistent client is open.
	// Default 30s. Prevents NAT/firewall mid-apply drops. Set to
	// 0 to disable.
	KeepaliveInterval time.Duration

	// PwshPath is the binary the remote shell invokes per call. Default:
	// "powershell.exe" -- universally available on Windows. Set to "pwsh"
	// or "pwsh.exe" to prefer PS 7+ if installed.
	PwshPath string

	// MaxConcurrentSessions caps the number of in-flight RunScript calls
	// against the persistent ssh.Client. OpenSSH's per-connection
	// MaxSessions limit (default 10, often 4-6 on hardened Windows
	// builds) rejects late session opens with "rejected: connect failed
	// (open failed)" once exceeded. Default 4 stays well under typical
	// Windows OpenSSH caps; raise it on hosts with sshd_config tuned
	// upward, lower it if the bench surfaces the error under heavier
	// fanout. Set to 0 to use the default; negative values disable the
	// cap entirely (not recommended).
	MaxConcurrentSessions int
}

SSHOptions configures the SSH backend. The provider's Configure pass resolves env vars + provider attributes into this struct (see internal/provider/backend_select.go).

type WinRMOptions added in v0.2.0

type WinRMOptions struct {
	Host     string // required
	Port     int    // default 5986 (HTTPS) or 5985 (HTTP)
	Username string // required
	// Password is []byte so Close() can zero the long-lived copy held
	// by the backend. masterzen/winrm copies the value into its own
	// EndpointParams when we construct the client; that copy is outside
	// our reach, but our state stays clean across the connection's life.
	Password []byte // required for ntlm/basic; for kerberos, mutually exclusive with KrbCCachePath

	UseHTTPS bool   // default true; flip to false only for diagnosing TLS-only failures
	Insecure bool   // skip TLS certificate verification (default false)
	Auth     string // "ntlm" | "basic" | "kerberos"; default "ntlm"
	CACert   string // path to a CA bundle PEM; empty = system roots

	// Kerberos auth fields. Only meaningful when Auth=="kerberos"; ignored
	// otherwise. The provider-config layer is responsible for catching the
	// "kerberos fields set without auth=kerberos" misconfig at plan time;
	// this struct just transports the values.
	//
	// KrbRealm is required (NewWinRM rejects empty when Auth=="kerberos").
	// KrbSpn defaults to "HTTP/<Host>" when empty.
	// KrbConfigPath defaults to first-existing of $KRB5_CONFIG,
	// ~/.config/krb5.conf, /etc/krb5.conf when empty.
	// KrbCCachePath, when set, switches from password-mode (inline AS-REQ)
	// to ccache-mode (re-use a pre-existing TGT). When set, Password is
	// ignored.
	KrbRealm      string
	KrbSpn        string
	KrbConfigPath string
	KrbCCachePath string

	// Timeout sets `http.Client.Timeout` for every WSMan request.
	// Default 0 (no wall-clock cap) -- file transfers can run for
	// arbitrarily long. The initial TCP dial is bounded by the OS;
	// a connection that stalls mid-transfer (host freezes, packets
	// silently dropped) holds the apply until ctx cancellation
	// (operator Ctrl+C).
	Timeout time.Duration

	// CommandTimeout bounds a single RunScript call. Default 5m. A wedged
	// remote cmdlet surfaces as ErrTimeout instead of blocking the whole
	// apply. Set to 0 to disable.
	CommandTimeout time.Duration

	// PwshPath is the binary the remote shell invokes per call. Default:
	// "powershell.exe" -- universally available on Windows. Set to "pwsh"
	// or "pwsh.exe" to prefer PS 7+ if installed.
	PwshPath string
}

WinRMOptions configures the WinRM backend. The provider's Configure pass resolves env vars + provider attributes into this struct (see internal/provider/backend_select.go).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL