GitHub
10/23/2026, 7:56 PMosquery_rollouts table keyed by (environment, request id). Setting the target creates a new rollout and replaces the old one. Clearing the target aborts it. Progress counts (eligible, upgraded, waiting, rolled back) and the rolled-back host list are computed on demand with no new schema for them.
• `Resolve`: new reasons rollout_pending and rollout_paused for environment-sourced targets, evaluated after ahead_of_target. Both carry source and request_id and no target. The route stays O(1) and never scans hosts.
• osctrl-tls:
• A valid rollback carrying the rollout's request id pauses the rollout with a compare-and-set.
• The pause is audited once and names the host.
• A failed pause write answers 500 so osctrld retries.
• A rollback pauses only if it was first recorded, by server time, after the last Resume. osctrld resends its last rejected result, and without this rule a Resume would be undone about one sync later.
• New metric osctrld_osquery_rollout_paused{environment}. Aggregate it with max by (environment).
• osctrl-api:
• The environment osquery response gains a rollout object (null when there is none).
• New endpoints POST /api/v1/environments/osquery/{env}/rollout/resume and .../rollout/abort.
• OpenAPI is regenerated.
• Frontend: a rollout panel on Environment › Osquery.
• Running and paused states show the stage bar, counts, time to the next stage and the rolled-back hosts.
• Resume and Abort sit behind confirmations.
• Completed and aborted show one line.
• The Set target confirmation says the rollout starts at the first stage.
• The node page has wording for the two new reasons.
Behavior notes
• A rollback after the rollout reaches 100% still pauses it, so the 50% → 100% wave can be stopped. Resume and Abort both work from that state. This differs from the spec's "no gating after completion", and the spec carries an amendment.
• Hosts that rolled back are not retried under the same request id after Resume. This is osctrld's existing rejected-memory rule. Other hosts continue.
• Targets set before this change have no rollout row and are never gated.
• The node page shows a generic "not in the rollout yet" sentence. It does not give the stage or time, because the node response has no stage information.
Security and operational impact
• Permissions: Resume and Abort check the environment-admin permission server-side. Denials are audited, and cookie sessions need a CSRF token on both POST routes.
• Untrusted input:
• Host-reported values on the pause path are validated against the existing reason allowlist, a version format and a 32-hex request id.
• Host names go through a cleaner that drops control characters and caps the length.
• The UI renders them as plain text.
• A node can only pause the rollout of its own environment.
• Schema (additive only):
• new table osquery_rollouts
• new column upgrade_event_at on the node status table
• resumed_at on the rollout
• Cost: each GET or mutation on the environment osquery endpoint loads the environment's non-Windows hosts to compute counts. This is fine at normal sizes. Caching is the obvious follow-up for environments around 100,000 hosts. Aborted rollouts skip the scan.
• Rolling deploys: a mixed-version window can record an upgrade event without upgrade_event_at. That event does not pause a rollout. Nothing is lost, because old replicas have no pause logic.
• Windows: hosts are excluded from the rollout, matching the existing unsupported_platform answer.
Validation
• go test ./... -count=1, go vet ./..., gofmt -l cmd pkg
• `golangci-lint run ./...`: 0 issues
• `make openapi-check`: clean
• Frontend: npm test (638 tests) and npm run check
• New tests cover:
• thresholds for tiny environments
• the effective stage over time
• concurrent pause
• the Resume-then-resend case
• pins and ahead_of_target
• cross-environment isolation
• permission denials
• the Abort and Resume state rules
• host-name cleaning
• Not done: a manual rollout on a real staging environment. Please check that stage 1 picks hosts, stages open on schedule, a forced rollback pauses, Resume continues and Abort clears the target.
Compatibility
Pairs with the osctrld change that records the two new reasons. Merge order does not matter: older osctrld releases ignore the reasons, so gated hosts simply do not upgrade, and nothing breaks.
jmpsec/osctrlGitHub
10/23/2026, 8:19 PM