Documentation
¶
Overview ¶
Package node implements agenda-node: the resident per-machine agent that executes deploy commands on behalf of the control plane (replacing SSH) and acts as a local reverse proxy for gateway traffic. It is a separate binary (cmd/agenda-node) that shares internal/runner with the control plane.
Index ¶
Constants ¶
const ( StatusRunning = "running" StatusSuccess = "success" StatusFailed = "failed" )
Job execution states reported to the control plane.
const Version = "0.1.0"
Version is the node build version reported in heartbeats and /v1/health.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// ListenAddr is the management API (/v1/jobs, /v1/proxy, /v1/health). Bind
// it to a private interface — it is equivalent to remote code execution.
ListenAddr string `yaml:"listen_addr"`
// ProxyListenAddr is the reverse-proxy port gateway backends point at.
ProxyListenAddr string `yaml:"proxy_listen_addr"`
// ProxyBackendHost is the host used to reach a locally-registered app
// instance's published port — both ProxyHandler's reverse proxy and the
// metrics relay (fetchLocalMetrics) use it. Defaults to 127.0.0.1, correct
// when node runs bare-metal/VM alongside the apps it deploys. When node
// instead drives a separate host's dockerd over docker.sock (docker-
// outside-of-docker, e.g. this repo's deploy/quickstart topology), the
// app's published port lives on that host, not in node's own container
// network namespace — set this to "host.docker.internal" (with node's
// container given `extra_hosts: host.docker.internal:host-gateway`) so
// both can actually reach it.
ProxyBackendHost string `yaml:"proxy_backend_host"`
// MachineID matches the control plane's machine.id for heartbeats.
MachineID int64 `yaml:"machine_id"`
// Token is the shared per-machine secret; must equal machine.agent_token.
Token string `yaml:"token"`
// CentralBaseURL is the control plane's base URL for heartbeats.
CentralBaseURL string `yaml:"central_base_url"`
HeartbeatInterval Duration `yaml:"heartbeat_interval"`
MaxOutputBytes int `yaml:"max_output_bytes"`
JobRetention Duration `yaml:"job_retention"`
// ProxyDrainTimeout is how long shutdown waits for relayed WebSocket
// tunnels to end before force-closing them. Hijacked connections are
// invisible to http.Server.Shutdown, so without an explicit wait a node
// restart severs every tunnel through it instantly.
ProxyDrainTimeout Duration `yaml:"proxy_drain_timeout"`
}
Config is agenda-node's own configuration, loaded from agenda-node.yaml (independent of the control plane's agenda-v2.yaml).
type Heartbeat ¶
type Heartbeat struct {
// contains filtered or unexported fields
}
Heartbeat periodically POSTs the node's liveness to the control plane so the machine's online status can be derived from last_seen. It stops when ctx is cancelled. A failed heartbeat is logged and retried on the next tick — it is best-effort and never fatal.
func NewHeartbeat ¶
type JobStore ¶
type JobStore struct {
// contains filtered or unexported fields
}
Output is populated only when the job is terminal (success/failed); while running it is empty. Streaming partial output during a run is a deferred enhancement — returning it only at the end keeps the job buffer free of concurrent read/write (the run goroutine is the sole writer and has finished before any terminal read).
JobStore is agenda-node's in-memory task table. Jobs are not persisted — a node restart drops them, and the control plane treats a 404 on a job it dispatched as a lost task (step failure), the same outcome as a dropped SSH session. A background sweeper GCs finished jobs after retention.
func (*JobStore) Delete ¶
Delete cancels a still-running job and removes it. Returns false if unknown.
func (*JobStore) Dispatch ¶
Dispatch is idempotent: dispatching an existing job_id returns without starting a second command, so a retried POST never double-runs a deploy. run receives a context cancelled by DELETE (orphan reclamation) or maxJobDuration.
type ProxyHandler ¶
type ProxyHandler struct {
// contains filtered or unexported fields
}
ProxyHandler is the data-plane reverse proxy on the proxy listen port. It matches "/i/<instance>/<rest>", looks up the instance's current local port in the registry, and forwards to backendHost:<port>. This is a dumb forwarder — it does no health/circuit logic; routing decisions stay with the gateway.
That includes WebSockets: whether a route may be upgraded at all, which Origins are allowed and what the connection caps are were already decided by the gateway before the request got here, and re-deciding them at this hop would just be a second, weaker copy of the same policy. What this hop does own is the fact that a relayed tunnel holds a connection *on this machine*, so tunnels are tracked in order to be drained on shutdown.
func NewProxyHandler ¶
func NewProxyHandler(registry *ProxyRegistry, backendHost string) *ProxyHandler
NewProxyHandler builds a ProxyHandler. backendHost is the host apps' published ports are reachable at from node's own network namespace — "127.0.0.1" when node runs alongside the apps it deploys (bare metal/VM), or "host.docker.internal" when node drives a separate host's dockerd over docker.sock (docker-outside-of-docker) and apps' ports live on that host, not inside node's own container. Empty defaults to "127.0.0.1".
func (*ProxyHandler) ActiveTunnels ¶
func (h *ProxyHandler) ActiveTunnels() int
ActiveTunnels is the number of WebSocket tunnels currently relayed.
func (*ProxyHandler) BeginDrain ¶
func (h *ProxyHandler) BeginDrain()
BeginDrain stops the relay accepting new tunnels.
func (*ProxyHandler) Drain ¶
func (h *ProxyHandler) Drain(ctx context.Context) int
Drain waits for relayed tunnels to end, force-closing the remainder when ctx expires; it returns how many it had to force.
func (*ProxyHandler) ServeHTTP ¶
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
type ProxyRegistry ¶
type ProxyRegistry struct {
// contains filtered or unexported fields
}
ProxyRegistry maps an instance name to the local port it currently listens on. It is in-memory only (like the JobStore) — a node restart empties it, and the control plane repopulates it on the next deploy/route sync. This is what lets a gateway backend URL stay stable (node proxy port + /i/<instance>) even when the instance's real port drifts across redeploys.
func NewProxyRegistry ¶
func NewProxyRegistry() *ProxyRegistry
func (*ProxyRegistry) Get ¶
func (r *ProxyRegistry) Get(instance string) (int, bool)
Get returns the local port for instance, if registered.
func (*ProxyRegistry) Set ¶
func (r *ProxyRegistry) Set(instance string, port int)
Set records that instance currently listens on the given local port.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is agenda-node's management API (jobs + proxy registration + health). Commands execute locally via runner.New(nil) — the node reuses the exact same localRunner the control plane uses, so it never reimplements command running.