feat(deployer): configurable per-workload deploy strategy (blue-green for built sources)
Add a deploy_strategy field to each source's config blob — "" (default), "recreate", or "blue-green" — validated in each source's Validate and read on the deploy path. No new DB column, no migration: the field rides inside the existing SourceConfig JSON and every existing workload decodes "" to its historical behavior (image -> blue-green, others -> recreate). The real gap this closes: dockerfile and static stopped the old container before creating the new one on every redeploy — a downtime window image never had. Their blue-green branch now: - names the new "green" container with a unique suffix so it coexists with the still-serving blue (plumbed into both the container name AND the proxy forwardHost); - skips the collision teardown that destroyed blue early; - gates green — an HTTP readiness probe (deps.Health.Check) when a healthcheck is configured, else the existing liveness window; - swaps the route via a pure upsert (no pre-DeleteRoute) so NPM repoints in place with no gap; - persists green into the single runtime-state row BEFORE reaping blue, so a crash mid-swap can never orphan green or leave the row pointing at a removed container (state.go/teardown.go/reconcile.go stay untouched). image honors explicit "recreate" (reap existing containers after pull, before cutover); its default blue-green path is unchanged. compose stays stack-managed and rejects "blue-green" at Validate so the contract is honest. static forces recreate for storage-backed deno sites — blue-green would mount the same RW volume into both containers at once. Shared helper internal/workload/plugin/strategy.go (ValidateStrategy + BuildGreenName). Backend-only (phase 1); the field is usable today via the app's advanced-JSON editor — a friendly toggle + i18n follow in phase 2. Tests: ValidateStrategy matrix, per-source Validate (incl. the empty-key backward-compat lock), and effectiveStrategy defaults + the deno gate. Design + adversarial review: docs/plans/DEPLOY_STRATEGY_PLAN.md.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Deploy strategy values for a source's DeployStrategy config field.
|
||||
//
|
||||
// - "" (empty) — back-compat default; each source resolves it to its
|
||||
// historical behavior (image -> blue-green, others -> recreate). Every
|
||||
// pre-existing workload row decodes to this.
|
||||
// - StrategyRecreate — stop the old container, then start the new one
|
||||
// (a brief downtime window; what dockerfile/static/compose do today).
|
||||
// - StrategyBlueGreen — start the new container alongside the old, gate it,
|
||||
// swap the proxy route, then reap the old (zero-downtime under NPM).
|
||||
const (
|
||||
StrategyRecreate = "recreate"
|
||||
StrategyBlueGreen = "blue-green"
|
||||
)
|
||||
|
||||
// ValidateStrategy checks a deploy_strategy config value. "" is always valid
|
||||
// (the back-compat default). StrategyRecreate is always valid. StrategyBlueGreen
|
||||
// is valid only when the source supports it (allowBlueGreen) — compose passes
|
||||
// false because a whole-stack blue-green is not implemented. Reserved values
|
||||
// such as "rolling" are rejected until implemented so a config can't silently
|
||||
// claim a behavior the deployer won't honor.
|
||||
func ValidateStrategy(value string, allowBlueGreen bool) error {
|
||||
switch value {
|
||||
case "", StrategyRecreate:
|
||||
return nil
|
||||
case StrategyBlueGreen:
|
||||
if allowBlueGreen {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("deploy_strategy %q is not supported for this source kind; use \"recreate\"", value)
|
||||
default:
|
||||
return fmt.Errorf("invalid deploy_strategy %q (valid: \"\", %q, %q)", value, StrategyRecreate, StrategyBlueGreen)
|
||||
}
|
||||
}
|
||||
|
||||
// BuildGreenName appends a unique millisecond-hex suffix to a source's
|
||||
// otherwise-deterministic container name so a new "green" container can run
|
||||
// alongside the old "blue" during a blue-green cutover. Sources whose names
|
||||
// are deterministic (dockerfile, static) collide on Docker's per-daemon
|
||||
// unique-name constraint without this; the suffix lets both coexist until the
|
||||
// route swaps and blue is reaped. Mirrors the image source's ms-hex scheme.
|
||||
func BuildGreenName(base string, ts time.Time) string {
|
||||
return fmt.Sprintf("%s-%x", base, ts.UnixMilli())
|
||||
}
|
||||
Reference in New Issue
Block a user