//go:build linux package host import ( "bytes" "context" "crypto/ed25519" "encoding/json" "errors" "fmt" "os" "slices" "strings" "sync" "testing" "time" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" execlease "github.com/carlosframework/platform/internal/hostexec" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/park" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" ) // ackRefreshLease is a participant controller whose lease renews like the // root broker's: every renewal rotates the controller token and the lease // ETag and moves the expiry to renewal time plus the TTL. type ackRefreshLease struct { mu sync.Mutex state execlease.State n int } func (l *ackRefreshLease) renew(at time.Time) { l.mu.Lock() defer l.mu.Unlock() l.n++ l.state.ControllerID = fmt.Sprintf("token-%d", l.n) l.state.LeaseETag = fmt.Sprintf(`"lease-%d"`, l.n) l.state.Expires = at.Add(execlease.TTL) } func (l *ackRefreshLease) Ready(context.Context) (execlease.State, error) { l.mu.Lock() defer l.mu.Unlock() return l.state, nil } func (l *ackRefreshLease) Close(context.Context) error { return nil } const ackRefreshHost = "ackrefresh.bab.example" func ackRefreshCheck(t *testing.T, step string, err error) { t.Helper() if err != nil { t.Fatalf("%s: %v", step, err) } } type ackRefreshEvidence struct { box, console hostactors.Report identity []byte } func (e ackRefreshEvidence) BoxReport(context.Context, string, hostactors.Box) (hostactors.Report, error) { return e.box, nil } func (e ackRefreshEvidence) ConsoleReport(context.Context, string, hostactors.Console) (hostactors.Report, error) { return e.console, nil } func (e ackRefreshEvidence) LocalConsole(context.Context) (hostactors.Report, error) { return e.console, nil } func (e ackRefreshEvidence) Identity(context.Context, string) ([]byte, error) { return e.identity, nil } func (e ackRefreshEvidence) Fence(context.Context, hostactors.Observed, string, time.Time) (bool, error) { return false, nil } // ackRefreshController is the console's production wiring (the rootrunner // lifecycle test's lifecycleController) on a test clock: control writer, // mutation check and boundary, ProductionAuthority and Controller. func ackRefreshController(t *testing.T, raw release.Store, key ed25519.PrivateKey, actor hostbox.Actor, pointer release.SystemPointer, now func() time.Time) (hostops.Controller, *hostops.ProductionAuthority, *hostops.ControlWriter) { t.Helper() ctx := t.Context() signer := release.RootSigner(key) directory := hostactors.Directory{Store: raw, PublicKey: key.Public().(ed25519.PublicKey), Bucket: actor.ControlBucket} floor, err := hostfloor.Read(ctx, raw, directory.PublicKey, actor.ControlBucket) ackRefreshCheck(t, "controller floor", err) platform := release.AdoptPlatform() pointer.Current.ConsoleArtifacts = map[string]release.SystemArtifact{platform: {Key: "bootstrap/console/v2.0.0/carlos-" + platform, SHA256: pointer.Current.Artifacts[platform].SHA256}} signed, err := release.SignRootEnvelope(signer, pointer) ackRefreshCheck(t, "sign actor release", err) report := hostactors.Report{Component: "router", GeneratedAt: now(), Actor: actor.Box, Enrollment: actor.Enrollment, InstanceID: actor.Cloud.InstanceID, Release: "v2.0.0", ReleaseTag: "v2.0.0", Platform: platform, BinarySHA256: pointer.Current.Artifacts[platform].SHA256, MovesProtocol: 1, BootID: "external-box-boot", FloorSHA256: floor.Digest, FloorGuardReady: true, HostClaimEnabled: true} console := report console.Component, console.Actor, console.InstanceID, console.Enrollment, console.ProcessID = "console", "console", "console", strings.Repeat("d", 32), "console-process" control, err := hostops.NewControlWriter(raw, directory.PublicKey, now) ackRefreshCheck(t, "control writer", err) check, err := hostops.NewProductionMutationCheck(raw, control) ackRefreshCheck(t, "mutation check", err) records, err := hostops.NewConditionalRecordStore(raw, directory.PublicKey) ackRefreshCheck(t, "record boundary", err) data, err := hostops.NewObservedMutationBoundary(true, records, check, nil) ackRefreshCheck(t, "mutation boundary", err) directory.Store = data authority := &hostops.ProductionAuthority{HomeRegion: "ie", Store: data, Control: control, Directory: directory, ConsoleID: "console", Enrollment: console.Enrollment, Namespace: actor.Namespace, Signer: signer, Now: now, Evidence: ackRefreshEvidence{report, console, signed}} n := 0 controller := hostops.Controller{Store: data, Control: control, Authority: authority, Now: now, NewID: func() string { n++; return fmt.Sprintf("ackrefresh-%024d", n) }} return controller, authority, control } type ackRefreshWorld struct { store release.Store directory hostactors.Directory controller hostops.Controller control *hostops.ControlWriter recovery hostops.Recovery actor hostbox.Actor rec provision.Record id string t0 time.Time now func() time.Time set func(time.Time) } // ackRefreshCreate is G10 on a v0.28.4-equivalent console path: the // production create (registration, StartCreate, RecordEffects) through the // real hostops controller, authority and control writer on a test clock, // then the console's one in-request Complete, which runs before any ack and // gets ErrPending. The request then ends, so nothing renews the create's // registration. func ackRefreshCreate(t *testing.T) *ackRefreshWorld { t.Helper() ctx := t.Context() var mu sync.Mutex at := time.Now().UTC().Truncate(time.Second) w := &ackRefreshWorld{t0: at} w.now = func() time.Time { mu.Lock(); defer mu.Unlock(); return at } w.set = func(next time.Time) { mu.Lock(); at = next; mu.Unlock() } store := release.NewDirStore(t.TempDir()) w.store = store key := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{1}, ed25519.SeedSize)) actor := hostbox.Actor{Box: "box", Enrollment: strings.Repeat("a", 32), Namespace: "platform", Region: "ie", ControlBucket: "deployment", Cloud: hostbox.CloudIdentity{Provider: "aws", Account: "111122223333", Region: "eu-west-1", InstanceID: "i-0abc"}} w.actor = actor roster := hostactors.Roster{V: 1, HomeRegion: "ie", DeploymentBucket: actor.ControlBucket, Revision: 1, Boxes: map[string]hostactors.Box{actor.Box: {BoxID: actor.Box, Enrollment: actor.Enrollment, Namespace: actor.Namespace, Label: actor.Box, Region: actor.Region, Pool: "default", Cloud: hostactors.Cloud{Provider: actor.Cloud.Provider, Account: actor.Cloud.Account, Region: actor.Cloud.Region, InstanceID: actor.Cloud.InstanceID}, EvidenceSHA256: strings.Repeat("c", 64), State: "active"}}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}} raw, err := release.SignRootEnvelope(release.RootSigner(key), roster) ackRefreshCheck(t, "sign roster", err) _, err = store.Put(ctx, hostactors.Key, raw, "") ackRefreshCheck(t, "write roster", err) platform := release.AdoptPlatform() pointer := release.SystemPointer{Schema: 1, Component: "router", Channel: "stable", Spread: "0s", PromotedAt: w.t0, Current: release.SystemRelease{Version: "v2.0.0", ReleaseTag: "v2.0.0", MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{platform: {Key: release.SystemArtifactKey("v2.0.0", platform), SHA256: strings.Repeat("a", 64)}}}} signer := release.RootSigner(key) directory := hostactors.Directory{Store: store, PublicKey: key.Public().(ed25519.PublicKey), Bucket: actor.ControlBucket} w.directory = directory ackRefreshCheck(t, "publish test deployment floor", hostfloor.Publish(ctx, store, signer, actor.ControlBucket, hostfloor.Floor{Min: 1, MinRelease: "v2.0.0"})) floor, err := hostfloor.Read(ctx, store, directory.PublicKey, actor.ControlBucket) ackRefreshCheck(t, "read deployment floor", err) observedRoster, err := directory.Read(ctx) ackRefreshCheck(t, "read actor roster", err) observedRoster.Roster.Publication, observedRoster.Roster.FloorSHA256 = "published", floor.Digest observedRoster.Roster.Regions = map[string]hostactors.Region{actor.Region: {Account: actor.Cloud.Account, AWSRegion: actor.Cloud.Region, Namespaces: []string{actor.Namespace}, Pools: []string{"default"}}} observedRoster.Roster.Consoles["console"] = hostactors.Console{Enrollment: strings.Repeat("d", 32), InstanceID: "console", Namespaces: []string{actor.Namespace}, DeploymentBucket: actor.ControlBucket} _, err = directory.Replace(ctx, signer, observedRoster, observedRoster.Roster) ackRefreshCheck(t, "publish actors", err) controller, authority, control := ackRefreshController(t, store, key, actor, pointer, w.now) w.controller, w.control = controller, control rec := provision.Record{Schema: 1, Host: ackRefreshHost, Account: "bab", App: "lifecycle", Channel: "stable", CreatedAt: w.t0} data := controller.Store appID, err := control.EnrollNewApp(ctx, rec.Account, rec.App) ackRefreshCheck(t, "enroll app", err) before, err := hostclaim.Read(ctx, data, rec.Host) ackRefreshCheck(t, "read before registration", err) w.id = "create-" + rec.App request, endRequest := context.WithCancel(ctx) start, err := control.RegisterStart(request, rec.Account, rec.App, appID, w.id, hostops.Registration{Kind: "create", Host: rec.Host, Owner: hostops.RegistrationOwner{Console: "console", Enrollment: strings.Repeat("d", 32), Controller: "console-controller"}, AppID: appID, Epoch: 1, Claim: hostops.SavedObservation(before)}, w.now()) ackRefreshCheck(t, "register create", err) rec.Acquisition = w.id requested := rec requested.CreatedAt = time.Time{} record, err := hostops.PrepareInstance(requested, rec.CreatedAt, park.PlacementRegion(rec.Region)) ackRefreshCheck(t, "prepare create record", err) incarnation, err := record.Incarnation() ackRefreshCheck(t, "create incarnation", err) ackRefreshCheck(t, "bind registration", start.Bind(request, rec.CreatedAt, incarnation, w.now())) createCtx := hostops.WithStartHandle(start.Lifecycle(request), start) observed, err := authority.StartCreate(createCtx, before, record, w.id, "runner") ackRefreshCheck(t, "production begin create", err) ackRefreshCheck(t, "production record effects", authority.RecordEffects(createCtx, observed, record)) if _, err = controller.Complete(createCtx, rec.Host, w.id); !errors.Is(err, hostops.ErrPending) { t.Fatalf("in-request Complete before any ack = %v, want ErrPending", err) } endRequest() w.rec = rec noop := func(context.Context) error { return nil } noopObserved := func(context.Context, hostclaim.Observed) error { return nil } // SweepAdmissions settles registrations as the console's sweep does once // no exclusive operation is open (console claim_runtime.go). sweep := func(ctx context.Context) error { apps, err := hostops.Apps(ctx, store) if err != nil { return err } for _, app := range apps { a, err := hostops.ReadCanonicalAdmission(ctx, store, app.Account, app.Name) if errors.Is(err, os.ErrNotExist) { continue } if err != nil { return err } if a.Value.Op != nil { return fmt.Errorf("unexpected exclusive op %s", a.Value.Op.ID) } for id := range a.Value.Registrations { if err := control.SettleRegistration(ctx, app.Account, app.Name, a.Value.AppID, id, w.now()); err != nil && !errors.Is(err, hostops.ErrPending) { return err } } } return nil } w.recovery = hostops.Recovery{Controller: controller, Purge: noopObserved, Placement: noopObserved, SweepRecords: noop, SweepPurges: noop, SweepAdmissions: sweep} return w } func (w *ackRefreshWorld) claim(t *testing.T) hostclaim.Observed { t.Helper() o, err := hostclaim.Read(t.Context(), w.store, w.rec.Host) if err != nil || o.Absent || o.Claim == nil { t.Fatalf("read claim: %+v %v", o, err) } return o } func (w *ackRefreshWorld) ack(t *testing.T) hostclaim.ExecutionAcknowledgement { t.Helper() var ack hostclaim.ExecutionAcknowledgement if err := json.Unmarshal([]byte(w.claim(t).Claim.Op.Journal["exec_ack."+w.actor.Box]), &ack); err != nil { t.Fatal("missing acknowledgement", err) } return ack } // registration is the create's admission registration, which the stuck // harness still listed, expired. func (w *ackRefreshWorld) registration(t *testing.T) (hostops.Registration, bool) { t.Helper() a, err := hostops.ReadCanonicalAdmission(t.Context(), w.store, w.rec.Account, w.rec.App) if err != nil { t.Fatal(err) } reg, ok := a.Value.Registrations[w.id] return reg, ok } // participant is the box's participation for the create, holding lease as // its controller in a slot it opened. func (w *ackRefreshWorld) participant(t *testing.T, lease *ackRefreshLease) *productionClaimLocal { t.Helper() o := w.claim(t) if o.Claim.Op.Kind != hostclaim.KindCreating || o.Claim.Op.ID != w.id || !slices.Contains(o.Claim.Op.Participants, w.actor.Box) { t.Fatalf("create is not waiting on the box: %+v", o.Claim.Op) } lease.state = execlease.State{V: 1, State: "active", Binding: execlease.Binding{Box: w.actor.Box, Enrollment: w.actor.Enrollment, Host: w.rec.Host, Incarnation: o.Claim.Holder.Incarnation, Op: w.id, BootID: "boot-1"}} ready := make(chan struct{}) close(ready) return &productionClaimLocal{Resources: ClaimRuntimeResources{Mode: "agent"}, Store: w.store, now: w.now, slots: map[string]*claimParticipation{w.rec.Host: {Ready: ready, Controller: lease}}} } func (w *ackRefreshWorld) pass(t *testing.T, p *productionClaimLocal) { t.Helper() if err := p.acknowledgeOperation(t.Context(), w.actor, w.claim(t)); err != nil { t.Fatal("participation pass", err) } } // TestClaimAckRefreshCompletesCreateThroughRecovery is G10 end to end on // the console's recovery path: the box's ack lands 15 s after the op starts, // the box keeps it fresh on its 30 s participation tick, and the console's // recovery pass (RecoveryPass -> RecoverCreate -> Complete -> Executor -> // FirstPhase) completes the create at started_at + 5 min. By then the create // registration has expired but is still listed, both effects are complete // and the roster hold is still present. Without the refresh, recovery gets // ErrPending and the create stays stuck, as on the harness. func TestClaimAckRefreshCompletesCreateThroughRecovery(t *testing.T) { for _, refresh := range []bool{true, false} { name := "refreshed" if !refresh { name = "first ack only" } t.Run(name, func(t *testing.T) { w := ackRefreshCreate(t) lease := &ackRefreshLease{} box := w.participant(t, lease) first := w.t0.Add(15 * time.Second) w.set(first) lease.renew(first) w.pass(t, box) ack0 := w.ack(t) if ack0.ControllerID != "token-1" || !ack0.Expires.Equal(first.Add(execlease.TTL)) { t.Fatalf("first ack %+v", ack0) } deadline := w.t0.Add(5*time.Minute + time.Second) for tick := first.Add(30 * time.Second); ; tick = tick.Add(30 * time.Second) { if tick.After(deadline) { tick = deadline } w.set(tick) // The worst case for the box: its last renewal was a full // renewal interval ago. lease.renew(tick.Add(-execlease.Renewal)) if refresh { // Usability only shrinks with time, so an ack still usable // at each tick, before that tick's pass, never lapsed. if ack := w.ack(t); !w.now().Before(ack.Expires.Add(-hostops.RegistrationSkew)) { t.Fatalf("ack lapsed before the pass at +%s: %+v", tick.Sub(w.t0), ack) } w.pass(t, box) ack := w.ack(t) if !w.now().Before(ack.Expires.Add(-hostops.RegistrationSkew)) { t.Fatalf("ack unusable at +%s: %+v", tick.Sub(w.t0), ack) } if ack.Op != ack0.Op || ack.Enrollment != ack0.Enrollment || ack.BootID != ack0.BootID || ack.ControllerID != ack0.ControllerID { t.Fatalf("refresh rebound the ack: %+v, first %+v", ack, ack0) } } if tick.Equal(deadline) { break } // Recovery runs before the op is five minutes old: it neither // completes nor cancels it, and the sweep keeps the registration. if err := w.control.RecoveryPass(t.Context(), w.recovery); err != nil { t.Fatalf("recovery pass at +%s: %v", tick.Sub(w.t0), err) } if o := w.claim(t); o.Claim.Op.Kind != hostclaim.KindCreating || o.Claim.Op.ID != w.id { t.Fatalf("op left creating at +%s: %+v", tick.Sub(w.t0), o.Claim.Op) } if _, ok := w.registration(t); !ok { t.Fatalf("sweep removed the create registration at +%s", tick.Sub(w.t0)) } } if refresh && w.ack(t).Expires.Equal(ack0.Expires) { t.Fatal("ack never refreshed") } if !refresh && w.ack(t) != ack0 { t.Fatal("first-ack-only box changed its ack") } // The harness's state at recovery time. o := w.claim(t) reg, ok := w.registration(t) if !ok || reg.Kind != "create" || !reg.Claim.Absent || hostops.RegistrationLive(reg, w.now()) { t.Fatalf("want an expired, still-listed create registration: %+v %v", reg, ok) } adm, err := hostops.ReadCanonicalAdmission(t.Context(), w.store, w.rec.Account, w.rec.App) if err != nil { t.Fatal(err) } effects := 0 for _, e := range adm.Value.Effects { if e.OperationID == w.id { effects++ if e.State != "complete" { t.Fatalf("effect not complete: %+v", e) } } } if effects != 2 { t.Fatalf("create effects = %d, want 2", effects) } roster, err := w.directory.Read(t.Context()) if err != nil { t.Fatal(err) } if hold, ok := roster.Roster.Holds[o.Claim.Op.Intent["actors_hold"]]; !ok || hold.OpID != w.id { t.Fatalf("roster hold missing: %+v", roster.Roster.Holds) } if err := w.control.RecoveryPass(t.Context(), w.recovery); err != nil { t.Fatal("recovery pass", err) } o = w.claim(t) if !refresh { if o.Claim.Op.Kind != hostclaim.KindCreating || o.Claim.Op.ID != w.id { t.Fatalf("expired ack completed or cancelled the create: %+v", o.Claim.Op) } if err := w.controller.RecoverCreate(t.Context(), w.rec.Host); !errors.Is(err, hostops.ErrPending) { t.Fatalf("RecoverCreate with an expired ack = %v, want ErrPending", err) } if _, ok := w.registration(t); !ok { t.Fatal("stuck create lost its registration") } return } if o.Claim.Op.Kind != hostclaim.KindServing || o.Claim.Holder == nil || o.Claim.Holder.Stamp == 0 { t.Fatalf("recovery did not complete the create: %+v %+v", o.Claim.Op, o.Claim.Holder) } if _, ok := w.registration(t); ok { t.Fatal("completed create kept its registration") } }) } } // TestClaimAckRefreshLeavesOtherControllersAlone: the box refreshes only an // ack its own controller wrote in this boot. A restarted agent (a new slot) // or another boot leaves the old ack to expire, and the ack is not touched // while more than ackRefreshBefore remains. func TestClaimAckRefreshLeavesOtherControllersAlone(t *testing.T) { for _, tc := range []string{"not yet due", "restarted controller", "other boot", "lease not renewed", "foreign enrollment", "foreign op", "foreign controller", "lease for another incarnation"} { t.Run(tc, func(t *testing.T) { w := ackRefreshCreate(t) lease := &ackRefreshLease{} box := w.participant(t, lease) first := w.t0.Add(15 * time.Second) w.set(first) lease.renew(first) w.pass(t, box) ack0 := w.ack(t) at := ack0.Expires.Add(-ackRefreshBefore + time.Second) switch tc { case "not yet due": at = ack0.Expires.Add(-ackRefreshBefore) case "restarted controller": box = w.participant(t, lease) lease.state.Expires = ack0.Expires case "other boot": lease.mu.Lock() lease.state.BootID = "boot-2" lease.mu.Unlock() case "foreign enrollment", "foreign op", "foreign controller": // A journalled ack this slot did not write, or one naming // another enrollment or op, is never extended. foreign := ack0 switch tc { case "foreign op": foreign.Op = "other-op" case "foreign enrollment": foreign.Enrollment = strings.Repeat("b", 32) default: foreign.ControllerID = "earlier-controller" } raw, err := json.Marshal(foreign) if err != nil { t.Fatal(err) } if _, err = hostclaim.Update(t.Context(), w.store, w.claim(t), func(c *hostclaim.Claim) error { c.Op.Journal["exec_ack."+w.actor.Box] = string(raw) return nil }); err != nil { t.Fatal(err) } ack0 = foreign } w.set(at) if tc != "lease not renewed" { lease.renew(at) } if tc == "lease for another incarnation" { lease.mu.Lock() lease.state.Incarnation = "other-incarnation" lease.mu.Unlock() if err := box.acknowledgeOperation(t.Context(), w.actor, w.claim(t)); !errors.Is(err, execlease.ErrLost) { t.Fatalf("refresh from a lease for another incarnation = %v, want ErrLost", err) } } else { w.pass(t, box) } if got := w.ack(t); got != ack0 { t.Fatalf("ack changed: %+v, was %+v", got, ack0) } if tc == "not yet due" { w.set(at.Add(time.Second)) w.pass(t, box) if got := w.ack(t); got.Expires.Equal(ack0.Expires) || got.ControllerID != ack0.ControllerID { t.Fatalf("due ack not refreshed: %+v", got) } } }) } } // TestClaimAckRefreshProductionClient runs the refresh against the real root // broker and RootClient. A renewal rotates the controller token, so the // refreshed ack keeps the first ack's controller ID and carries the renewed // lease ETag and expiry. func TestClaimAckRefreshProductionClient(t *testing.T) { if testing.Short() { t.Skip("waits for one real lease renewal") } f, root, _, _, local := productionExecutionFixture(t, hostclaim.KindLanding) before := f.read() h := before.Claim.Holder rec := provision.Record{Schema: 1, Host: before.Host, Account: h.Account, App: h.App, Channel: "stable", Region: "ie", CreatedAt: f.now} body, err := json.Marshal(rec) if err != nil { t.Fatal(err) } _, err = hostclaim.Update(t.Context(), f.claims, before, func(c *hostclaim.Claim) error { *c = hostclaim.Claim{V: c.V, Host: c.Host, Gen: c.Gen} return hostclaim.BeginSelectedCreate(hostclaim.NewHolder{Host: rec.Host, Account: rec.Account, App: rec.App, Family: "instance", Key: h.Key, Region: "ie", RecordRegion: "ie", CreatedAt: rec.CreatedAt}, body, "create", "test", f.now, "box-1", "en", "app-id", 1, "own", "record")(c) }) if err != nil { t.Fatal(err) } actor, err := root.InventoryActor(t.Context()) if err != nil { t.Fatal(err) } ack := func() hostclaim.ExecutionAcknowledgement { var a hostclaim.ExecutionAcknowledgement if err := json.Unmarshal([]byte(f.read().Claim.Op.Journal["exec_ack."+actor.Box]), &a); err != nil { t.Fatal("missing acknowledgement", err) } return a } if err = local.participateHost(t.Context(), actor, rec.Host); err != nil { t.Fatal("participate and acknowledge", err) } ack0 := ack() controller := local.slots[rec.Host].Controller var state execlease.State for deadline := time.Now().Add(2 * execlease.Renewal); ; { if state, err = controller.Ready(t.Context()); err != nil { t.Fatal(err) } if state.Expires.After(ack0.Expires) { break } if time.Now().After(deadline) { t.Fatal("lease never renewed") } time.Sleep(250 * time.Millisecond) } if state.ControllerID == ack0.ControllerID || state.BootID != ack0.BootID { t.Fatalf("renewal did not rotate the controller token: %+v, ack %+v", state, ack0) } // Past the first ack's usable window, as at the console's recovery. local.now = func() time.Time { return ack0.Expires.Add(-hostops.RegistrationSkew) } if err = local.participateHost(t.Context(), actor, rec.Host); err != nil { t.Fatal("refreshing pass", err) } got := ack() if state, err = controller.Ready(t.Context()); err != nil { t.Fatal(err) } if got.Op != ack0.Op || got.Enrollment != ack0.Enrollment || got.BootID != ack0.BootID || got.ControllerID != ack0.ControllerID || got.LeaseETag == "" || got.LeaseETag == ack0.LeaseETag || !got.Expires.After(ack0.Expires) || got.Expires.After(state.Expires) { t.Fatalf("refreshed ack %+v, first %+v, lease %+v", got, ack0, state) } }