Files
tiny-forge/internal/workload/plugin/source/dockerfile/strategy_test.go
T
alexei.dolgolyov e3d140c57a 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.
2026-06-19 16:51:20 +03:00

50 lines
1.4 KiB
Go

package dockerfile
import (
"encoding/json"
"testing"
"github.com/alexei/tinyforge/internal/workload/plugin"
)
// validCfg is the smallest config that passes the non-strategy checks, so a
// test isolates the deploy_strategy behavior.
func validCfg(strategy string) json.RawMessage {
m := map[string]any{"repo_owner": "o", "repo_name": "r", "port": 8080}
if strategy != "" {
m["deploy_strategy"] = strategy
}
b, _ := json.Marshal(m)
return b
}
func TestValidate_Strategy(t *testing.T) {
cases := []struct {
strategy string
wantErr bool
}{
{"", false}, // backward-compat: no key -> valid
{"recreate", false},
{"blue-green", false}, // dockerfile supports blue-green
{"rolling", true}, // reserved, not yet implemented
{"junk", true},
}
for _, c := range cases {
t.Run("strategy="+c.strategy, func(t *testing.T) {
err := (&source{}).Validate(validCfg(c.strategy))
if (err != nil) != c.wantErr {
t.Fatalf("Validate(strategy=%q) err=%v, wantErr=%v", c.strategy, err, c.wantErr)
}
})
}
}
func TestEffectiveStrategy_Default(t *testing.T) {
if got := effectiveStrategy(Config{}); got != plugin.StrategyRecreate {
t.Fatalf("empty strategy = %q, want recreate (historical default)", got)
}
if got := effectiveStrategy(Config{DeployStrategy: plugin.StrategyBlueGreen}); got != plugin.StrategyBlueGreen {
t.Fatalf("explicit blue-green = %q, want blue-green", got)
}
}