Documentation
¶
Overview ¶
Package lifecyclegateway — Discoverable + Gateway + factory.
Package lifecyclegateway exposes the pkg/lifecycle.Manager's read + operator-mutation surface over HTTP and WebSocket. ADR-047 PR 3. The endpoints are deliberately framework-shape — every app's lifecycle-managed workflow types (drone missions, sensor lifecycles, manufacturing batches, scenario executions, API request lifecycles) become uniformly inspectable + operator- patchable through one gateway component.
Wiring contract:
- The component reads Dependencies.LifecycleManager. Apps that declare no lifecycle-managed entity types simply don't run this gateway; apps that DO run it pass the same Manager into Dependencies that the rule processor consumes for lifecycle_* actions + $entity.lifecycle.* substitutions.
- RegisterHTTPHandlers mounts the routes under the configured PathPrefix (default "/workflows"). Each app deployment chooses where in its gateway placement to mount this — there's no hardcoded port or standalone binary.
Endpoint surface (ADR-047 line 305-315):
- GET {prefix} → list registered workflow types + instance counts
- GET {prefix}/{type} → list instances (Phase/Active/Match/Limit/Offset query params)
- GET {prefix}/{type}/{id} → get full instance state
- GET {prefix}/{type}/{id}/history → phase transition history (KV revision replay)
- GET {prefix}/{type}/{id}/children → child instance summaries
- POST {prefix}/{type}/{id}/state → operator patch (validates lifecycle:"operator_writable")
- POST {prefix}/{type}/{id}/transition → operator-initiated transition (validates transitions table)
- GET {prefix}/{type}?stream=true (websocket) → live updates via Manager.Watch
All endpoints loud-fail on missing manager / unknown workflow / unknown entity rather than silently returning empty results. Error responses use a uniform JSON envelope {"error": "..."} so operator dashboards can render the underlying cause without per-endpoint parsing.
Scaling cliffs documented at ADR-047 § "Scaling cliff (operator guidance)" apply to the gateway's List/Watch/History/Children paths since they are direct Manager pass-throughs. Operators observing dashboard latency at high cardinality should file a bottleneck issue per the ADR's v2 secondary-index upgrade path.
Package lifecyclegateway — HTTP + WebSocket handlers.
Single catch-all `serveHTTP` dispatches by trimmed path + method. Path-tree shape (ADR-047 § Operator API):
{root} GET → list workflow types + counts
{root}/{type} GET → list instances (query params for filters) — or WS upgrade when ?stream=true
{root}/{type}/{id} GET → instance state
{root}/{type}/{id}/history GET → phase-transition history
{root}/{type}/{id}/children GET → child instances
{root}/{type}/{id}/state POST → operator patch
{root}/{type}/{id}/transition POST → operator transition
Error model: every non-2xx response is `{"error": "..."}` so operator dashboards parse one envelope. The mapping from pkg/lifecycle errors → HTTP status is centralized in `errorToStatus`. Workflow-not-registered + entity-not-found map to 404; field-not-operator-writable + invalid-transition + terminal-phase map to 400 (client must fix); retries-exhausted maps to 409 (race with another writer); anything else 500.
Package lifecyclegateway — OpenAPI spec.
The lifecycle-gateway implements gateway.OpenAPIProvider so the operator-facing endpoints show up in the framework's auto- generated OpenAPI surface (specs/openapi.v3.yaml). Matches the pattern from gateway/graph-gateway/openapi.go.
Path templates use literal "{prefix}/{type}/{id}" placeholders — the actual path prefix is operator-configurable per deployment, so the spec advertises the shape, not the bound paths.
Index ¶
- Constants
- func CreateLifecycleGateway(rawConfig json.RawMessage, deps component.Dependencies) (component.Discoverable, error)
- func Register(registry *component.Registry) error
- type Component
- func (c *Component) ConfigSchema() component.ConfigSchema
- func (c *Component) DataFlow() component.FlowMetrics
- func (c *Component) Health() component.HealthStatus
- func (c *Component) Initialize() error
- func (c *Component) InputPorts() []component.Port
- func (c *Component) Meta() component.Metadata
- func (c *Component) OpenAPISpec() *service.OpenAPISpec
- func (c *Component) OutputPorts() []component.Port
- func (c *Component) RegisterHTTPHandlers(prefix string, mux *http.ServeMux)
- func (c *Component) Start(ctx context.Context) error
- func (c *Component) Stop(_ time.Duration) error
- type Config
- type LifecycleManager
- type StatePatchRequest
- type TransitionRequest
Constants ¶
const ComponentName = "lifecycle-gateway"
ComponentName is the registry name. Exported so apps can resolve the component from ComponentRegistry by string without copying the literal.
const DefaultMaxBodyBytes int64 = 1 << 20
DefaultMaxBodyBytes is the request-body size cap applied to operator-supplied JSON on POST endpoints when Config.MaxBodyBytes is zero or negative. 1 MiB is generous for `operator_writable` patches (the protected-field surface should be narrow) and small enough to make resource-exhaustion attacks visible.
Variables ¶
This section is empty.
Functions ¶
func CreateLifecycleGateway ¶
func CreateLifecycleGateway(rawConfig json.RawMessage, deps component.Dependencies) (component.Discoverable, error)
CreateLifecycleGateway is the component-factory function. The rule processor and lifecycle-gateway both pull the same Manager from deps.LifecycleManager. Refusing to instantiate when the manager is nil is the loud-fail signal: a deployment that configures the gateway but doesn't wire a Manager has no workflows to expose — failing at boot surfaces the gap before an operator hits an empty endpoint and assumes "no instances."
Types ¶
type Component ¶
type Component struct {
// contains filtered or unexported fields
}
Component is the lifecycle-gateway runtime. Wraps the shared Manager + emits the HTTP/WS surface.
func (*Component) ConfigSchema ¶
func (c *Component) ConfigSchema() component.ConfigSchema
ConfigSchema implements component.Discoverable.
func (*Component) DataFlow ¶
func (c *Component) DataFlow() component.FlowMetrics
DataFlow reports gateway metrics. Throughput is averaged since startup; sliding-window per-second is a future addition matching the http gateway TODO.
func (*Component) Health ¶
func (c *Component) Health() component.HealthStatus
Health reports gateway health. Healthy = running. ErrorCount maps to failed-request count so operator dashboards see uniform across-gateway metrics.
func (*Component) Initialize ¶
Initialize is a no-op; the Manager is wired at factory time.
func (*Component) InputPorts ¶
InputPorts returns no input ports — the gateway is request-driven, not NATS-fed.
func (*Component) OpenAPISpec ¶
func (c *Component) OpenAPISpec() *service.OpenAPISpec
OpenAPISpec implements gateway.OpenAPIProvider.
func (*Component) OutputPorts ¶
OutputPorts returns no output ports — the gateway is request-driven, not NATS-fed.
func (*Component) RegisterHTTPHandlers ¶
RegisterHTTPHandlers mounts the lifecycle gateway routes under the parent prefix. The routing logic is intentionally non-mux-based at the leaf — net/http.ServeMux only handles prefix-based dispatch, so the gateway uses a single catch-all HandleFunc rooted at `{prefix}{path_prefix}/` and parses the remaining path segments inside the handler. This keeps the ServeMux contract simple (one entry per gateway component instance) and the path-parsing testable in isolation.
func (*Component) Start ¶
Start records the start time + flips running. The gateway has no background goroutines of its own — handlers serve on demand via the parent HTTP server.
func (*Component) Stop ¶
Stop flips running off. Live WebSocket connections + in-flight HTTP requests are owned by the parent HTTP server — they close as part of the server's graceful shutdown, not via any check inside this component (we don't gate handlers on running.Load() — graph- gateway and the http gateway follow the same convention).
type Config ¶
type Config struct {
// PathPrefix is mounted under the parent component prefix (the
// arg to RegisterHTTPHandlers). Default "workflows" so the
// fully-resolved routes look like /lifecycle-gateway/workflows
// or /workflows when mounted at root. Leading slash is added
// automatically; trailing slash is normalized.
PathPrefix string `` /* 272-byte string literal not displayed */
// EnableWebSocket toggles the WS upgrade path for
// {prefix}/{type}?stream=true. Default true. Operators that
// don't need live updates (purely poll-based dashboards) can
// disable it by setting `"enable_websocket": false` in config.
//
// Pointer type (vs bare bool) so ApplyDefaults can distinguish
// "field absent from config" (→ default true) from "field
// explicitly set to false" (→ disable). Matches the
// MaxIterations precedent in processor/rule/actions.go.
// Callers read via the wsEnabled() helper, never field-direct.
EnableWebSocket *bool `` /* 265-byte string literal not displayed */
// MaxBodyBytes caps the size of operator-supplied JSON bodies on
// POST {type}/{id}/state and POST {type}/{id}/transition (S2
// reviewer fix — unbounded reads were a DoS vector). Default 1MB.
// Operators that genuinely need larger patch bodies (uncommon —
// operator_writable surface should be narrow) raise this
// explicitly. Zero or negative means "no override" → default.
MaxBodyBytes int64 `` /* 221-byte string literal not displayed */
// AllowedOrigins is the WebSocket-upgrade origin allowlist
// (S7 reviewer fix — the gateway's POST surface mutates state,
// so default-permissive cross-origin is a real risk; an explicit
// allowlist forces operators to make the cross-origin decision
// rather than inheriting the default). Empty list means
// "permit all" — the gateway emits a Warn log at Start time so
// operators see the policy choice in their logs. Wildcards are
// not supported; explicit origins only.
AllowedOrigins []string `` /* 248-byte string literal not displayed */
}
Config is the operator-facing configuration surface. Field tags populate the generated OpenAPI schema (`schemas/lifecycle-gateway.v1.json`) — every field MUST carry `schema:"type:...,description:...,category:..."` so operator dashboards can render the config surface. See `component/schema_tags.go` for the conventions.
func DefaultConfig ¶
func DefaultConfig() Config
DefaultConfig returns a Config ready for use in tests + reasonable-default deployments.
func (*Config) ApplyDefaults ¶
func (c *Config) ApplyDefaults()
ApplyDefaults populates omitted fields. Called by the factory before Validate so operator-provided values stay sticky and unset fields fall back to framework conventions.
EnableWebSocket is the only nullable field: nil means "operator omitted the key" → resolve to true. A non-nil pointer (even to false) is honored as-is. Same shape as MaxIterations in processor/rule/actions.go.
type LifecycleManager ¶
type LifecycleManager interface {
ListWorkflows() []lifecycle.WorkflowDef
List(ctx context.Context, workflow string, opts lifecycle.ListOptions) ([]lifecycle.Participant, error)
Get(ctx context.Context, workflow, entityID string) (lifecycle.Participant, error)
History(ctx context.Context, workflow, entityID string) ([]lifecycle.TransitionEvent, error)
Children(ctx context.Context, parentEntityID string, opts lifecycle.ChildOptions) ([]lifecycle.ChildResult, error)
References(ctx context.Context, entityID string) ([]lifecycle.ReferenceStub, error)
UpdateFromOperator(ctx context.Context, workflow, entityID string, patch map[string]any) error
Transition(ctx context.Context, workflow, entityID, newPhase string, source lifecycle.TransitionSource, note string) error
Watch(ctx context.Context, workflow string) (<-chan lifecycle.Participant, error)
}
LifecycleManager is the subset of *lifecycle.Manager used by the gateway. Defined as an interface here (consumer-defined, matching the rule processor's pattern in processor/rule.LifecycleManager) so tests can swap in a mock without depending on pkg/lifecycle internals (kvMockStore is unexported by design).
The interface is wide enough to support all 8 endpoints + the WS watch surface; production deployments pass the concrete *lifecycle.Manager via Dependencies.LifecycleManager which implements every method implicitly.
type StatePatchRequest ¶
StatePatchRequest is the JSON body shape for POST {type}/{id}/state. Reflective-typed schema; the actual content is a free-form `map[string]any` field-name → value patch validated server-side against `lifecycle:"operator_writable"` tags.
type TransitionRequest ¶
TransitionRequest is the JSON body shape for POST {type}/{id}/transition. Phase is required; Note is operator commentary persisted into the TransitionEvent.Note for audit.