diff --git a/overlord/devicestate/devicestate.go b/overlord/devicestate/devicestate.go index 4c4cece5cf4..c7b520de12d 100644 --- a/overlord/devicestate/devicestate.go +++ b/overlord/devicestate/devicestate.go @@ -298,7 +298,7 @@ func delayedCrossMgrInit() { snapstate.DeviceCtx = DeviceCtx snapstate.RemodelingChange = RemodelingChange snapstate.SeedRefreshTasks = SeedRefreshTasks - snapstate.AppendSeedRefreshSetupTaskIDs = AppendSeedRefreshSetupTaskIDs + snapstate.UpdateSeedRefreshChange = UpdateSeedRefreshChange } // proxyStore returns the store assertion for the proxy store if one is set. @@ -1763,26 +1763,41 @@ func removeRecoverySystemTask(st *state.State, label string) *state.Task { } // SeedRefreshTasks returns a [snapstate.SeedRefreshTaskSet] that carries the -// tasks needed to refresh the seed managed by seed-refresh mode. The caller -// must provide the tasks IDs that can be used by the seed creation tasks to -// find the new snaps to include in the seed. Otherwise, already installed snaps -// will be used to create the seed. Older seed-refresh systems are removed so +// tasks needed to refresh the seed managed by seed-refresh mode, plus the snap +// names selected for that seed refresh. The selected setup task IDs are written +// into the recovery-system setup payload so the new seed can consume the +// refreshed snaps and components. Older seed-refresh systems are removed so // that, after finalize records the new system, the two most recently created // seed-refresh systems remain tracked. -func SeedRefreshTasks(st *state.State, snapSetupTasks, compSetupTasks []string) (*snapstate.SeedRefreshTaskSet, error) { +func SeedRefreshTasks(st *state.State, dctx snapstate.DeviceContext, candidates []snapstate.SeedRefreshCandidate) (*snapstate.SeedRefreshTaskSet, map[string]bool, error) { + var snapsups, compsups []string + added := make(map[string]bool, len(candidates)) + for _, candidate := range candidates { + if !seedRefreshIncludesSnap(dctx, candidate.InstanceName) { + continue + } + added[candidate.InstanceName] = true + + snapsups = append(snapsups, candidate.SnapSetupTaskIDs...) + compsups = append(compsups, candidate.ComponentSetupTaskIDs...) + } + if len(added) == 0 { + return nil, nil, nil + } + labelBase := timeNow().Format("20060102") label, err := pickRecoverySystemLabel(labelBase) if err != nil { - return nil, fmt.Errorf("cannot select non-conflicting label for recovery system %q: %v", labelBase, err) + return nil, nil, fmt.Errorf("cannot select non-conflicting label for recovery system %q: %v", labelBase, err) } - ts, err := createRecoverySystemTasks(st, label, snapSetupTasks, compSetupTasks, CreateRecoverySystemOptions{ + ts, err := createRecoverySystemTasks(st, label, snapsups, compsups, CreateRecoverySystemOptions{ TestSystem: true, MarkDefault: true, SeedRefresh: true, }) if err != nil { - return nil, err + return nil, nil, err } var create, finalize *state.Task @@ -1796,12 +1811,12 @@ func SeedRefreshTasks(st *state.State, snapSetupTasks, compSetupTasks []string) } if create == nil || finalize == nil { - return nil, errors.New("internal error: expected create and finalize recovery system tasks") + return nil, nil, errors.New("internal error: expected create and finalize recovery system tasks") } removeLabels, err := seedRefreshLabelsToRemove(st) if err != nil { - return nil, err + return nil, nil, err } removals := make([]*state.Task, 0, len(removeLabels)) @@ -1815,23 +1830,99 @@ func SeedRefreshTasks(st *state.State, snapSetupTasks, compSetupTasks []string) Create: create, Finalize: finalize, Remove: removals, - }, nil + }, added, nil +} + +// UpdateSeedRefreshChange adds a late candidate to an existing seed-refresh +// change when the snap should participate in the refreshed seed. Returns nil if +// snap isn't part of the seed refresh, otherwise returns the seed refresh task +// set. +func UpdateSeedRefreshChange(chg *state.Change, dctx snapstate.DeviceContext, candidate snapstate.SeedRefreshCandidate) (*snapstate.SeedRefreshTaskSet, error) { + if !seedRefreshIncludesSnap(dctx, candidate.InstanceName) { + return nil, nil + } + + seedTS, err := findSeedRefreshTasks(chg) + if err != nil { + return nil, err + } + + if err := appendSeedRefreshCandidate(seedTS.Create, candidate.SnapSetupTaskIDs, candidate.ComponentSetupTaskIDs); err != nil { + return nil, err + } + + return seedTS, nil } -// AppendSeedRefreshSetupTaskIDs appends unique setup task IDs to the -// create-recovery-system task recovery-system-setup payload. -func AppendSeedRefreshSetupTaskIDs(create *state.Task, snapSetupTask string, compSetupTasks []string) error { +func appendSeedRefreshCandidate(create *state.Task, snapSetupTasks, compSetupTasks []string) error { setup, err := taskRecoverySystemSetup(create) if err != nil { return err } - setup.SnapSetupTasks = appendUnique(setup.SnapSetupTasks, snapSetupTask) + setup.SnapSetupTasks = appendUnique(setup.SnapSetupTasks, snapSetupTasks...) setup.ComponentSetupTasks = appendUnique(setup.ComponentSetupTasks, compSetupTasks...) return setTaskRecoverySystemSetup(create, setup) } +func seedRefreshIncludesSnap(dctx snapstate.DeviceContext, instanceName string) bool { + // TODO:SEEDREFRESH: consider the intersections of snaps in the model and + // snaps currently present in the seed, not all snaps in the model. + if instanceName == "snapd" { + return true + } + + for _, sn := range dctx.Model().AllSnaps() { + if sn.SnapName() == instanceName { + return true + } + } + + return false +} + +func findSeedRefreshTasks(chg *state.Change) (*snapstate.SeedRefreshTaskSet, error) { + var finalize *state.Task + var removals []*state.Task + for _, t := range chg.Tasks() { + switch t.Kind() { + case "finalize-recovery-system": + if t.Status().Ready() { + continue + } + if finalize != nil { + return nil, errors.New("internal error: found multiple pending seed finalization tasks in change") + } + finalize = t + case "remove-recovery-system": + if !t.Status().Ready() { + removals = append(removals, t) + } + } + } + + if finalize == nil { + return nil, errors.New("internal error: seed-refresh change is missing pending finalize-recovery-system task") + } + + var createID string + if err := finalize.Get("recovery-system-setup-task", &createID); err != nil { + return nil, err + } + + create := chg.State().Task(createID) + if create == nil || create.Change().ID() != chg.ID() || create.Kind() != "create-recovery-system" { + return nil, errors.New("internal error: seed-refresh change is missing paired create-recovery-system task") + } + + return &snapstate.SeedRefreshTaskSet{ + Create: create, + Finalize: finalize, + Remove: removals, + }, nil +} + func appendUnique(slice []string, additions ...string) []string { seen := make(map[string]bool, len(slice)) for _, id := range slice { diff --git a/overlord/devicestate/devicestate_systems_test.go b/overlord/devicestate/devicestate_systems_test.go index 1265e89f412..8ec3f66605d 100644 --- a/overlord/devicestate/devicestate_systems_test.go +++ b/overlord/devicestate/devicestate_systems_test.go @@ -2755,8 +2755,14 @@ func (s *deviceMgrSystemsCreateSuite) TestSeedRefreshTasksFinalizeUndoDoesNotRes } s.state.Set("seeded-systems", []devicestate.SeededSystem{keepSeededSystem, removeSeededSystem}) - seedTS, err := devicestate.SeedRefreshTasks(s.state, nil, nil) + dctx := &snapstatetest.TrivialDeviceContext{DeviceModel: s.model} + seedTS, added, err := devicestate.SeedRefreshTasks(s.state, dctx, []snapstate.SeedRefreshCandidate{ + { + InstanceName: s.model.Kernel(), + }, + }) c.Assert(err, IsNil) + c.Assert(added, DeepEquals, map[string]bool{s.model.Kernel(): true}) c.Assert(seedTS, NotNil) c.Assert(seedTS.Remove, HasLen, 1) diff --git a/overlord/devicestate/devicestate_test.go b/overlord/devicestate/devicestate_test.go index 9191051926e..81efd1910d3 100644 --- a/overlord/devicestate/devicestate_test.go +++ b/overlord/devicestate/devicestate_test.go @@ -2131,9 +2131,21 @@ func (s *deviceMgrSuite) TestCreateSeedRefreshTasks(c *C) { tSnap1 := s.state.NewTask("fake-download", "...") tSnap2 := s.state.NewTask("fake-download", "...") tComp1 := s.state.NewTask("fake-download-component", "...") + dctx := s.seedRefreshDeviceContext(c, "snap-1", "snap-2") - seedTS, err := devicestate.SeedRefreshTasks(s.state, []string{tSnap1.ID(), tSnap2.ID()}, []string{tComp1.ID()}) + seedTS, added, err := devicestate.SeedRefreshTasks(s.state, dctx, []snapstate.SeedRefreshCandidate{ + { + InstanceName: "snap-1", + SnapSetupTaskIDs: []string{tSnap1.ID()}, + ComponentSetupTaskIDs: []string{tComp1.ID()}, + }, + { + InstanceName: "snap-2", + SnapSetupTaskIDs: []string{tSnap2.ID()}, + }, + }) c.Assert(err, IsNil) + c.Assert(added, DeepEquals, map[string]bool{"snap-1": true, "snap-2": true}) c.Assert(seedTS.Create.Kind(), Equals, "create-recovery-system") c.Assert(seedTS.Finalize.Kind(), Equals, "finalize-recovery-system") @@ -2178,9 +2190,16 @@ func (s *deviceMgrSuite) TestCreateSeedRefreshTasksUsesNextAvailableLabel(c *C) c.Assert(os.MkdirAll(filepath.Join(systemsDir, labelBase+"-2"), 0755), IsNil) tSnap := s.state.NewTask("fake-download", "...") + dctx := s.seedRefreshDeviceContext(c, "snap-1") - seedTS, err := devicestate.SeedRefreshTasks(s.state, []string{tSnap.ID()}, nil) + seedTS, added, err := devicestate.SeedRefreshTasks(s.state, dctx, []snapstate.SeedRefreshCandidate{ + { + InstanceName: "snap-1", + SnapSetupTaskIDs: []string{tSnap.ID()}, + }, + }) c.Assert(err, IsNil) + c.Assert(added, DeepEquals, map[string]bool{"snap-1": true}) c.Assert(seedTS.Create.Kind(), Equals, "create-recovery-system") c.Assert(seedTS.Finalize.Kind(), Equals, "finalize-recovery-system") @@ -2213,9 +2232,17 @@ func (s *deviceMgrSuite) TestCreateSeedRefreshTasksAddsPruneTasks(c *C) { {System: "old-seed-refresh-1", SeedRefresh: true}, {System: "old-seed-refresh-2", SeedRefresh: true}, }) + dctx := s.seedRefreshDeviceContext(c, "snap-1") + tSnap := s.state.NewTask("fake-download", "...") - seedTS, err := devicestate.SeedRefreshTasks(s.state, []string{s.state.NewTask("fake-download", "...").ID()}, nil) + seedTS, added, err := devicestate.SeedRefreshTasks(s.state, dctx, []snapstate.SeedRefreshCandidate{ + { + InstanceName: "snap-1", + SnapSetupTaskIDs: []string{tSnap.ID()}, + }, + }) c.Assert(err, IsNil) + c.Assert(added, DeepEquals, map[string]bool{"snap-1": true}) c.Assert(seedTS.Remove, HasLen, 2) c.Check(seedTS.Remove[0].WaitTasks(), DeepEquals, []*state.Task{seedTS.Finalize}) c.Check(seedTS.Remove[1].WaitTasks(), DeepEquals, []*state.Task{seedTS.Finalize}) @@ -2230,27 +2257,142 @@ func (s *deviceMgrSuite) TestCreateSeedRefreshTasksAddsPruneTasks(c *C) { c.Check(labels, testutil.DeepUnsortedMatches, []string{"old-seed-refresh-1", "old-seed-refresh-2"}) } -func (s *deviceMgrSuite) TestAppendSeedRefreshSetupTaskIDs(c *C) { +func (s *deviceMgrSuite) TestUpdateSeedRefreshChange(c *C) { s.state.Lock() defer s.state.Unlock() + dctx := s.seedRefreshDeviceContext(c, "snap-1", "snap-2") + chg := s.state.NewChange("seed-refresh", "...") + snap1Task := s.state.NewTask("fake-download", "...") + snap2Task := s.state.NewTask("fake-download", "...") + snap3Task := s.state.NewTask("fake-download", "...") create := s.state.NewTask("create-recovery-system", "...") create.Set("recovery-system-setup", &devicestate.RecoverySystemSetup{ Label: "20260227", Directory: filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", "20260227"), - SnapSetupTasks: []string{"snap-1"}, + SnapSetupTasks: []string{snap1Task.ID()}, ComponentSetupTasks: []string{"comp-1"}, }) + finalize := s.state.NewTask("finalize-recovery-system", "...") + finalize.Set("recovery-system-setup-task", create.ID()) + remove := s.state.NewTask("remove-recovery-system", "...") + remove.WaitFor(finalize) + chg.AddTask(create) + chg.AddTask(finalize) + chg.AddTask(remove) + + seedTS, err := devicestate.UpdateSeedRefreshChange(chg, dctx, snapstate.SeedRefreshCandidate{ + InstanceName: "snap-2", + SnapSetupTaskIDs: []string{snap2Task.ID()}, + ComponentSetupTaskIDs: []string{"comp-2", "comp-1"}, + }) + c.Assert(err, IsNil) + c.Assert(seedTS, NotNil) + c.Check(seedTS.Create, Equals, create) + c.Check(seedTS.Finalize, Equals, finalize) + c.Check(seedTS.Remove, DeepEquals, []*state.Task{remove}) + + seedTS, err = devicestate.UpdateSeedRefreshChange(chg, dctx, snapstate.SeedRefreshCandidate{ + InstanceName: "snap-1", + SnapSetupTaskIDs: []string{snap1Task.ID()}, + ComponentSetupTaskIDs: []string{"comp-3"}, + }) + c.Assert(err, IsNil) + c.Assert(seedTS, NotNil) - c.Assert(devicestate.AppendSeedRefreshSetupTaskIDs(create, "snap-2", []string{"comp-2", "comp-1"}), IsNil) - c.Assert(devicestate.AppendSeedRefreshSetupTaskIDs(create, "snap-1", []string{"comp-3"}), IsNil) + seedTS, err = devicestate.UpdateSeedRefreshChange(chg, dctx, snapstate.SeedRefreshCandidate{ + InstanceName: "snap-3", + SnapSetupTaskIDs: []string{snap3Task.ID()}, + ComponentSetupTaskIDs: []string{"comp-4"}, + }) + c.Assert(err, IsNil) + c.Check(seedTS, IsNil) var setup devicestate.RecoverySystemSetup c.Assert(create.Get("recovery-system-setup", &setup), IsNil) - c.Check(setup.SnapSetupTasks, DeepEquals, []string{"snap-1", "snap-2"}) + c.Check(setup.SnapSetupTasks, DeepEquals, []string{snap1Task.ID(), snap2Task.ID()}) c.Check(setup.ComponentSetupTasks, DeepEquals, []string{"comp-1", "comp-2", "comp-3"}) } +func (s *deviceMgrSuite) TestUpdateSeedRefreshChangeUsesPendingSeedRefreshTasks(c *C) { + s.state.Lock() + defer s.state.Unlock() + + dctx := s.seedRefreshDeviceContext(c, "snap-1", "snap-2") + chg := s.state.NewChange("seed-refresh", "...") + + oldSnapTask := s.state.NewTask("fake-download", "...") + oldCreate := s.state.NewTask("create-recovery-system", "...") + oldCreate.Set("recovery-system-setup", &devicestate.RecoverySystemSetup{ + Label: "20260226", + Directory: filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", "20260226"), + SnapSetupTasks: []string{oldSnapTask.ID()}, + }) + oldFinalize := s.state.NewTask("finalize-recovery-system", "...") + oldFinalize.Set("recovery-system-setup-task", oldCreate.ID()) + oldRemove := s.state.NewTask("remove-recovery-system", "...") + oldRemove.WaitFor(oldFinalize) + oldCreate.SetStatus(state.DoneStatus) + oldFinalize.SetStatus(state.DoneStatus) + oldRemove.SetStatus(state.DoneStatus) + + currentSnapTask := s.state.NewTask("fake-download", "...") + currentCreate := s.state.NewTask("create-recovery-system", "...") + currentCreate.Set("recovery-system-setup", &devicestate.RecoverySystemSetup{ + Label: "20260227", + Directory: filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", "20260227"), + SnapSetupTasks: []string{currentSnapTask.ID()}, + }) + currentFinalize := s.state.NewTask("finalize-recovery-system", "...") + currentFinalize.Set("recovery-system-setup-task", currentCreate.ID()) + currentRemove := s.state.NewTask("remove-recovery-system", "...") + currentRemove.WaitFor(currentFinalize) + + chg.AddTask(oldCreate) + chg.AddTask(oldFinalize) + chg.AddTask(oldRemove) + chg.AddTask(currentCreate) + chg.AddTask(currentFinalize) + chg.AddTask(currentRemove) + + nextSnapTask := s.state.NewTask("fake-download", "...") + seedTS, err := devicestate.UpdateSeedRefreshChange(chg, dctx, snapstate.SeedRefreshCandidate{ + InstanceName: "snap-2", + SnapSetupTaskIDs: []string{nextSnapTask.ID()}, + }) + c.Assert(err, IsNil) + c.Assert(seedTS, NotNil) + c.Check(seedTS.Create, Equals, currentCreate) + c.Check(seedTS.Finalize, Equals, currentFinalize) + c.Check(seedTS.Remove, DeepEquals, []*state.Task{currentRemove}) + + var oldSetup devicestate.RecoverySystemSetup + c.Assert(oldCreate.Get("recovery-system-setup", &oldSetup), IsNil) + c.Check(oldSetup.SnapSetupTasks, DeepEquals, []string{oldSnapTask.ID()}) + + var currentSetup devicestate.RecoverySystemSetup + c.Assert(currentCreate.Get("recovery-system-setup", ¤tSetup), IsNil) + c.Check(currentSetup.SnapSetupTasks, DeepEquals, []string{currentSnapTask.ID(), nextSnapTask.ID()}) +} + +func (s *deviceMgrSuite) seedRefreshDeviceContext(c *C, requiredSnaps ...string) snapstate.DeviceContext { + extras := map[string]any{ + "architecture": "amd64", + "kernel": "pc-kernel", + "gadget": "pc", + } + if len(requiredSnaps) != 0 { + required := make([]any, len(requiredSnaps)) + for i, snapName := range requiredSnaps { + required[i] = snapName + } + extras["required-snaps"] = required + } + + model := s.makeModelAssertionInState(c, "canonical", "pc", extras) + return &snapstatetest.TrivialDeviceContext{DeviceModel: model} +} + func (s *deviceMgrSuite) TestRemoveRecoverySystemBlockedWhenAnotherRemovalRunning(c *C) { s.state.Lock() defer s.state.Unlock() diff --git a/overlord/snapstate/handlers.go b/overlord/snapstate/handlers.go index f1fbd4ee12d..c6e634c731e 100644 --- a/overlord/snapstate/handlers.go +++ b/overlord/snapstate/handlers.go @@ -618,7 +618,7 @@ func updatePrereqIfOutdated(t *state.Task, snapName string, contentAttrs []strin return nil, nil } - if err := maybeMergeLateSeedRefreshPrereq(t.Change(), deviceCtx, snapName, ts); err != nil { + if err := maybeMergeLateSeedRefreshPrereq(t.Change(), deviceCtx, ts); err != nil { return nil, err } diff --git a/overlord/snapstate/reboot_test.go b/overlord/snapstate/reboot_test.go index 8847c4ba26e..18b9b1d14ab 100644 --- a/overlord/snapstate/reboot_test.go +++ b/overlord/snapstate/reboot_test.go @@ -103,53 +103,6 @@ func (s *rebootSuite) snapInstallTaskSetForSnapSetup(snapName, base string, snap ) } -func (s *rebootSuite) componentExclusiveInstallTaskSetForSnapSetup(snapName string, snapType snap.Type) snapstate.SnapInstallTaskSet { - snapsup := &snapstate.SnapSetup{ - SideInfo: &snap.SideInfo{ - RealName: snapName, - SnapID: snapName, - Revision: snap.R(1), - }, - Type: snapType, - ComponentExclusiveOperation: true, - } - - downloadComp := s.state.NewTask("download-component", "...") - - setupSecurity := s.state.NewTask("setup-profiles", "...") - setupSecurity.Set("snap-setup", snapsup) - setupSecurity.WaitFor(downloadComp) - - downloadComp.Set("snap-setup-task", setupSecurity.ID()) - - setupSecurity.Set("component-setup-tasks", []string{downloadComp.ID()}) - - linkComp := s.state.NewTask("link-component", "...") - linkComp.Set("snap-setup-task", setupSecurity.ID()) - linkComp.WaitFor(setupSecurity) - - postLink := s.state.NewTask("run-hook", "...") - postLink.Set("snap-setup-task", setupSecurity.ID()) - postLink.WaitFor(linkComp) - - ts := state.NewTaskSet(downloadComp, setupSecurity, linkComp, postLink) - ts.MarkEdge(downloadComp, snapstate.BeginEdge) - ts.MarkEdge(downloadComp, snapstate.LastBeforeLocalModificationsEdge) - ts.MarkEdge(setupSecurity, snapstate.SnapSetupEdge) - ts.MarkEdge(postLink, snapstate.EndEdge) - - ts.JoinLane(s.state.NewLane()) - - return snapstate.NewSnapInstallTaskSetForTest( - snapsup, - ts, - []*state.Task{downloadComp}, - nil, - []*state.Task{setupSecurity, linkComp}, - []*state.Task{postLink}, - ) -} - func taskSetsFromInstallSets(stss []snapstate.SnapInstallTaskSet) []*state.TaskSet { tss := make([]*state.TaskSet, 0, len(stss)) for _, sts := range stss { @@ -964,11 +917,13 @@ func (s *rebootSuite) TestArrangeSnapInstallTaskSetsNoEarlyDownloads(c *C) { c.Check(firstLocalModTask.WaitTasks(), Not(testutil.Contains), kernelLastBefore) } -func (s *rebootSuite) TestArrangeSnapInstallTaskSetsSeedRefreshComponentExclusive(c *C) { +func (s *rebootSuite) TestArrangeSnapInstallTaskSetsSeedRefreshComponentExclusiveCandidate(c *C) { defer snapstatetest.MockDeviceModel(MakeModel(map[string]any{ "base": "core20", "required-snaps": []any{"some-app"}, }))() + observed, restore := mockSeedRefreshHooks([]string{"some-app"}) + defer restore() s.state.Lock() defer s.state.Unlock() @@ -977,41 +932,54 @@ func (s *rebootSuite) TestArrangeSnapInstallTaskSetsSeedRefreshComponentExclusiv c.Assert(tr.Set("core", "experimental.seed-refresh", true), IsNil) tr.Commit() - oldSeedRefreshTasks := snapstate.SeedRefreshTasks - snapstate.SeedRefreshTasks = func(st *state.State, snapSetupTasks, compSetupTasks []string) (*snapstate.SeedRefreshTaskSet, error) { - create := st.NewTask("create-recovery-system", "...") - restart.MarkTaskAsRestartBoundary(create, restart.RestartBoundaryDirectionDo) - - finalize := st.NewTask("finalize-recovery-system", "...") - finalize.WaitFor(create) - return &snapstate.SeedRefreshTaskSet{ - Create: create, - Finalize: finalize, - }, nil + snapsup := &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: "some-app", + SnapID: "some-app", + Revision: snap.R(1), + }, + Type: snap.TypeApp, + ComponentExclusiveOperation: true, } - defer func() { - snapstate.SeedRefreshTasks = oldSeedRefreshTasks - }() - stss := []snapstate.SnapInstallTaskSet{ - s.componentExclusiveInstallTaskSetForSnapSetup("some-app", snap.TypeApp), - } + downloadComp := s.state.NewTask("download-component", "...") + setupSecurity := s.state.NewTask("setup-profiles", "...") + setupSecurity.Set("snap-setup", snapsup) + setupSecurity.WaitFor(downloadComp) - seedUpdateTS, err := snapstate.ArrangeRebootAndUpdateSeed(s.state, stss, nil, snapstate.Options{DeviceCtx: s.deviceCtx(c)}) - c.Assert(err, IsNil) - c.Assert(seedUpdateTS, NotNil) + componentSetupTasks := []string{downloadComp.ID()} + setupSecurity.Set("component-setup-tasks", componentSetupTasks) - lastBeforeLocalTask, err := stss[0].TaskSet().Edge(snapstate.LastBeforeLocalModificationsEdge) - c.Assert(err, IsNil) - snapEndTask, err := stss[0].TaskSet().Edge(snapstate.EndEdge) - c.Assert(err, IsNil) + linkComp := s.state.NewTask("link-component", "...") + linkComp.WaitFor(setupSecurity) + + postLink := s.state.NewTask("run-hook", "...") + postLink.WaitFor(linkComp) - seedCreate := seedUpdateTS.Tasks()[0] - seedFinalize := seedUpdateTS.Tasks()[len(seedUpdateTS.Tasks())-1] + ts := state.NewTaskSet(downloadComp, setupSecurity, linkComp, postLink) + ts.MarkEdge(downloadComp, snapstate.BeginEdge) + ts.MarkEdge(downloadComp, snapstate.LastBeforeLocalModificationsEdge) + ts.MarkEdge(setupSecurity, snapstate.SnapSetupEdge) + ts.MarkEdge(postLink, snapstate.EndEdge) + ts.JoinLane(s.state.NewLane()) - c.Check(seedCreate.WaitTasks(), testutil.Contains, lastBeforeLocalTask) - c.Check(waitsOnTransitively(seedFinalize, snapEndTask), Equals, true) - c.Check(taskSetLanes(seedUpdateTS), DeepEquals, taskSetLanes(stss[0].TaskSet())) + sts := snapstate.NewSnapInstallTaskSetForTest( + snapsup, + ts, + []*state.Task{downloadComp}, + nil, + []*state.Task{setupSecurity, linkComp}, + []*state.Task{postLink}, + ) + + _, err := snapstate.ArrangeRebootAndUpdateSeed(s.state, []snapstate.SnapInstallTaskSet{sts}, nil, snapstate.Options{DeviceCtx: s.deviceCtx(c)}) + c.Assert(err, IsNil) + + // component-exclusive operations provide only component setup tasks, so snap setup tasks must stay empty. + observed.CheckInitialCandidates(c, snapstate.SeedRefreshCandidate{ + InstanceName: "some-app", + ComponentSetupTaskIDs: componentSetupTasks, + }) } func (s *rebootSuite) TestArrangeSnapInstallTaskSetsSnapd(c *C) { diff --git a/overlord/snapstate/seed.go b/overlord/snapstate/seed.go index eaea7c3e650..e2d36c0755b 100644 --- a/overlord/snapstate/seed.go +++ b/overlord/snapstate/seed.go @@ -35,16 +35,55 @@ type SeedRefreshTaskSet struct { Remove []*state.Task } +// SeedRefreshCandidate carries information about a snap that might trigger a +// seed refresh. +type SeedRefreshCandidate struct { + // InstanceName is the snap's instance name. + InstanceName string + // SnapSetupTaskIDs are the snap tasks that should be considered as inputs to + // recovery system creation. Will be empty for component-only refreshes. + SnapSetupTaskIDs []string + // ComponentSetupTaskIDs are the component tasks that should be considered as + // inputs to recovery system creation. Will be empty for snap-only + // refreshes. + ComponentSetupTaskIDs []string +} + // SeedRefreshTasks is set by devicestate to avoid an import cycle. See // devicestate.SeedRefreshTasks. -var SeedRefreshTasks = func(st *state.State, snapSetupTasks, compSetupTasks []string) (*SeedRefreshTaskSet, error) { +var SeedRefreshTasks = func(st *state.State, dctx DeviceContext, candidates []SeedRefreshCandidate) (*SeedRefreshTaskSet, map[string]bool, error) { panic("internal error: snapstate.SeedRefreshTasks is unset") } -// AppendSeedRefreshSetupTaskIDs is set by devicestate to avoid an import -// cycle. See devicestate.AppendSeedRefreshSetupTaskIDs. -var AppendSeedRefreshSetupTaskIDs = func(create *state.Task, snapSetupTask string, compSetupTasks []string) error { - panic("internal error: snapstate.AppendSeedRefreshSetupTaskIDs is unset") +// UpdateSeedRefreshChange is set by devicestate to avoid an import cycle. See +// devicestate.UpdateSeedRefreshChange. +var UpdateSeedRefreshChange = func(chg *state.Change, dctx DeviceContext, candidate SeedRefreshCandidate) (*SeedRefreshTaskSet, error) { + panic("internal error: snapstate.UpdateSeedRefreshChange is unset") +} + +func seedRefreshCandidateForTaskSet(ts *state.TaskSet) (SeedRefreshCandidate, error) { + t, err := ts.Edge(SnapSetupEdge) + if err != nil { + return SeedRefreshCandidate{}, err + } + + snapsup, err := TaskSnapSetup(t) + if err != nil { + return SeedRefreshCandidate{}, err + } + + candidate := SeedRefreshCandidate{ + InstanceName: snapsup.InstanceName(), + } + if !snapsup.ComponentExclusiveOperation { + candidate.SnapSetupTaskIDs = append(candidate.SnapSetupTaskIDs, t.ID()) + } + + if err := t.Get("component-setup-tasks", &candidate.ComponentSetupTaskIDs); err != nil && !errors.Is(err, state.ErrNoState) { + return SeedRefreshCandidate{}, err + } + + return candidate, nil } // seedRefreshEnabled reports whether the experimental seed-refresh feature is @@ -94,104 +133,69 @@ func seedRefreshAndSeedSnapTaskSets(st *state.State, stss []snapInstallTaskSet, return nil, nil, err } - seedSnapTaskSets := taskSetsForSeedSnaps(stss, deviceCtx) - - // none of the seed snaps are being updated, in that case there isn't - // anything to do. - if len(seedSnapTaskSets) == 0 { - return nil, nil, nil + candidates := make([]SeedRefreshCandidate, 0, len(stss)) + for _, sts := range stss { + candidate, err := seedRefreshCandidateForTaskSet(sts.ts) + if err != nil { + return nil, nil, err + } + candidates = append(candidates, candidate) } - snapsupIDs, compsupIDs, err := setupTaskIDsForSeedCreation(seedSnapTaskSets) + seedTS, added, err := SeedRefreshTasks(st, deviceCtx, candidates) if err != nil { return nil, nil, err } - - seedTS, err := SeedRefreshTasks(st, snapsupIDs, compsupIDs) - if err != nil { - return nil, nil, err + if len(added) == 0 { + return nil, nil, nil } - return seedTS, seedSnapTaskSets, nil -} - -// setupTaskIDsForSeedCreation collects the snap and component setup task IDs -// that seed creation should consume. -func setupTaskIDsForSeedCreation(seedSnapUpdates map[string]snapInstallTaskSet) (snapsupIDs, compsupIDs []string, err error) { - for _, sts := range seedSnapUpdates { - t, err := sts.ts.Edge(SnapSetupEdge) - if err != nil { - return nil, nil, err - } - - snapsup, err := TaskSnapSetup(t) - if err != nil { - return nil, nil, err - } - - if !snapsup.ComponentExclusiveOperation { - snapsupIDs = append(snapsupIDs, t.ID()) - } - - var compsups []string - if err := t.Get("component-setup-tasks", &compsups); err != nil && !errors.Is(err, state.ErrNoState) { - return nil, nil, err + seedSnapTaskSets := make(map[string]snapInstallTaskSet, len(added)) + for _, sts := range stss { + if added[sts.snapsup.InstanceName()] { + seedSnapTaskSets[sts.snapsup.InstanceName()] = sts } - - compsupIDs = append(compsupIDs, compsups...) } - return snapsupIDs, compsupIDs, nil + return seedTS, seedSnapTaskSets, nil } // maybeMergeLateSeedRefreshPrereq folds a prerequisite refresh into an // in-flight seed refresh when the current change still has pending // recovery-system tasks and the prerequisite snap is part of the model. -func maybeMergeLateSeedRefreshPrereq(chg *state.Change, dctx DeviceContext, snapName string, providerTS *state.TaskSet) error { +func maybeMergeLateSeedRefreshPrereq(chg *state.Change, dctx DeviceContext, providerTS *state.TaskSet) error { if !changeHasPendingSeedRefresh(chg) { return nil } - // TODO:SEEDREFRESH: consider the intersections of snaps in the model and - // snaps currently present in the seed, not all snaps in the model - for _, sn := range dctx.Model().AllSnaps() { - if snapName != sn.SnapName() { - continue - } - - // TODO:SEEDREFRESH: drop this check - if err := errorIfPrereqNeedsInFlightBaseBlockedBySeedCreation(chg, providerTS); err != nil { - return err - } - - return mergeLateSeedRefreshPrereq(chg, providerTS) + candidate, err := seedRefreshCandidateForTaskSet(providerTS) + if err != nil { + return err } - return nil -} + seedTS, err := UpdateSeedRefreshChange(chg, dctx, candidate) + if err != nil { + return err + } -func findRecoverySystemTasks(chg *state.Change) (create, finalize *state.Task, err error) { - for _, t := range chg.Tasks() { - switch t.Kind() { - case "create-recovery-system": - create = t - case "finalize-recovery-system": - finalize = t - } + // snap didn't trigger a seed refresh + if seedTS == nil { + return nil } - if create == nil || finalize == nil { - return nil, nil, errors.New("internal error: seed-refresh change is missing recovery-system tasks") + // TODO:SEEDREFRESH: drop this check + if err := errorIfPrereqNeedsInFlightBaseBlockedBySeedCreation(chg, seedTS, providerTS); err != nil { + return err } - return create, finalize, nil + return mergeLateSeedRefreshPrereq(seedTS, providerTS) } // errorIfPrereqNeedsInFlightBaseBlockedBySeedCreation rejects the currently // unsupported case where a prerequisite refresh depends on a base refresh whose // link-snap is ordered after create-recovery-system. Without extra // synchronization, the prerequisite refresh would wait forever on that base. -func errorIfPrereqNeedsInFlightBaseBlockedBySeedCreation(chg *state.Change, providerTS *state.TaskSet) error { +func errorIfPrereqNeedsInFlightBaseBlockedBySeedCreation(chg *state.Change, seedTS *SeedRefreshTaskSet, providerTS *state.TaskSet) error { snapsupTask, err := providerTS.Edge(SnapSetupEdge) if err != nil { return errors.New("internal error: seed-refresh provider task set is missing required edge") @@ -210,16 +214,11 @@ func errorIfPrereqNeedsInFlightBaseBlockedBySeedCreation(chg *state.Change, prov base = defaultCoreSnapName } - create, _, err := findRecoverySystemTasks(chg) - if err != nil { - return err - } - baseLink, err := maybeFindTaskInChangeForSnap(chg, "link-snap", base) if err != nil { return err } - if baseLink == nil || !willWaitOn(baseLink, create) { + if baseLink == nil || !willWaitOn(baseLink, seedTS.Create) { return nil } @@ -231,31 +230,18 @@ func errorIfPrereqNeedsInFlightBaseBlockedBySeedCreation(chg *state.Change, prov return fmt.Errorf("cannot automatically update prerequisite %q during seed-refresh while base %q waits for create-recovery-system", snapsup.InstanceName(), base) } -// mergeLateSeedRefreshPrereq folds a prerequisite refresh into the existing -// seed-refresh task graph by adding its setup tasks to create-recovery-system, -// joining its task set to the seed-refresh lanes, and ensuring seed creation -// tasks depend on the prerequisite refresh tasks. -func mergeLateSeedRefreshPrereq(chg *state.Change, providerTS *state.TaskSet) error { - create, finalize, err := findRecoverySystemTasks(chg) - if err != nil { - return err - } - - snapsup, err := providerTS.Edge(SnapSetupEdge) +// mergeLateSeedRefreshPrereq folds a prerequisite refresh selected by +// devicestate.UpdateSeedRefreshChange into the existing seed-refresh task +// graph. At this point, devicestate has already updated the recovery-system +// setup payload. this function only joins lanes and adds the ordering +// dependencies needed by seed refresh. +func mergeLateSeedRefreshPrereq(seedTS *SeedRefreshTaskSet, providerTS *state.TaskSet) error { + _, err := providerTS.Edge(SnapSetupEdge) if err != nil { return errors.New("internal error: seed-refresh provider task set is missing required edge") } - var compsups []string - if err := snapsup.Get("component-setup-tasks", &compsups); err != nil && !errors.Is(err, state.ErrNoState) { - return err - } - - if err := AppendSeedRefreshSetupTaskIDs(create, snapsup.ID(), compsups); err != nil { - return err - } - - for _, lane := range create.Lanes() { + for _, lane := range seedTS.Create.Lanes() { providerTS.JoinLane(lane) } @@ -274,32 +260,8 @@ func mergeLateSeedRefreshPrereq(chg *state.Change, providerTS *state.TaskSet) er // reboots that would interfere with the original single-reboot // orchestration. however, this is not a uniquely seed-refresh problem. - waitForIfNeeded(create, lastBeforeLocal) - waitForIfNeeded(finalize, end) + waitForIfNeeded(seedTS.Create, lastBeforeLocal) + waitForIfNeeded(seedTS.Finalize, end) return nil } - -// taskSetsForSeedSnaps returns the selected refresh task sets keyed by -// snap name for snaps present in the model. -func taskSetsForSeedSnaps(stss []snapInstallTaskSet, dctx DeviceContext) map[string]snapInstallTaskSet { - // TODO:SEEDREFRESH: consider the intersections of snaps in the model and - // snaps currently present in the seed, not all snaps in the model - seedSnaps := make(map[string]bool) - for _, sn := range dctx.Model().AllSnaps() { - seedSnaps[sn.SnapName()] = true - } - - // some models have an implicit snapd, make sure that we account for it here - seedSnaps["snapd"] = true - - seedUpdates := make(map[string]snapInstallTaskSet, len(seedSnaps)) - for _, sts := range stss { - name := sts.snapsup.InstanceName() - if seedSnaps[name] { - seedUpdates[name] = sts - } - } - - return seedUpdates -} diff --git a/overlord/snapstate/snapstate_test.go b/overlord/snapstate/snapstate_test.go index dfdb38a8243..b63107f1e08 100644 --- a/overlord/snapstate/snapstate_test.go +++ b/overlord/snapstate/snapstate_test.go @@ -80,6 +80,71 @@ import ( func TestSnapManager(t *testing.T) { TestingT(t) } +type observedSeedRefreshCandidates struct { + initial [][]snapstate.SeedRefreshCandidate + prerequisites []snapstate.SeedRefreshCandidate +} + +func mockSeedRefreshHooks(triggers []string) (*observedSeedRefreshCandidates, func()) { + oldSeedRefreshTasks := snapstate.SeedRefreshTasks + oldUpdateSeedRefreshChange := snapstate.UpdateSeedRefreshChange + triggered := make(map[string]bool, len(triggers)) + for _, instanceName := range triggers { + triggered[instanceName] = true + } + + var observed observedSeedRefreshCandidates + var currentSeedTS *snapstate.SeedRefreshTaskSet + + snapstate.SeedRefreshTasks = func(st *state.State, _ snapstate.DeviceContext, candidates []snapstate.SeedRefreshCandidate) (*snapstate.SeedRefreshTaskSet, map[string]bool, error) { + observed.initial = append(observed.initial, candidates) + + added := make(map[string]bool, len(candidates)) + for _, candidate := range candidates { + if !triggered[candidate.InstanceName] { + continue + } + + added[candidate.InstanceName] = true + } + if len(added) == 0 { + return nil, nil, nil + } + + create := st.NewTask("create-recovery-system", "Create recovery system") + restart.MarkTaskAsRestartBoundary(create, restart.RestartBoundaryDirectionDo) + + finalize := st.NewTask("finalize-recovery-system", "Finalize recovery system") + finalize.WaitFor(create) + finalize.Set("recovery-system-setup-task", create.ID()) + + currentSeedTS = &snapstate.SeedRefreshTaskSet{ + Create: create, + Finalize: finalize, + } + return currentSeedTS, added, nil + } + + snapstate.UpdateSeedRefreshChange = func(chg *state.Change, _ snapstate.DeviceContext, candidate snapstate.SeedRefreshCandidate) (*snapstate.SeedRefreshTaskSet, error) { + observed.prerequisites = append(observed.prerequisites, candidate) + + if !triggered[candidate.InstanceName] { + return nil, nil + } + + if currentSeedTS == nil { + return nil, fmt.Errorf("missing recovery-system tasks") + } + + return currentSeedTS, nil + } + + return &observed, func() { + snapstate.SeedRefreshTasks = oldSeedRefreshTasks + snapstate.UpdateSeedRefreshChange = oldUpdateSeedRefreshChange + } +} + type snapmgrBaseTest struct { testutil.BaseTest o *overlord.Overlord @@ -227,8 +292,6 @@ func (s *snapmgrBaseTest) SetUpTest(c *C) { oldSetupRemoveHook := snapstate.SetupRemoveHook oldSnapServiceOptions := snapstate.SnapServiceOptions oldEnsureSnapAbsentFromQuotaGroup := snapstate.EnsureSnapAbsentFromQuotaGroup - oldCreateRecoverySystemTasks := snapstate.SeedRefreshTasks - oldAppendSeedRefreshSetupTaskIDs := snapstate.AppendSeedRefreshSetupTaskIDs snapstate.SetupInstallHook = hookstate.SetupInstallHook snapstate.SetupInstallComponentHook = hookstate.SetupInstallComponentHook snapstate.SetupPostRefreshComponentHook = hookstate.SetupPostRefreshComponentHook @@ -238,37 +301,10 @@ func (s *snapmgrBaseTest) SetUpTest(c *C) { snapstate.SetupRemoveHook = hookstate.SetupRemoveHook snapstate.SnapServiceOptions = servicestate.SnapServiceOptions snapstate.EnsureSnapAbsentFromQuotaGroup = servicestate.EnsureSnapAbsentFromQuota - snapstate.SeedRefreshTasks = func(st *state.State, snapSetupTasks, compSetupTasks []string) (*snapstate.SeedRefreshTaskSet, error) { - create := st.NewTask("create-recovery-system", "Create recovery system") - create.Set("recovery-system-setup", map[string]any{ - "snap-setup-tasks": snapSetupTasks, - "component-setup-tasks": compSetupTasks, - }) - - restart.MarkTaskAsRestartBoundary(create, restart.RestartBoundaryDirectionDo) - - finalize := st.NewTask("finalize-recovery-system", "Finalize recovery system") - finalize.WaitFor(create) - finalize.Set("recovery-system-setup-task", create.ID()) - - return &snapstate.SeedRefreshTaskSet{ - Create: create, - Finalize: finalize, - }, nil - } - snapstate.AppendSeedRefreshSetupTaskIDs = func(create *state.Task, snapSetupTask string, compSetupTasks []string) error { - var setup map[string][]string - if err := create.Get("recovery-system-setup", &setup); err != nil { - return err - } - - setup["snap-setup-tasks"] = append(setup["snap-setup-tasks"], snapSetupTask) - setup["component-setup-tasks"] = append(setup["component-setup-tasks"], compSetupTasks...) - create.Set("recovery-system-setup", setup) - return nil - } + _, restore := mockSeedRefreshHooks(nil) + s.AddCleanup(restore) - restore := snapstate.MockEnforcedValidationSets(func(st *state.State, extraVss ...*asserts.ValidationSet) (*snapasserts.ValidationSets, error) { + restore = snapstate.MockEnforcedValidationSets(func(st *state.State, extraVss ...*asserts.ValidationSet) (*snapasserts.ValidationSets, error) { return snapasserts.NewValidationSets(), nil }) s.AddCleanup(restore) @@ -313,8 +349,6 @@ func (s *snapmgrBaseTest) SetUpTest(c *C) { snapstate.SetupRemoveHook = oldSetupRemoveHook snapstate.SnapServiceOptions = oldSnapServiceOptions snapstate.EnsureSnapAbsentFromQuotaGroup = oldEnsureSnapAbsentFromQuotaGroup - snapstate.SeedRefreshTasks = oldCreateRecoverySystemTasks - snapstate.AppendSeedRefreshSetupTaskIDs = oldAppendSeedRefreshSetupTaskIDs dirs.SetRootDir("/") }) diff --git a/overlord/snapstate/snapstate_update_test.go b/overlord/snapstate/snapstate_update_test.go index b4f8b6df900..0a2eb94c919 100644 --- a/overlord/snapstate/snapstate_update_test.go +++ b/overlord/snapstate/snapstate_update_test.go @@ -166,11 +166,6 @@ func findSeedRefreshTaskSet(tss []*state.TaskSet) *state.TaskSet { return nil } -type recoverySystemSetupForTest struct { - SnapSetupTasks []string `json:"snap-setup-tasks,omitempty"` - ComponentSetupTasks []string `json:"component-setup-tasks,omitempty"` -} - func hasDoRestartBoundary(task *state.Task) bool { var boundary restart.RestartBoundaryDirection if err := task.Get("restart-boundary", &boundary); err != nil { @@ -188,10 +183,12 @@ type seedRefreshSnap struct { revision snap.Revision } -func (s *snapmgrTestSuite) setupSeedRefreshUpdateTest(c *C, classic, enabled bool, model map[string]any) (restore func()) { +func (s *snapmgrTestSuite) setupSeedRefreshUpdateTest(c *C, classic, enabled bool, model map[string]any, triggers []string) (observed *observedSeedRefreshCandidates, restore func()) { + observed, restoreSeedRefreshHooks := mockSeedRefreshHooks(triggers) restores := []func(){ release.MockOnClassic(classic), snapstatetest.MockDeviceModel(MakeModel(model)), + restoreSeedRefreshHooks, } if enabled { @@ -202,7 +199,7 @@ func (s *snapmgrTestSuite) setupSeedRefreshUpdateTest(c *C, classic, enabled boo s.state.Unlock() } - return func() { + return observed, func() { for i := len(restores) - 1; i >= 0; i-- { restores[i]() } @@ -287,6 +284,38 @@ func parseSeedRefreshTaskSets(uts *snapstate.UpdateTaskSets) (map[string]*state. return taskSetsBySnap, findSeedRefreshTaskSet(uts.Refresh) } +func seedRefreshCandidateFromTaskSet(c *C, ts *state.TaskSet) snapstate.SeedRefreshCandidate { + t, err := ts.Edge(snapstate.SnapSetupEdge) + c.Assert(err, IsNil) + + snapsup, err := snapstate.TaskSnapSetup(t) + c.Assert(err, IsNil) + + candidate := snapstate.SeedRefreshCandidate{ + InstanceName: snapsup.InstanceName(), + } + if !snapsup.ComponentExclusiveOperation { + candidate.SnapSetupTaskIDs = append(candidate.SnapSetupTaskIDs, t.ID()) + } + + if err := t.Get("component-setup-tasks", &candidate.ComponentSetupTaskIDs); err != nil && !errors.Is(err, state.ErrNoState) { + c.Assert(err, IsNil) + } + + return candidate +} + +func (observed *observedSeedRefreshCandidates) CheckInitialCandidates(c *C, expected ...snapstate.SeedRefreshCandidate) { + // we expect just one set of initial candidates, since we should only call + // [snapstate.SeedRefreshTasks] just once + c.Assert(observed.initial, HasLen, 1) + c.Check(observed.initial[0], testutil.DeepUnsortedMatches, expected) +} + +func (observed *observedSeedRefreshCandidates) CheckPrereqCandidates(c *C, expected ...snapstate.SeedRefreshCandidate) { + c.Check(observed.prerequisites, testutil.DeepUnsortedMatches, expected) +} + func mustTaskSetForSnap(c *C, taskSetsBySnap map[string]*state.TaskSet, snapName string) *state.TaskSet { ts := taskSetsBySnap[snapName] c.Assert(ts, NotNil, Commentf("missing task set for %q", snapName)) @@ -19990,108 +20019,40 @@ func (s *snapmgrTestSuite) TestStopSnapServicesComputesRemovedServices(c *C) { }) } -// TODO: switch this test to the newer seed-refresh helpers func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefresh(c *C) { - restore := release.MockOnClassic(false) - defer restore() - restore = snapstate.MockRevisionDate(nil) + restore := snapstate.MockRevisionDate(nil) defer restore() - - s.state.Lock() - defer s.state.Unlock() - - tr := config.NewTransaction(s.state) - c.Assert(tr.Set("core", "experimental.seed-refresh", true), IsNil) - tr.Commit() - - restore = snapstatetest.MockDeviceModel(MakeModel(map[string]any{ + observed, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ "kernel": "kernel", "base": "core18", - })) + }, []string{"kernel", "core18"}) defer restore() - kernel := snap.SideInfo{ - RealName: "kernel", - Revision: snap.R(7), - SnapID: "kernel-id", - } - base := snap.SideInfo{ - RealName: "core18", - Revision: snap.R(7), - SnapID: "core18-snap-id", - } - app := snap.SideInfo{ - RealName: "some-app", - Revision: snap.R(7), - SnapID: "some-app-id", - } - - types := map[string]string{ - "kernel": "kernel", - "core18": "base", - "some-app": "app", - } - - snaptest.MockSnap(c, fmt.Sprintf("name: %s", kernel.RealName), &kernel) - snaptest.MockSnap(c, fmt.Sprintf("name: %s\nbase: core18", base.RealName), &base) - snaptest.MockSnap(c, fmt.Sprintf("name: %s\nbase: core18", app.RealName), &app) + s.state.Lock() + defer s.state.Unlock() - for _, si := range []snap.SideInfo{kernel, base, app} { - si := si - s.fakeStore.registerID(si.RealName, si.SnapID) - snapstate.Set(s.state, si.RealName, &snapstate.SnapState{ - Active: true, - Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}), - Current: si.Revision, - TrackingChannel: "latest/stable", - SnapType: types[si.RealName], - }) - } + s.installSeedRefreshSnaps(c, + seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"}, + seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"}, + seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"}, + ) - updates := []snapstate.StoreUpdate{ + uts, _ := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{ {InstanceName: "kernel"}, {InstanceName: "core18"}, {InstanceName: "some-app"}, - } - goal := snapstate.StoreUpdateGoal(updates...) - - affected, uts, err := snapstate.UpdateWithGoal(context.Background(), s.state, goal, nil, snapstate.Options{ - UserID: s.user.ID, - Flags: snapstate.Flags{ - Transaction: client.TransactionPerSnap, - }, }) - c.Assert(err, IsNil) - c.Assert(affected, testutil.DeepUnsortedMatches, []string{"core18", "kernel", "some-app"}) - - chg := s.state.NewChange("refresh", "refresh kernel, base, and app") - var baseTS, kernelTS, appTS *state.TaskSet - for _, ts := range uts.Refresh { - chg.AddAll(ts) - for _, t := range ts.Tasks() { - snapsup, err := snapstate.TaskSnapSetup(t) - if err != nil { - continue - } - - switch snapsup.Type { - case snap.TypeKernel: - kernelTS = ts - case snap.TypeBase: - baseTS = ts - case snap.TypeApp: - appTS = ts - } + taskSetsBySnap, seedTS := parseSeedRefreshTaskSets(uts) - break - } - } - c.Check(chg.CheckTaskDependencies(), IsNil) - c.Assert(baseTS, NotNil) - c.Assert(kernelTS, NotNil) - c.Assert(appTS, NotNil) + baseTS := mustTaskSetForSnap(c, taskSetsBySnap, "core18") + kernelTS := mustTaskSetForSnap(c, taskSetsBySnap, "kernel") + appTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-app") + observed.CheckInitialCandidates(c, + seedRefreshCandidateFromTaskSet(c, kernelTS), + seedRefreshCandidateFromTaskSet(c, baseTS), + seedRefreshCandidateFromTaskSet(c, appTS), + ) - seedTS := findSeedRefreshTaskSet(uts.Refresh) c.Assert(seedTS, NotNil) c.Check(taskSetsShareLane(baseTS, kernelTS), Equals, true) c.Check(taskSetsShareLane(baseTS, appTS), Equals, false) @@ -20104,14 +20065,6 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefresh(c *C) { lastBeforeLocalKernel, err := kernelTS.Edge(snapstate.LastBeforeLocalModificationsEdge) c.Assert(err, IsNil) - var seedSetup recoverySystemSetupForTest - c.Assert(seedCreate.Get("recovery-system-setup", &seedSetup), IsNil) - for _, ts := range []*state.TaskSet{baseTS, kernelTS} { - t, err := ts.Edge(snapstate.SnapSetupEdge) - c.Assert(err, IsNil) - c.Check(seedSetup.SnapSetupTasks, testutil.Contains, t.ID()) - } - for _, t := range []*state.Task{lastBeforeLocalBase, lastBeforeLocalKernel} { c.Check(waitsOnTransitively(seedCreate, t), Equals, true) } @@ -20141,119 +20094,45 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefresh(c *C) { } } -// TODO: switch this test to the newer seed-refresh helpers func (s *snapmgrTestSuite) testUpdateWithGoalSeedRefreshEarlyDownloadModelSnap(c *C, classic bool) { - restore := release.MockOnClassic(classic) - defer restore() - restore = snapstate.MockRevisionDate(nil) + restore := snapstate.MockRevisionDate(nil) defer restore() - - s.state.Lock() - defer s.state.Unlock() - - tr := config.NewTransaction(s.state) - c.Assert(tr.Set("core", "experimental.seed-refresh", true), IsNil) - tr.Commit() - - restore = snapstatetest.MockDeviceModel(MakeModel(map[string]any{ + observed, restore := s.setupSeedRefreshUpdateTest(c, classic, true, map[string]any{ "kernel": "kernel", "base": "core18", "required-snaps": []any{"some-app"}, - })) + }, []string{"kernel", "core18", "some-app"}) defer restore() - kernel := snap.SideInfo{ - RealName: "kernel", - Revision: snap.R(7), - SnapID: "kernel-id", - } - base := snap.SideInfo{ - RealName: "core18", - Revision: snap.R(7), - SnapID: "core18-snap-id", - } - app := snap.SideInfo{ - RealName: "some-app", - Revision: snap.R(7), - SnapID: "some-app-id", - } - extraApp := snap.SideInfo{ - RealName: "some-other-snap", - Revision: snap.R(7), - SnapID: "some-other-snap-id", - } - - types := map[string]string{ - "kernel": "kernel", - "core18": "base", - "some-app": "app", - "some-other-snap": "app", - } - - snaptest.MockSnap(c, fmt.Sprintf("name: %s", kernel.RealName), &kernel) - snaptest.MockSnap(c, fmt.Sprintf("name: %s\nbase: core18", base.RealName), &base) - snaptest.MockSnap(c, fmt.Sprintf("name: %s\nbase: core18", app.RealName), &app) - snaptest.MockSnap(c, fmt.Sprintf("name: %s\nbase: core18", extraApp.RealName), &extraApp) + s.state.Lock() + defer s.state.Unlock() - for _, si := range []snap.SideInfo{kernel, base, app, extraApp} { - si := si - s.fakeStore.registerID(si.RealName, si.SnapID) - snapstate.Set(s.state, si.RealName, &snapstate.SnapState{ - Active: true, - Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}), - Current: si.Revision, - TrackingChannel: "latest/stable", - SnapType: types[si.RealName], - }) - } + s.installSeedRefreshSnaps(c, + seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"}, + seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"}, + seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"}, + seedRefreshSnap{name: "some-other-snap", snapID: "some-other-snap-id", snapType: "app", base: "core18"}, + ) - updates := []snapstate.StoreUpdate{ + uts, _ := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{ {InstanceName: "kernel"}, {InstanceName: "core18"}, {InstanceName: "some-app"}, {InstanceName: "some-other-snap"}, - } - goal := snapstate.StoreUpdateGoal(updates...) - - affected, uts, err := snapstate.UpdateWithGoal(context.Background(), s.state, goal, nil, snapstate.Options{ - UserID: s.user.ID, - Flags: snapstate.Flags{ - Transaction: client.TransactionPerSnap, - }, }) - c.Assert(err, IsNil) - c.Assert(affected, testutil.DeepUnsortedMatches, []string{"core18", "kernel", "some-app", "some-other-snap"}) - - chg := s.state.NewChange("refresh", "refresh kernel, base, and app") - var baseTS, kernelTS, appTS, extraAppTS *state.TaskSet - for _, ts := range uts.Refresh { - chg.AddAll(ts) - for _, t := range ts.Tasks() { - snapsup, err := snapstate.TaskSnapSetup(t) - if err != nil { - continue - } - - switch snapsup.InstanceName() { - case "kernel": - kernelTS = ts - case "core18": - baseTS = ts - case "some-app": - appTS = ts - case "some-other-snap": - extraAppTS = ts - } + taskSetsBySnap, seedTS := parseSeedRefreshTaskSets(uts) - break - } - } - c.Check(chg.CheckTaskDependencies(), IsNil) - if baseTS == nil || kernelTS == nil || appTS == nil || extraAppTS == nil { - c.Fatalf("missing task sets: base=%v kernel=%v app=%v extraApp=%v", baseTS != nil, kernelTS != nil, appTS != nil, extraAppTS != nil) - } + baseTS := mustTaskSetForSnap(c, taskSetsBySnap, "core18") + kernelTS := mustTaskSetForSnap(c, taskSetsBySnap, "kernel") + appTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-app") + extraAppTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-other-snap") + observed.CheckInitialCandidates(c, + seedRefreshCandidateFromTaskSet(c, kernelTS), + seedRefreshCandidateFromTaskSet(c, baseTS), + seedRefreshCandidateFromTaskSet(c, appTS), + seedRefreshCandidateFromTaskSet(c, extraAppTS), + ) - seedTS := findSeedRefreshTaskSet(uts.Refresh) c.Assert(seedTS, NotNil) c.Check(taskSetsShareLane(baseTS, kernelTS, appTS), Equals, true) c.Check(taskSetsShareLane(baseTS, extraAppTS), Equals, false) @@ -20261,14 +20140,6 @@ func (s *snapmgrTestSuite) testUpdateWithGoalSeedRefreshEarlyDownloadModelSnap(c seedCreate, seedEnd, _ := splitSeedRefreshTasks(c, seedTS) - var seedSetup recoverySystemSetupForTest - c.Assert(seedCreate.Get("recovery-system-setup", &seedSetup), IsNil) - for _, ts := range []*state.TaskSet{baseTS, kernelTS, appTS} { - t, err := ts.Edge(snapstate.SnapSetupEdge) - c.Assert(err, IsNil) - c.Check(seedSetup.SnapSetupTasks, testutil.Contains, t.ID()) - } - kernelLinkTask, err := kernelTS.Edge(snapstate.MaybeRebootEdge) c.Assert(err, IsNil) c.Check(kernelLinkTask.HaltTasks(), testutil.Contains, seedCreate) @@ -20326,11 +20197,11 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshEarlyDownloadModelSnapOn } func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshBaseAndModelSnapRun(c *C) { - restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ + _, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ "kernel": "kernel", "base": "core18", "required-snaps": []any{"some-snap-with-core18-base"}, - }) + }, []string{"core18", "some-snap-with-core18-base"}) defer restore() s.state.Lock() @@ -20367,11 +20238,11 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshBaseAndModelSnapRun(c *C } func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshPrerequisitesUpdatesModelSnap(c *C) { - restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ + observed, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ "kernel": "kernel", "base": "core18", "required-snaps": []any{"content-provider", "some-app"}, - }) + }, []string{"content-provider", "some-app"}) defer restore() s.state.Lock() @@ -20422,19 +20293,19 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshPrerequisitesUpdatesMode s.settle(c) seedCreate, seedFinalize, _ := splitSeedRefreshTasks(c, seedTS) + c.Assert(observed.prerequisites, HasLen, 1) + c.Assert(observed.prerequisites[0].SnapSetupTaskIDs, HasLen, 1) - var seedSetup recoverySystemSetupForTest - c.Assert(seedCreate.Get("recovery-system-setup", &seedSetup), IsNil) - - providerSetupTaskID := seedSetup.SnapSetupTasks[len(seedSetup.SnapSetupTasks)-1] - providerSnapSetupTask := s.state.Task(providerSetupTaskID) + providerSnapSetupTask := s.state.Task(observed.prerequisites[0].SnapSetupTaskIDs[0]) c.Assert(providerSnapSetupTask, NotNil) providerSnapSetup, err := snapstate.TaskSnapSetup(providerSnapSetupTask) c.Assert(err, IsNil) c.Check(providerSnapSetup.InstanceName(), Equals, "content-provider") - - c.Check(seedSetup.SnapSetupTasks, testutil.Contains, providerSnapSetupTask.ID()) + observed.CheckPrereqCandidates(c, snapstate.SeedRefreshCandidate{ + InstanceName: providerSnapSetup.InstanceName(), + SnapSetupTaskIDs: []string{providerSnapSetupTask.ID()}, + }) // ensure create-recovery-system waits on the LastBeforeLocalModificationsEdge for content-provider c.Check(waitTasksContainKindForSnap(c, seedCreate, "content-provider", "validate-snap"), Equals, true) @@ -20448,11 +20319,11 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshPrerequisitesUpdatesMode } func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshPrerequisitesDoNotMergeWhenSeedRefreshAlreadyReady(c *C) { - restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ + observed, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ "kernel": "kernel", "base": "core18", "required-snaps": []any{"content-provider", "some-app"}, - }) + }, []string{"content-provider", "some-app"}) defer restore() s.state.Lock() @@ -20500,24 +20371,11 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshPrerequisitesDoNotMergeW s.settle(c) - var seedSetup recoverySystemSetupForTest - c.Assert(seedCreate.Get("recovery-system-setup", &seedSetup), IsNil) - - // ensure that the first recovery-system-setup doesn't have the - // content-provider as a source - for _, taskID := range seedSetup.SnapSetupTasks { - task := s.state.Task(taskID) - c.Assert(task, NotNil) - - snapsup, err := snapstate.TaskSnapSetup(task) - c.Assert(err, IsNil) - c.Check(snapsup.InstanceName(), Not(Equals), "content-provider") - } - // ensure that the seed-refresh tasks didn't get dependencies on the // content-provider c.Check(waitTasksContainKindForSnap(c, seedCreate, "content-provider", "validate-snap"), Equals, false) c.Check(waitTasksContainKindForSnap(c, seedFinalize, "content-provider", "run-hook"), Equals, false) + c.Check(observed.prerequisites, HasLen, 0) c.Check(restart.Pending(s.state), Equals, restart.RestartSystem) @@ -20531,11 +20389,11 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshPrerequisitesDoNotMergeW // TODO:SEEDREFRESH: update this test once this scenario is supported func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshPrerequisitesFailsForProviderSwitchingToInFlightNonEssentialBase(c *C) { - restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ + _, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ "kernel": "kernel", "base": "core18", "required-snaps": []any{"content-provider", "some-app"}, - }) + }, []string{"content-provider", "some-app"}) defer restore() s.state.Lock() @@ -20594,11 +20452,11 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshPrerequisitesFailsForPro } func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshAllowsRequestedModelContentProviderRefresh(c *C) { - restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ + _, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ "kernel": "kernel", "base": "core18", "required-snaps": []any{"content-provider", "some-app"}, - }) + }, []string{"content-provider", "some-app"}) defer restore() s.state.Lock() @@ -20657,129 +20515,50 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshAllowsRequestedModelCont c.Assert(chg.IsReady(), Equals, true) } -// TODO: switch this test to the newer seed-refresh helpers func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshEarlyDownloadWithSnapd(c *C) { - restore := release.MockOnClassic(false) - defer restore() - restore = snapstate.MockRevisionDate(nil) + restore := snapstate.MockRevisionDate(nil) defer restore() - - s.state.Lock() - defer s.state.Unlock() - - tr := config.NewTransaction(s.state) - c.Assert(tr.Set("core", "experimental.seed-refresh", true), IsNil) - tr.Commit() - - restore = snapstatetest.MockDeviceModel(MakeModel(map[string]any{ + observed, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ "kernel": "kernel", "base": "core18", "required-snaps": []any{"some-app"}, - })) + }, []string{"snapd", "kernel", "core18", "some-app"}) defer restore() - snapd := snap.SideInfo{ - RealName: "snapd", - Revision: snap.R(7), - SnapID: "snapd-snap-id", - } - kernel := snap.SideInfo{ - RealName: "kernel", - Revision: snap.R(7), - SnapID: "kernel-id", - } - base := snap.SideInfo{ - RealName: "core18", - Revision: snap.R(7), - SnapID: "core18-snap-id", - } - app := snap.SideInfo{ - RealName: "some-app", - Revision: snap.R(7), - SnapID: "some-app-id", - } - extraApp := snap.SideInfo{ - RealName: "some-other-snap", - Revision: snap.R(7), - SnapID: "some-other-snap-id", - } - - types := map[string]string{ - "snapd": "snapd", - "kernel": "kernel", - "core18": "base", - "some-app": "app", - "some-other-snap": "app", - } - - snaptest.MockSnap(c, fmt.Sprintf("name: %s\ntype: snapd", snapd.RealName), &snapd) - snaptest.MockSnap(c, fmt.Sprintf("name: %s", kernel.RealName), &kernel) - snaptest.MockSnap(c, fmt.Sprintf("name: %s\nbase: core18", base.RealName), &base) - snaptest.MockSnap(c, fmt.Sprintf("name: %s\nbase: core18", app.RealName), &app) - snaptest.MockSnap(c, fmt.Sprintf("name: %s\nbase: core18", extraApp.RealName), &extraApp) + s.state.Lock() + defer s.state.Unlock() - for _, si := range []snap.SideInfo{snapd, kernel, base, app, extraApp} { - si := si - s.fakeStore.registerID(si.RealName, si.SnapID) - snapstate.Set(s.state, si.RealName, &snapstate.SnapState{ - Active: true, - Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}), - Current: si.Revision, - TrackingChannel: "latest/stable", - SnapType: types[si.RealName], - }) - } + s.installSeedRefreshSnaps(c, + seedRefreshSnap{name: "snapd", snapID: "snapd-snap-id", snapType: "snapd"}, + seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"}, + seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"}, + seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"}, + seedRefreshSnap{name: "some-other-snap", snapID: "some-other-snap-id", snapType: "app", base: "core18"}, + ) - updates := []snapstate.StoreUpdate{ + uts, _ := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{ {InstanceName: "snapd"}, {InstanceName: "kernel"}, {InstanceName: "core18"}, {InstanceName: "some-app"}, {InstanceName: "some-other-snap"}, - } - goal := snapstate.StoreUpdateGoal(updates...) - - affected, uts, err := snapstate.UpdateWithGoal(context.Background(), s.state, goal, nil, snapstate.Options{ - UserID: s.user.ID, - Flags: snapstate.Flags{ - Transaction: client.TransactionPerSnap, - }, }) - c.Assert(err, IsNil) - c.Assert(affected, testutil.DeepUnsortedMatches, []string{"snapd", "core18", "kernel", "some-app", "some-other-snap"}) - - chg := s.state.NewChange("refresh", "refresh snapd, kernel, base, and apps") - var snapdTS, baseTS, kernelTS, appTS, extraAppTS *state.TaskSet - for _, ts := range uts.Refresh { - chg.AddAll(ts) - for _, t := range ts.Tasks() { - snapsup, err := snapstate.TaskSnapSetup(t) - if err != nil { - continue - } + taskSetsBySnap, seedTS := parseSeedRefreshTaskSets(uts) - switch snapsup.InstanceName() { - case "snapd": - snapdTS = ts - case "kernel": - kernelTS = ts - case "core18": - baseTS = ts - case "some-app": - appTS = ts - case "some-other-snap": - extraAppTS = ts - } + snapdTS := mustTaskSetForSnap(c, taskSetsBySnap, "snapd") + baseTS := mustTaskSetForSnap(c, taskSetsBySnap, "core18") + kernelTS := mustTaskSetForSnap(c, taskSetsBySnap, "kernel") + appTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-app") + extraAppTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-other-snap") - break - } - } - c.Check(chg.CheckTaskDependencies(), IsNil) - if snapdTS == nil || baseTS == nil || kernelTS == nil || appTS == nil || extraAppTS == nil { - c.Fatalf("missing task sets: snapd=%v base=%v kernel=%v app=%v extraApp=%v", snapdTS != nil, baseTS != nil, kernelTS != nil, appTS != nil, extraAppTS != nil) - } + observed.CheckInitialCandidates(c, + seedRefreshCandidateFromTaskSet(c, snapdTS), + seedRefreshCandidateFromTaskSet(c, kernelTS), + seedRefreshCandidateFromTaskSet(c, baseTS), + seedRefreshCandidateFromTaskSet(c, appTS), + seedRefreshCandidateFromTaskSet(c, extraAppTS), + ) - seedTS := findSeedRefreshTaskSet(uts.Refresh) c.Assert(seedTS, NotNil) c.Check(taskSetsShareLane(snapdTS, baseTS, kernelTS, appTS), Equals, true) c.Check(taskSetsShareLane(baseTS, extraAppTS), Equals, false) @@ -20787,14 +20566,6 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshEarlyDownloadWithSnapd(c seedCreate, seedEnd, _ := splitSeedRefreshTasks(c, seedTS) - var seedSetup recoverySystemSetupForTest - c.Assert(seedCreate.Get("recovery-system-setup", &seedSetup), IsNil) - for _, ts := range []*state.TaskSet{snapdTS, baseTS, kernelTS, appTS} { - t, err := ts.Edge(snapstate.SnapSetupEdge) - c.Assert(err, IsNil) - c.Check(seedSetup.SnapSetupTasks, testutil.Contains, t.ID()) - } - kernelLinkTask, err := kernelTS.Edge(snapstate.MaybeRebootEdge) c.Assert(err, IsNil) c.Check(kernelLinkTask.HaltTasks(), testutil.Contains, seedCreate) @@ -20851,11 +20622,11 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshEarlyDownloadWithSnapd(c } func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshUndo(c *C) { - restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ + _, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ "kernel": "kernel", "base": "core18", "required-snaps": []any{"some-app"}, - }) + }, []string{"kernel", "core18", "some-app"}) defer restore() s.state.Lock() @@ -20912,11 +20683,11 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshUndo(c *C) { } func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshExtraSnapFailureDoesNotUndoSeed(c *C) { - restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ + _, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ "kernel": "kernel", "base": "core18", "required-snaps": []any{"some-app"}, - }) + }, []string{"kernel", "core18", "some-app"}) defer restore() s.state.Lock() @@ -20983,18 +20754,29 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshExtraSnapFailureDoesNotU } func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshRemoveSystemFailureDoesNotUndoSeed(c *C) { - restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ + _, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ "kernel": "kernel", "base": "core18", "required-snaps": []any{"some-app"}, - }) + }, nil) defer restore() s.state.Lock() defer s.state.Unlock() oldSeedRefreshTasks := snapstate.SeedRefreshTasks - snapstate.SeedRefreshTasks = func(st *state.State, snapSetupTasks, compSetupTasks []string) (*snapstate.SeedRefreshTaskSet, error) { + snapstate.SeedRefreshTasks = func(st *state.State, _ snapstate.DeviceContext, candidates []snapstate.SeedRefreshCandidate) (*snapstate.SeedRefreshTaskSet, map[string]bool, error) { + added := make(map[string]bool, len(candidates)) + for _, candidate := range candidates { + if candidate.InstanceName != "kernel" && candidate.InstanceName != "core18" && candidate.InstanceName != "some-app" { + continue + } + added[candidate.InstanceName] = true + } + if len(added) == 0 { + return nil, nil, nil + } + create := st.NewTask("create-recovery-system", "Create recovery system") restart.MarkTaskAsRestartBoundary(create, restart.RestartBoundaryDirectionDo) @@ -21011,7 +20793,7 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshRemoveSystemFailureDoesN Create: create, Finalize: finalize, Remove: []*state.Task{remove1, remove2}, - }, nil + }, added, nil } defer func() { snapstate.SeedRefreshTasks = oldSeedRefreshTasks @@ -21104,11 +20886,11 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshRemoveSystemFailureDoesN } func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshKernelPostRebootFailureUndoesSeed(c *C) { - restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ + _, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ "kernel": "kernel", "base": "core18", "required-snaps": []any{"some-app"}, - }) + }, []string{"kernel", "core18", "some-app"}) defer restore() s.state.Lock() @@ -21175,11 +20957,11 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshKernelPostRebootFailureU } func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshNoEssentials(c *C) { - restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ + _, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ "kernel": "kernel", "base": "core18", "required-snaps": []any{"some-app"}, - }) + }, []string{"some-app"}) defer restore() s.state.Lock() @@ -21209,11 +20991,11 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshNoEssentials(c *C) { } func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshNoEssentialsWithAdditionalComponents(c *C) { - restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ + observed, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ "kernel": "kernel", "base": "core18", "required-snaps": []any{"some-app"}, - }) + }, []string{"some-app"}) defer restore() s.state.Lock() @@ -21248,6 +21030,17 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshNoEssentialsWithAddition taskSetsBySnap, seedTS := parseSeedRefreshTaskSets(uts) appTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-app") + appSnapSetupTask, err := appTS.Edge(snapstate.SnapSetupEdge) + c.Assert(err, IsNil) + + var appCompSetupTaskIDs []string + c.Assert(appSnapSetupTask.Get("component-setup-tasks", &appCompSetupTaskIDs), IsNil) + c.Assert(appCompSetupTaskIDs, HasLen, 1) + observed.CheckInitialCandidates(c, snapstate.SeedRefreshCandidate{ + InstanceName: "some-app", + SnapSetupTaskIDs: []string{appSnapSetupTask.ID()}, + ComponentSetupTaskIDs: appCompSetupTaskIDs, + }) c.Assert(seedTS, NotNil) c.Check(taskSetLanes(seedTS), testutil.DeepUnsortedMatches, taskSetLanes(appTS)) @@ -21261,26 +21054,14 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshNoEssentialsWithAddition appEndTask, err := appTS.Edge(snapstate.EndEdge) c.Assert(err, IsNil) c.Check(waitsOnTransitively(seedEnd, appEndTask), Equals, true) - - appSnapSetupTask, err := appTS.Edge(snapstate.SnapSetupEdge) - c.Assert(err, IsNil) - - var appCompSetupTaskIDs []string - c.Assert(appSnapSetupTask.Get("component-setup-tasks", &appCompSetupTaskIDs), IsNil) - c.Assert(appCompSetupTaskIDs, HasLen, 1) - - var seedSetup recoverySystemSetupForTest - c.Assert(seedCreate.Get("recovery-system-setup", &seedSetup), IsNil) - c.Check(seedSetup.SnapSetupTasks, testutil.Contains, appSnapSetupTask.ID()) - c.Check(seedSetup.ComponentSetupTasks, DeepEquals, appCompSetupTaskIDs) } func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshSkippedWhileParentSeedRefreshNotReady(c *C) { - restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ + _, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ "kernel": "kernel", "base": "core18", "required-snaps": []any{"some-app", "content-provider"}, - }) + }, []string{"some-app"}) defer restore() s.state.Lock() @@ -21310,11 +21091,10 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshSkippedWhileParentSeedRe } func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshAllowedOnceParentSeedRefreshReady(c *C) { - restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ - "kernel": "kernel", - "base": "core18", - "required-snaps": []any{"some-app", "content-provider"}, - }) + _, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ + "kernel": "kernel", + "base": "core18", + }, []string{"core18"}) defer restore() s.state.Lock() @@ -21323,11 +21103,9 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshAllowedOnceParentSeedRef s.installSeedRefreshSnaps(c, seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"}, seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"}, - seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"}, - seedRefreshSnap{name: "content-provider", snapID: "content-provider-id", snapType: "app", base: "core18"}, ) - uts, chg := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{{InstanceName: "some-app"}}) + uts, chg := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{{InstanceName: "core18"}}) seedTS := findSeedRefreshTaskSet(uts.Refresh) c.Assert(seedTS, NotNil) @@ -21335,7 +21113,7 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshAllowedOnceParentSeedRef seedCreate.SetStatus(state.DoneStatus) seedFinalize.SetStatus(state.DoneStatus) - goal := snapstate.StoreUpdateGoal(snapstate.StoreUpdate{InstanceName: "content-provider"}) + goal := snapstate.StoreUpdateGoal(snapstate.StoreUpdate{InstanceName: "core18"}) ts, err := snapstate.UpdateOne(context.Background(), s.state, goal, nil, snapstate.Options{ UserID: s.user.ID, ConflictOptions: snapstate.ConflictOptions{FromChange: chg.ID()}, @@ -21347,11 +21125,11 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshAllowedOnceParentSeedRef } func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshReRefreshCreatesSecondSeed(c *C) { - restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ + _, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ "kernel": "kernel", "base": "core18", "required-snaps": []any{"some-app"}, - }) + }, []string{"some-app"}) defer restore() s.state.Lock() @@ -21411,11 +21189,11 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshReRefreshCreatesSecondSe } func (s *snapmgrTestSuite) TestUpdateOneIncludeFromChangeInTaskConflictCheckIgnoresSameChangeSeedRefreshExclusivity(c *C) { - restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ + _, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ "kernel": "kernel", "base": "core18", "required-snaps": []any{"content-provider"}, - }) + }, []string{"kernel", "core18", "content-provider"}) defer restore() s.state.Lock() @@ -21453,10 +21231,10 @@ func (s *snapmgrTestSuite) TestUpdateOneIncludeFromChangeInTaskConflictCheckIgno } func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshBlockedByOtherChanges(c *C) { - restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ + _, restore := s.setupSeedRefreshUpdateTest(c, false, true, map[string]any{ "kernel": "kernel", "base": "core18", - }) + }, []string{"kernel", "core18"}) defer restore() s.state.Lock() @@ -21482,104 +21260,39 @@ func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshBlockedByOtherChanges(c c.Assert(err, ErrorMatches, `other changes in progress \(conflicting change "unrelated"\), change "seed refresh" not allowed until they are done`) } -// TODO: switch this test to the newer seed-refresh helpers func (s *snapmgrTestSuite) TestUpdateWithGoalSeedRefreshDisabled(c *C) { - restore := release.MockOnClassic(false) - defer restore() - restore = snapstate.MockRevisionDate(nil) + restore := snapstate.MockRevisionDate(nil) defer restore() s.state.Lock() defer s.state.Unlock() - restore = snapstatetest.MockDeviceModel(MakeModel(map[string]any{ + observed, restore := s.setupSeedRefreshUpdateTest(c, false, false, map[string]any{ "kernel": "kernel", "base": "core18", - })) + }, nil) defer restore() - kernel := snap.SideInfo{ - RealName: "kernel", - Revision: snap.R(7), - SnapID: "kernel-id", - } - base := snap.SideInfo{ - RealName: "core18", - Revision: snap.R(7), - SnapID: "core18-snap-id", - } - app := snap.SideInfo{ - RealName: "some-app", - Revision: snap.R(7), - SnapID: "some-app-id", - } - - types := map[string]string{ - "kernel": "kernel", - "core18": "base", - "some-app": "app", - } - - snaptest.MockSnap(c, fmt.Sprintf("name: %s", kernel.RealName), &kernel) - snaptest.MockSnap(c, fmt.Sprintf("name: %s\nbase: core18", base.RealName), &base) - snaptest.MockSnap(c, fmt.Sprintf("name: %s\nbase: core18", app.RealName), &app) - - for _, si := range []snap.SideInfo{kernel, base, app} { - si := si - s.fakeStore.registerID(si.RealName, si.SnapID) - snapstate.Set(s.state, si.RealName, &snapstate.SnapState{ - Active: true, - Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}), - Current: si.Revision, - TrackingChannel: "latest/stable", - SnapType: types[si.RealName], - }) - } + s.installSeedRefreshSnaps(c, + seedRefreshSnap{name: "kernel", snapID: "kernel-id", snapType: "kernel"}, + seedRefreshSnap{name: "core18", snapID: "core18-snap-id", snapType: "base"}, + seedRefreshSnap{name: "some-app", snapID: "some-app-id", snapType: "app", base: "core18"}, + ) - updates := []snapstate.StoreUpdate{ + uts, _ := runSeedRefreshUpdate(c, s.state, s.user.ID, []snapstate.StoreUpdate{ {InstanceName: "kernel"}, {InstanceName: "core18"}, {InstanceName: "some-app"}, - } - goal := snapstate.StoreUpdateGoal(updates...) - - affected, uts, err := snapstate.UpdateWithGoal(context.Background(), s.state, goal, nil, snapstate.Options{ - UserID: s.user.ID, - Flags: snapstate.Flags{ - Transaction: client.TransactionPerSnap, - }, }) - c.Assert(err, IsNil) - c.Assert(affected, testutil.DeepUnsortedMatches, []string{"core18", "kernel", "some-app"}) - - chg := s.state.NewChange("refresh", "refresh kernel, base, and app") - var baseTS, kernelTS, appTS *state.TaskSet - for _, ts := range uts.Refresh { - chg.AddAll(ts) - for _, t := range ts.Tasks() { - snapsup, err := snapstate.TaskSnapSetup(t) - if err != nil { - continue - } + taskSetsBySnap, seedTS := parseSeedRefreshTaskSets(uts) - switch snapsup.Type { - case snap.TypeKernel: - kernelTS = ts - case snap.TypeBase: - baseTS = ts - case snap.TypeApp: - appTS = ts - } + baseTS := mustTaskSetForSnap(c, taskSetsBySnap, "core18") + kernelTS := mustTaskSetForSnap(c, taskSetsBySnap, "kernel") + appTS := mustTaskSetForSnap(c, taskSetsBySnap, "some-app") - break - } - } - c.Check(chg.CheckTaskDependencies(), IsNil) - c.Assert(baseTS, NotNil) - c.Assert(kernelTS, NotNil) - c.Assert(appTS, NotNil) - seedTS := findSeedRefreshTaskSet(uts.Refresh) c.Assert(seedTS, IsNil) + c.Check(observed.initial, HasLen, 0) + c.Check(observed.prerequisites, HasLen, 0) lastBeforeLocalBase, err := baseTS.Edge(snapstate.LastBeforeLocalModificationsEdge) c.Assert(err, IsNil)