Documentation
¶
Overview ¶
Package agent is the transport boundary: the interface ADR 003 describes ("the reconciler and everything above the transport boundary never knows whether it's talking to a local in-process agent or a remote one over mTLS") but that Phase 1 never actually built, confirmed directly against this repo before writing this package: every reconcile controller (internal/reconcile/application, internal/reconcile/database) takes a bare docker.Runtime, and cmd/levelrail/main.go's dynamicSource hands every controller the same single local docker.Runtime with no node concept anywhere. There is exactly one implicit node today.
This package makes that boundary explicit without moving it: Transport has exactly docker.Runtime's method set (see the type below for why), so nothing above this package changes shape yet. Wiring internal/reconcile controllers to actually route through a Transport selected per node, instead of the single shared docker.Runtime dynamicSource still hands out today, is separate work, once placement (which service runs on which node) exists to select by. The real gRPC implementation of Transport, reached over the reverse- dialed mTLS connection ADR 003 describes, is GRPCTransport (grpc_transport.go).
Index ¶
- Constants
- Variables
- func CertFingerprint(der []byte) string
- func Execute(ctx context.Context, rt docker.Runtime, req *agentpb.AgentRequest, ...) *agentpb.AgentResponse
- func IsAuthRejection(err error) bool
- func LocalAgentInfo() *agentpb.AgentInfo
- func NewKeyAndCSR(commonName string) (keyPEM, csrDER []byte, err error)
- func NewServerCredentials(ca *CA, hosts []string, validFor time.Duration) (credentials.TransportCredentials, error)
- func RunSession(ctx context.Context, addr string, id *Identity, rt docker.Runtime, ...) error
- type BuildDispatcher
- type BuildRelay
- type BuildRunner
- type CA
- func (ca *CA) CertPEM() []byte
- func (ca *CA) Fingerprint() string
- func (ca *CA) IssueClientCert(commonName string, validFor time.Duration) (certPEM, keyPEM []byte, err error)
- func (ca *CA) IssueServerCert(hosts []string, validFor time.Duration) (certPEM, keyPEM []byte, err error)
- func (ca *CA) KeyPEM() []byte
- func (ca *CA) SignClientCSR(commonName string, csrDER []byte, validFor time.Duration, now time.Time) (IssuedCert, error)
- type EnrollOption
- type EnrollStore
- type ExecRelay
- func (r *ExecRelay) Cancel(execID string)
- func (r *ExecRelay) CloseAll()
- func (r *ExecRelay) Credit(c *agentpb.ExecCredit)
- func (r *ExecRelay) Input(in *agentpb.ExecInput)
- func (r *ExecRelay) Resize(rz *agentpb.ExecResize)
- func (r *ExecRelay) Start(ctx context.Context, execID string, req *agentpb.ExecRequest)
- type GPUProbe
- type GPUSink
- type GRPCSink
- type GRPCTransport
- func (t *GRPCTransport) ApplyMesh(ctx context.Context, cfg network.DeviceConfig) (network.NodeIdentity, error)
- func (t *GRPCTransport) BuildOnNode(ctx context.Context, req build.RemoteRequest, image io.Writer, ...) (*build.Result, error)
- func (t *GRPCTransport) Create(ctx context.Context, spec docker.ContainerSpec) (string, error)
- func (t *GRPCTransport) EnsureNetwork(ctx context.Context, name string) (string, error)
- func (t *GRPCTransport) EnsureVolume(ctx context.Context, name string) error
- func (t *GRPCTransport) Events(ctx context.Context) (<-chan docker.Event, <-chan error)
- func (t *GRPCTransport) Exec(ctx context.Context, containerID string, cmd []string) (io.ReadCloser, error)
- func (t *GRPCTransport) ExecTTY(ctx context.Context, containerID string, opts docker.ExecTTYOptions) (docker.ExecSession, error)
- func (t *GRPCTransport) ExecWithInput(ctx context.Context, containerID string, cmd []string, stdin io.Reader) (io.ReadCloser, error)
- func (t *GRPCTransport) InspectByName(ctx context.Context, name string) (*docker.ContainerState, error)
- func (t *GRPCTransport) InspectExitState(ctx context.Context, name string) (*docker.ExitState, error)
- func (t *GRPCTransport) ListByPrefix(ctx context.Context, prefix string) ([]docker.ContainerState, error)
- func (t *GRPCTransport) ListImages(ctx context.Context, repo string) ([]docker.ImageInfo, error)
- func (t *GRPCTransport) ListNetworksByPrefix(ctx context.Context, prefix string) ([]docker.NetworkInfo, error)
- func (t *GRPCTransport) NetworkConnect(ctx context.Context, network, containerID string) error
- func (t *GRPCTransport) NetworkDisconnect(ctx context.Context, network, containerID string, force bool) error
- func (t *GRPCTransport) Remove(ctx context.Context, id string, force bool) error
- func (t *GRPCTransport) RemoveNetwork(ctx context.Context, name string) error
- func (t *GRPCTransport) RotateMeshKey(ctx context.Context) (network.RotationResult, error)
- func (t *GRPCTransport) Start(ctx context.Context, id string) error
- func (t *GRPCTransport) Stats(ctx context.Context, containerID string) (docker.ContainerStats, error)
- func (t *GRPCTransport) Stop(ctx context.Context, id string, timeout time.Duration) error
- func (t *GRPCTransport) UpdateResources(ctx context.Context, id string, resources docker.Resources) error
- type Identity
- type IdentityChecker
- type IdentityFile
- func (f *IdentityFile) CheckWritable() error
- func (f *IdentityFile) Confirm() error
- func (f *IdentityFile) HasStaged() bool
- func (f *IdentityFile) Load() (*Identity, error)
- func (f *IdentityFile) LoadPrev() (*Identity, error)
- func (f *IdentityFile) PrevPath() string
- func (f *IdentityFile) ResolveStaged(ctx context.Context, check func(context.Context, *Identity) error) (*Identity, error)
- func (f *IdentityFile) Rollback() error
- func (f *IdentityFile) Save(id *Identity) error
- func (f *IdentityFile) Stage(next *Identity) error
- type IdentityHolder
- type IdentityPersister
- type IssuedCert
- type Local
- func (l Local) ExecTTY(ctx context.Context, containerID string, opts docker.ExecTTYOptions) (docker.ExecSession, error)
- func (l Local) InspectExitState(ctx context.Context, name string) (*docker.ExitState, error)
- func (l Local) InspectImageID(ctx context.Context, ref string) (string, error)
- func (l Local) Stats(ctx context.Context, containerID string) (docker.ContainerStats, error)
- type MeshApplier
- type Option
- type Registry
- type RemoteBuilder
- type RenewConfig
- type Renewer
- type Server
- func (s *Server) CheckIdentity(ctx context.Context, _ *agentpb.CheckIdentityRequest) (*agentpb.CheckIdentityResponse, error)
- func (s *Server) Disconnect(nodeID string)
- func (s *Server) Enroll(ctx context.Context, req *agentpb.EnrollRequest) (*agentpb.EnrollResponse, error)
- func (s *Server) NotifyShutdown()
- func (s *Server) Reenroll(ctx context.Context, req *agentpb.ReenrollRequest) (*agentpb.ReenrollResponse, error)
- func (s *Server) Renew(ctx context.Context, req *agentpb.RenewRequest) (*agentpb.RenewResponse, error)
- func (s *Server) Session(stream agentpb.AgentService_SessionServer) error
- type SessionOption
- func WithBuildRunner(runner BuildRunner) SessionOption
- func WithGPUProbe(p GPUProbe) SessionOption
- func WithHeartbeatInterval(d time.Duration) SessionOption
- func WithKeepalive(pingTime, timeout time.Duration) SessionOption
- func WithMesh(nodeID string, mesh MeshApplier) SessionOption
- func WithRenewer(r *Renewer) SessionOption
- type Transport
Constants ¶
const ( DefaultClientCertValidity = 90 * 24 * time.Hour DefaultCertRenewGrace = 24 * time.Hour )
Defaults for the env-tunable certificate settings (ADR 021).
const (
DefaultRenewFraction = 2.0 / 3.0
)
Defaults for RenewConfig. Renewing at two thirds of the lifetime leaves the last third to retry through outages (the step-ca and cert-manager default).
Variables ¶
var ErrBuildWindowViolated = errors.New("agent: build peer exceeded its flow-control window")
ErrBuildWindowViolated ends a dispatched build whose peer sent more unacknowledged data frames than buildWindowFrames allows, for the same reason ErrExecWindowViolated ends an exec.
var ErrCAFingerprintMismatch = errors.New("agent: control plane CA does not match the pinned fingerprint")
ErrCAFingerprintMismatch is returned by DialEnroll when the control plane's CA does not match the pinned fingerprint.
var ErrExecWindowViolated = errors.New("agent: exec peer exceeded its flow-control window")
ErrExecWindowViolated ends an exec stream whose peer sent more unacknowledged data frames than execWindowFrames allows. Failing the one exec is the only safe response: blocking recvLoop would stall every other call sharing this session, and dropping the frame would corrupt the stream silently.
var ErrExitStateUnsupported = errors.New("agent: this node's container runtime cannot inspect container exit state")
ErrExitStateUnsupported is what an InspectExitState request gets when this node's runtime cannot inspect a container's exit state. Callers treat any error here as "can't tell" and fall back to waiting out the full readiness budget.
var ErrInvalidCSR = errors.New("agent: invalid certificate signing request")
ErrInvalidCSR is returned by SignClientCSR for a malformed request, a bad signature, or a key type this CA does not issue for.
ErrMeshUnavailable is what an ApplyMesh/RotateMeshKey request gets when this node's Session was started with no MeshApplier wired up (RunSession's own WithMesh option never called): mesh networking not enabled for this node, or this agent process predates mesh support. The control plane's Coordinator treats this the same as any other per-node ApplyMesh failure (NodeResult.Err), never a fleet-wide error.
var ErrNodeNotMeshCapable = fmt.Errorf("network: node's transport does not support mesh operations")
ErrNodeNotMeshCapable is returned when nodeID's registered transport exists (the node is connected) but does not support mesh operations, which in practice means it isn't really a *GRPCTransport at all (a test double registered directly, or a future transport kind that never implements mesh). Never expected against a real agent connection, since every real one is built by newGRPCTransport.
var ErrNodeNotRegistered = errors.New("agent: node not registered in this transport registry")
ErrNodeNotRegistered is Registry.Get's failure mode for an unknown node ID.
var ErrSessionClosed = errors.New("agent: session closed")
ErrSessionClosed is returned by mux.Call once the underlying stream has ended, for any pending or future call.
var ErrStatsUnsupported = errors.New("agent: this node's container runtime cannot report container stats")
ErrStatsUnsupported is what a Stats request gets when this node's runtime cannot report container resource usage.
var ErrTTYUnsupported = errors.New("agent: this node's container runtime does not support interactive exec")
ErrTTYUnsupported is what an ExecRequest asking for a PTY gets when this agent's runtime has no interactive exec to offer.
Functions ¶
func CertFingerprint ¶
CertFingerprint returns a stable, hex-encoded SHA-256 digest of a DER-encoded certificate: store.Node.CertFingerprint's own value, and what the control plane's Session handler recomputes from an incoming mTLS connection's actual peer certificate to confirm it matches the fingerprint recorded at enrollment, rather than trusting the certificate's CommonName field alone (which a differently-issued certificate could also claim).
func Execute ¶
func Execute(ctx context.Context, rt docker.Runtime, req *agentpb.AgentRequest, emitEvent func(*agentpb.ProxiedEvent)) *agentpb.AgentResponse
Execute runs one AgentRequest against rt and returns the matching AgentResponse, always carrying req.RequestId. A docker.Runtime failure becomes AgentResponse.Error (a plain string over the wire, deliberately: this is a control-plane-to-agent RPC boundary, not a package boundary within one Go process, so wrapping with %w has nothing left to attach to on the receiving end; GRPCTransport reconstructs a plain error from this string instead), never a Go error return of Execute's own: this function's contract is "always produce exactly one response for exactly one request," success or failure alike, so the caller always has something to send back.
ctx is the whole Session's own lifetime, not a fresh per-call one: WatchEvents in particular needs to keep running past this one call returning, for as long as the session itself stays open, and using the same ctx for every other operation keeps "when does this stop" answered identically everywhere in this function rather than inventing a second, shorter-lived context needing its own justification. emitEvent is only ever called from the WatchEvents path, unprompted, for as long as ctx stays alive.
func IsAuthRejection ¶ added in v0.2.0
IsAuthRejection is isAuthRejection for callers outside this package.
func LocalAgentInfo ¶ added in v0.2.0
LocalAgentInfo describes this agent binary and the machine it runs on.
func NewKeyAndCSR ¶ added in v0.2.0
NewKeyAndCSR generates a fresh ed25519 private key on this machine and a CSR for it. The key never leaves the caller; only the CSR is sent.
func NewServerCredentials ¶
func NewServerCredentials(ca *CA, hosts []string, validFor time.Duration) (credentials.TransportCredentials, error)
NewServerCredentials issues a fresh server certificate from ca (covering hosts) and returns grpc TransportCredentials configured for this package's mTLS model: a client certificate is verified when presented but not required (VerifyClientCertIfGiven), because Enroll (ADR 003's join-token exchange) is called with no client certificate at all, by design (DialEnroll's own doc comment explains why), while Session always presents one, verified against ca when it does.
Must be passed to grpc.NewServer via grpc.Creds, not used to build a raw tls.Listener handed to Serve directly: grpc-go's own peer-info extraction (what Server.Session's peerIdentity relies on to read the client certificate back out of a request's context) only populates correctly when grpc's credentials layer performs the handshake itself. A raw tls.Listener does the TLS handshake transparently at the net.Conn level, which works for plain data transfer but leaves grpc with no TLS state to attach to the connection's context, confirmed the hard way: an earlier version of this wiring, tested against a real listener, failed with "no peer TLS info on this connection" on every real mTLS connection until this file switched to grpc.Creds.
func RunSession ¶
func RunSession(ctx context.Context, addr string, id *Identity, rt docker.Runtime, logger *slog.Logger, opts ...SessionOption) error
RunSession dials addr with id's mTLS credentials (verified against id.CACertPEM, closing DialEnroll's own TOFU window), opens the one persistent Session stream ADR 003 describes, and serves every incoming AgentRequest against rt until ctx is cancelled or the connection fails. Returns the error that ended the session (nil only if ctx itself was the cause); never retries or reconnects on its own, that's cmd/levelrail-agent's own reconnect loop's job (ADR 003's Consequences section's own "real, tested" reconnection/backpressure/version-negotiation requirement), kept out of this function so it stays a single, directly testable connection attempt rather than a policy about how many times or how fast to retry.
Types ¶
type BuildDispatcher ¶ added in v0.2.0
type BuildDispatcher struct {
// contains filtered or unexported fields
}
BuildDispatcher resolves a node ID to that node's session and runs a build there. Satisfies internal/build.NodeBuilder, which is how build.Router reaches a node without knowing anything about transports.
func NewBuildDispatcher ¶ added in v0.2.0
func NewBuildDispatcher(registry *Registry) *BuildDispatcher
NewBuildDispatcher builds a dispatcher over registry.
func (*BuildDispatcher) BuildOnNode ¶ added in v0.2.0
func (d *BuildDispatcher) BuildOnNode(ctx context.Context, nodeID string, req build.RemoteRequest, image io.Writer, progress func(build.ProgressEvent)) (*build.Result, error)
BuildOnNode implements internal/build.NodeBuilder.
type BuildRelay ¶ added in v0.2.0
type BuildRelay struct {
// contains filtered or unexported fields
}
BuildRelay owns the agent side of every in-flight dispatched build on one Session, the same per-session state ExecRelay holds for exec.
func NewBuildRelay ¶ added in v0.2.0
func NewBuildRelay(runner BuildRunner, send func(*agentpb.AgentMessage)) *BuildRelay
NewBuildRelay builds a relay running builds through runner and writing its frames through send, which must serialize writes to the Session stream. A nil runner is valid: every dispatched build is then rejected with a clear error.
func (*BuildRelay) Cancel ¶ added in v0.2.0
func (r *BuildRelay) Cancel(buildID string)
Cancel stops a build the control plane no longer wants. No terminal frame follows: the caller that cancelled is not reading anymore.
func (*BuildRelay) CloseAll ¶ added in v0.2.0
func (r *BuildRelay) CloseAll()
CloseAll stops every in-flight build, for a session that is ending.
func (*BuildRelay) Credit ¶ added in v0.2.0
func (r *BuildRelay) Credit(c *agentpb.BuildCredit)
Credit records the control plane's refund of this build's output window.
func (*BuildRelay) Input ¶ added in v0.2.0
func (r *BuildRelay) Input(in *agentpb.BuildInput)
Input routes one build-context frame to its build.
func (*BuildRelay) Start ¶ added in v0.2.0
func (r *BuildRelay) Start(ctx context.Context, buildID string, req *agentpb.BuildRequest)
Start begins the build identified by buildID (the request's own RequestId, which also tags every frame of this build). It returns immediately: creating the context directory and running the build both happen off the session's receive loop.
type BuildRunner ¶ added in v0.2.0
type BuildRunner interface {
SolveRemote(ctx context.Context, req build.RemoteRequest, out io.Writer, progress func(build.ProgressEvent)) (*build.Result, error)
}
BuildRunner is what BuildRelay needs from internal/build to actually run a dispatched build. *build.Client satisfies it; an agent with no reachable BuildKit is constructed without one and rejects dispatched builds explicitly rather than failing halfway through one.
type CA ¶
type CA struct {
// contains filtered or unexported fields
}
CA is the control plane's own certificate authority: one self-signed root, generated once (or loaded from disk across restarts) and used to sign both the control plane's own gRPC server certificate and every enrolling agent's client certificate.
func GenerateCA ¶
GenerateCA creates a new self-signed CA. ed25519 throughout this package (CA and every leaf certificate): fast key generation, small keys, and full support in Go's crypto/tls for both signing and TLS handshakes, with none of RSA's parameter-size decisions to make.
func LoadCA ¶
LoadCA parses a CA previously persisted via CertPEM/KeyPEM, the counterpart GenerateCA's caller needs across a control plane restart: generating a fresh CA on every startup would invalidate every already-enrolled agent's certificate for no reason.
func (*CA) CertPEM ¶
CertPEM returns the CA's own certificate, PEM-encoded: what every agent needs to verify the control plane's server certificate (returned to an enrolling agent as EnrollResponse.CaCertPem), and what this process needs to persist to disk to survive a restart via LoadCA.
func (*CA) Fingerprint ¶ added in v0.2.0
Fingerprint returns the hex SHA-256 of the CA certificate's DER, the value an agent pins at enrollment (APP_CA_FINGERPRINT).
func (*CA) IssueClientCert ¶
func (ca *CA) IssueClientCert(commonName string, validFor time.Duration) (certPEM, keyPEM []byte, err error)
IssueClientCert issues a client certificate identifying commonName (a node's store.Node.ID), signed by ca, for the enrollment flow. validFor has no auto-renewal built in here: certificate rotation on a schedule is real, named Phase 3 scope (ADR 003's Consequences section), a caller-level concern layered on top of this primitive, not solved by this one function alone.
func (*CA) IssueServerCert ¶
func (ca *CA) IssueServerCert(hosts []string, validFor time.Duration) (certPEM, keyPEM []byte, err error)
IssueServerCert issues the control plane's own gRPC listener TLS certificate, signed by ca, valid for the given hostnames/IPs agents will dial. Not agent-facing in the enrollment response the way IssueClientCert's output is: the control plane loads this cert locally for its own gRPC server, per cmd/levelrail's own startup wiring, not built here.
func (*CA) KeyPEM ¶
KeyPEM returns the CA's own private key, PEM-encoded, PKCS#8: the other half LoadCA needs. Never sent over the wire to an agent, only ever persisted locally by the control plane's own startup code.
func (*CA) SignClientCSR ¶ added in v0.2.0
func (ca *CA) SignClientCSR(commonName string, csrDER []byte, validFor time.Duration, now time.Time) (IssuedCert, error)
SignClientCSR issues a client certificate for commonName over the CSR's public key. The CSR's own subject, SANs and extensions are ignored: the control plane alone decides what the certificate asserts.
type EnrollOption ¶ added in v0.2.0
type EnrollOption func(*enrollConfig)
EnrollOption configures DialEnroll.
func WithPinnedCAFingerprint ¶ added in v0.2.0
func WithPinnedCAFingerprint(fingerprint string) EnrollOption
WithPinnedCAFingerprint makes DialEnroll refuse any control plane whose CA certificate's SHA-256 (hex, colons optional) is not fingerprint.
func WithTrustedCA ¶ added in v0.2.0
func WithTrustedCA(caPEM []byte) EnrollOption
WithTrustedCA verifies the control plane against caPEM, the CA an already-enrolled node has on disk, instead of a fingerprint pin.
type EnrollStore ¶
type EnrollStore interface {
GetNodeJoinTokenByHash(ctx context.Context, hash string) (*store.NodeJoinToken, error)
MarkNodeJoinTokenUsed(ctx context.Context, id string) error
SaveNode(ctx context.Context, n store.Node) error
GetNode(ctx context.Context, id string) (*store.Node, error)
UpdateNodeStatus(ctx context.Context, id string, status store.NodeStatus) error
TouchNodeLastSeen(ctx context.Context, id string) error
RotateNodeCert(ctx context.Context, id, presentedFingerprint string, c store.NodeCert, grace time.Duration, now time.Time) error
ReenrollNodeCert(ctx context.Context, id string, c store.NodeCert, now time.Time) error
SyncNodeCertDetails(ctx context.Context, id, fingerprint, serial string, notAfter time.Time) error
UpdateNodeAgentInfo(ctx context.Context, id string, info store.NodeAgentInfo, now time.Time) error
}
EnrollStore is the store surface Server needs. *store.DB satisfies it.
type ExecRelay ¶ added in v0.2.0
type ExecRelay struct {
// contains filtered or unexported fields
}
ExecRelay owns the agent side of every in-flight remote exec on one Session: it runs the command against the local docker.Runtime, streams its output back frame by frame under the control plane's flow-control window, feeds arriving stdin frames into the command as they land, and stops the local exec when the control plane cancels or the session ends. Unlike every other operation, an exec spans many frames in both directions, so it needs per-session state Execute's stateless dispatch cannot hold.
func NewExecRelay ¶ added in v0.2.0
func NewExecRelay(rt docker.Runtime, send func(*agentpb.AgentMessage)) *ExecRelay
NewExecRelay builds a relay running commands against rt and writing its frames through send, which must serialize writes to the Session stream.
func (*ExecRelay) Cancel ¶ added in v0.2.0
Cancel stops an exec the control plane no longer wants. No terminal frame follows: the caller that cancelled is not reading anymore.
func (*ExecRelay) CloseAll ¶ added in v0.2.0
func (r *ExecRelay) CloseAll()
CloseAll stops every in-flight exec, for a session that is ending.
func (*ExecRelay) Credit ¶ added in v0.2.0
func (r *ExecRelay) Credit(c *agentpb.ExecCredit)
Credit records the control plane's refund of this exec's output window.
func (*ExecRelay) Resize ¶ added in v0.2.0
func (r *ExecRelay) Resize(rz *agentpb.ExecResize)
Resize records the newest terminal size for a PTY exec. A no-op for an exec that never asked for one, or that has already ended.
func (*ExecRelay) Start ¶ added in v0.2.0
Start begins the exec identified by execID (the request's own RequestId, which also tags every frame of this exec). It returns immediately: attaching to the container is a Docker round trip, and doing it inline would stall the session's whole receive loop. The acknowledgment, or the error that stopped the exec from attaching, is sent once that round trip finishes.
type GRPCSink ¶ added in v0.2.0
type GRPCSink struct {
// contains filtered or unexported fields
}
GRPCSink dispatches mesh config to remote nodes over their own agent Session stream, resolved per call through registry.
func NewGRPCSink ¶ added in v0.2.0
NewGRPCSink builds a GRPCSink that resolves each node's transport through registry, the same *Registry Server.Session already populates (server.go) and every other remote-dispatch path in this package (GRPCTransport itself, build dispatch) already shares.
func (*GRPCSink) ApplyMesh ¶ added in v0.2.0
func (s *GRPCSink) ApplyMesh(ctx context.Context, nodeID string, cfg network.DeviceConfig) (network.NodeIdentity, error)
ApplyMesh implements network.ConfigSink by forwarding cfg to nodeID's live agent Session, if it has one. A node with no registered transport (never enrolled, or disconnected) fails this one node's pass with agent.ErrNodeNotRegistered wrapped, exactly the "one node's problem, never the fleet's" contract ConfigSink.ApplyMesh's own doc comment requires: Distribute records it in that node's NodeResult.Err and keeps distributing to every other node.
type GRPCTransport ¶
type GRPCTransport struct {
// contains filtered or unexported fields
}
GRPCTransport implements Transport by dispatching every call over an established Session stream to a real, remote agent. Built from a *mux, not a raw stream: the request/response multiplexing this type depends on is already handled underneath it. Unexported constructor (newGRPCTransport): callers get one of these from the control plane's Session handler once an agent's stream is accepted and authenticated (server.go's remaining wiring), never constructed directly.
func (*GRPCTransport) ApplyMesh ¶ added in v0.2.0
func (t *GRPCTransport) ApplyMesh(ctx context.Context, cfg network.DeviceConfig) (network.NodeIdentity, error)
ApplyMesh sends cfg to this transport's own node over its Session stream and returns what the node reported back about itself. Part of meshTransport (mesh_sink.go), not Transport itself: mesh operations are not a docker.Runtime concern, so keeping them off Transport's own interface means a caller that only wants container operations never has to know mesh dispatch exists.
func (*GRPCTransport) BuildOnNode ¶ added in v0.2.0
func (t *GRPCTransport) BuildOnNode(ctx context.Context, req build.RemoteRequest, image io.Writer, progress func(build.ProgressEvent)) (*build.Result, error)
BuildOnNode implements RemoteBuilder: it dispatches req to this transport's node, streams req.ContextDir up as a tar, relays progress through progress, and writes the docker-save image tar the build produced into image. The image is never loaded on the build node: the control plane that asked for the build is the one that wants it.
func (*GRPCTransport) Create ¶
func (t *GRPCTransport) Create(ctx context.Context, spec docker.ContainerSpec) (string, error)
Create implements Transport (docker.Runtime).
func (*GRPCTransport) EnsureNetwork ¶
EnsureNetwork implements Transport (docker.Runtime).
func (*GRPCTransport) EnsureVolume ¶
func (t *GRPCTransport) EnsureVolume(ctx context.Context, name string) error
EnsureVolume implements Transport (docker.Runtime).
func (*GRPCTransport) Events ¶
Events implements Transport (docker.Runtime). The one method that doesn't fit mux.Call's request/response shape: docker.Runtime.Events returns two channels streaming until ctx is cancelled, not one response. This issues a single WatchEvents request (acked immediately like any other Empty-result call, then relays every ProxiedEvent mux.subscribe's channel receives onto a fresh pair of channels this method returns, translating docker.Event's wire shape back and closing both channels once ctx is done or the subscription itself ends (session closed, agent-side stream error). ADR 003's push-not- poll event design, real end to end.
Backpressure: mux's eventChanBuffer (64) bounds how far this can fall behind the agent's own event rate before frames start being dropped rather than blocking the shared recvLoop; a caller needing a guarantee stronger than "recent events, best effort" would need a different mechanism, not built here.
func (*GRPCTransport) Exec ¶
func (t *GRPCTransport) Exec(ctx context.Context, containerID string, cmd []string) (io.ReadCloser, error)
Exec implements Transport (docker.Runtime). The initial round trip is synchronous, matching docker.Client.Exec's own behavior: an exec that cannot attach (no such container, daemon refused) fails here rather than on a later Read. Output arrives afterwards as ExecOutput frames, reassembled by the returned stream.
func (*GRPCTransport) ExecTTY ¶ added in v0.2.0
func (t *GRPCTransport) ExecTTY(ctx context.Context, containerID string, opts docker.ExecTTYOptions) (docker.ExecSession, error)
ExecTTY implements docker.TTYRuntime. The PTY lives on the agent's side; this end is the same windowed frame stream every other exec uses, plus a resize frame, so a remote terminal and a local one differ only in how far the bytes travel.
func (*GRPCTransport) ExecWithInput ¶
func (t *GRPCTransport) ExecWithInput(ctx context.Context, containerID string, cmd []string, stdin io.Reader) (io.ReadCloser, error)
ExecWithInput implements Transport (docker.Runtime). Exec plus a stdin direction: a goroutine drains stdin into ExecInput frames while output frames stream back, the same concurrency docker.Client.ExecWithInput needs locally to keep a command that writes while it reads from deadlocking.
func (*GRPCTransport) InspectByName ¶
func (t *GRPCTransport) InspectByName(ctx context.Context, name string) (*docker.ContainerState, error)
InspectByName implements Transport (docker.Runtime).
func (*GRPCTransport) InspectExitState ¶ added in v0.2.0
func (t *GRPCTransport) InspectExitState(ctx context.Context, name string) (*docker.ExitState, error)
InspectExitState implements docker.ExitStateInspector over the wire, so a service placed on a remote node gets the same fail-fast-on-crash readiness behavior a locally placed one already had: without it, a container that OOM-killed itself mid-readiness looked merely "not ready yet" until the whole readiness budget expired.
An agent too old to know this op answers with an error, which every caller of docker.ExitStateInspector already treats as "can't tell" and degrades to waiting out the full budget.
func (*GRPCTransport) ListByPrefix ¶
func (t *GRPCTransport) ListByPrefix(ctx context.Context, prefix string) ([]docker.ContainerState, error)
ListByPrefix implements Transport (docker.Runtime).
func (*GRPCTransport) ListImages ¶
ListImages implements Transport (docker.Runtime).
func (*GRPCTransport) ListNetworksByPrefix ¶
func (t *GRPCTransport) ListNetworksByPrefix(ctx context.Context, prefix string) ([]docker.NetworkInfo, error)
ListNetworksByPrefix implements Transport (docker.Runtime).
func (*GRPCTransport) NetworkConnect ¶ added in v0.2.0
func (t *GRPCTransport) NetworkConnect(ctx context.Context, network, containerID string) error
NetworkConnect implements Transport (docker.Runtime).
func (*GRPCTransport) NetworkDisconnect ¶ added in v0.2.0
func (t *GRPCTransport) NetworkDisconnect(ctx context.Context, network, containerID string, force bool) error
NetworkDisconnect implements Transport (docker.Runtime).
func (*GRPCTransport) RemoveNetwork ¶
func (t *GRPCTransport) RemoveNetwork(ctx context.Context, name string) error
RemoveNetwork implements Transport (docker.Runtime).
func (*GRPCTransport) RotateMeshKey ¶ added in v0.2.0
func (t *GRPCTransport) RotateMeshKey(ctx context.Context) (network.RotationResult, error)
RotateMeshKey asks this transport's own node to rotate its mesh key and returns the change. Part of meshTransport, same reasoning as ApplyMesh above.
func (*GRPCTransport) Start ¶
func (t *GRPCTransport) Start(ctx context.Context, id string) error
Start implements Transport (docker.Runtime).
func (*GRPCTransport) Stats ¶ added in v0.2.0
func (t *GRPCTransport) Stats(ctx context.Context, containerID string) (docker.ContainerStats, error)
Stats implements docker.StatsInspector over the wire: the remote-node half of the control plane's own telemetry.Collector, which otherwise only ever sees this process's own local Docker socket. An agent too old to know this op answers with ErrStatsUnsupported's own string, which the collector's caller treats as "skip this target, try again next tick," the same degrade InspectExitState's doc comment describes for an old agent.
func (*GRPCTransport) Stop ¶
Stop implements Transport (docker.Runtime). timeout < 0 (wait indefinitely, docker.Runtime.Stop's own convention) is carried across the wire as TimeoutMs: -1, not clamped to 0, so the agent applies the identical "wait indefinitely" semantics rather than a silently different one.
func (*GRPCTransport) UpdateResources ¶
func (t *GRPCTransport) UpdateResources(ctx context.Context, id string, resources docker.Resources) error
UpdateResources implements Transport (docker.Runtime).
type Identity ¶
Identity is what an enrolled node needs to reconnect: its own client certificate/key and the control plane's CA certificate, all PEM, exactly EnrollResponse's three credential fields. cmd/levelrail-agent owns persisting/loading this to/from disk across restarts; this package only consumes it.
func DialEnroll ¶
func DialEnroll(ctx context.Context, addr, joinToken, nodeName string, opts ...EnrollOption) (*Identity, error)
DialEnroll connects to addr and exchanges joinToken for an Identity. The private key is generated here and only a CSR is sent (ADR 021); a control plane too old to sign CSRs returns a key of its own instead, which is used as is. There is no CA on disk yet, so the server is verified against the pinned CA fingerprint when one is given (see ADR 003), and trusted on first use otherwise.
func DialReenroll ¶ added in v0.2.0
func DialReenroll(ctx context.Context, addr, token, nodeID string, opts ...EnrollOption) (*Identity, error)
DialReenroll exchanges a re-enrollment token for a new certificate for an existing node, keeping its identity. nodeID may be empty when the old identity file is gone; the token alone names the node.
type IdentityChecker ¶ added in v0.2.0
IdentityChecker proves an identity is accepted by the control plane without opening a Session.
func CheckIdentityAt ¶ added in v0.2.0
func CheckIdentityAt(addr string) IdentityChecker
CheckIdentityAt returns an IdentityChecker that dials addr presenting the identity and calls CheckIdentity, requiring it to be current.
type IdentityFile ¶ added in v0.2.0
type IdentityFile struct {
Path string
// contains filtered or unexported fields
}
IdentityFile persists an agent Identity as JSON (0600). Writes go to a temp file that is fsynced and renamed over the target, so a crash leaves either the old or the new identity on disk, never a torn one. During a renewal the previous identity is kept beside it as <path>.prev until the new certificate has been proven to work.
func NewIdentityFile ¶ added in v0.2.0
func NewIdentityFile(path string) *IdentityFile
NewIdentityFile returns an IdentityFile for path.
func (*IdentityFile) CheckWritable ¶ added in v0.2.0
func (f *IdentityFile) CheckWritable() error
CheckWritable proves the identity file's directory accepts new files by creating and removing a probe, so enrollment can fail before it spends the one-time join token on an identity that could never be saved.
func (*IdentityFile) Confirm ¶ added in v0.2.0
func (f *IdentityFile) Confirm() error
Confirm drops the pre-renewal backup once the new identity works.
func (*IdentityFile) HasStaged ¶ added in v0.2.0
func (f *IdentityFile) HasStaged() bool
HasStaged reports whether a renewal was staged but never confirmed or rolled back, meaning the agent stopped in between.
func (*IdentityFile) Load ¶ added in v0.2.0
func (f *IdentityFile) Load() (*Identity, error)
Load reads the identity and removes temp files a crashed write left behind. It returns an error wrapping os.ErrNotExist when none exists.
func (*IdentityFile) LoadPrev ¶ added in v0.2.0
func (f *IdentityFile) LoadPrev() (*Identity, error)
LoadPrev reads the pre-renewal identity kept at PrevPath.
func (*IdentityFile) PrevPath ¶ added in v0.2.0
func (f *IdentityFile) PrevPath() string
PrevPath is where the pre-renewal identity is kept.
func (*IdentityFile) ResolveStaged ¶ added in v0.2.0
func (f *IdentityFile) ResolveStaged(ctx context.Context, check func(context.Context, *Identity) error) (*Identity, error)
ResolveStaged settles a renewal the agent stopped in the middle of: the staged identity is kept if check accepts it, otherwise the backup is restored. It returns the identity to use.
func (*IdentityFile) Rollback ¶ added in v0.2.0
func (f *IdentityFile) Rollback() error
Rollback restores the pre-renewal identity over the current one.
func (*IdentityFile) Save ¶ added in v0.2.0
func (f *IdentityFile) Save(id *Identity) error
Save atomically replaces the identity on disk with id.
func (*IdentityFile) Stage ¶ added in v0.2.0
func (f *IdentityFile) Stage(next *Identity) error
Stage keeps the current identity file as PrevPath, then saves next. Confirm or Rollback completes it.
type IdentityHolder ¶ added in v0.2.0
type IdentityHolder struct {
// contains filtered or unexported fields
}
IdentityHolder is the agent's in-memory current identity, swapped after a renewal so the next connection presents the new certificate without a restart.
func NewIdentityHolder ¶ added in v0.2.0
func NewIdentityHolder(id *Identity) *IdentityHolder
NewIdentityHolder wraps id.
func (*IdentityHolder) Current ¶ added in v0.2.0
func (h *IdentityHolder) Current() *Identity
Current returns the identity in use.
func (*IdentityHolder) Set ¶ added in v0.2.0
func (h *IdentityHolder) Set(id *Identity)
Set replaces the identity in use.
type IdentityPersister ¶ added in v0.2.0
IdentityPersister stages a renewed identity durably and then confirms or rolls it back. *IdentityFile implements it.
type IssuedCert ¶ added in v0.2.0
IssuedCert is a signed client certificate plus the fields the control plane records about it.
type Local ¶
Local adapts a docker.Runtime this process already has open (this machine's own Docker socket, via docker.NewClient) into a Transport for this process's own node. This is the node-communication design's "single-node mode... in-memory transport that implements the same interface the gRPC transport implements," made real: every method call happens in-process, no network, no serialization, and (because Transport is exactly docker.Runtime's shape) any docker.Runtime value already satisfies Transport structurally without needing this wrapper type at all. Local exists anyway, as a named, documented adapter, so call sites read "this is a node transport" rather than relying on Go's structural typing to make that intent legible.
func (Local) ExecTTY ¶ added in v0.2.0
func (l Local) ExecTTY(ctx context.Context, containerID string, opts docker.ExecTTYOptions) (docker.ExecSession, error)
ExecTTY implements docker.TTYRuntime by forwarding to the wrapped runtime, which the embedded docker.Runtime interface would otherwise hide even when the concrete value behind it supports interactive exec.
func (Local) InspectExitState ¶ added in v0.2.0
InspectExitState implements docker.ExitStateInspector by forwarding to the wrapped runtime, which the embedded docker.Runtime interface would otherwise hide even when the concrete value behind it can inspect exit state.
func (Local) InspectImageID ¶ added in v0.2.0
InspectImageID implements docker.ImageInspector by forwarding; an unsupporting runtime reports "" so digest verification is skipped.
type MeshApplier ¶ added in v0.2.0
type MeshApplier interface {
ApplyMesh(ctx context.Context, nodeID string, cfg network.DeviceConfig) (network.NodeIdentity, error)
RotateKey(ctx context.Context, nodeID string) (network.RotationResult, error)
}
MeshApplier is what serveSession needs to apply this node's own mesh config and rotate its own mesh key. Exactly *network.LocalSink's own two methods (ApplyMesh, RotateKey): a real WireGuard device wired up by cmd/levelrail-agent satisfies this by constructing a *network.LocalSink around its own network.Mesh and private key, with zero adapter code needed, the identical reuse the control plane's own setupMesh (cmd/levelrail/mesh.go) already gets from LocalSink for its own local node. See RunSession's WithMesh option for how this and this node's own ID reach serveSession.
type Option ¶
type Option func(*Server)
Option configures optional Server behavior.
func WithCertValidity ¶ added in v0.2.0
WithCertValidity sets how long issued agent certificates stay valid.
func WithGPUSink ¶ added in v0.2.0
WithGPUSink persists GPU reports agents send up their Session stream.
func WithRenewGrace ¶ added in v0.2.0
WithRenewGrace sets how long a renewed node's previous certificate stays accepted, so a renewal whose response was lost cannot lock the node out.
func WithRequireCSR ¶ added in v0.2.0
WithRequireCSR refuses enrollments from agents that do not send a CSR, closing the legacy path where the control plane generates the key.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry resolves a node ID to the Transport that reaches it. A single-node deployment registers exactly one entry (its own Local transport, dynamicSource's own local docker.Runtime wrapped per Local's doc comment); a multi-node deployment registers one entry per enrolled node, most of them real gRPC transports. Registry itself has no opinion on how a node's transport got built or how long it should live, only "given an ID, hand back the Transport for it," the same "pure lookup, not a decision" shape GetAPITokenByHash establishes for a different resource in internal/store.
func (*Registry) Register ¶
Register associates nodeID with t, replacing any existing entry for that ID. Safe for concurrent use.
func (*Registry) Resolve ¶ added in v0.2.0
Resolve is Get plus the local node's own ID, for callers that just need a runtime for whichever node a resource is placed on.
func (*Registry) SetLocal ¶ added in v0.2.0
SetLocal makes Resolve answer localID with t, this process's own runtime. It is deliberately not in byID: Get callers (build dispatch, mesh sink) must keep treating the local node as "not a remote agent".
func (*Registry) Unregister ¶
Unregister removes nodeID's entry, if any. Not an error if nodeID was never registered, matching this codebase's established idempotent- delete convention (store.DeleteDesiredService, store.DeleteNode).
type RemoteBuilder ¶ added in v0.2.0
type RemoteBuilder interface {
BuildOnNode(ctx context.Context, req build.RemoteRequest, image io.Writer, progress func(build.ProgressEvent)) (*build.Result, error)
}
RemoteBuilder is the optional capability a Transport advertises when it can run a build dispatched to its node. Only *GRPCTransport does: a Local transport is this process's own Docker socket, which is what "build locally" already means, not somewhere to dispatch to.
type RenewConfig ¶ added in v0.2.0
type RenewConfig struct {
// Fraction of the certificate's lifetime after which renewal starts.
Fraction float64
// Jitter is the fraction of lifetime added at random so nodes enrolled
// together do not all renew at once.
Jitter float64
RetryMin, RetryMax time.Duration
}
RenewConfig tunes when and how often the agent renews its certificate.
type Renewer ¶ added in v0.2.0
type Renewer struct {
// contains filtered or unexported fields
}
Renewer keeps an agent's certificate fresh over its authenticated connection (ADR 021).
func NewRenewer ¶ added in v0.2.0
func NewRenewer(holder *IdentityHolder, persist IdentityPersister, check IdentityChecker, cfg RenewConfig, logger *slog.Logger) *Renewer
NewRenewer builds a Renewer. check defaults to CheckIdentityAt(addr) when nil.
func (*Renewer) Holder ¶ added in v0.2.0
func (r *Renewer) Holder() *IdentityHolder
Holder returns the identity holder the renewer updates.
func (*Renewer) RenewAt ¶ added in v0.2.0
RenewAt is when id should be renewed: Fraction of its lifetime plus up to Jitter more.
func (*Renewer) RenewOnce ¶ added in v0.2.0
RenewOnce generates a fresh key, has the control plane sign it, persists the result, and proves it works before switching to it. On any failure the agent keeps using its current certificate, which the control plane still accepts inside the renewal overlap window.
type Server ¶
type Server struct {
agentpb.UnimplementedAgentServiceServer
// contains filtered or unexported fields
}
Server implements agentpb.AgentServiceServer.
func NewServer ¶
func NewServer(ca *CA, st EnrollStore, registry *Registry, logger *slog.Logger, opts ...Option) *Server
NewServer builds a Server. logger defaults to slog.Default() if nil.
func (*Server) CheckIdentity ¶ added in v0.2.0
func (s *Server) CheckIdentity(ctx context.Context, _ *agentpb.CheckIdentityRequest) (*agentpb.CheckIdentityResponse, error)
CheckIdentity implements agentpb.AgentServiceServer.
func (*Server) Disconnect ¶ added in v0.2.0
Disconnect ends nodeID's live Session, if any, so a node whose certificate was just revoked stops acting on an already-open stream.
func (*Server) Enroll ¶
func (s *Server) Enroll(ctx context.Context, req *agentpb.EnrollRequest) (*agentpb.EnrollResponse, error)
Enroll implements agentpb.AgentServiceServer.
func (*Server) NotifyShutdown ¶ added in v0.2.0
func (s *Server) NotifyShutdown()
NotifyShutdown sends a GoAway frame to every connected agent, telling each to close its own end of the Session stream now rather than wait to be cut off by the control plane's own bounded GracefulStop timeout (cmd/levelrail's stopAgentGRPCServer). Best-effort: a send failure here just means that agent falls back to the timeout, same as before this existed.
func (*Server) Reenroll ¶ added in v0.2.0
func (s *Server) Reenroll(ctx context.Context, req *agentpb.ReenrollRequest) (*agentpb.ReenrollResponse, error)
Reenroll implements agentpb.AgentServiceServer: a token bound to an existing node buys that node a new certificate. Minting the token was the operator's authorization, so a revocation is cleared.
func (*Server) Renew ¶ added in v0.2.0
func (s *Server) Renew(ctx context.Context, req *agentpb.RenewRequest) (*agentpb.RenewResponse, error)
Renew implements agentpb.AgentServiceServer. The CSR is signed before the store is updated: if the update fails, the new certificate was never recorded and so is never trusted.
func (*Server) Session ¶
func (s *Server) Session(stream agentpb.AgentService_SessionServer) error
Session implements agentpb.AgentServiceServer: accepts an mTLS-authenticated agent's persistent stream once its certificate is confirmed to be one this node may use, and wires a GRPCTransport into Registry until the stream ends.
type SessionOption ¶ added in v0.2.0
type SessionOption func(*sessionConfig)
SessionOption configures optional RunSession behavior.
func WithBuildRunner ¶ added in v0.2.0
func WithBuildRunner(runner BuildRunner) SessionOption
WithBuildRunner lets this node accept builds dispatched to it by the control plane. Without one, a dispatched build is rejected with a clear error instead of failing partway through: an agent whose local BuildKit is unreachable still serves every container operation normally.
func WithGPUProbe ¶ added in v0.2.0
func WithGPUProbe(p GPUProbe) SessionOption
WithGPUProbe makes RunSession report this node's GPUs to the control plane on connect and periodically after.
func WithHeartbeatInterval ¶
func WithHeartbeatInterval(d time.Duration) SessionOption
WithHeartbeatInterval overrides how often RunSession sends an unprompted Heartbeat frame up the Session stream. Without one configured, defaultHeartbeatInterval applies. cmd/levelrail-agent's own main.go reads APP_NODE_HEARTBEAT_INTERVAL and passes the parsed duration here, the project's "no hardcoded thresholds, use env vars" rule; this package itself never reads the environment directly.
func WithKeepalive ¶ added in v0.2.0
func WithKeepalive(pingTime, timeout time.Duration) SessionOption
WithKeepalive overrides this connection's HTTP/2 PING keepalive timing. Without one configured, defaultKeepaliveTime/ defaultKeepaliveTimeout apply. cmd/levelrail-agent's own main.go reads APP_NODE_KEEPALIVE_TIME/APP_NODE_KEEPALIVE_TIMEOUT and passes the parsed durations here, the same env-var-with-default convention WithHeartbeatInterval above follows.
func WithMesh ¶ added in v0.2.0
func WithMesh(nodeID string, mesh MeshApplier) SessionOption
WithMesh lets this node accept and apply mesh config (and rotate its own mesh key) dispatched by the control plane, over ApplyMesh/ RotateMeshKey requests on this same Session stream. nodeID is this node's own ID, since RotateMeshKeyRequest carries no fields of its own (this file's own header on why: it always means "rotate the node holding this stream," never another). Without this option, both request kinds answer with ErrMeshUnavailable: a node whose Docker operations work fine but has no mesh device configured, not a broken connection.
func WithRenewer ¶ added in v0.2.0
func WithRenewer(r *Renewer) SessionOption
WithRenewer keeps the session's certificate fresh with r while the session is open. The identity r holds replaces the one RunSession was given, so each reconnect presents the newest certificate.
type Transport ¶
Transport is what a reconcile controller needs from whichever node a resource is placed on. Deliberately identical in shape to docker.Runtime (embedded, not duplicated field by field, so the two can never silently drift apart) rather than a new, narrower interface: docker.Runtime is already the exact narrow surface scoped down to "what a reconcile controller needs from Docker," and Transport's whole point is "the same thing, now reachable on a specific node," not a different capability set. An RPC surface an agent exposes beyond container operations stays off this interface and is advertised as an optional capability instead (see RemoteBuilder, build_dispatch.go), so a Local transport does not have to pretend to implement something only a remote agent can do.