sidecar

package
v0.7.162 Latest Latest
Warning

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

Go to latest
Published: Aug 28, 2026 License: MIT Imports: 37 Imported by: 0

Documentation

Index

Constants

View Source
const StaleAfter = 5 * 24 * time.Hour

StaleAfter is how long a sidecar's state file may go untouched before the sidecar is treated as abandoned and deleted.

Every save rewrites the state file, so its mtime is the only record of when the sidecar was last used, and a cold file means nobody has used it since. Five days keeps a sidecar alive across a long weekend while still reclaiming spend on the ones whose session was closed and forgotten.

Variables

View Source
var (
	// ErrMutuallyExclusiveKeys indicates both a key string and key file were provided.
	ErrMutuallyExclusiveKeys = errors.New("public key and public key file are mutually exclusive")
	// ErrPublicKeyRequired indicates neither a key string nor key file was provided.
	ErrPublicKeyRequired = errors.New("public key is required")
	// ErrPrivateKeyProvided indicates a private key was given where a public key was expected.
	ErrPrivateKeyProvided = errors.New("provided key is a private key")
	// ErrAuthSockNotSet indicates the SSH agent socket is not configured.
	ErrAuthSockNotSet = errors.New("ssh auth socket not set")
)

Functions

func AddSSHKey

func AddSSHKey(ctx context.Context, client *circleci.Client, sidecarID, publicKey, publicKeyFile string) (*circleci.AddSSHKeyResponse, error)

func AllProjectRoots added in v0.7.138

func AllProjectRoots() ([]string, error)

AllProjectRoots returns the roots of all projects that have ever saved a sidecar state, by reading the breadcrumb files written by SaveActiveTo.

func BundleSync added in v0.7.122

func BundleSync(ctx context.Context,
	client *circleci.Client, sidecarID, identityFile, authSock, workdir, cwd string, status iostream.StatusFunc) error

BundleSync synchronises local commits and working-tree changes to a sidecar using git bundle, without requiring the branch to be pushed to GitHub.

On first sync (no LastSyncedRef) a full bundle of HEAD is sent. On subsequent syncs only commits since the last synced ref are bundled (incremental). In both cases any uncommitted working-tree changes are applied on top as a patch.

When neither the HEAD commit nor the working-tree patch has changed since the previous sync, all remote operations are skipped entirely. When only the working-tree changed (no new commits), a reverse+apply delta is used instead of a full reset+clean+apply, leaving committed files untouched.

func ClearActive

func ClearActive(ctx context.Context) error

ClearActive removes the active sidecar state file.

func ClearActiveByOrg added in v0.7.160

func ClearActiveByOrg(orgID string) (int, error)

ClearActiveByOrg removes all sidecar state files across every known project whose OrgID matches orgID. It is called after a bulk prune so that the TUI does not show deleted sidecars on the next watch tick. Returns the number of files removed. Per-project and per-file failures do not stop the sweep; they are accumulated into the returned error so the caller can warn about a prune that only partially cleared state.

func ClearActiveFrom added in v0.7.38

func ClearActiveFrom(ctx context.Context, dir string) error

ClearActiveFrom removes the active sidecar state file in dir.

func ClearActiveSnapshot added in v0.7.35

func ClearActiveSnapshot(ctx context.Context) error

ClearActiveSnapshot removes the active snapshot state file.

func ClearSnapshotFrom added in v0.7.38

func ClearSnapshotFrom(ctx context.Context, dir string) error

ClearSnapshotFrom removes the active snapshot state file in dir.

func Create

func Create(ctx context.Context, client *circleci.Client, orgID, name, image string) (*circleci.Sidecar, error)

func CurrentBranch added in v0.7.60

func CurrentBranch(root string) string

CurrentBranch returns the current git branch for the repo rooted at root. Returns "" on any error (no git, detached HEAD, etc.).

func DefaultKeyPath

func DefaultKeyPath() (string, error)

DefaultKeyPath returns the default SSH private key path used by chunk.

func Exec

func Exec(
	ctx context.Context, client *circleci.Client, sidecarID, command string, args []string, onOutput circleci.OutputFn,
) (*circleci.ExecResponse, error)

func GenerateKeyPair

func GenerateKeyPair(path string) error

GenerateKeyPair generates an ed25519 keypair and writes the private key to path and the public key to path+".pub". The .ssh directory is created if it does not exist.

func InteractiveShell

func InteractiveShell(ctx context.Context, session *Session, envVars map[string]string) (err error)

InteractiveShell opens an interactive shell session to the sidecar with PTY. It intentionally uses os.Stdin/os.Stdout/os.Stderr directly rather than iostream.Streams: term.MakeRaw and term.GetSize require a real *os.File fd, and PTY I/O must be wired to the process's actual terminal.

func LoadEnvFileAt

func LoadEnvFileAt(path string) (_ map[string]string, err error)

LoadEnvFileAt reads the env file at path. Returns nil, nil if the file does not exist. Returns an error for permission or parse failures.

func MergeEnv

func MergeEnv(layers ...map[string]string) map[string]string

MergeEnv merges maps left to right; later layers win on duplicate keys.

func ParseEnvFile

func ParseEnvFile(r io.Reader) (map[string]string, error)

ParseEnvFile reads dotenv-format KEY=VALUE lines from r. Supports blank lines, # comments, optional "export " prefix, and optional single/double quoting of values. No variable interpolation.

func ParseEnvPairs

func ParseEnvPairs(pairs []string) (map[string]string, error)

ParseEnvPairs parses a slice of KEY=VALUE strings and returns a map. Returns an error if any entry does not contain "=".

func PruneID added in v0.7.144

func PruneID(ctx context.Context, client *circleci.Client, sidecarID string, deleteRemote bool) error

PruneID removes every state file for the current project that names sidecarID, deleting the sidecar through the API first when it still exists.

Removing one file is not enough. Adoption re-keys an ID under the adopting session and branch, and older chunk versions copied rather than moved the file, so the same ID can be recorded two or three times over; dropping a single file leaves a duplicate behind that resurrects the dead ID on the next run.

deleteRemote separates the two ways a sidecar stops being usable. Already deleted (404) leaves nothing to delete. Out of date (410) means it is still running, still costing money, and can never be used again, so it is deleted before its state is dropped.

func ResolveWorkspace added in v0.7.51

func ResolveWorkspace(ctx context.Context, cliWorkdir, repo string) (string, error)

ResolveWorkspace determines the workspace path. Priority: 1. CLI --workdir flag 2. sidecar.json workspace 3. default <sidecarHome>/<repo>. Returns an error if no repo-specific path can be determined (repo empty and no saved workspace), because the bare home dir is not safe to pass to rm -rf.

func SSH

func SSH(ctx context.Context, client *circleci.Client, sidecarID, identityFile, authSock string, args []string, envVars map[string]string, streams iostream.Streams, stdin io.Reader) error

SSH opens a session and either runs a command or starts an interactive shell. stdin is forwarded to the remote command when non-nil; callers should pass os.Stdin when the process stdin is a pipe, nil otherwise.

func SaveActive

func SaveActive(ctx context.Context, a ActiveSidecar) error

SaveActive writes the active sidecar to XDG_DATA_HOME for the current project.

func SaveActiveSnapshot added in v0.7.35

func SaveActiveSnapshot(ctx context.Context, a ActiveSnapshot) error

SaveActiveSnapshot writes the active snapshot to XDG_DATA_HOME for the current project.

func SaveActiveTo added in v0.7.38

func SaveActiveTo(ctx context.Context, dir string, a ActiveSidecar) error

SaveActiveTo writes the active sidecar to dir.

func SaveSnapshotTo added in v0.7.38

func SaveSnapshotTo(ctx context.Context, dir string, a ActiveSnapshot) error

SaveSnapshotTo writes the active snapshot to dir.

func ShellEscape

func ShellEscape(arg string) string

ShellEscape escapes a string for safe use in a POSIX shell single-quoted context.

func ShellJoin

func ShellJoin(args []string) string

ShellJoin joins args into a shell command string with POSIX single-quote escaping.

func StateDir added in v0.7.38

func StateDir() (string, error)

StateDir returns the XDG_DATA_HOME directory for the current project. Callers performing multiple sidecar or snapshot operations can resolve once and pass the result to the dir-accepting variants (LoadActiveFrom, SaveActiveTo, ClearActiveFrom, LoadSnapshotFrom, SaveSnapshotTo, ClearSnapshotFrom) to avoid repeated filesystem walks.

func StateFileName added in v0.7.60

func StateFileName(sessionID, branch string) string

StateFileName returns the sidecar state file name for the given session ID and git branch. Exposed so tests can construct expected paths.

func Sync

func Sync(ctx context.Context,
	client *circleci.Client, sidecarID, identityFile, authSock, workdir string, status iostream.StatusFunc) error

Sync synchronises local changes to a sidecar over SSH. It ensures the workspace base exists, clones the repo into workdir if absent, then resets to the remote base and applies a patch of local changes. workdir overrides the destination path; defaults to /home/user/<repo>.

func SyncEphemeral added in v0.7.147

func SyncEphemeral(ctx context.Context,
	client *circleci.Client, sidecarID, identityFile, authSock, workdir string, status iostream.StatusFunc) error

SyncEphemeral synchronises like Sync but neither reads nor writes the active sidecar file. Callers that drive several sidecars concurrently — mutation variants, one sidecar per variant — would otherwise race on that shared file and leave it naming whichever worker happened to finish last, silently repointing the user's own session at a sidecar that is about to be deleted. workdir is required for the same reason: there is no shared state to fall back on, so each caller must name its own destination.

Types

type ActiveSidecar

type ActiveSidecar struct {
	SidecarID string `json:"sidecar_id"`
	Name      string `json:"name,omitempty"`
	// OrgID records which org the sidecar belongs to, so Reap can tell a sidecar
	// that has been deleted from one that simply lives in an org it is not
	// listing. Empty on state written before this field existed.
	OrgID         string `json:"org_id,omitempty"`
	SessionID     string `json:"session_id,omitempty"`
	Workspace     string `json:"workspace,omitempty"`
	LastSyncedRef string `json:"last_synced_ref,omitempty"`
	// LastSyncedPatchHash is a SHA-256 hex digest of the working-tree patch
	// last applied to the sidecar.
	LastSyncedPatchHash string `json:"last_synced_patch_hash,omitempty"`
}

ActiveSidecar holds the currently active sidecar for a project.

func LoadActive

func LoadActive(ctx context.Context) (*ActiveSidecar, error)

LoadActive reads the active sidecar for the current project from XDG_DATA_HOME. Returns nil if not found.

func LoadActiveFrom added in v0.7.38

func LoadActiveFrom(ctx context.Context, dir string) (*ActiveSidecar, error)

LoadActiveFrom reads the active sidecar from dir.

type ActiveSnapshot added in v0.7.35

type ActiveSnapshot struct {
	ID   string `json:"id"`
	Name string `json:"name,omitempty"`
}

ActiveSnapshot holds the most recently created snapshot for a project.

func LoadActiveSnapshot added in v0.7.35

func LoadActiveSnapshot(ctx context.Context) (*ActiveSnapshot, error)

LoadActiveSnapshot reads the active snapshot for the current project from XDG_DATA_HOME. Returns nil if not found.

func LoadSnapshotFrom added in v0.7.38

func LoadSnapshotFrom(ctx context.Context, dir string) (*ActiveSnapshot, error)

LoadSnapshotFrom reads the active snapshot from dir.

type ExecResult

type ExecResult struct {
	Stdout   string
	Stderr   string
	ExitCode int
}

ExecResult holds the output of a command executed over SSH.

func ExecOverSSH

func ExecOverSSH(ctx context.Context, session *Session, command string, stdin io.Reader, envVars map[string]string) (_ *ExecResult, err error)

ExecOverSSH connects to the sidecar via SSH-over-TLS and executes a command.

type KeyNotFoundError

type KeyNotFoundError struct {
	Path string
}

KeyNotFoundError indicates the SSH private key file does not exist.

func (*KeyNotFoundError) Error

func (e *KeyNotFoundError) Error() string

type NoOriginRemoteError added in v0.7.67

type NoOriginRemoteError struct {
	Err error
}

NoOriginRemoteError indicates git remote "origin" is not configured.

func (*NoOriginRemoteError) Error added in v0.7.67

func (e *NoOriginRemoteError) Error() string

func (*NoOriginRemoteError) Unwrap added in v0.7.67

func (e *NoOriginRemoteError) Unwrap() error

type PublicKeyNotFoundError

type PublicKeyNotFoundError struct {
	KeyPath      string
	IdentityFile string
}

PublicKeyNotFoundError indicates the SSH public key file does not exist.

func (*PublicKeyNotFoundError) Error

func (e *PublicKeyNotFoundError) Error() string

type ReapResult added in v0.7.144

type ReapResult struct {
	// Deleted holds sidecars that were still running but abandoned, and so were
	// deleted through the API.
	Deleted []string
	// Vanished holds sidecars already absent server-side. Only their local state
	// files were removed; there was nothing left to delete.
	Vanished []string
	// Failed holds sidecars the API refused to delete. Their state is still
	// dropped, so the leak is reported rather than silent.
	Failed []string
}

ReapResult records what Reap did, so the caller can report it.

func Reap added in v0.7.144

func Reap(ctx context.Context, client *circleci.Client, orgID string) (ReapResult, error)

Reap deletes abandoned sidecars for the current project and removes their local state files.

State accumulates one file per session and branch, and nothing else removes them. Forgotten sidecars keep costing money, and a state file naming a sidecar that no longer exists causes every command against it to fail with a bare 404.

Three cases, deliberately treated differently:

  • Absent from the org's sidecar list: the state is worthless, so the file goes. This includes the current session's own file, because there is no live sidecar left to protect and leaving it is exactly what causes the resurrection.
  • Present but untouched for longer than StaleAfter: deleted through the API, then the file goes. The current session's file is spared, since deleting the sidecar this run is about to use only forces an immediate recreate.
  • Present and recently used: left alone. A concurrent session on another branch legitimately owns its own sidecar, and its warm mtime says so.

Reap fails open. If the sidecar list cannot be fetched, nothing is deleted and nothing is pruned: an empty or partial list is not proof of absence, and destroying a live sidecar is far worse than leaving a stale file behind.

func (ReapResult) Empty added in v0.7.144

func (r ReapResult) Empty() bool

Empty reports whether the reap changed nothing.

func (ReapResult) Summary added in v0.7.144

func (r ReapResult) Summary() string

Summary describes the result in one line, or "" when nothing changed.

type RemoteBaseError

type RemoteBaseError struct {
	Err error
}

RemoteBaseError indicates the remote merge base could not be resolved.

func (*RemoteBaseError) Error

func (e *RemoteBaseError) Error() string

func (*RemoteBaseError) Unwrap

func (e *RemoteBaseError) Unwrap() error

type Session

type Session struct {
	URL          string // WebSocket tunnel URL (ws:// or wss://)
	IdentityFile string // path to SSH private key (empty when using agent)
	KnownHosts   string // path to known_hosts file
	UseAgent     bool   // true when authenticating via ssh-agent
	AuthSock     string // SSH_AUTH_SOCK path (only used when UseAgent is true)
}

Session holds the info needed to SSH into a sidecar. It is a plain value type with no open connections or resources to close. Each call to ExecOverSSH opens and closes its own SSH connection.

func OpenSession

func OpenSession(ctx context.Context, client *circleci.Client, sidecarID, identityFile, authSock string) (*Session, error)

OpenSession registers an SSH key with the sidecar and returns session info. authSock is the SSH_AUTH_SOCK path; when non-empty and no identityFile is given, the agent is tried first.

type SnapshotCriteria added in v0.7.158

type SnapshotCriteria struct {
	// Repo is the repository name, e.g. "chunk-cli".
	Repo string
	// Stack is the detected tech stack as envbuilder names it, e.g. "go" or
	// "typescript". Callers pass "" when detection failed.
	Stack string
}

SnapshotCriteria describes the repository a sidecar is being created for. Both fields are optional: an empty Repo or Stack simply contributes no score.

type SnapshotMatch added in v0.7.158

type SnapshotMatch struct {
	Snapshot circleci.Snapshot
	// Reason is a short human-readable justification, e.g. "matches repo
	// chunk-cli". Callers surface it so the choice is never silent.
	Reason string
}

SnapshotMatch is a selected snapshot together with why it was selected.

func ResolveSnapshot added in v0.7.158

func ResolveSnapshot(ctx context.Context, client *circleci.Client, orgID string, c SnapshotCriteria) (SnapshotMatch, bool, error)

ResolveSnapshot lists the org's snapshots and selects the one that best fits criteria. It reports false when the org has no suitable snapshot.

func SelectSnapshot added in v0.7.158

func SelectSnapshot(snapshots []circleci.Snapshot, c SnapshotCriteria) (SnapshotMatch, bool)

SelectSnapshot picks the snapshot that best fits criteria, reporting false when none of them relate to the repository at all. Returning false rather than an arbitrary snapshot is deliberate: booting the wrong prepared environment is more confusing than booting the plain default image, because the failures it produces look like the repo's own.

Jump to

Keyboard shortcuts

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