| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "os" |
| 7 | "path/filepath" |
| 8 | "reflect" |
| 9 | "testing" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/activator" |
| 12 | "github.com/carlosframework/platform/internal/hostbox" |
| 13 | "github.com/carlosframework/platform/internal/hostclaim" |
| 14 | "github.com/carlosframework/platform/internal/provision" |
| 15 | "github.com/carlosframework/platform/internal/registry" |
| 16 | ) |
| 17 | |
| 18 | type isolationInputs struct { |
| 19 | recoveryInputs |
| 20 | roles []string |
| 21 | } |
| 22 | |
| 23 | func (in isolationInputs) Select(ctx context.Context, rt registry.Route, o hostclaim.Observed, policy activator.StartPolicy) (ClaimLaunchInputs, error) { |
| 24 | selected, err := in.recoveryInputs.Select(ctx, rt, o, policy) |
| 25 | selected.Roles = in.roles |
| 26 | return selected, err |
| 27 | } |
| 28 | |
| 29 | func TestClaimWakeRefusesEveryUnisolatedShapeBeforeAdmission(t *testing.T) { |
| 30 | for _, shape := range []string{"nested", "flat", "foreign", "systemd", "seed", "standalone-sidecar", "co-spawn", "no-roles"} { |
| 31 | t.Run(shape, func(t *testing.T) { |
| 32 | f := newRecoveryFixture(t) |
| 33 | c := f.controller("boot-1", "controller-1", 4101, "41") |
| 34 | // Install the concrete production preflight, not a fixture override. |
| 35 | if shape != "no-roles" { |
| 36 | c.wake.Roles = &ClaimRoleAssembly{Manager: c.wake.Manager, Journal: f.journal} |
| 37 | } |
| 38 | roles := []string{"main"} |
| 39 | route := registry.Route{Host: recoveryHost, Kind: "instance", Hibernate: true} |
| 40 | switch shape { |
| 41 | case "nested": |
| 42 | route.DBPath = provision.NestedDBPath(f.guard.DataDir, route.Host) |
| 43 | route.Socket = filepath.Join(provision.SocketDir, "instances", provision.InstanceID(route.Host), "app.sock") |
| 44 | case "flat": |
| 45 | route.DBPath = provision.DefaultDBPath(f.guard.DataDir, route.Host) |
| 46 | route.Socket = filepath.Join(provision.SocketDir, route.Host+".sock") |
| 47 | case "foreign": |
| 48 | route.DBPath, route.Socket = "/foreign/data.db", "/foreign/app.sock" |
| 49 | case "systemd": |
| 50 | route.Unit = "tenant.service" |
| 51 | case "seed": |
| 52 | roles = []string{"seed"} |
| 53 | case "standalone-sidecar": |
| 54 | route.Kind = "sidecar" |
| 55 | case "co-spawn": |
| 56 | roles = []string{"main", "sidecar"} |
| 57 | } |
| 58 | c.wake.Inputs = isolationInputs{recoveryInputs{f}, roles} |
| 59 | before := f.read() |
| 60 | guardBefore, err := os.ReadDir(filepath.Join(f.guard.DataDir, "hostguard")) |
| 61 | if err != nil { |
| 62 | t.Fatal(err) |
| 63 | } |
| 64 | err = c.runtime.Prerequisites(t.Context(), route, before, recoveryPolicy) |
| 65 | if !errors.Is(err, hostbox.ErrUnsupported) { |
| 66 | t.Fatalf("shape %s: %v", shape, err) |
| 67 | } |
| 68 | if after := f.read(); after.ETag != before.ETag || after.Claim.Holder.WakeRetry != nil { |
| 69 | t.Fatalf("refusal changed claim or spent retry: %+v", after) |
| 70 | } |
| 71 | guardAfter, err := os.ReadDir(filepath.Join(f.guard.DataDir, "hostguard")) |
| 72 | if err != nil || !reflect.DeepEqual(guardBefore, guardAfter) || len(c.wake.pending) != 0 || len(f.requests) != 0 { |
| 73 | t.Fatalf("refusal reached guard/stop/lease: err=%v pending=%d requests=%d", err, len(c.wake.pending), len(f.requests)) |
| 74 | } |
| 75 | if _, err := f.journal.Read(t.Context(), recoveryTuple(before, c.local)); err == nil { |
| 76 | t.Fatal("refusal journaled a launch") |
| 77 | } |
| 78 | }) |
| 79 | } |
| 80 | } |
| 81 | |