| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "os" |
| 7 | "path/filepath" |
| 8 | "testing" |
| 9 | "time" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/activator" |
| 12 | "github.com/carlosframework/platform/internal/domains" |
| 13 | "github.com/carlosframework/platform/internal/provision" |
| 14 | "github.com/carlosframework/platform/internal/registry" |
| 15 | "github.com/carlosframework/platform/internal/release" |
| 16 | ) |
| 17 | |
| 18 | func TestPlan14OrdinaryDispatchSourceGap(t *testing.T) { |
| 19 | t.Setenv("CARLOS_HOST_CLAIM", "") |
| 20 | store := release.NewDirStore(t.TempDir()) |
| 21 | account := domains.Sqid(2) |
| 22 | manifest, err := json.Marshal(map[string]any{"id": 2, "sqid": account, "name": "fixture", "created_at": time.Now()}) |
| 23 | if err != nil { |
| 24 | t.Fatal(err) |
| 25 | } |
| 26 | if _, err := store.Put(t.Context(), "accounts/"+account+"/account.json", manifest, ""); err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | if err := provision.PutManifest(t.Context(), store, account, "app", provision.Manifest{Schema: 1, App: "app", Domain: "example.com", Pool: "test"}); err != nil { |
| 30 | t.Fatal(err) |
| 31 | } |
| 32 | record := provision.Record{Schema: 1, Host: "app.example.com", Slug: "app", Account: account, App: "app", Region: "ie", Pool: "test", Channel: "stable", CreatedAt: time.Now()} |
| 33 | if err := provision.CreateRecord(t.Context(), store, record); err != nil { |
| 34 | t.Fatal(err) |
| 35 | } |
| 36 | _, before, err := store.Get(t.Context(), provision.RecordKey(account, "app", record.Host)) |
| 37 | if err != nil { |
| 38 | t.Fatal(err) |
| 39 | } |
| 40 | for _, nested := range []bool{false, true} { |
| 41 | t.Run(map[bool]string{false: "flat", true: "nested"}[nested], func(t *testing.T) { |
| 42 | reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) |
| 43 | if err != nil { |
| 44 | t.Fatal(err) |
| 45 | } |
| 46 | defer reg.Close() |
| 47 | cfg := provision.ReconcileConfig{Region: "ie", Pools: []string{"test"}, DataDir: t.TempDir()} |
| 48 | if nested { |
| 49 | cfg.Layout = func(context.Context, release.Store) (string, error) { return release.LayoutModeNested, nil } |
| 50 | } |
| 51 | if err := provision.Reconcile(t.Context(), store, reg, cfg); err != nil { |
| 52 | t.Fatal(err) |
| 53 | } |
| 54 | row, err := reg.Get(record.Host) |
| 55 | if err != nil || row == nil { |
| 56 | t.Fatalf("provisioned route: %+v %v", row, err) |
| 57 | } |
| 58 | wantSocket := provision.DefaultSocket(record.Host) |
| 59 | if nested { |
| 60 | wantSocket = provision.NestedSocket(record.Host) |
| 61 | } |
| 62 | if row.Unit != "" || row.Backing != "" || row.Socket != wantSocket { |
| 63 | t.Fatalf("unexpected ordinary route: %+v", row) |
| 64 | } |
| 65 | source := &proofBindingSource{store: store} |
| 66 | for _, forge := range []string{"honest", "unit", "backing", "both"} { |
| 67 | candidate := *row |
| 68 | if forge == "unit" || forge == "both" { |
| 69 | candidate.Unit = "console.service" |
| 70 | } |
| 71 | if forge == "backing" || forge == "both" { |
| 72 | candidate.Backing = registry.BackingUnit |
| 73 | } |
| 74 | need, err := source.needsRestart(t.Context(), candidate) |
| 75 | if err != nil || need { |
| 76 | t.Fatalf("%s ordinary record dispatched: need=%v err=%v", forge, need, err) |
| 77 | } |
| 78 | if err := reg.Delete(candidate.Host); err != nil { |
| 79 | t.Fatal(err) |
| 80 | } |
| 81 | if err := reg.Upsert(candidate); err != nil { |
| 82 | t.Fatal(err) |
| 83 | } |
| 84 | u := &plan14OrdinaryUnits{} |
| 85 | checks := rootRestartChecks{targetFile: os.Open, dispatch: source.needsRestart} |
| 86 | if err := rootRestartRouteWith(reg, u, "", nil, nil, checks)(record.Host); err != nil { |
| 87 | t.Fatal(err) |
| 88 | } |
| 89 | if candidate.Unit == "" { |
| 90 | if len(u.effects) != 1 || u.effects[0] != activator.UnitName(record.Host) { |
| 91 | t.Fatalf("ordinary restart: %v", u.effects) |
| 92 | } |
| 93 | } else if len(u.effects) != 0 { |
| 94 | t.Fatalf("forged named restart: %v", u.effects) |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | }) |
| 99 | } |
| 100 | _, after, err := store.Get(t.Context(), provision.RecordKey(account, "app", record.Host)) |
| 101 | if err != nil || before != after { |
| 102 | t.Fatalf("record changed between local layouts: %v", err) |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | func TestPlan14NamedRecordDispatch(t *testing.T) { |
| 107 | t.Parallel() |
| 108 | e, rt, u, _, _ := plan14AdoptionFixture(t) |
| 109 | source := productionProofBindingSource(e.store, u, u.opt) |
| 110 | need, err := source.needsRestart(t.Context(), rt) |
| 111 | if err != nil || !need { |
| 112 | t.Fatalf("named record excluded: need=%v err=%v", need, err) |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | type plan14OrdinaryUnits struct{ plan14RestartUnits } |
| 117 | |
| 118 | func (u *plan14OrdinaryUnits) TryRestart(unit string) error { |
| 119 | u.effects = append(u.effects, unit) |
| 120 | return nil |
| 121 | } |
| 122 | |