package host import ( "context" "encoding/base64" "errors" "fmt" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/genselect" "os" "os/exec" "path/filepath" "reflect" "strings" "testing" "time" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/moves" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" ) func TestPlan10G2ReadAccess(t *testing.T) { t.Parallel() f := newAdminTokenFixture(t, "box-1") ctx := t.Context() o, err := hostclaim.Read(ctx, f.claims, adminTokenHost) if err != nil { t.Fatal(err) } o, err = hostclaim.Update(ctx, f.claims, o, func(c *hostclaim.Claim) error { c.Op = hostclaim.Op{Kind: hostclaim.KindLanding, ID: "m1", Phase: hostclaim.PhaseProbing, Exec: "box-1", Participants: []string{"box-0", "box-1"}, Intent: map[string]string{"source": "box-0", "target": "box-1", "to": "ie"}} return nil }) if err != nil { t.Fatal(err) } if err = f.tokens.Guard.Activate(ctx, adminTokenHost, "box-1", nil); err != nil { t.Fatal(err) } captured, err := hostguard.Capture(f.data, adminTokenHost) if err != nil { t.Fatal(err) } reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } defer reg.Close() if err = reg.Upsert(f.rt); err != nil { t.Fatal(err) } reads, launches, prepared := 0, 0, 0 original := ClaimServingLaunch{Args: []string{"arg"}, Env: []string{"A=B"}} report := moves.EnvReport{Vars: map[string]string{"A": "hash"}} a := &claimMoveReadAccess{actor: f.actor, check: claimBoundIdentity(f.verifyActor, f.actor), route: func(host string) (*registry.Route, error) { reads++; return reg.Get(host) }, launch: func(context.Context, registry.Route) (ClaimServingLaunch, moves.EnvReport, error) { launches++ return original, report, nil }} checkAll := func(want error) { t.Helper() if _, err := a.Route(ctx, adminTokenHost); !errors.Is(err, want) { t.Fatalf("route: %v", err) } if _, _, err := a.Launch(ctx, adminTokenHost); !errors.Is(err, want) { t.Fatalf("launch: %v", err) } if _, err := a.HeldWork(ctx, adminTokenHost, "held-config", captured); !errors.Is(err, want) { t.Fatalf("work: %v", err) } } checkAll(hostwrite.ErrUnavailable) if reads != 0 || launches != 0 { t.Fatal("access before Build") } source := hostbox.WorkSource{Guard: f.tokens.Guard, Box: f.actor.Box, Region: f.actor.Region, Now: time.Now, Policy: func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error { return nil }, Local: func(context.Context, *hostclaim.Claim, hostbox.Selection) error { return nil }, Prepare: func(context.Context, hostclaim.Observed) error { prepared++; return nil }} f.verify = hostbox.ErrChanged if err = a.bind(ctx, source); !errors.Is(err, hostbox.ErrChanged) { t.Fatal("binding without verification", err) } checkAll(hostwrite.ErrUnavailable) f.verify = nil if err = a.bind(ctx, source); err != nil { t.Fatal(err) } if _, err = a.HeldWork(ctx, adminTokenHost, "held-config", captured); err != nil { t.Fatal(err) } if prepared != 0 { t.Fatal("read-only selection called Prepare") } for _, action := range []string{"spawn", "install", "held-timers", "delete"} { if _, err = a.HeldWork(ctx, adminTokenHost, action, captured); !errors.Is(err, hostbox.ErrUnsupported) { t.Fatalf("action %s: %v", action, err) } } rt, err := a.Route(ctx, adminTokenHost) if err != nil { t.Fatal(err) } rt.DBPath = "foreign" stored, err := reg.Get(adminTokenHost) if err != nil || stored.DBPath != f.rt.DBPath { t.Fatal("route read changed registry", err) } launch, got, err := a.Launch(ctx, adminTokenHost) if err != nil { t.Fatal(err) } launch.Env[0] = "changed" got.Vars["A"] = "changed" if original.Env[0] != "A=B" || report.Vars["A"] != "hash" { t.Fatal("accessor aliases owner data") } // A present host executed by another box is foreign too. current, err := hostclaim.Read(ctx, f.claims, adminTokenHost) if err != nil { t.Fatal(err) } current, err = hostclaim.Update(ctx, f.claims, current, func(c *hostclaim.Claim) error { c.Op.Exec = "box-2"; return nil }) if err != nil { t.Fatal(err) } checkAll(hostbox.ErrChanged) if _, err = hostclaim.Update(ctx, f.claims, current, func(c *hostclaim.Claim) error { c.Op.Exec = "box-1"; return nil }); err != nil { t.Fatal(err) } previous := reads if _, err = a.Route(ctx, "foreign.example"); !errors.Is(err, hostbox.ErrChanged) { t.Fatal(err) } if _, _, err = a.Launch(ctx, "foreign.example"); !errors.Is(err, hostbox.ErrChanged) { t.Fatal(err) } if _, err = a.HeldWork(ctx, "foreign.example", "held-config", captured); !errors.Is(err, hostbox.ErrChanged) { t.Fatal(err) } if reads != previous { t.Fatal("foreign host reached registry") } work, err := a.HeldWork(ctx, adminTokenHost, "held-release", captured) if err != nil { t.Fatal(err) } f.verify = hostbox.ErrChanged checkAll(hostbox.ErrChanged) if err = work.With(ctx, hostwrite.Identity{}, func() error { t.Fatal("effect after lost identity"); return nil }); !errors.Is(err, hostbox.ErrChanged) { t.Fatal(err) } if reads != previous { t.Fatal("unverified identity reached registry") } } func TestPlan10G2LaunchReadOnlyAndSameReport(t *testing.T) { f := newAdminTokenFixture(t, "box-1") source := claimMoveLaunchSource(f.backend) if _, _, err := source(t.Context(), f.rt); err == nil { t.Fatal("missing token accepted") } if _, err := os.Stat(f.envPath()); !errors.Is(err, os.ErrNotExist) { t.Fatal("read created token", err) } t.Setenv("CARLOS_SECRETS_KEY", base64.StdEncoding.EncodeToString(make([]byte, 32))) if err := os.WriteFile(f.envPath(), []byte("CARLOS_ADMIN_TOKEN=existing\nSECRET=value\nCARLOS_MOVE_PROBE=1\n"), 0600); err != nil { t.Fatal(err) } before, err := os.ReadFile(f.envPath()) if err != nil { t.Fatal(err) } launch, report, err := source(t.Context(), f.rt) if err != nil { t.Fatal(err) } after, _ := os.ReadFile(f.envPath()) if !reflect.DeepEqual(before, after) { t.Fatal("launch read wrote environment") } env := map[string]string{} for _, kv := range launch.Env { k, v, ok := strings.Cut(kv, "=") if ok { env[k] = v } } if env["CARLOS_CONTROL_SOCKET"] != f.rt.Socket+".control" { t.Fatal("wrong role environment") } if _, ok := env["CARLOS_MOVE_PROBE"]; ok { t.Fatal("probe-only input included in pin") } key, err := moves.ReportKeyFromSecrets(os.Getenv("CARLOS_SECRETS_KEY")) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(report, moves.ReportEnv(key, env)) { t.Fatal("report is not the returned launch environment") } } // Count dispatches while using main's concrete move pin checker. type plan10InputOwner struct { *movesProbeOwners calls int } func (m *plan10InputOwner) InputPolicy(ctx context.Context, c *hostclaim.Claim, s hostbox.Selection, id hostwrite.Identity) error { m.calls++ return m.movesProbeOwners.InputPolicy(ctx, c, s, id) } func TestPlan10CommittedInputDispatch(t *testing.T) { reader := &mpReader{rec: mpRecord()} reader.rec.Move.State = provision.MoveCommitted reader.rec.Move.Inputs.EnvReport = "env2:sha256:report" // a v2 pin (moves.CurrentEnvPin) owner := &plan10InputOwner{movesProbeOwners: &movesProbeOwners{Records: reader.read}} serving := 0 legacy := func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error { serving++ return hostbox.ErrChanged } out := ClaimOwnerBindings{InputPolicy: legacy, MovesPolicy: MovesClaimPolicy(reader.read), Moves: ClaimMovesOwners{ClaimKindOwners: ClaimKindOwners{Inputs: owner}}} bindProductionClaimInputPolicy(func(string) string { return "1" }, &out, reader.read) policy := hostbox.Policy{Inputs: out.InputPolicy, Moves: out.MovesPolicy, Held: func(context.Context, string) (string, error) { return "", nil }} c := mpClaim(hostclaim.KindLanding, hostclaim.PhaseLandingCommitted) c.Holder.WriterBox = mpTarget s := mpSelection(c, "spawn") id := pinnedLaunchIdentity(reader.rec.Move.Inputs) if err := hostbox.WakePhase(c, mpTarget, false); err != nil { t.Fatal("cold wake phase", err) } if err := policy.Check(t.Context(), c, s, id); err != nil { t.Fatal("matching committed cold-spawn inputs", err) } for _, bad := range []hostwrite.Identity{{Config: "different", Release: id.Release}, {Config: id.Config, Release: "different"}} { if err := policy.Check(t.Context(), c, s, bad); err == nil { t.Fatal("mismatching committed inputs accepted") } } if owner.calls != 3 || serving != 0 { t.Fatalf("moves=%d serving=%d", owner.calls, serving) } for _, kind := range []string{hostclaim.KindMoving, hostclaim.KindLanding, hostclaim.KindReverting, hostclaim.KindRestoring} { c.Op.Kind = kind s.Kind = kind before := owner.calls _ = out.InputPolicy(t.Context(), c, s, id) if owner.calls != before+1 || serving != 0 { t.Fatalf("%s fell back", kind) } } c.Op.Kind = hostclaim.KindServing s.Kind = c.Op.Kind _ = out.InputPolicy(t.Context(), c, s, id) if serving != 1 { t.Fatal("serving lost own check") } c.Op.Kind = hostclaim.KindLanding s.Kind = c.Op.Kind missing := ClaimOwnerDispatch{Serving: ClaimKindOwners{InputPolicy: legacy}} if err := missing.InputPolicy(t.Context(), c, s, id); err == nil || serving != 1 { t.Fatal("missing move owner fell back") } for _, off := range []string{"CARLOS_HOST_CLAIM", "CARLOS_MOVES_PHASE1"} { legacyOut := ClaimOwnerBindings{InputPolicy: legacy, MovesPolicy: MovesClaimPolicy(reader.read), Moves: out.Moves} bindProductionClaimInputPolicy(func(k string) string { if k == off { return "" } return "1" }, &legacyOut, reader.read) if reflect.ValueOf(legacyOut.InputPolicy).Pointer() != reflect.ValueOf(legacy).Pointer() { t.Fatal("flag-off callback changed") } if err := legacyOut.MovesPolicy(t.Context(), c, s, id); err == nil { t.Fatal("flag-off committed spawn granted") } } } type plan10PinnedInputs struct { plan9D2Inputs identity hostwrite.Identity } func (p plan10PinnedInputs) Select(ctx context.Context, rt registry.Route, o hostclaim.Observed, policy activator.StartPolicy) (ClaimLaunchInputs, error) { in, err := p.plan9D2Inputs.Select(ctx, rt, o, policy) in.Identity = p.identity return in, err } func TestPlan10CommittedColdAdmissionInputs(t *testing.T) { for _, mismatch := range []bool{false, true} { t.Run(fmt.Sprint(mismatch), func(t *testing.T) { f := newRecoveryFixture(t) f.recordPrelaunch = true ctx := t.Context() o, err := hostclaim.Update(ctx, f.claims, f.read(), func(c *hostclaim.Claim) error { c.Op = hostclaim.Op{Kind: hostclaim.KindLanding, ID: "m1", Phase: hostclaim.PhaseLandingCommitted, Exec: "box-1", Participants: []string{"box-0", "box-1"}, Intent: map[string]string{"to": "ie", "source": "box-0", "target": "box-1"}} return nil }) if err != nil { t.Fatal(err) } rec := &provision.Record{Host: recoveryHost, Move: &provision.MoveState{ID: "m1", Incarnation: o.Claim.Holder.Incarnation, State: provision.MoveCommitted, Inputs: mpPins()}} rec.Move.Inputs.EnvReport = "env2:sha256:report" // a v2 pin (moves.CurrentEnvPin) read := MovesRecordReader(func(context.Context, string, string, string) (*provision.Record, error) { return rec, nil }) owner := &plan10InputOwner{movesProbeOwners: &movesProbeOwners{Records: read}} bindings := ClaimOwnerBindings{InputPolicy: func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error { t.Fatal("serving fallback") return nil }, Moves: ClaimMovesOwners{ClaimKindOwners: ClaimKindOwners{Inputs: owner}}} bindProductionClaimInputPolicy(func(string) string { return "1" }, &bindings, read) policy := hostbox.Policy{Inputs: bindings.InputPolicy, Moves: bindings.MovesPolicy, Held: func(context.Context, string) (string, error) { return "", nil }} c := f.controller("boot-1", "controller-1", 4101, "start-1") id := pinnedLaunchIdentity(rec.Move.Inputs) if mismatch { id.Release = "different" } c.wake.Inputs = plan10PinnedInputs{plan9D2Inputs{recoveryInputs{f}}, id} c.wake.Continue = hostclaim.SessionContinues c.wake.Operations = &plan9D2Attempts{} c.wake.ProbeReady = func(context.Context, *exec.Cmd, registry.Route, ClaimLaunchInputs, string) error { return nil } source := hostbox.WorkSource{Guard: f.guard, Box: "box-1", Region: "ie", Now: func() time.Time { return f.now }, Policy: policy.Check, Local: func(context.Context, *hostclaim.Claim, hostbox.Selection) error { return nil }} writer := hostbox.Writer{Claims: f.claims, Box: "box-1", Region: "ie", AdmissionEvidence: func(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error { return nil }} c.wake.mu.Lock() c.wake.pending = nil c.wake.mu.Unlock() runtime, err := c.wake.Build(ctx, source, writer) if err != nil { t.Fatal(err) } live, cancel := context.WithCancel(ctx) defer cancel() done := make(chan struct{}) go func() { <-live.Done(); close(done) }() c.wake.mu.Lock() c.wake.pending[recoveryHost] = &claimWakeAttempt{observed: o, held: recoveryHeld{}, ctx: live, cancel: cancel, done: done} c.wake.mu.Unlock() admitted, err := runtime.Admit(ctx, registry.Route{Host: recoveryHost}, o, activator.StartPolicy{MaxStarts: 5, Window: 30 * time.Second}) if mismatch { if err == nil { t.Fatal("mismatching committed cold admission accepted") } } else if err != nil { t.Fatal(err) } else if admitted.Claim.Holder.Session <= o.Claim.Holder.Session || admitted.Claim.Holder.Generation == o.Claim.Holder.Generation { t.Fatal("not a cold admission") } if owner.calls == 0 { t.Fatal("admission bypassed moves input check") } }) } }