Documentation
¶
Overview ¶
Package common — humane error rendering for API responses and network failures.
The guiding philosophy: the CLI owes its users clear, honest, explicit messages when something goes wrong. Dumping a raw JSON blob at the user's terminal is a cop-out. Every error should read like a human wrote it — informal where it helps, informative always, honest when it's our fault.
Typical usage from a command:
resp, err := common.DoRequest(http.MethodPost, url, body)
if err != nil {
fmt.Println(common.TransportError("create slice", err))
return
}
defer resp.Body.Close()
respBody, err := common.CheckResponse(resp, "create slice")
if err != nil {
fmt.Println(err)
return
}
// ... use respBody
prompt.go — minimal interactive prompts using only stdlib + golang.org/x/term (which we already pull in indirectly via cobra). Replaced manifoldco/promptui — that package was unmaintained since 2021, and our two call sites are simple enough that 30 lines of stdlib reads cleaner than the dependency.
Index ¶
- Constants
- Variables
- func AtomicHeader() string
- func BackboneHeader() string
- func BoldText(s string) string
- func CanvasHeader() string
- func CapitalizeFirst(s string) string
- func Check() string
- func CheckResponse(resp *http.Response, op string) ([]byte, error)
- func CompareVersions(a, b string) int
- func Cross() string
- func DoJSONRequest(method, url string, body io.Reader) (*http.Response, error)
- func DoRequest(method, url string, body io.Reader) (*http.Response, error)
- func DoRequestWithContentType(method, url, contentType string, body io.Reader) (*http.Response, error)
- func DoRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*http.Response, error)
- func DoRequestWithHeaders(method, url string, body io.Reader, headers map[string]string) (*http.Response, error)
- func GetActiveSlice() string
- func GetOrCreateDeviceID() string
- func GetTokenFromSession() (token string, refreshToken string, err error)
- func GetUsername() string
- func Highlight(s string) string
- func Hint(s string) string
- func IsStyleEnabled() bool
- func NewAuthenticatedRequest(method, url string, body io.Reader) (*http.Request, error)
- func OpenBrowser(rawURL string) error
- func PromptForInput(label string) string
- func PromptForInputHidden(label string) string
- func ReadPasswordFromStdin() string
- func RequireActiveSlice() (string, error)
- func SaveActiveSlice(name string) error
- func SaveSession(token, refresh_token string) error
- func TransportError(op string, err error) error
- func ZipFolder(folderPath string) (*bytes.Buffer, error)
- type APIError
- type LatestRelease
- type Session
- type Spinner
Constants ¶
const CLIModulePath = "github.com/ondrift/cli/cmd/drift"
CLIModulePath is the go-installable path of the drift binary.
const CLIRepo = "ondrift/cli"
CLIRepo is the GitHub owner/repo the drift CLI is released from.
const SessionFile = "~/.drift/session.json"
Variables ¶
var APIBaseURL = "https://api.ondrift.eu"
APIBaseURL is the base URL for the Drift API gateway. It defaults to the public production gateway so a plain `go install` works out of the box; a local/dev build points it elsewhere via:
go build -ldflags "-X github.com/ondrift/cli/common.APIBaseURL=http://api.localhost:30036"
At runtime, the DRIFT_API_URL environment variable takes precedence over the compiled-in default (useful for self-hosted instances or staging).
var ConfiguratorBaseURL = "https://configurator.ondrift.eu"
ConfiguratorBaseURL is the base URL for the configurator service. The CLI hits this directly (rather than via the api gateway) for the slice create/resize browser handoff: handoff mints a session, redeem polls for the result. Like APIBaseURL it defaults to production and a local/dev build overrides it via -ldflags.
At runtime, the DRIFT_CONFIGURATOR_URL environment variable takes precedence over the compiled-in default.
Functions ¶
func BackboneHeader ¶
func BackboneHeader() string
BackboneHeader styles the "Backbone" section header.
func CapitalizeFirst ¶
func CheckResponse ¶
CheckResponse validates an HTTP response. On 2xx it returns the fully-read body and a nil error. On any other status it parses the server's error message (if any) and returns a humane *APIError.
Callers remain responsible for `defer resp.Body.Close()`.
func CompareVersions ¶ added in v1.11.1
CompareVersions compares two "vMAJOR.MINOR.PATCH" strings. A leading "v" is optional, missing segments count as 0, and any pre-release/build suffix ("-rc1", "+meta") is ignored. Returns -1 if a<b, 0 if equal, +1 if a>b.
func DoJSONRequest ¶
DoJSONRequest is a convenience wrapper for JSON request bodies.
func DoRequest ¶
DoRequest executes an authenticated HTTP request. If the server returns 401 (token expired), it automatically refreshes the JWT using the stored refresh token and retries once. The body parameter is read, buffered, and replayed on retry so callers don't need to worry about re-seekable readers.
func DoRequestWithContentType ¶
func DoRequestWithContentType(method, url, contentType string, body io.Reader) (*http.Response, error)
DoRequestWithContentType is like DoRequest but sets the Content-Type header on both the original request and the post-refresh retry. Pass an empty contentType to skip setting it.
func DoRequestWithContext ¶ added in v1.6.0
func DoRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*http.Response, error)
DoRequestWithContext is like DoRequest but binds the request (and its post-refresh retry) to ctx. Use it for best-effort calls that must not stall a command on the default 30s client timeout — pass a context with a short deadline and treat a deadline error as "skip the optimization".
func DoRequestWithHeaders ¶
func DoRequestWithHeaders(method, url string, body io.Reader, headers map[string]string) (*http.Response, error)
DoRequestWithHeaders is like DoRequest but applies the given headers to both the original request and the post-refresh retry.
func GetActiveSlice ¶
func GetActiveSlice() string
GetActiveSlice returns the active slice name, or empty string if none set.
func GetOrCreateDeviceID ¶
func GetOrCreateDeviceID() string
GetOrCreateDeviceID returns a stable per-workstation random ID. Used to bind refresh tokens: the server stores the ID at login, and a presented refresh token whose device_id doesn't match is treated as theft (revokes every live token for that user). Read-once cached on first call.
Stored alongside the session file at ~/.drift/device_id with mode 0600. A stealer that copies session.json without device_id is locked out at the next refresh.
func GetTokenFromSession ¶
func GetUsername ¶
func GetUsername() string
GetUsername extracts the username from the stored JWT access token. Returns an empty string if the session or token is missing/unparseable.
func IsStyleEnabled ¶
func IsStyleEnabled() bool
IsStyleEnabled reports whether ANSI styling should be emitted. Styling is suppressed when NO_COLOR is set, when output is being piped, or when the terminal advertises itself as dumb.
func NewAuthenticatedRequest ¶
func OpenBrowser ¶
OpenBrowser launches the user's default web browser at the given URL.
The URL arrives over the network (a configurator handoff response). Even though that response comes back over HTTPS in production, an attacker with TLS-MITM access or a compromised configurator could influence the URL. The OS-level launchers (`open` on macOS, `xdg-open` on Linux, `cmd /c start` on Windows) all interpret a URL that begins with `-` as a flag — and macOS's `open --background` would silently load the URL in any zero-day-vulnerable browser without bringing it to the foreground. Two defences:
- Validate the URL parses with a known scheme (https, or http to localhost for dev) before passing it to any shell. Reject anything else.
- On macOS / Linux, insert `--` between the command and the URL so the launcher stops interpreting flags.
We avoid pulling in a third-party dependency for the launch itself — the per-OS command is a one-liner. If the launch fails (no display, SSH session, container, locked-down environment) we return an error so the caller can fall back to printing the URL for the user to open manually.
func PromptForInput ¶
PromptForInput prints a label and reads a line from stdin. The returned string has its trailing newline stripped. If stdin is closed or unreadable, returns the empty string.
func PromptForInputHidden ¶
PromptForInputHidden prints a label and reads a line from stdin with terminal echo disabled — so the typed value (a password) doesn't appear on screen. Falls back to non-hidden read if stdin isn't a terminal (e.g. piped input in CI).
term.ReadPassword restores the terminal to its prior state on return, so the user's shell doesn't end up in a no-echo state even on signal-driven termination.
func ReadPasswordFromStdin ¶
func ReadPasswordFromStdin() string
ReadPasswordFromStdin reads a single line of plaintext from stdin, strips the trailing newline, and returns it. Used by `--password-stdin` flags so the password never appears as a CLI argument (which would expose it to `ps`, shell history, and process-listing log surfaces). Standard pattern matched by gh, docker login, kubectl, doctl, op. Returns the empty string on read error.
func RequireActiveSlice ¶
RequireActiveSlice returns the active slice or an error instructing the user to select one with "drift slice use <name>".
func SaveActiveSlice ¶
SaveActiveSlice persists the active slice name into the session file.
func SaveSession ¶
func TransportError ¶
TransportError wraps a network-level failure (DNS, dial, TLS, timeout) in a humane message. Pass the same `op` string you'd give to CheckResponse.
This is the right function to call when DoRequest returns an error — the return value is already a formatted, printable error.
Types ¶
type APIError ¶
type APIError struct {
// Op is a short, lowercase, imperative description of what the CLI
// was trying to do ("create slice", "deploy atomic function"). Used
// as the lead-in: "Couldn't {op}: ...".
Op string
// Status is the HTTP status code the server returned.
Status int
// Detail is the server-supplied reason, extracted from the JSON body
// ("error" or "message" fields). May be empty when the body is not
// JSON or when the server didn't include one.
Detail string
// Raw is the trimmed response body. Used only as a last-resort
// fallback when Detail is empty AND the status code has no specific
// mapping — we'd rather show the raw body than nothing.
Raw string
}
APIError is the humane rendering of a non-2xx response from the Drift API. It implements the error interface; callers should just `fmt.Println(err)`.
type LatestRelease ¶ added in v1.11.1
type LatestRelease struct {
Tag string // e.g. "v1.11.0"
URL string // best-effort link to see what's in it
}
LatestRelease is the salient subset of the CLI's latest pushed version tag.
func FetchLatestCLIRelease ¶ added in v1.11.1
func FetchLatestCLIRelease() (LatestRelease, error)
FetchLatestCLIRelease asks GitHub for the CLI's latest pushed version tag. Failures are returned to the caller: the dashboard swallows them (the banner just stays hidden); `drift upgrade` surfaces them.
type Spinner ¶
type Spinner struct {
// contains filtered or unexported fields
}
Spinner renders a single-line animation next to a label by repeatedly rewriting the current line with \r. It is intentionally minimal: no nested spinners, no out-of-band logging while spinning. Callers should avoid writing to stdout between Start and Stop.
When stdout is not a terminal (or NO_COLOR is set), Spinner becomes a no-op: Start renders nothing and Stop just leaves the cursor where it is, so piped output stays clean and machine-parseable.
func StartSpinner ¶
StartSpinner kicks off a background goroutine that animates the given label after `indent` spaces of leading whitespace. Call Stop to clear the line before printing the persistent success/failure marker.