| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "net/http" |
| 6 | "net/http/httptest" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "testing" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/activator" |
| 13 | "github.com/carlosframework/platform/internal/registry" |
| 14 | "github.com/carlosframework/platform/internal/release" |
| 15 | "github.com/carlosframework/platform/internal/rollout" |
| 16 | ) |
| 17 | |
| 18 | // liveIdentityBackend is liveLaunchBackend with the exec backend's identity |
| 19 | // capability exposed, as production's ExecBackend and SplitBackend expose |
| 20 | // it: the activator's cold wake then captures live/<host> once at step 2b |
| 21 | // (LiveIdentity) and runs that file by path (StartGenExe, SeedExe), never |
| 22 | // the backend's live-link Start. before runs inside LiveIdentity, before |
| 23 | // the capture: root's repoint after the plan. |
| 24 | type liveIdentityBackend struct { |
| 25 | liveLaunchBackend |
| 26 | } |
| 27 | |
| 28 | func (b *liveIdentityBackend) LiveIdentity(ctx context.Context, rt registry.Route) (release.StagedIdentity, error) { |
| 29 | if b.before != nil { |
| 30 | b.before() |
| 31 | } |
| 32 | return b.eb.LiveIdentity(ctx, rt) |
| 33 | } |
| 34 | |
| 35 | func (b *liveIdentityBackend) StartGenExe(ctx context.Context, rt registry.Route, n uint64, perGen bool, id release.StagedIdentity) (activator.Proc, activator.Gen, error) { |
| 36 | return b.eb.StartGenExe(ctx, rt, n, perGen, id) |
| 37 | } |
| 38 | |
| 39 | func (b *liveIdentityBackend) SeedExe(ctx context.Context, rt registry.Route, id release.StagedIdentity) error { |
| 40 | return b.eb.SeedExe(ctx, rt, id) |
| 41 | } |
| 42 | |
| 43 | // TestDeclinedBoxRefusesAHeldBuildAtLaunch on the path production takes on |
| 44 | // this branch (merge of zdd-flag-readiness): the wake plans with the link on |
| 45 | // v1, root repoints it at held v2 before the capture, and the capture is |
| 46 | // asked of the declined box's gate before it is recorded or run. Nothing of |
| 47 | // v2 is exec'd and no build state names it. |
| 48 | func TestDeclinedBoxRefusesAHeldCapturedIdentityAtLaunch(t *testing.T) { |
| 49 | prev := activator.GenerationsEnabled() |
| 50 | activator.SetGenerations(false) |
| 51 | t.Cleanup(func() { activator.SetGenerations(prev) }) |
| 52 | resetRollout(t) |
| 53 | env := newFastFixture(t) |
| 54 | t.Setenv("CARLOS_OPT_DIR", env.optDir) |
| 55 | host := "warm.test" |
| 56 | run := t.TempDir() |
| 57 | rt := registry.Route{Host: host, Kind: "instance", App: "fastapp", Account: "bab", Channel: "edge", |
| 58 | Status: "ready", Hibernate: true, Version: "v1", GoodVersion: "v1", GoodDigest: "sha256:v1", |
| 59 | Socket: filepath.Join(run, host+".sock"), DBPath: filepath.Join(env.dataDir, host+".db")} |
| 60 | if err := env.reg.Upsert(rt); err != nil { |
| 61 | t.Fatal(err) |
| 62 | } |
| 63 | env.stageLive(t, host, "v1") |
| 64 | mark := filepath.Join(t.TempDir(), "v2.ran") |
| 65 | if err := release.StageArtifact(env.optDir, "bab", "v2", "fastapp", []byte("#!/bin/sh\necho ran > '"+mark+"'\nexec sleep 30\n")); err != nil { |
| 66 | t.Fatal(err) |
| 67 | } |
| 68 | writeFailureBook(t, env.dataDir, []rollout.FailureRecord{{Account: release.RouteAccount(rt), App: "fastapp", Build: "v2", Host: "sibling.test"}}, nil) |
| 69 | eb := &activator.ExecBackend{Cfg: activator.Config{Bucket: "test-bucket", Idle: time.Hour}, |
| 70 | OptDir: env.optDir, DataDir: t.TempDir(), RunDirOverride: run} |
| 71 | backend := &liveIdentityBackend{liveLaunchBackend{eb: eb}} |
| 72 | svc := activator.New(activator.Config{Bucket: "test-bucket", Idle: time.Hour}, backend, env.reg) |
| 73 | svc.Routes = env.reg |
| 74 | var recorded []string |
| 75 | svc.BeforeExec = func(_ context.Context, _ string, build string) error { |
| 76 | recorded = append(recorded, build) |
| 77 | return nil |
| 78 | } |
| 79 | t.Cleanup(func() { |
| 80 | sctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) |
| 81 | defer cancel() |
| 82 | svc.Shutdown(sctx) |
| 83 | }) |
| 84 | declineWithBook(t, svc, env.reg, env.dataDir) |
| 85 | if eb.LiveExecGate == nil { |
| 86 | t.Fatal("no LiveExecGate: the box did not decline with its book") |
| 87 | } |
| 88 | if p := svc.Planner(context.Background(), rt); !isTodaysWake(p) { |
| 89 | t.Fatalf("plan with the link on v1 = %+v, want today's wake", p) |
| 90 | } |
| 91 | backend.before = func() { |
| 92 | if err := release.PointLive(env.optDir, host, "bab", "v2", "fastapp"); err != nil { |
| 93 | t.Error(err) |
| 94 | } |
| 95 | } |
| 96 | rec := httptest.NewRecorder() |
| 97 | proxy := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) }) |
| 98 | if !svc.ServeHTTP(rec, httptest.NewRequest("GET", "http://"+host+"/", nil), rt, proxy) { |
| 99 | t.Fatal("the activator did not take the request") |
| 100 | } |
| 101 | if rec.Code == http.StatusNoContent { |
| 102 | t.Fatal("the wake onto held v2 was served") |
| 103 | } |
| 104 | time.Sleep(200 * time.Millisecond) |
| 105 | if _, err := os.Stat(mark); err == nil { |
| 106 | t.Fatal("held v2 was exec'd by path: the declined gate never saw the captured identity") |
| 107 | } |
| 108 | if len(recorded) != 0 { |
| 109 | t.Fatalf("build states recorded %v: a build the box will not start was named as about to run", recorded) |
| 110 | } |
| 111 | } |
| 112 | |