| 1 | package release |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "os" |
| 7 | "reflect" |
| 8 | "testing" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/registry" |
| 11 | ) |
| 12 | |
| 13 | func addSidecar(t *testing.T, reg *registry.Registry, host, app, channel string, hibernate bool) { |
| 14 | t.Helper() |
| 15 | if err := reg.Upsert(registry.Route{ |
| 16 | Host: host, Kind: "sidecar", App: app, Channel: channel, Hibernate: hibernate, |
| 17 | Args: `["sidecar","run"]`, Status: "ready", |
| 18 | }); err != nil { |
| 19 | t.Fatalf("upsert %s: %v", host, err) |
| 20 | } |
| 21 | } |
| 22 | |
| 23 | func TestSidecarAdoptableIsASecondPredicateNotAWiderOne(t *testing.T) { |
| 24 | base := registry.Route{Host: "bot.example.com", Kind: "sidecar", App: "myapp", Status: "ready"} |
| 25 | for _, c := range []struct { |
| 26 | name string |
| 27 | mut func(*registry.Route) |
| 28 | sidecar, instance bool |
| 29 | }{ |
| 30 | {"ready sidecar", func(*registry.Route) {}, true, false}, |
| 31 | {"status empty", func(r *registry.Route) { r.Status = "" }, true, false}, |
| 32 | {"no app", func(r *registry.Route) { r.App = "" }, false, false}, |
| 33 | {"platform", func(r *registry.Route) { r.Platform = true }, false, false}, |
| 34 | {"creating", func(r *registry.Route) { r.Status = "creating" }, false, false}, |
| 35 | {"an instance", func(r *registry.Route) { r.Kind = "instance" }, false, true}, |
| 36 | {"static", func(r *registry.Route) { r.Kind = "static" }, false, false}, |
| 37 | } { |
| 38 | rt := base |
| 39 | c.mut(&rt) |
| 40 | if got := SidecarAdoptable(rt); got != c.sidecar { |
| 41 | t.Errorf("%s: SidecarAdoptable = %v, want %v", c.name, got, c.sidecar) |
| 42 | } |
| 43 | if got := Adoptable(rt); got != c.instance { |
| 44 | t.Errorf("%s: Adoptable = %v, want %v (Adoptable is not widened)", c.name, got, c.instance) |
| 45 | } |
| 46 | } |
| 47 | unit := base |
| 48 | unit.Backing = registry.BackingUnit |
| 49 | if CapabilityRoute(unit) { |
| 50 | t.Error("CapabilityRoute admitted a sidecar: no capability drop-ins are ever written for one") |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | // Spec §7.9: sidecars go through the same selection, staging, repoint and |
| 55 | // stamp, and the pass never restarts them; RestartHibernating changes nothing |
| 56 | // for them. |
| 57 | func TestPassAdoptsASidecarWithoutRestartingIt(t *testing.T) { |
| 58 | s := NewDirStore(t.TempDir()) |
| 59 | ctx := context.Background() |
| 60 | priv, pub := testKeys(t) |
| 61 | opt := t.TempDir() |
| 62 | reg := testRegistry(t) |
| 63 | shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin")) |
| 64 | if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil { |
| 65 | t.Fatal(err) |
| 66 | } |
| 67 | addRoute(t, reg, "web.example.com", "myapp", "canary/x", false) |
| 68 | addSidecar(t, reg, "bot.example.com", "myapp", "canary/x", false) |
| 69 | addSidecar(t, reg, "nap.example.com", "myapp", "canary/x", true) |
| 70 | var restarted []string |
| 71 | restart := func(h string) error { restarted = append(restarted, h); return nil } |
| 72 | if err := Pass(ctx, reg, s, pub, opt, restart, RestartHibernating(restart)); err != nil { |
| 73 | t.Fatalf("Pass: %v", err) |
| 74 | } |
| 75 | for _, h := range []string{"web.example.com", "bot.example.com", "nap.example.com"} { |
| 76 | if v, _ := LiveVersion(opt, h); v != "v1" { |
| 77 | t.Errorf("%s live = %q, want v1", h, v) |
| 78 | } |
| 79 | if rt, _ := reg.Get(h); rt == nil || rt.Version != "v1" { |
| 80 | t.Errorf("%s row = %+v, want Version v1 (root's stamp)", h, rt) |
| 81 | } |
| 82 | } |
| 83 | if !reflect.DeepEqual(restarted, []string{"web.example.com"}) { |
| 84 | t.Fatalf("restarted = %v; the pass must never restart a sidecar", restarted) |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | // Sidecars go after every instance, so a build an instance poisons this pass |
| 89 | // never reaches a sidecar. |
| 90 | func TestSidecarIsNotMovedOntoABuildAnInstancePoisoned(t *testing.T) { |
| 91 | s := NewDirStore(t.TempDir()) |
| 92 | ctx := context.Background() |
| 93 | priv, pub := testKeys(t) |
| 94 | opt := t.TempDir() |
| 95 | reg := testRegistry(t) |
| 96 | shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("good")) |
| 97 | shipRelease(t, s, "bab", "myapp", "v2", adoptPlatform, []byte("bad")) |
| 98 | if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil { |
| 99 | t.Fatal(err) |
| 100 | } |
| 101 | addRoute(t, reg, "web.example.com", "myapp", "canary/x", false) |
| 102 | addSidecar(t, reg, "bot.example.com", "myapp", "canary/x", false) |
| 103 | if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil { |
| 104 | t.Fatal(err) |
| 105 | } |
| 106 | if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v2"); err != nil { |
| 107 | t.Fatal(err) |
| 108 | } |
| 109 | restart := func(h string) error { |
| 110 | if h == "web.example.com" { |
| 111 | return errors.New("did not come up") |
| 112 | } |
| 113 | return nil |
| 114 | } |
| 115 | if err := Pass(ctx, reg, s, pub, opt, restart); err != nil { |
| 116 | t.Fatal(err) |
| 117 | } |
| 118 | if v, _ := LiveVersion(opt, "bot.example.com"); v != "v1" { |
| 119 | t.Fatalf("sidecar live = %q, want v1: a build that failed an instance this pass must not reach it", v) |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | // Spec §7.9: sidecars take the same bake holds. |
| 124 | func TestSidecarWaitsOutItsBakeWindow(t *testing.T) { |
| 125 | s := NewDirStore(t.TempDir()) |
| 126 | ctx := context.Background() |
| 127 | priv, pub := testKeys(t) |
| 128 | opt := t.TempDir() |
| 129 | reg := testRegistry(t) |
| 130 | shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin")) |
| 131 | if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil { |
| 132 | t.Fatal(err) |
| 133 | } |
| 134 | if err := SetProduction(ctx, s, priv, "bab", "myapp", true, "test"); err != nil { |
| 135 | t.Fatal(err) |
| 136 | } |
| 137 | addSidecar(t, reg, "bot.example.com", "myapp", "stable", false) |
| 138 | if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil { |
| 139 | t.Fatal(err) |
| 140 | } |
| 141 | if v, _ := LiveVersion(opt, "bot.example.com"); v != "" { |
| 142 | t.Fatalf("sidecar live = %q, want unmoved inside stable's bake window", v) |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | // Spec §7.9: cold materialization of a new sidecar uses the same leg. |
| 147 | func TestMaterializeHostMaterializesANewSidecar(t *testing.T) { |
| 148 | s := NewDirStore(t.TempDir()) |
| 149 | ctx := context.Background() |
| 150 | priv, pub := testKeys(t) |
| 151 | opt := t.TempDir() |
| 152 | reg := testRegistry(t) |
| 153 | shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin")) |
| 154 | if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil { |
| 155 | t.Fatal(err) |
| 156 | } |
| 157 | addSidecar(t, reg, "bot.example.com", "myapp", "canary/x", true) |
| 158 | if err := MaterializeHost(ctx, reg, s, pub, opt, t.TempDir(), "bot.example.com"); err != nil { |
| 159 | t.Fatalf("MaterializeHost: %v", err) |
| 160 | } |
| 161 | if _, err := os.Stat(LiveLink(opt, "bot.example.com")); err != nil { |
| 162 | t.Fatalf("live link does not resolve: %v", err) |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | // Unchanged behaviour: what happens to instances does not depend on whether |
| 167 | // the box also carries a sidecar. |
| 168 | func TestInstanceOutcomesDoNotDependOnTheSidecarLeg(t *testing.T) { |
| 169 | type outcome struct { |
| 170 | restarted []string |
| 171 | live, row string |
| 172 | } |
| 173 | run := func(withSidecar bool) outcome { |
| 174 | s := NewDirStore(t.TempDir()) |
| 175 | ctx := context.Background() |
| 176 | priv, pub := testKeys(t) |
| 177 | opt := t.TempDir() |
| 178 | reg := testRegistry(t) |
| 179 | shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin")) |
| 180 | if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil { |
| 181 | t.Fatal(err) |
| 182 | } |
| 183 | addRoute(t, reg, "web.example.com", "myapp", "canary/x", false) |
| 184 | addRoute(t, reg, "nap.example.com", "myapp", "canary/x", true) |
| 185 | if withSidecar { |
| 186 | addSidecar(t, reg, "bot.example.com", "myapp", "canary/x", false) |
| 187 | } |
| 188 | var o outcome |
| 189 | restart := func(h string) error { o.restarted = append(o.restarted, h); return nil } |
| 190 | if err := Pass(ctx, reg, s, pub, opt, restart, RestartHibernating(restart)); err != nil { |
| 191 | t.Fatal(err) |
| 192 | } |
| 193 | o.live, _ = LiveVersion(opt, "web.example.com") |
| 194 | if rt, _ := reg.Get("nap.example.com"); rt != nil { |
| 195 | o.row = rt.Version |
| 196 | } |
| 197 | return o |
| 198 | } |
| 199 | without, with := run(false), run(true) |
| 200 | if !reflect.DeepEqual(without, with) { |
| 201 | t.Fatalf("instance outcomes changed when a sidecar was present:\nwithout %+v\nwith %+v", without, with) |
| 202 | } |
| 203 | } |
| 204 | |