package host import ( "context" "crypto/ed25519" "database/sql" "encoding/json" "errors" "time" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostguard" ) var ErrClaimStopControllerAlive = errors.New("claim stop controller is still alive") type stopRecoveryKernel interface { RecoverStopped(context.Context, hostguard.FenceTarget, StopInventory) (StopKernelEvidence, DrainAcknowledgement, error) } type exactStopInventory interface { ReadExactStopInventory(context.Context, hostguard.FenceTarget) (StopInventory, error) } func (c ClaimStopCoordinator) verifyUnjournaled(ctx context.Context, target hostguard.FenceTarget, in StopInventory, cause error) (StopKernelEvidence, error) { kernel, ok := c.Kernel.(interface { VerifyUnjournaled(context.Context, hostguard.FenceTarget, StopInventory) (StopKernelEvidence, error) }) if !ok || c.Guard.Root == nil || !errors.Is(cause, sql.ErrNoRows) { return StopKernelEvidence{}, cause } if err := c.Guard.ResumeFence(ctx, target); err != nil { return StopKernelEvidence{}, err } return kernel.VerifyUnjournaled(ctx, target, in) } func (c ClaimStopCoordinator) recoverKernel(ctx context.Context, kernel stopRecoveryKernel, target hostguard.FenceTarget, in StopInventory) (StopKernelEvidence, DrainAcknowledgement, error) { evidence, ack, err := kernel.RecoverStopped(ctx, target, in) if err != nil { evidence, err = c.verifyUnjournaled(ctx, target, in, err) if err == nil { ack = DrainClaimAcknowledgement(target.FenceID, target.ControllerID, "unlaunched", nil, hostbox.ProducerCompletion{}) } } return evidence, ack, err } func (c ClaimStopCoordinator) RecoverStopped(ctx context.Context, target hostguard.FenceTarget) (StopResult, error) { kernel, ok := c.Kernel.(stopRecoveryKernel) if !ok || c.Now == nil || c.NewID == nil || len(c.EvidenceSigner) != ed25519.PrivateKeySize || c.RequestLimit != hostbox.RequestLimit || !stopOperationTuple(target.Op, target.Phase, target.ObligationID) { return StopResult{}, ErrClaimStopUnavailable } inventory, ok := c.Inventory.(exactStopInventory) if !ok { return StopResult{}, ErrClaimStopInventory } // A fence persisted without a roster numeral cannot produce a receipt the // console accepts; refuse it before any kill (roster binding N1). numeral, err := stopReceiptNumeral(target) if err != nil { return StopResult{}, err } if err := c.Guard.ResumeFence(ctx, target); err != nil { return StopResult{}, err } in, err := inventory.ReadExactStopInventory(ctx, target) if err != nil { return StopResult{}, err } if in.Host != target.Host || in.Incarnation != target.Incarnation || in.InstallationStamp != target.Stamp || in.Box != target.Box || in.Enrollment != target.Enrollment || in.DirectoryRevision != target.DirectoryRevision { return StopResult{}, ErrClaimStopInventory } evidence, ack, err := c.recoverKernel(ctx, kernel, target, in) if err != nil { return StopResult{}, err } if ack.FenceID != target.FenceID || ack.ControllerID != target.ControllerID || ack.ProcessStart == "" || !stopDigest(ack.Digest) { return StopResult{}, ErrClaimStopIdentity } id := c.NewID() if !stopComponent(id) { return StopResult{}, ErrClaimStopUnavailable } at := c.Now().UTC() if at.IsZero() || !stopDigest(evidence.InventorySHA256) || !stopDigest(evidence.LocalJournalSHA256) || len(evidence.CgroupIdentities) == 0 { return StopResult{}, ErrClaimStopReceipt } receipt := StopReceipt{RequestLimitNS: int64(c.RequestLimit), Region: target.Region, ClaimETag: target.ClaimETag, ObservedBootID: evidence.ObservedBootID, V: 1, ID: id, ClaimGen: target.ClaimGen, Host: target.Host, Incarnation: target.Incarnation, InstallationStamp: target.Stamp, WriterSession: target.Session, OperationID: target.Op, Phase: target.Phase, ObligationID: target.ObligationID, Box: target.Box, Enrollment: target.Enrollment, BootID: target.BootID, ControllerID: target.ControllerID, DirectoryRevision: numeral, Binding: target.DirectoryRevision, Kind: "stopped", EvidenceAt: at, FenceID: target.FenceID, InventorySHA256: evidence.InventorySHA256, LocalJournalSHA256: evidence.LocalJournalSHA256, ProducerDrainDigest: ack.Digest, CgroupIdentities: evidence.CgroupIdentities, UnitInvocationIDs: evidence.UnitInvocationIDs} body, err := json.Marshal(receipt) if err != nil { return StopResult{}, err } if err := c.Guard.CompleteFence(ctx, target, body, at, func(context.Context, hostguard.FenceTarget, []byte, time.Time) error { return nil }); err != nil { return StopResult{}, err } return StopResult{Target: target, Key: stopEvidenceKey(target.Host, target.Op, target.Box, id), Digest: digestRawStop(body), Bytes: body, EvidenceAt: at}, nil } func (h ClaimStopHTTP) RecoverPending(ctx context.Context) error { targets, err := h.Coordinator.Guard.PendingStopFences(ctx) if err != nil { return err } for _, target := range targets { if _, err := h.Coordinator.RecoverStopped(ctx, target); err != nil && !errors.Is(err, ErrClaimStopControllerAlive) { return err } } return nil }