node

package
v0.0.0-...-7694264 Latest Latest
Warning

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

Go to latest
Published: Aug 21, 2026 License: AGPL-3.0 Imports: 27 Imported by: 0

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

View Source
const (
	StatusRunning = "running"
	StatusSuccess = "success"
	StatusFailed  = "failed"
)

Job execution states reported to the control plane.

View Source
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).

func Load

func Load(path string) (*Config, error)

Load reads the node config from path (or $AGENDA_NODE_CONFIG when path is "").

type Duration

type Duration struct{ time.Duration }

Duration is a yaml-unmarshalable time.Duration ("15s", "1h").

func (*Duration) UnmarshalYAML

func (d *Duration) UnmarshalYAML(value *yaml.Node) error

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

func NewHeartbeat(centralBaseURL string, machineID int64, token string, interval time.Duration) *Heartbeat

func (*Heartbeat) Start

func (h *Heartbeat) Start(ctx context.Context)

Start launches the heartbeat loop in a goroutine. It sends one immediately so the control plane sees the node come online without waiting a full interval.

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 NewJobStore

func NewJobStore(maxOutputBytes int, retention time.Duration) *JobStore

func (*JobStore) Delete

func (s *JobStore) Delete(id string) bool

Delete cancels a still-running job and removes it. Returns false if unknown.

func (*JobStore) Dispatch

func (s *JobStore) Dispatch(id string, run func(ctx context.Context, buf *bytes.Buffer) error)

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.

func (*JobStore) Get

func (s *JobStore) Get(id string) (contract.NodeJobStatus, bool)

Get returns the job's current status; ok is false if unknown. Output is included only when terminal (the run goroutine has finished writing buf).

func (*JobStore) StartGC

func (s *JobStore) StartGC(ctx context.Context)

StartGC runs a sweeper that removes finished jobs older than retention until ctx is cancelled.

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.

func NewServer

func NewServer(token string, jobs *JobStore, registry *ProxyRegistry, backendHost string) *Server

func (*Server) Handler

func (s *Server) Handler() http.Handler

Handler exposes the gin engine (for httptest and for embedding).

Jump to

Keyboard shortcuts

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