From 5305352d45cac0f16f7c8deedf37fa6bee8d4f69 Mon Sep 17 00:00:00 2001 From: Andrew Phelps <136256549+andrewphelpsj@users.noreply.github.com> Date: Tue, 14 Jan 2025 16:21:28 -0500 Subject: [PATCH] o/devicestate: refactor remodeling code to use new snapstate API (#14898) * o/devicestate, overlord, daemon: refactor devicestate.Remodel api to take in a type that can wrap local snaps and components * o/devicestate: remove usage of type that can be replaced by type that contains components * o/devicestate: change remodeling to use new snapstate API and refactor in preparation for components The refactor makes it so all of the decisions about what to do for a snap in the incoming model are all in the same place. I feel that this is pretty useful now, and it will simplify adding the logic that is needed for handling components as well. * o/snapstate, o/devicestate, daemon: introduce snapstate.PathComponent that is used in snapstate.PathSnap * o/devicestate: eliminate some types that can be replaced with types from snapstate * o/devicestate: update some comments on the remodeler and methods * o/devicestate, o/snapstate: use snapstate.PathSnap as part of snapstate.PathInstallGoal api * o/devicestate: rename remodelTarget to remodelSnapTarget * o/snapstate: move PathComponent definition * o/devicestate: do not change a snap's channel during a remodel if the current snap is not tracking a channel * o/devicestate: remove unused function --- daemon/api_model.go | 14 +- daemon/api_model_test.go | 10 +- daemon/api_sideload_n_try.go | 7 +- daemon/api_sideload_n_try_test.go | 16 +- daemon/export_api_model_test.go | 3 +- overlord/devicestate/devicestate.go | 791 ++++++------- .../devicestate/devicestate_remodel_test.go | 1052 ++++++++++------- overlord/devicestate/export_test.go | 32 +- overlord/devicestate/firstboot.go | 15 +- overlord/devicestate/handlers_remodel.go | 2 +- overlord/devicestate/handlers_test.go | 13 +- overlord/managers_test.go | 50 +- overlord/snapstate/snapstate.go | 16 +- overlord/snapstate/snapstate_install_test.go | 48 +- overlord/snapstate/snapstate_update_test.go | 13 +- overlord/snapstate/target.go | 35 +- overlord/snapstate/target_test.go | 123 +- 17 files changed, 1247 insertions(+), 993 deletions(-) diff --git a/daemon/api_model.go b/daemon/api_model.go index a7f159f070c..b0a3925c747 100644 --- a/daemon/api_model.go +++ b/daemon/api_model.go @@ -38,7 +38,6 @@ import ( "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/devicestate" "github.com/snapcore/snapd/overlord/state" - "github.com/snapcore/snapd/snap" ) var ( @@ -118,7 +117,7 @@ func remodelJSON(c *Command, r *http.Request) Response { st.Lock() defer st.Unlock() - chg, err := devicestateRemodel(st, newModel, nil, nil, devicestate.RemodelOptions{ + chg, err := devicestateRemodel(st, newModel, nil, devicestate.RemodelOptions{ Offline: data.Offline, }) if err != nil { @@ -187,8 +186,7 @@ func startOfflineRemodelChange(st *state.State, newModel *asserts.Model, } *pathsToNotRemove = make([]string, 0, len(slInfo.snaps)) - sideInfos := make([]*snap.SideInfo, 0, len(slInfo.snaps)) - paths := make([]string, 0, len(slInfo.snaps)) + localSnaps := make([]devicestate.LocalSnap, 0, len(slInfo.snaps)) for _, psi := range slInfo.snaps { // Move file to the same name of what a downloaded one would have dest := filepath.Join(dirs.SnapBlobDir, @@ -197,12 +195,14 @@ func startOfflineRemodelChange(st *state.State, newModel *asserts.Model, // Avoid trying to remove a file that does not exist anymore *pathsToNotRemove = append(*pathsToNotRemove, psi.tmpPath) - sideInfos = append(sideInfos, &psi.info.SideInfo) - paths = append(paths, dest) + localSnaps = append(localSnaps, devicestate.LocalSnap{ + SideInfo: &psi.info.SideInfo, + Path: dest, + }) } // Now create and start the remodel change - chg, err := devicestateRemodel(st, newModel, sideInfos, paths, devicestate.RemodelOptions{ + chg, err := devicestateRemodel(st, newModel, localSnaps, devicestate.RemodelOptions{ // since this is the codepath that parses the form, offline is implcit // because local snaps are being provided. Offline: true, diff --git a/daemon/api_model_test.go b/daemon/api_model_test.go index 27620df7674..78de9f61e78 100644 --- a/daemon/api_model_test.go +++ b/daemon/api_model_test.go @@ -133,7 +133,7 @@ func (s *modelSuite) testPostRemodel(c *check.C, offline bool) { defer restore() var devicestateRemodelGotModel *asserts.Model - defer daemon.MockDevicestateRemodel(func(st *state.State, nm *asserts.Model, localSnaps []*snap.SideInfo, paths []string, opts devicestate.RemodelOptions) (*state.Change, error) { + defer daemon.MockDevicestateRemodel(func(st *state.State, nm *asserts.Model, localSnaps []devicestate.LocalSnap, opts devicestate.RemodelOptions) (*state.Change, error) { c.Check(opts.Offline, check.Equals, offline) devicestateRemodelGotModel = nm chg := st.NewChange("remodel", "...") @@ -600,12 +600,12 @@ func (s *modelSuite) testPostOfflineRemodel(c *check.C, params *testPostOfflineR snapRev := 1001 var devicestateRemodelGotModel *asserts.Model defer daemon.MockDevicestateRemodel(func(st *state.State, nm *asserts.Model, - localSnaps []*snap.SideInfo, paths []string, opts devicestate.RemodelOptions) (*state.Change, error) { + localSnaps []devicestate.LocalSnap, opts devicestate.RemodelOptions) (*state.Change, error) { c.Check(opts.Offline, check.Equals, true) c.Check(len(localSnaps), check.Equals, 1) - c.Check(localSnaps[0].RealName, check.Equals, snapName) - c.Check(localSnaps[0].Revision, check.Equals, snap.Revision{N: snapRev}) - c.Check(strings.HasSuffix(paths[0], + c.Check(localSnaps[0].SideInfo.RealName, check.Equals, snapName) + c.Check(localSnaps[0].SideInfo.Revision, check.Equals, snap.Revision{N: snapRev}) + c.Check(strings.HasSuffix(localSnaps[0].Path, "/var/lib/snapd/snaps/"+snapName+"_"+strconv.Itoa(snapRev)+".snap"), check.Equals, true) diff --git a/daemon/api_sideload_n_try.go b/daemon/api_sideload_n_try.go index 72d5c718b89..6a309987bc6 100644 --- a/daemon/api_sideload_n_try.go +++ b/daemon/api_sideload_n_try.go @@ -375,9 +375,12 @@ func sideloadTaskSets(ctx context.Context, st *state.State, sideload *sideloaded // handle everything else var pathSnaps []snapstate.PathSnap for _, sn := range sideload.snaps { - comps := make(map[*snap.ComponentSideInfo]string, len(sn.components)) + comps := make([]snapstate.PathComponent, 0, len(sn.components)) for _, ci := range sn.components { - comps[ci.sideInfo] = ci.tmpPath + comps = append(comps, snapstate.PathComponent{ + SideInfo: ci.sideInfo, + Path: ci.tmpPath, + }) } pathSnaps = append(pathSnaps, snapstate.PathSnap{ diff --git a/daemon/api_sideload_n_try_test.go b/daemon/api_sideload_n_try_test.go index b1288718443..abed3a82565 100644 --- a/daemon/api_sideload_n_try_test.go +++ b/daemon/api_sideload_n_try_test.go @@ -1566,9 +1566,9 @@ func (s *sideloadSuite) testSideloadManySnapsAndComponents(c *check.C, opts side comps, ok := expectedSnapsToComps[sn.SideInfo.RealName] c.Assert(ok, check.Equals, true, check.Commentf("unexpected snap name %q", sn.SideInfo.RealName)) foundComps := make([]string, 0, len(comps)) - for csi := range sn.Components { - c.Check(csi.Revision.Unset(), check.Equals, true) - foundComps = append(foundComps, csi.Component.ComponentName) + for _, comp := range sn.Components { + c.Check(comp.SideInfo.Revision.Unset(), check.Equals, true) + foundComps = append(foundComps, comp.SideInfo.Component.ComponentName) } c.Check(foundComps, testutil.DeepUnsortedMatches, comps) @@ -1733,16 +1733,16 @@ func (s *sideloadSuite) TestSideloadManyAssertedSnapsAndComponents(c *check.C) { comps, ok := snapsToComps[sn.SideInfo.RealName] c.Assert(ok, check.Equals, true, check.Commentf("unexpected snap name %q", sn.SideInfo.RealName)) foundComps := make([]string, 0, len(comps)) - for csi, compPath := range sn.Components { - c.Check(csi.Revision.Unset(), check.Equals, false) - foundComps = append(foundComps, csi.Component.ComponentName) + for _, comp := range sn.Components { + c.Check(comp.SideInfo.Revision.Unset(), check.Equals, false) + foundComps = append(foundComps, comp.SideInfo.Component.ComponentName) - container, err := snapfile.Open(compPath) + container, err := snapfile.Open(comp.Path) c.Assert(err, check.IsNil) ci, err := snap.ReadComponentInfoFromContainer(container, nil, nil) c.Assert(err, check.IsNil) - c.Check(ci.Component, check.Equals, csi.Component) + c.Check(ci.Component, check.Equals, comp.SideInfo.Component) } c.Check(foundComps, testutil.DeepUnsortedMatches, comps) diff --git a/daemon/export_api_model_test.go b/daemon/export_api_model_test.go index 236f116dd80..f9dfbcd54f9 100644 --- a/daemon/export_api_model_test.go +++ b/daemon/export_api_model_test.go @@ -23,10 +23,9 @@ import ( "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/overlord/devicestate" "github.com/snapcore/snapd/overlord/state" - "github.com/snapcore/snapd/snap" ) -func MockDevicestateRemodel(mock func(*state.State, *asserts.Model, []*snap.SideInfo, []string, devicestate.RemodelOptions) (*state.Change, error)) (restore func()) { +func MockDevicestateRemodel(mock func(*state.State, *asserts.Model, []devicestate.LocalSnap, devicestate.RemodelOptions) (*state.Change, error)) (restore func()) { oldDevicestateRemodel := devicestateRemodel devicestateRemodel = mock return func() { diff --git a/overlord/devicestate/devicestate.go b/overlord/devicestate/devicestate.go index 75f0d670458..a57f180d596 100644 --- a/overlord/devicestate/devicestate.go +++ b/overlord/devicestate/devicestate.go @@ -57,13 +57,14 @@ import ( ) var ( - snapstateInstallWithDeviceContext = snapstate.InstallWithDeviceContext - snapstateInstallPathWithDeviceContext = snapstate.InstallPathWithDeviceContext - snapstateUpdateWithDeviceContext = snapstate.UpdateWithDeviceContext - snapstateSwitch = snapstate.Switch - snapstateUpdatePathWithDeviceContext = snapstate.UpdatePathWithDeviceContext - snapstateDownload = snapstate.Download - snapstateDownloadComponents = snapstate.DownloadComponents + snapstateDownloadComponents = snapstate.DownloadComponents + snapstateDownload = snapstate.Download + snapstateUpdateOne = snapstate.UpdateOne + snapstateInstallOne = snapstate.InstallOne + snapstateStoreInstallGoal = snapstate.StoreInstallGoal + snapstatePathInstallGoal = snapstate.PathInstallGoal + snapstateStoreUpdateGoal = snapstate.StoreUpdateGoal + snapstatePathUpdateGoal = snapstate.PathUpdateGoal ) // findModel returns the device model assertion. @@ -380,7 +381,8 @@ func extractBeforeLocalModificationsEdgesTs(ts *state.TaskSet) (firstDl, lastDl, return nil, nil, nil, nil, errNoBeforeLocalModificationsEdge } tasks := ts.Tasks() - // we know we always start with downloads + // we know we always start with downloads (or prepare-snap tasks, in the + // case of an offline remodel) firstDl = tasks[0] // and always end with installs lastInst = tasks[len(tasks)-1] @@ -395,57 +397,6 @@ func extractBeforeLocalModificationsEdgesTs(ts *state.TaskSet) (firstDl, lastDl, return firstDl, tasks[edgeTaskIndex], tasks[edgeTaskIndex+1], lastInst, nil } -func isNotInstalled(err error) bool { - _, ok := err.(*snap.NotInstalledError) - return ok -} - -func notInstalled(st *state.State, name string) (bool, error) { - _, err := snapstate.CurrentInfo(st, name) - if isNotInstalled(err) { - return true, nil - } - return false, err -} - -func installedSnapRevisionChanged(st *state.State, modelSnapName string, requiredRevision snap.Revision) (bool, error) { - if requiredRevision.Unset() { - return false, nil - } - - var ss snapstate.SnapState - if err := snapstate.Get(st, modelSnapName, &ss); err != nil { - // this is unexpected as we know the snap exists - return false, err - } - - if ss.Current.Local() { - return false, errors.New("cannot determine if unasserted snap revision matches required revision") - } - - return ss.Current != requiredRevision, nil -} - -func installedSnapChannelChanged(st *state.State, modelSnapName, declaredChannel string) (changed bool, err error) { - if declaredChannel == "" { - return false, nil - } - var ss snapstate.SnapState - if err := snapstate.Get(st, modelSnapName, &ss); err != nil { - // this is unexpected as we know the snap exists - return false, err - } - if ss.Current.Local() { - // currently installed snap has a local revision, since it's - // unasserted we cannot say whether it needs a change or not - return false, nil - } - if ss.TrackingChannel != declaredChannel { - return true, nil - } - return false, nil -} - func modelSnapChannelFromDefaultOrPinnedTrack(new *asserts.Model, s *asserts.ModelSnap) (string, error) { if new.Grade() == asserts.ModelGradeUnset { if s == nil { @@ -463,298 +414,397 @@ func modelSnapChannelFromDefaultOrPinnedTrack(new *asserts.Model, s *asserts.Mod // pass both the snap name and the model snap, as it is possible that // the model snap is nil for UC16 models type modelSnapsForRemodel struct { - currentSnap string - currentModelSnap *asserts.ModelSnap - new *asserts.Model - newSnap string - newModelSnap *asserts.ModelSnap - newRequiredRevision snap.Revision - newModelValidationSets *snapasserts.ValidationSets -} + new *asserts.Model -func (ms *modelSnapsForRemodel) canHaveUC18PinnedTrack() bool { - return ms.newModelSnap != nil && - (ms.newModelSnap.SnapType == "kernel" || ms.newModelSnap.SnapType == "gadget") + oldSnap string + oldModelSnap *asserts.ModelSnap + + newSnap string + newModelSnap *asserts.ModelSnap } -type remodelVariant struct { - offline bool - localSnaps []*snap.SideInfo - localSnapPaths []string +type remodeler struct { + newModel *asserts.Model + offline bool + localSnaps map[string]snapstate.PathSnap + + // TODO:COMPS: keep track of local components here + + vsets *snapasserts.ValidationSets + tracker *snap.SelfContainedSetPrereqTracker + deviceCtx snapstate.DeviceContext + fromChange string } -type pathSideInfo struct { - localSi *snap.SideInfo - path string +// remodelSnapTarget represents a snap that is part of the model that we are +// remodeling to. +type remodelSnapTarget struct { + // name is the name of the snap. + name string + // channel is the channel that the snap should be installed from and track. + channel string + // newModelSnap is the model snap for this target. This might be nil for + // either the snapd snap (which is implicitly in the model) or for the base + // snap on UC16 models. Always check for nil before using. + newModelSnap *asserts.ModelSnap + // oldModelSnap is the corresponding model snap for the snap that this + // target is replacing. This will be nil for non-essential snaps, and it + // might be nil for the snapd snap (which is implicitly in the model) or for + // the base snap on UC16 models. Always check for nil before using. + oldModelSnap *asserts.ModelSnap } -func (ro *remodelVariant) UpdateWithDeviceContext(st *state.State, snapName string, snapID string, opts *snapstate.RevisionOptions, - userID int, snapStateFlags snapstate.Flags, tracker snapstate.PrereqTracker, - deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { - logger.Debugf("snap %s track changed", snapName) - if opts == nil { - opts = &snapstate.RevisionOptions{} - } +// canHaveUC18PinnedTrack returns whether the given model snap can have a pinned +// track. Only the kernel and gadget snaps from a UC18 model can have a pinned +// track. Note that this is different than the default-channel that is used for +// UC20+ models. +func canHaveUC18PinnedTrack(ms *asserts.ModelSnap) bool { + return ms != nil && (ms.SnapType == "kernel" || ms.SnapType == "gadget") +} - // if an online context, we can go directly to the store - if !ro.offline { - return snapstateUpdateWithDeviceContext(st, snapName, opts, - userID, snapStateFlags, tracker, deviceCtx, fromChange) - } +// uc20Model returns true if the given model is a UC20+ model. UC20+ models can +// be identified by the presence of a grade in the model. +func uc20Model(m *asserts.Model) bool { + return m.Grade() != asserts.ModelGradeUnset +} - pathSI := ro.maybeSideInfoAndPathFromID(snapID) +type remodelAction int - // if we find the side info in the locally provided snaps, then we can - // directly call snapstate.UpdatePathWithDeviceContext on it - if pathSI != nil { - return snapstateUpdatePathWithDeviceContext(st, pathSI.localSi, pathSI.path, snapName, opts, - userID, snapStateFlags, tracker, deviceCtx, fromChange) - } +const ( + remodelInvalidAction remodelAction = iota + remodelNoAction + remodelChannelSwitch + remodelInstallAction + remodelUpdateAction +) - // if we cannot find the side info in the locally provided snaps, then we - // will try to use an already installed snap. if the installed snap does not - // match the requested revision, then we will return an error. if the snap - // does match the requested revision, then we will switch the channel to the - // requested channel. see the comment below about how calling this method in - // the case where the snap needs neither a revision nor channel change would - // be a bug. +func (r *remodeler) maybeInstallOrUpdate(ctx context.Context, st *state.State, rt remodelSnapTarget) (remodelAction, []*state.TaskSet, error) { + var snapst snapstate.SnapState + if err := snapstate.Get(st, rt.name, &snapst); err != nil { + if !errors.Is(err, state.ErrNoState) { + return 0, nil, err + } - // TODO: currently, we only consider the snap revision that currently - // installed. this should also take into account other revisions that we - // might have on the system (the revisions in SnapState.Sequence) - info, err := snapstate.CurrentInfo(st, snapName) - if err != nil { - // this case is unexpected, since UpdateWithDeviceContext should - // only be called if the snap is already installed - if errors.Is(err, &snap.NotInstalledError{}) { - return nil, fmt.Errorf("internal error: no snap file provided for %q", snapName) + // if the snap isn't already installed and it isn't required, then we + // can skip installing it. anything that has a nil model snap is + // implicitly required (either snapd or a UC16 base) + if rt.newModelSnap != nil && rt.newModelSnap.Presence != "required" { + return remodelNoAction, nil, nil } - return nil, err - } - if opts != nil && !opts.Revision.Unset() && info.Revision != opts.Revision { - var ss snapstate.SnapState - if err := snapstate.Get(st, snapName, &ss); err != nil { - return nil, err + goal, err := r.installGoal(rt) + if err != nil { + return 0, nil, err } - // if the current revision isn't the revision that is installed, then - // look at the previous revisions that we have to see if any of those - // match - if ss.Sequence.LastIndex(opts.Revision) == -1 { - return nil, fmt.Errorf("installed snap %q does not match revision required to be used for offline remodel: %s != %s", snapName, opts.Revision, info.Revision) + _, ts, err := snapstateInstallOne(ctx, st, goal, snapstate.Options{ + DeviceCtx: r.deviceCtx, + FromChange: r.fromChange, + PrereqTracker: r.tracker, + Flags: snapstate.Flags{NoReRefresh: true, Required: true}, + }) + if err != nil { + return 0, nil, err } - // this won't reach out to the store since we know that we already have - // the snap revision on disk - return snapstateUpdateWithDeviceContext(st, snapName, opts, - userID, snapStateFlags, tracker, deviceCtx, fromChange) + return remodelInstallAction, []*state.TaskSet{ts}, nil } - // this would only occur from programmer error, since - // UpdateWithDeviceContext should only be called if either the snap revision - // or channel needs to change. once we get here, we know that the revision - // is the same, so the channel should be different. - if opts == nil || opts.Channel == info.Channel { - return nil, fmt.Errorf("internal error: installed snap %q already on channel %q", snapName, info.Channel) + // on UC20+ models, we look at the currently tracked channel to determine if + // we are switching the channel. on UC18 models, we compare the pinned track + // on the new model snap with the pinned track on the old model snap. note + // that only the kernel and gadget snaps can have a pinned track on UC18 + // models. + var currentChannelOrTrack string + if uc20Model(r.newModel) { + currentChannelOrTrack = snapst.TrackingChannel + } else if canHaveUC18PinnedTrack(rt.oldModelSnap) { + currentChannelOrTrack = rt.oldModelSnap.PinnedTrack } + needsChannelChange := rt.channel != "" && rt.channel != currentChannelOrTrack && !snapst.Current.Local() - // since snapstate.Switch doesn't take a prereq tracker, we need to add - // it explicitly - tracker.Add(info) + currentInfo, err := snapst.CurrentInfo() + if err != nil { + return 0, nil, err + } - return snapstateSwitch(st, snapName, opts) -} + constraints, err := r.vsets.Presence(naming.Snap(rt.name)) + if err != nil { + return 0, nil, err + } -func (ro *remodelVariant) InstallWithDeviceContext(ctx context.Context, st *state.State, - snapName string, snapID string, opts *snapstate.RevisionOptions, userID int, - snapStateFlags snapstate.Flags, tracker snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, - fromChange string) (*state.TaskSet, error) { - logger.Debugf("snap %s needs install", snapName) - if opts == nil { - opts = &snapstate.RevisionOptions{} + if !constraints.Revision.Unset() && snapst.Current.Local() { + return 0, nil, errors.New("cannot determine if unasserted snap revision matches required revision") } - if ro.offline { - pathSI := ro.maybeSideInfoAndPathFromID(snapID) - // if we can't find the snap as a locally provided snap, then there is - // nothing to do but return an error. that is because this method should - // only be called if the snap is not already installed. - if pathSI == nil { - return nil, fmt.Errorf("no snap file provided for %q", snapName) + // we need to change the revision if either the incoming model's validation + // sets require a specific revision that we don't have installed + // + // TODO: if the current revision doesn't support the components that we + // need, will also need to change the revision here + needsRevisionChange := (!constraints.Revision.Unset() && constraints.Revision != snapst.Current) + + // TODO: we don't properly handle snaps and components that are invalid in + // the incoming model and required by the previous model. this would require + // removing things during a remodel, which isn't something we do at the + // moment. afaict, there it is impossible to remodel from a model that + // requires a snap that is invalid in the incoming model. + + switch { + case needsRevisionChange || needsChannelChange: + if r.shouldJustSwitch(rt, needsRevisionChange) { + ts, err := snapstate.Switch(st, rt.name, &snapstate.RevisionOptions{ + Channel: rt.channel, + }) + if err != nil { + return 0, nil, err + } + + return remodelChannelSwitch, []*state.TaskSet{ts}, nil } - return snapstateInstallPathWithDeviceContext(st, pathSI.localSi, pathSI.path, snapName, opts, - userID, snapStateFlags, tracker, deviceCtx, fromChange) - } - return snapstateInstallWithDeviceContext(ctx, st, snapName, - opts, userID, snapStateFlags, tracker, deviceCtx, fromChange) -} + goal, err := r.updateGoal(st, rt, constraints) + if err != nil { + return 0, nil, err + } -// maybeSideInfoAndPathFromID returns the SideInfo/path for a given snap ID. -// Note that this will work only for asserted snaps, that is the only case we -// support for remodeling at the moment. If the snap cannot be found, then nil -// is returned. -func (ro *remodelVariant) maybeSideInfoAndPathFromID(id string) *pathSideInfo { - for i, si := range ro.localSnaps { - if si.SnapID == id { - return &pathSideInfo{localSi: ro.localSnaps[i], path: ro.localSnapPaths[i]} + ts, err := snapstateUpdateOne(ctx, st, goal, nil, snapstate.Options{ + DeviceCtx: r.deviceCtx, + FromChange: r.fromChange, + PrereqTracker: r.tracker, + Flags: snapstate.Flags{NoReRefresh: true}, + }) + if err != nil { + return 0, nil, err } + + // if there are any local modfifications, we know that we're doing more + // than a channel switch + if ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge) != nil { + return remodelUpdateAction, []*state.TaskSet{ts}, nil + } + + return remodelChannelSwitch, []*state.TaskSet{ts}, nil + default: + // nothing to do but add the snap to the prereq tracker + r.tracker.Add(currentInfo) + return remodelNoAction, nil, nil } - return nil } -func revisionOptionsForRemodel(channel string, revision snap.Revision, valsets *snapasserts.ValidationSets) *snapstate.RevisionOptions { - opts := &snapstate.RevisionOptions{ - Channel: channel, - Revision: revision, +func (r *remodeler) shouldJustSwitch(rt remodelSnapTarget, needsRevisionChange bool) bool { + if !r.offline { + return false + } + + if needsRevisionChange { + return false } - if !opts.Revision.Unset() { - opts.ValidationSets = valsets + // if we have a local container for this snap, then we should use that in + // addition to switching the tracked channel + if _, ok := r.localSnaps[rt.name]; ok { + return false } - return opts + return true } -func remodelEssentialSnapTasks(ctx context.Context, st *state.State, ms modelSnapsForRemodel, remodelVar remodelVariant, deviceCtx snapstate.DeviceContext, fromChange string, tracker snapstate.PrereqTracker) (*state.TaskSet, error) { - userID := 0 - newModelSnapChannel, err := modelSnapChannelFromDefaultOrPinnedTrack(ms.new, ms.newModelSnap) - if err != nil { - return nil, err +func (r *remodeler) installGoal(sn remodelSnapTarget) (snapstate.InstallGoal, error) { + if r.offline { + ls, ok := r.localSnaps[sn.name] + if !ok { + return nil, fmt.Errorf("no snap file provided for %q", sn.name) + } + + opts := snapstate.RevisionOptions{ + Channel: sn.channel, + ValidationSets: r.vsets, + } + + return snapstatePathInstallGoal(snapstate.PathSnap{ + Path: ls.Path, + SideInfo: ls.SideInfo, + RevOpts: opts, + }), nil } - revOpts := revisionOptionsForRemodel(newModelSnapChannel, ms.newRequiredRevision, ms.newModelValidationSets) + return snapstateStoreInstallGoal(snapstate.StoreSnap{ + InstanceName: sn.name, + RevOpts: snapstate.RevisionOptions{ + Channel: sn.channel, + ValidationSets: r.vsets, + }, + }), nil +} + +// installedRevisionUpdateGoal returns an update goal which will install a snap +// revision that was previously installed on the system and still in the +// sequence. Note that this is using a [snapstate.StoreUpdateGoal], but it does +// not actually reach out to the store. +func (r *remodeler) installedRevisionUpdateGoal( + st *state.State, + sn remodelSnapTarget, + constraints snapasserts.SnapPresenceConstraints, +) (snapstate.UpdateGoal, error) { + if constraints.Revision.Unset() { + return nil, errors.New("internal error: falling back to a previous revision requires that we have a speicifc revision to pick") + } - var newSnapID string - // a nil model snap will happen for bases on UC16 models. - if ms.newModelSnap != nil { - newSnapID = ms.newModelSnap.SnapID + var snapst snapstate.SnapState + if err := snapstate.Get(st, sn.name, &snapst); err != nil { + return nil, err } - if ms.currentSnap == ms.newSnap { - // new model uses the same base, kernel or gadget snap - channelChanged := false - if ms.new.Grade() != asserts.ModelGradeUnset { - // UC20 models can specify default channel for all snaps - // including base, kernel and gadget - channelChanged, err = installedSnapChannelChanged(st, ms.newSnap, newModelSnapChannel) + index := snapst.LastIndex(constraints.Revision) + if index == -1 { + return nil, fmt.Errorf("installed snap %q does not have the required revision in its sequence to be used for offline remodel: %s", sn.name, constraints.Revision) + } + + return snapstateStoreUpdateGoal(snapstate.StoreUpdate{ + InstanceName: sn.name, + RevOpts: snapstate.RevisionOptions{ + Channel: sn.channel, + ValidationSets: r.vsets, + Revision: constraints.Revision, + }, + }), nil +} + +func (r *remodeler) updateGoal(st *state.State, sn remodelSnapTarget, constraints snapasserts.SnapPresenceConstraints) (snapstate.UpdateGoal, error) { + if r.offline { + ls, ok := r.localSnaps[sn.name] + if !ok { + // this attempts to create a snapstate.StoreUpdateGoal that will + // switch back to a previously installed snap revision that is still + // in the sequence + g, err := r.installedRevisionUpdateGoal(st, sn, constraints) if err != nil { return nil, err } - } else if ms.canHaveUC18PinnedTrack() { - // UC18 models could only specify track for the kernel - // and gadget snaps - channelChanged = ms.currentModelSnap.PinnedTrack != ms.newModelSnap.PinnedTrack - } - - revisionChanged, err := installedSnapRevisionChanged(st, ms.newSnap, ms.newRequiredRevision) - if err != nil { - return nil, err + return g, nil } - if channelChanged || revisionChanged { - // new model specifies the same snap, but with a new channel or - // different revision than the existing one - return remodelVar.UpdateWithDeviceContext(st, ms.newSnap, newSnapID, revOpts, userID, - snapstate.Flags{NoReRefresh: true}, tracker, deviceCtx, fromChange, - ) + opts := snapstate.RevisionOptions{ + Channel: sn.channel, + ValidationSets: r.vsets, } - // if we are here, then the snap is already installed and does not need - // any changes. thus, add it to the prereq tracker. - info, err := snapstate.CurrentInfo(st, ms.currentSnap) - if err != nil { - return nil, err - } - tracker.Add(info) + // TODO: verify against validation sets, since we don't do that in + // snapstate for by-path installs (why don't we?) - return nil, nil + return snapstatePathUpdateGoal(snapstate.PathSnap{ + Path: ls.Path, + SideInfo: ls.SideInfo, + RevOpts: opts, + }), nil } - // new model specifies a different snap - needsInstall, err := notInstalled(st, ms.newModelSnap.SnapName()) + return snapstateStoreUpdateGoal(snapstate.StoreUpdate{ + InstanceName: sn.name, + RevOpts: snapstate.RevisionOptions{ + Channel: sn.channel, + ValidationSets: r.vsets, + }, + }), nil +} + +func remodelEssentialSnapTasks( + ctx context.Context, + st *state.State, + rm remodeler, + ms modelSnapsForRemodel, +) ([]*state.TaskSet, error) { + newModelSnapChannel, err := modelSnapChannelFromDefaultOrPinnedTrack(ms.new, ms.newModelSnap) if err != nil { return nil, err } - if needsInstall { - // which needs to be installed - return remodelVar.InstallWithDeviceContext(ctx, st, ms.newSnap, newSnapID, revOpts, userID, - snapstate.Flags{}, tracker, deviceCtx, fromChange, - ) - } - // in UC20+ models, the model can specify a channel for each - // snap, thus making it possible to change already installed - // kernel or base snaps - channelChanged := false - if ms.new.Grade() != asserts.ModelGradeUnset { - channelChanged, err = installedSnapChannelChanged(st, ms.newModelSnap.SnapName(), newModelSnapChannel) - if err != nil { - return nil, err - } + rt := remodelSnapTarget{ + name: ms.newSnap, + channel: newModelSnapChannel, + newModelSnap: ms.newModelSnap, + oldModelSnap: ms.oldModelSnap, } - revisionChanged, err := installedSnapRevisionChanged(st, ms.newSnap, ms.newRequiredRevision) + action, tss, err := rm.maybeInstallOrUpdate(ctx, st, rt) if err != nil { return nil, err } - if !channelChanged && !revisionChanged { - // if we are here, the new snap is already installed. thus, add it to - // the prereq tracker. - info, err := snapstate.CurrentInfo(st, ms.newSnap) - if err != nil { - return nil, err - } - tracker.Add(info) + // if we're not swapping to a new essential snap, then it should already be + // fully available during the remodel. + if ms.newSnap == ms.oldSnap { + return tss, nil + } + // below covers some edge cases for remodeling when the current system + // already has some of the new model's essential snaps installed. + // + // note that it may seem that we are unnecessarily handling some cases for + // kernels and gadgets, which usually are exclusive on a system. however, + // since we do not remove snaps during a remodel, a system might have + // multiple gadget or kernel snaps installed from a previous remodel. in + // those cases, we will need to create the tasks to make them available + // during the remodel, since they won't have been boot participants until + // now. + + // when we're not modifying anything to do with the snap itself, we need to + // create some tasks to ensure that the essential snap is available during + // the remodel. this is done in the link-snap task, which checks to see if + // the snap is a boot participant. + switchEssentialTasks := func(name, fromChange string) (*state.TaskSet, error) { if ms.newModelSnap != nil && ms.newModelSnap.SnapType == "gadget" { - return snapstate.SwitchToNewGadget(st, ms.newSnap, fromChange) + return snapstate.SwitchToNewGadget(st, name, fromChange) } - return snapstate.LinkNewBaseOrKernel(st, ms.newSnap, fromChange) + return snapstate.LinkNewBaseOrKernel(st, name, fromChange) } - ts, err := remodelVar.UpdateWithDeviceContext(st, - ms.newSnap, newSnapID, revOpts, userID, - snapstate.Flags{NoReRefresh: true}, tracker, deviceCtx, fromChange) - if err != nil { - return nil, err - } + // as a bit of a special case, we support adding the needed tasks that make + // the snap available during the remodel to an existing task set. this is + // used when we create a task set that only changes the snap's channel. + appendSwitchEssentialTasks := func(tss []*state.TaskSet) (*state.TaskSet, error) { + if len(tss) != 1 { + return nil, errors.New("internal error: a channel switch should only have one task set") + } - if edgeTask := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge); edgeTask != nil { - // no task is marked as being last before local modifications are - // introduced, indicating that the update is a simple - // switch-snap-channel - return ts, nil + if ms.newModelSnap != nil && ms.newModelSnap.SnapType == "gadget" { + return snapstate.AddGadgetAssetsTasks(st, tss[0]) + } + return snapstate.AddLinkNewBaseOrKernel(st, tss[0]) } - switch ms.newModelSnap.SnapType { - case "kernel", "base": - // in other cases make sure that the kernel or base is linked and - // available, and that kernel updates boot assets if needed - ts, err = snapstate.AddLinkNewBaseOrKernel(st, ts) + switch action { + case remodelUpdateAction, remodelInstallAction: + // if we're updating or installing a new essential snap, everything will + // already be handled + return tss, nil + case remodelNoAction: + ts, err := switchEssentialTasks(ms.newSnap, rm.fromChange) if err != nil { return nil, err } - case "gadget": - // gadget snaps may need gadget related tasks such as assets update or - // command line update - ts, err = snapstate.AddGadgetAssetsTasks(st, ts) + return append(tss, ts), nil + case remodelChannelSwitch: + ts, err := appendSwitchEssentialTasks(tss) if err != nil { return nil, err } + return []*state.TaskSet{ts}, nil + default: + return nil, fmt.Errorf("internal error: unhandled remodel action: %d", action) } - return ts, nil } // tasksForEssentialSnap returns tasks for essential snaps (actually, // except for the snapd snap). -func tasksForEssentialSnap(ctx context.Context, st *state.State, - snapType string, current, new *asserts.Model, - revision snap.Revision, vsets *snapasserts.ValidationSets, remodelVar remodelVariant, - tracker snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string, -) (*state.TaskSet, error) { +func tasksForEssentialSnap( + ctx context.Context, + st *state.State, + snapType string, + current, new *asserts.Model, + rm remodeler, +) ([]*state.TaskSet, error) { var currentSnap, newSnap string var currentModelSnap, newModelSnap *asserts.ModelSnap switch snapType { @@ -778,26 +828,16 @@ func tasksForEssentialSnap(ctx context.Context, st *state.State, } ms := modelSnapsForRemodel{ - currentSnap: currentSnap, - currentModelSnap: currentModelSnap, - new: new, - newSnap: newSnap, - newModelSnap: newModelSnap, - newRequiredRevision: revision, - newModelValidationSets: vsets, - } - ts, err := remodelEssentialSnapTasks(ctx, st, ms, remodelVar, deviceCtx, fromChange, tracker) - if err != nil { - return nil, err + oldSnap: currentSnap, + oldModelSnap: currentModelSnap, + new: new, + newSnap: newSnap, + newModelSnap: newModelSnap, } - return ts, err + return remodelEssentialSnapTasks(ctx, st, rm, ms) } -func remodelSnapdSnapTasks( - st *state.State, newModel *asserts.Model, rev snap.Revision, - vsets *snapasserts.ValidationSets, remodelVar remodelVariant, - tracker snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string, -) (*state.TaskSet, error) { +func remodelSnapdSnapTasks(ctx context.Context, st *state.State, rm remodeler) ([]*state.TaskSet, error) { // First check if snapd snap is installed at all (might be the case // for uc16, which happens for some tests). var ss snapstate.SnapState @@ -810,30 +850,20 @@ func remodelSnapdSnapTasks( // Implicit new channel if snapd is not explicitly in the model newSnapdChannel := "latest/stable" - essentialSnaps := newModel.EssentialSnaps() + essentialSnaps := rm.newModel.EssentialSnaps() if essentialSnaps[0].SnapType == "snapd" { // snapd can be specified explicitly in the model (UC20+) newSnapdChannel = essentialSnaps[0].DefaultChannel } - channelChanged, err := installedSnapChannelChanged(st, "snapd", newSnapdChannel) - if err != nil { - return nil, err - } - - revisionChanged, err := installedSnapRevisionChanged(st, "snapd", rev) + _, tss, err := rm.maybeInstallOrUpdate(ctx, st, remodelSnapTarget{ + name: "snapd", + channel: newSnapdChannel, + }) if err != nil { return nil, err } - - if channelChanged || revisionChanged { - revOpts := revisionOptionsForRemodel(newSnapdChannel, rev, vsets) - - userID := 0 - return remodelVar.UpdateWithDeviceContext(st, "snapd", naming.WellKnownSnapID("snapd"), revOpts, userID, - snapstate.Flags{NoReRefresh: true}, tracker, deviceCtx, fromChange) - } - return nil, nil + return tss, nil } func sortNonEssentialRemodelTaskSetsBasesFirst(snaps []*asserts.ModelSnap) []*asserts.ModelSnap { @@ -855,43 +885,40 @@ func sortNonEssentialRemodelTaskSetsBasesFirst(snaps []*asserts.ModelSnap) []*as } func remodelTasks(ctx context.Context, st *state.State, current, new *asserts.Model, - deviceCtx snapstate.DeviceContext, fromChange string, localSnaps []*snap.SideInfo, paths []string, opts RemodelOptions) ([]*state.TaskSet, error) { + deviceCtx snapstate.DeviceContext, fromChange string, localSnaps []LocalSnap, opts RemodelOptions) ([]*state.TaskSet, error) { logger.Debugf("creating remodeling tasks") - var tss []*state.TaskSet - tracker := snap.NewSelfContainedSetPrereqTracker() + vsets, err := verifyModelValidationSets(st, new, opts.Offline, deviceCtx) + if err != nil { + return nil, err + } // If local snaps are provided, all needed snaps must be locally // provided. We check this flag whenever a snap installation/update is // found needed for the remodel. - remodelVar := remodelVariant{ - offline: opts.Offline, - localSnaps: localSnaps, - localSnapPaths: paths, - } - - validationSets, err := verifyModelValidationSets(st, new, opts.Offline, deviceCtx) - if err != nil { - return nil, err + rm := remodeler{ + newModel: new, + offline: opts.Offline, + vsets: vsets, + tracker: snap.NewSelfContainedSetPrereqTracker(), + deviceCtx: deviceCtx, + fromChange: fromChange, + localSnaps: make(map[string]snapstate.PathSnap, len(localSnaps)), } - // any snap that has a required revision will be in this map, if the snap's - // version is unconstrained, then we'll get a default-initialized revision - // from the map - snapRevisions, err := validationSets.Revisions() - if err != nil { - return nil, err + for _, ls := range localSnaps { + rm.localSnaps[ls.SideInfo.RealName] = snapstate.PathSnap{ + Path: ls.Path, + SideInfo: ls.SideInfo, + } } // First handle snapd as a special case - ts, err := remodelSnapdSnapTasks(st, new, snapRevisions["snapd"], validationSets, remodelVar, tracker, deviceCtx, fromChange) + tss, err := remodelSnapdSnapTasks(ctx, st, rm) if err != nil { return nil, err } - if ts != nil { - tss = append(tss, ts) - } // TODO: this order is not correct, and needs to be changed to match the // order that is described in the comment on essentialSnapsRestartOrder in @@ -903,15 +930,11 @@ func remodelTasks(ctx context.Context, st *state.State, current, new *asserts.Mo // Already handled continue } - ts, err := tasksForEssentialSnap(ctx, st, - modelSnap.SnapType, current, new, - snapRevisions[modelSnap.SnapName()], validationSets, remodelVar, tracker, deviceCtx, fromChange) + sets, err := tasksForEssentialSnap(ctx, st, modelSnap.SnapType, current, new, rm) if err != nil { return nil, err } - if ts != nil { - tss = append(tss, ts) - } + tss = append(tss, sets...) } // if base is not set, then core will not be returned in the list of snaps @@ -924,7 +947,7 @@ func remodelTasks(ctx context.Context, st *state.State, current, new *asserts.Mo if err != nil { return nil, err } - tracker.Add(currentBase) + rm.tracker.Add(currentBase) } // sort the snaps so that we collect the task sets for base snaps first, and @@ -932,108 +955,33 @@ func remodelTasks(ctx context.Context, st *state.State, current, new *asserts.Mo // snap, but the base is not yet installed. snapsWithoutEssential := sortNonEssentialRemodelTaskSetsBasesFirst(new.SnapsWithoutEssential()) - const userID = 0 - // go through all the model snaps, see if there are new required snaps // or a track for existing ones needs to be updated for _, modelSnap := range snapsWithoutEssential { logger.Debugf("adding remodel tasks for non-essential snap %s", modelSnap.Name) - // TODO|XXX: have methods that take refs directly - // to respect the snap ids - currentInfo, err := snapstate.CurrentInfo(st, modelSnap.SnapName()) - needsInstall := false - if err != nil { - if !isNotInstalled(err) { - return nil, err - } - - // if the snap isn't already installed, and it isn't required, - // then there is nothing to do. note that if the snap is installed, - // we might need to change the channel. - if modelSnap.Presence != "required" { - continue - } - - needsInstall = true - } - // default channel can be set only in UC20 models newModelSnapChannel, err := modelSnapChannelFromDefaultOrPinnedTrack(new, modelSnap) if err != nil { return nil, err } - revOpts := revisionOptionsForRemodel(newModelSnapChannel, snapRevisions[modelSnap.SnapName()], validationSets) - - if needsInstall { - ts, err := remodelVar.InstallWithDeviceContext(ctx, st, modelSnap.SnapName(), modelSnap.ID(), revOpts, - userID, snapstate.Flags{Required: true}, tracker, deviceCtx, - fromChange) - if err != nil { - return nil, err - } - tss = append(tss, ts) - - continue - } - - // the snap is already installed and has its default channel declared in - // the model, but the local install may be tracking a different channel - channelChanged, err := installedSnapChannelChanged(st, modelSnap.SnapName(), newModelSnapChannel) - if err != nil { - return nil, err - } - - // the validation-sets might require a specific version of the snap - revisionChanged, err := installedSnapRevisionChanged( - st, modelSnap.SnapName(), snapRevisions[modelSnap.SnapName()], - ) + _, sets, err := rm.maybeInstallOrUpdate(ctx, st, remodelSnapTarget{ + name: modelSnap.SnapName(), + channel: newModelSnapChannel, + newModelSnap: modelSnap, + }) if err != nil { return nil, err } - - // snap is installed already, so we have 2 possible scenarios: - // 1. the snap will be updated (new channel or revision), in which case - // we should make sure that the prerequisites of the new revision are - // accounted for - // 2. the snap channel or revision is not being modified so grab - // whatever is required for the current revision - if channelChanged || revisionChanged { - ts, err := remodelVar.UpdateWithDeviceContext(st, - modelSnap.SnapName(), modelSnap.ID(), - revOpts, userID, snapstate.Flags{NoReRefresh: true}, tracker, - deviceCtx, fromChange, - ) - if err != nil { - return nil, err - } - tss = append(tss, ts) - - // we can know that the snap's revision was changed by checking for - // the presence of an edge on the task set that separates tasks that - // do and do not modify the system. if the edge is present, then the - // revision was changed, and we need to extract the snap's - // prerequisites from the task set. the absence of this edge, - // indicates that only the snap's channel was changed and the - // revision was unchanged. in this case, we treat the snap as if it - // were unchanged. - if ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge) != nil { - continue - } - } - - // if we're here, the snap that is installed is unchanged from the snap - // that the model requires. the snap may have had a channel change, but - // that channel change did not result in a revision change. - tracker.Add(currentInfo) + tss = append(tss, sets...) } - if err := checkRequiredGadgetMatchesModelBase(new, tracker); err != nil { + if err := checkRequiredGadgetMatchesModelBase(new, rm.tracker); err != nil { return nil, err } - warnings, errs := tracker.Check() + warnings, errs := rm.tracker.Check() for _, w := range warnings { logger.Noticef("remodel prerequisites warning: %v", w) } @@ -1246,6 +1194,8 @@ func checkForRequiredSnapsNotRequiredInModel(model *asserts.Model, vSets *snapas } } + // TODO:COMPS: consider relationship with required components here + return nil } @@ -1255,7 +1205,12 @@ func checkForInvalidSnapsInModel(model *asserts.Model, vSets *snapasserts.Valida } for _, sn := range model.AllSnaps() { - if !vSets.CanBePresent(sn) { + pres, err := vSets.Presence(sn) + if err != nil { + return err + } + + if pres.Presence == asserts.PresenceInvalid { return fmt.Errorf("snap presence is marked invalid by validation set: %s", sn.SnapName()) } } @@ -1297,7 +1252,7 @@ type RemodelOptions struct { // (need to check that even unchanged snaps are accessible) // - Make sure this works with Core 20 as well, in the Core 20 case // we must enforce the default-channels from the model as well -func Remodel(st *state.State, new *asserts.Model, localSnaps []*snap.SideInfo, paths []string, opts RemodelOptions) (*state.Change, error) { +func Remodel(st *state.State, new *asserts.Model, localSnaps []LocalSnap, opts RemodelOptions) (*state.Change, error) { var seeded bool err := st.Get("seeded", &seeded) if err != nil && !errors.Is(err, state.ErrNoState) { @@ -1435,7 +1390,7 @@ func Remodel(st *state.State, new *asserts.Model, localSnaps []*snap.SideInfo, p // the remodel are added to an existing and running change. this will // allow us to avoid things like calling snapstate.CheckChangeConflictRunExclusively again. var err error - tss, err = remodelTasks(context.TODO(), st, current, new, remodCtx, "", localSnaps, paths, opts) + tss, err = remodelTasks(context.TODO(), st, current, new, remodCtx, "", localSnaps, opts) if err != nil { return nil, err } @@ -1605,11 +1560,11 @@ func createRecoverySystemTasks(st *state.State, label string, snapSetupTasks, co // that is represented by the snap.SideInfo. type LocalSnap struct { // SideInfo is the snap.SideInfo struct that represents a local snap that - // will be used to create a recovery system. + // will be used to create a recovery system or remodel the system. SideInfo *snap.SideInfo // Path is the path on disk to a snap that will be used to create a recovery - // system. + // system or remodel the system. Path string } diff --git a/overlord/devicestate/devicestate_remodel_test.go b/overlord/devicestate/devicestate_remodel_test.go index 4fc5bbffd63..93dde57285c 100644 --- a/overlord/devicestate/devicestate_remodel_test.go +++ b/overlord/devicestate/devicestate_remodel_test.go @@ -68,6 +68,59 @@ func (s *deviceMgrRemodelSuite) SetUpTest(c *C) { classic := false s.setupBaseTest(c, classic) snapstate.EnforceLocalValidationSets = assertstate.ApplyLocalEnforcedValidationSets + + devicestate.MockSnapstateStoreInstallGoal(newStoreInstallGoalRecorder) + devicestate.MockSnapstateStoreUpdateGoal(newStoreUpdateGoalRecorder) + devicestate.MockSnapstatePathInstallGoal(newPathInstallGoalRecorder) + devicestate.MockSnapstatePathUpdateGoal(newPathUpdateGoalRecorder) +} + +type storeInstallGoalRecorder struct { + snapstate.InstallGoal + snaps []snapstate.StoreSnap +} + +func newStoreInstallGoalRecorder(snaps ...snapstate.StoreSnap) snapstate.InstallGoal { + return &storeInstallGoalRecorder{ + snaps: snaps, + InstallGoal: snapstate.StoreInstallGoal(snaps...), + } +} + +type pathUpdateGoalRecorder struct { + snapstate.UpdateGoal + snaps []snapstate.PathSnap +} + +func newPathUpdateGoalRecorder(snaps ...snapstate.PathSnap) snapstate.UpdateGoal { + return &pathUpdateGoalRecorder{ + snaps: snaps, + UpdateGoal: snapstate.PathUpdateGoal(snaps...), + } +} + +type storeUpdateGoalRecorder struct { + snapstate.UpdateGoal + snaps []snapstate.StoreUpdate +} + +func newStoreUpdateGoalRecorder(snaps ...snapstate.StoreUpdate) snapstate.UpdateGoal { + return &storeUpdateGoalRecorder{ + snaps: snaps, + UpdateGoal: snapstate.StoreUpdateGoal(snaps...), + } +} + +type pathInstallGoalRecorder struct { + snap snapstate.PathSnap + snapstate.InstallGoal +} + +func newPathInstallGoalRecorder(sn snapstate.PathSnap) snapstate.InstallGoal { + return &pathInstallGoalRecorder{ + snap: sn, + InstallGoal: snapstate.PathInstallGoal(sn), + } } func (s *deviceMgrRemodelSuite) TestRemodelUnhappyNotSeeded(c *C) { @@ -80,7 +133,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelUnhappyNotSeeded(c *C) { "kernel": "pc-kernel", "gadget": "pc", }) - _, err := devicestate.Remodel(s.state, newModel, nil, nil, devicestate.RemodelOptions{}) + _, err := devicestate.Remodel(s.state, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, ErrorMatches, "cannot remodel until fully seeded") } @@ -110,7 +163,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelSnapdBasedToCoreBased(c *C) { "revision": "1", }) - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, ErrorMatches, `cannot remodel from UC18\+ \(using snapd snap\) system back to UC16 system \(using core snap\)`) c.Assert(chg, IsNil) } @@ -191,7 +244,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelUnhappy(c *C) { } { mergeMockModelHeaders(cur, t.new) new := s.brands.Model(t.new["brand"].(string), t.new["model"].(string), t.new) - chg, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Check(chg, IsNil) c.Check(err, ErrorMatches, t.errStr) } @@ -228,7 +281,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelFromClassicUnhappy(c *C) { "classic": cur["classic"], }) - _, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + _, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Check(err, ErrorMatches, `cannot remodel from classic \(non-hybrid\) model`) } @@ -266,7 +319,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelCheckGrade(c *C) { c.Logf("tc: %v", idx) mergeMockModelHeaders(cur, t.new) new := s.brands.Model(t.new["brand"].(string), t.new["model"].(string), t.new) - chg, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Check(chg, IsNil) c.Check(err, ErrorMatches, t.errStr) } @@ -302,7 +355,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelCannotUseOldModel(c *C) { } mergeMockModelHeaders(cur, newModelHdrs) new := s.brands.Model("canonical", "pc-model", newModelHdrs) - chg, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Check(chg, IsNil) c.Check(err, ErrorMatches, "cannot remodel to older revision 1 of model canonical/pc-model than last revision 2 known to the device") } @@ -336,7 +389,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelRequiresSerial(c *C) { } mergeMockModelHeaders(cur, newModelHdrs) new := s.brands.Model("canonical", "pc-model", newModelHdrs) - chg, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Check(chg, IsNil) c.Check(err, ErrorMatches, "cannot remodel without a serial") } @@ -363,10 +416,15 @@ func (s *deviceMgrRemodelSuite) testRemodelTasksSwitchTrack(c *C, whatRefreshes var testDeviceCtx snapstate.DeviceContext - restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { - c.Check(flags.Required, Equals, true) - c.Check(deviceCtx, Equals, testDeviceCtx) - c.Check(fromChange, Equals, "99") + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*storeInstallGoalRecorder) + + sn := g.snaps[0] + name := sn.InstanceName + + c.Check(opts.Flags.Required, Equals, true) + c.Check(opts.DeviceCtx, Equals, testDeviceCtx) + c.Check(opts.FromChange, Equals, "99") tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) tDownload.Set("snap-setup", &snapstate.SnapSetup{ @@ -380,19 +438,24 @@ func (s *deviceMgrRemodelSuite) testRemodelTasksSwitchTrack(c *C, whatRefreshes tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() - restore = devicestate.MockSnapstateUpdateWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { - c.Check(flags.Required, Equals, false) - c.Check(flags.NoReRefresh, Equals, true) - c.Check(deviceCtx, Equals, testDeviceCtx) - c.Check(fromChange, Equals, "99") + restore = devicestate.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + g := goal.(*storeUpdateGoalRecorder) + sn := g.snaps[0] + name := sn.InstanceName + channel := sn.RevOpts.Channel + + c.Check(opts.Flags.Required, Equals, false) + c.Check(opts.Flags.NoReRefresh, Equals, true) + c.Check(opts.DeviceCtx, Equals, testDeviceCtx) + c.Check(opts.FromChange, Equals, "99") c.Check(name, Equals, whatRefreshes) - c.Check(opts.Channel, Equals, "18") + c.Check(channel, Equals, "18") - tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s to track %s", name, opts.Channel)) + tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s to track %s", name, channel)) tDownload.Set("snap-setup", &snapstate.SnapSetup{ SideInfo: &snap.SideInfo{ RealName: name, @@ -400,7 +463,7 @@ func (s *deviceMgrRemodelSuite) testRemodelTasksSwitchTrack(c *C, whatRefreshes }) tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) tValidate.WaitFor(tDownload) - tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) + tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, channel)) tUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tUpdate) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) @@ -437,7 +500,7 @@ func (s *deviceMgrRemodelSuite) testRemodelTasksSwitchTrack(c *C, whatRefreshes testDeviceCtx = &snapstatetest.TrivialDeviceContext{Remodeling: true, DeviceModel: new, OldDeviceModel: current} - tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99", nil, nil, devicestate.RemodelOptions{}) + tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99", nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) // 2 snaps, plus one track switch plus the remodel task, the // wait chain is tested in TestRemodel* @@ -470,33 +533,31 @@ epoch: 1 func (s *deviceMgrRemodelSuite) TestRemodelTasksSwitchGadget(c *C) { newTrack := map[string]string{"other-gadget": "18"} s.testRemodelSwitchTasks(c, newTrack, - map[string]interface{}{"gadget": "other-gadget=18"}, nil, nil, "") + map[string]interface{}{"gadget": "other-gadget=18"}, nil, "") } func (s *deviceMgrRemodelSuite) TestRemodelTasksSwitchLocalGadget(c *C) { newTrack := map[string]string{"other-gadget": "18"} - sis := make([]*snap.SideInfo, 1) - paths := make([]string, 1) - sis[0], paths[0] = createLocalSnap(c, "pc", "pcididididididididididididididid", 3, "gadget", "", nil) + localSnaps := make([]devicestate.LocalSnap, 1) + localSnaps[0].SideInfo, localSnaps[0].Path = createLocalSnap(c, "other-gadget", "pcididididididididididididididid", 3, "gadget", "", nil) s.testRemodelSwitchTasks(c, newTrack, map[string]interface{}{"gadget": "other-gadget=18"}, - sis, paths, "") + localSnaps, "") } func (s *deviceMgrRemodelSuite) TestRemodelTasksSwitchKernel(c *C) { newTrack := map[string]string{"other-kernel": "18"} s.testRemodelSwitchTasks(c, newTrack, - map[string]interface{}{"kernel": "other-kernel=18"}, nil, nil, "") + map[string]interface{}{"kernel": "other-kernel=18"}, nil, "") } func (s *deviceMgrRemodelSuite) TestRemodelTasksSwitchLocalKernel(c *C) { newTrack := map[string]string{"other-kernel": "18"} - sis := make([]*snap.SideInfo, 1) - paths := make([]string, 1) - sis[0], paths[0] = createLocalSnap(c, "pc-kernel", "pckernelidididididididididididid", 3, "kernel", "", nil) + localSnaps := make([]devicestate.LocalSnap, 1) + localSnaps[0].SideInfo, localSnaps[0].Path = createLocalSnap(c, "other-kernel", "pckernelidididididididididididid", 3, "kernel", "", nil) s.testRemodelSwitchTasks(c, newTrack, map[string]interface{}{"kernel": "other-kernel=18"}, - sis, paths, "") + localSnaps, "") } func (s *deviceMgrRemodelSuite) TestRemodelTasksSwitchKernelAndGadget(c *C) { @@ -504,37 +565,35 @@ func (s *deviceMgrRemodelSuite) TestRemodelTasksSwitchKernelAndGadget(c *C) { s.testRemodelSwitchTasks(c, newTrack, map[string]interface{}{ "kernel": "other-kernel=18", - "gadget": "other-gadget=18"}, nil, nil, "") + "gadget": "other-gadget=18"}, nil, "") } func (s *deviceMgrRemodelSuite) TestRemodelTasksSwitchLocalKernelAndGadget(c *C) { newTrack := map[string]string{"other-kernel": "18", "other-gadget": "18"} - sis := make([]*snap.SideInfo, 2) - paths := make([]string, 2) - sis[0], paths[0] = createLocalSnap(c, "pc-kernel", "pckernelidididididididididididid", 3, "kernel", "", nil) - sis[1], paths[1] = createLocalSnap(c, "pc", "pcididididididididididididididid", 3, "gadget", "", nil) + localSnaps := make([]devicestate.LocalSnap, 2) + localSnaps[0].SideInfo, localSnaps[0].Path = createLocalSnap(c, "other-kernel", "pckernelidididididididididididid", 3, "kernel", "", nil) + localSnaps[1].SideInfo, localSnaps[1].Path = createLocalSnap(c, "other-gadget", "pcididididididididididididididid", 3, "gadget", "", nil) s.testRemodelSwitchTasks(c, newTrack, map[string]interface{}{ "kernel": "other-kernel=18", "gadget": "other-gadget=18"}, - sis, paths, "") + localSnaps, "") } func (s *deviceMgrRemodelSuite) TestRemodelTasksSwitchLocalKernelAndGadgetFails(c *C) { // Fails as if we use local files, all need to be provided to the API. newTrack := map[string]string{"other-kernel": "18", "other-gadget": "18"} - sis := make([]*snap.SideInfo, 1) - paths := make([]string, 1) - sis[0], paths[0] = createLocalSnap(c, "pc-kernel", "pckernelidididididididididididid", 3, "kernel", "", nil) + localSnaps := make([]devicestate.LocalSnap, 1) + localSnaps[0].SideInfo, localSnaps[0].Path = createLocalSnap(c, "other-kernel", "pckernelidididididididididididid", 3, "kernel", "", nil) s.testRemodelSwitchTasks(c, newTrack, map[string]interface{}{ "kernel": "other-kernel=18", "gadget": "other-gadget=18"}, - sis, paths, + localSnaps, `no snap file provided for "other-gadget"`) } -func (s *deviceMgrRemodelSuite) testRemodelSwitchTasks(c *C, whatNewTrack map[string]string, newModelOverrides map[string]interface{}, localSnaps []*snap.SideInfo, paths []string, expectedErr string) { +func (s *deviceMgrRemodelSuite) testRemodelSwitchTasks(c *C, whatNewTrack map[string]string, newModelOverrides map[string]interface{}, localSnaps []devicestate.LocalSnap, expectedErr string) { s.state.Lock() defer s.state.Unlock() s.state.Set("seeded", true) @@ -545,57 +604,65 @@ func (s *deviceMgrRemodelSuite) testRemodelSwitchTasks(c *C, whatNewTrack map[st var testDeviceCtx snapstate.DeviceContext var snapstateInstallWithDeviceContextCalled int - restore := devicestate.MockSnapstateInstallPathWithDeviceContext(func(st *state.State, si *snap.SideInfo, path, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { - snapstateInstallWithDeviceContextCalled++ - newTrack, ok := whatNewTrack[name] - c.Check(ok, Equals, true) - c.Check(opts.Channel, Equals, newTrack) - if localSnaps != nil { - found := false - for i := range localSnaps { - if si.RealName == localSnaps[i].RealName { - found = true + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + switch g := goal.(type) { + case *pathInstallGoalRecorder: + name := g.snap.SideInfo.RealName + snapstateInstallWithDeviceContextCalled++ + + newTrack, ok := whatNewTrack[name] + c.Check(ok, Equals, true) + c.Check(g.snap.RevOpts.Channel, Equals, newTrack) + if localSnaps != nil { + found := false + for i := range localSnaps { + if g.snap.SideInfo.RealName == localSnaps[i].SideInfo.RealName { + found = true + } } + c.Check(found, Equals, true) + } else { + c.Check(g.snap.SideInfo, IsNil) } - c.Check(found, Equals, true) - } else { - c.Check(si, IsNil) - } - - tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) - tDownload.Set("snap-setup", &snapstate.SnapSetup{ - SideInfo: &snap.SideInfo{ - RealName: name, - }, - }) - tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) - tValidate.WaitFor(tDownload) - tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) - tInstall.WaitFor(tValidate) - ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil - }) - defer restore() - restore = devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { - snapstateInstallWithDeviceContextCalled++ - newTrack, ok := whatNewTrack[name] - c.Check(ok, Equals, true) - c.Check(opts.Channel, Equals, newTrack) - tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) - tDownload.Set("snap-setup", &snapstate.SnapSetup{ - SideInfo: &snap.SideInfo{ - RealName: name, - }, - }) - tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) - tValidate.WaitFor(tDownload) - tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) - tInstall.WaitFor(tValidate) - ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) + tDownload.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: name, + }, + }) + tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) + tValidate.WaitFor(tDownload) + tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) + tInstall.WaitFor(tValidate) + ts := state.NewTaskSet(tDownload, tValidate, tInstall) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) + return nil, ts, nil + case *storeInstallGoalRecorder: + sn := g.snaps[0] + name := sn.InstanceName + channel := sn.RevOpts.Channel + + snapstateInstallWithDeviceContextCalled++ + newTrack, ok := whatNewTrack[name] + c.Check(ok, Equals, true) + c.Check(channel, Equals, newTrack) + + tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) + tDownload.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: name, + }, + }) + tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) + tValidate.WaitFor(tDownload) + tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) + tInstall.WaitFor(tValidate) + ts := state.NewTaskSet(tDownload, tValidate, tInstall) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) + return nil, ts, nil + } + return nil, nil, fmt.Errorf("unexpected goal type: %T", goal) }) defer restore() @@ -631,10 +698,8 @@ func (s *deviceMgrRemodelSuite) testRemodelSwitchTasks(c *C, whatNewTrack map[st testDeviceCtx = &snapstatetest.TrivialDeviceContext{Remodeling: true, DeviceModel: new, OldDeviceModel: current} - offline := len(localSnaps) > 0 - - tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99", localSnaps, paths, devicestate.RemodelOptions{ - Offline: offline, + tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99", localSnaps, devicestate.RemodelOptions{ + Offline: len(localSnaps) > 0, }) if expectedErr == "" { c.Assert(err, IsNil) @@ -655,10 +720,13 @@ func (s *deviceMgrRemodelSuite) TestRemodelRequiredSnaps(c *C) { snapstatetest.InstallEssentialSnaps(c, s.state, "core18", nil, nil) - restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { - c.Check(flags.Required, Equals, true) - c.Check(deviceCtx, NotNil) - c.Check(deviceCtx.ForRemodeling(), Equals, true) + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*storeInstallGoalRecorder) + name := g.snaps[0].InstanceName + + c.Check(opts.Flags.Required, Equals, true) + c.Check(opts.DeviceCtx, NotNil) + c.Check(opts.DeviceCtx.ForRemodeling(), Equals, true) tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) tDownload.Set("snap-setup", &snapstate.SnapSetup{ @@ -672,7 +740,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelRequiredSnaps(c *C) { tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() @@ -698,7 +766,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelRequiredSnaps(c *C) { "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"}, "revision": "1", }) - chg, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") @@ -774,10 +842,13 @@ func (s *deviceMgrRemodelSuite) TestRemodelSwitchKernelTrack(c *C) { snapstatetest.InstallEssentialSnaps(c, s.state, "core18", nil, nil) - restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { - c.Check(flags.Required, Equals, true) - c.Check(deviceCtx, NotNil) - c.Check(deviceCtx.ForRemodeling(), Equals, true) + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*storeInstallGoalRecorder) + name := g.snaps[0].InstanceName + + c.Check(opts.Flags.Required, Equals, true) + c.Check(opts.DeviceCtx, NotNil) + c.Check(opts.DeviceCtx.ForRemodeling(), Equals, true) tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) tDownload.Set("snap-setup", &snapstate.SnapSetup{ @@ -791,20 +862,24 @@ func (s *deviceMgrRemodelSuite) TestRemodelSwitchKernelTrack(c *C) { tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() - restore = devicestate.MockSnapstateUpdateWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { - c.Check(flags.Required, Equals, false) - c.Check(flags.NoReRefresh, Equals, true) - c.Check(deviceCtx, NotNil) - c.Check(deviceCtx.ForRemodeling(), Equals, true) + restore = devicestate.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + g := goal.(*storeUpdateGoalRecorder) + name := g.snaps[0].InstanceName + channel := g.snaps[0].RevOpts.Channel + + c.Check(opts.Flags.Required, Equals, false) + c.Check(opts.Flags.NoReRefresh, Equals, true) + c.Check(opts.DeviceCtx, NotNil) + c.Check(opts.DeviceCtx.ForRemodeling(), Equals, true) - tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s from track %s", name, opts.Channel)) + tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s from track %s", name, channel)) tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) tValidate.WaitFor(tDownload) - tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) + tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, channel)) tUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tUpdate) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) @@ -834,7 +909,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelSwitchKernelTrack(c *C) { "required-snaps": []interface{}{"new-required-snap-1"}, "revision": "1", }) - chg, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") @@ -914,7 +989,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelLessRequiredSnaps(c *C) { "base": "core18", "revision": "1", }) - chg, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") @@ -946,12 +1021,14 @@ func (s *deviceMgrRemodelSuite) TestRemodelStoreSwitch(c *C) { var testStore snapstate.StoreService - restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { - c.Check(flags.Required, Equals, true) - c.Check(deviceCtx, NotNil) - c.Check(deviceCtx.ForRemodeling(), Equals, true) + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*storeInstallGoalRecorder) + name := g.snaps[0].InstanceName - c.Check(deviceCtx.Store(), Equals, testStore) + c.Check(opts.Flags.Required, Equals, true) + c.Check(opts.DeviceCtx, NotNil) + c.Check(opts.DeviceCtx.ForRemodeling(), Equals, true) + c.Check(opts.DeviceCtx.Store(), Equals, testStore) tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) tDownload.Set("snap-setup", &snapstate.SnapSetup{ @@ -965,7 +1042,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelStoreSwitch(c *C) { tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() @@ -1005,7 +1082,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelStoreSwitch(c *C) { return testStore } - chg, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") @@ -1064,7 +1141,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelRereg(c *C) { return nil } - chg, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) c.Assert(chg.Summary(), Equals, "Remodel device to canonical/rereg-model (0)") @@ -1123,9 +1200,15 @@ func (s *deviceMgrRemodelSuite) TestRemodelReregLocalFails(c *C) { return nil } - sis := []*snap.SideInfo{{RealName: "pc-kernel"}, {RealName: "pc"}} - paths := []string{"pc-kernel_1.snap", "pc_1.snap"} - chg, err := devicestate.Remodel(s.state, new, sis, paths, devicestate.RemodelOptions{ + localSnaps := []devicestate.LocalSnap{{ + SideInfo: &snap.SideInfo{RealName: "pc-kernel"}, + Path: "pc-kernel_1.snap", + }, { + SideInfo: &snap.SideInfo{RealName: "pc"}, + Path: "pc_1.snap", + }} + + chg, err := devicestate.Remodel(s.state, new, localSnaps, devicestate.RemodelOptions{ Offline: true, }) c.Assert(err.Error(), Equals, "cannot remodel offline to different brand ID / model yet") @@ -1140,7 +1223,10 @@ func (s *deviceMgrRemodelSuite) TestRemodelClash(c *C) { var clashing *asserts.Model - restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*storeInstallGoalRecorder) + name := g.snaps[0].InstanceName + // simulate things changing under our feet assertstatetest.AddMany(st, clashing) devicestatetest.SetDevice(s.state, &auth.DeviceState{ @@ -1160,7 +1246,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelClash(c *C) { tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() @@ -1197,7 +1283,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelClash(c *C) { }) clashing = other - _, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + _, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Check(err, DeepEquals, &snapstate.ChangeConflictError{ Message: "cannot start remodel, clashing with concurrent remodel to canonical/pc-model-other (0)", }) @@ -1209,7 +1295,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelClash(c *C) { Serial: "1234", }) clashing = new - _, err = devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + _, err = devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Check(err, DeepEquals, &snapstate.ChangeConflictError{ Message: "cannot start remodel, clashing with concurrent remodel to canonical/pc-model (1)", }) @@ -1222,7 +1308,10 @@ func (s *deviceMgrRemodelSuite) TestRemodelClashInProgress(c *C) { s.state.Set("refresh-privacy-key", "some-privacy-key") var chg *state.Change - restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*storeInstallGoalRecorder) + name := g.snaps[0].InstanceName + // simulate another started remodeling chg = st.NewChange("remodel", "other remodel") @@ -1238,7 +1327,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelClashInProgress(c *C) { tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() @@ -1267,7 +1356,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelClashInProgress(c *C) { "revision": "1", }) - _, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + _, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Check(err, DeepEquals, &snapstate.ChangeConflictError{ Message: "cannot start remodel, clashing with concurrent one", ChangeKind: "remodel", @@ -1282,7 +1371,10 @@ func (s *deviceMgrRemodelSuite) TestRemodelClashWithRecoverySystem(c *C) { s.state.Set("refresh-privacy-key", "some-privacy-key") var chg *state.Change - restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*storeInstallGoalRecorder) + name := g.snaps[0].InstanceName + // simulate another recovery system being created chg = s.state.NewChange("create-recovery-system", "...") chg.AddTask(s.state.NewTask("fake-create-recovery-system", "...")) @@ -1299,7 +1391,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelClashWithRecoverySystem(c *C) { tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() @@ -1328,7 +1420,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelClashWithRecoverySystem(c *C) { "revision": "1", }) - _, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + _, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Check(err, DeepEquals, &snapstate.ChangeConflictError{ Message: "creating recovery system in progress, no other changes allowed until this is done", ChangeKind: chg.Kind(), @@ -1374,7 +1466,7 @@ func (s *deviceMgrRemodelSuite) TestReregRemodelClashAnyChange(c *C) { chg := s.state.NewChange("chg", "other change") chg.SetStatus(state.DoingStatus) - _, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + _, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Assert(err, NotNil) c.Assert(err, DeepEquals, &snapstate.ChangeConflictError{ ChangeKind: "chg", @@ -1807,7 +1899,10 @@ volumes: {"meta/gadget.yaml", remodelGadgetYaml}, }) - restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*storeInstallGoalRecorder) + name := g.snaps[0].InstanceName + tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) tValidate.WaitFor(tDownload) @@ -1819,7 +1914,7 @@ volumes: tGadgetUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tGadgetUpdate) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() restore = release.MockOnClassic(false) @@ -1914,7 +2009,7 @@ volumes: }) defer restore() - chg, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) s.state.Unlock() @@ -1995,7 +2090,10 @@ func (s *deviceMgrRemodelSuite) TestRemodelGadgetAssetsParanoidCheck(c *C) { RealName: "new-gadget-unexpected", Revision: snap.R(34), } - restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*storeInstallGoalRecorder) + name := g.snaps[0].InstanceName + tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) tValidate.WaitFor(tDownload) @@ -2007,7 +2105,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelGadgetAssetsParanoidCheck(c *C) { tGadgetUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tGadgetUpdate) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() restore = release.MockOnClassic(false) @@ -2019,7 +2117,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelGadgetAssetsParanoidCheck(c *C) { }) defer restore() - chg, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) s.state.Unlock() @@ -2043,7 +2141,10 @@ func (s *deviceMgrSuite) TestRemodelSwitchBaseIncompatibleGadget(c *C) { var testDeviceCtx snapstate.DeviceContext - restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*storeInstallGoalRecorder) + name := g.snaps[0].InstanceName + c.Check(name, Equals, "core20") tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) @@ -2053,7 +2154,7 @@ func (s *deviceMgrSuite) TestRemodelSwitchBaseIncompatibleGadget(c *C) { tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() @@ -2081,7 +2182,7 @@ func (s *deviceMgrSuite) TestRemodelSwitchBaseIncompatibleGadget(c *C) { testDeviceCtx = &snapstatetest.TrivialDeviceContext{Remodeling: true, DeviceModel: new, OldDeviceModel: current} - _, err = devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99", nil, nil, devicestate.RemodelOptions{}) + _, err = devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99", nil, devicestate.RemodelOptions{}) c.Assert(err, ErrorMatches, `cannot remodel with gadget snap that has a different base than the model: "core18" \!= "core20"`) } @@ -2096,7 +2197,10 @@ func (s *deviceMgrSuite) TestRemodelSwitchBase(c *C) { var testDeviceCtx snapstate.DeviceContext var snapstateInstallWithDeviceContextCalled int - restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*storeInstallGoalRecorder) + name := g.snaps[0].InstanceName + snapstateInstallWithDeviceContextCalled++ switch name { case "core20", "pc-20": @@ -2111,7 +2215,7 @@ func (s *deviceMgrSuite) TestRemodelSwitchBase(c *C) { tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() @@ -2139,7 +2243,7 @@ func (s *deviceMgrSuite) TestRemodelSwitchBase(c *C) { testDeviceCtx = &snapstatetest.TrivialDeviceContext{Remodeling: true, DeviceModel: new, OldDeviceModel: current} - tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99", nil, nil, devicestate.RemodelOptions{}) + tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99", nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) // 1 switch to a new base, 1 switch to new gadget, plus the remodel task c.Assert(tss, HasLen, 3) @@ -2153,10 +2257,13 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20RequiredSnapsAndRecoverySystem(c s.state.Set("seeded", true) s.state.Set("refresh-privacy-key", "some-privacy-key") - restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { - c.Check(flags.Required, Equals, true) - c.Check(deviceCtx, NotNil) - c.Check(deviceCtx.ForRemodeling(), Equals, true) + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*storeInstallGoalRecorder) + name := g.snaps[0].InstanceName + + c.Check(opts.Flags.Required, Equals, true) + c.Check(opts.DeviceCtx, NotNil) + c.Check(opts.DeviceCtx.ForRemodeling(), Equals, true) tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) tDownload.Set("snap-setup", &snapstate.SnapSetup{ @@ -2170,16 +2277,20 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20RequiredSnapsAndRecoverySystem(c tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() - restore = devicestate.MockSnapstateUpdateWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { - c.Check(flags.Required, Equals, false) - c.Check(flags.NoReRefresh, Equals, true) - c.Check(deviceCtx, NotNil) + restore = devicestate.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + g := goal.(*storeUpdateGoalRecorder) + name := g.snaps[0].InstanceName + channel := g.snaps[0].RevOpts.Channel - tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s from track %s", name, opts.Channel)) + c.Check(opts.Flags.Required, Equals, false) + c.Check(opts.Flags.NoReRefresh, Equals, true) + c.Check(opts.DeviceCtx, NotNil) + + tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s from track %s", name, channel)) tDownload.Set("snap-setup", &snapstate.SnapSetup{ SideInfo: &snap.SideInfo{ RealName: name, @@ -2187,7 +2298,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20RequiredSnapsAndRecoverySystem(c }) tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) tValidate.WaitFor(tDownload) - tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) + tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, channel)) tUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tUpdate) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) @@ -2326,7 +2437,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20RequiredSnapsAndRecoverySystem(c }, }, }) - chg, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") @@ -2477,59 +2588,67 @@ func (s *deviceMgrRemodelSuite) testRemodelUC20SwitchKernelGadgetBaseSnaps(c *C, s.state.Set("seeded", true) s.state.Set("refresh-privacy-key", "some-privacy-key") - restore := devicestate.MockSnapstateUpdateWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { - c.Check(flags.Required, Equals, false) - c.Check(flags.NoReRefresh, Equals, true) - c.Check(deviceCtx, NotNil) - c.Check(testFlags.localSnaps, Equals, false) - - // This task would not really be added if we have a local snap, - // but we keep it anyway to simplify the checks we do at the end. - tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s from track %s", name, opts.Channel)) - tDownload.Set("snap-setup", &snapstate.SnapSetup{ - SideInfo: &snap.SideInfo{ - RealName: name, - }, - }) - tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) - tValidate.WaitFor(tDownload) - tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) - tUpdate.WaitFor(tValidate) - ts := state.NewTaskSet(tDownload, tValidate, tUpdate) - ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil - }) - defer restore() - - restore = devicestate.MockSnapstateUpdatePathWithDeviceContext(func(st *state.State, si *snap.SideInfo, path, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { - c.Check(flags.Required, Equals, false) - c.Check(flags.NoReRefresh, Equals, true) - c.Check(deviceCtx, NotNil) - c.Check(si, NotNil) - c.Check(si.RealName, Equals, name) + restore := devicestate.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + switch g := goal.(type) { + case *storeUpdateGoalRecorder: + name := g.snaps[0].InstanceName + channel := g.snaps[0].RevOpts.Channel + c.Check(opts.Flags.Required, Equals, false) + c.Check(opts.Flags.NoReRefresh, Equals, true) + c.Check(opts.DeviceCtx, NotNil) + c.Check(testFlags.localSnaps, Equals, false) + + // This task would not really be added if we have a local snap, + // but we keep it anyway to simplify the checks we do at the end. + tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s from track %s", name, channel)) + tDownload.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: name, + }, + }) + tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) + tValidate.WaitFor(tDownload) + tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, channel)) + tUpdate.WaitFor(tValidate) + ts := state.NewTaskSet(tDownload, tValidate, tUpdate) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) + return ts, nil + case *pathUpdateGoalRecorder: + name := g.snaps[0].SideInfo.RealName + channel := g.snaps[0].RevOpts.Channel + si := g.snaps[0].SideInfo + + c.Check(opts.Flags.Required, Equals, false) + c.Check(opts.Flags.NoReRefresh, Equals, true) + c.Check(opts.DeviceCtx, NotNil) + c.Check(si, NotNil) + c.Check(si.RealName, Equals, name) + + // This task would not really be added if we have a local snap, + // but we keep it anyway to simplify the checks we do at the end. + tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s from track %s", name, channel)) + tDownload.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: name, + }, + }) + tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) + tValidate.WaitFor(tDownload) + tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, channel)) + tUpdate.WaitFor(tValidate) + ts := state.NewTaskSet(tDownload, tValidate, tUpdate) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) + return ts, nil + } - // This task would not really be added if we have a local snap, - // but we keep it anyway to simplify the checks we do at the end. - tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s from track %s", name, opts.Channel)) - tDownload.Set("snap-setup", &snapstate.SnapSetup{ - SideInfo: &snap.SideInfo{ - RealName: name, - }, - }) - tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) - tValidate.WaitFor(tDownload) - tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) - tUpdate.WaitFor(tValidate) - ts := state.NewTaskSet(tDownload, tValidate, tUpdate) - ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, fmt.Errorf("unexpected goal type: %T", goal) }) defer restore() - restore = devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore = devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { // snaps will be refreshed so calls go through update c.Errorf("unexpected call, test broken") - return nil, fmt.Errorf("unexpected call") + return nil, nil, errors.New("unexpected call") }) defer restore() @@ -2635,18 +2754,24 @@ func (s *deviceMgrRemodelSuite) testRemodelUC20SwitchKernelGadgetBaseSnaps(c *C, }, }) - var localSnaps []*snap.SideInfo - var paths []string + var localSnaps []devicestate.LocalSnap if testFlags.localSnaps { - localSnaps = []*snap.SideInfo{siModelKernel, siModelBase} - paths = []string{"pc-kernel_101.snap", "core20"} + localSnaps = []devicestate.LocalSnap{{ + SideInfo: siModelKernel, + Path: "pc-kernel_101.snap", + }, { + SideInfo: siModelBase, + Path: "core20", + }} if !testFlags.missingSnap { - localSnaps = append(localSnaps, siModelGadget) - paths = append(paths, "pc_101.snap") + localSnaps = append(localSnaps, devicestate.LocalSnap{ + SideInfo: siModelGadget, + Path: "pc_101.snap", + }) } } - chg, err := devicestate.Remodel(s.state, new, localSnaps, paths, devicestate.RemodelOptions{ + chg, err := devicestate.Remodel(s.state, new, localSnaps, devicestate.RemodelOptions{ Offline: testFlags.localSnaps, }) if testFlags.missingSnap { @@ -2775,17 +2900,20 @@ func (s *deviceMgrRemodelSuite) TestRemodelOfflineUseInstalledSnaps(c *C) { s.state.Set("seeded", true) s.state.Set("refresh-privacy-key", "some-privacy-key") - restore := devicestate.MockSnapstateInstallPathWithDeviceContext(func(_ *state.State, si *snap.SideInfo, _ string, name string, opts *snapstate.RevisionOptions, _ int, _ snapstate.Flags, _ snapstate.PrereqTracker, _ snapstate.DeviceContext, _ string) (*state.TaskSet, error) { - c.Check(si.RealName, Equals, "app-snap") + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*pathInstallGoalRecorder) + name := g.snap.SideInfo.RealName + + c.Check(g.snap.SideInfo.RealName, Equals, "app-snap") tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) tValidate.Set("snap-setup", - &snapstate.SnapSetup{SideInfo: si, Channel: opts.Channel}) + &snapstate.SnapSetup{SideInfo: g.snap.SideInfo, Channel: g.snap.RevOpts.Channel}) tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tValidate, tInstall) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() @@ -2884,7 +3012,8 @@ func (s *deviceMgrRemodelSuite) TestRemodelOfflineUseInstalledSnaps(c *C) { }, }) - chg, err := devicestate.Remodel(s.state, new, []*snap.SideInfo{appSnap}, []string{appSnapPath}, devicestate.RemodelOptions{ + localSnaps := []devicestate.LocalSnap{{SideInfo: appSnap, Path: appSnapPath}} + chg, err := devicestate.Remodel(s.state, new, localSnaps, devicestate.RemodelOptions{ Offline: true, }) c.Assert(err, IsNil) @@ -3032,17 +3161,19 @@ func (s *deviceMgrRemodelSuite) TestRemodelOfflineUseInstalledSnapsChannelSwitch s.state.Set("seeded", true) s.state.Set("refresh-privacy-key", "some-privacy-key") - restore := devicestate.MockSnapstateInstallPathWithDeviceContext(func(_ *state.State, si *snap.SideInfo, _ string, name string, opts *snapstate.RevisionOptions, _ int, _ snapstate.Flags, _ snapstate.PrereqTracker, _ snapstate.DeviceContext, _ string) (*state.TaskSet, error) { - c.Check(si.RealName, Equals, "app-snap") + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*pathInstallGoalRecorder) - tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) + c.Check(g.snap.SideInfo.RealName, Equals, "app-snap") + + tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", g.snap.SideInfo.RealName)) tValidate.Set("snap-setup", - &snapstate.SnapSetup{SideInfo: si, Channel: opts.Channel}) - tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) + &snapstate.SnapSetup{SideInfo: g.snap.SideInfo, Channel: g.snap.RevOpts.Channel}) + tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", g.snap.SideInfo.RealName)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tValidate, tInstall) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() @@ -3141,7 +3272,12 @@ func (s *deviceMgrRemodelSuite) TestRemodelOfflineUseInstalledSnapsChannelSwitch }, }) - chg, err := devicestate.Remodel(s.state, new, []*snap.SideInfo{appSnap}, []string{appSnapPath}, devicestate.RemodelOptions{ + localSnaps := []devicestate.LocalSnap{{ + SideInfo: appSnap, + Path: appSnapPath, + }} + + chg, err := devicestate.Remodel(s.state, new, localSnaps, devicestate.RemodelOptions{ Offline: true, }) c.Assert(err, IsNil) @@ -3290,17 +3426,17 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20SwitchKernelBaseGadgetSnapsInstal s.state.Set("seeded", true) s.state.Set("refresh-privacy-key", "some-privacy-key") - restore := devicestate.MockSnapstateUpdateWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { // no snaps are getting updated c.Errorf("unexpected call, test broken") return nil, fmt.Errorf("unexpected call") }) defer restore() - restore = devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore = devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { // no snaps are getting installed c.Errorf("unexpected call, test broken") - return nil, fmt.Errorf("unexpected call") + return nil, nil, errors.New("unexpected call") }) defer restore() @@ -3410,7 +3546,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20SwitchKernelBaseGadgetSnapsInstal }, }, }) - chg, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") @@ -3559,75 +3695,84 @@ func (s *deviceMgrRemodelSuite) testRemodelUC20SwitchKernelBaseGadgetSnapsInstal s.state.Set("refresh-privacy-key", "some-privacy-key") callsToMockedUpdate := 0 - restore := devicestate.MockSnapstateUpdateWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { - c.Assert(strutil.ListContains([]string{"core24-new", "pc-kernel-new", "pc-new"}, name), Equals, true, - Commentf("unexpected snap %q", name)) - callsToMockedUpdate++ - c.Check(flags.Required, Equals, false) - c.Check(flags.NoReRefresh, Equals, true) - c.Check(deviceCtx, NotNil) - - // pretend the new channel has the same revision, so update is a - // simple channel switch - tSwitchChannel := s.state.NewTask("switch-snap-channel", fmt.Sprintf("Switch %s channel to %s", name, opts.Channel)) - typ := "kernel" - rev := snap.R(222) - if name == "core24-new" { - typ = "base" - rev = snap.R(223) - } else if name == "pc-new" { - typ = "gadget" - rev = snap.R(224) - } - tSwitchChannel.Set("snap-setup", &snapstate.SnapSetup{ - SideInfo: &snap.SideInfo{ - RealName: name, - Revision: rev, - SnapID: snaptest.AssertedSnapID(name), - }, - Flags: snapstate.Flags{}.ForSnapSetup(), - Type: snap.Type(typ), - }) - ts := state.NewTaskSet(tSwitchChannel) - // no download-and-checks-done edge - return ts, nil - }) - defer restore() - callsToMockedUpdatePath := 0 - restore = devicestate.MockSnapstateUpdatePathWithDeviceContext(func(st *state.State, si *snap.SideInfo, path, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { - callsToMockedUpdatePath++ - c.Assert(strutil.ListContains([]string{"core24-new", "pc-kernel-new", "pc-new"}, name), Equals, true, - Commentf("unexpected snap %q", name)) - c.Check(flags.Required, Equals, false) - c.Check(flags.NoReRefresh, Equals, true) - c.Check(deviceCtx, NotNil) - c.Check(si, NotNil) - c.Check(si.RealName, Equals, name) - - // switch channel using SideInfo from the local snap - tSwitchChannel := s.state.NewTask("switch-snap-channel", fmt.Sprintf("Switch %s channel to %s", name, opts.Channel)) - typ := "kernel" - if name == "core24-new" { - typ = "base" - } else if name == "pc-new" { - typ = "gadget" + restore := devicestate.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + + switch g := goal.(type) { + case *storeUpdateGoalRecorder: + name := g.snaps[0].InstanceName + channel := g.snaps[0].RevOpts.Channel + + c.Assert(strutil.ListContains([]string{"core24-new", "pc-kernel-new", "pc-new"}, name), Equals, true, + Commentf("unexpected snap %q", name)) + callsToMockedUpdate++ + c.Check(opts.Flags.Required, Equals, false) + c.Check(opts.Flags.NoReRefresh, Equals, true) + c.Check(opts.DeviceCtx, NotNil) + + // pretend the new channel has the same revision, so update is a + // simple channel switch + tSwitchChannel := s.state.NewTask("switch-snap-channel", fmt.Sprintf("Switch %s channel to %s", name, channel)) + typ := "kernel" + rev := snap.R(222) + if name == "core24-new" { + typ = "base" + rev = snap.R(223) + } else if name == "pc-new" { + typ = "gadget" + rev = snap.R(224) + } + tSwitchChannel.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: name, + Revision: rev, + SnapID: snaptest.AssertedSnapID(name), + }, + Flags: snapstate.Flags{}.ForSnapSetup(), + Type: snap.Type(typ), + }) + ts := state.NewTaskSet(tSwitchChannel) + // no download-and-checks-done edge + return ts, nil + case *pathUpdateGoalRecorder: + name := g.snaps[0].SideInfo.RealName + channel := g.snaps[0].RevOpts.Channel + si := g.snaps[0].SideInfo + + callsToMockedUpdatePath++ + c.Assert(strutil.ListContains([]string{"core24-new", "pc-kernel-new", "pc-new"}, name), Equals, true, + Commentf("unexpected snap %q", name)) + c.Check(opts.Flags.Required, Equals, false) + c.Check(opts.Flags.NoReRefresh, Equals, true) + c.Check(opts.DeviceCtx, NotNil) + c.Check(si, NotNil) + c.Check(si.RealName, Equals, name) + + // switch channel using SideInfo from the local snap + tSwitchChannel := s.state.NewTask("switch-snap-channel", fmt.Sprintf("Switch %s channel to %s", name, channel)) + typ := "kernel" + if name == "core24-new" { + typ = "base" + } else if name == "pc-new" { + typ = "gadget" + } + tSwitchChannel.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: si, + Flags: snapstate.Flags{}.ForSnapSetup(), + Type: snap.Type(typ), + }) + ts := state.NewTaskSet(tSwitchChannel) + // no download-and-checks-done edge + return ts, nil } - tSwitchChannel.Set("snap-setup", &snapstate.SnapSetup{ - SideInfo: si, - Flags: snapstate.Flags{}.ForSnapSetup(), - Type: snap.Type(typ), - }) - ts := state.NewTaskSet(tSwitchChannel) - // no download-and-checks-done edge - return ts, nil + return nil, fmt.Errorf("unexpected goal type: %T", goal) }) defer restore() - restore = devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore = devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { // no snaps are getting installed c.Errorf("unexpected call, test broken") - return nil, fmt.Errorf("unexpected call") + return nil, nil, errors.New("unexpected call") }) defer restore() @@ -3744,17 +3889,15 @@ func (s *deviceMgrRemodelSuite) testRemodelUC20SwitchKernelBaseGadgetSnapsInstal }, }) - var localSnaps []*snap.SideInfo - var paths []string + var localSnaps []devicestate.LocalSnap if opts.localSnaps { for i, name := range []string{"pc-kernel-new", "core24-new", "pc-new"} { si, path := createLocalSnap(c, name, snaptest.AssertedSnapID(name), 222+i, "", "", nil) - localSnaps = append(localSnaps, si) - paths = append(paths, path) + localSnaps = append(localSnaps, devicestate.LocalSnap{SideInfo: si, Path: path}) } } - chg, err := devicestate.Remodel(s.state, new, localSnaps, paths, devicestate.RemodelOptions{ + chg, err := devicestate.Remodel(s.state, new, localSnaps, devicestate.RemodelOptions{ Offline: opts.localSnaps, }) c.Assert(err, IsNil) @@ -3883,15 +4026,19 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20SwitchKernelBaseSnapsInstalledSna s.state.Set("seeded", true) s.state.Set("refresh-privacy-key", "some-privacy-key") - restore := devicestate.MockSnapstateUpdateWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { - c.Check(flags.Required, Equals, false) - c.Check(flags.NoReRefresh, Equals, true) - c.Check(deviceCtx, NotNil) + restore := devicestate.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + g := goal.(*storeUpdateGoalRecorder) + name := g.snaps[0].InstanceName + channel := g.snaps[0].RevOpts.Channel + + c.Check(opts.Flags.Required, Equals, false) + c.Check(opts.Flags.NoReRefresh, Equals, true) + c.Check(opts.DeviceCtx, NotNil) - tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s from track %s", name, opts.Channel)) + tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s from track %s", name, channel)) tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) tValidate.WaitFor(tDownload) - tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) + tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, channel)) tUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tUpdate) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) @@ -3899,10 +4046,10 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20SwitchKernelBaseSnapsInstalledSna }) defer restore() - restore = devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore = devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { // no snaps are getting installed c.Errorf("unexpected call, test broken") - return nil, fmt.Errorf("unexpected call") + return nil, nil, errors.New("unexpected call") }) defer restore() @@ -4014,7 +4161,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20SwitchKernelBaseSnapsInstalledSna }, }, }) - chg, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") @@ -4120,15 +4267,15 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20EssentialSnapsTrackingDifferentCh s.state.Set("seeded", true) s.state.Set("refresh-privacy-key", "some-privacy-key") - restore := devicestate.MockSnapstateUpdateWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { // no snaps are getting updated return nil, fmt.Errorf("unexpected update call") }) defer restore() - restore = devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore = devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { // no snaps are getting installed - return nil, fmt.Errorf("unexpected install call") + return nil, nil, errors.New("unexpected install call") }) defer restore() @@ -4233,7 +4380,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20EssentialSnapsTrackingDifferentCh }, }, }) - chg, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") @@ -4296,15 +4443,15 @@ func (s *deviceMgrRemodelSuite) TestRemodelFailWhenUsingUnassertedSnapForSpecifi s.state.Set("seeded", true) s.state.Set("refresh-privacy-key", "some-privacy-key") - restore := devicestate.MockSnapstateUpdateWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { // no snaps are getting updated return nil, fmt.Errorf("unexpected update call") }) defer restore() - restore = devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore = devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { // no snaps are getting installed - return nil, fmt.Errorf("unexpected install call") + return nil, nil, errors.New("unexpected install call") }) defer restore() @@ -4437,7 +4584,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelFailWhenUsingUnassertedSnapForSpecifi }, }) - _, err = devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + _, err = devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Assert(err, ErrorMatches, "cannot determine if unasserted snap revision matches required revision") } @@ -4450,23 +4597,27 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20BaseNoDownloadSimpleChannelSwitch s.state.Set("seeded", true) s.state.Set("refresh-privacy-key", "some-privacy-key") - restore := devicestate.MockSnapstateUpdateWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + g := goal.(*storeUpdateGoalRecorder) + name := g.snaps[0].InstanceName + channel := g.snaps[0].RevOpts.Channel + // expecting an update call for the base snap c.Assert(name, Equals, "core20") - c.Check(flags.Required, Equals, false) - c.Check(flags.NoReRefresh, Equals, true) - c.Check(deviceCtx, NotNil) + c.Check(opts.Flags.Required, Equals, false) + c.Check(opts.Flags.NoReRefresh, Equals, true) + c.Check(opts.DeviceCtx, NotNil) - tSwitchChannel := s.state.NewTask("switch-snap-channel", fmt.Sprintf("Switch %s channel to %s", name, opts.Channel)) + tSwitchChannel := s.state.NewTask("switch-snap-channel", fmt.Sprintf("Switch %s channel to %s", name, channel)) ts := state.NewTaskSet(tSwitchChannel) // no download-and-checks-done edge return ts, nil }) defer restore() - restore = devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore = devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { // no snaps are getting installed - return nil, fmt.Errorf("unexpected install call") + return nil, nil, errors.New("unexpected install call") }) defer restore() @@ -4569,7 +4720,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20BaseNoDownloadSimpleChannelSwitch }, }, }) - chg, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") @@ -4637,23 +4788,27 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20EssentialNoDownloadSimpleChannelS s.state.Set("seeded", true) s.state.Set("refresh-privacy-key", "some-privacy-key") - restore := devicestate.MockSnapstateUpdateWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + g := goal.(*storeUpdateGoalRecorder) + name := g.snaps[0].InstanceName + channel := g.snaps[0].RevOpts.Channel + // expecting an update call for the base snap c.Assert(name, Equals, "snap-1") - c.Check(flags.Required, Equals, false) - c.Check(flags.NoReRefresh, Equals, true) - c.Check(deviceCtx, NotNil) + c.Check(opts.Flags.Required, Equals, false) + c.Check(opts.Flags.NoReRefresh, Equals, true) + c.Check(opts.DeviceCtx, NotNil) - tSwitchChannel := s.state.NewTask("switch-snap-channel", fmt.Sprintf("Switch %s channel to %s", name, opts.Channel)) + tSwitchChannel := s.state.NewTask("switch-snap-channel", fmt.Sprintf("Switch %s channel to %s", name, channel)) ts := state.NewTaskSet(tSwitchChannel) // no download-and-checks-done edge return ts, nil }) defer restore() - restore = devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore = devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { // no snaps are getting installed - return nil, fmt.Errorf("unexpected install call") + return nil, nil, errors.New("unexpected install call") }) defer restore() @@ -4805,7 +4960,7 @@ base: snap-1-base }, }, }) - chg, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") @@ -4854,9 +5009,9 @@ func (s *deviceMgrRemodelSuite) testRemodelUC20LabelConflicts(c *C, tc remodelUC s.state.Set("seeded", true) s.state.Set("refresh-privacy-key", "some-privacy-key") - restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { c.Errorf("unexpected call, test broken") - return nil, fmt.Errorf("unexpected call") + return nil, nil, errors.New("unexpected call") }) defer restore() @@ -4975,7 +5130,7 @@ func (s *deviceMgrRemodelSuite) testRemodelUC20LabelConflicts(c *C, tc remodelUC defer os.Chmod(systemsDir, 0755) } - chg, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) if tc.expectedErr == "" { c.Assert(err, IsNil) c.Assert(chg, NotNil) @@ -5148,7 +5303,10 @@ func (s *deviceMgrRemodelSuite) testUC20RemodelSetModel(c *C, tc uc20RemodelSetM }, }) - restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*storeInstallGoalRecorder) + name := g.snaps[0].InstanceName + tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) tValidate.WaitFor(tDownload) @@ -5156,7 +5314,7 @@ func (s *deviceMgrRemodelSuite) testUC20RemodelSetModel(c *C, tc uc20RemodelSetM tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() restore = release.MockOnClassic(false) @@ -5194,7 +5352,7 @@ func (s *deviceMgrRemodelSuite) testUC20RemodelSetModel(c *C, tc uc20RemodelSetM }) defer restore() - chg, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) var setModelTask *state.Task for _, tsk := range chg.Tasks() { @@ -5478,36 +5636,42 @@ func (s *deviceMgrRemodelSuite) testUC20RemodelLocalNonEssential(c *C, tc *uc20R }) installWithDeviceContextCalled := 0 - restore := devicestate.MockSnapstateInstallPathWithDeviceContext(func(st *state.State, si *snap.SideInfo, path, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*pathInstallGoalRecorder) + installWithDeviceContextCalled++ - c.Check(si, NotNil) - c.Check(si.RealName, Equals, name) - c.Check(si.RealName, Not(Equals), "not-used-snap") + c.Check(g.snap.SideInfo, NotNil) + c.Check(g.snap.SideInfo.RealName, Not(Equals), "not-used-snap") - tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) + tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", g.snap.SideInfo.RealName)) tValidate.Set("snap-setup", - &snapstate.SnapSetup{SideInfo: si, Channel: opts.Channel}) - tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) + &snapstate.SnapSetup{SideInfo: g.snap.SideInfo, Channel: g.snap.RevOpts.Channel}) + tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", g.snap.SideInfo.RealName)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tValidate, tInstall) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() updateWithDeviceContextCalled := 0 - restore = devicestate.MockSnapstateUpdatePathWithDeviceContext(func(st *state.State, si *snap.SideInfo, path, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore = devicestate.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + g := goal.(*pathUpdateGoalRecorder) + name := g.snaps[0].SideInfo.RealName + si := g.snaps[0].SideInfo + channel := g.snaps[0].RevOpts.Channel + updateWithDeviceContextCalled++ - c.Check(flags.Required, Equals, false) - c.Check(flags.NoReRefresh, Equals, true) - c.Check(deviceCtx, NotNil) + c.Check(opts.Flags.Required, Equals, false) + c.Check(opts.Flags.NoReRefresh, Equals, true) + c.Check(opts.DeviceCtx, NotNil) c.Check(si, NotNil) c.Check(si.RealName, Equals, name) c.Check(si.RealName, Not(Equals), "not-used-snap") tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) tValidate.Set("snap-setup", - &snapstate.SnapSetup{SideInfo: si, Channel: opts.Channel}) + &snapstate.SnapSetup{SideInfo: si, Channel: channel}) tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tValidate, tInstall) @@ -5548,15 +5712,19 @@ func (s *deviceMgrRemodelSuite) testUC20RemodelLocalNonEssential(c *C, tc *uc20R defer restore() siSomeSnapNew, path := createLocalSnap(c, "some-snap", snaptest.AssertedSnapID("some-snap"), 3, "app", "", nil) - localSnaps := []*snap.SideInfo{siSomeSnapNew} - paths := []string{path} + localSnaps := []devicestate.LocalSnap{{ + SideInfo: siSomeSnapNew, + Path: path, + }} if tc.notUsedSnap { siNotUsed, pathNotUsed := createLocalSnap(c, "not-used-snap", snaptest.AssertedSnapID("not-used-snap"), 3, "app", "", nil) - localSnaps = append(localSnaps, siNotUsed) - paths = append(paths, pathNotUsed) + localSnaps = append(localSnaps, devicestate.LocalSnap{ + SideInfo: siNotUsed, + Path: pathNotUsed, + }) } - chg, err := devicestate.Remodel(s.state, new, localSnaps, paths, devicestate.RemodelOptions{ + chg, err := devicestate.Remodel(s.state, new, localSnaps, devicestate.RemodelOptions{ Offline: true, }) c.Assert(err, IsNil) @@ -5772,7 +5940,10 @@ func (s *deviceMgrRemodelSuite) TestUC20RemodelSetModelWithReboot(c *C) { }, }) - restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*storeInstallGoalRecorder) + name := g.snaps[0].InstanceName + tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) tValidate.WaitFor(tDownload) @@ -5780,7 +5951,7 @@ func (s *deviceMgrRemodelSuite) TestUC20RemodelSetModelWithReboot(c *C) { tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() restore = release.MockOnClassic(false) @@ -5865,7 +6036,7 @@ func (s *deviceMgrRemodelSuite) TestUC20RemodelSetModelWithReboot(c *C) { }) defer restore() - chg, err := devicestate.Remodel(s.state, new, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, new, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) // since we cannot panic in random place in code that runs under @@ -6081,16 +6252,19 @@ plugs: }) } - restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*storeInstallGoalRecorder) + name := g.snaps[0].InstanceName + if missingWhen != "install" { c.Errorf("unexpected call to install for snap %q", name) - return nil, fmt.Errorf("unexpected call") + return nil, nil, fmt.Errorf("unexpected call") } - c.Check(flags.Required, Equals, true) - c.Check(deviceCtx, Equals, testDeviceCtx) - c.Check(fromChange, Equals, "99") + c.Check(opts.Flags.Required, Equals, true) + c.Check(opts.DeviceCtx, Equals, testDeviceCtx) + c.Check(opts.FromChange, Equals, "99") - prqt.Add(info) + opts.PrereqTracker.Add(info) tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) tDownload.Set("snap-setup", snapsupTemplate) @@ -6100,37 +6274,41 @@ plugs: tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() - restore = devicestate.MockSnapstateUpdateWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore = devicestate.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + g := goal.(*storeUpdateGoalRecorder) + name := g.snaps[0].InstanceName + channel := g.snaps[0].RevOpts.Channel + if missingWhen == "install" { c.Errorf("unexpected call to update for snap %q", name) return nil, fmt.Errorf("unexpected call") } - c.Check(flags.Required, Equals, false) - c.Check(flags.NoReRefresh, Equals, true) - c.Check(deviceCtx, Equals, testDeviceCtx) - c.Check(fromChange, Equals, "99") - c.Check(opts.Channel, Equals, "latest/stable") + c.Check(opts.Flags.Required, Equals, false) + c.Check(opts.Flags.NoReRefresh, Equals, true) + c.Check(opts.DeviceCtx, Equals, testDeviceCtx) + c.Check(opts.FromChange, Equals, "99") + c.Check(channel, Equals, "latest/stable") - prqt.Add(info) + opts.PrereqTracker.Add(info) var ts *state.TaskSet if missingWhen == "update" { - tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s to track %s", name, opts.Channel)) + tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s to track %s", name, channel)) tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) tValidate.WaitFor(tDownload) // set snap-setup on a different task now tValidate.Set("snap-setup", snapsupTemplate) - tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) + tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, channel)) tUpdate.WaitFor(tValidate) ts = state.NewTaskSet(tDownload, tValidate, tUpdate) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) } else { // switch-channel - tSwitch := s.state.NewTask("fake-switch-channel", fmt.Sprintf("Switch snap %s channel to %s", name, opts.Channel)) + tSwitch := s.state.NewTask("fake-switch-channel", fmt.Sprintf("Switch snap %s channel to %s", name, channel)) ts = state.NewTaskSet(tSwitch) // no edge } @@ -6205,7 +6383,7 @@ plugs: testDeviceCtx = &snapstatetest.TrivialDeviceContext{Remodeling: true} - tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99", nil, nil, devicestate.RemodelOptions{}) + tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99", nil, devicestate.RemodelOptions{}) msg := `cannot remodel to model that is not self contained:` if strutil.ListContains(missingWhat, "base") { @@ -6307,16 +6485,19 @@ plugs: RealName: "foo-missing-deps", }) - restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*storeInstallGoalRecorder) + name := g.snaps[0].InstanceName + if name != "foo-missing-deps" { c.Errorf("unexpected call to install for snap %q", name) - return nil, fmt.Errorf("unexpected call") + return nil, nil, fmt.Errorf("unexpected call") } - c.Check(flags.Required, Equals, true) - c.Check(deviceCtx, Equals, testDeviceCtx) - c.Check(fromChange, Equals, "99") + c.Check(opts.Flags.Required, Equals, true) + c.Check(opts.DeviceCtx, Equals, testDeviceCtx) + c.Check(opts.FromChange, Equals, "99") - prqt.Add(fooInfo) + opts.PrereqTracker.Add(fooInfo) tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) snapsupFoo := &snapstate.SnapSetup{ @@ -6336,7 +6517,7 @@ plugs: tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() @@ -6367,18 +6548,22 @@ plugs: TrackingChannel: "latest/stable", }) - restore = devicestate.MockSnapstateUpdateWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore = devicestate.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + g := goal.(*storeUpdateGoalRecorder) + name := g.snaps[0].InstanceName + channel := g.snaps[0].RevOpts.Channel + if name != "bar-missing-deps" { c.Errorf("unexpected call to update for snap %q", name) return nil, fmt.Errorf("unexpected call") } - c.Check(flags.Required, Equals, false) - c.Check(flags.NoReRefresh, Equals, true) - c.Check(deviceCtx, Equals, testDeviceCtx) - c.Check(fromChange, Equals, "99") - c.Check(opts.Channel, Equals, "latest/stable") + c.Check(opts.Flags.Required, Equals, false) + c.Check(opts.Flags.NoReRefresh, Equals, true) + c.Check(opts.DeviceCtx, Equals, testDeviceCtx) + c.Check(opts.FromChange, Equals, "99") + c.Check(channel, Equals, "latest/stable") - prqt.Add(barInfo) + opts.PrereqTracker.Add(barInfo) return state.NewTaskSet(), nil }) @@ -6455,7 +6640,7 @@ plugs: testDeviceCtx = &snapstatetest.TrivialDeviceContext{Remodeling: true} - tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99", nil, nil, devicestate.RemodelOptions{}) + tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99", nil, devicestate.RemodelOptions{}) msg := `cannot remodel to model that is not self contained: - cannot use snap "foo-missing-deps": base "foo-base" is missing @@ -6513,16 +6698,23 @@ func (s *deviceMgrSuite) testRemodelUpdateFromValidationSet(c *C, sequence strin Type: "app", } - restore := devicestate.MockSnapstateUpdateWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + g := goal.(*storeUpdateGoalRecorder) + name := g.snaps[0].InstanceName + channel := g.snaps[0].RevOpts.Channel + switch name { case "snap-1", "pc": default: c.Fatalf("unexpected snap update: %s", name) } - c.Check(opts.Revision, Equals, snap.R(2)) + // snapstate handles picking the right revision based on the given + // validation sets + rev := g.snaps[0].RevOpts.Revision + c.Check(rev.Unset(), Equals, true) - tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s to track %s", name, opts.Channel)) + tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s to track %s", name, channel)) tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) tValidate.WaitFor(tDownload) @@ -6533,7 +6725,7 @@ func (s *deviceMgrSuite) testRemodelUpdateFromValidationSet(c *C, sequence strin tValidate.Set("snap-setup", essentialSnapsupTemplate) } - tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) + tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, channel)) tUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tUpdate) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) @@ -6706,7 +6898,7 @@ func (s *deviceMgrSuite) testRemodelUpdateFromValidationSet(c *C, sequence strin }, } - tss, err := devicestate.RemodelTasks(context.Background(), s.state, currentModel, newModel, testDeviceCtx, "99", nil, nil, devicestate.RemodelOptions{}) + tss, err := devicestate.RemodelTasks(context.Background(), s.state, currentModel, newModel, testDeviceCtx, "99", nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) // 2*snap update, create recovery system, set model @@ -6821,7 +7013,7 @@ func (s *deviceMgrSuite) testRemodelInvalidFromValidationSet(c *C, invalidSnap s }, } - _, err = devicestate.RemodelTasks(context.Background(), s.state, currentModel, newModel, testDeviceCtx, "99", nil, nil, devicestate.RemodelOptions{}) + _, err = devicestate.RemodelTasks(context.Background(), s.state, currentModel, newModel, testDeviceCtx, "99", nil, devicestate.RemodelOptions{}) c.Assert(err, ErrorMatches, fmt.Sprintf("snap presence is marked invalid by validation set: %s", invalidSnap)) } @@ -6940,11 +7132,10 @@ func (s *deviceMgrSuite) testOfflineRemodelValidationSet(c *C, withValSet bool) // content doesn't really matter for this test, since we just use the // presence of local snaps to determine if this is an offline remodel - sis := make([]*snap.SideInfo, 1) - paths := make([]string, 1) - sis[0], paths[0] = createLocalSnap(c, "pc", snaptest.AssertedSnapID("pc"), 1, "gadget", "", nil) + localSnaps := make([]devicestate.LocalSnap, 1) + localSnaps[0].SideInfo, localSnaps[0].Path = createLocalSnap(c, "pc", snaptest.AssertedSnapID("pc"), 1, "gadget", "", nil) - _, err = devicestate.RemodelTasks(context.Background(), s.state, currentModel, newModel, testDeviceCtx, "99", sis, paths, devicestate.RemodelOptions{ + _, err = devicestate.RemodelTasks(context.Background(), s.state, currentModel, newModel, testDeviceCtx, "99", localSnaps, devicestate.RemodelOptions{ Offline: true, }) if !withValSet { @@ -7011,7 +7202,7 @@ func (s *deviceMgrSuite) TestOfflineRemodelMissingSnap(c *C) { snapstatetest.InstallEssentialSnaps(c, s.state, "core20", nil, nil) - _, err = devicestate.RemodelTasks(context.Background(), s.state, currentModel, newModel, testDeviceCtx, "99", nil, nil, devicestate.RemodelOptions{ + _, err = devicestate.RemodelTasks(context.Background(), s.state, currentModel, newModel, testDeviceCtx, "99", nil, devicestate.RemodelOptions{ Offline: true, }) c.Assert(err, ErrorMatches, `no snap file provided for "pc-new"`) @@ -7103,13 +7294,13 @@ func (s *deviceMgrSuite) TestOfflineRemodelPreinstalledIncorrectRevision(c *C) { snapstatetest.InstallEssentialSnaps(c, s.state, "core20", nil, nil) - _, err = devicestate.RemodelTasks(context.Background(), s.state, currentModel, newModel, testDeviceCtx, "99", nil, nil, devicestate.RemodelOptions{ + _, err = devicestate.RemodelTasks(context.Background(), s.state, currentModel, newModel, testDeviceCtx, "99", nil, devicestate.RemodelOptions{ Offline: true, }) - c.Assert(err, ErrorMatches, `installed snap "pc-kernel" does not match revision required to be used for offline remodel: 2 != 1`) + c.Assert(err, ErrorMatches, `installed snap "pc-kernel" does not have the required revision in its sequence to be used for offline remodel: 2`) } -func (s *deviceMgrSuite) TestOfflineRemodelPreinstalledUseOldRevision(c *C) { +func (s *deviceMgrRemodelSuite) TestOfflineRemodelPreinstalledUseOldRevision(c *C) { s.state.Lock() defer s.state.Unlock() s.state.Set("seeded", true) @@ -7156,7 +7347,10 @@ func (s *deviceMgrSuite) TestOfflineRemodelPreinstalledUseOldRevision(c *C) { Channel: "latest/stable", }, snapstatetest.InstallSnapOptions{Required: true, PreserveSequence: true}) - restore := devicestate.MockSnapstateUpdateWithDeviceContext(func(_ *state.State, name string, opts *snapstate.RevisionOptions, _ int, _ snapstate.Flags, prqt snapstate.PrereqTracker, _ snapstate.DeviceContext, _ string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + g := goal.(*storeUpdateGoalRecorder) + name := g.snaps[0].InstanceName + var info *snap.Info switch name { case "pc-kernel": @@ -7167,8 +7361,9 @@ func (s *deviceMgrSuite) TestOfflineRemodelPreinstalledUseOldRevision(c *C) { c.Fatalf("unexpected snap update: %s", name) } - c.Check(opts.Revision, Equals, snap.R(1)) - prqt.Add(baseInfo) + rev := g.snaps[0].RevOpts.Revision + c.Check(rev, Equals, snap.R(1)) + opts.PrereqTracker.Add(baseInfo) prepare := s.state.NewTask("prepare-snap", fmt.Sprintf("prepare %s", name)) prepare.Set("snap-setup", &snapstate.SnapSetup{ @@ -7267,7 +7462,7 @@ func (s *deviceMgrSuite) TestOfflineRemodelPreinstalledUseOldRevision(c *C) { err = assertstate.Add(s.state, vset) c.Assert(err, IsNil) - chg, err := devicestate.Remodel(s.state, newModel, nil, nil, devicestate.RemodelOptions{ + chg, err := devicestate.Remodel(s.state, newModel, nil, devicestate.RemodelOptions{ Offline: true, }) c.Assert(err, IsNil) @@ -7382,7 +7577,7 @@ func (s *deviceMgrSuite) TestRemodelRequiredSnapMissingFromModel(c *C) { }, } - _, err = devicestate.RemodelTasks(context.Background(), s.state, currentModel, newModel, testDeviceCtx, "99", nil, nil, devicestate.RemodelOptions{}) + _, err = devicestate.RemodelTasks(context.Background(), s.state, currentModel, newModel, testDeviceCtx, "99", nil, devicestate.RemodelOptions{}) c.Assert(err, ErrorMatches, "missing required snap in model: snap-1") } @@ -7462,14 +7657,17 @@ func (s *deviceMgrRemodelSuite) TestRemodelVerifyOrderOfTasks(c *C) { Type: "base", } - restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*storeInstallGoalRecorder) + name := g.snaps[0].InstanceName + // currently we do not set essential snaps as required as they are // prevented from being removed by other means if name != "kernel-new" { - c.Check(flags.Required, Equals, true) + c.Check(opts.Flags.Required, Equals, true) } - c.Check(deviceCtx, Equals, testDeviceCtx) - c.Check(fromChange, Equals, "99") + c.Check(opts.DeviceCtx, Equals, testDeviceCtx) + c.Check(opts.FromChange, Equals, "99") tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) switch name { @@ -7492,7 +7690,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelVerifyOrderOfTasks(c *C) { tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() @@ -7579,7 +7777,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelVerifyOrderOfTasks(c *C) { testDeviceCtx = &snapstatetest.TrivialDeviceContext{Remodeling: true, DeviceModel: new, OldDeviceModel: current} - tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99", nil, nil, devicestate.RemodelOptions{}) + tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99", nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) // 5 snaps + create recovery system + set model @@ -7619,8 +7817,12 @@ func (s *deviceMgrRemodelSuite) TestRemodelHybridSystemSkipSeed(c *C) { s.state.Set("seeded", true) s.state.Set("refresh-privacy-key", "some-privacy-key") - restore := devicestate.MockSnapstateUpdateWithDeviceContext(func(_ *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, _ snapstate.PrereqTracker, _ snapstate.DeviceContext, _ string) (*state.TaskSet, error) { - tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s from track %s", name, opts.Channel)) + restore := devicestate.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + g := goal.(*storeUpdateGoalRecorder) + name := g.snaps[0].InstanceName + channel := g.snaps[0].RevOpts.Channel + + tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s from track %s", name, channel)) tDownload.Set("snap-setup", &snapstate.SnapSetup{ SideInfo: &snap.SideInfo{ RealName: name, @@ -7628,7 +7830,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelHybridSystemSkipSeed(c *C) { }) tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) tValidate.WaitFor(tDownload) - tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) + tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, channel)) tUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tUpdate) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) @@ -7719,7 +7921,7 @@ volumes: }, }) - chg, err := devicestate.Remodel(s.state, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) c.Check(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") @@ -7750,8 +7952,12 @@ func (s *deviceMgrRemodelSuite) TestRemodelHybridSystem(c *C) { s.state.Set("seeded", true) s.state.Set("refresh-privacy-key", "some-privacy-key") - restore := devicestate.MockSnapstateUpdateWithDeviceContext(func(_ *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, _ snapstate.PrereqTracker, _ snapstate.DeviceContext, _ string) (*state.TaskSet, error) { - tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s from track %s", name, opts.Channel)) + restore := devicestate.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + g := goal.(*storeUpdateGoalRecorder) + name := g.snaps[0].InstanceName + channel := g.snaps[0].RevOpts.Channel + + tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s from track %s", name, channel)) tDownload.Set("snap-setup", &snapstate.SnapSetup{ SideInfo: &snap.SideInfo{ RealName: name, @@ -7759,7 +7965,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelHybridSystem(c *C) { }) tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) tValidate.WaitFor(tDownload) - tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) + tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, channel)) tUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tUpdate) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) @@ -7846,7 +8052,7 @@ volumes: }, }) - chg, err := devicestate.Remodel(s.state, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(s.state, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) c.Check(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") diff --git a/overlord/devicestate/export_test.go b/overlord/devicestate/export_test.go index 4e28338d994..a9de4195114 100644 --- a/overlord/devicestate/export_test.go +++ b/overlord/devicestate/export_test.go @@ -161,28 +161,28 @@ func MockRepeatRequestSerial(label string) (restore func()) { } } -func MockSnapstateInstallWithDeviceContext(f func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error)) (restore func()) { - r := testutil.Backup(&snapstateInstallWithDeviceContext) - snapstateInstallWithDeviceContext = f - return r +func MockSnapstateUpdateOne(mock func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error)) (restore func()) { + return testutil.Mock(&snapstateUpdateOne, mock) } -func MockSnapstateInstallPathWithDeviceContext(f func(st *state.State, si *snap.SideInfo, path, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error)) (restore func()) { - r := testutil.Backup(&snapstateInstallPathWithDeviceContext) - snapstateInstallPathWithDeviceContext = f - return r +func MockSnapstateInstallOne(mock func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error)) (restore func()) { + return testutil.Mock(&snapstateInstallOne, mock) } -func MockSnapstateUpdateWithDeviceContext(f func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error)) (restore func()) { - r := testutil.Backup(&snapstateUpdateWithDeviceContext) - snapstateUpdateWithDeviceContext = f - return r +func MockSnapstatePathUpdateGoal(mock func(snaps ...snapstate.PathSnap) snapstate.UpdateGoal) (restore func()) { + return testutil.Mock(&snapstatePathUpdateGoal, mock) } -func MockSnapstateUpdatePathWithDeviceContext(f func(st *state.State, si *snap.SideInfo, path, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error)) (restore func()) { - r := testutil.Backup(&snapstateUpdatePathWithDeviceContext) - snapstateUpdatePathWithDeviceContext = f - return r +func MockSnapstateStoreInstallGoal(mock func(snaps ...snapstate.StoreSnap) snapstate.InstallGoal) (restore func()) { + return testutil.Mock(&snapstateStoreInstallGoal, mock) +} + +func MockSnapstateStoreUpdateGoal(mock func(snaps ...snapstate.StoreUpdate) snapstate.UpdateGoal) (restore func()) { + return testutil.Mock(&snapstateStoreUpdateGoal, mock) +} + +func MockSnapstatePathInstallGoal(mock func(snapstate.PathSnap) snapstate.InstallGoal) (restore func()) { + return testutil.Mock(&snapstatePathInstallGoal, mock) } func MockSnapstateDownload(f func(ctx context.Context, st *state.State, name string, components []string, blobDirectory string, revOpts snapstate.RevisionOptions, opts snapstate.Options) (*state.TaskSet, *snap.Info, error)) (restore func()) { diff --git a/overlord/devicestate/firstboot.go b/overlord/devicestate/firstboot.go index eeebac88345..afe525f4ebf 100644 --- a/overlord/devicestate/firstboot.go +++ b/overlord/devicestate/firstboot.go @@ -55,15 +55,22 @@ func installSeedSnap(st *state.State, sn *seed.Snap, flags snapstate.Flags, prqt flags.DevMode = true } - compsSideInfos := make(map[*snap.ComponentSideInfo]string, len(sn.Components)) + components := make([]snapstate.PathComponent, 0, len(sn.Components)) for _, comp := range sn.Components { // Prevent reusing loop variable comp := comp - compsSideInfos[&comp.CompSideInfo] = comp.Path + components = append(components, snapstate.PathComponent{ + Path: comp.Path, + SideInfo: &comp.CompSideInfo, + }) } - goal := snapstate.PathInstallGoal("", sn.Path, sn.SideInfo, compsSideInfos, - snapstate.RevisionOptions{Channel: sn.Channel}) + goal := snapstate.PathInstallGoal(snapstate.PathSnap{ + Path: sn.Path, + SideInfo: sn.SideInfo, + Components: components, + RevOpts: snapstate.RevisionOptions{Channel: sn.Channel}, + }) info, ts, err := snapstate.InstallOne(context.Background(), st, goal, snapstate.Options{ Flags: flags, PrereqTracker: prqt, diff --git a/overlord/devicestate/handlers_remodel.go b/overlord/devicestate/handlers_remodel.go index 521f023e62e..6b3e9dcb863 100644 --- a/overlord/devicestate/handlers_remodel.go +++ b/overlord/devicestate/handlers_remodel.go @@ -320,7 +320,7 @@ func (m *DeviceManager) doPrepareRemodeling(t *state.Task, tmb *tomb.Tomb) error chgID := t.Change().ID() - tss, err := remodelTasks(tmb.Context(nil), st, current, remodCtx.Model(), remodCtx, chgID, nil, nil, RemodelOptions{}) + tss, err := remodelTasks(tmb.Context(nil), st, current, remodCtx.Model(), remodCtx, chgID, nil, RemodelOptions{}) if err != nil { return err } diff --git a/overlord/devicestate/handlers_test.go b/overlord/devicestate/handlers_test.go index 86fbce1ba06..7ea1bb563bb 100644 --- a/overlord/devicestate/handlers_test.go +++ b/overlord/devicestate/handlers_test.go @@ -499,10 +499,13 @@ func (s *deviceMgrSuite) TestDoPrepareRemodeling(c *C) { var testStore snapstate.StoreService - restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, prqt snapstate.PrereqTracker, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { - c.Check(flags.Required, Equals, true) - c.Check(deviceCtx, NotNil) - c.Check(deviceCtx.ForRemodeling(), Equals, true) + restore := devicestate.MockSnapstateInstallOne(func(ctx context.Context, st *state.State, goal snapstate.InstallGoal, opts snapstate.Options) (*snap.Info, *state.TaskSet, error) { + g := goal.(*storeInstallGoalRecorder) + name := g.snaps[0].InstanceName + + c.Check(opts.Flags.Required, Equals, true) + c.Check(opts.DeviceCtx, NotNil) + c.Check(opts.DeviceCtx.ForRemodeling(), Equals, true) tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) tDownload.Set("snap-setup", &snapstate.SnapSetup{ @@ -516,7 +519,7 @@ func (s *deviceMgrSuite) TestDoPrepareRemodeling(c *C) { tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) - return ts, nil + return nil, ts, nil }) defer restore() diff --git a/overlord/managers_test.go b/overlord/managers_test.go index 38043d7df51..2556ec315b5 100644 --- a/overlord/managers_test.go +++ b/overlord/managers_test.go @@ -5170,7 +5170,7 @@ func (s *mgrsSuiteCore) TestRemodelRequiredSnapsAdded(c *C) { "revision": "1", }) - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) c.Check(devicestate.RemodelingChange(st), NotNil) @@ -5279,7 +5279,7 @@ func (s *mgrsSuiteCore) TestRemodelRequiredSnapsAddedUndo(c *C) { devicestate.InjectSetModelError(fmt.Errorf("boom")) defer devicestate.InjectSetModelError(nil) - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) st.Unlock() @@ -5346,7 +5346,7 @@ type: base` "revision": "1", }) - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, ErrorMatches, "cannot remodel from core to bases yet") c.Assert(chg, IsNil) } @@ -5453,7 +5453,7 @@ version: 20.04` "required-snaps": []interface{}{"foo"}, }) - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) st.Unlock() @@ -5625,7 +5625,7 @@ version: 20.04` devicestate.InjectSetModelError(fmt.Errorf("boom")) defer devicestate.InjectSetModelError(nil) - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) st.Unlock() @@ -5786,7 +5786,7 @@ version: 20.04` "required-snaps": []interface{}{"foo"}, }) - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) st.Unlock() @@ -6161,7 +6161,7 @@ func (s *kernelSuite) TestRemodelSwitchKernelTrack(c *C) { "required-snaps": []interface{}{"foo"}, }) - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) st.Unlock() @@ -6215,7 +6215,7 @@ func (ms *kernelSuite) TestRemodelSwitchToDifferentKernel(c *C) { "required-snaps": []interface{}{"foo"}, }) - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) st.Unlock() @@ -6307,7 +6307,7 @@ func (ms *kernelSuite) TestRemodelSwitchToDifferentKernelUndo(c *C) { devicestate.InjectSetModelError(fmt.Errorf("boom")) defer devicestate.InjectSetModelError(nil) - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) st.Unlock() @@ -6365,7 +6365,7 @@ func (ms *kernelSuite) TestRemodelSwitchToDifferentKernelUndoOnRollback(c *C) { devicestate.InjectSetModelError(fmt.Errorf("boom")) defer devicestate.InjectSetModelError(nil) - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) st.Unlock() @@ -6487,7 +6487,7 @@ func (s *mgrsSuiteCore) TestRemodelStoreSwitch(c *C) { s.expectedStore = "switched-store" s.sessionMacaroon = "switched-store-session" - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) st.Unlock() @@ -6601,7 +6601,7 @@ volumes: }) defer r() - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) st.Unlock() @@ -6769,7 +6769,7 @@ volumes: "revision": "1", }) - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) st.Unlock() @@ -6904,7 +6904,7 @@ volumes: "revision": "1", }) - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) st.Unlock() @@ -7145,7 +7145,7 @@ func (s *mgrsSuiteCore) TestRemodelReregistration(c *C) { s.expectedStore = "my-brand-substore" s.sessionMacaroon = "other-store-session" - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) st.Unlock() @@ -7597,7 +7597,7 @@ func (s *mgrsSuiteCore) testRemodelUC20WithRecoverySystem(c *C, encrypted bool) }) defer restore() - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) c.Check(devicestate.RemodelingChange(st), NotNil) @@ -7986,7 +7986,7 @@ func (s *mgrsSuiteCore) TestRemodelUC20DifferentKernelChannel(c *C) { }) defer r() - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) st.Unlock() err = s.o.Settle(settleTimeout) @@ -8146,7 +8146,7 @@ func (s *mgrsSuiteCore) TestRemodelUC20DifferentGadgetChannel(c *C) { }) defer restore() - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) st.Unlock() err = s.o.Settle(settleTimeout) @@ -8270,7 +8270,7 @@ func (s *mgrsSuiteCore) TestRemodelUC20DifferentBaseChannel(c *C) { now := time.Now() expectedLabel := now.Format("20060102") - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) st.Unlock() err = s.o.Settle(settleTimeout) @@ -8437,7 +8437,7 @@ func (s *mgrsSuiteCore) TestRemodelUC20BackToPreviousGadget(c *C) { }) defer restore() - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) st.Unlock() err = s.o.Settle(settleTimeout) @@ -8625,7 +8625,7 @@ func (s *mgrsSuiteCore) TestRemodelUC20ExistingGadgetSnapDifferentChannel(c *C) }) defer restore() - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) st.Unlock() err = s.o.Settle(settleTimeout) @@ -8801,7 +8801,7 @@ func (s *mgrsSuiteCore) TestRemodelUC20SnapWithPrereqsMissingDeps(c *C) { }, }) - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) msg := `cannot remodel to model that is not self contained: - cannot use snap "prereq": base "prereq-base" is missing @@ -9110,7 +9110,7 @@ func (s *mgrsSuiteCore) TestRemodelRollbackValidationSets(c *C) { now := time.Now() expectedLabel := now.Format("20060102") - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) dumpTasks(c, "at the beginning", chg.Tasks()) @@ -9570,7 +9570,7 @@ func (s *mgrsSuiteCore) TestRemodelReplaceValidationSets(c *C) { now := time.Now() expectedLabel := now.Format("20060102") - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) dumpTasks(c, "at the beginning", chg.Tasks()) @@ -9878,7 +9878,7 @@ func (s *mgrsSuiteCore) testRemodelUC20ToUC22(c *C, mockSnapdRefresh bool) { now := time.Now() expectedLabel := now.Format("20060102") - chg, err := devicestate.Remodel(st, newModel, nil, nil, devicestate.RemodelOptions{}) + chg, err := devicestate.Remodel(st, newModel, nil, devicestate.RemodelOptions{}) c.Assert(err, IsNil) dumpTasks(c, "at the beginning", chg.Tasks()) diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go index 28644e3f9d3..193569795c8 100644 --- a/overlord/snapstate/snapstate.go +++ b/overlord/snapstate/snapstate.go @@ -1444,9 +1444,13 @@ type PrereqTracker interface { // local revision and sideloading, or full metadata in which case it // the snap will appear as installed from the store. func InstallPath(st *state.State, si *snap.SideInfo, path, instanceName, channel string, flags Flags, prqt PrereqTracker) (*state.TaskSet, *snap.Info, error) { - target := PathInstallGoal(instanceName, path, si, nil, RevisionOptions{ - Channel: channel, + target := PathInstallGoal(PathSnap{ + InstanceName: instanceName, + Path: path, + SideInfo: si, + RevOpts: RevisionOptions{Channel: channel}, }) + // TODO have caller pass a context info, ts, err := InstallOne(context.Background(), st, target, Options{ Flags: flags, @@ -1524,7 +1528,13 @@ func InstallPathWithDeviceContext(st *state.State, si *snap.SideInfo, path, name opts = &RevisionOptions{} } - target := PathInstallGoal(name, path, si, nil, *opts) + target := PathInstallGoal(PathSnap{ + InstanceName: name, + Path: path, + SideInfo: si, + RevOpts: *opts, + }) + _, ts, err := InstallOne(context.Background(), st, target, Options{ Flags: flags, UserID: userID, diff --git a/overlord/snapstate/snapstate_install_test.go b/overlord/snapstate/snapstate_install_test.go index 6cf71365fa9..6291412dd7c 100644 --- a/overlord/snapstate/snapstate_install_test.go +++ b/overlord/snapstate/snapstate_install_test.go @@ -1136,7 +1136,10 @@ func (s *snapmgrTestSuite) TestInstallPathTooEarly(c *C) { defer r() mockSnap := makeTestSnap(c, "name: some-snap\nversion: 1.0") - t := snapstate.PathInstallGoal("some-snap", mockSnap, &snap.SideInfo{RealName: "some-snap"}, nil, snapstate.RevisionOptions{}) + t := snapstate.PathInstallGoal(snapstate.PathSnap{ + Path: mockSnap, + SideInfo: &snap.SideInfo{RealName: "some-snap"}, + }) _, _, err := snapstate.InstallWithGoal(context.Background(), s.state, t, snapstate.Options{ Seed: true, }) @@ -7092,7 +7095,7 @@ func (s *snapmgrTestSuite) testInstallComponentsFromPathRunThrough(c *C, opts te instanceName := snap.InstanceName(opts.snapName, opts.instanceKey) - components := make(map[*snap.ComponentSideInfo]string, len(opts.components)) + components := make([]snapstate.PathComponent, 0, len(opts.components)) compPaths := make(map[string]string, len(opts.components)) compRevs := make(map[string]snap.Revision) for i, compName := range opts.components { @@ -7120,7 +7123,10 @@ version: 1.0 path := snaptest.MakeTestComponent(c, componentYaml) compPaths[csi.Component.ComponentName] = path - components[csi] = path + components = append(components, snapstate.PathComponent{ + SideInfo: csi, + Path: path, + }) csSideInfo := &snap.ComponentSideInfo{ Component: naming.NewComponentRef(opts.snapName, compName), @@ -7175,7 +7181,12 @@ components: si.Revision = snap.Revision{} } - goal := snapstate.PathInstallGoal(instanceName, snapPath, si, components, snapstate.RevisionOptions{}) + goal := snapstate.PathInstallGoal(snapstate.PathSnap{ + InstanceName: instanceName, + Path: snapPath, + SideInfo: si, + Components: components, + }) info, ts, err := snapstate.InstallOne(context.Background(), s.state, goal, snapstate.Options{ Flags: snapstate.Flags{ @@ -7379,8 +7390,8 @@ components: } c.Check(snapPath, fileChecker) - for _, compPath := range components { - c.Check(compPath, fileChecker) + for _, comp := range components { + c.Check(comp.Path, fileChecker) } } } @@ -7422,9 +7433,10 @@ func (s *snapmgrTestSuite) TestInstallComponentsFromPathInvalidComponentFile(c * err := os.WriteFile(compPath, []byte("invalid-component"), 0644) c.Assert(err, IsNil) - components := map[*snap.ComponentSideInfo]string{ - &csi: compPath, - } + components := []snapstate.PathComponent{{ + SideInfo: &csi, + Path: compPath, + }} snapPath := makeTestSnap(c, `name: test-snap version: 1.0 @@ -7438,7 +7450,11 @@ components: Revision: snapRevision, } - goal := snapstate.PathInstallGoal(snapName, snapPath, si, components, snapstate.RevisionOptions{}) + goal := snapstate.PathInstallGoal(snapstate.PathSnap{ + Path: snapPath, + SideInfo: si, + Components: components, + }) _, _, err = snapstate.InstallOne(context.Background(), s.state, goal, snapstate.Options{}) c.Assert(err, ErrorMatches, fmt.Sprintf(`.*cannot process snap or snapdir: file "%s" is invalid.*`, compPath)) } @@ -7462,9 +7478,9 @@ func (s *snapmgrTestSuite) TestInstallComponentsFromPathInvalidComponentName(c * Revision: snap.R(1), } - components := map[*snap.ComponentSideInfo]string{ - &csi: "", - } + components := []snapstate.PathComponent{{ + SideInfo: &csi, + }} snapPath := makeTestSnap(c, `name: test-snap version: 1.0 @@ -7478,7 +7494,11 @@ components: Revision: snapRevision, } - goal := snapstate.PathInstallGoal(snapName, snapPath, si, components, snapstate.RevisionOptions{}) + goal := snapstate.PathInstallGoal(snapstate.PathSnap{ + Path: snapPath, + SideInfo: si, + Components: components, + }) _, _, err := snapstate.InstallOne(context.Background(), s.state, goal, snapstate.Options{}) c.Assert(err, ErrorMatches, fmt.Sprintf(`invalid snap name: "%s"`, componentName)) } diff --git a/overlord/snapstate/snapstate_update_test.go b/overlord/snapstate/snapstate_update_test.go index 53e624d66a3..ce79223fc10 100644 --- a/overlord/snapstate/snapstate_update_test.go +++ b/overlord/snapstate/snapstate_update_test.go @@ -17260,7 +17260,7 @@ func (s *snapmgrTestSuite) testUpdateWithComponentsFromPathRunThrough(c *C, inst InstanceKey: instanceKey, }) - components := make(map[*snap.ComponentSideInfo]string, len(compNames)) + components := make([]snapstate.PathComponent, 0, len(compNames)) componentPaths := make(map[string]string, len(compNames)) for _, cs := range expectedComponentStates { componentYaml := fmt.Sprintf(`component: %s @@ -17270,7 +17270,10 @@ version: 1.0 path := snaptest.MakeTestComponent(c, componentYaml) componentPaths[cs.SideInfo.Component.ComponentName] = path - components[cs.SideInfo] = path + components = append(components, snapstate.PathComponent{ + Path: path, + SideInfo: cs.SideInfo, + }) } var snapPath string @@ -17364,7 +17367,7 @@ components: name: instanceName, revno: newSnapRev, componentName: compName, - componentPath: components[cs.SideInfo], + componentPath: componentPaths[cs.SideInfo.Component.ComponentName], componentRev: compRev, componentSideInfo: *cs.SideInfo, componentSkipAssertionsDownload: true, @@ -17599,8 +17602,8 @@ components: c.Assert(snapst.Sequence, DeepEquals, currentSeq) c.Check(snapPath, testutil.FileAbsent) - for _, compPath := range components { - c.Check(compPath, testutil.FileAbsent) + for _, comp := range components { + c.Check(comp.Path, testutil.FileAbsent) } } else { // make sure everything is back to how it started diff --git a/overlord/snapstate/target.go b/overlord/snapstate/target.go index 9096e764c73..af09c41be64 100644 --- a/overlord/snapstate/target.go +++ b/overlord/snapstate/target.go @@ -813,19 +813,13 @@ type pathInstallGoal struct { // PathInstallGoal creates a new InstallGoal to install a snap from a given from // a path on disk. If instanceName is not provided, si.RealName will be used. -func PathInstallGoal(instanceName, path string, si *snap.SideInfo, components map[*snap.ComponentSideInfo]string, opts RevisionOptions) InstallGoal { - if instanceName == "" { - instanceName = si.RealName +func PathInstallGoal(sn PathSnap) InstallGoal { + if sn.InstanceName == "" { + sn.InstanceName = sn.SideInfo.RealName } return &pathInstallGoal{ - snap: PathSnap{ - InstanceName: instanceName, - Path: path, - RevOpts: opts, - SideInfo: si, - Components: components, - }, + snap: sn, } } @@ -843,17 +837,17 @@ func (p *pathInstallGoal) toInstall(ctx context.Context, st *state.State, opts O return []target{t}, nil } -func componentSetupsFromPaths(snapInfo *snap.Info, components map[*snap.ComponentSideInfo]string) ([]ComponentSetup, error) { +func componentSetupsFromPaths(snapInfo *snap.Info, components []PathComponent) ([]ComponentSetup, error) { setups := make([]ComponentSetup, 0, len(components)) - for csi, path := range components { - compInfo, err := validatedComponentInfo(path, snapInfo, csi) + for _, pc := range components { + compInfo, err := validatedComponentInfo(pc.Path, snapInfo, pc.SideInfo) if err != nil { return nil, err } setups = append(setups, ComponentSetup{ - CompPath: path, - CompSideInfo: csi, + CompPath: pc.Path, + CompSideInfo: pc.SideInfo, CompType: compInfo.Type, }) } @@ -1322,6 +1316,15 @@ func initRefreshAllStoreUpdates(st *state.State, opts Options, allSnaps map[stri return updates, nil } +// PathComponent represents a component of a snap that is to be installed +// alongside a PathSnap. +type PathComponent struct { + // SideInfo contains extra information about the component. + SideInfo *snap.ComponentSideInfo + // Path is the path to the component on disk. + Path string +} + // PathSnap represents a single snap to be installed or updated from a path on // disk. type PathSnap struct { @@ -1336,7 +1339,7 @@ type PathSnap struct { SideInfo *snap.SideInfo // Components is a mapping of component side infos to paths that should be // installed alongside this snap. - Components map[*snap.ComponentSideInfo]string + Components []PathComponent } // pathUpdateGoal implements the UpdateGoal interface and represents a group of diff --git a/overlord/snapstate/target_test.go b/overlord/snapstate/target_test.go index 6b4b5693aac..9f55cb8dac6 100644 --- a/overlord/snapstate/target_test.go +++ b/overlord/snapstate/target_test.go @@ -260,15 +260,21 @@ version: 1.0 } snapPath := makeTestSnap(c, snapYaml) - csi := &snap.ComponentSideInfo{ + csi := snap.ComponentSideInfo{ Component: naming.NewComponentRef(snapName, compName), Revision: snap.R(3), } - components := map[*snap.ComponentSideInfo]string{ - csi: snaptest.MakeTestComponent(c, componentYaml), - } - goal := snapstate.PathInstallGoal(snapName, snapPath, si, components, snapstate.RevisionOptions{}) + components := []snapstate.PathComponent{{ + SideInfo: &csi, + Path: snaptest.MakeTestComponent(c, componentYaml), + }} + + goal := snapstate.PathInstallGoal(snapstate.PathSnap{ + Path: snapPath, + SideInfo: si, + Components: components, + }) info, ts, err := snapstate.InstallOne(context.Background(), s.state, goal, snapstate.Options{}) c.Assert(err, IsNil) @@ -308,15 +314,21 @@ version: 1.0 } snapPath := makeTestSnap(c, snapYaml) - csi := &snap.ComponentSideInfo{ + csi := snap.ComponentSideInfo{ Component: naming.NewComponentRef(snapName, compName), Revision: snap.R(3), } - components := map[*snap.ComponentSideInfo]string{ - csi: snaptest.MakeTestComponent(c, componentYaml), - } - goal := snapstate.PathInstallGoal(snapName, snapPath, si, components, snapstate.RevisionOptions{}) + components := []snapstate.PathComponent{{ + SideInfo: &csi, + Path: snaptest.MakeTestComponent(c, componentYaml), + }} + + goal := snapstate.PathInstallGoal(snapstate.PathSnap{ + Path: snapPath, + SideInfo: si, + Components: components, + }) _, _, err := snapstate.InstallOne(context.Background(), s.state, goal, snapstate.Options{}) c.Assert(err, ErrorMatches, "cannot mix unasserted snap and asserted components") @@ -353,15 +365,21 @@ version: 1.0 } snapPath := makeTestSnap(c, snapYaml) - csi := &snap.ComponentSideInfo{ + csi := snap.ComponentSideInfo{ Component: naming.NewComponentRef(snapName, compName), Revision: snap.Revision{}, } - components := map[*snap.ComponentSideInfo]string{ - csi: snaptest.MakeTestComponent(c, componentYaml), - } - goal := snapstate.PathInstallGoal(snapName, snapPath, si, components, snapstate.RevisionOptions{}) + components := []snapstate.PathComponent{{ + SideInfo: &csi, + Path: snaptest.MakeTestComponent(c, componentYaml), + }} + + goal := snapstate.PathInstallGoal(snapstate.PathSnap{ + Path: snapPath, + SideInfo: si, + Components: components, + }) _, _, err := snapstate.InstallOne(context.Background(), s.state, goal, snapstate.Options{}) c.Assert(err, ErrorMatches, "cannot mix asserted snap and unasserted components") @@ -436,7 +454,12 @@ func (s *targetTestSuite) TestInvalidPathGoals(c *C) { _, err := snapstate.UpdateOne(context.Background(), s.state, update, nil, snapstate.Options{}) c.Check(err, ErrorMatches, t.err) - install := snapstate.PathInstallGoal(t.snap.InstanceName, t.snap.Path, t.snap.SideInfo, nil, t.snap.RevOpts) + install := snapstate.PathInstallGoal(snapstate.PathSnap{ + InstanceName: t.snap.InstanceName, + Path: t.snap.Path, + SideInfo: t.snap.SideInfo, + RevOpts: t.snap.RevOpts, + }) _, _, err = snapstate.InstallOne(context.Background(), s.state, install, snapstate.Options{}) c.Check(err, ErrorMatches, t.err) } @@ -477,7 +500,11 @@ components: Revision: snap.R(1), } - goal := snapstate.PathInstallGoal(si.RealName, snapPath, si, nil, snapstate.RevisionOptions{}) + goal := snapstate.PathInstallGoal(snapstate.PathSnap{ + InstanceName: si.RealName, + Path: snapPath, + SideInfo: si, + }) info, ts, err := snapstate.InstallOne(context.Background(), s.state, goal, snapstate.Options{}) c.Assert(err, IsNil) @@ -513,9 +540,10 @@ func (s *targetTestSuite) TestInstallComponentsFromPathInvalidComponentFile(c *C err := os.WriteFile(compPath, []byte("invalid-component"), 0644) c.Assert(err, IsNil) - components := map[*snap.ComponentSideInfo]string{ - &csi: compPath, - } + components := []snapstate.PathComponent{{ + SideInfo: &csi, + Path: compPath, + }} snapPath := makeTestSnap(c, `name: test-snap version: 1.0 @@ -529,7 +557,11 @@ components: Revision: snapRevision, } - goal := snapstate.PathInstallGoal(snapName, snapPath, si, components, snapstate.RevisionOptions{}) + goal := snapstate.PathInstallGoal(snapstate.PathSnap{ + Path: snapPath, + SideInfo: si, + Components: components, + }) _, _, err = snapstate.InstallOne(context.Background(), s.state, goal, snapstate.Options{}) c.Assert(err, ErrorMatches, fmt.Sprintf(`.*cannot process snap or snapdir: file "%s" is invalid.*`, compPath)) } @@ -551,7 +583,11 @@ components: Channel: "edge", } - goal := snapstate.PathInstallGoal(si.RealName, snapPath, si, nil, snapstate.RevisionOptions{}) + goal := snapstate.PathInstallGoal(snapstate.PathSnap{ + InstanceName: si.RealName, + Path: snapPath, + SideInfo: si, + }) info, ts, err := snapstate.InstallOne(context.Background(), s.state, goal, snapstate.Options{}) c.Assert(err, IsNil) @@ -580,8 +616,10 @@ components: Revision: snap.R(1), } - goal := snapstate.PathInstallGoal(si.RealName, snapPath, si, nil, snapstate.RevisionOptions{ - Channel: "edge", + goal := snapstate.PathInstallGoal(snapstate.PathSnap{ + Path: snapPath, + SideInfo: si, + RevOpts: snapstate.RevisionOptions{Channel: "edge"}, }) info, ts, err := snapstate.InstallOne(context.Background(), s.state, goal, snapstate.Options{}) @@ -615,8 +653,10 @@ components: Channel: "stable", } - goal := snapstate.PathInstallGoal(si.RealName, snapPath, si, nil, snapstate.RevisionOptions{ - Channel: "edge", + goal := snapstate.PathInstallGoal(snapstate.PathSnap{ + Path: snapPath, + SideInfo: si, + RevOpts: snapstate.RevisionOptions{Channel: "edge"}, }) _, _, err := snapstate.InstallOne(context.Background(), s.state, goal, snapstate.Options{}) @@ -874,13 +914,15 @@ version: 1.0 } snapPath := makeTestSnap(c, snapYaml) - csi := &snap.ComponentSideInfo{ + csi := snap.ComponentSideInfo{ Component: naming.NewComponentRef(snapName, compName), Revision: snap.R(2), } - components := map[*snap.ComponentSideInfo]string{ - csi: snaptest.MakeTestComponent(c, componentYaml), - } + + components := []snapstate.PathComponent{{ + SideInfo: &csi, + Path: snaptest.MakeTestComponent(c, componentYaml), + }} goal := snapstate.PathUpdateGoal(snapstate.PathSnap{ InstanceName: snapName, @@ -958,9 +1000,10 @@ version: 1.0 err := os.WriteFile(compPath, []byte("invalid-component"), 0644) c.Assert(err, IsNil) - components := map[*snap.ComponentSideInfo]string{ - &csi: compPath, - } + components := []snapstate.PathComponent{{ + SideInfo: &csi, + Path: compPath, + }} goal := snapstate.PathUpdateGoal(snapstate.PathSnap{ InstanceName: snapName, @@ -1035,9 +1078,10 @@ version: 1.0 err := os.WriteFile(compPath, []byte("invalid-component"), 0644) c.Assert(err, IsNil) - components := map[*snap.ComponentSideInfo]string{ - &csi: compPath, - } + components := []snapstate.PathComponent{{ + SideInfo: &csi, + Path: compPath, + }} goal := snapstate.PathUpdateGoal(snapstate.PathSnap{ InstanceName: snapName, @@ -1099,9 +1143,10 @@ version: 1.0 Revision: snap.R(2), } - components := map[*snap.ComponentSideInfo]string{ - &csi: snaptest.MakeTestComponent(c, componentYaml), - } + components := []snapstate.PathComponent{{ + SideInfo: &csi, + Path: snaptest.MakeTestComponent(c, componentYaml), + }} goal := snapstate.PathUpdateGoal(snapstate.PathSnap{ InstanceName: snapName,