e3d140c57a
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.
59 lines
1.8 KiB
Go
59 lines
1.8 KiB
Go
package static
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/alexei/tinyforge/internal/workload/plugin"
|
|
)
|
|
|
|
func validCfg(extra map[string]any) json.RawMessage {
|
|
m := map[string]any{"repo_owner": "o", "repo_name": "r"}
|
|
for k, v := range extra {
|
|
m[k] = v
|
|
}
|
|
b, _ := json.Marshal(m)
|
|
return b
|
|
}
|
|
|
|
func TestValidate_Strategy(t *testing.T) {
|
|
cases := []struct {
|
|
strategy string
|
|
wantErr bool
|
|
}{
|
|
{"", false},
|
|
{"recreate", false},
|
|
{"blue-green", false},
|
|
{"rolling", true},
|
|
{"junk", true},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run("strategy="+c.strategy, func(t *testing.T) {
|
|
err := (&source{}).Validate(validCfg(map[string]any{"deploy_strategy": c.strategy}))
|
|
if (err != nil) != c.wantErr {
|
|
t.Fatalf("Validate(strategy=%q) err=%v, wantErr=%v", c.strategy, err, c.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEffectiveStrategy_DefaultAndDenoGate(t *testing.T) {
|
|
if got := effectiveStrategy(Config{}); got != plugin.StrategyRecreate {
|
|
t.Fatalf("empty strategy = %q, want recreate", got)
|
|
}
|
|
if got := effectiveStrategy(Config{DeployStrategy: plugin.StrategyBlueGreen}); got != plugin.StrategyBlueGreen {
|
|
t.Fatalf("plain blue-green = %q, want blue-green", got)
|
|
}
|
|
// Storage-backed deno site requesting blue-green is forced to recreate to
|
|
// avoid a concurrent-writer overlap on the shared /app/data volume.
|
|
denoStorage := Config{DeployStrategy: plugin.StrategyBlueGreen, StorageEnabled: true, Mode: "deno"}
|
|
if got := effectiveStrategy(denoStorage); got != plugin.StrategyRecreate {
|
|
t.Fatalf("deno+storage blue-green = %q, want recreate (forced)", got)
|
|
}
|
|
// A deno site WITHOUT storage may use blue-green.
|
|
denoNoStorage := Config{DeployStrategy: plugin.StrategyBlueGreen, Mode: "deno"}
|
|
if got := effectiveStrategy(denoNoStorage); got != plugin.StrategyBlueGreen {
|
|
t.Fatalf("deno (no storage) blue-green = %q, want blue-green", got)
|
|
}
|
|
}
|