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) } }