| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "github.com/carlosframework/platform/internal/provision" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "testing" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/hostwrite" |
| 13 | "github.com/carlosframework/platform/internal/registry" |
| 14 | "github.com/carlosframework/platform/internal/release" |
| 15 | "github.com/carlosframework/platform/internal/rootstate" |
| 16 | ) |
| 17 | |
| 18 | type activationPreflightOwner struct{ calls int } |
| 19 | |
| 20 | func (p *activationPreflightOwner) OpenClaimOwners(context.Context, release.Store, ClaimRuntimeResources) (ClaimOwnerBindings, error) { |
| 21 | p.calls++ |
| 22 | return ClaimOwnerBindings{}, os.ErrNotExist |
| 23 | } |
| 24 | |
| 25 | func TestPlan14ActivationNamedUnitPreflight(t *testing.T) { |
| 26 | oldOpen, oldShapes := openClaimActivationState, productionClaimUnitShapes |
| 27 | t.Cleanup(func() { openClaimActivationState, productionClaimUnitShapes = oldOpen, oldShapes }) |
| 28 | openClaimActivationState = func(string, bool, bool) (rootstate.Store, error) { return nil, os.ErrNotExist } |
| 29 | productionClaimUnitShapes = func() (map[string]claimUnitShape, error) { return map[string]claimUnitShape{}, nil } |
| 30 | |
| 31 | for _, mode := range []string{"agent", "adopt", "instance-control"} { |
| 32 | for _, kind := range []string{"named", "backing", "ordinary", "empty"} { |
| 33 | t.Run(mode+"/"+kind, func(t *testing.T) { |
| 34 | dir := t.TempDir() |
| 35 | reg, err := registry.Open(filepath.Join(dir, "registry.db")) |
| 36 | if err != nil { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | defer reg.Close() |
| 40 | row := registry.Route{Host: "console.example", Account: "bab", App: "console", Socket: "/run/console.sock"} |
| 41 | if kind == "named" { |
| 42 | row.Unit = "console.service" |
| 43 | } |
| 44 | if kind == "backing" { |
| 45 | row.Backing = "unit" |
| 46 | } |
| 47 | if kind != "empty" { |
| 48 | if err := reg.Upsert(row); err != nil { |
| 49 | t.Fatal(err) |
| 50 | } |
| 51 | } |
| 52 | p := &activationPreflightOwner{} |
| 53 | r := ClaimRuntimeResources{Mode: mode, Registry: reg, DataDir: filepath.Join(dir, "data"), OptDir: filepath.Join(dir, "opt"), UnitsDir: filepath.Join(dir, "units")} |
| 54 | _, err = OpenProductionClaimAssembly(t.Context(), release.NewDirStore(filepath.Join(dir, "bucket")), r, p) |
| 55 | blocked := !claimNamedUnit4aComplete && (kind == "named" || kind == "backing") |
| 56 | if blocked { |
| 57 | if !errors.Is(err, hostwrite.ErrUnavailable) || p.calls != 0 { |
| 58 | t.Fatalf("preflight=%v provider calls=%d", err, p.calls) |
| 59 | } |
| 60 | } else if !errors.Is(err, os.ErrNotExist) || p.calls != 1 { |
| 61 | t.Fatalf("ordinary construction=%v calls=%d", err, p.calls) |
| 62 | } |
| 63 | if mode == "instance-control" { |
| 64 | p.calls = 0 |
| 65 | err = migrateClaimJournalsWith(t.Context(), release.NewDirStore(filepath.Join(dir, "bucket")), r, p, true) |
| 66 | if blocked && (!errors.Is(err, hostwrite.ErrUnavailable) || p.calls != 0) { |
| 67 | t.Fatalf("migration preflight: %v calls=%d", err, p.calls) |
| 68 | } |
| 69 | if !blocked && (!errors.Is(err, os.ErrNotExist) || p.calls != 1) { |
| 70 | t.Fatalf("migration ordinary: %v calls=%d", err, p.calls) |
| 71 | } |
| 72 | } |
| 73 | for _, path := range []string{r.DataDir, r.OptDir, r.UnitsDir} { |
| 74 | if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) { |
| 75 | t.Fatalf("claim state created: %s %v", path, err) |
| 76 | } |
| 77 | } |
| 78 | }) |
| 79 | } |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | func TestPlan14ActivationRejectsHiddenNamedRecords(t *testing.T) { |
| 84 | for _, attack := range []string{"omitted", "cleared"} { |
| 85 | t.Run(attack, func(t *testing.T) { |
| 86 | t.Setenv("CARLOS_REGION", "") |
| 87 | store := release.NewDirStore(t.TempDir()) |
| 88 | plan14PutRecord(t, store, provision.Record{Schema: 1, Host: "console.example", Account: "bab", App: "console", Channel: "stable", Unit: "console.service", CreatedAt: time.Now()}) |
| 89 | reg := restartTestRegistry(t) |
| 90 | if attack == "cleared" { |
| 91 | if err := reg.Upsert(registry.Route{Host: "console.example", Account: "bab", App: "console", Socket: "/run/console.sock"}); err != nil { |
| 92 | t.Fatal(err) |
| 93 | } |
| 94 | } |
| 95 | for _, mode := range []string{"agent", "instance-control"} { |
| 96 | p := &activationPreflightOwner{} |
| 97 | dir := t.TempDir() |
| 98 | r := ClaimRuntimeResources{Mode: mode, Registry: reg, DataDir: dir, OptDir: dir, UnitsDir: dir} |
| 99 | if _, err := OpenProductionClaimAssembly(t.Context(), store, r, p); !errors.Is(err, hostwrite.ErrUnavailable) || p.calls != 0 { |
| 100 | t.Fatal("hidden named route entered construction", err, p.calls) |
| 101 | } |
| 102 | r.Mode = "instance-control" |
| 103 | if err := migrateClaimJournalsWith(t.Context(), store, r, p, true); !errors.Is(err, hostwrite.ErrUnavailable) || p.calls != 0 { |
| 104 | t.Fatal("hidden named route entered migration", err, p.calls) |
| 105 | } |
| 106 | } |
| 107 | }) |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | func TestPlan14ActivationRejectsHiddenLocalUnits(t *testing.T) { |
| 112 | for _, unit := range []string{"carlos-app@shop.example.service", "console.service", "woodstar.service.d/override.conf"} { |
| 113 | for _, attack := range []string{"omitted", "cleared"} { |
| 114 | t.Run(unit+"/"+attack, func(t *testing.T) { |
| 115 | reg := restartTestRegistry(t) |
| 116 | if attack == "cleared" { |
| 117 | if err := reg.Upsert(registry.Route{Host: "shop.example", Account: "bab", App: "shop", Socket: "/run/shop.sock"}); err != nil { |
| 118 | t.Fatal(err) |
| 119 | } |
| 120 | } |
| 121 | dir := t.TempDir() |
| 122 | units := filepath.Join(dir, "units") |
| 123 | path := filepath.Join(units, unit) |
| 124 | if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { |
| 125 | t.Fatal(err) |
| 126 | } |
| 127 | if err := os.WriteFile(path, []byte("[Service]\nExecStart=/opt/carlos/live/shop.example serve\n"), 0644); err != nil { |
| 128 | t.Fatal(err) |
| 129 | } |
| 130 | store := release.NewDirStore(filepath.Join(dir, "bucket")) |
| 131 | for _, mode := range []string{"agent", "instance-control"} { |
| 132 | p := &activationPreflightOwner{} |
| 133 | r := ClaimRuntimeResources{Mode: mode, Registry: reg, DataDir: filepath.Join(dir, "data"), OptDir: filepath.Join(dir, "opt"), UnitsDir: units} |
| 134 | if _, err := OpenProductionClaimAssembly(t.Context(), store, r, p); !errors.Is(err, hostwrite.ErrUnavailable) || p.calls != 0 { |
| 135 | t.Fatal(err, p.calls) |
| 136 | } |
| 137 | r.Mode = "instance-control" |
| 138 | if err := migrateClaimJournalsWith(t.Context(), store, r, p, true); !errors.Is(err, hostwrite.ErrUnavailable) || p.calls != 0 { |
| 139 | t.Fatal(err, p.calls) |
| 140 | } |
| 141 | for _, path := range []string{r.DataDir, r.OptDir} { |
| 142 | if _, err := os.Stat(path); !os.IsNotExist(err) { |
| 143 | t.Fatal("created state", path, err) |
| 144 | } |
| 145 | } |
| 146 | } |
| 147 | }) |
| 148 | } |
| 149 | } |
| 150 | } |
| 151 | |