weftclient

package module
v0.2.2 Latest Latest
Warning

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

Go to latest
Published: Jun 2, 2026 License: BSD-3-Clause Imports: 22 Imported by: 0

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:

  1. POST <issuer>/device/code with client_id + scope → returns device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval.
  2. Display user_code + verification_uri to the operator. They open the URL in a browser and authenticate.
  3. 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

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

func HumanBytes(b int64) string

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

func StateString(s weftv1.VMState) string

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

func WithSSH(sshSocket, sshKey string) Option

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

func WithTimeout(d time.Duration) Option

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.

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.

Jump to

Keyboard shortcuts

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