github.com/bugraaydogar/snapd@v0.0.0-20210315170335-8c70bb858939/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  		UserOK:   true,
    50  		PolkitOK: "io.snapcraft.snapd.manage",
    51  		GET:      getSnapInfo,
    52  		POST:     postSnap,
    53  	}
    54  
    55  	snapsCmd = &Command{
    56  		Path:     "/v2/snaps",
    57  		UserOK:   true,
    58  		PolkitOK: "io.snapcraft.snapd.manage",
    59  		GET:      getSnapsInfo,
    60  		POST:     postSnaps,
    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, nil)
    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, &Meta{Change: 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  
   229  	return flags, nil
   230  }
   231  
   232  func (inst *snapInstruction) validate() error {
   233  	if inst.CohortKey != "" {
   234  		if inst.Action != "install" && inst.Action != "refresh" && inst.Action != "switch" {
   235  			return fmt.Errorf("cohort-key can only be specified for install, refresh, or switch")
   236  		}
   237  	}
   238  	if inst.LeaveCohort {
   239  		if inst.Action != "refresh" && inst.Action != "switch" {
   240  			return fmt.Errorf("leave-cohort can only be specified for refresh or switch")
   241  		}
   242  	}
   243  	if inst.Action == "install" {
   244  		for _, snapName := range inst.Snaps {
   245  			// FIXME: alternatively we could simply mutate *inst
   246  			//        and s/ubuntu-core/core/ ?
   247  			if snapName == "ubuntu-core" {
   248  				return fmt.Errorf(`cannot install "ubuntu-core", please use "core" instead`)
   249  			}
   250  		}
   251  	}
   252  
   253  	return inst.snapRevisionOptions.validate()
   254  }
   255  
   256  type snapInstructionResult struct {
   257  	Summary  string
   258  	Affected []string
   259  	Tasksets []*state.TaskSet
   260  	Result   map[string]interface{}
   261  }
   262  
   263  var errDevJailModeConflict = errors.New("cannot use devmode and jailmode flags together")
   264  var errClassicDevmodeConflict = errors.New("cannot use classic and devmode flags together")
   265  var errNoJailMode = errors.New("this system cannot honour the jailmode flag")
   266  
   267  func modeFlags(devMode, jailMode, classic bool) (snapstate.Flags, error) {
   268  	flags := snapstate.Flags{}
   269  	devModeOS := sandbox.ForceDevMode()
   270  	switch {
   271  	case jailMode && devModeOS:
   272  		return flags, errNoJailMode
   273  	case jailMode && devMode:
   274  		return flags, errDevJailModeConflict
   275  	case devMode && classic:
   276  		return flags, errClassicDevmodeConflict
   277  	}
   278  	// NOTE: jailmode and classic are allowed together. In that setting,
   279  	// jailmode overrides classic and the app gets regular (non-classic)
   280  	// confinement.
   281  	flags.JailMode = jailMode
   282  	flags.Classic = classic
   283  	flags.DevMode = devMode
   284  	return flags, nil
   285  }
   286  
   287  func snapInstall(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   288  	if len(inst.Snaps[0]) == 0 {
   289  		return "", nil, fmt.Errorf(i18n.G("cannot install snap with empty name"))
   290  	}
   291  
   292  	flags, err := inst.installFlags()
   293  	if err != nil {
   294  		return "", nil, err
   295  	}
   296  
   297  	var ckey string
   298  	if inst.CohortKey == "" {
   299  		logger.Noticef("Installing snap %q revision %s", inst.Snaps[0], inst.Revision)
   300  	} else {
   301  		ckey = strutil.ElliptLeft(inst.CohortKey, 10)
   302  		logger.Noticef("Installing snap %q from cohort %q", inst.Snaps[0], ckey)
   303  	}
   304  	tset, err := snapstateInstall(inst.ctx, st, inst.Snaps[0], inst.revnoOpts(), inst.userID, flags)
   305  	if err != nil {
   306  		return "", nil, err
   307  	}
   308  
   309  	msg := fmt.Sprintf(i18n.G("Install %q snap"), inst.Snaps[0])
   310  	if inst.Channel != "stable" && inst.Channel != "" {
   311  		msg += fmt.Sprintf(" from %q channel", inst.Channel)
   312  	}
   313  	if inst.CohortKey != "" {
   314  		msg += fmt.Sprintf(" from %q cohort", ckey)
   315  	}
   316  	return msg, []*state.TaskSet{tset}, nil
   317  }
   318  
   319  func snapUpdate(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   320  	// TODO: bail if revision is given (and != current?), *or* behave as with install --revision?
   321  	flags, err := inst.modeFlags()
   322  	if err != nil {
   323  		return "", nil, err
   324  	}
   325  	if inst.IgnoreValidation {
   326  		flags.IgnoreValidation = true
   327  	}
   328  	if inst.IgnoreRunning {
   329  		flags.IgnoreRunning = true
   330  	}
   331  	if inst.Amend {
   332  		flags.Amend = true
   333  	}
   334  
   335  	// we need refreshed snap-declarations to enforce refresh-control as best as we can
   336  	if err = assertstateRefreshSnapDeclarations(st, inst.userID); err != nil {
   337  		return "", nil, err
   338  	}
   339  
   340  	ts, err := snapstateUpdate(st, inst.Snaps[0], inst.revnoOpts(), inst.userID, flags)
   341  	if err != nil {
   342  		return "", nil, err
   343  	}
   344  
   345  	msg := fmt.Sprintf(i18n.G("Refresh %q snap"), inst.Snaps[0])
   346  	if inst.Channel != "stable" && inst.Channel != "" {
   347  		msg = fmt.Sprintf(i18n.G("Refresh %q snap from %q channel"), inst.Snaps[0], inst.Channel)
   348  	}
   349  
   350  	return msg, []*state.TaskSet{ts}, nil
   351  }
   352  
   353  func snapRemove(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   354  	ts, err := snapstate.Remove(st, inst.Snaps[0], inst.Revision, &snapstate.RemoveFlags{Purge: inst.Purge})
   355  	if err != nil {
   356  		return "", nil, err
   357  	}
   358  
   359  	msg := fmt.Sprintf(i18n.G("Remove %q snap"), inst.Snaps[0])
   360  	return msg, []*state.TaskSet{ts}, nil
   361  }
   362  
   363  func snapRevert(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   364  	var ts *state.TaskSet
   365  
   366  	flags, err := inst.modeFlags()
   367  	if err != nil {
   368  		return "", nil, err
   369  	}
   370  
   371  	if inst.Revision.Unset() {
   372  		ts, err = snapstateRevert(st, inst.Snaps[0], flags)
   373  	} else {
   374  		ts, err = snapstateRevertToRevision(st, inst.Snaps[0], inst.Revision, flags)
   375  	}
   376  	if err != nil {
   377  		return "", nil, err
   378  	}
   379  
   380  	msg := fmt.Sprintf(i18n.G("Revert %q snap"), inst.Snaps[0])
   381  	return msg, []*state.TaskSet{ts}, nil
   382  }
   383  
   384  func snapEnable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   385  	if !inst.Revision.Unset() {
   386  		return "", nil, errors.New("enable takes no revision")
   387  	}
   388  	ts, err := snapstate.Enable(st, inst.Snaps[0])
   389  	if err != nil {
   390  		return "", nil, err
   391  	}
   392  
   393  	msg := fmt.Sprintf(i18n.G("Enable %q snap"), inst.Snaps[0])
   394  	return msg, []*state.TaskSet{ts}, nil
   395  }
   396  
   397  func snapDisable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   398  	if !inst.Revision.Unset() {
   399  		return "", nil, errors.New("disable takes no revision")
   400  	}
   401  	ts, err := snapstate.Disable(st, inst.Snaps[0])
   402  	if err != nil {
   403  		return "", nil, err
   404  	}
   405  
   406  	msg := fmt.Sprintf(i18n.G("Disable %q snap"), inst.Snaps[0])
   407  	return msg, []*state.TaskSet{ts}, nil
   408  }
   409  
   410  func snapSwitch(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   411  	if !inst.Revision.Unset() {
   412  		return "", nil, errors.New("switch takes no revision")
   413  	}
   414  	ts, err := snapstateSwitch(st, inst.Snaps[0], inst.revnoOpts())
   415  	if err != nil {
   416  		return "", nil, err
   417  	}
   418  
   419  	var msg string
   420  	switch {
   421  	case inst.LeaveCohort && inst.Channel != "":
   422  		msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q and away from cohort"), inst.Snaps[0], inst.Channel)
   423  	case inst.LeaveCohort:
   424  		msg = fmt.Sprintf(i18n.G("Switch %q snap away from cohort"), inst.Snaps[0])
   425  	case inst.CohortKey == "" && inst.Channel != "":
   426  		msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q"), inst.Snaps[0], inst.Channel)
   427  	case inst.CohortKey != "" && inst.Channel == "":
   428  		msg = fmt.Sprintf(i18n.G("Switch %q snap to cohort %q"), inst.Snaps[0], strutil.ElliptLeft(inst.CohortKey, 10))
   429  	default:
   430  		msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q and cohort %q"), inst.Snaps[0], inst.Channel, strutil.ElliptLeft(inst.CohortKey, 10))
   431  	}
   432  	return msg, []*state.TaskSet{ts}, nil
   433  }
   434  
   435  type snapActionFunc func(*snapInstruction, *state.State) (string, []*state.TaskSet, error)
   436  
   437  var snapInstructionDispTable = map[string]snapActionFunc{
   438  	"install": snapInstall,
   439  	"refresh": snapUpdate,
   440  	"remove":  snapRemove,
   441  	"revert":  snapRevert,
   442  	"enable":  snapEnable,
   443  	"disable": snapDisable,
   444  	"switch":  snapSwitch,
   445  }
   446  
   447  func (inst *snapInstruction) dispatch() snapActionFunc {
   448  	if len(inst.Snaps) != 1 {
   449  		logger.Panicf("dispatch only handles single-snap ops; got %d", len(inst.Snaps))
   450  	}
   451  	return snapInstructionDispTable[inst.Action]
   452  }
   453  
   454  func (inst *snapInstruction) errToResponse(err error) Response {
   455  	if len(inst.Snaps) == 0 {
   456  		return errToResponse(err, nil, BadRequest, "cannot %s: %v", inst.Action)
   457  	}
   458  
   459  	return errToResponse(err, inst.Snaps, BadRequest, "cannot %s %s: %v", inst.Action, strutil.Quoted(inst.Snaps))
   460  }
   461  
   462  func postSnaps(c *Command, r *http.Request, user *auth.UserState) Response {
   463  	contentType := r.Header.Get("Content-Type")
   464  
   465  	mediaType, params, err := mime.ParseMediaType(contentType)
   466  	if err != nil {
   467  		return BadRequest("cannot parse content type: %v", err)
   468  	}
   469  
   470  	if mediaType == "application/json" {
   471  		charset := strings.ToUpper(params["charset"])
   472  		if charset != "" && charset != "UTF-8" {
   473  			return BadRequest("unknown charset in content type: %s", contentType)
   474  		}
   475  		return snapOpMany(c, r, user)
   476  	}
   477  
   478  	if !strings.HasPrefix(contentType, "multipart/") {
   479  		return BadRequest("unknown content type: %s", contentType)
   480  	}
   481  
   482  	return sideloadOrTrySnap(c, r.Body, params["boundary"], user)
   483  }
   484  
   485  func snapOpMany(c *Command, r *http.Request, user *auth.UserState) Response {
   486  	route := c.d.router.Get(stateChangeCmd.Path)
   487  	if route == nil {
   488  		return InternalError("cannot find route for change")
   489  	}
   490  
   491  	decoder := json.NewDecoder(r.Body)
   492  	var inst snapInstruction
   493  	if err := decoder.Decode(&inst); err != nil {
   494  		return BadRequest("cannot decode request body into snap instruction: %v", err)
   495  	}
   496  
   497  	// TODO: inst.Amend, etc?
   498  	if inst.Channel != "" || !inst.Revision.Unset() || inst.DevMode || inst.JailMode || inst.CohortKey != "" || inst.LeaveCohort || inst.Purge {
   499  		return BadRequest("unsupported option provided for multi-snap operation")
   500  	}
   501  	if err := inst.validate(); err != nil {
   502  		return BadRequest("%v", err)
   503  	}
   504  
   505  	st := c.d.overlord.State()
   506  	st.Lock()
   507  	defer st.Unlock()
   508  
   509  	if user != nil {
   510  		inst.userID = user.ID
   511  	}
   512  
   513  	op := inst.dispatchForMany()
   514  	if op == nil {
   515  		return BadRequest("unsupported multi-snap operation %q", inst.Action)
   516  	}
   517  	res, err := op(&inst, st)
   518  	if err != nil {
   519  		return inst.errToResponse(err)
   520  	}
   521  
   522  	var chg *state.Change
   523  	if len(res.Tasksets) == 0 {
   524  		chg = st.NewChange(inst.Action+"-snap", res.Summary)
   525  		chg.SetStatus(state.DoneStatus)
   526  	} else {
   527  		chg = newChange(st, inst.Action+"-snap", res.Summary, res.Tasksets, res.Affected)
   528  		ensureStateSoon(st)
   529  	}
   530  
   531  	chg.Set("api-data", map[string]interface{}{"snap-names": res.Affected})
   532  
   533  	return AsyncResponse(res.Result, &Meta{Change: chg.ID()})
   534  }
   535  
   536  type snapManyActionFunc func(*snapInstruction, *state.State) (*snapInstructionResult, error)
   537  
   538  func (inst *snapInstruction) dispatchForMany() (op snapManyActionFunc) {
   539  	switch inst.Action {
   540  	case "refresh":
   541  		op = snapUpdateMany
   542  	case "install":
   543  		op = snapInstallMany
   544  	case "remove":
   545  		op = snapRemoveMany
   546  	case "snapshot":
   547  		// see api_snapshots.go
   548  		op = snapshotMany
   549  	}
   550  	return op
   551  }
   552  
   553  func snapInstallMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) {
   554  	for _, name := range inst.Snaps {
   555  		if len(name) == 0 {
   556  			return nil, fmt.Errorf(i18n.G("cannot install snap with empty name"))
   557  		}
   558  	}
   559  	installed, tasksets, err := snapstateInstallMany(st, inst.Snaps, inst.userID)
   560  	if err != nil {
   561  		return nil, err
   562  	}
   563  
   564  	var msg string
   565  	switch len(inst.Snaps) {
   566  	case 0:
   567  		return nil, fmt.Errorf("cannot install zero snaps")
   568  	case 1:
   569  		msg = fmt.Sprintf(i18n.G("Install snap %q"), inst.Snaps[0])
   570  	default:
   571  		quoted := strutil.Quoted(inst.Snaps)
   572  		// TRANSLATORS: the %s is a comma-separated list of quoted snap names
   573  		msg = fmt.Sprintf(i18n.G("Install snaps %s"), quoted)
   574  	}
   575  
   576  	return &snapInstructionResult{
   577  		Summary:  msg,
   578  		Affected: installed,
   579  		Tasksets: tasksets,
   580  	}, nil
   581  }
   582  
   583  func snapUpdateMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) {
   584  	// 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
   585  	if err := assertstateRefreshSnapDeclarations(st, inst.userID); err != nil {
   586  		return nil, err
   587  	}
   588  
   589  	// TODO: use a per-request context
   590  	updated, tasksets, err := snapstateUpdateMany(context.TODO(), st, inst.Snaps, inst.userID, nil)
   591  	if err != nil {
   592  		return nil, err
   593  	}
   594  
   595  	var msg string
   596  	switch len(updated) {
   597  	case 0:
   598  		if len(inst.Snaps) != 0 {
   599  			// TRANSLATORS: the %s is a comma-separated list of quoted snap names
   600  			msg = fmt.Sprintf(i18n.G("Refresh snaps %s: no updates"), strutil.Quoted(inst.Snaps))
   601  		} else {
   602  			msg = i18n.G("Refresh all snaps: no updates")
   603  		}
   604  	case 1:
   605  		msg = fmt.Sprintf(i18n.G("Refresh snap %q"), updated[0])
   606  	default:
   607  		quoted := strutil.Quoted(updated)
   608  		// TRANSLATORS: the %s is a comma-separated list of quoted snap names
   609  		msg = fmt.Sprintf(i18n.G("Refresh snaps %s"), quoted)
   610  	}
   611  
   612  	return &snapInstructionResult{
   613  		Summary:  msg,
   614  		Affected: updated,
   615  		Tasksets: tasksets,
   616  	}, nil
   617  }
   618  
   619  func snapRemoveMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) {
   620  	removed, tasksets, err := snapstateRemoveMany(st, inst.Snaps)
   621  	if err != nil {
   622  		return nil, err
   623  	}
   624  
   625  	var msg string
   626  	switch len(inst.Snaps) {
   627  	case 0:
   628  		return nil, fmt.Errorf("cannot remove zero snaps")
   629  	case 1:
   630  		msg = fmt.Sprintf(i18n.G("Remove snap %q"), inst.Snaps[0])
   631  	default:
   632  		quoted := strutil.Quoted(inst.Snaps)
   633  		// TRANSLATORS: the %s is a comma-separated list of quoted snap names
   634  		msg = fmt.Sprintf(i18n.G("Remove snaps %s"), quoted)
   635  	}
   636  
   637  	return &snapInstructionResult{
   638  		Summary:  msg,
   639  		Affected: removed,
   640  		Tasksets: tasksets,
   641  	}, nil
   642  }
   643  
   644  // query many snaps
   645  func getSnapsInfo(c *Command, r *http.Request, user *auth.UserState) Response {
   646  
   647  	if shouldSearchStore(r) {
   648  		logger.Noticef("Jumping to \"find\" to better support legacy request %q", r.URL)
   649  		return searchStore(c, r, user)
   650  	}
   651  
   652  	route := c.d.router.Get(snapCmd.Path)
   653  	if route == nil {
   654  		return InternalError("cannot find route for snaps")
   655  	}
   656  
   657  	query := r.URL.Query()
   658  	var all bool
   659  	sel := query.Get("select")
   660  	switch sel {
   661  	case "all":
   662  		all = true
   663  	case "enabled", "":
   664  		all = false
   665  	default:
   666  		return BadRequest("invalid select parameter: %q", sel)
   667  	}
   668  	var wanted map[string]bool
   669  	if ns := query.Get("snaps"); len(ns) > 0 {
   670  		nsl := strutil.CommaSeparatedList(ns)
   671  		wanted = make(map[string]bool, len(nsl))
   672  		for _, name := range nsl {
   673  			wanted[name] = true
   674  		}
   675  	}
   676  
   677  	found, err := allLocalSnapInfos(c.d.overlord.State(), all, wanted)
   678  	if err != nil {
   679  		return InternalError("cannot list local snaps! %v", err)
   680  	}
   681  
   682  	results := make([]*json.RawMessage, len(found))
   683  
   684  	sd := servicestate.NewStatusDecorator(progress.Null)
   685  	for i, x := range found {
   686  		name := x.info.InstanceName()
   687  		rev := x.info.Revision
   688  
   689  		url, err := route.URL("name", name)
   690  		if err != nil {
   691  			logger.Noticef("Cannot build URL for snap %q revision %s: %v", name, rev, err)
   692  			continue
   693  		}
   694  
   695  		data, err := json.Marshal(webify(mapLocal(x, sd), url.String()))
   696  		if err != nil {
   697  			return InternalError("cannot serialize snap %q revision %s: %v", name, rev, err)
   698  		}
   699  		raw := json.RawMessage(data)
   700  		results[i] = &raw
   701  	}
   702  
   703  	return SyncResponse(results, &Meta{Sources: []string{"local"}})
   704  }
   705  
   706  func shouldSearchStore(r *http.Request) bool {
   707  	// we should jump to the old behaviour iff q is given, or if
   708  	// sources is given and either empty or contains the word
   709  	// 'store'.  Otherwise, local results only.
   710  
   711  	query := r.URL.Query()
   712  
   713  	if _, ok := query["q"]; ok {
   714  		logger.Debugf("use of obsolete \"q\" parameter: %q", r.URL)
   715  		return true
   716  	}
   717  
   718  	if src, ok := query["sources"]; ok {
   719  		logger.Debugf("use of obsolete \"sources\" parameter: %q", r.URL)
   720  		if len(src) == 0 || strings.Contains(src[0], "store") {
   721  			return true
   722  		}
   723  	}
   724  
   725  	return false
   726  }