lifecyclegateway

package
v1.0.0-beta.113 Latest Latest
Warning

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

Go to latest
Published: Jun 19, 2026 License: MIT Imports: 17 Imported by: 0

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

View Source
const ComponentName = "lifecycle-gateway"

ComponentName is the registry name. Exported so apps can resolve the component from ComponentRegistry by string without copying the literal.

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

func Register

func Register(registry *component.Registry) error

Register registers the lifecycle-gateway factory.

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

func (c *Component) Initialize() error

Initialize is a no-op; the Manager is wired at factory time.

func (*Component) InputPorts

func (c *Component) InputPorts() []component.Port

InputPorts returns no input ports — the gateway is request-driven, not NATS-fed.

func (*Component) Meta

func (c *Component) Meta() component.Metadata

Meta implements component.Discoverable.

func (*Component) OpenAPISpec

func (c *Component) OpenAPISpec() *service.OpenAPISpec

OpenAPISpec implements gateway.OpenAPIProvider.

func (*Component) OutputPorts

func (c *Component) OutputPorts() []component.Port

OutputPorts returns no output ports — the gateway is request-driven, not NATS-fed.

func (*Component) RegisterHTTPHandlers

func (c *Component) RegisterHTTPHandlers(prefix string, mux *http.ServeMux)

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

func (c *Component) Start(ctx context.Context) error

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

func (c *Component) Stop(_ time.Duration) error

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.

func (*Config) Validate

func (c *Config) Validate() error

Validate enforces operator-friendly constraints on the config. Defaults are applied by ApplyDefaults; Validate runs AFTER defaults so any invariant violation is genuine.

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

type StatePatchRequest map[string]any

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

type TransitionRequest struct {
	Phase string `json:"phase"`
	Note  string `json:"note,omitempty"`
}

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.

Jump to

Keyboard shortcuts

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