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
- Variables
- func ConnectURL(server string) (string, error)
- func Facts(runtimeVersion string) map[string]string
- func ParseLink(link string) (server, token string, err error)
- func ReadToken(r io.Reader) (string, error)
- func Run(ctx context.Context, id *Identity, opts Options) error
- func SaveIdentity(dir string, id *Identity) error
- type DirStore
- type ExecSession
- type Identity
- type IdentityStore
- type Options
Constants ¶
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 ¶
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.
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 ¶
ConnectURL turns the enrolled server origin into the socket URL.
func ParseLink ¶
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 Run ¶
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 ¶
SaveIdentity writes atomically with owner-only permissions on the directory and file.
Types ¶
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 ¶
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 ¶
LoadIdentity returns nil, nil when the agent has never enrolled.
func (*Identity) MatchesEnrollment ¶
MatchesEnrollment prevents a new link from silently reusing another enrollment.
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
}