//go:build linux package host import ( "context" "encoding/json" "errors" "net" "os" "path/filepath" "strings" "testing" "time" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" execlease "github.com/carlosframework/platform/internal/hostexec" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/instancectl" "github.com/carlosframework/platform/internal/provision" ) type rev2Local struct{ err error } func (l *rev2Local) Check(context.Context, hostbox.Actor, *hostclaim.Claim, hostbox.Selection) error { return l.err } type rev2Clock struct { wall time.Time boot int64 } func (c *rev2Clock) Now() (time.Time, int64, error) { return c.wall, c.boot, nil } func (c *rev2Clock) After(d time.Duration) <-chan time.Time { return time.After(d) } type rev2Attempts struct { attempt hostclaim.WakeRetryAttempt op hostclaim.Op stamp, session uint64 } func (a *rev2Attempts) ReadAttempt(_ context.Context, o hostclaim.Observed) (hostclaim.WakeRetryAttempt, error) { if o.Claim.Op.ID != a.op.ID || o.Claim.Op.Kind != a.op.Kind || o.Claim.Op.Phase != a.op.Phase || o.Claim.Holder.Stamp != a.stamp || o.Claim.Holder.Session != a.session { return hostclaim.WakeRetryAttempt{}, hostbox.ErrChanged } return a.attempt, nil } func configureRev2Launch(t *testing.T, f *launchFixture) { t.Helper() f.root.DataRoot = f.root.Guard.DataDir f.root.Local = &rev2Local{} f.root.Clock = &rev2Clock{wall: time.Now(), boot: 100} f.root.ReadyBudget, f.root.ReadyPoll = 40*time.Millisecond, time.Millisecond f.root.ExecLockDir = t.TempDir() f.kernel.digest = strings.Repeat("a", 64) f.kernel.observed.Running = f.kernel.observed.Executable f.req.Spec = &hostbox.LaunchSpec{Identity: hostwrite.Identity{Config: "config", Release: "release"}, Executable: f.kernel.path, Digest: f.kernel.digest, Args: []string{"--socket", filepath.Join(provision.InstanceRunDir(f.target.Host), "app.sock"), "--db", provision.NestedDBPath(f.root.DataRoot, f.target.Host)}, Env: []string{"CARLOS_ADMIN_TOKEN=test"}} digest, err := f.req.Spec.Hash() if err != nil { t.Fatal(err) } f.req.Reservation.Inputs = digest a := hostclaim.WakeRetryAttempt{ID: "attempt-1", InvocationID: "inv-1", LaunchInputs: digest, Box: f.target.Box, Enrollment: f.target.Enrollment, BootID: f.peer.BootID, ControllerID: f.peer.ControllerID, Session: f.target.Session} if f.claims.obs.Claim.Op.Kind == hostclaim.KindServing { f.claims.set(func(c *hostclaim.Claim) { c.Holder.WakeRetry = &hostclaim.WakeRetry{EpisodeID: "episode", State: "attempting", AttemptsReserved: 1, Attempt: &a} }) } else { f.root.Attempts = map[string]claimLaunchAttemptReader{f.claims.obs.Claim.Op.Kind: &rev2Attempts{attempt: a, op: f.claims.obs.Claim.Op, stamp: f.target.Stamp, session: f.target.Session}} } configureLaunchParticipation(t, f) } func writeRev2Local(t *testing.T, f *launchFixture, status string, deadline int64) { t.Helper() q := execlease.ExecutionRequest{Schema: 1, ID: "begin", Operation: "executionBegin", Host: f.target.Host, Incarnation: f.target.Incarnation, Stamp: f.target.Stamp, Session: f.target.Session, Op: f.target.Op} r := execlease.ExecutionRecord{Schema: 1, Request: q, Peer: execlease.ControllerProcess{KernelBootID: f.peer.BootID, PID: f.peer.PID, Start: f.peer.ProcessStart}, Authority: execlease.ExecutionAuthority{Box: f.target.Box, Enrollment: f.target.Enrollment, Migration: 1, Installation: "root-installation"}, Broker: "root", Token: "root-token", Status: status, SafeUntil: deadline, LeaseETag: "root-etag", Expires: time.Now().Add(time.Minute), Replay: map[string]execlease.ExecutionReplay{}} raw, _ := json.Marshal(r) if err := f.root.executionState.Write(f.target.Host+".json", raw); err != nil { t.Fatal(err) } } func rev2Operation() hostclaim.Op { return hostclaim.Op{Kind: hostclaim.KindRelocating, ID: "operation", Phase: "probing", Exec: "box-1", Journal: map[string]string{"install_stamp": "11"}} } func TestPlan11Rev2OperationVerifierAndLocalState(t *testing.T) { for _, name := range []string{"valid", "no verifier", "wrong attempt", "wrong input", "expired", "pending", "lost", "admission closed", "released live lock", "missing state"} { t.Run(name, func(t *testing.T) { f := newLaunchFixture(t, rev2Operation()) switch name { case "no verifier": f.root.Attempts = nil case "wrong attempt": f.root.Attempts[hostclaim.KindRelocating].(*rev2Attempts).attempt.InvocationID = "other" case "wrong input": f.req.Spec.Env = []string{"CARLOS_ADMIN_TOKEN=other"} case "expired": writeRev2Local(t, f, "active", 100) case "pending": writeRev2Local(t, f, "pending", 1000) case "lost": writeRev2Local(t, f, "lost", 1000) case "admission closed": f.root.Local.(*rev2Local).err = execlease.ErrLost case "released live lock": if err := os.Remove(filepath.Join(f.root.executionState.(*launchParticipationDisk).dir, f.target.Host+".live")); err != nil { t.Fatal(err) } case "missing state": if err := os.Remove(filepath.Join(f.root.executionState.(*launchParticipationDisk).dir, f.target.Host+".json")); err != nil { t.Fatal(err) } } _, err := f.root.Serve(t.Context(), f.peer, f.req) if name == "valid" { if err != nil { t.Fatal(err) } return } if err == nil { t.Fatal("unsafe operation launch") } f.noReservation(t, name) }) } } func TestPlan11Rev2RechecksAfterUnitPreparation(t *testing.T) { for _, name := range []string{"deadline", "quiet", "closed", "reservation", "roster"} { t.Run(name, func(t *testing.T) { f := newLaunchFixture(t, rev2Operation()) f.units.beforeCheck = func() { switch name { case "deadline": f.root.Clock.(*rev2Clock).boot = 1000 case "quiet": f.claims.set(func(c *hostclaim.Claim) { c.QuietAfter = map[string]time.Time{"eu": time.Now().Add(time.Hour)} }) case "closed": f.root.Local.(*rev2Local).err = execlease.ErrLost case "reservation": f.root.Attempts[hostclaim.KindRelocating].(*rev2Attempts).attempt.ID = "replaced" case "roster": f.actor.Eligible = false } } if _, err := f.root.Serve(t.Context(), f.peer, f.req); err == nil { t.Fatal("stale final check") } if starts, _ := f.units.counts(); starts != 0 { t.Fatal("tenant code started") } got, err := f.journal.UnitReservation(t.Context(), f.target, f.req.Reservation.ID) if err != nil || got.State != hostbox.UnitSettled { t.Fatalf("unstarted intention not settled: %+v %v", got, err) } }) } } func TestPlan11Rev2WaitsForSameInvocation(t *testing.T) { for _, name := range []string{"slow valid", "timeout", "replacement invocation", "reused pid", "wrong executable", "cancel"} { t.Run(name, func(t *testing.T) { f := newLaunchFixture(t, servingOp()) calls := 0 ctx, cancel := context.WithCancel(t.Context()) defer cancel() f.kernel.observe = func(instancectl.ClaimUnitStarted) (ClaimLaunchObserved, error) { calls++ got := f.kernel.observed if name == "replacement invocation" && calls == 1 { f.units.sameErr = instancectl.ErrClaimUnit } if name == "reused pid" && calls > 1 { got.ProcessStart = "reused" } if name == "cancel" { cancel() } if calls < 4 || name == "timeout" { return ClaimLaunchObserved{ProcessStart: got.ProcessStart, Group: got.Group}, errClaimLaunchPending } if name == "wrong executable" { got.Running.Inode++ } return got, nil } _, err := f.root.Serve(ctx, f.peer, f.req) starts, stops := f.units.counts() if starts != 1 || f.claims.obs.Claim.Holder.WakeRetry.AttemptsReserved != 1 { t.Fatalf("readiness consumed an attempt: %d", starts) } if name == "slow valid" { if err != nil || stops != 0 || calls != 4 { t.Fatalf("valid start killed: %v calls=%d stops=%d", err, calls, stops) } return } wantStops := 1 if name == "replacement invocation" { wantStops = 0 } if err == nil || stops != wantStops { t.Fatalf("unverified start granted or replacement stopped: %v stops=%d", err, stops) } if name == "timeout" && !errors.Is(err, context.DeadlineExceeded) { t.Fatal(err) } }) } } func TestPlan11Rev2DigestAndExecutable(t *testing.T) { f := newLaunchFixture(t, servingOp()) s := f.req.Spec want, err := ClaimLaunchDigest(ClaimLaunchInputs{Identity: s.Identity, Executable: s.Executable, ExecutableSHA256: s.Digest, Args: s.Args, Env: s.Env}) got, e := s.Hash() if err != nil || e != nil || got != want { t.Fatalf("digest contract drift: %q %q %v %v", got, want, err, e) } for _, name := range []string{"config", "release", "executable", "sha", "args", "env", "inode"} { t.Run(name, func(t *testing.T) { f := newLaunchFixture(t, servingOp()) s := *f.req.Spec f.req.Spec = &s switch name { case "config": s.Identity.Config = "other" case "release": s.Identity.Release = "other" case "executable": s.Executable = "/opt/other" case "sha": s.Digest = strings.Repeat("b", 64) case "args": s.Args = append([]string(nil), s.Args...) s.Args[1] += "x" case "env": s.Env = []string{"CARLOS_ADMIN_TOKEN=other"} case "inode": f.req.Reservation.Executable.Inode++ } if _, err := f.root.Serve(t.Context(), f.peer, f.req); err == nil { t.Fatal("input substitution launched") } f.noReservation(t, name) }) } } func TestPlan11Rev2KindPreflightRefusesUnverifiedOperation(t *testing.T) { f := newLaunchFixture(t, servingOp()) f.root.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return f.peer, nil } client := NewClaimRoleRootClient(serveClaimRoot(t, ClaimRootWithLaunch{Launch: f.root})) roles := &ClaimRoleAssembly{Root: client} for _, kind := range []string{hostclaim.KindLanding, hostclaim.KindRelocating, "unknown"} { if err := roles.PreflightKind(t.Context(), kind); err == nil { t.Fatalf("unverified %s passed prerequisites", kind) } } f.noReservation(t, "unsupported kinds") if err := roles.PreflightKind(t.Context(), hostclaim.KindServing); err != nil { t.Fatal(err) } }