package hostbox import ( "context" "errors" "testing" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostwrite" ) func plan11Relocating(phase string) *hostclaim.Claim { exec := "target" switch phase { case "starting", "releasing": exec = "source" case "pinning": exec = "none" } return &hostclaim.Claim{Holder: &hostclaim.Holder{Session: 7, WriterBox: "target"}, Op: hostclaim.Op{Kind: hostclaim.KindRelocating, ID: "r1", Phase: phase, Exec: exec, Participants: []string{"source", "target"}, Intent: map[string]string{"region": "uk", "source_box": "source", "target_box": "target"}}} } // WakePhase admits a relocation's probe at probing on op.exec only. No // ordinary relocating wake is admitted at any phase: the source's ordinary // wake in starting and releasing is not part of the accepted R1. func TestPlan11WakePhaseAdmitsOnlyTheRelocationProbe(t *testing.T) { for _, tc := range []struct { phase, box string probe, want bool }{ {"probing", "target", true, true}, {"probing", "target", false, false}, {"probing", "source", true, false}, {"starting", "source", false, false}, {"releasing", "source", false, false}, {"starting", "source", true, false}, {"installed", "target", true, false}, {"installing", "target", true, false}, {"probe-failed", "target", true, false}, {"serving", "target", false, false}, {"serving", "target", true, false}, } { if got := WakePhase(plan11Relocating(tc.phase), tc.box, tc.probe) == nil; got != tc.want { t.Errorf("%+v: admitted=%v", tc, got) } } c := plan11Relocating("probing") c.Lost = []string{"target"} if WakePhase(c, "target", true) == nil { t.Fatal("a lost target probes") } c = plan11Relocating("probing") c.Op.Journal = map[string]string{"stuck": "deadline"} if WakePhase(c, "target", true) == nil { t.Fatal("a stuck relocation probes") } } type plan11Owner struct{ calls []string } func (o *plan11Owner) check(name string, err error) func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error { return func(_ context.Context, _ *hostclaim.Claim, s Selection, _ hostwrite.Identity) error { o.calls = append(o.calls, name+":"+s.Action) return err } } func plan11Policy(o *plan11Owner, hold string) Policy { return Policy{Held: func(context.Context, string) (string, error) { return hold, nil }, Moves: o.check("moves", nil), Relocation: o.check("relocation", nil)} } // Policy.Check under relocating: the claim's own gate runs first and only // the "probe" action of op.exec at probing (or its session carried into // serving) reaches the typed relocation owner. Every other action, a stale // op, a foreign hold, and a nil owner refuse without consulting it. The // moves owner never sees a relocating claim. func TestPlan11PolicyGivesProbeAuthorityOnlyToTheRelocationProbe(t *testing.T) { ctx := context.Background() o := &plan11Owner{} p := plan11Policy(o, "") probe := Selection{Host: "x.example", Box: "target", Op: "r1", Action: "probe", Phase: "probing", Session: 7} if err := p.Check(ctx, plan11Relocating("probing"), probe, hostwrite.Identity{}); err != nil { t.Fatal(err) } if len(o.calls) != 1 || o.calls[0] != "relocation:probe" { t.Fatalf("owner calls %v", o.calls) } // The admitted probe session carried into relocating/serving. o.calls = nil if err := p.Check(ctx, plan11Relocating("serving"), probe, hostwrite.Identity{}); err != nil { t.Fatalf("carried session: %v", err) } // A probe hold by this op is the op's own; another op's hold refuses. if err := plan11Policy(o, "r1").Check(ctx, plan11Relocating("probing"), probe, hostwrite.Identity{}); err != nil { t.Fatalf("own hold: %v", err) } o.calls = nil refused := map[string]struct { p Policy c *hostclaim.Claim s Selection }{ "spawn": {plan11Policy(o, ""), plan11Relocating("probing"), func() Selection { s := probe; s.Action = "spawn"; return s }()}, "seed": {plan11Policy(o, ""), plan11Relocating("probing"), func() Selection { s := probe; s.Action = "seed"; return s }()}, "held-config": {plan11Policy(o, "r1"), plan11Relocating("probing"), func() Selection { s := probe; s.Action = "held-config"; return s }()}, "install": {plan11Policy(o, ""), plan11Relocating("installing"), func() Selection { s := probe; s.Action = "install"; return s }()}, "reconcile": {plan11Policy(o, ""), plan11Relocating("probing"), func() Selection { s := probe; s.Action = "reconcile"; return s }()}, "probe installed": {plan11Policy(o, ""), plan11Relocating("installed"), probe}, "probe probe-failed": {plan11Policy(o, ""), plan11Relocating("probe-failed"), probe}, "source probe": {plan11Policy(o, ""), plan11Relocating("starting"), func() Selection { s := probe; s.Box = "source"; return s }()}, "source spawn": {plan11Policy(o, ""), plan11Relocating("releasing"), func() Selection { s := probe; s.Box, s.Action = "source", "spawn"; return s }()}, "stale op": {plan11Policy(o, ""), plan11Relocating("probing"), func() Selection { s := probe; s.Op = "r0"; return s }()}, "foreign hold": {plan11Policy(o, "m9"), plan11Relocating("probing"), probe}, "serving, selected at serving": {plan11Policy(o, ""), plan11Relocating("serving"), func() Selection { s := probe; s.Phase = "serving"; return s }()}, "serving, other writer": {plan11Policy(o, ""), func() *hostclaim.Claim { c := plan11Relocating("serving"); c.Holder.WriterBox = "source"; return c }(), probe}, "no owner": {Policy{Held: func(context.Context, string) (string, error) { return "", nil }, Moves: o.check("moves", nil)}, plan11Relocating("probing"), probe}, } for name, tc := range refused { if err := tc.p.Check(ctx, tc.c, tc.s, hostwrite.Identity{}); err == nil { t.Errorf("%s: admitted", name) } } if len(o.calls) != 0 { t.Fatalf("owners consulted on refused selections: %v", o.calls) } // The owner can refuse what the claim's gate allows. deny := errors.New("region HA refuses") p.Relocation = o.check("relocation", deny) if err := p.Check(ctx, plan11Relocating("probing"), probe, hostwrite.Identity{}); !errors.Is(err, deny) { t.Fatalf("owner refusal: %v", err) } // "probe" is never an action of any other kind. landing := &hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{Kind: hostclaim.KindLanding, ID: "m1", Phase: "probing", Exec: "target"}} o.calls = nil if err := plan11Policy(o, "m1").Check(ctx, landing, Selection{Host: "x.example", Box: "target", Op: "m1", Action: "probe"}, hostwrite.Identity{}); err == nil || len(o.calls) != 0 { t.Fatalf("landing probe action: %v %v", err, o.calls) } } // R8 as accepted: an append-only policy check. Refusals run only after the // whole base check passed, so they can add a refusal and never remove one; // WithRefusals copies, so an earlier Policy value is unchanged. func TestPlan11PolicyRefusalsAreAppendOnly(t *testing.T) { ctx := context.Background() serving := &hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box"}} s := Selection{Host: "x.example", Box: "box", Action: "spawn"} base := Policy{Held: func(context.Context, string) (string, error) { return "", nil }} if err := base.Check(ctx, serving, s, hostwrite.Identity{}); err != nil { t.Fatal(err) } var seen []string deny := errors.New("refused by region HA") pass := func(name string) PolicyRefusal { return func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error { seen = append(seen, name) return nil } } refuse := func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error { seen = append(seen, "refuse") return deny } one := base.WithRefusals(pass("a")) two := one.WithRefusals(refuse) if err := one.Check(ctx, serving, s, hostwrite.Identity{}); err != nil || len(seen) != 1 { t.Fatalf("passing refusal: %v %v", err, seen) } seen = nil if err := two.Check(ctx, serving, s, hostwrite.Identity{}); !errors.Is(err, deny) || len(seen) != 2 || seen[0] != "a" { t.Fatalf("added refusal: %v %v", err, seen) } seen = nil if err := one.Check(ctx, serving, s, hostwrite.Identity{}); err != nil || len(seen) != 1 { t.Fatalf("WithRefusals changed an earlier policy: %v %v", err, seen) } if err := base.Check(ctx, serving, s, hostwrite.Identity{}); err != nil { t.Fatalf("base changed: %v", err) } // A refused base never reaches the refusals. seen = nil foreign := s foreign.Box = "other" if err := two.Check(ctx, serving, foreign, hostwrite.Identity{}); !errors.Is(err, ErrChanged) || len(seen) != 0 { t.Fatalf("refusals consulted after a base refusal: %v %v", err, seen) } // A nil refusal fails closed. if err := base.WithRefusals(nil).Check(ctx, serving, s, hostwrite.Identity{}); !errors.Is(err, ErrUnsupported) { t.Fatalf("nil refusal: %v", err) } // Aliasing: appending to two copies of the same parent keeps each its own. x, y := one.WithRefusals(refuse), one.WithRefusals(pass("y")) seen = nil if err := y.Check(ctx, serving, s, hostwrite.Identity{}); err != nil || len(seen) != 2 || seen[1] != "y" { t.Fatalf("sibling copies share refusals: %v %v", err, seen) } if err := x.Check(ctx, serving, s, hostwrite.Identity{}); !errors.Is(err, deny) { t.Fatalf("sibling refusal lost: %v", err) } }