diff --git a/overlord/snapstate/backend_test.go b/overlord/snapstate/backend_test.go index 89cc6c3ead50..b95b6a645c08 100644 --- a/overlord/snapstate/backend_test.go +++ b/overlord/snapstate/backend_test.go @@ -523,7 +523,7 @@ func (f *fakeStore) lookupRefresh(cand refreshCand) (*snap.Info, error) { confinement = snap.ClassicConfinement case "channel-for-devmode/stable": confinement = snap.DevModeConfinement - case "channel-for-components": + case "channel-for-components", "channel-for-components-only-component-refresh": components = map[string]*snap.Component{ "test-component": { Type: snap.TestComponent, @@ -628,6 +628,12 @@ func (f *fakeStore) lookupRefresh(cand refreshCand) (*snap.Info, error) { return info, nil } + // this is a special case for testing the case where we only refresh + // component revisions + if cand.channel == "channel-for-components-only-component-refresh" { + return info, nil + } + return nil, store.ErrNoUpdateAvailable } diff --git a/overlord/snapstate/component.go b/overlord/snapstate/component.go index 362cf8a50b53..c8a76f279759 100644 --- a/overlord/snapstate/component.go +++ b/overlord/snapstate/component.go @@ -256,18 +256,18 @@ type componentInstallFlags struct { } type componentInstallTaskSet struct { - compSetupTaskID string - beforeLink []*state.Task - linkTask *state.Task - postOpHookAndAfter []*state.Task - discardTask *state.Task + compSetupTaskID string + beforeLink []*state.Task + linkTask *state.Task + postOpHookToDiscard []*state.Task + discardTask *state.Task } func (c *componentInstallTaskSet) taskSet() *state.TaskSet { - tasks := make([]*state.Task, 0, len(c.beforeLink)+1+len(c.postOpHookAndAfter)+1) + tasks := make([]*state.Task, 0, len(c.beforeLink)+1+len(c.postOpHookToDiscard)+1) tasks = append(tasks, c.beforeLink...) tasks = append(tasks, c.linkTask) - tasks = append(tasks, c.postOpHookAndAfter...) + tasks = append(tasks, c.postOpHookToDiscard...) if c.discardTask != nil { tasks = append(tasks, c.discardTask) } @@ -428,7 +428,7 @@ func doInstallComponent(st *state.State, snapst *SnapState, compSetup ComponentS } else { postOpHook = SetupPostRefreshComponentHook(st, snapsup.InstanceName(), compSi.Component.ComponentName) } - componentTS.postOpHookAndAfter = append(componentTS.postOpHookAndAfter, postOpHook) + componentTS.postOpHookToDiscard = append(componentTS.postOpHookToDiscard, postOpHook) addTask(postOpHook) if !compSetup.MultiComponentInstall && kmodSetup == nil && compSetup.CompType == snap.KernelModulesComponent { @@ -437,7 +437,7 @@ func doInstallComponent(st *state.State, snapst *SnapState, compSetup ComponentS compSi.Component, revisionStr)) kmodSetup.Set("component-setup-task", prepare.ID()) kmodSetup.Set("snap-setup-task", snapSetupTaskID) - componentTS.postOpHookAndAfter = append(componentTS.postOpHookAndAfter, kmodSetup) + componentTS.postOpHookToDiscard = append(componentTS.postOpHookToDiscard, kmodSetup) } if kmodSetup != nil { // note that we don't use addTask here because this task is shared and diff --git a/overlord/snapstate/handlers.go b/overlord/snapstate/handlers.go index 64cab875de0b..7c00cfe57b9a 100644 --- a/overlord/snapstate/handlers.go +++ b/overlord/snapstate/handlers.go @@ -2714,7 +2714,7 @@ func (m *SnapManager) undoLinkSnap(t *state.Task, _ *tomb.Tomb) error { return err } - if len(snapst.Sequence.Revisions) == 1 { + if oldCurrent.Unset() { // XXX: shouldn't these two just log and carry on? this is an undo handler... timings.Run(perfTimings, "discard-snap-namespace", fmt.Sprintf("discard the namespace of snap %q", snapsup.InstanceName()), func(tm timings.Measurer) { err = m.backend.DiscardSnapNamespace(snapsup.InstanceName()) diff --git a/overlord/snapstate/handlers_components.go b/overlord/snapstate/handlers_components.go index caecd4af38dc..f956cff27492 100644 --- a/overlord/snapstate/handlers_components.go +++ b/overlord/snapstate/handlers_components.go @@ -523,12 +523,16 @@ func (m *SnapManager) doUnlinkCurrentComponent(t *state.Task, _ *tomb.Tomb) (err defer st.Unlock() // snapSt is a copy of the current state - compSetup, _, snapSt, err := compSetupAndState(t) + compSetup, snapsup, snapSt, err := compSetupAndState(t) if err != nil { return err } cref := compSetup.CompSideInfo.Component + if err := saveCurrentKernelModuleComponents(t, snapsup, snapSt); err != nil { + return err + } + // Expected to be installed snapInfo, err := snapSt.CurrentInfo() if err != nil { @@ -561,12 +565,6 @@ func (m *SnapManager) doUnlinkComponent(t *state.Task, _ *tomb.Tomb) (err error) return err } - // TODO:COMPS: test taking this branch when unlinking components during a - // refresh where we lose components - if err := saveCurrentKernelModuleComponents(t, snapSup, snapSt); err != nil { - return err - } - cref := compSetup.CompSideInfo.Component // Remove component for the specified revision if err := m.unlinkComponent( diff --git a/overlord/snapstate/handlers_components_link_test.go b/overlord/snapstate/handlers_components_link_test.go index cc188d3c037c..6252d1cb4ec2 100644 --- a/overlord/snapstate/handlers_components_link_test.go +++ b/overlord/snapstate/handlers_components_link_test.go @@ -289,7 +289,7 @@ func (s *linkCompSnapSuite) TestDoUnlinkCurrentComponent(c *C) { setStateWithOneComponent(s.state, snapName, snapRev, compName, compRev) s.state.Unlock() - s.testDoUnlinkComponent(c, snapName, snapRev, compName, compRev, "unlink-current-component", nil) + s.testDoUnlinkComponent(c, snapName, snapRev, compName, compRev, "unlink-current-component", []*snap.ComponentSideInfo{}) } func (s *linkCompSnapSuite) TestDoUnlinkCurrentComponentOtherCompPresent(c *C) { @@ -303,11 +303,11 @@ func (s *linkCompSnapSuite) TestDoUnlinkCurrentComponentOtherCompPresent(c *C) { csi1 := snap.NewComponentSideInfo(naming.NewComponentRef(snapName, compName), compRev) csi2 := snap.NewComponentSideInfo(naming.NewComponentRef(snapName, "other-comp"), snap.R(33)) cs1 := sequence.NewComponentState(csi1, snap.TestComponent) - cs2 := sequence.NewComponentState(csi2, snap.TestComponent) + cs2 := sequence.NewComponentState(csi2, snap.KernelModulesComponent) setStateWithComponents(s.state, snapName, snapRev, []*sequence.ComponentState{cs1, cs2}) s.state.Unlock() - s.testDoUnlinkComponent(c, snapName, snapRev, compName, compRev, "unlink-current-component", nil) + s.testDoUnlinkComponent(c, snapName, snapRev, compName, compRev, "unlink-current-component", []*snap.ComponentSideInfo{cs2.SideInfo}) } func (s *linkCompSnapSuite) TestDoUnlinkCurrentComponentTwoTasks(c *C) { @@ -471,11 +471,6 @@ func (s *linkCompSnapSuite) TestDoUnlinkComponent(c *C) { s.state.Lock() - kmodCsi := &snap.ComponentSideInfo{ - Component: naming.NewComponentRef(snapName, "kmod-comp"), - Revision: snap.R(10), - } - // State must contain the component. Note that in this case // the snap does not need to be active. ssi := &snap.SideInfo{RealName: snapName, Revision: snapRev, @@ -483,7 +478,6 @@ func (s *linkCompSnapSuite) TestDoUnlinkComponent(c *C) { csi := snap.NewComponentSideInfo(naming.NewComponentRef(snapName, compName), compRev) comps := []*sequence.ComponentState{ sequence.NewComponentState(csi, snap.TestComponent), - sequence.NewComponentState(kmodCsi, snap.KernelModulesComponent), } snapstate.Set(s.state, snapName, &snapstate.SnapState{ Active: false, @@ -495,7 +489,7 @@ func (s *linkCompSnapSuite) TestDoUnlinkComponent(c *C) { s.state.Unlock() - s.testDoUnlinkComponent(c, snapName, snapRev, compName, compRev, "unlink-component", []*snap.ComponentSideInfo{kmodCsi}) + s.testDoUnlinkComponent(c, snapName, snapRev, compName, compRev, "unlink-component", nil) } func (s *linkCompSnapSuite) TestDoUnlinkThenUndoUnlinkComponent(c *C) { diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go index 71c729d1f07e..a8e5deed3519 100644 --- a/overlord/snapstate/snapstate.go +++ b/overlord/snapstate/snapstate.go @@ -453,8 +453,14 @@ func doInstall(st *state.State, snapst *SnapState, snapsup SnapSetup, compsups [ addTask(t) } - // run refresh hooks when updating existing snap, otherwise run install hook further down. - runRefreshHooks := snapst.IsInstalled() && !snapsup.Flags.Revert + // if the snap is already installed, and the revision we are refreshing to + // is the same as the current revision, and we're not forcing an update, + // then we know that we're really modifying the state of components. + componentOnlyUpdate := snapst.IsInstalled() && snapsup.Revision() == snapst.Current && !snapsup.AlwaysUpdate + + // run refresh hooks when updating existing snap, otherwise run install hook + // further down. + runRefreshHooks := snapst.IsInstalled() && !componentOnlyUpdate && !snapsup.Flags.Revert if runRefreshHooks { preRefreshHook := SetupPreRefreshHook(st, snapsup.InstanceName()) addTask(preRefreshHook) @@ -620,8 +626,6 @@ func doInstall(st *state.State, snapst *SnapState, snapsup SnapSetup, compsups [ startSnapServices := st.NewTask("start-snap-services", fmt.Sprintf(i18n.G("Start snap %q%s services"), snapsup.InstanceName(), revisionStr)) addTask(startSnapServices) - // TODO:COMPS: test discarding components during a snap refresh (coming - // soon!) for _, t := range tasksBeforeDiscard { addTask(t) } @@ -776,7 +780,7 @@ func splitComponentTasksForInstall( tasksBeforePreRefreshHook = append(tasksBeforePreRefreshHook, componentTS.beforeLink...) tasksAfterLinkSnap = append(tasksAfterLinkSnap, componentTS.linkTask) - tasksAfterPostOpHook = append(tasksAfterPostOpHook, componentTS.postOpHookAndAfter...) + tasksAfterPostOpHook = append(tasksAfterPostOpHook, componentTS.postOpHookToDiscard...) if componentTS.discardTask != nil { tasksBeforeDiscard = append(tasksBeforeDiscard, componentTS.discardTask) } @@ -1881,12 +1885,32 @@ type update struct { // revision of the snap. // // TODO:COMPS: check if we need to change the state of components -func (u *update) revisionSatisfied() bool { +func (u *update) revisionSatisfied() (bool, error) { if u.Setup.AlwaysUpdate || !u.SnapState.IsInstalled() { - return false + return false, nil + } + + if u.SnapState.Current != u.Setup.Revision() { + return false, nil + } + + comps, err := u.SnapState.CurrentComponentInfos() + if err != nil { + return false, err } - return u.SnapState.Current == u.Setup.Revision() + currentCompRevs := make(map[string]snap.Revision, len(comps)) + for _, comp := range comps { + currentCompRevs[comp.Component.ComponentName] = comp.Revision + } + + for _, comp := range u.Components { + if currentCompRevs[comp.CompSideInfo.Component.ComponentName] != comp.Revision() { + return false, nil + } + } + + return true, nil } func doPotentiallySplitUpdate(st *state.State, requested []string, updates []update, opts Options) ([]string, *UpdateTaskSets, error) { @@ -1991,7 +2015,12 @@ func doUpdate(st *state.State, requested []string, updates []update, opts Option // and bases and then other snaps for _, up := range updates { // if the update is already satisfied, then we can skip it - if up.revisionSatisfied() { + ok, err := up.revisionSatisfied() + if err != nil { + return nil, false, nil, err + } + + if ok { alreadySatisfied = append(alreadySatisfied, up) continue } @@ -2267,7 +2296,12 @@ func autoAliasesUpdate(st *state.State, requested []string, updates []update) (c // snaps with updates updating := make(map[string]bool, len(updates)) for _, up := range updates { - updating[up.Setup.InstanceName()] = !up.revisionSatisfied() + ok, err := up.revisionSatisfied() + if err != nil { + return nil, nil, nil, err + } + + updating[up.Setup.InstanceName()] = !ok } // add explicitly auto-aliases only for snaps that are not updated diff --git a/overlord/snapstate/snapstate_install_test.go b/overlord/snapstate/snapstate_install_test.go index faf588fe2e6d..2b5ff462567d 100644 --- a/overlord/snapstate/snapstate_install_test.go +++ b/overlord/snapstate/snapstate_install_test.go @@ -6430,8 +6430,22 @@ func undoOps(instanceName string, newSequence, prevSequence *sequence.RevisionSi for i := len(newComponents) - 1; i >= 0; i-- { csi := newComponents[i].SideInfo - containerName := fmt.Sprintf("%s+%s", instanceName, csi.Component.ComponentName) - filename := fmt.Sprintf("%s_%v.comp", containerName, csi.Revision) + compName := csi.Component.ComponentName + compRev := csi.Revision + + containerName := fmt.Sprintf("%s+%s", instanceName, compName) + filename := fmt.Sprintf("%s_%v.comp", containerName, compRev) + + // if the snap revision isn't changing, then we need to re-link the old + // component + if snapRevision == prevRevision { + oldCS := prevSequence.FindComponent(csi.Component) + + ops = append(ops, []fakeOp{{ + op: "link-component", + path: snap.ComponentMountDir(compName, oldCS.SideInfo.Revision, instanceName), + }}...) + } ops = append(ops, []fakeOp{{ op: "undo-setup-component", @@ -6444,16 +6458,18 @@ func undoOps(instanceName string, newSequence, prevSequence *sequence.RevisionSi }}...) } - ops = append(ops, []fakeOp{{ - op: "undo-setup-snap", - name: instanceName, - stype: "app", - path: snapMount, - }, { - op: "remove-snap-dir", - name: instanceName, - path: filepath.Join(dirs.SnapMountDir, instanceName), - }}...) + if snapRevision != prevRevision { + ops = append(ops, []fakeOp{{ + op: "undo-setup-snap", + name: instanceName, + stype: "app", + path: snapMount, + }, { + op: "remove-snap-dir", + name: instanceName, + path: filepath.Join(dirs.SnapMountDir, instanceName), + }}...) + } return ops } diff --git a/overlord/snapstate/snapstate_update_test.go b/overlord/snapstate/snapstate_update_test.go index 8ec9a9b4bf8a..cac6e3d1d9eb 100644 --- a/overlord/snapstate/snapstate_update_test.go +++ b/overlord/snapstate/snapstate_update_test.go @@ -14057,17 +14057,17 @@ func (s *snapmgrTestSuite) TestUpdateWithComponentsBackToPrevRevision(c *C) { } func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThrough(c *C) { - s.testUpdateWithComponentsRunThrough(c, updateWIthComponentsOpts{ + s.testUpdateWithComponentsRunThrough(c, updateWithComponentsOpts{ components: []string{"test-component", "kernel-modules-component"}, }) } func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughNoComponents(c *C) { - s.testUpdateWithComponentsRunThrough(c, updateWIthComponentsOpts{}) + s.testUpdateWithComponentsRunThrough(c, updateWithComponentsOpts{}) } func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughUndo(c *C) { - s.testUpdateWithComponentsRunThrough(c, updateWIthComponentsOpts{ + s.testUpdateWithComponentsRunThrough(c, updateWithComponentsOpts{ components: []string{"test-component", "kernel-modules-component"}, refreshAppAwarenessUX: true, undo: true, @@ -14075,7 +14075,7 @@ func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughUndo(c *C) { } func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughInstanceKey(c *C) { - s.testUpdateWithComponentsRunThrough(c, updateWIthComponentsOpts{ + s.testUpdateWithComponentsRunThrough(c, updateWithComponentsOpts{ instanceKey: "key", components: []string{"test-component", "kernel-modules-component"}, refreshAppAwarenessUX: true, @@ -14083,7 +14083,7 @@ func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughInstanceKey(c *C) { } func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughInstanceKeyUndo(c *C) { - s.testUpdateWithComponentsRunThrough(c, updateWIthComponentsOpts{ + s.testUpdateWithComponentsRunThrough(c, updateWithComponentsOpts{ instanceKey: "key", components: []string{"test-component", "kernel-modules-component"}, refreshAppAwarenessUX: true, @@ -14092,7 +14092,7 @@ func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughInstanceKeyUndo(c * } func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughLoseComponents(c *C) { - s.testUpdateWithComponentsRunThrough(c, updateWIthComponentsOpts{ + s.testUpdateWithComponentsRunThrough(c, updateWithComponentsOpts{ instanceKey: "key", components: []string{"test-component", "kernel-modules-component"}, postRefreshComponents: []string{"test-component"}, @@ -14101,7 +14101,7 @@ func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughLoseComponents(c *C } func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughLoseComponentsUndo(c *C) { - s.testUpdateWithComponentsRunThrough(c, updateWIthComponentsOpts{ + s.testUpdateWithComponentsRunThrough(c, updateWithComponentsOpts{ instanceKey: "key", components: []string{"test-component", "kernel-modules-component"}, postRefreshComponents: []string{"test-component"}, @@ -14110,7 +14110,7 @@ func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughLoseComponentsUndo( }) } -type updateWIthComponentsOpts struct { +type updateWithComponentsOpts struct { instanceKey string components []string postRefreshComponents []string @@ -14118,7 +14118,7 @@ type updateWIthComponentsOpts struct { undo bool } -func (s *snapmgrTestSuite) testUpdateWithComponentsRunThrough(c *C, opts updateWIthComponentsOpts) { +func (s *snapmgrTestSuite) testUpdateWithComponentsRunThrough(c *C, opts updateWithComponentsOpts) { if opts.refreshAppAwarenessUX { s.enableRefreshAppAwarenessUX() } @@ -14126,11 +14126,12 @@ func (s *snapmgrTestSuite) testUpdateWithComponentsRunThrough(c *C, opts updateW const ( snapName = "some-snap" snapID = "some-snap-id" - channel = "channel-for-components" ) + channel := "channel-for-components" currentSnapRev := snap.R(7) newSnapRev := snap.R(11) + instanceName := snap.InstanceName(snapName, opts.instanceKey) if opts.postRefreshComponents == nil { @@ -14754,3 +14755,454 @@ func (s *snapmgrTestSuite) TestUpdateTasksWithComponentsRemoved(c *C) { c.Assert(err, IsNil) c.Check(compSup.CompSideInfo.Component, Equals, cref2) } + +func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughOnlyComponentUpdate(c *C) { + s.testUpdateWithComponentsRunThroughOnlyComponentUpdate(c, updateWithComponentsOpts{ + instanceKey: "key", + components: []string{"test-component", "kernel-modules-component"}, + refreshAppAwarenessUX: true, + }) +} + +func (s *snapmgrTestSuite) TestUpdateWithComponentsRunThroughOnlyComponentUpdateUndo(c *C) { + s.testUpdateWithComponentsRunThroughOnlyComponentUpdate(c, updateWithComponentsOpts{ + instanceKey: "key", + components: []string{"test-component", "kernel-modules-component"}, + refreshAppAwarenessUX: true, + undo: true, + }) +} + +func (s *snapmgrTestSuite) testUpdateWithComponentsRunThroughOnlyComponentUpdate(c *C, opts updateWithComponentsOpts) { + if opts.refreshAppAwarenessUX { + s.enableRefreshAppAwarenessUX() + } + + const ( + snapName = "some-snap" + snapID = "some-snap-id" + ) + + channel := "channel-for-components-only-component-refresh" + currentSnapRev := snap.R(7) + s.fakeStore.refreshRevnos = map[string]snap.Revision{ + snapID: currentSnapRev, + } + + instanceName := snap.InstanceName(snapName, opts.instanceKey) + + if opts.postRefreshComponents == nil { + opts.postRefreshComponents = opts.components + } + + sort.Strings(opts.components) + sort.Strings(opts.postRefreshComponents) + + originalCompRevisions := make(map[string]snap.Revision) + for i, compName := range opts.components { + originalCompRevisions[compName] = snap.R(i + 1) + } + + updatedCompRevisions := make(map[string]snap.Revision) + for i, compName := range opts.components { + updatedCompRevisions[compName] = snap.R(i + 2) + } + + compNameToType := func(name string) snap.ComponentType { + typ, ok := strings.CutSuffix(name, "-component") + if !ok { + c.Fatalf("unexpected component name %q", name) + } + return snap.ComponentType(typ) + } + + s.fakeStore.snapResourcesFn = func(info *snap.Info) []store.SnapResourceResult { + c.Assert(info.InstanceName(), DeepEquals, instanceName) + var results []store.SnapResourceResult + for _, compName := range opts.postRefreshComponents { + results = append(results, store.SnapResourceResult{ + DownloadInfo: snap.DownloadInfo{ + DownloadURL: "http://example.com/" + compName, + }, + Name: compName, + Revision: updatedCompRevisions[compName].N, + Type: fmt.Sprintf("component/%s", compNameToType(compName)), + Version: "1.0", + CreatedAt: "2024-01-01T00:00:00Z", + }) + } + return results + } + + // we start without the auxiliary store info (or with an older one) + c.Check(snapstate.AuxStoreInfoFilename(snapID), testutil.FileAbsent) + + si := snap.SideInfo{ + RealName: snapName, + Revision: currentSnapRev, + SnapID: snapID, + Channel: channel, + } + snaptest.MockSnapInstance(c, instanceName, fmt.Sprintf("name: %s", snapName), &si) + fi, err := os.Stat(snap.MountFile(instanceName, si.Revision)) + c.Assert(err, IsNil) + + refreshedDate := fi.ModTime() + + restore := snapstate.MockRevisionDate(nil) + defer restore() + + now, err := time.Parse(time.RFC3339, "2021-06-10T10:00:00Z") + c.Assert(err, IsNil) + + restore = snapstate.MockTimeNow(func() time.Time { + return now + }) + defer restore() + + s.state.Lock() + defer s.state.Unlock() + + if opts.instanceKey != "" { + tr := config.NewTransaction(s.state) + tr.Set("core", "experimental.parallel-instances", true) + tr.Commit() + } + + currentSeq := snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&si}) + + var currentResources map[string]snap.Revision + for _, comp := range opts.components { + err := currentSeq.AddComponentForRevision(currentSnapRev, &sequence.ComponentState{ + SideInfo: &snap.ComponentSideInfo{ + Component: naming.NewComponentRef(snapName, comp), + Revision: originalCompRevisions[comp], + }, + CompType: compNameToType(comp), + }) + c.Assert(err, IsNil) + + if currentResources == nil { + currentResources = make(map[string]snap.Revision, len(opts.components)) + } + currentResources[comp] = originalCompRevisions[comp] + } + currentComponentStates := currentSeq.Revisions[0].Components + + expectedComponentStates := make([]*sequence.ComponentState, 0, len(opts.postRefreshComponents)) + for _, comp := range opts.postRefreshComponents { + expectedComponentStates = append(expectedComponentStates, &sequence.ComponentState{ + SideInfo: &snap.ComponentSideInfo{ + Component: naming.NewComponentRef(snapName, comp), + Revision: updatedCompRevisions[comp], + }, + CompType: compNameToType(comp), + }) + } + + s.AddCleanup(snapstate.MockReadComponentInfo(func( + compMntDir string, info *snap.Info, csi *snap.ComponentSideInfo, + ) (*snap.ComponentInfo, error) { + return &snap.ComponentInfo{ + Component: csi.Component, + Type: compNameToType(csi.Component.ComponentName), + Version: "1.0", + ComponentSideInfo: *csi, + }, nil + })) + + snapstate.Set(s.state, instanceName, &snapstate.SnapState{ + Active: true, + Sequence: currentSeq, + Current: si.Revision, + SnapType: "app", + TrackingChannel: channel, + InstanceKey: opts.instanceKey, + }) + + ts, err := snapstate.Update(s.state, instanceName, nil, s.user.ID, snapstate.Flags{}) + c.Assert(err, IsNil) + + chg := s.state.NewChange("refresh", "refresh a snap") + chg.AddAll(ts) + + if opts.undo { + last := lastWithLane(ts.Tasks()) + c.Assert(last, NotNil) + + terr := s.state.NewTask("error-trigger", "provoking total undo") + terr.WaitFor(last) + terr.JoinLane(last.Lanes()[0]) + chg.AddTask(terr) + } + + // check unlink-reason + unlinkTask := findLastTask(chg, "unlink-current-snap") + c.Assert(unlinkTask, NotNil) + var unlinkReason string + unlinkTask.Get("unlink-reason", &unlinkReason) + c.Check(unlinkReason, Equals, "refresh") + + // local modifications, edge must be set + te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge) + c.Assert(te, NotNil) + c.Assert(te.Kind(), Equals, "prepare-snap") + + s.settle(c) + + if opts.undo { + c.Assert(chg.Err(), NotNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks()))) + } else { + c.Assert(chg.Err(), IsNil, Commentf("change tasks:\n%s", printTasks(chg.Tasks()))) + } + + expected := fakeOps{ + { + op: "storesvc-snap-action", + curSnaps: []store.CurrentSnap{{ + InstanceName: instanceName, + SnapID: snapID, + Revision: currentSnapRev, + TrackingChannel: channel, + RefreshedDate: refreshedDate, + Epoch: snap.E("1*"), + Resources: currentResources, + }}, + userID: 1, + }, + { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "refresh", + InstanceName: instanceName, + SnapID: snapID, + Channel: channel, + Flags: store.SnapActionEnforceValidation, + }, + revno: currentSnapRev, + userID: 1, + }, + } + + if !opts.refreshAppAwarenessUX { + expected = append(expected, fakeOp{ + op: "remove-snap-aliases", + name: instanceName, + }) + } + + for _, cs := range expectedComponentStates { + compName := cs.SideInfo.Component.ComponentName + compRev := cs.SideInfo.Revision + containerName := fmt.Sprintf("%s+%s", instanceName, compName) + filename := fmt.Sprintf("%s_%v.comp", containerName, compRev) + + expected = append(expected, []fakeOp{{ + op: "storesvc-download", + name: cs.SideInfo.Component.String(), + }, { + op: "validate-component:Doing", + name: instanceName, + revno: currentSnapRev, + componentName: compName, + componentPath: filepath.Join(dirs.SnapBlobDir, filename), + componentRev: compRev, + componentSideInfo: *cs.SideInfo, + }, { + op: "setup-component", + containerName: containerName, + containerFileName: filename, + }, { + op: "unlink-component", + path: snap.ComponentMountDir(compName, originalCompRevisions[compName], instanceName), + }}...) + } + + expected = append(expected, fakeOps{ + { + op: "run-inhibit-snap-for-unlink", + name: instanceName, + inhibitHint: "refresh", + }, + { + op: "unlink-snap", + path: filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()), + unlinkSkipBinaries: opts.refreshAppAwarenessUX, + }, + { + op: "copy-data", + path: filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()), + old: filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()), + }, + { + op: "setup-snap-save-data", + path: filepath.Join(dirs.SnapDataSaveDir, instanceName), + }, + }...) + + expected = append(expected, fakeOps{ + { + op: "setup-profiles:Doing", + name: instanceName, + revno: currentSnapRev, + }, + { + op: "candidate", + sinfo: snap.SideInfo{ + RealName: snapName, + SnapID: snapID, + Channel: channel, + Revision: currentSnapRev, + }, + }, + { + op: "link-snap", + path: filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()), + }, + }...) + + for _, cs := range expectedComponentStates { + compName := cs.SideInfo.Component.ComponentName + compRev := cs.SideInfo.Revision + expected = append(expected, []fakeOp{ + { + op: "link-component", + path: snap.ComponentMountDir(compName, compRev, instanceName), + }, + }...) + } + + expected = append(expected, fakeOps{ + { + op: "auto-connect:Doing", + name: instanceName, + revno: currentSnapRev, + }, + { + op: "update-aliases", + }, + }...) + + var currentKmodComps []*snap.ComponentSideInfo + for _, cs := range currentComponentStates { + if cs.CompType == snap.KernelModulesComponent { + currentKmodComps = append(currentKmodComps, cs.SideInfo) + } + } + + var newKmodComps []*snap.ComponentSideInfo + for _, cs := range expectedComponentStates { + if cs.CompType == snap.KernelModulesComponent { + newKmodComps = append(newKmodComps, cs.SideInfo) + } + } + + if len(currentKmodComps) > 0 || len(newKmodComps) > 0 { + expected = append(expected, fakeOp{ + op: "prepare-kernel-modules-components", + currentComps: currentKmodComps, + finalComps: newKmodComps, + }) + } + + for _, cs := range currentComponentStates { + compName := cs.SideInfo.Component.ComponentName + compRev := cs.SideInfo.Revision + containerName := fmt.Sprintf("%s+%s", instanceName, compName) + removedFilename := fmt.Sprintf("%s_%v.comp", containerName, compRev) + expected = append(expected, []fakeOp{ + { + op: "undo-setup-component", + containerName: containerName, + containerFileName: removedFilename, + }, + { + op: "remove-component-dir", + containerName: containerName, + containerFileName: removedFilename, + }, + }...) + } + + expectedSideState := sequence.NewRevisionSideState(&si, expectedComponentStates) + originalSideState := currentSeq.Revisions[0] + + if opts.undo { + expected = append(expected, undoOps(instanceName, expectedSideState, originalSideState)...) + } else { + expected = append(expected, fakeOp{ + op: "cleanup-trash", + name: instanceName, + revno: currentSnapRev, + }) + } + + downloads := make([]fakeDownload, 0, len(opts.postRefreshComponents)) + for _, compName := range opts.postRefreshComponents { + downloads = append(downloads, fakeDownload{ + macaroon: s.user.StoreMacaroon, + name: fmt.Sprintf("%s+%s", snapName, compName), + target: filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s+%s_%d.comp", instanceName, compName, updatedCompRevisions[compName].N)), + }) + } + c.Check(s.fakeStore.downloads, DeepEquals, downloads) + + c.Check(s.fakeStore.seenPrivacyKeys["privacy-key"], Equals, true, Commentf("salts seen: %v", s.fakeStore.seenPrivacyKeys)) + + // start with an easier-to-read error if this fails: + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Assert(s.fakeBackend.ops, DeepEquals, expected) + + task := ts.Tasks()[1] + + // verify snapSetup info + var snapsup snapstate.SnapSetup + err = task.Get("snap-setup", &snapsup) + c.Assert(err, IsNil) + c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{ + Channel: channel, + UserID: s.user.ID, + DownloadInfo: &snap.DownloadInfo{ + DownloadURL: "https://some-server.com/some/path.snap", + }, + SideInfo: snapsup.SideInfo, + Type: snap.TypeApp, + Version: "some-snapVer", + PlugsOnly: true, + Flags: snapstate.Flags{ + Transaction: client.TransactionPerSnap, + }, + InstanceKey: opts.instanceKey, + PreUpdateKernelModuleComponents: currentKmodComps, + }) + c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{ + RealName: snapName, + Revision: currentSnapRev, + Channel: channel, + SnapID: snapID, + }) + + // verify snaps in the system state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, instanceName, &snapst) + c.Assert(err, IsNil) + + if !opts.undo { + c.Assert(snapst.LastRefreshTime, NotNil) + c.Check(snapst.LastRefreshTime.Equal(now), Equals, true) + c.Assert(snapst.Active, Equals, true) + + // no new revision added to the sequence, the components should have + // been replaced in-place + c.Assert(snapst.Sequence.Revisions, HasLen, 1) + c.Assert(snapst.Sequence.Revisions[0], DeepEquals, expectedSideState) + } else { + // make sure everything is back to how it started + c.Assert(snapst.Active, Equals, true) + c.Assert(snapst.Sequence.Revisions, HasLen, 1) + c.Assert(snapst.Sequence.Revisions[0].Snap, DeepEquals, currentSeq.Revisions[0].Snap) + + // TODO: figure out why this is out of order and if it is a problem + c.Assert(snapst.Sequence.Revisions[0].Components, testutil.DeepUnsortedMatches, currentSeq.Revisions[0].Components) + } +} diff --git a/overlord/snapstate/target.go b/overlord/snapstate/target.go index efcae64c9712..28196e828327 100644 --- a/overlord/snapstate/target.go +++ b/overlord/snapstate/target.go @@ -860,7 +860,11 @@ func (p *updatePlan) revisionChanges(st *state.State, opts Options) ([]*snap.Inf changes := make([]*snap.Info, 0, len(updates)) for _, up := range updates { - if up.revisionSatisfied() { + ok, err := up.revisionSatisfied() + if err != nil { + return nil, err + } + if ok { continue }