diff --git a/client/snap_op.go b/client/snap_op.go index 5eaa8fe6cc0..f1e7b65355d 100644 --- a/client/snap_op.go +++ b/client/snap_op.go @@ -116,6 +116,11 @@ func (client *Client) Revert(name string, options *SnapOptions) (changeID string return client.doSnapAction("revert", name, options) } +// Switch moves the snap to a different channel without a refresh +func (client *Client) Switch(name string, options *SnapOptions) (changeID string, err error) { + return client.doSnapAction("switch", name, options) +} + var ErrDangerousNotApplicable = fmt.Errorf("dangerous option only meaningful when installing from a local file") func (client *Client) doSnapAction(actionName string, snapName string, options *SnapOptions) (changeID string, err error) { diff --git a/cmd/snap/cmd_snap_op.go b/cmd/snap/cmd_snap_op.go index ccc952e439a..6dd6d5a68d5 100644 --- a/cmd/snap/cmd_snap_op.go +++ b/cmd/snap/cmd_snap_op.go @@ -820,6 +820,42 @@ func (x *cmdRevert) Execute(args []string) error { return nil } +var shortSwitchHelp = i18n.G("Switch snap to a different channel") +var longSwitchHelp = i18n.G(` +The switch command switch the given snap to a different channel without +actually doing a refresh. +`) + +type cmdSwitch struct { + channelMixin + + Positional struct { + Snap installedSnapName `positional-arg-name:"" required:"1"` + Channel string `positional-arg-name:"" required:"1"` + } `positional-args:"yes" required:"yes"` +} + +func (x cmdSwitch) Execute(args []string) error { + cli := Client() + name := string(x.Positional.Snap) + channel := string(x.Positional.Channel) + opts := &client.SnapOptions{ + Channel: channel, + } + changeID, err := cli.Switch(name, opts) + if err != nil { + return err + } + + _, err = wait(cli, changeID) + if err != nil { + return err + } + + fmt.Fprintf(Stdout, i18n.G("%s switched to %s\n"), name, channel) + return nil +} + func init() { addCommand("remove", shortRemoveHelp, longRemoveHelp, func() flags.Commander { return &cmdRemove{} }, map[string]string{"revision": i18n.G("Remove only the given revision")}, nil) @@ -841,4 +877,6 @@ func init() { addCommand("revert", shortRevertHelp, longRevertHelp, func() flags.Commander { return &cmdRevert{} }, modeDescs.also(map[string]string{ "revision": "Revert to the given revision", }), nil) + addCommand("switch", shortSwitchHelp, longSwitchHelp, func() flags.Commander { return &cmdSwitch{} }, nil, nil) + } diff --git a/daemon/api.go b/daemon/api.go index e8a4b768b10..1dd7d895379 100644 --- a/daemon/api.go +++ b/daemon/api.go @@ -758,6 +758,7 @@ var ( snapstateRemoveMany = snapstate.RemoveMany snapstateRevert = snapstate.Revert snapstateRevertToRevision = snapstate.RevertToRevision + snapstateSwitch = snapstate.Switch assertstateRefreshSnapDeclarations = assertstate.RefreshSnapDeclarations ) @@ -1009,6 +1010,19 @@ func snapDisable(inst *snapInstruction, st *state.State) (string, []*state.TaskS return msg, []*state.TaskSet{ts}, nil } +func snapSwitch(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { + if !inst.Revision.Unset() { + return "", nil, errors.New("switch takes no revision") + } + ts, err := snapstate.Switch(st, inst.Snaps[0], inst.Channel) + if err != nil { + return "", nil, err + } + + msg := fmt.Sprintf(i18n.G("Switch %q snap to %s"), inst.Snaps[0], inst.Channel) + return msg, []*state.TaskSet{ts}, nil +} + type snapActionFunc func(*snapInstruction, *state.State) (string, []*state.TaskSet, error) var snapInstructionDispTable = map[string]snapActionFunc{ @@ -1018,6 +1032,7 @@ var snapInstructionDispTable = map[string]snapActionFunc{ "revert": snapRevert, "enable": snapEnable, "disable": snapDisable, + "switch": snapSwitch, } func (inst *snapInstruction) dispatch() snapActionFunc { diff --git a/daemon/api_test.go b/daemon/api_test.go index 965bee7ef34..f40e21b4148 100644 --- a/daemon/api_test.go +++ b/daemon/api_test.go @@ -495,6 +495,7 @@ func (s *apiSuite) TestListIncludesAll(c *check.C) { "snapstateRefreshCandidates", "snapstateRevert", "snapstateRevertToRevision", + "snapstateSwitch", "assertstateRefreshSnapDeclarations", "unsafeReadSnapInfo", "osutilAddUser", diff --git a/overlord/snapstate/snapmgr.go b/overlord/snapstate/snapmgr.go index 660cdb573b2..5805dd24911 100644 --- a/overlord/snapstate/snapmgr.go +++ b/overlord/snapstate/snapmgr.go @@ -394,6 +394,9 @@ func Manager(st *state.State) (*SnapManager, error) { runner.AddHandler("setup-aliases", m.doSetupAliases, m.undoSetupAliases) runner.AddHandler("remove-aliases", m.doRemoveAliases, m.doSetupAliases) + // misc + runner.AddHandler("switch-snap", m.doSwitchSnap, nil) + // control serialisation runner.SetBlocked(m.blockedTask) @@ -994,9 +997,23 @@ func (m *SnapManager) doCopySnapData(t *state.Task, _ *tomb.Tomb) error { return m.backend.CopySnapData(newInfo, oldInfo, pb) } -func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) error { +func (m *SnapManager) doSwitchSnap(t *state.Task, _ *tomb.Tomb) error { st := t.State() + st.Lock() + defer st.Unlock() + snapsup, snapst, err := snapSetupAndState(t) + if err != nil { + return err + } + snapst.Channel = snapsup.Channel + + Set(st, snapsup.Name(), snapst) + return nil +} + +func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) error { + st := t.State() st.Lock() defer st.Unlock() diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go index 663909d4c61..462ca24eabc 100644 --- a/overlord/snapstate/snapstate.go +++ b/overlord/snapstate/snapstate.go @@ -674,6 +674,28 @@ func autoAliasesUpdate(st *state.State, names []string, updates []*snap.Info) (n return new, mustRetire, transferTargets, nil } +// Switch switches a snap to a new channel +func Switch(st *state.State, name, channel string) (*state.TaskSet, error) { + var snapst SnapState + err := Get(st, name, &snapst) + if err != nil && err != state.ErrNoState { + return nil, err + } + if !snapst.HasCurrent() { + return nil, fmt.Errorf("cannot find snap %q", name) + } + + snapsup := &SnapSetup{ + SideInfo: snapst.CurrentSideInfo(), + Channel: channel, + } + + switchSnap := st.NewTask("switch-snap", fmt.Sprintf(i18n.G("Switch snap %q to %s"), snapsup.Name(), snapsup.Channel)) + switchSnap.Set("snap-setup", &snapsup) + + return state.NewTaskSet(switchSnap), nil +} + // Update initiates a change updating a snap. // Note that the state must be locked by the caller. func Update(st *state.State, name, channel string, revision snap.Revision, userID int, flags Flags) (*state.TaskSet, error) { diff --git a/tests/main/snap-switch/task.yaml b/tests/main/snap-switch/task.yaml new file mode 100644 index 00000000000..2a043c5da13 --- /dev/null +++ b/tests/main/snap-switch/task.yaml @@ -0,0 +1,8 @@ +summary: Ensure that the snap switch command works + +execute: | + snap install test-snapd-tools + snap info test-snapd-tools|MATCH "tracking: +stable" + + snap switch test-snapd-tools edge + snap info test-snapd-tools|MATCH "tracking: +edge"