| new file mode 100644 |
| index 000000000..1f85ce8a5 |
| --- /dev/null |
| +++ b/internal/release/sidecar_adopt_test.go |
| @@ -0,0 +1,203 @@ |
| +package release |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "os" |
| + "reflect" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/registry" |
| +) |
| + |
| +func addSidecar(t *testing.T, reg *registry.Registry, host, app, channel string, hibernate bool) { |
| + t.Helper() |
| + if err := reg.Upsert(registry.Route{ |
| + Host: host, Kind: "sidecar", App: app, Channel: channel, Hibernate: hibernate, |
| + Args: `["sidecar","run"]`, Status: "ready", |
| + }); err != nil { |
| + t.Fatalf("upsert %s: %v", host, err) |
| + } |
| +} |
| + |
| +func TestSidecarAdoptableIsASecondPredicateNotAWiderOne(t *testing.T) { |
| + base := registry.Route{Host: "bot.example.com", Kind: "sidecar", App: "myapp", Status: "ready"} |
| + for _, c := range []struct { |
| + name string |
| + mut func(*registry.Route) |
| + sidecar, instance bool |
| + }{ |
| + {"ready sidecar", func(*registry.Route) {}, true, false}, |
| + {"status empty", func(r *registry.Route) { r.Status = "" }, true, false}, |
| + {"no app", func(r *registry.Route) { r.App = "" }, false, false}, |
| + {"platform", func(r *registry.Route) { r.Platform = true }, false, false}, |
| + {"creating", func(r *registry.Route) { r.Status = "creating" }, false, false}, |
| + {"an instance", func(r *registry.Route) { r.Kind = "instance" }, false, true}, |
| + {"static", func(r *registry.Route) { r.Kind = "static" }, false, false}, |
| + } { |
| + rt := base |
| + c.mut(&rt) |
| + if got := SidecarAdoptable(rt); got != c.sidecar { |
| + t.Errorf("%s: SidecarAdoptable = %v, want %v", c.name, got, c.sidecar) |
| + } |
| + if got := Adoptable(rt); got != c.instance { |
| + t.Errorf("%s: Adoptable = %v, want %v (Adoptable is not widened)", c.name, got, c.instance) |
| + } |
| + } |
| + unit := base |
| + unit.Backing = registry.BackingUnit |
| + if CapabilityRoute(unit) { |
| + t.Error("CapabilityRoute admitted a sidecar: no capability drop-ins are ever written for one") |
| + } |
| +} |
| + |
| +// Spec §7.9: sidecars go through the same selection, staging, repoint and |
| +// stamp, and the pass never restarts them; RestartHibernating changes nothing |
| +// for them. |
| +func TestPassAdoptsASidecarWithoutRestartingIt(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + opt := t.TempDir() |
| + reg := testRegistry(t) |
| + shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin")) |
| + if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil { |
| + t.Fatal(err) |
| + } |
| + addRoute(t, reg, "web.example.com", "myapp", "canary/x", false) |
| + addSidecar(t, reg, "bot.example.com", "myapp", "canary/x", false) |
| + addSidecar(t, reg, "nap.example.com", "myapp", "canary/x", true) |
| + var restarted []string |
| + restart := func(h string) error { restarted = append(restarted, h); return nil } |
| + if err := Pass(ctx, reg, s, pub, opt, restart, RestartHibernating(restart)); err != nil { |
| + t.Fatalf("Pass: %v", err) |
| + } |
| + for _, h := range []string{"web.example.com", "bot.example.com", "nap.example.com"} { |
| + if v, _ := LiveVersion(opt, h); v != "v1" { |
| + t.Errorf("%s live = %q, want v1", h, v) |
| + } |
| + if rt, _ := reg.Get(h); rt == nil || rt.Version != "v1" { |
| + t.Errorf("%s row = %+v, want Version v1 (root's stamp)", h, rt) |
| + } |
| + } |
| + if !reflect.DeepEqual(restarted, []string{"web.example.com"}) { |
| + t.Fatalf("restarted = %v; the pass must never restart a sidecar", restarted) |
| + } |
| +} |
| + |
| +// Sidecars go after every instance, so a build an instance poisons this pass |
| +// never reaches a sidecar. |
| +func TestSidecarIsNotMovedOntoABuildAnInstancePoisoned(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + opt := t.TempDir() |
| + reg := testRegistry(t) |
| + shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("good")) |
| + shipRelease(t, s, "bab", "myapp", "v2", adoptPlatform, []byte("bad")) |
| + if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil { |
| + t.Fatal(err) |
| + } |
| + addRoute(t, reg, "web.example.com", "myapp", "canary/x", false) |
| + addSidecar(t, reg, "bot.example.com", "myapp", "canary/x", false) |
| + if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v2"); err != nil { |
| + t.Fatal(err) |
| + } |
| + restart := func(h string) error { |
| + if h == "web.example.com" { |
| + return errors.New("did not come up") |
| + } |
| + return nil |
| + } |
| + if err := Pass(ctx, reg, s, pub, opt, restart); err != nil { |
| + t.Fatal(err) |
| + } |
| + if v, _ := LiveVersion(opt, "bot.example.com"); v != "v1" { |
| + t.Fatalf("sidecar live = %q, want v1: a build that failed an instance this pass must not reach it", v) |
| + } |
| +} |
| + |
| +// Spec §7.9: sidecars take the same bake holds. |
| +func TestSidecarWaitsOutItsBakeWindow(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + opt := t.TempDir() |
| + reg := testRegistry(t) |
| + shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin")) |
| + if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := SetProduction(ctx, s, priv, "bab", "myapp", true, "test"); err != nil { |
| + t.Fatal(err) |
| + } |
| + addSidecar(t, reg, "bot.example.com", "myapp", "stable", false) |
| + if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil { |
| + t.Fatal(err) |
| + } |
| + if v, _ := LiveVersion(opt, "bot.example.com"); v != "" { |
| + t.Fatalf("sidecar live = %q, want unmoved inside stable's bake window", v) |
| + } |
| +} |
| + |
| +// Spec §7.9: cold materialization of a new sidecar uses the same leg. |
| +func TestMaterializeHostMaterializesANewSidecar(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + opt := t.TempDir() |
| + reg := testRegistry(t) |
| + shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin")) |
| + if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil { |
| + t.Fatal(err) |
| + } |
| + addSidecar(t, reg, "bot.example.com", "myapp", "canary/x", true) |
| + if err := MaterializeHost(ctx, reg, s, pub, opt, t.TempDir(), "bot.example.com"); err != nil { |
| + t.Fatalf("MaterializeHost: %v", err) |
| + } |
| + if _, err := os.Stat(LiveLink(opt, "bot.example.com")); err != nil { |
| + t.Fatalf("live link does not resolve: %v", err) |
| + } |
| +} |
| + |
| +// Unchanged behaviour: what happens to instances does not depend on whether |
| +// the box also carries a sidecar. |
| +func TestInstanceOutcomesDoNotDependOnTheSidecarLeg(t *testing.T) { |
| + type outcome struct { |
| + restarted []string |
| + live, row string |
| + } |
| + run := func(withSidecar bool) outcome { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + opt := t.TempDir() |
| + reg := testRegistry(t) |
| + shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin")) |
| + if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil { |
| + t.Fatal(err) |
| + } |
| + addRoute(t, reg, "web.example.com", "myapp", "canary/x", false) |
| + addRoute(t, reg, "nap.example.com", "myapp", "canary/x", true) |
| + if withSidecar { |
| + addSidecar(t, reg, "bot.example.com", "myapp", "canary/x", false) |
| + } |
| + var o outcome |
| + restart := func(h string) error { o.restarted = append(o.restarted, h); return nil } |
| + if err := Pass(ctx, reg, s, pub, opt, restart, RestartHibernating(restart)); err != nil { |
| + t.Fatal(err) |
| + } |
| + o.live, _ = LiveVersion(opt, "web.example.com") |
| + if rt, _ := reg.Get("nap.example.com"); rt != nil { |
| + o.row = rt.Version |
| + } |
| + return o |
| + } |
| + without, with := run(false), run(true) |
| + if !reflect.DeepEqual(without, with) { |
| + t.Fatalf("instance outcomes changed when a sidecar was present:\nwithout %+v\nwith %+v", without, with) |
| + } |
| +} |