api

package
v1.0.0-rc5 Latest Latest
Warning

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

Go to latest
Published: Aug 3, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package api serves the controller's HTTP interface.

Per D1 it is designed UI-first: the API comes first, a TUI view second and a web UI third, and it has to already be the shape those need. That is a constraint on the endpoint set rather than a slogan — every screen a UI has is one request, and every action a user can take is one endpoint:

GET  /healthz                                unauthenticated liveness
GET  /api/v1/status                          the controller's own state
GET  /api/v1/applications                    the list view
GET  /api/v1/applications/{app}              the detail view
GET  /api/v1/applications/{app}/diff         the diff view
GET  /api/v1/applications/{app}/history      the history view
POST /api/v1/applications/{app}/sync         the sync button
GET  /api/v1/events                          live updates, so nothing polls

Applications are read-only: they are declared in the app set, which is either mounted at deploy time or committed to git, and changing them means changing that file rather than posting to this API. The paths are nouns so that CRUD can be added later without any of them moving.

That set is the core's. A companion module adds routes of its own through the extension seam, and everything Handler registers — core route or companion's — is served behind guard, with the authz.Action the route declares, unless the companion declared it public on purpose. See docs/extensibility.md.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Controller

type Controller interface {
	Status() application.ControllerStatus
}

Controller reports the controller's own state, as distinct from the applications'. *appset.Loop implements it.

type Options

type Options struct {
	Authorizer authz.Authorizer
	Log        *slog.Logger
	// Controller reports where the app set came from and whether it is loading.
	// Absent, the status endpoint still answers — with the application count and
	// an empty app-set mode, which is what "no app-set source is wired" looks
	// like. A status endpoint that 404s is a status endpoint a monitor cannot
	// tell from a dead controller.
	Controller Controller
}

Options tune a Server. Every field has a working default.

type Reconciler

type Reconciler interface {
	Views() []application.View
	View(app string) (application.View, bool)
	Diffs(app string) ([]application.ReleaseDiff, error)
	History(ctx context.Context, app string) (application.History, error)
	// AcceptSync rather than SyncNow: the handler has to know whether a sync was
	// started before it writes the response, and the sync itself outlives the
	// request. See sync below.
	AcceptSync(app string) (func(context.Context) error, error)
}

Reconciler is what the API serves. *reconcile.Reconciler implements it.

This package does not import reconcile, and the two sentinels the handlers below match on live in application for that reason. An alternative reconciler is the whole point of stating this as an interface, and one that had to import the OSS applier — go-git, the chart engine, the moby client — for two error values would have had no way to take it up.

type RegisteredRoute

type RegisteredRoute struct {
	Pattern   string // "GET /api/v1/projects"
	Extension string // the name it registered under; empty for a core route
	Public    bool   // served with no authentication
}

RegisteredRoute is one route the server serves, and who put it there.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is the HTTP interface. It is also a notify.Notifier: the event stream is fed by the same seam that feeds the log, which is why notify appends rather than replaces — a companion adding Slack must not silently kill the UI's live updates.

func New

func New(rec Reconciler, o Options) *Server

New returns a Server over rec.

It does not register itself as a notifier. The caller does that, so that the notifier list is not appended to as a side effect of constructing a server — which in a test suite means one stream per test, all still subscribed.

func (*Server) Drain

func (s *Server) Drain()

Drain ends every connected event stream. The caller registers it with http.Server.RegisterOnShutdown; see stream.closeAll for why Shutdown cannot do it on its own.

func (*Server) Handler

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

Handler returns the router.

This is where the extension seam is read, and it is read exactly once. A seam.List is not settled at the end of init() and a consumer may not snapshot it at construction — but nothing joins this one after the mux exists, so the moment the mux is built is the moment the answer stops changing, and reading it here is the consumer-side half of that rule rather than an exception to it.

The error is a refusal to start. Everything a companion declared is checked before a single route reaches the mux, because both alternatives reach an operator as an outage rather than as a controller that would not start: net/http panics on a duplicate pattern, deep inside wiring, and a nil handler would panic on the first request that hit it.

func (*Server) Notify

func (s *Server) Notify(ctx context.Context, e notifyEvent)

Notify feeds the event stream. It is the notify.Notifier implementation; the caller registers it.

func (*Server) Routes

func (s *Server) Routes() []RegisteredRoute

Routes reports every route Handler registered — the core's first, then each extension's in registration order — and is meaningful only once Handler has succeeded, because until the seam has been read the set is not decided.

It exists for the startup log rather than for the router. An operator's only signal that a companion added an unauthenticated endpoint to a process holding the docker socket is a line naming the pattern and the module that added it, and a companion's source is not something the operator running the binary necessarily has.

The caller logs the guarded ones as a list and warns, one line each, on the public routes that carry an extension name. Not on a public core route: this repository argued for that one in its own source and no deployment can change it, and a WARN on every startup about something nobody can act on is how an operator learns to ignore the WARN that matters.

Jump to

Keyboard shortcuts

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