client

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 27 Imported by: 0

Documentation

Overview

Package client is the KyYard agent: durable identity, enrollment and the connection loop from docs/agent-protocol.md sections 2, 3, 6 and 9.

Index

Constants

View Source
const PendingKeyLife = 8 * 24 * time.Hour

PendingKeyLife is a day longer than the server's seven-day window: the agent must forget an offer strictly after the server stops accepting it, so an acknowledgement can never land on a key the agent no longer holds. The extra day is clock-skew margin, not a mirror.

Variables

View Source
var (
	ErrRevoked         = errors.New("identity revoked by the control plane")
	ErrInstanceChanged = errors.New("control plane identity changed; re-enroll this host")
	ErrIncompatible    = errors.New("control plane does not speak this protocol version")
)

Errors the loop treats as terminal: reconnecting cannot help.

View Source
var ErrServerUnproven = errors.New("server does not prove its identity; upgrade the server")

ErrServerUnproven is retried: upgrading the server fixes it without touching the agent.

Functions

func ConnectURL

func ConnectURL(server string) (string, error)

ConnectURL turns the enrolled server origin into the socket URL.

func Facts

func Facts(runtimeVersion string) map[string]string

Facts is the bounded enrollment report; the server keeps only the keys it knows.

func ParseLink(link string) (server, token string, err error)

ParseLink accepts only the generated HTTPS enrollment link. The token fragment is extracted locally and never sent as part of an HTTP URL or error message.

func ReadToken

func ReadToken(r io.Reader) (string, error)

ReadToken supports stdin enrollment for native clients and older installations.

func Run

func Run(ctx context.Context, id *Identity, opts Options) error

Run keeps the agent connected until ctx ends or a terminal error occurs. Backoff starts at one second, doubles to a minute, and carries ±20 % jitter.

func SaveIdentity

func SaveIdentity(dir string, id *Identity) error

SaveIdentity writes atomically with owner-only permissions on the directory and file.

Types

type DirStore

type DirStore string

DirStore is the identity file in a directory, owner-only.

func (DirStore) Load

func (d DirStore) Load() (*Identity, error)

func (DirStore) Save

func (d DirStore) Save(id *Identity) error

type ExecSession

type ExecSession interface {
	io.ReadWriteCloser
	Resize(context.Context, protocol.TerminalSize) error
	Inspect(context.Context) (protocol.ExecStatus, error)
}

ExecSession is the runtime adapter boundary. Close must unblock Read and Write; it closes the attachment, without promising that the process has been killed. Implementations must permit concurrent Close calls and enforce the protocol idle/absolute deadlines; the transport bounds queue waits separately.

type Identity

type Identity struct {
	EndpointID          string `json:"endpoint_id"`
	PrivateKey          []byte `json:"private_key"`
	InstanceFingerprint string `json:"instance_fingerprint"`
	Server              string `json:"server"`
	// EnrollmentHash binds restart arguments to the redeemed token without retaining it.
	EnrollmentHash string `json:"enrollment_hash,omitempty"`
	// Generation is the last inventory generation sent; it only rises, across restarts too.
	Generation uint64 `json:"generation"`
	// A rotated key waits here until the operator acknowledges it; the current key keeps
	// authenticating meanwhile.
	PendingPrivateKey  []byte    `json:"pending_private_key,omitempty"`
	PendingFingerprint string    `json:"pending_fingerprint,omitempty"`
	PendingSince       time.Time `json:"pending_since,omitempty"`
	// PendingRecorded says the server answered that it holds this offer. A key saved but never
	// confirmed (the session died between the write and the answer) may not exist server-side,
	// so it is the last key to try, never the first.
	PendingRecorded bool `json:"pending_recorded,omitempty"`
	// LapsedPrivateKey is an offer the agent gave up on. It is kept until the server proves it
	// gone (by recording a new offer), because with skewed clocks the server may still
	// acknowledge it, and key_retired must then be able to promote it. LapsedRecorded carries
	// the same provenance PendingRecorded did, because an offer that lapsed unconfirmed is no
	// more trustworthy for having waited.
	LapsedPrivateKey []byte `json:"lapsed_private_key,omitempty"`
	LapsedRecorded   bool   `json:"lapsed_recorded,omitempty"`
	// RecoveryAttempt is how far along the candidate list a refused key has walked. It is an
	// index, not a promotion: no key is moved or overwritten until the server accepts one, so
	// a fault that refuses every candidate leaves the stored identity as it was. It is never
	// written to disk, because it only means anything against the candidate order computed in
	// the same run: a restart must begin again at the identity's own key.
	RecoveryAttempt int       `json:"-"`
	RotatedAt       time.Time `json:"rotated_at"`
}

Identity is everything the agent keeps between runs. The enrollment token is never in it.

func DecodeIdentity

func DecodeIdentity(raw []byte) (*Identity, error)

DecodeIdentity is the one reading of a stored identity, whichever store held it.

func Enroll

func Enroll(ctx context.Context, httpClient *http.Client, server string, store IdentityStore, name, tokenB64 string, facts map[string]string) (*Identity, error)

Enroll redeems a token for an identity and pins the server's instance fingerprint. The token is used once and dropped; only the resulting identity is persisted, to store. Facts are the bounded report the approver reads beside the fingerprint (Facts for a Docker host).

func LoadIdentity

func LoadIdentity(dir string) (*Identity, error)

LoadIdentity returns nil, nil when the agent has never enrolled.

func (*Identity) MatchesEnrollment

func (id *Identity) MatchesEnrollment(server, token string) bool

MatchesEnrollment prevents a new link from silently reusing another enrollment.

func (*Identity) Promote

func (id *Identity) Promote()

Promote makes the acknowledged pending key the current one.

type IdentityStore

type IdentityStore interface {
	// Load returns nil, nil when the agent has never enrolled.
	Load() (*Identity, error)
	Save(*Identity) error
}

IdentityStore keeps the identity between runs: a directory on a Docker host, a Secret in a cluster (internal/runtime/kubernetes).

type Options

type Options struct {
	Inspect func(context.Context, protocol.InspectionTarget) (*protocol.ContainerInspection, error)
	// Configure reads a container's full configuration (Docker only); it advertises container.configure.
	Configure func(context.Context, protocol.InspectionTarget) (*protocol.ContainerConfiguration, error)

	HTTPClient *http.Client
	Version    string
	Log        *log.Logger
	// Identities is where the rising inventory generation and rotation state are written back.
	// Nil falls back to IdentityDir.
	Identities IdentityStore
	// IdentityDir is the identity directory when Identities is nil, and the default CommandDir.
	IdentityDir string
	// Kubernetes marks a cluster agent: it advertises kubernetes.inventory, and pod.logs when
	// Logs is set, in place of every Docker capability, and its facts-only snapshot carries an
	// empty cluster inventory so it still has the shape a Kubernetes endpoint requires.
	Kubernetes bool
	// CommandDir overrides the durable command ledger directory for embedded agents and for
	// cluster agents (their scratch volume). With neither it nor IdentityDir the ledgers stay
	// in memory and a restart forgets them.
	CommandDir string
	// RotateEvery is how often the agent offers a new key; zero disables rotation.
	RotateEvery time.Duration
	// Snapshot reads the runtime; nil reports facts only (no runtime reachable).
	Snapshot func(ctx context.Context) (*protocol.Snapshot, error)
	// Metrics samples the running containers named in the last snapshot; nil sends none.
	Metrics func(ctx context.Context, running []string) protocol.Metrics
	// InventoryEvery is how often a fresh snapshot is sent while connected.
	InventoryEvery time.Duration
	// Operate runs one container action and reports the outcome and a short reason. Nil means
	// this agent has no runtime to operate, and every command is refused rather than dropped.
	// The context ends when the session that carried the command does, so a dropped socket
	// stops work nobody is waiting for rather than leaving it running blind against the host.
	Operate func(ctx context.Context, cmd protocol.Command) (outcome, detail string)
	// Logs streams one container's log to sink until the log ends, the context is cancelled
	// or sink refuses. Nil means this agent has no runtime, and a request for logs is closed
	// with that reason rather than left open.
	Logs func(ctx context.Context, req protocol.LogRequest, sink func([]byte) error) error
	// Exec opens a PTY attachment after a nonce-bound control-plane grant.
	// Nil disables exec when no runtime adapter is configured.
	Exec func(context.Context, protocol.ExecSpec) (ExecSession, error)
	// Deploy applies one deployment plan and reports its result. It runs on the agent's root
	// context, not the session's, so a dropped socket never stops it; the request bounds it by
	// its deadline. Nil means this agent has no runtime, and every apply is refused.
	// The runtime calls the third argument once, immediately before its first phase-two
	// mutation; it returns once the deployer has recorded durably that the host may change.
	Deploy func(context.Context, protocol.DeploymentRequest, func()) protocol.DeploymentResult
	// ReadWorkload reads a workload's pod template for a cluster agent's configuration grant.
	// With Operate and ApplyWorkload it advertises kubernetes.workloads.
	ReadWorkload func(context.Context, protocol.InspectionTarget) (*protocol.WorkloadConfiguration, error)
	// ApplyWorkload applies one workload.apply frame. It shares Deploy's slot, ledger and root
	// context and answers deployment.result.
	ApplyWorkload func(context.Context, protocol.WorkloadApply, func()) protocol.DeploymentResult
	// Remove tears down the containers a removal names and reports its result. It shares
	// Deploy's single slot and root context and is bounded by the request's deadline. Nil
	// means this agent has no runtime, and every removal is refused.
	Remove func(context.Context, protocol.RemovalRequest, func()) protocol.DeploymentResult
	// OnState is called with the state the server reported at connect (tests).
	OnState func(state string)
	// contains filtered or unexported fields
}

Jump to

Keyboard shortcuts

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