HyperFleet Sentinel is a Kubernetes resource watcher that polls the HyperFleet API for cluster/nodepool updates, makes orchestration decisions via CEL-based decision logic, and publishes CloudEvents to message brokers. Stateless, horizontally scalable via label-based sharding, delegates all state persistence to the API.
- Language: Go 1.26 (see
go.mod) - Messaging: Broker abstraction (RabbitMQ, GCP Pub/Sub, Stub)
- API Client: Generated from hyperfleet-api-spec — see openapi/README.md
- Deployment: Helm chart in
charts/
Sentinel is one component in the HyperFleet control plane:
- API — persists cluster/nodepool state (source of truth)
- Sentinel — watches API, decides when resources need reconciliation, publishes events
- Adapters — consume events, execute provisioning/deprovisioning, report back to API
- Broker (RabbitMQ or Pub/Sub) — decouples Sentinel from adapters
Generated OpenAPI client is NOT committed to git. Before any build, test, or development task:
make generate # Extracts OpenAPI spec from hyperfleet-api-spec module and generates Go clientSetup sequence for a fresh clone:
make generate— generate OpenAPI client inpkg/api/openapi/make download— fetch Go dependenciesmake install-hooks— install pre-commit hooks (secret scanning, linting, etc.)make build— buildbin/sentinelbinarymake test— verify unit tests pass
| Command | What it does |
|---|---|
make verify |
go vet + format check (fast) |
make lint |
golangci-lint (pinned in tools/go.mod) |
make test |
all tests (./...), writes coverage.out profile |
make test-unit |
unit tests only — specific internal/ and pkg/ packages |
make test-integration |
integration tests with testcontainers (Docker required) |
make test-coverage |
runs make test then opens HTML coverage report |
make test-helm |
Helm chart lint + template validation (10 scenarios) |
make test-all |
test + test-integration + test-helm + lint |
Quick feedback: make verify && make test-unit. Full pre-push: make test-all.
PR pre-flight order:
make generatemake fmtmake lintmake test-unitmake test-integration— if broker/API changesmake test-helm— if chart changes- Update CHANGELOG.md if the change is user-visible
| Topic | Where to look |
|---|---|
| Configuration reference | docs/config.md |
| Metrics definitions | docs/metrics.md, internal/metrics/ |
| Development setup | docs/development.md |
| Helm deployment | docs/deployment.md |
| GKE dev deployment | docs/sentinel-for-gke-dev.md for GKE dev deployment |
| Multi-instance sharding | docs/multi-instance-deployment.md |
| Alerts and runbooks | docs/alerts.md, docs/runbook.md |
| Helm values | charts/values.yaml |
| Contributing and setup | CONTRIBUTING.md |
| OpenAPI client generation | openapi/README.md |
| Example configs | configs/dev-example.yaml, configs/rabbitmq-example.yaml, configs/gcp-pubsub-example.yaml |
| Broker configuration | broker.yaml (loaded by hyperfleet-broker; override path via BROKER_CONFIG_FILE env var) |
| CloudEvents / CEL payloads | internal/payload/ |
| Resource profiling | docs/resource-profiling.md |
Sentinel's job: decide when, not execute how. It can be killed and restarted at any time without data loss — this is what makes label-based sharding safe. The message_decision config uses CEL expressions to decide when to publish — see DefaultMessageDecision() in internal/config/config.go for default expressions.
- Config validation fails fast —
Validate()returns error at startup,LoadConfig()propagates to main which exits non-zero - Context propagation —
context.Contextthreaded through all calls with correlation keys (OpID, TraceID, SpanID, DecisionReason) - Health probes —
/healthz(liveness: stale poll detection),/readyz(readiness: broker + first successful poll)
Format: HYPERFLEET-### - type: description
Example:
HYPERFLEET-427 - feat: add standard metrics labels
Adds resource_type and resource_selector labels to all
Prometheus metrics for consistent querying.
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By trailer required on all Claude-assisted commits.
- Config struct in
internal/config/config.go— YAML struct tags, validation viaValidate() - All durations use
time.Durationwith YAMLdurationformat (e.g.,5s,30m) - Config precedence (highest wins): CLI flags > env vars (
HYPERFLEET_*) > YAML file > defaults - Broker credentials handled separately via
broker.yaml(orBROKER_CONFIG_FILEenv var)
sentinel serve --config config.yaml— run the servicesentinel config-dump --config config.yaml— print merged config (debug precedence issues)sentinel version— print version, commit, build date- Run
sentinel serve --helpfor full flag list
- Log at boundaries (main service loop), not deep in call stack
- Custom structured logger in
pkg/logger/— stdlib only, no external deps - Interface:
logger.HyperFleetLoggerwithInfo(),Error(),Warn(),Debug(),V(level)(verbosity),Extra() - Create via
logger.NewHyperFleetLogger()— uses global config - Chaining:
logger.Extra("key", val).Extra("key2", val2).Info("msg") - IMPORTANT: always use
pkg/logger, neverlog/slogdirectly
message_data config uses CEL expressions, not static values:
message_data:
id: resource.id
kind: resource.kind
href: resource.hrefCEL context:
resource— cluster/nodepool object from API (id, kind, href, generation, status, labels, etc.)reason— decision reason string from engine (e.g.,"message decision matched","message decision result is false")condition("Type")— custom function to look up resource status condition by type namenow— current timestamptimestamp(),duration()— standard CEL time functions
- Table-driven tests with plain
ifassertions — no testify - Mocking via simple interface implementations (e.g., MockPublisher), no gomock
- Unit tests live alongside code:
foo_test.gonext tofoo.go - Integration tests in
test/integration/with//go:build integrationtag - Prometheus metrics verified with
prometheus/testutil - Run single test:
go test -run TestDecisionEngine ./internal/engine/...
- Branch from
main, PR back tomain - Branch naming:
HYPERFLEET-###-short-description
Install: make install-hooks
Hooks:
leaktk.git.pre-commit— secret scanning (open-source, no VPN required)hyperfleet-commitlint— validates commit message format (commit-msg stage)hyperfleet-gofmt— Go code formattinghyperfleet-golangci-lint— lintinghyperfleet-go-vet— Go vet checkstrailing-whitespace— removes trailing whitespaceend-of-file-fixer— ensures files end with newlinecheck-added-large-files— prevents large files from being committed
DO NOT:
- Add business logic to Sentinel — orchestration decisions only, execution belongs in adapters
- Store state in Sentinel — it is stateless, API is source of truth
- Hardcode the resource polling interval — always use
poll_intervalfrom config for the main sentinel loop; adding a second resource polling loop bypasses the single-ticker backpressure model
DO:
- Update
hyperfleet-api-specversion ingo.modand runmake generatewhen API spec changes - New exported functions require unit tests; new broker/API interactions require integration tests
- Add metrics when adding observable behavior — see docs/metrics.md for conventions
- Convention:
message_datashould includeid,kind,hreffields (not enforced by validation, but expected by downstream adapters) — seeconfigs/dev-example.yaml - Use broker abstraction (
hyperfleet-broker) — never import RabbitMQ/Pub/Sub clients directly
make generateis mandatory — build and tests fail without it; generated code is gitignoredpkg/api/openapi/is read-only — never hand-edit, always regenerate- Broker config comes from
broker.yaml(orBROKER_CONFIG_FILEenv var), not sentinel YAML config — handled by hyperfleet-broker library - CEL expressions in
message_dataare compiled at startup — syntax errors fail fast, but semantic errors (wrong field names on resource) surface at evaluation time - Metrics labels must include
resource_typeandresource_selector— see docs/metrics.md for naming conventions - Metrics use
sync.Onceregistration — callResetSentinelMetrics()in tests to avoid duplicate registration panics - No testify — project uses plain Go assertions and table-driven tests; don't introduce testify