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
- Variables
- func ProgressLine(verb string, done, total int64) string
- func Pull(c *Client, sessionID, remotePath, localDir string, ...) error
- func Push(c *Client, sessionID, localDir, remotePath string, ...) error
- func RandHex(n int) string
- func Save(c Config) error
- type APIError
- type Client
- type Config
- func (c Config) Active() (Context, bool)
- func (c Config) ActiveName() string
- func (c Config) Names() []string
- func (c *Config) RemoveContext(name string) bool
- func (c *Config) SetContext(name string, ctx Context)
- func (c *Config) UpdateContext(name string, ctx Context)
- func (c *Config) Use(name string) bool
- type Context
- type Option
- type TokenPair
Constants ¶
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 ¶
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 ¶
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 ¶
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 ¶
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, which `rainier login` still is) 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.
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
}
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.
type Client ¶
type Client struct {
Base string
Token string
HTTP *http.Client
Workspace string
RefreshToken string
SaveTokens func(TokenPair) error
// 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
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 ¶
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 ¶
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.
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 ¶
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
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
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
Names returns every context name, sorted — `rainier context list`'s order.
func (*Config) RemoveContext ¶ added in v0.0.2
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
SetContext stores ctx under name and makes it current.
func (*Config) UpdateContext ¶ added in v0.0.2
UpdateContext stores ctx under name without changing which context is current — how a rotated hosted token pair is written back.
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).
type Option ¶
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 ¶
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.