Documentation
¶
Overview ¶
internal/worker/ca.go
internal/worker/cert_holder.go
internal/worker/client.go
internal/worker/registry.go
Index ¶
- Constants
- Variables
- func CAKeyEncKey(authSecret string) []byte
- func Fingerprint(cert *x509.Certificate) string
- func IsControlPlaneClientCert(cert *x509.Certificate) bool
- func NewRemoteRuntime(lookup WorkerLookup, tier string, dialer AgentDialer) process.Runtime
- func NewReplicaServer(cfg ReplicaServerConfig) *replicaServer
- func NodeIDFromCert(cert *x509.Certificate) string
- func Register(ctx context.Context, serverURL string, req workerapi.RegisterRequest, ...) (workerapi.RegisterResponse, error)
- type AgentDialer
- type AgentServer
- type AgentServerConfig
- type BundleEnsurer
- type CA
- func (c *CA) CertPEM() []byte
- func (c *CA) ControlClientCertificate() (tls.Certificate, error)
- func (c *CA) ListenerTLSConfig(hosts ...string) (*tls.Config, error)
- func (c *CA) Pool() *x509.CertPool
- func (c *CA) ServerCertificate(hosts ...string) (tls.Certificate, error)
- func (c *CA) SignWorkerCSR(nodeID string, csrPEM []byte, ttl time.Duration) ([]byte, error)
- func (c *CA) VerifyJoinToken(token string) bool
- type CAHolder
- type CAStore
- type CertHolder
- type Client
- type RegisterParams
- type Registry
- func (r *Registry) Forget(nodeID string)
- func (r *Registry) Heartbeat(nodeID, fingerprint string, reported int64) (bool, int64, error)
- func (r *Registry) MarkDown(nodeID string) error
- func (r *Registry) PlanPlacementForTier(tier, slug string, count int) []db.Worker
- func (r *Registry) Reap(nodeID string) error
- func (r *Registry) Refresh() error
- func (r *Registry) Register(p RegisterParams) (db.Worker, error)
- func (r *Registry) Revoke(nodeID string) error
- func (r *Registry) Worker(nodeID string) (db.Worker, bool)
- func (r *Registry) WorkerForTier(tier string) (db.Worker, bool)
- func (r *Registry) WorkersForTier(tier string) []db.Worker
- type ReplicaServerConfig
- type ReplicaTransportBuilder
- type WorkerGetter
- type WorkerLookup
Constants ¶
const ControlPlaneCommonName = "shinyhub-control-plane"
ControlPlaneCommonName is the Subject CommonName the control plane's client and server certificates carry (see ControlClientCertificate / ServerCertificate). A worker agent listener pins inbound peers to this identity so one worker's own CA-signed certificate cannot be presented as a client to command another worker.
const ProviderRemoteDocker = "remote_docker"
ProviderRemoteDocker labels replicas started on a remote Docker worker.
Variables ¶
ErrControlPlaneUnavailable is returned by Register when the control plane responds 503: it is reachable but not the ready owner (a standby, or an owner still warming up). Callers retry it, unlike auth/validation failures (401/400) which are permanent.
Functions ¶
func CAKeyEncKey ¶ added in v0.9.2
CAKeyEncKey derives the key-encryption key that protects the worker CA private key at rest, from the auth secret. Exposed so auth.secret-rotation tooling can re-encrypt the stored CA key without duplicating the domain-separation string.
func Fingerprint ¶
func Fingerprint(cert *x509.Certificate) string
Fingerprint returns the hex SHA-256 of a certificate's DER, used to record the trusted client cert on the worker row.
func IsControlPlaneClientCert ¶ added in v0.9.6
func IsControlPlaneClientCert(cert *x509.Certificate) bool
IsControlPlaneClientCert reports whether cert is the control plane's client certificate rather than a worker's own cert. A worker leaf always carries a namespaced node DNS SAN (<nodeid>.node.shinyhub.internal); the control-plane client cert carries none. Requiring the control-plane CN AND the absence of a node SAN rejects a worker even if its assigned node id ever collided with the control-plane CN.
func NewRemoteRuntime ¶
func NewRemoteRuntime(lookup WorkerLookup, tier string, dialer AgentDialer) process.Runtime
NewRemoteRuntime builds a tier-bound runtime that delegates to whichever worker is currently live for the tier, dialing it over the mTLS tunnel.
func NewReplicaServer ¶
func NewReplicaServer(cfg ReplicaServerConfig) *replicaServer
NewReplicaServer constructs a replicaServer from the given config. If AllocatePort is nil, deploy.AllocatePort is used.
func NodeIDFromCert ¶
func NodeIDFromCert(cert *x509.Certificate) string
NodeIDFromCert recovers the node id bound into a worker certificate, preferring the namespaced DNS SAN and falling back to the CN.
func Register ¶
func Register(ctx context.Context, serverURL string, req workerapi.RegisterRequest, caPEM []byte) (workerapi.RegisterResponse, error)
Register performs the join: POST the token + CSR over HTTPS pinned to caPEM (no client cert yet at join time). Returns the signed cert, CA bundle, and node id.
Types ¶
type AgentDialer ¶
type AgentDialer interface {
// DialWorker returns a client whose transport authenticates to the worker
// and the base URL (scheme://host) to prefix request paths with.
DialWorker(w db.Worker) (*http.Client, string, error)
// Transport returns the RoundTripper used to reach the given worker's data
// plane (for the proxy and health checks).
Transport(w db.Worker) (http.RoundTripper, error)
}
AgentDialer returns an HTTP client and base URL for talking to a worker over its mTLS tunnel. It is a seam: production builds an mTLS client keyed by the worker's node id; tests supply a stub backed by httptest.
func NewMTLSDialer ¶
func NewMTLSDialer(mintClient func() (tls.Certificate, error), caPool *x509.CertPool) (AgentDialer, error)
NewMTLSDialer constructs the default control-plane-to-worker dialer. mintClient issues the control plane's short-lived client certificate; the dialer re-mints it past its half-life so it is never presented expired. It returns the AgentDialer interface because callers wire it into a remoteRuntime, which depends only on the interface.
type AgentServer ¶
type AgentServer struct {
// contains filtered or unexported fields
}
AgentServer is the agent's inbound mTLS HTTPS listener. The control plane dials it to issue Start/Signal/Wait/Stats/RunOnce commands and to proxy data-plane traffic through /v1/data/{token}/*.
func NewAgentServer ¶
func NewAgentServer(cfg AgentServerConfig) *AgentServer
NewAgentServer constructs an AgentServer from cfg.
func (*AgentServer) Listen ¶ added in v0.6.2
func (s *AgentServer) Listen() (net.Listener, error)
Listen binds an mTLS listener on the configured ListenAddr. It is the fail-fast half of serving: agent.Run binds synchronously so a port conflict surfaces before the worker announces liveness, then hands the listener to ServeListener to serve until ctx is cancelled.
func (*AgentServer) ServeListener ¶ added in v0.6.2
ServeListener serves the replica-control API and data-plane proxy on ln until ctx is cancelled. ln must already terminate TLS (e.g. from tls.Listen with TLSConfig); callers that need a known bound port can pass their own listener.
func (*AgentServer) TLSConfig ¶
func (s *AgentServer) TLSConfig() *tls.Config
TLSConfig returns the tls.Config for the listener. The served cert and the client-CA pool are resolved per handshake through GetConfigForClient so a renewed cert or a rotated CA bundle takes effect on the next connection without restarting the listener. Exposed so tests can verify the security posture without binding a port.
type AgentServerConfig ¶
type AgentServerConfig struct {
// ListenAddr is the host:port the server binds. The agent command passes
// the --advertise-addr value (or a port offset from it).
ListenAddr string
// CertSource holds the issued keypair from SignWorkerCSR (ServerAuth +
// ClientAuth EKUs, <nodeid>.node.shinyhub.internal SAN). Reading the cert
// through the holder lets the agent swap a renewed cert in without restarting
// the listener, so the worker's routing surface survives cert rotation.
CertSource *CertHolder
// CASource holds the CA pool the worker trusts; the control plane must present
// a client cert signed by this CA to authenticate. Reading it through the
// holder lets a CA bundle rotated on heartbeat take effect on the next
// handshake without restarting the listener.
CASource *CAHolder
// NodeID is the assigned node id, embedded in the server cert SAN.
NodeID string
// Replicas is the replica-control server whose Routes are mounted. Set
// after construction once the replicaServer is built in worker.go.
Replicas *replicaServer
}
AgentServerConfig configures the agent's inbound mTLS HTTP server.
type BundleEnsurer ¶
BundleEnsurer makes an app bundle present on the worker's local disk, keyed by content digest, and returns the extracted bundle directory. It is an interface rather than the concrete *agent.BundleCache because the agent package imports this one; depending on the concrete type would create an import cycle.
type CA ¶
type CA struct {
// contains filtered or unexported fields
}
CA is the control plane's internal certificate authority. It signs short-lived worker client certificates that bind a node id, and pins itself as the trust root both sides verify against. The keypair is generated on first OpenCA and persisted under dir.
func LoadOrInitCA ¶ added in v0.7.4
LoadOrInitCA loads/initializes the shared worker CA (see loadOrInitCA) and mirrors the public CA certificate to caDir/ca-cert.pem so worker bootstrap (`shinyhub worker --ca-file <ca_dir>/ca-cert.pem`) and operator tooling can read it. The private key is never written to disk; it stays encrypted in the database only.
func OpenCA ¶
OpenCA loads a worker CA keypair from disk, generating and persisting it on first run. Production boot sources the CA from the database via LoadOrInitCA; OpenCA remains as the disk-based constructor used by tests (and disk-only setups).
func (*CA) ControlClientCertificate ¶
func (c *CA) ControlClientCertificate() (tls.Certificate, error)
ControlClientCertificate issues a short-lived ECDSA client certificate that the control plane presents to worker agents over mTLS. The cert is signed by this CA, carries only the ClientAuth EKU, and verifies against CA.Pool().
func (*CA) ListenerTLSConfig ¶ added in v0.6.2
ListenerTLSConfig builds the TLS config for the control plane's worker-facing listener. The server cert is minted off the CA for the given hosts and served through GetCertificate, which re-mints it past its half-life so a control plane running beyond the cert's lifetime keeps serving without a restart. Client certs are requested but not required (register runs before a worker has a cert) and verified against the CA when presented.
func (*CA) ServerCertificate ¶
func (c *CA) ServerCertificate(hosts ...string) (tls.Certificate, error)
ServerCertificate signs a fresh server keypair off the CA for the control plane's worker-facing listener. When no hosts are given it defaults to loopback (127.0.0.1, ::1, localhost). IP-literal hosts become IP SANs and the rest become DNS SANs. The returned certificate carries the leaf and the CA certificate so clients pinning the CA can build a full chain.
func (*CA) SignWorkerCSR ¶
SignWorkerCSR signs a worker CSR, binding nodeID into the certificate (CN plus a DNS SAN) so the node id is recoverable from the presented cert. The cert is valid for ttl (short-lived; renewed on heartbeat).
func (*CA) VerifyJoinToken ¶
VerifyJoinToken reports whether token matches a configured join token, using a constant-time comparison to avoid leaking token length/contents via timing.
type CAHolder ¶ added in v0.6.2
type CAHolder struct {
// contains filtered or unexported fields
}
CAHolder holds the CA trust bundle a worker pins, behind a read-write lock so the inbound server and outbound client can read the current pool on every handshake while a heartbeat swaps in a rotated bundle without a restart. It is safe for concurrent use.
func NewCAHolder ¶ added in v0.6.2
NewCAHolder parses caPEM into a trust pool, erroring if no certificate parses.
func (*CAHolder) Pool ¶ added in v0.6.2
Pool returns the current trust pool. Callers must treat it as read-only.
type CAStore ¶ added in v0.7.4
type CAStore interface {
GetWorkerCA() (certPEM, keyEnc []byte, found bool, err error)
PutWorkerCAIfAbsent(certPEM, keyEnc []byte) (inserted bool, err error)
}
CAStore is the minimal store the worker CA bootstrap needs. *db.Store satisfies it. found (not an ErrNotFound sentinel) signals an empty row.
type CertHolder ¶ added in v0.6.2
type CertHolder struct {
// contains filtered or unexported fields
}
CertHolder stores a TLS certificate that can be atomically swapped at runtime. A renewed certificate set via Set takes effect on the next TLS handshake of every listener and client wired to the holder through GetCertificate or GetClientCertificate, so a worker can rotate its expiring cert without restarting its inbound server or rebuilding its outbound client. The zero value is not usable; construct with NewCertHolder.
func NewCertHolder ¶ added in v0.6.2
func NewCertHolder(cert tls.Certificate) *CertHolder
NewCertHolder returns a holder seeded with cert.
func (*CertHolder) Get ¶ added in v0.6.2
func (h *CertHolder) Get() tls.Certificate
Get returns the currently held certificate.
func (*CertHolder) GetCertificate ¶ added in v0.6.2
func (h *CertHolder) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error)
GetCertificate adapts the holder to tls.Config.GetCertificate so a server presents the current certificate on each handshake.
func (*CertHolder) GetClientCertificate ¶ added in v0.6.2
func (h *CertHolder) GetClientCertificate(*tls.CertificateRequestInfo) (*tls.Certificate, error)
GetClientCertificate adapts the holder to tls.Config.GetClientCertificate so a client presents the current certificate on each handshake.
func (*CertHolder) Set ¶ added in v0.6.2
func (h *CertHolder) Set(cert tls.Certificate)
Set replaces the held certificate. Subsequent handshakes present it.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a worker's mTLS HTTP client to its control plane. After bootstrap it carries the signed client cert and pins the CA; the same transport backs the data-plane tunnel the control-plane proxy dials in reverse (C1).
func NewClient ¶
func NewClient(serverURL string, certSource *CertHolder, caSource *CAHolder) (*Client, error)
NewClient builds an mTLS client that presents the certificate held by certSource and verifies the control plane against the trust bundle held by caSource. Sourcing both through holders lets the worker rotate its expiring cert and a rotated CA bundle without rebuilding the client.
func (*Client) FetchBundle ¶
FetchBundle streams the bundle zip for a content digest. The caller verifies the digest on the returned stream.
func (*Client) Heartbeat ¶
func (c *Client) Heartbeat(ctx context.Context, version, renewCSRPEM string, incarnation int64) (workerapi.HeartbeatResponse, error)
Heartbeat posts a heartbeat and returns the control plane's response. When renewCSRPEM is non-empty it is sent as a certificate renewal request; the caller applies any cert the response carries. incarnation is the worker's currently-held generation counter, letting the control plane detect and fence a stale rejoin.
func (*Client) Transport ¶
func (c *Client) Transport() http.RoundTripper
Transport exposes the underlying transport for the data-plane tunnel (C1).
type RegisterParams ¶
type RegisterParams struct {
Name string
AdvertiseAddr string
Tier string
Version string
Fingerprint string
}
RegisterParams is the data a joining worker supplies (node id is allocated by the registry, not chosen by the worker).
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is the control plane's view of joined workers: durable in the workers table, indexed in memory for routing. Multiple distinct-address workers may be up on one tier (real multi-worker capacity); the invariant is one up worker per (tier, advertise address), so a stale duplicate that rejoins at an endpoint a live worker already owns is superseded.
func NewRegistry ¶
NewRegistry constructs a registry and rebuilds its in-memory index from the workers table, so a control-plane restart re-adopts known workers.
func (*Registry) Forget ¶ added in v0.6.2
Forget drops a worker from the in-memory index. The monitor calls it after the store reaps a long-dead worker row so the fleet snapshot does not keep listing a node that no longer exists. Unknown node ids are a no-op. Forget never touches the store; the row is already gone.
It holds regMu so the delete is serialized against a concurrent Refresh, whose older ListWorkers snapshot could otherwise reinsert the just-reaped worker.
func (*Registry) Heartbeat ¶ added in v0.6.2
Heartbeat records a worker's liveness and refreshes its trusted cert fingerprint (cert renewal) in both the store and the index. It keeps an up worker up, and promotes a not-up worker - a joining worker on its first heartbeat, or a down worker reaped for missed heartbeats or superseded while offline and now restarted under its old identity - to up only when no other up worker owns its (tier, advertise address). Gating the promotion on endpoint ownership keeps the one-up-worker-per-(tier,address) invariant: a worker the endpoint's live owner holds cannot resurrect itself alongside that owner. A joining worker becomes routable here, and the agent sends this first heartbeat only after its listener binds, so an up worker is always one that is listening.
reported is the incarnation the worker last learned from the control plane (at registration or a prior heartbeat). A reported incarnation behind the stored one means the control plane reaped this worker - bumping its incarnation and reassigning its replicas - while it was partitioned: the worker may still be running orphaned copies of those replicas, so the heartbeat is fenced (kept down, told to self-fence) instead of re-upped. reported == 0 is a legacy pre-fence agent and is never fenced, so a rolling upgrade does not fence agents that have not yet learned to report an incarnation. Heartbeat returns (fenced, current incarnation, error).
func (*Registry) MarkDown ¶
MarkDown transitions a worker to down in both the durable store and the in-memory routing index, so a worker whose heartbeat went stale is excluded from routing without waiting for a control-plane restart to rebuild the index. Unknown node ids are a no-op in memory; the store write still runs.
It holds regMu so the store write and the index update are one unit against a concurrent Refresh: without it, a Refresh whose ListWorkers snapshot predates this MarkDown could overwrite the index and resurrect the worker as "up".
func (*Registry) PlanPlacementForTier ¶ added in v0.7.0
PlanPlacementForTier plans where to place count new replicas of slug across a tier's coexisting workers, spreading load deterministically. It returns one worker per replica, in assignment order; the returned slice is empty when no worker is up on the tier and may be shorter than count only in that case.
Each pick chooses the worker hosting the fewest running replicas; ties break toward the worker hosting fewer of slug's own replicas (anti-affinity, so an app's replicas spread for HA), then toward the lowest node id (deterministic). Picks within one call fold into a running tally before the next pick, so a batch deployed concurrently spreads across workers instead of stacking every replica on the lowest node id (which a per-replica read of the same pre-deploy snapshot would do). If the load query fails it plans from a zero baseline rather than refusing to place, so a transient store hiccup does not block deploys.
func (*Registry) Reap ¶ added in v0.9.5
Reap marks a worker down AND bumps its incarnation, then refreshes the index from the store so the bumped value is visible to Heartbeat's fence check. The down-monitor calls this (not MarkDown) because a reaped worker also has its replicas stripped and reassigned elsewhere; the incarnation bump is what fences the worker if it later reconnects still running them.
func (*Registry) Refresh ¶ added in v0.8.0
Refresh rebuilds the in-memory routing index from the workers table. An instance becoming the control-plane owner calls it so its routing decisions reflect every worker row the previous owner wrote before it died (registrations, heartbeats, supersedes, reaps) - not just the rows present when this instance booted. It is a full replace: workers added since boot appear and workers removed from the store disappear. Idempotent.
It holds regMu (like Register/Heartbeat/Revoke), preserving the regMu-then-mu lock order, so the rebuild is a consistent snapshot against any in-flight registration on this instance.
func (*Registry) Register ¶
func (r *Registry) Register(p RegisterParams) (db.Worker, error)
Register allocates a node id, persists the worker, and indexes it. Re-register with a known node id is handled by the caller passing that id via Reregister.
func (*Registry) Revoke ¶ added in v0.6.2
Revoke administratively revokes a worker. It persists the revocation (which also marks the node down) and refreshes the in-memory index from the store so the node is excluded from routing immediately, without waiting for a control-plane restart. The row is kept (not deleted) so the revocation stays auditable. Returns db.ErrNotFound for an unknown node.
It holds regMu for the same reason Heartbeat and Register do: a heartbeat performs a read-decide-write of the worker's status, so revoking without that lock could land between a heartbeat's read and its write and let the heartbeat resurrect the revoked node to up. Serializing closes that window.
func (*Registry) WorkerForTier ¶
WorkerForTier returns a single up worker routing a tier, if any: the first of WorkersForTier (lowest node id), so single-worker routing is deterministic.
func (*Registry) WorkersForTier ¶ added in v0.7.0
WorkersForTier returns every up worker on a tier, sorted by node id so the order is deterministic (map iteration order is not). The returned slice is a fresh copy safe for the caller to retain.
type ReplicaServerConfig ¶
type ReplicaServerConfig struct {
Runtime process.Runtime
DataDir string
NodeID string
Advertise string // host:port base used to build tunnel URLs
AllocatePort func() int
// Bundles, when set, is called on every remote replica start to pull and
// verify the app bundle by content digest, returning the local extracted dir.
Bundles BundleEnsurer
}
ReplicaServerConfig configures a replicaServer. AllocatePort is injectable for tests; production wraps deploy.AllocatePort (which returns int, no error).
type ReplicaTransportBuilder ¶ added in v0.8.1
type ReplicaTransportBuilder struct {
// contains filtered or unexported fields
}
ReplicaTransportBuilder derives the HTTP transport for a replica row from the DB, without requiring the placement registry. Each instance is safe for concurrent use.
For remote_docker replicas it builds a worker mTLS transport from the DB worker row (fetching via GetWorker) and caches it by worker_id so the same *http.Transport is reused across calls - mTLS transports are instance- independent (shared CA pool + rotating client cert) and safe to reuse.
For all other providers (fargate, native, docker) it returns nil, directing the proxy to use the default HTTP transport. Fargate replicas reach their awsvpc private IP over plain HTTP inside the VPC; native and docker replicas run on localhost and require no special transport.
func NewReplicaTransportBuilder ¶ added in v0.8.1
func NewReplicaTransportBuilder(dialer AgentDialer, store WorkerGetter) *ReplicaTransportBuilder
NewReplicaTransportBuilder constructs a builder that resolves transports from the DB rather than the placement registry, so every instance (including standbys whose registry is empty) can build the correct per-worker transport.
func (*ReplicaTransportBuilder) TransportForReplica ¶ added in v0.8.1
func (b *ReplicaTransportBuilder) TransportForReplica(r *db.Replica) (http.RoundTripper, error)
TransportForReplica returns the HTTP transport to use when forwarding requests to the given replica. It returns nil for non-remote_docker providers (the proxy falls back to its tuned default backend transport in that case).
For remote_docker replicas the transport is built once per worker_id and cached; concurrent callers for the same worker_id are serialized through a mutex so the transport is built exactly once.
The cache is unbounded and never invalidated because the cached transport remains correct for the lifetime of a worker entry:
- The client certificate rotates automatically via the dialer's GetClientCertificate callback, so a cached *http.Transport always presents a fresh cert on the next handshake without needing to be rebuilt.
- The ServerName is derived from the worker's stable node ID, which never changes for a given worker row.
- A revoked worker never enters the cache: dialer.Transport returns an error for a revoked worker (checked before insertion), and the routing layer removes revoked workers from replica assignments before this function is called for them.
type WorkerGetter ¶ added in v0.8.1
WorkerGetter is the minimal store interface the transport builder requires to resolve a worker row by node ID. *db.Store satisfies it.
type WorkerLookup ¶
type WorkerLookup interface {
PlanPlacementForTier(tier, slug string, count int) []db.Worker
WorkersForTier(tier string) []db.Worker
Worker(nodeID string) (db.Worker, bool)
}
WorkerLookup resolves workers for routing. Implemented by *Registry in production; a stub is used in tests. PlanPlacementForTier plans where to place a batch of new replicas of a slug (least-loaded, spread across the tier, one worker per replica); WorkersForTier enumerates every up worker on a tier (inventory spans all of them); Worker resolves the specific worker that owns an existing handle or a pre-planned target.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal/worker/agent/agent.go
|
internal/worker/agent/agent.go |
|
Package api defines the JSON request/response types and the NDJSON streaming framing that form the wire contract between the ShinyHub control plane and a worker agent.
|
Package api defines the JSON request/response types and the NDJSON streaming framing that form the wire contract between the ShinyHub control plane and a worker agent. |