Documentation
¶
Overview ¶
Package weftclient is the shared gRPC client surface for weft consumers — weft, weft-microvm, and any future tool that needs to talk to the daemon. It centralises:
- dialing (default ~/.weft/weft.sock, optional SSH transport)
- the proto-state → human-string mapping
- human-byte formatting
Anything *display-flavoured* (table rendering, JSON shape) stays in each tool — those legitimately differ between the Docker-style weft-microvm output and the VZ-style weft output, and forcing one shape on both would just push complexity into formatting options.
Stability: this is an exported module; backwards-compatible additions only — breaking changes need a new major version.
Package weftclient — device.go implements the OAuth 2.0 Device Authorization Grant (RFC 8628) used by `weft login` to obtain a token from dex without a callback URL. The flow is:
- POST <issuer>/device/code with client_id + scope → returns device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval.
- Display user_code + verification_uri to the operator. They open the URL in a browser and authenticate.
- Poll <issuer>/token with grant_type=urn:ietf:params: oauth:grant-type:device_code + device_code, every `interval` seconds. dex returns the access_token (plus id_token if openid was in scope) once authorisation lands.
We keep this dependency-light: stdlib net/http for the requests, stdlib encoding/json for the OIDC responses (the dex wire format is JSON regardless of our HCL preference; HCL is for config we own).
Package weftclient — eventstream.go provides the shared "open a WatchEvents stream + render rows" helper used by `weft events` and `weft-microvm events`. Lifts the rendering out so both CLIs print the same shape: kind, subject, project, optional meta. Two output formats:
default (human): tab-separated columns
2026-05-23T10:23:45.123Z vm.state.running alpine team-alpha pid=12345
--format json: one JSON object per line, jq-friendly
Both formats stream-flush after every event so a piped consumer (`weft-microvm events | grep error`) reacts in real time.
Package weftclient — interceptor.go installs a gRPC client-side interceptor that attaches the cached OIDC access token to every outgoing call as `Authorization: Bearer <…>`. Used by weft and weft-microvm so the operator who ran `weft login` doesn't have to thread the token through every CLI flag.
Absence of a cached token (or an expired one without a refresh path wired yet) means: send no Authorization header at all. The server side (weft) treats that as a dev-mode caller when its validator is unset, and as Unauthenticated when it isn't — matching the contract in pkg/openweft/weft/auth.go.
Package weftclient — projectresolver.go is a small in-memory cache of (project UUID → display name) that the event streamer uses to swap raw UUIDs for readable names in human output.
The resolver is self-updating: it bootstraps with a single ListProjects RPC at stream open, then watches every `project.created` / `project.renamed` / `project.deleted` event flowing through the same WatchEvents stream and updates its map in place. So a project rename mid-stream is reflected in subsequent rows without re-polling weft.
Thread-safe — concurrent reads from the render goroutine and updates from the stream goroutine are guarded by a mutex. Unknown UUIDs return the UUID itself; better one piece of stable identification than an empty cell.
Package weftclient — token.go owns the on-disk OAuth2 token cache for weft / weft-microvm. The cache is HCL (per [[hcl-over-json]]) so an operator can `cat` it, comment out a stale entry, or hand-edit when debugging an SSO problem.
Layout: $XDG_CONFIG_HOME/weft/token.hcl (default ~/.config/weft/token.hcl). Mode 0600 — tokens are bearer credentials, treat them like SSH keys.
Schema:
# weft auth token cache. Created/updated by `weft login`. issuer = "https://dex.internal.example.com" client_id = "weft" access_token = "eyJ…" refresh_token = "…" # optional id_token = "eyJ…" expires_at = "2026-05-23T12:34:56Z"
Only one cached token at a time — multi-account is deferred.
Index ¶
- func BearerInterceptor(tokenSource func() string) grpc.UnaryClientInterceptor
- func BearerStreamInterceptor(tokenSource func() string) grpc.StreamClientInterceptor
- func CachedTokenSource() func() string
- func Client(socketPath string, opts ...Option) (weftv1.WeftAgentClient, *grpc.ClientConn, error)
- func DefaultSocket() string
- func DeleteCachedToken() error
- func Dial(socketPath string, opts ...Option) (*grpc.ClientConn, error)
- func HumanBytes(b int64) string
- func PollDeviceToken(ctx context.Context, issuer, clientID string, da *DeviceAuthResponse) (*oauth2.Token, string, error)
- func RenderEvent(w io.Writer, ev *weftv1.PlatformEvent, format string, ...) error
- func SaveCachedToken(t *CachedToken) error
- func StateString(s weftv1.VMState) string
- func StreamEvents(ctx context.Context, client weftv1.WeftAgentClient, opts EventStreamOptions, ...) error
- func TokenCachePath() string
- type CachedToken
- type DeviceAuthResponse
- type EventStreamOptions
- type Option
- type Options
- type ProjectResolver
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BearerInterceptor ¶
func BearerInterceptor(tokenSource func() string) grpc.UnaryClientInterceptor
BearerInterceptor returns a unary client interceptor that stamps the Authorization header onto every outgoing call. The `tokenSource` is called per request so a future refresh-token path can rotate tokens without restarting the client.
`tokenSource` returning an empty string means "send no Authorization header"; the request goes through unchanged.
func BearerStreamInterceptor ¶
func BearerStreamInterceptor(tokenSource func() string) grpc.StreamClientInterceptor
BearerStreamInterceptor is the streaming counterpart.
func CachedTokenSource ¶
func CachedTokenSource() func() string
CachedTokenSource is the canonical tokenSource closure: it reads the on-disk cache on every call. Cheap (one file read on disk, kernel page cache makes it near-free), correct (picks up a fresh token after `weft login` without restarting), and failure-safe (returns "" rather than panicking on an unreadable cache).
func Client ¶
func Client(socketPath string, opts ...Option) (weftv1.WeftAgentClient, *grpc.ClientConn, error)
Client is the typed-client convenience over Dial — most callers want this. Caller closes the returned ClientConn.
func DefaultSocket ¶
func DefaultSocket() string
DefaultSocket returns weft's default Unix-socket path (~/.weft/weft.sock). Falls back to /tmp/weft.sock when the user's home directory cannot be resolved — same behaviour weft prints at startup.
func DeleteCachedToken ¶
func DeleteCachedToken() error
DeleteCachedToken removes the on-disk cache (used by `weft logout`). Missing-file is treated as success.
func Dial ¶
func Dial(socketPath string, opts ...Option) (*grpc.ClientConn, error)
Dial opens a gRPC connection to weft. An empty socketPath resolves to DefaultSocket(); SSH transport kicks in only when WithSSH(_, key != "") is passed. Caller closes the returned ClientConn.
func HumanBytes ¶
HumanBytes formats a byte count using binary prefixes (KiB, MiB, GiB…). Same algorithm weft has shipped since v0; centralising it means a future change to display rules (decimal prefixes, localisation, …) lands in one place.
func PollDeviceToken ¶
func PollDeviceToken(ctx context.Context, issuer, clientID string, da *DeviceAuthResponse) (*oauth2.Token, string, error)
PollDeviceToken polls the token endpoint until the user authorises (or the device code expires / ctx is cancelled). Returns the oauth2.Token plus the raw `id_token` string (if dex issued one — depends on whether `openid` was in scope).
Retryable RFC 8628 errors (`authorization_pending`, `slow_down`) are handled inline; everything else surfaces immediately.
func RenderEvent ¶
func RenderEvent(w io.Writer, ev *weftv1.PlatformEvent, format string, resolver *ProjectResolver) error
RenderEvent writes one event to `w` in the requested format and flushes. When `resolver` is non-nil, the human format swaps the raw project UUID for the cached display name; the JSON format keeps both. Pass a nil resolver to skip resolution.
Returns the write error verbatim — caller decides whether a broken pipe ends the stream.
func SaveCachedToken ¶
func SaveCachedToken(t *CachedToken) error
SaveCachedToken writes a token to TokenCachePath() with mode 0600. The output is comment-headered HCL with the fields in a stable order so diffs across logins are noise-free.
func StateString ¶
StateString maps the proto VMState enum to a stable lowercase label. Lowercase ("running") is the Unix-conventional form weft has used since the start; weft-microvm matches that.
func StreamEvents ¶
func StreamEvents(ctx context.Context, client weftv1.WeftAgentClient, opts EventStreamOptions, w io.Writer) error
StreamEvents opens a WatchEvents stream and pumps every event through RenderEvent. Returns when the stream ends (server-side close), the caller cancels ctx, or a render error occurs.
A ProjectResolver is bootstrapped via one ListProjects RPC before the first event is received, then updated in-band from the `project.*` events that flow through the same stream — so a mid-stream rename is reflected in subsequent rows without re-polling weft. Bootstrap failures degrade gracefully: rows fall back to the raw UUID.
The caller owns the gRPC client; weft / weft-microvm both pass the one they already opened for their other subcommands. The bearer interceptor in weftclient.Dial stamps the cached OIDC token transparently, so authenticated streams "just work" once the operator has run `weft login`.
func TokenCachePath ¶
func TokenCachePath() string
TokenCachePath resolves the on-disk location of the cache. Honours XDG_CONFIG_HOME; falls back to $HOME/.config/weft.
Types ¶
type CachedToken ¶
type CachedToken struct {
Issuer string `hcl:"issuer"`
ClientID string `hcl:"client_id"`
AccessToken string `hcl:"access_token"`
RefreshToken string `hcl:"refresh_token,optional"`
IDToken string `hcl:"id_token,optional"`
ExpiresAt string `hcl:"expires_at"`
}
CachedToken is the HCL-decoded shape of token.hcl.
func FromOAuth2 ¶
func FromOAuth2(tok *oauth2.Token, issuer, clientID, idToken string) *CachedToken
FromOAuth2 converts a freshly-issued `*oauth2.Token` (with the `id_token` claim already extracted) into a CachedToken ready to Save.
func LoadCachedToken ¶
func LoadCachedToken() (*CachedToken, error)
LoadCachedToken reads + decodes the cached token. Returns (nil, nil) when no cache exists — that's the "not logged in" state, not an error.
func (*CachedToken) Bearer ¶
func (t *CachedToken) Bearer() string
Bearer returns the access token to send in `Authorization: Bearer <…>`. Empty when no usable token is cached. Does NOT check expiry — caller decides whether to refresh.
func (*CachedToken) ExpiresAtTime ¶
func (t *CachedToken) ExpiresAtTime() time.Time
ExpiresAtTime returns the parsed expiry time. Zero value when the cache field is empty or malformed (treated by callers as "already expired" so a refresh kicks in).
type DeviceAuthResponse ¶
type DeviceAuthResponse struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
VerificationURIComplete string `json:"verification_uri_complete"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
}
DeviceAuthResponse mirrors the RFC 8628 server response.
func DeviceAuth ¶
func DeviceAuth(ctx context.Context, issuer, clientID string, scopes []string) (*DeviceAuthResponse, error)
DeviceAuth starts the device flow. Returns the server response; the caller is expected to display it to the user and then call PollDeviceToken.
Scopes default to `openid profile email groups` — matches dex's canonical claim set for our use case.
type EventStreamOptions ¶
type EventStreamOptions struct {
KindPrefixes []string // server-side wildcard match
Project string // display name or UUID
Subject string // exact match on event.Subject; canonical use: VM name (`--vm NAME`)
Format string // "" → human; "json"
}
EventStreamOptions configures a tail-the-bus session. Filter fields are passed verbatim to weft's WatchEvents RPC and re-applied server-side; the client doesn't need to filter again locally.
type Option ¶
type Option func(*Options)
Option is the functional-options sugar; Dial / Client take a variadic slice.
func WithDialOption ¶
func WithDialOption(target string, opt grpc.DialOption) Option
WithDialOption routes the connection through a caller-supplied transport dial option instead of the default local socket — used to reach a target that isn't weft's Unix socket, e.g. a micro-VM's gRPC endpoint over a WireGuard overlay. `target` is the gRPC dial target reached through `opt` (the opt's own dialer determines the real endpoint, so target is typically a passthrough address). Takes precedence over WithSSH and the local socket.
Transport adapters such as weft-client/wgdial build this; keeping the heavy transport dependency in that subpackage means callers that only use the local socket or SSH never pull it in.
func WithSSH ¶
WithSSH switches the transport to an SSH-tunnelled connection via the grpc-transports/ssh package. `sshSocket` is the Unix socket weft's SSH server listens on (default ~/.weft/weft-ssh.sock when empty); `sshKey` is a private-key path. Setting `sshKey = ""` reverts to the local-socket happy path.
func WithTimeout ¶
WithTimeout overrides the default 3 s dial deadline.
type Options ¶
type Options struct {
// contains filtered or unexported fields
}
Options holds knobs for Dial / Client. Built via the functional option helpers below; zero-value is the local-unix-socket happy path.
type ProjectResolver ¶
type ProjectResolver struct {
// contains filtered or unexported fields
}
ProjectResolver maps project UUID → display name. Zero value is usable but empty (every Name lookup returns the input UUID verbatim); callers typically bootstrap via NewProjectResolver.
func NewProjectResolver ¶
func NewProjectResolver() *ProjectResolver
NewProjectResolver opens an empty resolver. Call Bootstrap to fill it before use; Apply to keep it current.
func (*ProjectResolver) Apply ¶
func (r *ProjectResolver) Apply(ev *weftv1.PlatformEvent)
Apply updates the cache from a single PlatformEvent. Watches the three `project.*` kinds; everything else is a no-op so the streamer can call this for every event without per-event dispatch.
Caller already verified the event is non-nil (the stream always emits non-nil; defensive guard inside is just polish).
func (*ProjectResolver) Bootstrap ¶
func (r *ProjectResolver) Bootstrap(ctx context.Context, c weftv1.WeftAgentClient)
Bootstrap fills the cache from a ListProjects RPC. Best-effort: a failed list logs nothing and leaves the cache empty — the streamer keeps working, just without name resolution.
func (*ProjectResolver) Name ¶
func (r *ProjectResolver) Name(uuid string) string
Name returns the display name for `uuid` or the UUID itself when unknown. Stable identity beats a blank.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package wgdial adapts the userspace WireGuard transport (github.com/grpc-transports/wireguard) into a weft-client dial option, letting the CLI reach a micro-VM's gRPC endpoint over an end-to-end WireGuard overlay — encrypted even against an untrusted hypervisor host, and requiring no root or interface configuration on the operator's machine.
|
Package wgdial adapts the userspace WireGuard transport (github.com/grpc-transports/wireguard) into a weft-client dial option, letting the CLI reach a micro-VM's gRPC endpoint over an end-to-end WireGuard overlay — encrypted even against an untrusted hypervisor host, and requiring no root or interface configuration on the operator's machine. |