github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/daemon/api_snaps.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2015-2020 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package daemon
    21  
    22  import (
    23  	"context"
    24  	"encoding/json"
    25  	"errors"
    26  	"fmt"
    27  	"mime"
    28  	"net/http"
    29  	"strings"
    30  
    31  	"github.com/snapcore/snapd/client"
    32  	"github.com/snapcore/snapd/i18n"
    33  	"github.com/snapcore/snapd/logger"
    34  	"github.com/snapcore/snapd/overlord/auth"
    35  	"github.com/snapcore/snapd/overlord/servicestate"
    36  	"github.com/snapcore/snapd/overlord/snapstate"
    37  	"github.com/snapcore/snapd/overlord/state"
    38  	"github.com/snapcore/snapd/progress"
    39  	"github.com/snapcore/snapd/sandbox"
    40  	"github.com/snapcore/snapd/snap"
    41  	"github.com/snapcore/snapd/snap/channel"
    42  	"github.com/snapcore/snapd/strutil"
    43  )
    44  
    45  var (
    46  	// see daemon.go:canAccess for details how the access is controlled
    47  	snapCmd = &Command{
    48  		Path:        "/v2/snaps/{name}",
    49  		GET:         getSnapInfo,
    50  		POST:        postSnap,
    51  		ReadAccess:  openAccess{},
    52  		WriteAccess: authenticatedAccess{Polkit: polkitActionManage},
    53  	}
    54  
    55  	snapsCmd = &Command{
    56  		Path:        "/v2/snaps",
    57  		GET:         getSnapsInfo,
    58  		POST:        postSnaps,
    59  		ReadAccess:  openAccess{},
    60  		WriteAccess: authenticatedAccess{Polkit: polkitActionManage},
    61  	}
    62  )
    63  
    64  func getSnapInfo(c *Command, r *http.Request, user *auth.UserState) Response {
    65  	vars := muxVars(r)
    66  	name := vars["name"]
    67  
    68  	about, err := localSnapInfo(c.d.overlord.State(), name)
    69  	if err != nil {
    70  		if err == errNoSnap {
    71  			return SnapNotFound(name, err)
    72  		}
    73  
    74  		return InternalError("%v", err)
    75  	}
    76  
    77  	route := c.d.router.Get(c.Path)
    78  	if route == nil {
    79  		return InternalError("cannot find route for %q snap", name)
    80  	}
    81  
    82  	url, err := route.URL("name", name)
    83  	if err != nil {
    84  		return InternalError("cannot build URL for %q snap: %v", name, err)
    85  	}
    86  
    87  	sd := servicestate.NewStatusDecorator(progress.Null)
    88  
    89  	result := webify(mapLocal(about, sd), url.String())
    90  
    91  	return SyncResponse(result)
    92  }
    93  
    94  func webify(result *client.Snap, resource string) *client.Snap {
    95  	if result.Icon == "" || strings.HasPrefix(result.Icon, "http") {
    96  		return result
    97  	}
    98  	result.Icon = ""
    99  
   100  	route := appIconCmd.d.router.Get(appIconCmd.Path)
   101  	if route != nil {
   102  		url, err := route.URL("name", result.Name)
   103  		if err == nil {
   104  			result.Icon = url.String()
   105  		}
   106  	}
   107  
   108  	return result
   109  }
   110  
   111  func postSnap(c *Command, r *http.Request, user *auth.UserState) Response {
   112  	route := c.d.router.Get(stateChangeCmd.Path)
   113  	if route == nil {
   114  		return InternalError("cannot find route for change")
   115  	}
   116  
   117  	decoder := json.NewDecoder(r.Body)
   118  	var inst snapInstruction
   119  	if err := decoder.Decode(&inst); err != nil {
   120  		return BadRequest("cannot decode request body into snap instruction: %v", err)
   121  	}
   122  	inst.ctx = r.Context()
   123  
   124  	state := c.d.overlord.State()
   125  	state.Lock()
   126  	defer state.Unlock()
   127  
   128  	if user != nil {
   129  		inst.userID = user.ID
   130  	}
   131  
   132  	vars := muxVars(r)
   133  	inst.Snaps = []string{vars["name"]}
   134  
   135  	if err := inst.validate(); err != nil {
   136  		return BadRequest("%s", err)
   137  	}
   138  
   139  	impl := inst.dispatch()
   140  	if impl == nil {
   141  		return BadRequest("unknown action %s", inst.Action)
   142  	}
   143  
   144  	msg, tsets, err := impl(&inst, state)
   145  	if err != nil {
   146  		return inst.errToResponse(err)
   147  	}
   148  
   149  	chg := newChange(state, inst.Action+"-snap", msg, tsets, inst.Snaps)
   150  
   151  	ensureStateSoon(state)
   152  
   153  	return AsyncResponse(nil, chg.ID())
   154  }
   155  
   156  type snapRevisionOptions struct {
   157  	Channel  string        `json:"channel"`
   158  	Revision snap.Revision `json:"revision"`
   159  
   160  	CohortKey   string `json:"cohort-key"`
   161  	LeaveCohort bool   `json:"leave-cohort"`
   162  }
   163  
   164  func (ropt *snapRevisionOptions) validate() error {
   165  	if ropt.CohortKey != "" {
   166  		if ropt.LeaveCohort {
   167  			return fmt.Errorf("cannot specify both cohort-key and leave-cohort")
   168  		}
   169  		if !ropt.Revision.Unset() {
   170  			return fmt.Errorf("cannot specify both cohort-key and revision")
   171  		}
   172  	}
   173  
   174  	if ropt.Channel != "" {
   175  		_, err := channel.Parse(ropt.Channel, "-")
   176  		if err != nil {
   177  			return err
   178  		}
   179  	}
   180  	return nil
   181  }
   182  
   183  type snapInstruction struct {
   184  	progress.NullMeter
   185  
   186  	Action string `json:"action"`
   187  	Amend  bool   `json:"amend"`
   188  	snapRevisionOptions
   189  	DevMode          bool     `json:"devmode"`
   190  	JailMode         bool     `json:"jailmode"`
   191  	Classic          bool     `json:"classic"`
   192  	IgnoreValidation bool     `json:"ignore-validation"`
   193  	IgnoreRunning    bool     `json:"ignore-running"`
   194  	Unaliased        bool     `json:"unaliased"`
   195  	Purge            bool     `json:"purge,omitempty"`
   196  	Snaps            []string `json:"snaps"`
   197  	Users            []string `json:"users"`
   198  
   199  	// The fields below should not be unmarshalled into. Do not export them.
   200  	userID int
   201  	ctx    context.Context
   202  }
   203  
   204  func (inst *snapInstruction) revnoOpts() *snapstate.RevisionOptions {
   205  	return &snapstate.RevisionOptions{
   206  		Channel:     inst.Channel,
   207  		Revision:    inst.Revision,
   208  		CohortKey:   inst.CohortKey,
   209  		LeaveCohort: inst.LeaveCohort,
   210  	}
   211  }
   212  
   213  func (inst *snapInstruction) modeFlags() (snapstate.Flags, error) {
   214  	return modeFlags(inst.DevMode, inst.JailMode, inst.Classic)
   215  }
   216  
   217  func (inst *snapInstruction) installFlags() (snapstate.Flags, error) {
   218  	flags, err := inst.modeFlags()
   219  	if err != nil {
   220  		return snapstate.Flags{}, err
   221  	}
   222  	if inst.Unaliased {
   223  		flags.Unaliased = true
   224  	}
   225  	if inst.IgnoreRunning {
   226  		flags.IgnoreRunning = true
   227  	}
   228  	if inst.IgnoreValidation {
   229  		flags.IgnoreValidation = true
   230  	}
   231  
   232  	return flags, nil
   233  }
   234  
   235  func (inst *snapInstruction) validate() error {
   236  	if inst.CohortKey != "" {
   237  		if inst.Action != "install" && inst.Action != "refresh" && inst.Action != "switch" {
   238  			return fmt.Errorf("cohort-key can only be specified for install, refresh, or switch")
   239  		}
   240  	}
   241  	if inst.LeaveCohort {
   242  		if inst.Action != "refresh" && inst.Action != "switch" {
   243  			return fmt.Errorf("leave-cohort can only be specified for refresh or switch")
   244  		}
   245  	}
   246  	if inst.Action == "install" {
   247  		for _, snapName := range inst.Snaps {
   248  			// FIXME: alternatively we could simply mutate *inst
   249  			//        and s/ubuntu-core/core/ ?
   250  			if snapName == "ubuntu-core" {
   251  				return fmt.Errorf(`cannot install "ubuntu-core", please use "core" instead`)
   252  			}
   253  		}
   254  	}
   255  
   256  	return inst.snapRevisionOptions.validate()
   257  }
   258  
   259  type snapInstructionResult struct {
   260  	Summary  string
   261  	Affected []string
   262  	Tasksets []*state.TaskSet
   263  	Result   map[string]interface{}
   264  }
   265  
   266  var errDevJailModeConflict = errors.New("cannot use devmode and jailmode flags together")
   267  var errClassicDevmodeConflict = errors.New("cannot use classic and devmode flags together")
   268  var errNoJailMode = errors.New("this system cannot honour the jailmode flag")
   269  
   270  func modeFlags(devMode, jailMode, classic bool) (snapstate.Flags, error) {
   271  	flags := snapstate.Flags{}
   272  	devModeOS := sandbox.ForceDevMode()
   273  	switch {
   274  	case jailMode && devModeOS:
   275  		return flags, errNoJailMode
   276  	case jailMode && devMode:
   277  		return flags, errDevJailModeConflict
   278  	case devMode && classic:
   279  		return flags, errClassicDevmodeConflict
   280  	}
   281  	// NOTE: jailmode and classic are allowed together. In that setting,
   282  	// jailmode overrides classic and the app gets regular (non-classic)
   283  	// confinement.
   284  	flags.JailMode = jailMode
   285  	flags.Classic = classic
   286  	flags.DevMode = devMode
   287  	return flags, nil
   288  }
   289  
   290  func snapInstall(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   291  	if len(inst.Snaps[0]) == 0 {
   292  		return "", nil, fmt.Errorf(i18n.G("cannot install snap with empty name"))
   293  	}
   294  
   295  	flags, err := inst.installFlags()
   296  	if err != nil {
   297  		return "", nil, err
   298  	}
   299  
   300  	var ckey string
   301  	if inst.CohortKey == "" {
   302  		logger.Noticef("Installing snap %q revision %s", inst.Snaps[0], inst.Revision)
   303  	} else {
   304  		ckey = strutil.ElliptLeft(inst.CohortKey, 10)
   305  		logger.Noticef("Installing snap %q from cohort %q", inst.Snaps[0], ckey)
   306  	}
   307  	tset, err := snapstateInstall(inst.ctx, st, inst.Snaps[0], inst.revnoOpts(), inst.userID, flags)
   308  	if err != nil {
   309  		return "", nil, err
   310  	}
   311  
   312  	msg := fmt.Sprintf(i18n.G("Install %q snap"), inst.Snaps[0])
   313  	if inst.Channel != "stable" && inst.Channel != "" {
   314  		msg += fmt.Sprintf(" from %q channel", inst.Channel)
   315  	}
   316  	if inst.CohortKey != "" {
   317  		msg += fmt.Sprintf(" from %q cohort", ckey)
   318  	}
   319  	return msg, []*state.TaskSet{tset}, nil
   320  }
   321  
   322  func snapUpdate(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   323  	// TODO: bail if revision is given (and != current?), *or* behave as with install --revision?
   324  	flags, err := inst.modeFlags()
   325  	if err != nil {
   326  		return "", nil, err
   327  	}
   328  	if inst.IgnoreValidation {
   329  		flags.IgnoreValidation = true
   330  	}
   331  	if inst.IgnoreRunning {
   332  		flags.IgnoreRunning = true
   333  	}
   334  	if inst.Amend {
   335  		flags.Amend = true
   336  	}
   337  
   338  	// we need refreshed snap-declarations to enforce refresh-control as best as we can
   339  	if err = assertstateRefreshSnapDeclarations(st, inst.userID); err != nil {
   340  		return "", nil, err
   341  	}
   342  
   343  	ts, err := snapstateUpdate(st, inst.Snaps[0], inst.revnoOpts(), inst.userID, flags)
   344  	if err != nil {
   345  		return "", nil, err
   346  	}
   347  
   348  	msg := fmt.Sprintf(i18n.G("Refresh %q snap"), inst.Snaps[0])
   349  	if inst.Channel != "stable" && inst.Channel != "" {
   350  		msg = fmt.Sprintf(i18n.G("Refresh %q snap from %q channel"), inst.Snaps[0], inst.Channel)
   351  	}
   352  
   353  	return msg, []*state.TaskSet{ts}, nil
   354  }
   355  
   356  func snapRemove(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   357  	ts, err := snapstate.Remove(st, inst.Snaps[0], inst.Revision, &snapstate.RemoveFlags{Purge: inst.Purge})
   358  	if err != nil {
   359  		return "", nil, err
   360  	}
   361  
   362  	msg := fmt.Sprintf(i18n.G("Remove %q snap"), inst.Snaps[0])
   363  	return msg, []*state.TaskSet{ts}, nil
   364  }
   365  
   366  func snapRevert(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   367  	var ts *state.TaskSet
   368  
   369  	flags, err := inst.modeFlags()
   370  	if err != nil {
   371  		return "", nil, err
   372  	}
   373  
   374  	if inst.Revision.Unset() {
   375  		ts, err = snapstateRevert(st, inst.Snaps[0], flags)
   376  	} else {
   377  		ts, err = snapstateRevertToRevision(st, inst.Snaps[0], inst.Revision, flags)
   378  	}
   379  	if err != nil {
   380  		return "", nil, err
   381  	}
   382  
   383  	msg := fmt.Sprintf(i18n.G("Revert %q snap"), inst.Snaps[0])
   384  	return msg, []*state.TaskSet{ts}, nil
   385  }
   386  
   387  func snapEnable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   388  	if !inst.Revision.Unset() {
   389  		return "", nil, errors.New("enable takes no revision")
   390  	}
   391  	ts, err := snapstate.Enable(st, inst.Snaps[0])
   392  	if err != nil {
   393  		return "", nil, err
   394  	}
   395  
   396  	msg := fmt.Sprintf(i18n.G("Enable %q snap"), inst.Snaps[0])
   397  	return msg, []*state.TaskSet{ts}, nil
   398  }
   399  
   400  func snapDisable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   401  	if !inst.Revision.Unset() {
   402  		return "", nil, errors.New("disable takes no revision")
   403  	}
   404  	ts, err := snapstate.Disable(st, inst.Snaps[0])
   405  	if err != nil {
   406  		return "", nil, err
   407  	}
   408  
   409  	msg := fmt.Sprintf(i18n.G("Disable %q snap"), inst.Snaps[0])
   410  	return msg, []*state.TaskSet{ts}, nil
   411  }
   412  
   413  func snapSwitch(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   414  	if !inst.Revision.Unset() {
   415  		return "", nil, errors.New("switch takes no revision")
   416  	}
   417  	ts, err := snapstateSwitch(st, inst.Snaps[0], inst.revnoOpts())
   418  	if err != nil {
   419  		return "", nil, err
   420  	}
   421  
   422  	var msg string
   423  	switch {
   424  	case inst.LeaveCohort && inst.Channel != "":
   425  		msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q and away from cohort"), inst.Snaps[0], inst.Channel)
   426  	case inst.LeaveCohort:
   427  		msg = fmt.Sprintf(i18n.G("Switch %q snap away from cohort"), inst.Snaps[0])
   428  	case inst.CohortKey == "" && inst.Channel != "":
   429  		msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q"), inst.Snaps[0], inst.Channel)
   430  	case inst.CohortKey != "" && inst.Channel == "":
   431  		msg = fmt.Sprintf(i18n.G("Switch %q snap to cohort %q"), inst.Snaps[0], strutil.ElliptLeft(inst.CohortKey, 10))
   432  	default:
   433  		msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q and cohort %q"), inst.Snaps[0], inst.Channel, strutil.ElliptLeft(inst.CohortKey, 10))
   434  	}
   435  	return msg, []*state.TaskSet{ts}, nil
   436  }
   437  
   438  type snapActionFunc func(*snapInstruction, *state.State) (string, []*state.TaskSet, error)
   439  
   440  var snapInstructionDispTable = map[string]snapActionFunc{
   441  	"install": snapInstall,
   442  	"refresh": snapUpdate,
   443  	"remove":  snapRemove,
   444  	"revert":  snapRevert,
   445  	"enable":  snapEnable,
   446  	"disable": snapDisable,
   447  	"switch":  snapSwitch,
   448  }
   449  
   450  func (inst *snapInstruction) dispatch() snapActionFunc {
   451  	if len(inst.Snaps) != 1 {
   452  		logger.Panicf("dispatch only handles single-snap ops; got %d", len(inst.Snaps))
   453  	}
   454  	return snapInstructionDispTable[inst.Action]
   455  }
   456  
   457  func (inst *snapInstruction) errToResponse(err error) *apiError {
   458  	if len(inst.Snaps) == 0 {
   459  		return errToResponse(err, nil, BadRequest, "cannot %s: %v", inst.Action)
   460  	}
   461  
   462  	return errToResponse(err, inst.Snaps, BadRequest, "cannot %s %s: %v", inst.Action, strutil.Quoted(inst.Snaps))
   463  }
   464  
   465  func postSnaps(c *Command, r *http.Request, user *auth.UserState) Response {
   466  	contentType := r.Header.Get("Content-Type")
   467  
   468  	mediaType, params, err := mime.ParseMediaType(contentType)
   469  	if err != nil {
   470  		return BadRequest("cannot parse content type: %v", err)
   471  	}
   472  
   473  	if mediaType == "application/json" {
   474  		charset := strings.ToUpper(params["charset"])
   475  		if charset != "" && charset != "UTF-8" {
   476  			return BadRequest("unknown charset in content type: %s", contentType)
   477  		}
   478  		return snapOpMany(c, r, user)
   479  	}
   480  
   481  	if !strings.HasPrefix(contentType, "multipart/") {
   482  		return BadRequest("unknown content type: %s", contentType)
   483  	}
   484  
   485  	return sideloadOrTrySnap(c, r.Body, params["boundary"], user)
   486  }
   487  
   488  func snapOpMany(c *Command, r *http.Request, user *auth.UserState) Response {
   489  	route := c.d.router.Get(stateChangeCmd.Path)
   490  	if route == nil {
   491  		return InternalError("cannot find route for change")
   492  	}
   493  
   494  	decoder := json.NewDecoder(r.Body)
   495  	var inst snapInstruction
   496  	if err := decoder.Decode(&inst); err != nil {
   497  		return BadRequest("cannot decode request body into snap instruction: %v", err)
   498  	}
   499  
   500  	// TODO: inst.Amend, etc?
   501  	if inst.Channel != "" || !inst.Revision.Unset() || inst.DevMode || inst.JailMode || inst.CohortKey != "" || inst.LeaveCohort || inst.Purge {
   502  		return BadRequest("unsupported option provided for multi-snap operation")
   503  	}
   504  	if err := inst.validate(); err != nil {
   505  		return BadRequest("%v", err)
   506  	}
   507  
   508  	st := c.d.overlord.State()
   509  	st.Lock()
   510  	defer st.Unlock()
   511  
   512  	if user != nil {
   513  		inst.userID = user.ID
   514  	}
   515  
   516  	op := inst.dispatchForMany()
   517  	if op == nil {
   518  		return BadRequest("unsupported multi-snap operation %q", inst.Action)
   519  	}
   520  	res, err := op(&inst, st)
   521  	if err != nil {
   522  		return inst.errToResponse(err)
   523  	}
   524  
   525  	var chg *state.Change
   526  	if len(res.Tasksets) == 0 {
   527  		chg = st.NewChange(inst.Action+"-snap", res.Summary)
   528  		chg.SetStatus(state.DoneStatus)
   529  	} else {
   530  		chg = newChange(st, inst.Action+"-snap", res.Summary, res.Tasksets, res.Affected)
   531  		ensureStateSoon(st)
   532  	}
   533  
   534  	chg.Set("api-data", map[string]interface{}{"snap-names": res.Affected})
   535  
   536  	return AsyncResponse(res.Result, chg.ID())
   537  }
   538  
   539  type snapManyActionFunc func(*snapInstruction, *state.State) (*snapInstructionResult, error)
   540  
   541  func (inst *snapInstruction) dispatchForMany() (op snapManyActionFunc) {
   542  	switch inst.Action {
   543  	case "refresh":
   544  		op = snapUpdateMany
   545  	case "install":
   546  		op = snapInstallMany
   547  	case "remove":
   548  		op = snapRemoveMany
   549  	case "snapshot":
   550  		// see api_snapshots.go
   551  		op = snapshotMany
   552  	}
   553  	return op
   554  }
   555  
   556  func snapInstallMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) {
   557  	for _, name := range inst.Snaps {
   558  		if len(name) == 0 {
   559  			return nil, fmt.Errorf(i18n.G("cannot install snap with empty name"))
   560  		}
   561  	}
   562  	installed, tasksets, err := snapstateInstallMany(st, inst.Snaps, inst.userID)
   563  	if err != nil {
   564  		return nil, err
   565  	}
   566  
   567  	var msg string
   568  	switch len(inst.Snaps) {
   569  	case 0:
   570  		return nil, fmt.Errorf("cannot install zero snaps")
   571  	case 1:
   572  		msg = fmt.Sprintf(i18n.G("Install snap %q"), inst.Snaps[0])
   573  	default:
   574  		quoted := strutil.Quoted(inst.Snaps)
   575  		// TRANSLATORS: the %s is a comma-separated list of quoted snap names
   576  		msg = fmt.Sprintf(i18n.G("Install snaps %s"), quoted)
   577  	}
   578  
   579  	return &snapInstructionResult{
   580  		Summary:  msg,
   581  		Affected: installed,
   582  		Tasksets: tasksets,
   583  	}, nil
   584  }
   585  
   586  func snapUpdateMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) {
   587  	// we need refreshed snap-declarations to enforce refresh-control as best as we can, this also ensures that snap-declarations and their prerequisite assertions are updated regularly
   588  	if err := assertstateRefreshSnapDeclarations(st, inst.userID); err != nil {
   589  		return nil, err
   590  	}
   591  
   592  	// TODO: use a per-request context
   593  	updated, tasksets, err := snapstateUpdateMany(context.TODO(), st, inst.Snaps, inst.userID, nil)
   594  	if err != nil {
   595  		return nil, err
   596  	}
   597  
   598  	var msg string
   599  	switch len(updated) {
   600  	case 0:
   601  		if len(inst.Snaps) != 0 {
   602  			// TRANSLATORS: the %s is a comma-separated list of quoted snap names
   603  			msg = fmt.Sprintf(i18n.G("Refresh snaps %s: no updates"), strutil.Quoted(inst.Snaps))
   604  		} else {
   605  			msg = i18n.G("Refresh all snaps: no updates")
   606  		}
   607  	case 1:
   608  		msg = fmt.Sprintf(i18n.G("Refresh snap %q"), updated[0])
   609  	default:
   610  		quoted := strutil.Quoted(updated)
   611  		// TRANSLATORS: the %s is a comma-separated list of quoted snap names
   612  		msg = fmt.Sprintf(i18n.G("Refresh snaps %s"), quoted)
   613  	}
   614  
   615  	return &snapInstructionResult{
   616  		Summary:  msg,
   617  		Affected: updated,
   618  		Tasksets: tasksets,
   619  	}, nil
   620  }
   621  
   622  func snapRemoveMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) {
   623  	removed, tasksets, err := snapstateRemoveMany(st, inst.Snaps)
   624  	if err != nil {
   625  		return nil, err
   626  	}
   627  
   628  	var msg string
   629  	switch len(inst.Snaps) {
   630  	case 0:
   631  		return nil, fmt.Errorf("cannot remove zero snaps")
   632  	case 1:
   633  		msg = fmt.Sprintf(i18n.G("Remove snap %q"), inst.Snaps[0])
   634  	default:
   635  		quoted := strutil.Quoted(inst.Snaps)
   636  		// TRANSLATORS: the %s is a comma-separated list of quoted snap names
   637  		msg = fmt.Sprintf(i18n.G("Remove snaps %s"), quoted)
   638  	}
   639  
   640  	return &snapInstructionResult{
   641  		Summary:  msg,
   642  		Affected: removed,
   643  		Tasksets: tasksets,
   644  	}, nil
   645  }
   646  
   647  // query many snaps
   648  func getSnapsInfo(c *Command, r *http.Request, user *auth.UserState) Response {
   649  
   650  	if shouldSearchStore(r) {
   651  		logger.Noticef("Jumping to \"find\" to better support legacy request %q", r.URL)
   652  		return searchStore(c, r, user)
   653  	}
   654  
   655  	route := c.d.router.Get(snapCmd.Path)
   656  	if route == nil {
   657  		return InternalError("cannot find route for snaps")
   658  	}
   659  
   660  	query := r.URL.Query()
   661  	var all bool
   662  	sel := query.Get("select")
   663  	switch sel {
   664  	case "all":
   665  		all = true
   666  	case "enabled", "":
   667  		all = false
   668  	default:
   669  		return BadRequest("invalid select parameter: %q", sel)
   670  	}
   671  	var wanted map[string]bool
   672  	if ns := query.Get("snaps"); len(ns) > 0 {
   673  		nsl := strutil.CommaSeparatedList(ns)
   674  		wanted = make(map[string]bool, len(nsl))
   675  		for _, name := range nsl {
   676  			wanted[name] = true
   677  		}
   678  	}
   679  
   680  	found, err := allLocalSnapInfos(c.d.overlord.State(), all, wanted)
   681  	if err != nil {
   682  		return InternalError("cannot list local snaps! %v", err)
   683  	}
   684  
   685  	results := make([]*json.RawMessage, len(found))
   686  
   687  	sd := servicestate.NewStatusDecorator(progress.Null)
   688  	for i, x := range found {
   689  		name := x.info.InstanceName()
   690  		rev := x.info.Revision
   691  
   692  		url, err := route.URL("name", name)
   693  		if err != nil {
   694  			logger.Noticef("Cannot build URL for snap %q revision %s: %v", name, rev, err)
   695  			continue
   696  		}
   697  
   698  		data, err := json.Marshal(webify(mapLocal(x, sd), url.String()))
   699  		if err != nil {
   700  			return InternalError("cannot serialize snap %q revision %s: %v", name, rev, err)
   701  		}
   702  		raw := json.RawMessage(data)
   703  		results[i] = &raw
   704  	}
   705  
   706  	return &findResponse{
   707  		Results: results,
   708  		Sources: []string{"local"},
   709  	}
   710  }
   711  
   712  func shouldSearchStore(r *http.Request) bool {
   713  	// we should jump to the old behaviour iff q is given, or if
   714  	// sources is given and either empty or contains the word
   715  	// 'store'.  Otherwise, local results only.
   716  
   717  	query := r.URL.Query()
   718  
   719  	if _, ok := query["q"]; ok {
   720  		logger.Debugf("use of obsolete \"q\" parameter: %q", r.URL)
   721  		return true
   722  	}
   723  
   724  	if src, ok := query["sources"]; ok {
   725  		logger.Debugf("use of obsolete \"sources\" parameter: %q", r.URL)
   726  		if len(src) == 0 || strings.Contains(src[0], "store") {
   727  			return true
   728  		}
   729  	}
   730  
   731  	return false
   732  }