Documentation
¶
Overview ¶
Package api defines the control-plane<->agent HTTP protocol.
Model B is keyless: the agent holds the GitHub App key, polls GitHub, and mints JIT configs itself. The control plane sees only metadata and never a credential. Agents dial out (NAT-friendly). Dispatch is two-phase:
- POST /v1/agents/{name}/poll with capacity, busy slots, labels, AND the agent's own queued jobs (derived from its GitHub polling). The control plane merges every agent's report, ranks by priority + aging, and returns the one job this agent should run as an Offer (200) or empty (204). No credential crosses the wire.
- POST /v1/agents/{name}/dispatches/{id}/claim to accept. The control plane records the dispatch in the ledger and returns 200; 410 means the offer expired or is unknown — drop it. The agent then mints the JIT locally (its own key) and provisions.
When the runner takes a job, the agent POSTs /v1/agents/{name}/slots/{handle}/started. GitHub may give the runner a different queued job, so the offered job is released for a new offer. When a dispatch finishes, the agent POSTs its terminal status to /v1/agents/{name}/slots/{handle}/done. The poll doubles as the agent's heartbeat: the control plane holds even a zero-capacity poll open, so a healthy agent is never silent for longer than the poll window.
Index ¶
- Constants
- Variables
- type Client
- func (c *Client) Claim(ctx context.Context, agent, id string) error
- func (c *Client) Poll(ctx context.Context, agent string, req PollRequest) (*Offer, error)
- func (c *Client) ReportDone(ctx context.Context, agent, handle string, req DoneRequest) error
- func (c *Client) ReportStarted(ctx context.Context, agent, handle string) error
- type DoneRequest
- type Offer
- type OrgQueue
- type PollRequest
- type PriorityPolicy
- type QueuedJob
- type Server
- type StaticPriorities
Constants ¶
const PollTimeout = 25 * time.Second
PollTimeout is how long the server holds a poll connection open before returning 204 if no work surfaces. Agents immediately re-poll afterward, which makes the poll a heartbeat with a guaranteed cadence — a full-capacity poll is held open too, not bounced.
Variables ¶
var ErrOfferGone = errors.New("offer gone")
ErrOfferGone reports a claim on an offer the server no longer holds (it expired or was resolved). The agent should drop the offer; the job has been re-enqueued server-side.
Functions ¶
This section is empty.
Types ¶
type Client ¶
type Client struct {
UpdateRequest string
UpdateVersion string
BaseURL string
Token string
HTTP *http.Client
}
Client is the agent-side HTTP client to the controller.
func NewClient ¶
NewClient constructs a Client. Default timeout is generous because /poll long-polls server-side.
func (*Client) Claim ¶
Claim accepts an offer: the control plane records the dispatch in its ledger and returns 200. No credential comes back — the agent mints the JIT locally. ErrOfferGone (410) means the offer expired or is unknown; drop it.
func (*Client) ReportDone ¶
ReportDone notifies the controller that a dispatch has finished.
type DoneRequest ¶
DoneRequest is what the agent posts when a dispatch finishes. Status is one of: "done" (clean exit), "failed" (provision error), "lost" (the slot was found orphaned after an agent restart and destroyed — its runner never reported, so its registration may linger).
type Offer ¶
type Offer struct {
ID string `json:"id"` // dispatch id == slot name == runner name
Org string `json:"org"` // GitHub org the runner will register to
JobID int64 `json:"job_id"` // which job — the agent mints for this
Labels []string `json:"labels"` // labels to advertise to GitHub
}
Offer names the job the agent should run. It carries no credential: the agent already holds the full job (it polled GitHub) and mints the JIT itself after claiming. JobID lets the agent match the decision to its local queue.
type OrgQueue ¶
type OrgQueue struct {
Org string `json:"org"`
Priority int `json:"priority"`
Jobs []QueuedJob `json:"jobs"`
}
OrgQueue is one org's pending jobs as reported by an agent. Priority is the agent's advisory org priority; the control plane owns the final ordering.
type PollRequest ¶
type PollRequest struct {
UpdateProtocol int `json:"update_protocol,omitempty"`
Version string `json:"version,omitempty"`
UpdateState string `json:"update_state,omitempty"`
UpdateError string `json:"update_error,omitempty"`
FailedRequest string `json:"failed_request,omitempty"`
FailedVersion string `json:"failed_version,omitempty"`
Capacity int `json:"capacity"` // total concurrent slots this agent supports
Busy []string `json:"busy"` // slot handles currently in use
Labels []string `json:"labels"` // labels this agent's runners advertise
Queues []OrgQueue `json:"queues"` // per-org queued jobs this agent can serve
// Disk* describe the filesystem backing the agent's image store, zero when
// it can't be measured. Blocked means the agent is refusing work because
// that store is under its free-space floor; it keeps polling so its
// heartbeat, queue report and buffered done-reports still flow.
DiskFreeBytes uint64 `json:"disk_free_bytes,omitempty"`
DiskTotalBytes uint64 `json:"disk_total_bytes,omitempty"`
Blocked bool `json:"blocked,omitempty"`
}
PollRequest is what the agent sends on each long-poll iteration. Queues is the agent's own view of pending work, polled from GitHub with its own key — the control plane schedules on this metadata, never touching a credential.
func (PollRequest) FreeCapacity ¶
func (r PollRequest) FreeCapacity() int
FreeCapacity returns how many more slots the agent can accept. A blocked agent has none: that is what makes the scheduler route the work to a host that can actually run it, instead of one that would fail every dispatch.
type PriorityPolicy ¶ added in v0.2.11
type PriorityPolicy interface {
// Priorities returns the policy priority for each given scope that has
// one, keyed by the lower-cased scope.
Priorities(ctx context.Context, scopes []string) (map[string]int, error)
}
PriorityPolicy is the controller-side priority for a tenant scope (a GitHub org login, or "owner/repo" for a repo-level scope). A scope with a policy scores at that priority whatever the agent reports; a scope without one keeps the agent-reported value.
type QueuedJob ¶
type QueuedJob struct {
JobID int64 `json:"job_id"`
Labels []string `json:"labels"`
WaitingSecs int `json:"waiting_secs"`
}
QueuedJob is one pending job's scheduling metadata. No repo, no secrets.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is the keyless control plane. It schedules on the queue metadata each agent reports (the agent holds the GitHub key and mints JIT itself) and never sees a credential.
func NewServer ¶
NewServer wires the dependencies. boostPerMinute is the priority aging rate. token is the static shared secret (OSS single-host and the current trial); enr, when non-nil, adds Neon-backed per-agent enrollment tokens and the CLI login handshake. If both are empty/nil, RequireAuthOrLoopback refuses to bind beyond loopback.
func (*Server) RequireAuthOrLoopback ¶
RequireAuthOrLoopback returns an error if listenAddr binds beyond loopback while the server has an empty auth token. Call before binding to fail loudly.
"loopback" means literally `localhost` or an IP that satisfies net.IP.IsLoopback. Other hostnames are rejected because they may resolve to public addresses and we can't tell at config-load time.
func (*Server) RequireScopedAuth ¶
RequireScopedAuth refuses a config that enables enrollment while also setting a static token. The static token is unscoped — it bypasses queue scoping and the per-tenant claim/resolve guard — so on a multi-tenant deploy it is a cross-tenant master key. Enroll every agent instead; its token carries an org and name. OSS single-host (no enrollment) keeps the static token.
func (*Server) WithPriorities ¶ added in v0.2.11
func (s *Server) WithPriorities(p PriorityPolicy)
WithPriorities sets the central priority policy. Call before serving; the default (nil) schedules on agent-reported priority alone.
func (*Server) WithTelemetry ¶
WithTelemetry swaps in a live Recorder (hosted control plane). Call before serving; the default is telemetry.Noop.
type StaticPriorities ¶ added in v0.2.11
StaticPriorities is the OSS policy, read from controller config.
func NewStaticPriorities ¶ added in v0.2.11
func NewStaticPriorities(m map[string]int) StaticPriorities
NewStaticPriorities lower-cases the keys: GitHub logins are case-insensitive and agents report operator-typed casing.