cli

package
v0.0.9 Latest Latest
Warning

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

Go to latest
Published: Sep 8, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

Documentation

Overview

Package cli is the rainier CLI's HTTP client and on-disk config — the pieces cmd/rainier composes into the actual command surface, and the pieces a smoke test can drive against a real controld without going through main().

Index

Constants

View Source
const DefaultContext = "default"

DefaultContext is the name of the context `rainier login` writes when no other is named, and the name a legacy single-server config migrates into.

Variables

View Source
var ErrLoginAgain = errors.New("session expired: log in again with `rainier login --cloud`")

ErrLoginAgain is the one thing the CLI can say when a hosted context's credentials are past saving: the refresh was refused (a replayed, expired, or revoked token), or the refreshed pair was refused too. It carries no token material and no server detail — there is exactly one action left, and this sentence is it.

Functions

func ProgressLine

func ProgressLine(verb string, done, total int64) string

ProgressLine renders one line of transfer progress. It is a function rather than a Printf at the call site so the formatting has a test and both directions read the same.

A total of zero means "unknown", which is a pull: nothing on the client side knows how big the archive is until it ends.

func Pull

func Pull(c *Client, sessionID, remotePath, localDir string, progress func(received, total int64)) error

Pull downloads remotePath from session id and extracts it into localDir, which is created if it does not exist.

progress, when non-nil, is called as bytes arrive; the total is 0 because nothing on this side knows it until the stream ends.

func Push

func Push(c *Client, sessionID, localDir, remotePath string, progress func(sent, total int64)) error

Push tars localDir and streams it to remotePath inside session id.

progress, when non-nil, is called after every acked chunk with the bytes sent so far and the archive's total size.

func RandHex

func RandHex(n int) string

RandHex returns n random bytes rendered as 2n lowercase hex characters — used for X-Request-Id (below) and by cmd/rainier for a fresh Idempotency-Key per `rainier new` invocation.

func Save

func Save(c Config) error

Save writes c to the config file, creating its directory (0700) if necessary. The file itself is 0600: it carries bearer tokens.

A caller that set ServerURL/Token/OwnerID directly (the pre-contexts way) has its values folded into the current context — that, and not a second write path, is what keeps those callers correct. The legacy fields themselves are never written back: what Save leaves on disk is always the current shape. Save serializes a complete replacement with credential refresh; use UpdateConfig for edits to existing configuration so a stale copy cannot overwrite a newer credential pair.

func UpdateConfig added in v0.0.7

func UpdateConfig(edit func(*Config) error) error

UpdateConfig applies one edit to the latest config under the same lock as credential refresh. Use it for read-modify-write changes: loading a Config and saving that stale copy could overwrite a sibling's freshly rotated tokens. The callback must not perform network I/O or call Save/UpdateConfig. An edit error leaves the file unchanged; successful writes replace it atomically.

func UpdateConfigContext added in v0.0.9

func UpdateConfigContext(ctx context.Context, edit func(*Config) error) error

UpdateConfigContext is UpdateConfig with cancellation covering the config lock. Browser-backed commands use it so an interrupt cannot leave the CLI waiting forever behind another process that owns the lock.

Types

type APIError

type APIError struct {
	Code    string
	Message string
	// Status is the HTTP status the envelope arrived with. It is what tells
	// an expired credential (401) from an ordinary refusal that happens to
	// share a code, and it is not part of Error's text.
	Status int
	// RetryAfter is transport guidance, preserved separately from untrusted prose.
	RetryAfter string
}

APIError is controld's stable non-2xx error envelope. Error preserves the CLI's existing "code: message" text while errors.As lets callers make decisions from Code instead of parsing prose.

func (*APIError) Error

func (e *APIError) Error() string

type Client

type Client struct {
	Base  string
	Token string
	HTTP  *http.Client

	Workspace    string
	RefreshToken string
	SaveTokens   func(TokenPair) error
	// LoadTokens re-reads the pair the context holds on disk. Before a
	// refresh the client asks it whether another process has already
	// rotated the pair, and adopts that pair instead of spending a refresh
	// token the edge would treat as replayed. Nil for a client with no
	// context of its own. False means the original context is no longer
	// available and recovery must stop rather than recreate it.
	LoadTokens func() (TokenPair, bool)
	// contains filtered or unexported fields
}

Client is the rainier CLI's HTTP client: a controld base URL, the bearer token to authenticate with, and (optionally) a caller-supplied *http.Client — nil means http.DefaultClient.

The remaining fields are what a hosted context adds, and a zero Client (every self-hosted caller, every test that builds one by hand) behaves exactly as it always did without them:

  • Workspace, when set, is sent as Rainier-Workspace on every request: the hosted edge scopes the call to that workspace.
  • RefreshToken, when set, lets one 401 be answered by refreshing the hosted token pair and retrying the request once, instead of surfacing as an error the user cannot act on. SaveTokens persists the rotated pair; it is nil for a client nobody expects to outlive the process.

func NewClient added in v0.0.2

func NewClient(cfg Config) *Client

NewClient builds the client for cfg's current context, carrying its workspace scope and — for a hosted context — the refresh token and the writer that saves a rotated pair back into that same context.

func (*Client) Do

func (c *Client) Do(method, path string, in, out any, opts ...Option) error

Do issues method against path (appended verbatim to c.Base — callers own their own query strings), marshaling in as the JSON body (skipped when in is nil) and decoding a 2xx response into out (skipped when out is nil). It always sets Authorization (when c.Token is set) and a fresh X-Request-Id; opts may add more headers (IdempotencyKey).

Errors are send's, below — including the error-envelope decoding every non-2xx response on this API gets.

func (*Client) DoContext

func (c *Client) DoContext(ctx context.Context, method, path string, in, out any, opts ...Option) error

DoContext is Do with the request bound to ctx. Use it for polling and other bounded operations so cancellation can interrupt a stalled transport.

func (*Client) Open

func (c *Client) Open(method, path string, opts ...Option) (io.ReadCloser, error)

Open is Do for a response that is NOT JSON: it returns the successful response's body for the caller to read and close. The one such response on this API is a pull's archive (GET /v0/sessions/{id}/files), which is streamed and may be hundreds of megabytes — decoding it into memory the way Do does would defeat the point of streaming it.

Failures are identical to Do's: a non-2xx is read as the error envelope and returned as an error, with the body already closed, so a caller only ever holds a body it is going to read.

func (*Client) RefreshAfterUnauthorized added in v0.0.7

func (c *Client) RefreshAfterUnauthorized(ctx context.Context) error

RefreshAfterUnauthorized recovers credentials after a server rejects an access token. HTTP requests and terminal upgrades use the same locked reload/rotation path, so another CLI's saved pair is adopted before spending a single-use refresh token. The caller must retry its rejected operation at most once before requiring login again; a successful connection starts a new recovery interval. No refresh credential, or a refused refresh, returns ErrLoginAgain. Transport and persistence errors are returned without retrying an exchange whose single-use token may already have been consumed.

type Config

type Config struct {
	Current  string             `json:"current"`
	Contexts map[string]Context `json:"contexts"`

	ServerURL string `json:"-"`
	Token     string `json:"-"`
	OwnerID   string `json:"-"`
}

Config is what `rainier login` writes and every other command reads: the named contexts the CLI knows and which one is current.

ServerURL, Token and OwnerID are not stored: they are the current context's, projected for every caller that only ever wants "the server I am talking to". Load fills them in; Save folds a change to them back into the current context, which is what keeps a pre-contexts caller — including `rainier login` itself — working unchanged.

func Load

func Load() (Config, error)

Load reads the config file, returning a zero Config (not an error) if it doesn't exist yet — "not logged in" is an ordinary state every command has to handle, not a failure Load itself should report.

A legacy single-server file is read as the "default" context and made current; nothing on disk changes until something calls Save.

func (Config) Active added in v0.0.2

func (c Config) Active() (Context, bool)

Active returns the current context, false when there is none (nothing has logged in yet, or the current one was removed).

func (Config) ActiveName added in v0.0.2

func (c Config) ActiveName() string

ActiveName is the name of the current context — Current, or "default" when nothing has named one (the shape a migrated legacy config has).

func (Config) Names added in v0.0.2

func (c Config) Names() []string

Names returns every context name, sorted — `rainier context list`'s order.

func (*Config) RemoveContext added in v0.0.2

func (c *Config) RemoveContext(name string) bool

RemoveContext deletes name, reporting false when there was no such context. Removing the current one leaves the config with none current: the next command says "not logged in" rather than silently acting against whichever context happened to sort first.

func (*Config) SetContext added in v0.0.2

func (c *Config) SetContext(name string, ctx Context)

SetContext stores ctx under name and makes it current.

func (*Config) UpdateContext added in v0.0.2

func (c *Config) UpdateContext(name string, ctx Context)

UpdateContext stores ctx under name without changing which context is current — how a rotated hosted token pair is written back.

func (*Config) Use added in v0.0.2

func (c *Config) Use(name string) bool

Use makes name the current context, reporting false (and changing nothing) when there is no such context.

type Context added in v0.0.2

type Context struct {
	Server          string `json:"server_url"`
	Token           string `json:"token"`
	OwnerID         string `json:"owner_id,omitempty"`
	Workspace       string `json:"workspace,omitempty"`
	RefreshToken    string `json:"refresh_token,omitempty"`
	AccessExpiresAt string `json:"access_expires_at,omitempty"`
}

Context is one server this CLI can talk to: a self-hosted controld, or a hosted edge and the workspace the caller is acting in.

OwnerID is the caller's own user id, learned at login from the identity controld returns (the same one GET /v0/me answers with) and cached here so every later command has it without a round trip. It is the string a session row carries as owner_id, and it is used to break ties when a session name resolves to more than one session; see cmd/rainier's resolveSessionID. Empty only for a config written before logins carried it — one `rainier login` fills it in.

Workspace, RefreshToken and AccessExpiresAt are hosted-only. A hosted access token is short-lived: the refresh token buys a new pair, is single-use, and rotates with every use (see Client's transparent refresh).

func (Context) Hosted added in v0.0.2

func (c Context) Hosted() bool

Hosted reports whether this context came from a hosted edge — the presence of a refresh token is what distinguishes the hosted passwordless login from a self-hosted GitHub login, whose token neither expires on its own nor rotates.

type Option

type Option func(*http.Request)

Option adjusts one Do call's request before it's sent — the "path-independent option" mechanism for headers that aren't part of every request, namely Idempotency-Key.

func IdempotencyKey

func IdempotencyKey(key string) Option

IdempotencyKey sets the Idempotency-Key header for one Do call. Callers that need one (POST /v0/sessions) generate a fresh key per invocation — Do itself never invents one, since only the caller knows which calls are retries of the same intent and which are new.

type TokenPair added in v0.0.2

type TokenPair struct {
	TokenType        string `json:"token_type"`
	AccessToken      string `json:"access_token"`
	AccessExpiresAt  string `json:"access_expires_at"`
	RefreshToken     string `json:"refresh_token"`
	RefreshExpiresAt string `json:"refresh_expires_at"`
}

TokenPair is the hosted edge's token response — the body POST /v0/auth/login-attempts/{id}/exchange and POST /v0/auth/refresh both answer with. The refresh token is single-use: every refresh rotates it, and replaying a spent one revokes the chain.

Jump to

Keyboard shortcuts

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