package host import ( "context" "encoding/json" "errors" "fmt" "strings" "testing" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" ) type noProcessKernelFake struct { stopKernelFunc calls int target hostguard.FenceTarget err error during func() } func (k *noProcessKernelFake) VerifyNoProcess(_ context.Context, t hostguard.FenceTarget, _ StopInventory) (StopKernelEvidence, error) { k.calls++ k.target = t if k.during != nil { k.during() } if k.err != nil { return StopKernelEvidence{}, k.err } return StopKernelEvidence{InventorySHA256: strings.Repeat("b", 64), LocalJournalSHA256: strings.Repeat("c", 64), CgroupIdentities: []string{"absent:carlos-exec@shop.example"}, UnitInvocationIDs: []string{}}, nil } // changingClaims returns obs on the first read and next afterwards. type changingClaims struct { obs, next hostclaim.Observed reads int change bool } func (c *changingClaims) Read(context.Context, string) (hostclaim.Observed, error) { c.reads++ if c.change && c.reads > 1 { return c.next, nil } return c.obs, nil } // noProcessFixture is a created incarnation that never admitted a writer, // executed on box-1 (serving, session 0). func noProcessFixture(t *testing.T) (ClaimStopCoordinator, StopRequest, *noProcessKernelFake, *changingClaims) { t.Helper() c, req, _ := claimStopFixture(t) observed, err := c.Claims.Read(t.Context(), req.Host) if err != nil { t.Fatal(err) } claim := *observed.Claim claim.Op = hostclaim.Op{Kind: hostclaim.KindServing, Exec: req.Box} claim.Holder = &hostclaim.Holder{Incarnation: "inc-1", Stamp: 11} observed.Claim = &claim claims := &changingClaims{obs: observed} c.Claims = claims kernel := &noProcessKernelFake{} c.Kernel = kernel c.Drainer = stopDrainFunc(func(context.Context, StopRequest, string) (DrainAcknowledgement, error) { t.Fatal("a session-zero stop asked for a producer drain") return DrainAcknowledgement{}, nil }) req.Region = "eu-west-1" req.WriterSession = 0 req.OperationID, req.Phase, req.ObligationID = "", "", "" return c, req, kernel, claims } func TestNoProcessWitnessPublishesASessionZeroReceipt(t *testing.T) { c, req, kernel, _ := noProcessFixture(t) result, err := c.FenceAndStop(t.Context(), req) if err != nil { t.Fatal(err) } if kernel.calls != 1 || kernel.target.Session != 0 || kernel.target.FenceID != "" { t.Fatalf("kernel = %d %+v", kernel.calls, kernel.target) } var receipt StopReceipt if err := json.Unmarshal(result.Bytes, &receipt); err != nil { t.Fatal(err) } if receipt.Kind != StopReceiptNoProcess || receipt.WriterSession != 0 || receipt.FenceID != "" || receipt.ProducerDrainDigest != "" || receipt.ClaimETag != req.ExpectedClaimETag || receipt.ClaimGen != 19 { t.Fatalf("receipt = %+v", receipt) } if !strings.Contains(result.Key, "/serving-evidence/box-1/") { t.Fatalf("key = %s", result.Key) } stored, _, err := c.Evidence.Get(t.Context(), result.Key) if err != nil || string(stored) == "" { t.Fatalf("not published: %v", err) } if _, err := validateTransportStop(req, result); err != nil { t.Fatalf("the agent's transport check refuses it: %v", err) } // No fence is installed: the claim's ETag, which any admission changes, // is what binds the witness. called := false if err := c.Guard.With(t.Context(), req.Host, hostguard.Captured{Incarnation: req.Incarnation, Stamp: req.InstallationStamp}, func(*hostclaim.Claim) error { return nil }, func() error { called = true; return nil }); err != nil || !called { t.Fatalf("a fence was installed: %v", err) } } func TestNoProcessWitnessRefuses(t *testing.T) { for name, tc := range map[string]struct { change func(*ClaimStopCoordinator, *StopRequest, *noProcessKernelFake, *changingClaims) want error }{ "a writer was admitted": {func(_ *ClaimStopCoordinator, _ *StopRequest, _ *noProcessKernelFake, cl *changingClaims) { cl.obs.Claim.Holder.Admissions = []hostclaim.AdmissionRecord{{Box: "box-1", Session: 1}} }, ErrClaimStopIdentity}, "a legacy holder without retention": {func(_ *ClaimStopCoordinator, r *StopRequest, _ *noProcessKernelFake, cl *changingClaims) { cl.obs.Claim.Holder.WriterBox = r.Box }, ErrClaimStopIdentity}, "a foreign writer box is named": {func(_ *ClaimStopCoordinator, _ *StopRequest, _ *noProcessKernelFake, cl *changingClaims) { cl.obs.Claim.Holder.WriterBox = "box-2" }, ErrClaimStopIdentity}, "another executor": {func(_ *ClaimStopCoordinator, _ *StopRequest, _ *noProcessKernelFake, cl *changingClaims) { cl.obs.Claim.Op.Exec = "box-2" }, ErrClaimStopIdentity}, "a stale claim": {func(_ *ClaimStopCoordinator, r *StopRequest, _ *noProcessKernelFake, _ *changingClaims) { r.ExpectedClaimETag = "etag-0" }, ErrClaimStopIdentity}, "an admission during the observation": {func(_ *ClaimStopCoordinator, _ *StopRequest, _ *noProcessKernelFake, cl *changingClaims) { next := cl.obs claim := *next.Claim claim.Holder = &hostclaim.Holder{Incarnation: "inc-1", Stamp: 11, Session: 1, WriterBox: "box-1"} next.Claim, next.ETag = &claim, "etag-2" cl.next, cl.change = next, true }, ErrClaimStopIdentity}, "recovery": {func(_ *ClaimStopCoordinator, r *StopRequest, _ *noProcessKernelFake, _ *changingClaims) { r.Recover = true }, ErrClaimStopUnavailable}, "cleanup": {func(_ *ClaimStopCoordinator, r *StopRequest, _ *noProcessKernelFake, _ *changingClaims) { r.Cleanup = true }, ErrClaimStopUnavailable}, "another controller": {func(_ *ClaimStopCoordinator, r *StopRequest, _ *noProcessKernelFake, _ *changingClaims) { r.ControllerID = "controller-2" }, ErrClaimStopIdentity}, "the kernel sees a process": {func(_ *ClaimStopCoordinator, _ *StopRequest, k *noProcessKernelFake, _ *changingClaims) { k.err = ErrClaimStopInventory }, ErrClaimStopInventory}, "a kernel without the witness": {func(c *ClaimStopCoordinator, _ *StopRequest, _ *noProcessKernelFake, _ *changingClaims) { c.Kernel = stopKernelFunc(nil) }, ErrClaimStopUnavailable}, } { t.Run(name, func(t *testing.T) { c, req, kernel, claims := noProcessFixture(t) tc.change(&c, &req, kernel, claims) if _, err := c.FenceAndStop(t.Context(), req); !errors.Is(err, tc.want) { t.Fatalf("got %v want %v", err, tc.want) } keys, _ := c.Evidence.List(t.Context(), "control/") if len(keys) != 0 { t.Fatalf("a refused witness published %v", keys) } }) } } // The agent asks root for the witness with its own tuple and offers no // drain: the session never launched anything. func TestStopCapturedSessionZeroAsksRootForTheWitness(t *testing.T) { f := newRecoveryFixture(t) observed := f.read() if observed.Claim.Holder.Session != 0 { t.Fatalf("fixture session = %d", observed.Claim.Holder.Session) } var got StopRequest var drain StopDrainer a := ClaimAssembly{Claims: f.claims, Wake: &ClaimWakeAssembly{}} a.stopLocal = func(context.Context) (claimStopLocal, error) { return claimStopLocal{Actor: hostbox.Actor{Box: "box-1", Enrollment: "enrollment-1", Region: "ie", Revision: 7, Binding: "7"}, BootID: "boot-1", ControllerID: "controller-1", PID: 42, ProcessStart: "start-42"}, nil } a.collectStop = func(_ context.Context, r StopRequest, d StopDrainer) (StopResult, error) { got, drain = r, d return StopResult{}, errors.New("stop here") } if err := a.stopCapturedSession(t.Context(), observed); err == nil || err.Error() != "stop here" { t.Fatalf("got %v", err) } if got.WriterSession != 0 || got.BootID != "boot-1" || got.ControllerID != "controller-1" || got.Recover || got.ExpectedClaimETag != observed.ETag || got.Region != "ie" { t.Fatalf("request = %+v", got) } if _, ok := drain.(recoveryStopDrain); !ok { t.Fatalf("drain = %T", drain) } } // A refused admission at session zero launched nothing: its cleanup needs // no witness, and publishes no receipt. func TestFailedSessionZeroAdmissionStopsNothing(t *testing.T) { stops := 0 w := &ClaimWakeAssembly{Stop: func(context.Context, hostclaim.Observed) error { stops++; return nil }} o := hostclaim.Observed{Host: "shop.example", Claim: &hostclaim.Claim{Holder: &hostclaim.Holder{Incarnation: "inc-1", Stamp: 11}, Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box-1"}}} if err := w.stopAttempt(t.Context(), &claimWakeAttempt{}, o); err != nil || stops != 0 { t.Fatalf("stopAttempt = %v, stops %d", err, stops) } o.Claim.Holder.Session = 1 if err := w.stopAttempt(t.Context(), &claimWakeAttempt{}, o); err != nil || stops != 1 { t.Fatalf("an admitted session was not stopped: %v, stops %d", err, stops) } } func TestNoProcessWitnessRetainsBeforePublication(t *testing.T) { for _, reject := range []bool{false, true} { t.Run(fmt.Sprint(reject), func(t *testing.T) { c, req, _, claims := noProcessFixture(t) claims.obs.Claim.Holder.WriterBox = req.Box calls := 0 c.RetainNoProcess = func(ctx context.Context, o hostclaim.Observed, result StopResult) error { calls++ if o.ETag != req.ExpectedClaimETag || o.Claim.Holder.Session != 0 || result.Target.FenceID != "" { t.Fatal("wrong retained observation") } keys, err := c.Evidence.List(ctx, "control/") if err != nil || len(keys) != 0 { t.Fatal("published before retention") } if reject { return hostbox.ErrChanged } return nil } _, err := c.FenceAndStop(t.Context(), req) if calls != 1 || (err != nil) != reject { t.Fatalf("retention %d %v", calls, err) } if reject { keys, _ := c.Evidence.List(t.Context(), "control/") if len(keys) != 0 { t.Fatal("published despite failed retention") } } }) } }