github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/daemon/api.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2015-2018 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  	"bytes"
    24  	"context"
    25  	"encoding/json"
    26  	"errors"
    27  	"fmt"
    28  	"io"
    29  	"io/ioutil"
    30  	"mime"
    31  	"mime/multipart"
    32  	"net"
    33  	"net/http"
    34  	"net/url"
    35  	"os"
    36  	"os/exec"
    37  	"path/filepath"
    38  	"sort"
    39  	"strconv"
    40  	"strings"
    41  	"time"
    42  
    43  	"github.com/gorilla/mux"
    44  	"github.com/jessevdk/go-flags"
    45  
    46  	"github.com/snapcore/snapd/arch"
    47  	"github.com/snapcore/snapd/asserts"
    48  	"github.com/snapcore/snapd/asserts/snapasserts"
    49  	"github.com/snapcore/snapd/client"
    50  	"github.com/snapcore/snapd/client/clientutil"
    51  	"github.com/snapcore/snapd/dirs"
    52  	"github.com/snapcore/snapd/httputil"
    53  	"github.com/snapcore/snapd/i18n"
    54  	"github.com/snapcore/snapd/interfaces"
    55  	"github.com/snapcore/snapd/jsonutil"
    56  	"github.com/snapcore/snapd/logger"
    57  	"github.com/snapcore/snapd/osutil"
    58  	"github.com/snapcore/snapd/overlord/assertstate"
    59  	"github.com/snapcore/snapd/overlord/auth"
    60  	"github.com/snapcore/snapd/overlord/configstate"
    61  	"github.com/snapcore/snapd/overlord/configstate/config"
    62  	"github.com/snapcore/snapd/overlord/hookstate/ctlcmd"
    63  	"github.com/snapcore/snapd/overlord/ifacestate"
    64  	"github.com/snapcore/snapd/overlord/servicestate"
    65  	"github.com/snapcore/snapd/overlord/snapshotstate"
    66  	"github.com/snapcore/snapd/overlord/snapstate"
    67  	"github.com/snapcore/snapd/overlord/state"
    68  	"github.com/snapcore/snapd/progress"
    69  	"github.com/snapcore/snapd/release"
    70  	"github.com/snapcore/snapd/sandbox"
    71  	"github.com/snapcore/snapd/snap"
    72  	"github.com/snapcore/snapd/snap/channel"
    73  	"github.com/snapcore/snapd/snap/snapfile"
    74  	"github.com/snapcore/snapd/store"
    75  	"github.com/snapcore/snapd/strutil"
    76  	"github.com/snapcore/snapd/systemd"
    77  )
    78  
    79  var api = []*Command{
    80  	rootCmd,
    81  	sysInfoCmd,
    82  	loginCmd,
    83  	logoutCmd,
    84  	appIconCmd,
    85  	findCmd,
    86  	snapsCmd,
    87  	snapCmd,
    88  	snapFileCmd,
    89  	snapDownloadCmd,
    90  	snapConfCmd,
    91  	interfacesCmd,
    92  	assertsCmd,
    93  	assertsFindManyCmd,
    94  	stateChangeCmd,
    95  	stateChangesCmd,
    96  	createUserCmd,
    97  	buyCmd,
    98  	readyToBuyCmd,
    99  	snapctlCmd,
   100  	usersCmd,
   101  	sectionsCmd,
   102  	aliasesCmd,
   103  	appsCmd,
   104  	logsCmd,
   105  	warningsCmd,
   106  	debugPprofCmd,
   107  	debugCmd,
   108  	snapshotCmd,
   109  	snapshotExportCmd,
   110  	connectionsCmd,
   111  	modelCmd,
   112  	cohortsCmd,
   113  	serialModelCmd,
   114  	systemsCmd,
   115  	systemsActionCmd,
   116  }
   117  
   118  var servicestateControl = servicestate.Control
   119  
   120  var (
   121  	// see daemon.go:canAccess for details how the access is controlled
   122  	rootCmd = &Command{
   123  		Path:    "/",
   124  		GuestOK: true,
   125  		GET:     tbd,
   126  	}
   127  
   128  	sysInfoCmd = &Command{
   129  		Path:    "/v2/system-info",
   130  		GuestOK: true,
   131  		GET:     sysInfo,
   132  	}
   133  
   134  	appIconCmd = &Command{
   135  		Path:   "/v2/icons/{name}/icon",
   136  		UserOK: true,
   137  		GET:    appIconGet,
   138  	}
   139  
   140  	findCmd = &Command{
   141  		Path:   "/v2/find",
   142  		UserOK: true,
   143  		GET:    searchStore,
   144  	}
   145  
   146  	snapsCmd = &Command{
   147  		Path:     "/v2/snaps",
   148  		UserOK:   true,
   149  		PolkitOK: "io.snapcraft.snapd.manage",
   150  		GET:      getSnapsInfo,
   151  		POST:     postSnaps,
   152  	}
   153  
   154  	snapCmd = &Command{
   155  		Path:     "/v2/snaps/{name}",
   156  		UserOK:   true,
   157  		PolkitOK: "io.snapcraft.snapd.manage",
   158  		GET:      getSnapInfo,
   159  		POST:     postSnap,
   160  	}
   161  
   162  	appsCmd = &Command{
   163  		Path:   "/v2/apps",
   164  		UserOK: true,
   165  		GET:    getAppsInfo,
   166  		POST:   postApps,
   167  	}
   168  
   169  	logsCmd = &Command{
   170  		Path:     "/v2/logs",
   171  		PolkitOK: "io.snapcraft.snapd.manage",
   172  		GET:      getLogs,
   173  	}
   174  
   175  	snapConfCmd = &Command{
   176  		Path: "/v2/snaps/{name}/conf",
   177  		GET:  getSnapConf,
   178  		PUT:  setSnapConf,
   179  	}
   180  
   181  	interfacesCmd = &Command{
   182  		Path:     "/v2/interfaces",
   183  		UserOK:   true,
   184  		PolkitOK: "io.snapcraft.snapd.manage-interfaces",
   185  		GET:      interfacesConnectionsMultiplexer,
   186  		POST:     changeInterfaces,
   187  	}
   188  
   189  	stateChangeCmd = &Command{
   190  		Path:     "/v2/changes/{id}",
   191  		UserOK:   true,
   192  		PolkitOK: "io.snapcraft.snapd.manage",
   193  		GET:      getChange,
   194  		POST:     abortChange,
   195  	}
   196  
   197  	stateChangesCmd = &Command{
   198  		Path:   "/v2/changes",
   199  		UserOK: true,
   200  		GET:    getChanges,
   201  	}
   202  
   203  	buyCmd = &Command{
   204  		Path: "/v2/buy",
   205  		POST: postBuy,
   206  	}
   207  
   208  	readyToBuyCmd = &Command{
   209  		Path: "/v2/buy/ready",
   210  		GET:  readyToBuy,
   211  	}
   212  
   213  	snapctlCmd = &Command{
   214  		Path:   "/v2/snapctl",
   215  		SnapOK: true,
   216  		POST:   runSnapctl,
   217  	}
   218  
   219  	sectionsCmd = &Command{
   220  		Path:   "/v2/sections",
   221  		UserOK: true,
   222  		GET:    getSections,
   223  	}
   224  
   225  	aliasesCmd = &Command{
   226  		Path:   "/v2/aliases",
   227  		UserOK: true,
   228  		GET:    getAliases,
   229  		POST:   changeAliases,
   230  	}
   231  
   232  	warningsCmd = &Command{
   233  		Path:     "/v2/warnings",
   234  		UserOK:   true,
   235  		PolkitOK: "io.snapcraft.snapd.manage",
   236  		GET:      getWarnings,
   237  		POST:     ackWarnings,
   238  	}
   239  
   240  	buildID = "unknown"
   241  )
   242  
   243  var systemdVirt = ""
   244  
   245  func init() {
   246  	// cache the build-id on startup to ensure that changes in
   247  	// the underlying binary do not affect us
   248  	if bid, err := osutil.MyBuildID(); err == nil {
   249  		buildID = bid
   250  	}
   251  	// cache systemd-detect-virt output as it's unlikely to change :-)
   252  	if buf, err := exec.Command("systemd-detect-virt").CombinedOutput(); err == nil {
   253  		systemdVirt = string(bytes.TrimSpace(buf))
   254  	}
   255  }
   256  
   257  func tbd(c *Command, r *http.Request, user *auth.UserState) Response {
   258  	return SyncResponse([]string{"TBD"}, nil)
   259  }
   260  
   261  func formatRefreshTime(t time.Time) string {
   262  	if t.IsZero() {
   263  		return ""
   264  	}
   265  	return fmt.Sprintf("%s", t.Truncate(time.Minute).Format(time.RFC3339))
   266  }
   267  
   268  func sysInfo(c *Command, r *http.Request, user *auth.UserState) Response {
   269  	st := c.d.overlord.State()
   270  	snapMgr := c.d.overlord.SnapManager()
   271  	deviceMgr := c.d.overlord.DeviceManager()
   272  	st.Lock()
   273  	defer st.Unlock()
   274  	nextRefresh := snapMgr.NextRefresh()
   275  	lastRefresh, _ := snapMgr.LastRefresh()
   276  	refreshHold, _ := snapMgr.EffectiveRefreshHold()
   277  	refreshScheduleStr, legacySchedule, err := snapMgr.RefreshSchedule()
   278  	if err != nil {
   279  		return InternalError("cannot get refresh schedule: %s", err)
   280  	}
   281  	users, err := auth.Users(st)
   282  	if err != nil && err != state.ErrNoState {
   283  		return InternalError("cannot get user auth data: %s", err)
   284  	}
   285  
   286  	refreshInfo := client.RefreshInfo{
   287  		Last: formatRefreshTime(lastRefresh),
   288  		Hold: formatRefreshTime(refreshHold),
   289  		Next: formatRefreshTime(nextRefresh),
   290  	}
   291  	if !legacySchedule {
   292  		refreshInfo.Timer = refreshScheduleStr
   293  	} else {
   294  		refreshInfo.Schedule = refreshScheduleStr
   295  	}
   296  
   297  	m := map[string]interface{}{
   298  		"series":         release.Series,
   299  		"version":        c.d.Version,
   300  		"build-id":       buildID,
   301  		"os-release":     release.ReleaseInfo,
   302  		"on-classic":     release.OnClassic,
   303  		"managed":        len(users) > 0,
   304  		"kernel-version": osutil.KernelVersion(),
   305  		"locations": map[string]interface{}{
   306  			"snap-mount-dir": dirs.SnapMountDir,
   307  			"snap-bin-dir":   dirs.SnapBinariesDir,
   308  		},
   309  		"refresh":      refreshInfo,
   310  		"architecture": arch.DpkgArchitecture(),
   311  		"system-mode":  deviceMgr.SystemMode(),
   312  	}
   313  	if systemdVirt != "" {
   314  		m["virtualization"] = systemdVirt
   315  	}
   316  
   317  	// NOTE: Right now we don't have a good way to differentiate if we
   318  	// only have partial confinement (ala AppArmor disabled and Seccomp
   319  	// enabled) or no confinement at all. Once we have a better system
   320  	// in place how we can dynamically retrieve these information from
   321  	// snapd we will use this here.
   322  	if sandbox.ForceDevMode() {
   323  		m["confinement"] = "partial"
   324  	} else {
   325  		m["confinement"] = "strict"
   326  	}
   327  
   328  	// Convey richer information about features of available security backends.
   329  	if features := sandboxFeatures(c.d.overlord.InterfaceManager().Repository().Backends()); features != nil {
   330  		m["sandbox-features"] = features
   331  	}
   332  
   333  	return SyncResponse(m, nil)
   334  }
   335  
   336  func sandboxFeatures(backends []interfaces.SecurityBackend) map[string][]string {
   337  	result := make(map[string][]string, len(backends)+1)
   338  	for _, backend := range backends {
   339  		features := backend.SandboxFeatures()
   340  		if len(features) > 0 {
   341  			sort.Strings(features)
   342  			result[string(backend.Name())] = features
   343  		}
   344  	}
   345  
   346  	// Add information about supported confinement types as a fake backend
   347  	features := make([]string, 1, 3)
   348  	features[0] = "devmode"
   349  	if !sandbox.ForceDevMode() {
   350  		features = append(features, "strict")
   351  	}
   352  	if dirs.SupportsClassicConfinement() {
   353  		features = append(features, "classic")
   354  	}
   355  	sort.Strings(features)
   356  	result["confinement-options"] = features
   357  
   358  	return result
   359  }
   360  
   361  // UserFromRequest extracts user information from request and return the respective user in state, if valid
   362  // It requires the state to be locked
   363  func UserFromRequest(st *state.State, req *http.Request) (*auth.UserState, error) {
   364  	// extract macaroons data from request
   365  	header := req.Header.Get("Authorization")
   366  	if header == "" {
   367  		return nil, auth.ErrInvalidAuth
   368  	}
   369  
   370  	authorizationData := strings.SplitN(header, " ", 2)
   371  	if len(authorizationData) != 2 || authorizationData[0] != "Macaroon" {
   372  		return nil, fmt.Errorf("authorization header misses Macaroon prefix")
   373  	}
   374  
   375  	var macaroon string
   376  	var discharges []string
   377  	for _, field := range strutil.CommaSeparatedList(authorizationData[1]) {
   378  		if strings.HasPrefix(field, `root="`) {
   379  			macaroon = strings.TrimSuffix(field[6:], `"`)
   380  		}
   381  		if strings.HasPrefix(field, `discharge="`) {
   382  			discharges = append(discharges, strings.TrimSuffix(field[11:], `"`))
   383  		}
   384  	}
   385  
   386  	if macaroon == "" {
   387  		return nil, fmt.Errorf("invalid authorization header")
   388  	}
   389  
   390  	user, err := auth.CheckMacaroon(st, macaroon, discharges)
   391  	return user, err
   392  }
   393  
   394  var muxVars = mux.Vars
   395  
   396  func getSnapInfo(c *Command, r *http.Request, user *auth.UserState) Response {
   397  	vars := muxVars(r)
   398  	name := vars["name"]
   399  
   400  	about, err := localSnapInfo(c.d.overlord.State(), name)
   401  	if err != nil {
   402  		if err == errNoSnap {
   403  			return SnapNotFound(name, err)
   404  		}
   405  
   406  		return InternalError("%v", err)
   407  	}
   408  
   409  	route := c.d.router.Get(c.Path)
   410  	if route == nil {
   411  		return InternalError("cannot find route for %q snap", name)
   412  	}
   413  
   414  	url, err := route.URL("name", name)
   415  	if err != nil {
   416  		return InternalError("cannot build URL for %q snap: %v", name, err)
   417  	}
   418  
   419  	sd := servicestate.NewStatusDecorator(progress.Null)
   420  
   421  	result := webify(mapLocal(about, sd), url.String())
   422  
   423  	return SyncResponse(result, nil)
   424  }
   425  
   426  func webify(result *client.Snap, resource string) *client.Snap {
   427  	if result.Icon == "" || strings.HasPrefix(result.Icon, "http") {
   428  		return result
   429  	}
   430  	result.Icon = ""
   431  
   432  	route := appIconCmd.d.router.Get(appIconCmd.Path)
   433  	if route != nil {
   434  		url, err := route.URL("name", result.Name)
   435  		if err == nil {
   436  			result.Icon = url.String()
   437  		}
   438  	}
   439  
   440  	return result
   441  }
   442  
   443  func getStore(c *Command) snapstate.StoreService {
   444  	st := c.d.overlord.State()
   445  	st.Lock()
   446  	defer st.Unlock()
   447  
   448  	return snapstate.Store(st, nil)
   449  }
   450  
   451  func getSections(c *Command, r *http.Request, user *auth.UserState) Response {
   452  	route := c.d.router.Get(snapCmd.Path)
   453  	if route == nil {
   454  		return InternalError("cannot find route for snaps")
   455  	}
   456  
   457  	theStore := getStore(c)
   458  
   459  	// TODO: use a per-request context
   460  	sections, err := theStore.Sections(context.TODO(), user)
   461  	switch err {
   462  	case nil:
   463  		// pass
   464  	case store.ErrBadQuery:
   465  		return SyncResponse(&resp{
   466  			Type:   ResponseTypeError,
   467  			Result: &errorResult{Message: err.Error(), Kind: client.ErrorKindBadQuery},
   468  			Status: 400,
   469  		}, nil)
   470  	case store.ErrUnauthenticated, store.ErrInvalidCredentials:
   471  		return Unauthorized("%v", err)
   472  	default:
   473  		return InternalError("%v", err)
   474  	}
   475  
   476  	return SyncResponse(sections, nil)
   477  }
   478  
   479  func searchStore(c *Command, r *http.Request, user *auth.UserState) Response {
   480  	route := c.d.router.Get(snapCmd.Path)
   481  	if route == nil {
   482  		return InternalError("cannot find route for snaps")
   483  	}
   484  	query := r.URL.Query()
   485  	q := query.Get("q")
   486  	commonID := query.Get("common-id")
   487  	// TODO: support both "category" (search v2) and "section"
   488  	section := query.Get("section")
   489  	name := query.Get("name")
   490  	scope := query.Get("scope")
   491  	private := false
   492  	prefix := false
   493  
   494  	if sel := query.Get("select"); sel != "" {
   495  		switch sel {
   496  		case "refresh":
   497  			if commonID != "" {
   498  				return BadRequest("cannot use 'common-id' with 'select=refresh'")
   499  			}
   500  			if name != "" {
   501  				return BadRequest("cannot use 'name' with 'select=refresh'")
   502  			}
   503  			if q != "" {
   504  				return BadRequest("cannot use 'q' with 'select=refresh'")
   505  			}
   506  			return storeUpdates(c, r, user)
   507  		case "private":
   508  			private = true
   509  		}
   510  	}
   511  
   512  	if name != "" {
   513  		if q != "" {
   514  			return BadRequest("cannot use 'q' and 'name' together")
   515  		}
   516  		if commonID != "" {
   517  			return BadRequest("cannot use 'common-id' and 'name' together")
   518  		}
   519  
   520  		if name[len(name)-1] != '*' {
   521  			return findOne(c, r, user, name)
   522  		}
   523  
   524  		prefix = true
   525  		q = name[:len(name)-1]
   526  	}
   527  
   528  	if commonID != "" && q != "" {
   529  		return BadRequest("cannot use 'common-id' and 'q' together")
   530  	}
   531  
   532  	theStore := getStore(c)
   533  	ctx := store.WithClientUserAgent(r.Context(), r)
   534  	found, err := theStore.Find(ctx, &store.Search{
   535  		Query:    q,
   536  		Prefix:   prefix,
   537  		CommonID: commonID,
   538  		Category: section,
   539  		Private:  private,
   540  		Scope:    scope,
   541  	}, user)
   542  	switch err {
   543  	case nil:
   544  		// pass
   545  	case store.ErrBadQuery:
   546  		return SyncResponse(&resp{
   547  			Type:   ResponseTypeError,
   548  			Result: &errorResult{Message: err.Error(), Kind: client.ErrorKindBadQuery},
   549  			Status: 400,
   550  		}, nil)
   551  	case store.ErrUnauthenticated, store.ErrInvalidCredentials:
   552  		return Unauthorized(err.Error())
   553  	default:
   554  		if e, ok := err.(*url.Error); ok {
   555  			if neterr, ok := e.Err.(*net.OpError); ok {
   556  				if dnserr, ok := neterr.Err.(*net.DNSError); ok {
   557  					return SyncResponse(&resp{
   558  						Type:   ResponseTypeError,
   559  						Result: &errorResult{Message: dnserr.Error(), Kind: client.ErrorKindDNSFailure},
   560  						Status: 400,
   561  					}, nil)
   562  				}
   563  			}
   564  		}
   565  		if e, ok := err.(net.Error); ok && e.Timeout() {
   566  			return SyncResponse(&resp{
   567  				Type:   ResponseTypeError,
   568  				Result: &errorResult{Message: err.Error(), Kind: client.ErrorKindNetworkTimeout},
   569  				Status: 400,
   570  			}, nil)
   571  		}
   572  		if e, ok := err.(*httputil.PerstistentNetworkError); ok {
   573  			return SyncResponse(&resp{
   574  				Type:   ResponseTypeError,
   575  				Result: &errorResult{Message: e.Error(), Kind: client.ErrorKindDNSFailure},
   576  				Status: 400,
   577  			}, nil)
   578  		}
   579  
   580  		return InternalError("%v", err)
   581  	}
   582  
   583  	meta := &Meta{
   584  		SuggestedCurrency: theStore.SuggestedCurrency(),
   585  		Sources:           []string{"store"},
   586  	}
   587  
   588  	return sendStorePackages(route, meta, found)
   589  }
   590  
   591  func findOne(c *Command, r *http.Request, user *auth.UserState, name string) Response {
   592  	if err := snap.ValidateName(name); err != nil {
   593  		return BadRequest(err.Error())
   594  	}
   595  
   596  	theStore := getStore(c)
   597  	spec := store.SnapSpec{
   598  		Name: name,
   599  	}
   600  	ctx := store.WithClientUserAgent(r.Context(), r)
   601  	snapInfo, err := theStore.SnapInfo(ctx, spec, user)
   602  	switch err {
   603  	case nil:
   604  		// pass
   605  	case store.ErrInvalidCredentials:
   606  		return Unauthorized("%v", err)
   607  	case store.ErrSnapNotFound:
   608  		return SnapNotFound(name, err)
   609  	default:
   610  		return InternalError("%v", err)
   611  	}
   612  
   613  	meta := &Meta{
   614  		SuggestedCurrency: theStore.SuggestedCurrency(),
   615  		Sources:           []string{"store"},
   616  	}
   617  
   618  	results := make([]*json.RawMessage, 1)
   619  	data, err := json.Marshal(webify(mapRemote(snapInfo), r.URL.String()))
   620  	if err != nil {
   621  		return InternalError(err.Error())
   622  	}
   623  	results[0] = (*json.RawMessage)(&data)
   624  	return SyncResponse(results, meta)
   625  }
   626  
   627  func shouldSearchStore(r *http.Request) bool {
   628  	// we should jump to the old behaviour iff q is given, or if
   629  	// sources is given and either empty or contains the word
   630  	// 'store'.  Otherwise, local results only.
   631  
   632  	query := r.URL.Query()
   633  
   634  	if _, ok := query["q"]; ok {
   635  		logger.Debugf("use of obsolete \"q\" parameter: %q", r.URL)
   636  		return true
   637  	}
   638  
   639  	if src, ok := query["sources"]; ok {
   640  		logger.Debugf("use of obsolete \"sources\" parameter: %q", r.URL)
   641  		if len(src) == 0 || strings.Contains(src[0], "store") {
   642  			return true
   643  		}
   644  	}
   645  
   646  	return false
   647  }
   648  
   649  func storeUpdates(c *Command, r *http.Request, user *auth.UserState) Response {
   650  	route := c.d.router.Get(snapCmd.Path)
   651  	if route == nil {
   652  		return InternalError("cannot find route for snaps")
   653  	}
   654  
   655  	state := c.d.overlord.State()
   656  	state.Lock()
   657  	updates, err := snapstateRefreshCandidates(state, user)
   658  	state.Unlock()
   659  	if err != nil {
   660  		return InternalError("cannot list updates: %v", err)
   661  	}
   662  
   663  	return sendStorePackages(route, nil, updates)
   664  }
   665  
   666  func sendStorePackages(route *mux.Route, meta *Meta, found []*snap.Info) Response {
   667  	results := make([]*json.RawMessage, 0, len(found))
   668  	for _, x := range found {
   669  		url, err := route.URL("name", x.InstanceName())
   670  		if err != nil {
   671  			logger.Noticef("Cannot build URL for snap %q revision %s: %v", x.InstanceName(), x.Revision, err)
   672  			continue
   673  		}
   674  
   675  		data, err := json.Marshal(webify(mapRemote(x), url.String()))
   676  		if err != nil {
   677  			return InternalError("%v", err)
   678  		}
   679  		raw := json.RawMessage(data)
   680  		results = append(results, &raw)
   681  	}
   682  
   683  	return SyncResponse(results, meta)
   684  }
   685  
   686  // plural!
   687  func getSnapsInfo(c *Command, r *http.Request, user *auth.UserState) Response {
   688  
   689  	if shouldSearchStore(r) {
   690  		logger.Noticef("Jumping to \"find\" to better support legacy request %q", r.URL)
   691  		return searchStore(c, r, user)
   692  	}
   693  
   694  	route := c.d.router.Get(snapCmd.Path)
   695  	if route == nil {
   696  		return InternalError("cannot find route for snaps")
   697  	}
   698  
   699  	query := r.URL.Query()
   700  	var all bool
   701  	sel := query.Get("select")
   702  	switch sel {
   703  	case "all":
   704  		all = true
   705  	case "enabled", "":
   706  		all = false
   707  	default:
   708  		return BadRequest("invalid select parameter: %q", sel)
   709  	}
   710  	var wanted map[string]bool
   711  	if ns := query.Get("snaps"); len(ns) > 0 {
   712  		nsl := strutil.CommaSeparatedList(ns)
   713  		wanted = make(map[string]bool, len(nsl))
   714  		for _, name := range nsl {
   715  			wanted[name] = true
   716  		}
   717  	}
   718  
   719  	found, err := allLocalSnapInfos(c.d.overlord.State(), all, wanted)
   720  	if err != nil {
   721  		return InternalError("cannot list local snaps! %v", err)
   722  	}
   723  
   724  	results := make([]*json.RawMessage, len(found))
   725  
   726  	sd := servicestate.NewStatusDecorator(progress.Null)
   727  	for i, x := range found {
   728  		name := x.info.InstanceName()
   729  		rev := x.info.Revision
   730  
   731  		url, err := route.URL("name", name)
   732  		if err != nil {
   733  			logger.Noticef("Cannot build URL for snap %q revision %s: %v", name, rev, err)
   734  			continue
   735  		}
   736  
   737  		data, err := json.Marshal(webify(mapLocal(x, sd), url.String()))
   738  		if err != nil {
   739  			return InternalError("cannot serialize snap %q revision %s: %v", name, rev, err)
   740  		}
   741  		raw := json.RawMessage(data)
   742  		results[i] = &raw
   743  	}
   744  
   745  	return SyncResponse(results, &Meta{Sources: []string{"local"}})
   746  }
   747  
   748  // licenseData holds details about the snap license, and may be
   749  // marshaled back as an error when the license agreement is pending,
   750  // and is expected as input to accept (or not) that license
   751  // agreement. As such, its field names are part of the API.
   752  type licenseData struct {
   753  	Intro   string `json:"intro"`
   754  	License string `json:"license"`
   755  	Agreed  bool   `json:"agreed"`
   756  }
   757  
   758  func (*licenseData) Error() string {
   759  	return "license agreement required"
   760  }
   761  
   762  type snapRevisionOptions struct {
   763  	Channel  string        `json:"channel"`
   764  	Revision snap.Revision `json:"revision"`
   765  
   766  	CohortKey   string `json:"cohort-key"`
   767  	LeaveCohort bool   `json:"leave-cohort"`
   768  }
   769  
   770  func (ropt *snapRevisionOptions) validate() error {
   771  	if ropt.CohortKey != "" {
   772  		if ropt.LeaveCohort {
   773  			return fmt.Errorf("cannot specify both cohort-key and leave-cohort")
   774  		}
   775  		if !ropt.Revision.Unset() {
   776  			return fmt.Errorf("cannot specify both cohort-key and revision")
   777  		}
   778  	}
   779  
   780  	if ropt.Channel != "" {
   781  		_, err := channel.Parse(ropt.Channel, "-")
   782  		if err != nil {
   783  			return err
   784  		}
   785  	}
   786  	return nil
   787  }
   788  
   789  type snapInstruction struct {
   790  	progress.NullMeter
   791  
   792  	Action string `json:"action"`
   793  	Amend  bool   `json:"amend"`
   794  	snapRevisionOptions
   795  	DevMode          bool `json:"devmode"`
   796  	JailMode         bool `json:"jailmode"`
   797  	Classic          bool `json:"classic"`
   798  	IgnoreValidation bool `json:"ignore-validation"`
   799  	Unaliased        bool `json:"unaliased"`
   800  	Purge            bool `json:"purge,omitempty"`
   801  	// dropping support temporarely until flag confusion is sorted,
   802  	// this isn't supported by client atm anyway
   803  	LeaveOld bool         `json:"temp-dropped-leave-old"`
   804  	License  *licenseData `json:"license"`
   805  	Snaps    []string     `json:"snaps"`
   806  	Users    []string     `json:"users"`
   807  
   808  	// The fields below should not be unmarshalled into. Do not export them.
   809  	userID int
   810  	ctx    context.Context
   811  }
   812  
   813  func (inst *snapInstruction) revnoOpts() *snapstate.RevisionOptions {
   814  	return &snapstate.RevisionOptions{
   815  		Channel:     inst.Channel,
   816  		Revision:    inst.Revision,
   817  		CohortKey:   inst.CohortKey,
   818  		LeaveCohort: inst.LeaveCohort,
   819  	}
   820  }
   821  
   822  func (inst *snapInstruction) modeFlags() (snapstate.Flags, error) {
   823  	return modeFlags(inst.DevMode, inst.JailMode, inst.Classic)
   824  }
   825  
   826  func (inst *snapInstruction) installFlags() (snapstate.Flags, error) {
   827  	flags, err := inst.modeFlags()
   828  	if err != nil {
   829  		return snapstate.Flags{}, err
   830  	}
   831  	if inst.Unaliased {
   832  		flags.Unaliased = true
   833  	}
   834  	return flags, nil
   835  }
   836  
   837  func (inst *snapInstruction) validate() error {
   838  	if inst.CohortKey != "" {
   839  		if inst.Action != "install" && inst.Action != "refresh" && inst.Action != "switch" {
   840  			return fmt.Errorf("cohort-key can only be specified for install, refresh, or switch")
   841  		}
   842  	}
   843  	if inst.LeaveCohort {
   844  		if inst.Action != "refresh" && inst.Action != "switch" {
   845  			return fmt.Errorf("leave-cohort can only be specified for refresh or switch")
   846  		}
   847  	}
   848  	if inst.Action == "install" {
   849  		for _, snapName := range inst.Snaps {
   850  			// FIXME: alternatively we could simply mutate *inst
   851  			//        and s/ubuntu-core/core/ ?
   852  			if snapName == "ubuntu-core" {
   853  				return fmt.Errorf(`cannot install "ubuntu-core", please use "core" instead`)
   854  			}
   855  		}
   856  	}
   857  
   858  	return inst.snapRevisionOptions.validate()
   859  }
   860  
   861  type snapInstructionResult struct {
   862  	Summary  string
   863  	Affected []string
   864  	Tasksets []*state.TaskSet
   865  	Result   map[string]interface{}
   866  }
   867  
   868  var (
   869  	snapstateInstall           = snapstate.Install
   870  	snapstateInstallPath       = snapstate.InstallPath
   871  	snapstateRefreshCandidates = snapstate.RefreshCandidates
   872  	snapstateTryPath           = snapstate.TryPath
   873  	snapstateUpdate            = snapstate.Update
   874  	snapstateUpdateMany        = snapstate.UpdateMany
   875  	snapstateInstallMany       = snapstate.InstallMany
   876  	snapstateRemoveMany        = snapstate.RemoveMany
   877  	snapstateRevert            = snapstate.Revert
   878  	snapstateRevertToRevision  = snapstate.RevertToRevision
   879  	snapstateSwitch            = snapstate.Switch
   880  
   881  	snapshotList    = snapshotstate.List
   882  	snapshotCheck   = snapshotstate.Check
   883  	snapshotForget  = snapshotstate.Forget
   884  	snapshotRestore = snapshotstate.Restore
   885  	snapshotSave    = snapshotstate.Save
   886  	snapshotExport  = snapshotstate.Export
   887  
   888  	assertstateRefreshSnapDeclarations = assertstate.RefreshSnapDeclarations
   889  )
   890  
   891  func ensureStateSoonImpl(st *state.State) {
   892  	st.EnsureBefore(0)
   893  }
   894  
   895  var ensureStateSoon = ensureStateSoonImpl
   896  
   897  var errDevJailModeConflict = errors.New("cannot use devmode and jailmode flags together")
   898  var errClassicDevmodeConflict = errors.New("cannot use classic and devmode flags together")
   899  var errNoJailMode = errors.New("this system cannot honour the jailmode flag")
   900  
   901  func modeFlags(devMode, jailMode, classic bool) (snapstate.Flags, error) {
   902  	flags := snapstate.Flags{}
   903  	devModeOS := sandbox.ForceDevMode()
   904  	switch {
   905  	case jailMode && devModeOS:
   906  		return flags, errNoJailMode
   907  	case jailMode && devMode:
   908  		return flags, errDevJailModeConflict
   909  	case devMode && classic:
   910  		return flags, errClassicDevmodeConflict
   911  	}
   912  	// NOTE: jailmode and classic are allowed together. In that setting,
   913  	// jailmode overrides classic and the app gets regular (non-classic)
   914  	// confinement.
   915  	flags.JailMode = jailMode
   916  	flags.Classic = classic
   917  	flags.DevMode = devMode
   918  	return flags, nil
   919  }
   920  
   921  func snapUpdateMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) {
   922  	// 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
   923  	if err := assertstateRefreshSnapDeclarations(st, inst.userID); err != nil {
   924  		return nil, err
   925  	}
   926  
   927  	// TODO: use a per-request context
   928  	updated, tasksets, err := snapstateUpdateMany(context.TODO(), st, inst.Snaps, inst.userID, nil)
   929  	if err != nil {
   930  		return nil, err
   931  	}
   932  
   933  	var msg string
   934  	switch len(updated) {
   935  	case 0:
   936  		if len(inst.Snaps) != 0 {
   937  			// TRANSLATORS: the %s is a comma-separated list of quoted snap names
   938  			msg = fmt.Sprintf(i18n.G("Refresh snaps %s: no updates"), strutil.Quoted(inst.Snaps))
   939  		} else {
   940  			msg = i18n.G("Refresh all snaps: no updates")
   941  		}
   942  	case 1:
   943  		msg = fmt.Sprintf(i18n.G("Refresh snap %q"), updated[0])
   944  	default:
   945  		quoted := strutil.Quoted(updated)
   946  		// TRANSLATORS: the %s is a comma-separated list of quoted snap names
   947  		msg = fmt.Sprintf(i18n.G("Refresh snaps %s"), quoted)
   948  	}
   949  
   950  	return &snapInstructionResult{
   951  		Summary:  msg,
   952  		Affected: updated,
   953  		Tasksets: tasksets,
   954  	}, nil
   955  }
   956  
   957  func snapInstallMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) {
   958  	for _, name := range inst.Snaps {
   959  		if len(name) == 0 {
   960  			return nil, fmt.Errorf(i18n.G("cannot install snap with empty name"))
   961  		}
   962  	}
   963  	installed, tasksets, err := snapstateInstallMany(st, inst.Snaps, inst.userID)
   964  	if err != nil {
   965  		return nil, err
   966  	}
   967  
   968  	var msg string
   969  	switch len(inst.Snaps) {
   970  	case 0:
   971  		return nil, fmt.Errorf("cannot install zero snaps")
   972  	case 1:
   973  		msg = fmt.Sprintf(i18n.G("Install snap %q"), inst.Snaps[0])
   974  	default:
   975  		quoted := strutil.Quoted(inst.Snaps)
   976  		// TRANSLATORS: the %s is a comma-separated list of quoted snap names
   977  		msg = fmt.Sprintf(i18n.G("Install snaps %s"), quoted)
   978  	}
   979  
   980  	return &snapInstructionResult{
   981  		Summary:  msg,
   982  		Affected: installed,
   983  		Tasksets: tasksets,
   984  	}, nil
   985  }
   986  
   987  func snapInstall(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   988  	if len(inst.Snaps[0]) == 0 {
   989  		return "", nil, fmt.Errorf(i18n.G("cannot install snap with empty name"))
   990  	}
   991  
   992  	flags, err := inst.installFlags()
   993  	if err != nil {
   994  		return "", nil, err
   995  	}
   996  
   997  	var ckey string
   998  	if inst.CohortKey == "" {
   999  		logger.Noticef("Installing snap %q revision %s", inst.Snaps[0], inst.Revision)
  1000  	} else {
  1001  		ckey = strutil.ElliptLeft(inst.CohortKey, 10)
  1002  		logger.Noticef("Installing snap %q from cohort %q", inst.Snaps[0], ckey)
  1003  	}
  1004  	tset, err := snapstateInstall(inst.ctx, st, inst.Snaps[0], inst.revnoOpts(), inst.userID, flags)
  1005  	if err != nil {
  1006  		return "", nil, err
  1007  	}
  1008  
  1009  	msg := fmt.Sprintf(i18n.G("Install %q snap"), inst.Snaps[0])
  1010  	if inst.Channel != "stable" && inst.Channel != "" {
  1011  		msg += fmt.Sprintf(" from %q channel", inst.Channel)
  1012  	}
  1013  	if inst.CohortKey != "" {
  1014  		msg += fmt.Sprintf(" from %q cohort", ckey)
  1015  	}
  1016  	return msg, []*state.TaskSet{tset}, nil
  1017  }
  1018  
  1019  func snapUpdate(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
  1020  	// TODO: bail if revision is given (and != current?), *or* behave as with install --revision?
  1021  	flags, err := inst.modeFlags()
  1022  	if err != nil {
  1023  		return "", nil, err
  1024  	}
  1025  	if inst.IgnoreValidation {
  1026  		flags.IgnoreValidation = true
  1027  	}
  1028  	if inst.Amend {
  1029  		flags.Amend = true
  1030  	}
  1031  
  1032  	// we need refreshed snap-declarations to enforce refresh-control as best as we can
  1033  	if err = assertstateRefreshSnapDeclarations(st, inst.userID); err != nil {
  1034  		return "", nil, err
  1035  	}
  1036  
  1037  	ts, err := snapstateUpdate(st, inst.Snaps[0], inst.revnoOpts(), inst.userID, flags)
  1038  	if err != nil {
  1039  		return "", nil, err
  1040  	}
  1041  
  1042  	msg := fmt.Sprintf(i18n.G("Refresh %q snap"), inst.Snaps[0])
  1043  	if inst.Channel != "stable" && inst.Channel != "" {
  1044  		msg = fmt.Sprintf(i18n.G("Refresh %q snap from %q channel"), inst.Snaps[0], inst.Channel)
  1045  	}
  1046  
  1047  	return msg, []*state.TaskSet{ts}, nil
  1048  }
  1049  
  1050  func snapRemoveMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) {
  1051  	removed, tasksets, err := snapstateRemoveMany(st, inst.Snaps)
  1052  	if err != nil {
  1053  		return nil, err
  1054  	}
  1055  
  1056  	var msg string
  1057  	switch len(inst.Snaps) {
  1058  	case 0:
  1059  		return nil, fmt.Errorf("cannot remove zero snaps")
  1060  	case 1:
  1061  		msg = fmt.Sprintf(i18n.G("Remove snap %q"), inst.Snaps[0])
  1062  	default:
  1063  		quoted := strutil.Quoted(inst.Snaps)
  1064  		// TRANSLATORS: the %s is a comma-separated list of quoted snap names
  1065  		msg = fmt.Sprintf(i18n.G("Remove snaps %s"), quoted)
  1066  	}
  1067  
  1068  	return &snapInstructionResult{
  1069  		Summary:  msg,
  1070  		Affected: removed,
  1071  		Tasksets: tasksets,
  1072  	}, nil
  1073  }
  1074  
  1075  func snapRemove(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
  1076  	ts, err := snapstate.Remove(st, inst.Snaps[0], inst.Revision, &snapstate.RemoveFlags{Purge: inst.Purge})
  1077  	if err != nil {
  1078  		return "", nil, err
  1079  	}
  1080  
  1081  	msg := fmt.Sprintf(i18n.G("Remove %q snap"), inst.Snaps[0])
  1082  	return msg, []*state.TaskSet{ts}, nil
  1083  }
  1084  
  1085  func snapRevert(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
  1086  	var ts *state.TaskSet
  1087  
  1088  	flags, err := inst.modeFlags()
  1089  	if err != nil {
  1090  		return "", nil, err
  1091  	}
  1092  
  1093  	if inst.Revision.Unset() {
  1094  		ts, err = snapstateRevert(st, inst.Snaps[0], flags)
  1095  	} else {
  1096  		ts, err = snapstateRevertToRevision(st, inst.Snaps[0], inst.Revision, flags)
  1097  	}
  1098  	if err != nil {
  1099  		return "", nil, err
  1100  	}
  1101  
  1102  	msg := fmt.Sprintf(i18n.G("Revert %q snap"), inst.Snaps[0])
  1103  	return msg, []*state.TaskSet{ts}, nil
  1104  }
  1105  
  1106  func snapEnable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
  1107  	if !inst.Revision.Unset() {
  1108  		return "", nil, errors.New("enable takes no revision")
  1109  	}
  1110  	ts, err := snapstate.Enable(st, inst.Snaps[0])
  1111  	if err != nil {
  1112  		return "", nil, err
  1113  	}
  1114  
  1115  	msg := fmt.Sprintf(i18n.G("Enable %q snap"), inst.Snaps[0])
  1116  	return msg, []*state.TaskSet{ts}, nil
  1117  }
  1118  
  1119  func snapDisable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
  1120  	if !inst.Revision.Unset() {
  1121  		return "", nil, errors.New("disable takes no revision")
  1122  	}
  1123  	ts, err := snapstate.Disable(st, inst.Snaps[0])
  1124  	if err != nil {
  1125  		return "", nil, err
  1126  	}
  1127  
  1128  	msg := fmt.Sprintf(i18n.G("Disable %q snap"), inst.Snaps[0])
  1129  	return msg, []*state.TaskSet{ts}, nil
  1130  }
  1131  
  1132  func snapSwitch(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
  1133  	if !inst.Revision.Unset() {
  1134  		return "", nil, errors.New("switch takes no revision")
  1135  	}
  1136  	ts, err := snapstateSwitch(st, inst.Snaps[0], inst.revnoOpts())
  1137  	if err != nil {
  1138  		return "", nil, err
  1139  	}
  1140  
  1141  	var msg string
  1142  	switch {
  1143  	case inst.LeaveCohort && inst.Channel != "":
  1144  		msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q and away from cohort"), inst.Snaps[0], inst.Channel)
  1145  	case inst.LeaveCohort:
  1146  		msg = fmt.Sprintf(i18n.G("Switch %q snap away from cohort"), inst.Snaps[0])
  1147  	case inst.CohortKey == "" && inst.Channel != "":
  1148  		msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q"), inst.Snaps[0], inst.Channel)
  1149  	case inst.CohortKey != "" && inst.Channel == "":
  1150  		msg = fmt.Sprintf(i18n.G("Switch %q snap to cohort %q"), inst.Snaps[0], strutil.ElliptLeft(inst.CohortKey, 10))
  1151  	default:
  1152  		msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q and cohort %q"), inst.Snaps[0], inst.Channel, strutil.ElliptLeft(inst.CohortKey, 10))
  1153  	}
  1154  	return msg, []*state.TaskSet{ts}, nil
  1155  }
  1156  
  1157  func snapshotMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) {
  1158  	setID, snapshotted, ts, err := snapshotSave(st, inst.Snaps, inst.Users)
  1159  	if err != nil {
  1160  		return nil, err
  1161  	}
  1162  
  1163  	var msg string
  1164  	if len(inst.Snaps) == 0 {
  1165  		msg = i18n.G("Snapshot all snaps")
  1166  	} else {
  1167  		// TRANSLATORS: the %s is a comma-separated list of quoted snap names
  1168  		msg = fmt.Sprintf(i18n.G("Snapshot snaps %s"), strutil.Quoted(inst.Snaps))
  1169  	}
  1170  
  1171  	return &snapInstructionResult{
  1172  		Summary:  msg,
  1173  		Affected: snapshotted,
  1174  		Tasksets: []*state.TaskSet{ts},
  1175  		Result:   map[string]interface{}{"set-id": setID},
  1176  	}, nil
  1177  }
  1178  
  1179  type snapActionFunc func(*snapInstruction, *state.State) (string, []*state.TaskSet, error)
  1180  
  1181  var snapInstructionDispTable = map[string]snapActionFunc{
  1182  	"install": snapInstall,
  1183  	"refresh": snapUpdate,
  1184  	"remove":  snapRemove,
  1185  	"revert":  snapRevert,
  1186  	"enable":  snapEnable,
  1187  	"disable": snapDisable,
  1188  	"switch":  snapSwitch,
  1189  }
  1190  
  1191  func (inst *snapInstruction) dispatch() snapActionFunc {
  1192  	if len(inst.Snaps) != 1 {
  1193  		logger.Panicf("dispatch only handles single-snap ops; got %d", len(inst.Snaps))
  1194  	}
  1195  	return snapInstructionDispTable[inst.Action]
  1196  }
  1197  
  1198  func (inst *snapInstruction) errToResponse(err error) Response {
  1199  	if len(inst.Snaps) == 0 {
  1200  		return errToResponse(err, nil, BadRequest, "cannot %s: %v", inst.Action)
  1201  	}
  1202  
  1203  	return errToResponse(err, inst.Snaps, BadRequest, "cannot %s %s: %v", inst.Action, strutil.Quoted(inst.Snaps))
  1204  }
  1205  
  1206  func postSnap(c *Command, r *http.Request, user *auth.UserState) Response {
  1207  	route := c.d.router.Get(stateChangeCmd.Path)
  1208  	if route == nil {
  1209  		return InternalError("cannot find route for change")
  1210  	}
  1211  
  1212  	decoder := json.NewDecoder(r.Body)
  1213  	var inst snapInstruction
  1214  	if err := decoder.Decode(&inst); err != nil {
  1215  		return BadRequest("cannot decode request body into snap instruction: %v", err)
  1216  	}
  1217  	inst.ctx = r.Context()
  1218  
  1219  	state := c.d.overlord.State()
  1220  	state.Lock()
  1221  	defer state.Unlock()
  1222  
  1223  	if user != nil {
  1224  		inst.userID = user.ID
  1225  	}
  1226  
  1227  	vars := muxVars(r)
  1228  	inst.Snaps = []string{vars["name"]}
  1229  
  1230  	if err := inst.validate(); err != nil {
  1231  		return BadRequest("%s", err)
  1232  	}
  1233  
  1234  	impl := inst.dispatch()
  1235  	if impl == nil {
  1236  		return BadRequest("unknown action %s", inst.Action)
  1237  	}
  1238  
  1239  	msg, tsets, err := impl(&inst, state)
  1240  	if err != nil {
  1241  		return inst.errToResponse(err)
  1242  	}
  1243  
  1244  	chg := newChange(state, inst.Action+"-snap", msg, tsets, inst.Snaps)
  1245  
  1246  	ensureStateSoon(state)
  1247  
  1248  	return AsyncResponse(nil, &Meta{Change: chg.ID()})
  1249  }
  1250  
  1251  func newChange(st *state.State, kind, summary string, tsets []*state.TaskSet, snapNames []string) *state.Change {
  1252  	chg := st.NewChange(kind, summary)
  1253  	for _, ts := range tsets {
  1254  		chg.AddAll(ts)
  1255  	}
  1256  	if snapNames != nil {
  1257  		chg.Set("snap-names", snapNames)
  1258  	}
  1259  	return chg
  1260  }
  1261  
  1262  const maxReadBuflen = 1024 * 1024
  1263  
  1264  func trySnap(c *Command, r *http.Request, user *auth.UserState, trydir string, flags snapstate.Flags) Response {
  1265  	st := c.d.overlord.State()
  1266  	st.Lock()
  1267  	defer st.Unlock()
  1268  
  1269  	if !filepath.IsAbs(trydir) {
  1270  		return BadRequest("cannot try %q: need an absolute path", trydir)
  1271  	}
  1272  	if !osutil.IsDirectory(trydir) {
  1273  		return BadRequest("cannot try %q: not a snap directory", trydir)
  1274  	}
  1275  
  1276  	// the developer asked us to do this with a trusted snap dir
  1277  	info, err := unsafeReadSnapInfo(trydir)
  1278  	if _, ok := err.(snap.NotSnapError); ok {
  1279  		return SyncResponse(&resp{
  1280  			Type: ResponseTypeError,
  1281  			Result: &errorResult{
  1282  				Message: err.Error(),
  1283  				Kind:    client.ErrorKindNotSnap,
  1284  			},
  1285  			Status: 400,
  1286  		}, nil)
  1287  	}
  1288  	if err != nil {
  1289  		return BadRequest("cannot read snap info for %s: %s", trydir, err)
  1290  	}
  1291  
  1292  	tset, err := snapstateTryPath(st, info.InstanceName(), trydir, flags)
  1293  	if err != nil {
  1294  		return errToResponse(err, []string{info.InstanceName()}, BadRequest, "cannot try %s: %s", trydir)
  1295  	}
  1296  
  1297  	msg := fmt.Sprintf(i18n.G("Try %q snap from %s"), info.InstanceName(), trydir)
  1298  	chg := newChange(st, "try-snap", msg, []*state.TaskSet{tset}, []string{info.InstanceName()})
  1299  	chg.Set("api-data", map[string]string{"snap-name": info.InstanceName()})
  1300  
  1301  	ensureStateSoon(st)
  1302  
  1303  	return AsyncResponse(nil, &Meta{Change: chg.ID()})
  1304  }
  1305  
  1306  func isTrue(form *multipart.Form, key string) bool {
  1307  	value := form.Value[key]
  1308  	if len(value) == 0 {
  1309  		return false
  1310  	}
  1311  	b, err := strconv.ParseBool(value[0])
  1312  	if err != nil {
  1313  		return false
  1314  	}
  1315  
  1316  	return b
  1317  }
  1318  
  1319  func snapsOp(c *Command, r *http.Request, user *auth.UserState) Response {
  1320  	route := c.d.router.Get(stateChangeCmd.Path)
  1321  	if route == nil {
  1322  		return InternalError("cannot find route for change")
  1323  	}
  1324  
  1325  	decoder := json.NewDecoder(r.Body)
  1326  	var inst snapInstruction
  1327  	if err := decoder.Decode(&inst); err != nil {
  1328  		return BadRequest("cannot decode request body into snap instruction: %v", err)
  1329  	}
  1330  
  1331  	// TODO: inst.Amend, etc?
  1332  	if inst.Channel != "" || !inst.Revision.Unset() || inst.DevMode || inst.JailMode || inst.CohortKey != "" || inst.LeaveCohort || inst.Purge {
  1333  		return BadRequest("unsupported option provided for multi-snap operation")
  1334  	}
  1335  	if err := inst.validate(); err != nil {
  1336  		return BadRequest("%v", err)
  1337  	}
  1338  
  1339  	st := c.d.overlord.State()
  1340  	st.Lock()
  1341  	defer st.Unlock()
  1342  
  1343  	if user != nil {
  1344  		inst.userID = user.ID
  1345  	}
  1346  
  1347  	var op func(*snapInstruction, *state.State) (*snapInstructionResult, error)
  1348  
  1349  	switch inst.Action {
  1350  	case "refresh":
  1351  		op = snapUpdateMany
  1352  	case "install":
  1353  		op = snapInstallMany
  1354  	case "remove":
  1355  		op = snapRemoveMany
  1356  	case "snapshot":
  1357  		op = snapshotMany
  1358  	default:
  1359  		return BadRequest("unsupported multi-snap operation %q", inst.Action)
  1360  	}
  1361  	res, err := op(&inst, st)
  1362  	if err != nil {
  1363  		return inst.errToResponse(err)
  1364  	}
  1365  
  1366  	var chg *state.Change
  1367  	if len(res.Tasksets) == 0 {
  1368  		chg = st.NewChange(inst.Action+"-snap", res.Summary)
  1369  		chg.SetStatus(state.DoneStatus)
  1370  	} else {
  1371  		chg = newChange(st, inst.Action+"-snap", res.Summary, res.Tasksets, res.Affected)
  1372  		ensureStateSoon(st)
  1373  	}
  1374  
  1375  	chg.Set("api-data", map[string]interface{}{"snap-names": res.Affected})
  1376  
  1377  	return AsyncResponse(res.Result, &Meta{Change: chg.ID()})
  1378  }
  1379  
  1380  func postSnaps(c *Command, r *http.Request, user *auth.UserState) Response {
  1381  	contentType := r.Header.Get("Content-Type")
  1382  
  1383  	mediaType, params, err := mime.ParseMediaType(contentType)
  1384  	if err != nil {
  1385  		return BadRequest("cannot parse content type: %v", err)
  1386  	}
  1387  
  1388  	if mediaType == "application/json" {
  1389  		charset := strings.ToUpper(params["charset"])
  1390  		if charset != "" && charset != "UTF-8" {
  1391  			return BadRequest("unknown charset in content type: %s", contentType)
  1392  		}
  1393  		return snapsOp(c, r, user)
  1394  	}
  1395  
  1396  	if !strings.HasPrefix(contentType, "multipart/") {
  1397  		return BadRequest("unknown content type: %s", contentType)
  1398  	}
  1399  
  1400  	route := c.d.router.Get(stateChangeCmd.Path)
  1401  	if route == nil {
  1402  		return InternalError("cannot find route for change")
  1403  	}
  1404  
  1405  	// POSTs to sideload snaps must be a multipart/form-data file upload.
  1406  	form, err := multipart.NewReader(r.Body, params["boundary"]).ReadForm(maxReadBuflen)
  1407  	if err != nil {
  1408  		return BadRequest("cannot read POST form: %v", err)
  1409  	}
  1410  
  1411  	dangerousOK := isTrue(form, "dangerous")
  1412  	flags, err := modeFlags(isTrue(form, "devmode"), isTrue(form, "jailmode"), isTrue(form, "classic"))
  1413  	if err != nil {
  1414  		return BadRequest(err.Error())
  1415  	}
  1416  
  1417  	if len(form.Value["action"]) > 0 && form.Value["action"][0] == "try" {
  1418  		if len(form.Value["snap-path"]) == 0 {
  1419  			return BadRequest("need 'snap-path' value in form")
  1420  		}
  1421  		return trySnap(c, r, user, form.Value["snap-path"][0], flags)
  1422  	}
  1423  	flags.RemoveSnapPath = true
  1424  
  1425  	flags.Unaliased = isTrue(form, "unaliased")
  1426  
  1427  	// find the file for the "snap" form field
  1428  	var snapBody multipart.File
  1429  	var origPath string
  1430  out:
  1431  	for name, fheaders := range form.File {
  1432  		if name != "snap" {
  1433  			continue
  1434  		}
  1435  		for _, fheader := range fheaders {
  1436  			snapBody, err = fheader.Open()
  1437  			origPath = fheader.Filename
  1438  			if err != nil {
  1439  				return BadRequest(`cannot open uploaded "snap" file: %v`, err)
  1440  			}
  1441  			defer snapBody.Close()
  1442  
  1443  			break out
  1444  		}
  1445  	}
  1446  	defer form.RemoveAll()
  1447  
  1448  	if snapBody == nil {
  1449  		return BadRequest(`cannot find "snap" file field in provided multipart/form-data payload`)
  1450  	}
  1451  
  1452  	// we are in charge of the tempfile life cycle until we hand it off to the change
  1453  	changeTriggered := false
  1454  	// if you change this prefix, look for it in the tests
  1455  	// also see localInstallCleanup in snapstate/snapmgr.go
  1456  	tmpf, err := ioutil.TempFile(dirs.SnapBlobDir, dirs.LocalInstallBlobTempPrefix)
  1457  	if err != nil {
  1458  		return InternalError("cannot create temporary file: %v", err)
  1459  	}
  1460  
  1461  	tempPath := tmpf.Name()
  1462  
  1463  	defer func() {
  1464  		if !changeTriggered {
  1465  			os.Remove(tempPath)
  1466  		}
  1467  	}()
  1468  
  1469  	if _, err := io.Copy(tmpf, snapBody); err != nil {
  1470  		return InternalError("cannot copy request into temporary file: %v", err)
  1471  	}
  1472  	tmpf.Sync()
  1473  
  1474  	if len(form.Value["snap-path"]) > 0 {
  1475  		origPath = form.Value["snap-path"][0]
  1476  	}
  1477  
  1478  	var instanceName string
  1479  
  1480  	if len(form.Value["name"]) > 0 {
  1481  		// caller has specified desired instance name
  1482  		instanceName = form.Value["name"][0]
  1483  		if err := snap.ValidateInstanceName(instanceName); err != nil {
  1484  			return BadRequest(err.Error())
  1485  		}
  1486  	}
  1487  
  1488  	st := c.d.overlord.State()
  1489  	st.Lock()
  1490  	defer st.Unlock()
  1491  
  1492  	var snapName string
  1493  	var sideInfo *snap.SideInfo
  1494  
  1495  	if !dangerousOK {
  1496  		si, err := snapasserts.DeriveSideInfo(tempPath, assertstate.DB(st))
  1497  		switch {
  1498  		case err == nil:
  1499  			snapName = si.RealName
  1500  			sideInfo = si
  1501  		case asserts.IsNotFound(err):
  1502  			// with devmode we try to find assertions but it's ok
  1503  			// if they are not there (implies --dangerous)
  1504  			if !isTrue(form, "devmode") {
  1505  				msg := "cannot find signatures with metadata for snap"
  1506  				if origPath != "" {
  1507  					msg = fmt.Sprintf("%s %q", msg, origPath)
  1508  				}
  1509  				return BadRequest(msg)
  1510  			}
  1511  			// TODO: set a warning if devmode
  1512  		default:
  1513  			return BadRequest(err.Error())
  1514  		}
  1515  	}
  1516  
  1517  	if snapName == "" {
  1518  		// potentially dangerous but dangerous or devmode params were set
  1519  		info, err := unsafeReadSnapInfo(tempPath)
  1520  		if err != nil {
  1521  			return BadRequest("cannot read snap file: %v", err)
  1522  		}
  1523  		snapName = info.SnapName()
  1524  		sideInfo = &snap.SideInfo{RealName: snapName}
  1525  	}
  1526  
  1527  	if instanceName != "" {
  1528  		requestedSnapName := snap.InstanceSnap(instanceName)
  1529  		if requestedSnapName != snapName {
  1530  			return BadRequest(fmt.Sprintf("instance name %q does not match snap name %q", instanceName, snapName))
  1531  		}
  1532  	} else {
  1533  		instanceName = snapName
  1534  	}
  1535  
  1536  	msg := fmt.Sprintf(i18n.G("Install %q snap from file"), instanceName)
  1537  	if origPath != "" {
  1538  		msg = fmt.Sprintf(i18n.G("Install %q snap from file %q"), instanceName, origPath)
  1539  	}
  1540  
  1541  	tset, _, err := snapstateInstallPath(st, sideInfo, tempPath, instanceName, "", flags)
  1542  	if err != nil {
  1543  		return errToResponse(err, []string{snapName}, InternalError, "cannot install snap file: %v")
  1544  	}
  1545  
  1546  	chg := newChange(st, "install-snap", msg, []*state.TaskSet{tset}, []string{instanceName})
  1547  	chg.Set("api-data", map[string]string{"snap-name": instanceName})
  1548  
  1549  	ensureStateSoon(st)
  1550  
  1551  	// only when the unlock succeeds (as opposed to panicing) is the handoff done
  1552  	// but this is good enough
  1553  	changeTriggered = true
  1554  
  1555  	return AsyncResponse(nil, &Meta{Change: chg.ID()})
  1556  }
  1557  
  1558  func unsafeReadSnapInfoImpl(snapPath string) (*snap.Info, error) {
  1559  	// Condider using DeriveSideInfo before falling back to this!
  1560  	snapf, err := snapfile.Open(snapPath)
  1561  	if err != nil {
  1562  		return nil, err
  1563  	}
  1564  	return snap.ReadInfoFromSnapFile(snapf, nil)
  1565  }
  1566  
  1567  var unsafeReadSnapInfo = unsafeReadSnapInfoImpl
  1568  
  1569  func iconGet(st *state.State, name string) Response {
  1570  	st.Lock()
  1571  	defer st.Unlock()
  1572  
  1573  	var snapst snapstate.SnapState
  1574  	err := snapstate.Get(st, name, &snapst)
  1575  	if err != nil {
  1576  		if err == state.ErrNoState {
  1577  			return SnapNotFound(name, err)
  1578  		}
  1579  		return InternalError("cannot consult state: %v", err)
  1580  	}
  1581  	sideInfo := snapst.CurrentSideInfo()
  1582  	if sideInfo == nil {
  1583  		return NotFound("snap has no current revision")
  1584  	}
  1585  
  1586  	icon := snapIcon(snap.MinimalPlaceInfo(name, sideInfo.Revision))
  1587  
  1588  	if icon == "" {
  1589  		return NotFound("local snap has no icon")
  1590  	}
  1591  
  1592  	return fileResponse(icon)
  1593  }
  1594  
  1595  func appIconGet(c *Command, r *http.Request, user *auth.UserState) Response {
  1596  	vars := muxVars(r)
  1597  	name := vars["name"]
  1598  
  1599  	return iconGet(c.d.overlord.State(), name)
  1600  }
  1601  
  1602  func getSnapConf(c *Command, r *http.Request, user *auth.UserState) Response {
  1603  	vars := muxVars(r)
  1604  	snapName := configstate.RemapSnapFromRequest(vars["name"])
  1605  
  1606  	keys := strutil.CommaSeparatedList(r.URL.Query().Get("keys"))
  1607  
  1608  	s := c.d.overlord.State()
  1609  	s.Lock()
  1610  	tr := config.NewTransaction(s)
  1611  	s.Unlock()
  1612  
  1613  	currentConfValues := make(map[string]interface{})
  1614  	// Special case - return root document
  1615  	if len(keys) == 0 {
  1616  		keys = []string{""}
  1617  	}
  1618  	for _, key := range keys {
  1619  		var value interface{}
  1620  		if err := tr.Get(snapName, key, &value); err != nil {
  1621  			if config.IsNoOption(err) {
  1622  				if key == "" {
  1623  					// no configuration - return empty document
  1624  					currentConfValues = make(map[string]interface{})
  1625  					break
  1626  				}
  1627  				return SyncResponse(&resp{
  1628  					Type: ResponseTypeError,
  1629  					Result: &errorResult{
  1630  						Message: err.Error(),
  1631  						Kind:    client.ErrorKindConfigNoSuchOption,
  1632  						Value:   err,
  1633  					},
  1634  					Status: 400,
  1635  				}, nil)
  1636  			} else {
  1637  				return InternalError("%v", err)
  1638  			}
  1639  		}
  1640  		if key == "" {
  1641  			if len(keys) > 1 {
  1642  				return BadRequest("keys contains zero-length string")
  1643  			}
  1644  			return SyncResponse(value, nil)
  1645  		}
  1646  
  1647  		currentConfValues[key] = value
  1648  	}
  1649  
  1650  	return SyncResponse(currentConfValues, nil)
  1651  }
  1652  
  1653  func setSnapConf(c *Command, r *http.Request, user *auth.UserState) Response {
  1654  	vars := muxVars(r)
  1655  	snapName := configstate.RemapSnapFromRequest(vars["name"])
  1656  
  1657  	var patchValues map[string]interface{}
  1658  	if err := jsonutil.DecodeWithNumber(r.Body, &patchValues); err != nil {
  1659  		return BadRequest("cannot decode request body into patch values: %v", err)
  1660  	}
  1661  
  1662  	st := c.d.overlord.State()
  1663  	st.Lock()
  1664  	defer st.Unlock()
  1665  
  1666  	taskset, err := configstate.ConfigureInstalled(st, snapName, patchValues, 0)
  1667  	if err != nil {
  1668  		// TODO: just return snap-not-installed instead ?
  1669  		if _, ok := err.(*snap.NotInstalledError); ok {
  1670  			return SnapNotFound(snapName, err)
  1671  		}
  1672  		return errToResponse(err, []string{snapName}, InternalError, "%v")
  1673  	}
  1674  
  1675  	summary := fmt.Sprintf("Change configuration of %q snap", snapName)
  1676  	change := newChange(st, "configure-snap", summary, []*state.TaskSet{taskset}, []string{snapName})
  1677  
  1678  	st.EnsureBefore(0)
  1679  
  1680  	return AsyncResponse(nil, &Meta{Change: change.ID()})
  1681  }
  1682  
  1683  // interfacesConnectionsMultiplexer multiplexes to either legacy (connection) or modern behavior (interfaces).
  1684  func interfacesConnectionsMultiplexer(c *Command, r *http.Request, user *auth.UserState) Response {
  1685  	query := r.URL.Query()
  1686  	qselect := query.Get("select")
  1687  	if qselect == "" {
  1688  		return getLegacyConnections(c, r, user)
  1689  	} else {
  1690  		return getInterfaces(c, r, user)
  1691  	}
  1692  }
  1693  
  1694  func getInterfaces(c *Command, r *http.Request, user *auth.UserState) Response {
  1695  	// Collect query options from request arguments.
  1696  	q := r.URL.Query()
  1697  	pselect := q.Get("select")
  1698  	if pselect != "all" && pselect != "connected" {
  1699  		return BadRequest("unsupported select qualifier")
  1700  	}
  1701  	var names []string // Interface names
  1702  	namesStr := q.Get("names")
  1703  	if namesStr != "" {
  1704  		names = strings.Split(namesStr, ",")
  1705  	}
  1706  	opts := &interfaces.InfoOptions{
  1707  		Names:     names,
  1708  		Doc:       q.Get("doc") == "true",
  1709  		Plugs:     q.Get("plugs") == "true",
  1710  		Slots:     q.Get("slots") == "true",
  1711  		Connected: pselect == "connected",
  1712  	}
  1713  	// Query the interface repository (this returns []*interface.Info).
  1714  	infos := c.d.overlord.InterfaceManager().Repository().Info(opts)
  1715  	infoJSONs := make([]*interfaceJSON, 0, len(infos))
  1716  
  1717  	for _, info := range infos {
  1718  		// Convert interfaces.Info into interfaceJSON
  1719  		plugs := make([]*plugJSON, 0, len(info.Plugs))
  1720  		for _, plug := range info.Plugs {
  1721  			plugs = append(plugs, &plugJSON{
  1722  				Snap:  plug.Snap.InstanceName(),
  1723  				Name:  plug.Name,
  1724  				Attrs: plug.Attrs,
  1725  				Label: plug.Label,
  1726  			})
  1727  		}
  1728  		slots := make([]*slotJSON, 0, len(info.Slots))
  1729  		for _, slot := range info.Slots {
  1730  			slots = append(slots, &slotJSON{
  1731  				Snap:  slot.Snap.InstanceName(),
  1732  				Name:  slot.Name,
  1733  				Attrs: slot.Attrs,
  1734  				Label: slot.Label,
  1735  			})
  1736  		}
  1737  		infoJSONs = append(infoJSONs, &interfaceJSON{
  1738  			Name:    info.Name,
  1739  			Summary: info.Summary,
  1740  			DocURL:  info.DocURL,
  1741  			Plugs:   plugs,
  1742  			Slots:   slots,
  1743  		})
  1744  	}
  1745  	return SyncResponse(infoJSONs, nil)
  1746  }
  1747  
  1748  func getLegacyConnections(c *Command, r *http.Request, user *auth.UserState) Response {
  1749  	connsjson, err := collectConnections(c.d.overlord.InterfaceManager(), collectFilter{})
  1750  	if err != nil {
  1751  		return InternalError("collecting connection information failed: %v", err)
  1752  	}
  1753  	legacyconnsjson := legacyConnectionsJSON{
  1754  		Plugs: connsjson.Plugs,
  1755  		Slots: connsjson.Slots,
  1756  	}
  1757  	return SyncResponse(legacyconnsjson, nil)
  1758  }
  1759  
  1760  func snapNamesFromConns(conns []*interfaces.ConnRef) []string {
  1761  	m := make(map[string]bool)
  1762  	for _, conn := range conns {
  1763  		m[conn.PlugRef.Snap] = true
  1764  		m[conn.SlotRef.Snap] = true
  1765  	}
  1766  	l := make([]string, 0, len(m))
  1767  	for name := range m {
  1768  		l = append(l, name)
  1769  	}
  1770  	sort.Strings(l)
  1771  	return l
  1772  }
  1773  
  1774  // changeInterfaces controls the interfaces system.
  1775  // Plugs can be connected to and disconnected from slots.
  1776  func changeInterfaces(c *Command, r *http.Request, user *auth.UserState) Response {
  1777  	var a interfaceAction
  1778  	decoder := json.NewDecoder(r.Body)
  1779  	if err := decoder.Decode(&a); err != nil {
  1780  		return BadRequest("cannot decode request body into an interface action: %v", err)
  1781  	}
  1782  	if a.Action == "" {
  1783  		return BadRequest("interface action not specified")
  1784  	}
  1785  	if len(a.Plugs) > 1 || len(a.Slots) > 1 {
  1786  		return NotImplemented("many-to-many operations are not implemented")
  1787  	}
  1788  	if a.Action != "connect" && a.Action != "disconnect" {
  1789  		return BadRequest("unsupported interface action: %q", a.Action)
  1790  	}
  1791  	if len(a.Plugs) == 0 || len(a.Slots) == 0 {
  1792  		return BadRequest("at least one plug and slot is required")
  1793  	}
  1794  
  1795  	var summary string
  1796  	var err error
  1797  
  1798  	var tasksets []*state.TaskSet
  1799  	var affected []string
  1800  
  1801  	st := c.d.overlord.State()
  1802  	st.Lock()
  1803  	defer st.Unlock()
  1804  
  1805  	for i := range a.Plugs {
  1806  		a.Plugs[i].Snap = ifacestate.RemapSnapFromRequest(a.Plugs[i].Snap)
  1807  	}
  1808  	for i := range a.Slots {
  1809  		a.Slots[i].Snap = ifacestate.RemapSnapFromRequest(a.Slots[i].Snap)
  1810  	}
  1811  
  1812  	switch a.Action {
  1813  	case "connect":
  1814  		var connRef *interfaces.ConnRef
  1815  		repo := c.d.overlord.InterfaceManager().Repository()
  1816  		connRef, err = repo.ResolveConnect(a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name)
  1817  		if err == nil {
  1818  			var ts *state.TaskSet
  1819  			affected = snapNamesFromConns([]*interfaces.ConnRef{connRef})
  1820  			summary = fmt.Sprintf("Connect %s:%s to %s:%s", connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name)
  1821  			ts, err = ifacestate.Connect(st, connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name)
  1822  			if _, ok := err.(*ifacestate.ErrAlreadyConnected); ok {
  1823  				change := newChange(st, a.Action+"-snap", summary, nil, affected)
  1824  				change.SetStatus(state.DoneStatus)
  1825  				return AsyncResponse(nil, &Meta{Change: change.ID()})
  1826  			}
  1827  			tasksets = append(tasksets, ts)
  1828  		}
  1829  	case "disconnect":
  1830  		var conns []*interfaces.ConnRef
  1831  		summary = fmt.Sprintf("Disconnect %s:%s from %s:%s", a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name)
  1832  		conns, err = c.d.overlord.InterfaceManager().ResolveDisconnect(a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name, a.Forget)
  1833  		if err == nil {
  1834  			if len(conns) == 0 {
  1835  				return InterfacesUnchanged("nothing to do")
  1836  			}
  1837  			repo := c.d.overlord.InterfaceManager().Repository()
  1838  			for _, connRef := range conns {
  1839  				var ts *state.TaskSet
  1840  				var conn *interfaces.Connection
  1841  				if a.Forget {
  1842  					ts, err = ifacestate.Forget(st, repo, connRef)
  1843  				} else {
  1844  					conn, err = repo.Connection(connRef)
  1845  					if err != nil {
  1846  						break
  1847  					}
  1848  					ts, err = ifacestate.Disconnect(st, conn)
  1849  					if err != nil {
  1850  						break
  1851  					}
  1852  				}
  1853  				if err != nil {
  1854  					break
  1855  				}
  1856  				ts.JoinLane(st.NewLane())
  1857  				tasksets = append(tasksets, ts)
  1858  			}
  1859  			affected = snapNamesFromConns(conns)
  1860  		}
  1861  	}
  1862  	if err != nil {
  1863  		return errToResponse(err, nil, BadRequest, "%v")
  1864  	}
  1865  
  1866  	change := newChange(st, a.Action+"-snap", summary, tasksets, affected)
  1867  	st.EnsureBefore(0)
  1868  
  1869  	return AsyncResponse(nil, &Meta{Change: change.ID()})
  1870  }
  1871  
  1872  type changeInfo struct {
  1873  	ID      string      `json:"id"`
  1874  	Kind    string      `json:"kind"`
  1875  	Summary string      `json:"summary"`
  1876  	Status  string      `json:"status"`
  1877  	Tasks   []*taskInfo `json:"tasks,omitempty"`
  1878  	Ready   bool        `json:"ready"`
  1879  	Err     string      `json:"err,omitempty"`
  1880  
  1881  	SpawnTime time.Time  `json:"spawn-time,omitempty"`
  1882  	ReadyTime *time.Time `json:"ready-time,omitempty"`
  1883  
  1884  	Data map[string]*json.RawMessage `json:"data,omitempty"`
  1885  }
  1886  
  1887  type taskInfo struct {
  1888  	ID       string           `json:"id"`
  1889  	Kind     string           `json:"kind"`
  1890  	Summary  string           `json:"summary"`
  1891  	Status   string           `json:"status"`
  1892  	Log      []string         `json:"log,omitempty"`
  1893  	Progress taskInfoProgress `json:"progress"`
  1894  
  1895  	SpawnTime time.Time  `json:"spawn-time,omitempty"`
  1896  	ReadyTime *time.Time `json:"ready-time,omitempty"`
  1897  }
  1898  
  1899  type taskInfoProgress struct {
  1900  	Label string `json:"label"`
  1901  	Done  int    `json:"done"`
  1902  	Total int    `json:"total"`
  1903  }
  1904  
  1905  func change2changeInfo(chg *state.Change) *changeInfo {
  1906  	status := chg.Status()
  1907  	chgInfo := &changeInfo{
  1908  		ID:      chg.ID(),
  1909  		Kind:    chg.Kind(),
  1910  		Summary: chg.Summary(),
  1911  		Status:  status.String(),
  1912  		Ready:   status.Ready(),
  1913  
  1914  		SpawnTime: chg.SpawnTime(),
  1915  	}
  1916  	readyTime := chg.ReadyTime()
  1917  	if !readyTime.IsZero() {
  1918  		chgInfo.ReadyTime = &readyTime
  1919  	}
  1920  	if err := chg.Err(); err != nil {
  1921  		chgInfo.Err = err.Error()
  1922  	}
  1923  
  1924  	tasks := chg.Tasks()
  1925  	taskInfos := make([]*taskInfo, len(tasks))
  1926  	for j, t := range tasks {
  1927  		label, done, total := t.Progress()
  1928  
  1929  		taskInfo := &taskInfo{
  1930  			ID:      t.ID(),
  1931  			Kind:    t.Kind(),
  1932  			Summary: t.Summary(),
  1933  			Status:  t.Status().String(),
  1934  			Log:     t.Log(),
  1935  			Progress: taskInfoProgress{
  1936  				Label: label,
  1937  				Done:  done,
  1938  				Total: total,
  1939  			},
  1940  			SpawnTime: t.SpawnTime(),
  1941  		}
  1942  		readyTime := t.ReadyTime()
  1943  		if !readyTime.IsZero() {
  1944  			taskInfo.ReadyTime = &readyTime
  1945  		}
  1946  		taskInfos[j] = taskInfo
  1947  	}
  1948  	chgInfo.Tasks = taskInfos
  1949  
  1950  	var data map[string]*json.RawMessage
  1951  	if chg.Get("api-data", &data) == nil {
  1952  		chgInfo.Data = data
  1953  	}
  1954  
  1955  	return chgInfo
  1956  }
  1957  
  1958  func getChange(c *Command, r *http.Request, user *auth.UserState) Response {
  1959  	chID := muxVars(r)["id"]
  1960  	state := c.d.overlord.State()
  1961  	state.Lock()
  1962  	defer state.Unlock()
  1963  	chg := state.Change(chID)
  1964  	if chg == nil {
  1965  		return NotFound("cannot find change with id %q", chID)
  1966  	}
  1967  
  1968  	return SyncResponse(change2changeInfo(chg), nil)
  1969  }
  1970  
  1971  func getChanges(c *Command, r *http.Request, user *auth.UserState) Response {
  1972  	query := r.URL.Query()
  1973  	qselect := query.Get("select")
  1974  	if qselect == "" {
  1975  		qselect = "in-progress"
  1976  	}
  1977  	var filter func(*state.Change) bool
  1978  	switch qselect {
  1979  	case "all":
  1980  		filter = func(*state.Change) bool { return true }
  1981  	case "in-progress":
  1982  		filter = func(chg *state.Change) bool { return !chg.Status().Ready() }
  1983  	case "ready":
  1984  		filter = func(chg *state.Change) bool { return chg.Status().Ready() }
  1985  	default:
  1986  		return BadRequest("select should be one of: all,in-progress,ready")
  1987  	}
  1988  
  1989  	if wantedName := query.Get("for"); wantedName != "" {
  1990  		outerFilter := filter
  1991  		filter = func(chg *state.Change) bool {
  1992  			if !outerFilter(chg) {
  1993  				return false
  1994  			}
  1995  
  1996  			var snapNames []string
  1997  			if err := chg.Get("snap-names", &snapNames); err != nil {
  1998  				logger.Noticef("Cannot get snap-name for change %v", chg.ID())
  1999  				return false
  2000  			}
  2001  
  2002  			for _, name := range snapNames {
  2003  				// due to
  2004  				// https://bugs.launchpad.net/snapd/+bug/1880560
  2005  				// the snap-names in service-control changes
  2006  				// could have included <snap>.<app>
  2007  				snapName, _ := snap.SplitSnapApp(name)
  2008  				if snapName == wantedName {
  2009  					return true
  2010  				}
  2011  			}
  2012  			return false
  2013  		}
  2014  	}
  2015  
  2016  	state := c.d.overlord.State()
  2017  	state.Lock()
  2018  	defer state.Unlock()
  2019  	chgs := state.Changes()
  2020  	chgInfos := make([]*changeInfo, 0, len(chgs))
  2021  	for _, chg := range chgs {
  2022  		if !filter(chg) {
  2023  			continue
  2024  		}
  2025  		chgInfos = append(chgInfos, change2changeInfo(chg))
  2026  	}
  2027  	return SyncResponse(chgInfos, nil)
  2028  }
  2029  
  2030  func abortChange(c *Command, r *http.Request, user *auth.UserState) Response {
  2031  	chID := muxVars(r)["id"]
  2032  	state := c.d.overlord.State()
  2033  	state.Lock()
  2034  	defer state.Unlock()
  2035  	chg := state.Change(chID)
  2036  	if chg == nil {
  2037  		return NotFound("cannot find change with id %q", chID)
  2038  	}
  2039  
  2040  	var reqData struct {
  2041  		Action string `json:"action"`
  2042  	}
  2043  
  2044  	decoder := json.NewDecoder(r.Body)
  2045  	if err := decoder.Decode(&reqData); err != nil {
  2046  		return BadRequest("cannot decode data from request body: %v", err)
  2047  	}
  2048  
  2049  	if reqData.Action != "abort" {
  2050  		return BadRequest("change action %q is unsupported", reqData.Action)
  2051  	}
  2052  
  2053  	if chg.Status().Ready() {
  2054  		return BadRequest("cannot abort change %s with nothing pending", chID)
  2055  	}
  2056  
  2057  	// flag the change
  2058  	chg.Abort()
  2059  
  2060  	// actually ask to proceed with the abort
  2061  	ensureStateSoon(state)
  2062  
  2063  	return SyncResponse(change2changeInfo(chg), nil)
  2064  }
  2065  
  2066  var (
  2067  	runSnapctlUcrednetGet = ucrednetGet
  2068  	ctlcmdRun             = ctlcmd.Run
  2069  )
  2070  
  2071  func convertBuyError(err error) Response {
  2072  	switch err {
  2073  	case nil:
  2074  		return nil
  2075  	case store.ErrInvalidCredentials:
  2076  		return Unauthorized(err.Error())
  2077  	case store.ErrUnauthenticated:
  2078  		return SyncResponse(&resp{
  2079  			Type: ResponseTypeError,
  2080  			Result: &errorResult{
  2081  				Message: err.Error(),
  2082  				Kind:    client.ErrorKindLoginRequired,
  2083  			},
  2084  			Status: 400,
  2085  		}, nil)
  2086  	case store.ErrTOSNotAccepted:
  2087  		return SyncResponse(&resp{
  2088  			Type: ResponseTypeError,
  2089  			Result: &errorResult{
  2090  				Message: err.Error(),
  2091  				Kind:    client.ErrorKindTermsNotAccepted,
  2092  			},
  2093  			Status: 400,
  2094  		}, nil)
  2095  	case store.ErrNoPaymentMethods:
  2096  		return SyncResponse(&resp{
  2097  			Type: ResponseTypeError,
  2098  			Result: &errorResult{
  2099  				Message: err.Error(),
  2100  				Kind:    client.ErrorKindNoPaymentMethods,
  2101  			},
  2102  			Status: 400,
  2103  		}, nil)
  2104  	case store.ErrPaymentDeclined:
  2105  		return SyncResponse(&resp{
  2106  			Type: ResponseTypeError,
  2107  			Result: &errorResult{
  2108  				Message: err.Error(),
  2109  				Kind:    client.ErrorKindPaymentDeclined,
  2110  			},
  2111  			Status: 400,
  2112  		}, nil)
  2113  	default:
  2114  		return InternalError("%v", err)
  2115  	}
  2116  }
  2117  
  2118  func postBuy(c *Command, r *http.Request, user *auth.UserState) Response {
  2119  	var opts client.BuyOptions
  2120  
  2121  	decoder := json.NewDecoder(r.Body)
  2122  	err := decoder.Decode(&opts)
  2123  	if err != nil {
  2124  		return BadRequest("cannot decode buy options from request body: %v", err)
  2125  	}
  2126  
  2127  	s := getStore(c)
  2128  
  2129  	buyResult, err := s.Buy(&opts, user)
  2130  
  2131  	if resp := convertBuyError(err); resp != nil {
  2132  		return resp
  2133  	}
  2134  
  2135  	return SyncResponse(buyResult, nil)
  2136  }
  2137  
  2138  func readyToBuy(c *Command, r *http.Request, user *auth.UserState) Response {
  2139  	s := getStore(c)
  2140  
  2141  	if resp := convertBuyError(s.ReadyToBuy(user)); resp != nil {
  2142  		return resp
  2143  	}
  2144  
  2145  	return SyncResponse(true, nil)
  2146  }
  2147  
  2148  func runSnapctl(c *Command, r *http.Request, user *auth.UserState) Response {
  2149  	var snapctlOptions client.SnapCtlOptions
  2150  	if err := jsonutil.DecodeWithNumber(r.Body, &snapctlOptions); err != nil {
  2151  		return BadRequest("cannot decode snapctl request: %s", err)
  2152  	}
  2153  
  2154  	if len(snapctlOptions.Args) == 0 {
  2155  		return BadRequest("snapctl cannot run without args")
  2156  	}
  2157  
  2158  	_, uid, _, err := runSnapctlUcrednetGet(r.RemoteAddr)
  2159  	if err != nil {
  2160  		return Forbidden("cannot get remote user: %s", err)
  2161  	}
  2162  
  2163  	// Ignore missing context error to allow 'snapctl -h' without a context;
  2164  	// Actual context is validated later by get/set.
  2165  	context, _ := c.d.overlord.HookManager().Context(snapctlOptions.ContextID)
  2166  	stdout, stderr, err := ctlcmdRun(context, snapctlOptions.Args, uid)
  2167  	if err != nil {
  2168  		if e, ok := err.(*ctlcmd.UnsuccessfulError); ok {
  2169  			result := map[string]interface{}{
  2170  				"stdout":    string(stdout),
  2171  				"stderr":    string(stderr),
  2172  				"exit-code": e.ExitCode,
  2173  			}
  2174  			return &resp{
  2175  				Type: ResponseTypeError,
  2176  				Result: &errorResult{
  2177  					Message: e.Error(),
  2178  					Kind:    client.ErrorKindUnsuccessful,
  2179  					Value:   result,
  2180  				},
  2181  				Status: 200,
  2182  			}
  2183  		}
  2184  		if e, ok := err.(*ctlcmd.ForbiddenCommandError); ok {
  2185  			return Forbidden(e.Error())
  2186  		}
  2187  		if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp {
  2188  			stdout = []byte(e.Error())
  2189  		} else {
  2190  			return BadRequest("error running snapctl: %s", err)
  2191  		}
  2192  	}
  2193  
  2194  	if context != nil && context.IsEphemeral() {
  2195  		context.Lock()
  2196  		defer context.Unlock()
  2197  		if err := context.Done(); err != nil {
  2198  			return BadRequest(i18n.G("set failed: %v"), err)
  2199  		}
  2200  	}
  2201  
  2202  	result := map[string]string{
  2203  		"stdout": string(stdout),
  2204  		"stderr": string(stderr),
  2205  	}
  2206  
  2207  	return SyncResponse(result, nil)
  2208  }
  2209  
  2210  // aliasAction is an action performed on aliases
  2211  type aliasAction struct {
  2212  	Action string `json:"action"`
  2213  	Snap   string `json:"snap"`
  2214  	App    string `json:"app"`
  2215  	Alias  string `json:"alias"`
  2216  	// old now unsupported api
  2217  	Aliases []string `json:"aliases"`
  2218  }
  2219  
  2220  func changeAliases(c *Command, r *http.Request, user *auth.UserState) Response {
  2221  	var a aliasAction
  2222  	decoder := json.NewDecoder(r.Body)
  2223  	if err := decoder.Decode(&a); err != nil {
  2224  		return BadRequest("cannot decode request body into an alias action: %v", err)
  2225  	}
  2226  	if len(a.Aliases) != 0 {
  2227  		return BadRequest("cannot interpret request, snaps can no longer be expected to declare their aliases")
  2228  	}
  2229  
  2230  	var taskset *state.TaskSet
  2231  	var err error
  2232  
  2233  	st := c.d.overlord.State()
  2234  	st.Lock()
  2235  	defer st.Unlock()
  2236  
  2237  	switch a.Action {
  2238  	default:
  2239  		return BadRequest("unsupported alias action: %q", a.Action)
  2240  	case "alias":
  2241  		taskset, err = snapstate.Alias(st, a.Snap, a.App, a.Alias)
  2242  	case "unalias":
  2243  		if a.Alias == a.Snap {
  2244  			// Do What I mean:
  2245  			// check if a snap is referred/intended
  2246  			// or just an alias
  2247  			var snapst snapstate.SnapState
  2248  			err := snapstate.Get(st, a.Snap, &snapst)
  2249  			if err != nil && err != state.ErrNoState {
  2250  				return InternalError("%v", err)
  2251  			}
  2252  			if err == state.ErrNoState { // not a snap
  2253  				a.Snap = ""
  2254  			}
  2255  		}
  2256  		if a.Snap != "" {
  2257  			a.Alias = ""
  2258  			taskset, err = snapstate.DisableAllAliases(st, a.Snap)
  2259  		} else {
  2260  			taskset, a.Snap, err = snapstate.RemoveManualAlias(st, a.Alias)
  2261  		}
  2262  	case "prefer":
  2263  		taskset, err = snapstate.Prefer(st, a.Snap)
  2264  	}
  2265  	if err != nil {
  2266  		return errToResponse(err, nil, BadRequest, "%v")
  2267  	}
  2268  
  2269  	var summary string
  2270  	switch a.Action {
  2271  	case "alias":
  2272  		summary = fmt.Sprintf(i18n.G("Setup alias %q => %q for snap %q"), a.Alias, a.App, a.Snap)
  2273  	case "unalias":
  2274  		if a.Alias != "" {
  2275  			summary = fmt.Sprintf(i18n.G("Remove manual alias %q for snap %q"), a.Alias, a.Snap)
  2276  		} else {
  2277  			summary = fmt.Sprintf(i18n.G("Disable all aliases for snap %q"), a.Snap)
  2278  		}
  2279  	case "prefer":
  2280  		summary = fmt.Sprintf(i18n.G("Prefer aliases of snap %q"), a.Snap)
  2281  	}
  2282  
  2283  	change := newChange(st, a.Action, summary, []*state.TaskSet{taskset}, []string{a.Snap})
  2284  	st.EnsureBefore(0)
  2285  
  2286  	return AsyncResponse(nil, &Meta{Change: change.ID()})
  2287  }
  2288  
  2289  type aliasStatus struct {
  2290  	Command string `json:"command"`
  2291  	Status  string `json:"status"`
  2292  	Manual  string `json:"manual,omitempty"`
  2293  	Auto    string `json:"auto,omitempty"`
  2294  }
  2295  
  2296  // getAliases produces a response with a map snap -> alias -> aliasStatus
  2297  func getAliases(c *Command, r *http.Request, user *auth.UserState) Response {
  2298  	state := c.d.overlord.State()
  2299  	state.Lock()
  2300  	defer state.Unlock()
  2301  
  2302  	res := make(map[string]map[string]aliasStatus)
  2303  
  2304  	allStates, err := snapstate.All(state)
  2305  	if err != nil {
  2306  		return InternalError("cannot list local snaps: %v", err)
  2307  	}
  2308  
  2309  	for snapName, snapst := range allStates {
  2310  		if err != nil {
  2311  			return InternalError("cannot retrieve info for snap %q: %v", snapName, err)
  2312  		}
  2313  		if len(snapst.Aliases) != 0 {
  2314  			snapAliases := make(map[string]aliasStatus)
  2315  			res[snapName] = snapAliases
  2316  			autoDisabled := snapst.AutoAliasesDisabled
  2317  			for alias, aliasTarget := range snapst.Aliases {
  2318  				aliasStatus := aliasStatus{
  2319  					Manual: aliasTarget.Manual,
  2320  					Auto:   aliasTarget.Auto,
  2321  				}
  2322  				status := "auto"
  2323  				tgt := aliasTarget.Effective(autoDisabled)
  2324  				if tgt == "" {
  2325  					status = "disabled"
  2326  					tgt = aliasTarget.Auto
  2327  				} else if aliasTarget.Manual != "" {
  2328  					status = "manual"
  2329  				}
  2330  				aliasStatus.Status = status
  2331  				aliasStatus.Command = snap.JoinSnapApp(snapName, tgt)
  2332  				snapAliases[alias] = aliasStatus
  2333  			}
  2334  		}
  2335  	}
  2336  
  2337  	return SyncResponse(res, nil)
  2338  }
  2339  
  2340  func getAppsInfo(c *Command, r *http.Request, user *auth.UserState) Response {
  2341  	query := r.URL.Query()
  2342  
  2343  	opts := appInfoOptions{}
  2344  	switch sel := query.Get("select"); sel {
  2345  	case "":
  2346  		// nothing to do
  2347  	case "service":
  2348  		opts.service = true
  2349  	default:
  2350  		return BadRequest("invalid select parameter: %q", sel)
  2351  	}
  2352  
  2353  	appInfos, rsp := appInfosFor(c.d.overlord.State(), strutil.CommaSeparatedList(query.Get("names")), opts)
  2354  	if rsp != nil {
  2355  		return rsp
  2356  	}
  2357  
  2358  	sd := servicestate.NewStatusDecorator(progress.Null)
  2359  
  2360  	clientAppInfos, err := clientutil.ClientAppInfosFromSnapAppInfos(appInfos, sd)
  2361  	if err != nil {
  2362  		return InternalError("%v", err)
  2363  	}
  2364  
  2365  	return SyncResponse(clientAppInfos, nil)
  2366  }
  2367  
  2368  func getLogs(c *Command, r *http.Request, user *auth.UserState) Response {
  2369  	query := r.URL.Query()
  2370  	n := 10
  2371  	if s := query.Get("n"); s != "" {
  2372  		m, err := strconv.ParseInt(s, 0, 32)
  2373  		if err != nil {
  2374  			return BadRequest(`invalid value for n: %q: %v`, s, err)
  2375  		}
  2376  		n = int(m)
  2377  	}
  2378  	follow := false
  2379  	if s := query.Get("follow"); s != "" {
  2380  		f, err := strconv.ParseBool(s)
  2381  		if err != nil {
  2382  			return BadRequest(`invalid value for follow: %q: %v`, s, err)
  2383  		}
  2384  		follow = f
  2385  	}
  2386  
  2387  	// only services have logs for now
  2388  	opts := appInfoOptions{service: true}
  2389  	appInfos, rsp := appInfosFor(c.d.overlord.State(), strutil.CommaSeparatedList(query.Get("names")), opts)
  2390  	if rsp != nil {
  2391  		return rsp
  2392  	}
  2393  	if len(appInfos) == 0 {
  2394  		return AppNotFound("no matching services")
  2395  	}
  2396  
  2397  	serviceNames := make([]string, len(appInfos))
  2398  	for i, appInfo := range appInfos {
  2399  		serviceNames[i] = appInfo.ServiceName()
  2400  	}
  2401  
  2402  	sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, progress.Null)
  2403  	reader, err := sysd.LogReader(serviceNames, n, follow)
  2404  	if err != nil {
  2405  		return InternalError("cannot get logs: %v", err)
  2406  	}
  2407  
  2408  	return &journalLineReaderSeqResponse{
  2409  		ReadCloser: reader,
  2410  		follow:     follow,
  2411  	}
  2412  }
  2413  
  2414  func namesToSnapNames(inst *servicestate.Instruction) []string {
  2415  	seen := make(map[string]struct{}, len(inst.Names))
  2416  	for _, snapOrSnapDotApp := range inst.Names {
  2417  		snapName, _ := snap.SplitSnapApp(snapOrSnapDotApp)
  2418  		seen[snapName] = struct{}{}
  2419  	}
  2420  	names := make([]string, 0, len(seen))
  2421  	for k := range seen {
  2422  		names = append(names, k)
  2423  	}
  2424  	// keep stable ordering
  2425  	sort.Strings(names)
  2426  	return names
  2427  }
  2428  
  2429  func postApps(c *Command, r *http.Request, user *auth.UserState) Response {
  2430  	var inst servicestate.Instruction
  2431  	decoder := json.NewDecoder(r.Body)
  2432  	if err := decoder.Decode(&inst); err != nil {
  2433  		return BadRequest("cannot decode request body into service operation: %v", err)
  2434  	}
  2435  	// XXX: decoder.More()
  2436  	if len(inst.Names) == 0 {
  2437  		// on POST, don't allow empty to mean all
  2438  		return BadRequest("cannot perform operation on services without a list of services to operate on")
  2439  	}
  2440  
  2441  	st := c.d.overlord.State()
  2442  	appInfos, rsp := appInfosFor(st, inst.Names, appInfoOptions{service: true})
  2443  	if rsp != nil {
  2444  		return rsp
  2445  	}
  2446  	if len(appInfos) == 0 {
  2447  		// can't happen: appInfosFor with a non-empty list of services
  2448  		// shouldn't ever return an empty appInfos with no error response
  2449  		return InternalError("no services found")
  2450  	}
  2451  
  2452  	tss, err := servicestateControl(st, appInfos, &inst, nil)
  2453  	if err != nil {
  2454  		// TODO: use errToResponse here too and introduce a proper error kind ?
  2455  		if _, ok := err.(servicestate.ServiceActionConflictError); ok {
  2456  			return Conflict(err.Error())
  2457  		}
  2458  		return BadRequest(err.Error())
  2459  	}
  2460  	st.Lock()
  2461  	defer st.Unlock()
  2462  	// names received in the request can be snap or snap.app, we need to
  2463  	// extract the actual snap names before associating them with a change
  2464  	chg := newChange(st, "service-control", fmt.Sprintf("Running service command"), tss, namesToSnapNames(&inst))
  2465  	st.EnsureBefore(0)
  2466  	return AsyncResponse(nil, &Meta{Change: chg.ID()})
  2467  }
  2468  
  2469  var (
  2470  	stateOkayWarnings    = (*state.State).OkayWarnings
  2471  	stateAllWarnings     = (*state.State).AllWarnings
  2472  	statePendingWarnings = (*state.State).PendingWarnings
  2473  )
  2474  
  2475  func ackWarnings(c *Command, r *http.Request, _ *auth.UserState) Response {
  2476  	defer r.Body.Close()
  2477  	var op struct {
  2478  		Action    string    `json:"action"`
  2479  		Timestamp time.Time `json:"timestamp"`
  2480  	}
  2481  	decoder := json.NewDecoder(r.Body)
  2482  	if err := decoder.Decode(&op); err != nil {
  2483  		return BadRequest("cannot decode request body into warnings operation: %v", err)
  2484  	}
  2485  	if op.Action != "okay" {
  2486  		return BadRequest("unknown warning action %q", op.Action)
  2487  	}
  2488  	st := c.d.overlord.State()
  2489  	st.Lock()
  2490  	defer st.Unlock()
  2491  	n := stateOkayWarnings(st, op.Timestamp)
  2492  
  2493  	return SyncResponse(n, nil)
  2494  }
  2495  
  2496  func getWarnings(c *Command, r *http.Request, _ *auth.UserState) Response {
  2497  	query := r.URL.Query()
  2498  	var all bool
  2499  	sel := query.Get("select")
  2500  	switch sel {
  2501  	case "all":
  2502  		all = true
  2503  	case "pending", "":
  2504  		all = false
  2505  	default:
  2506  		return BadRequest("invalid select parameter: %q", sel)
  2507  	}
  2508  
  2509  	st := c.d.overlord.State()
  2510  	st.Lock()
  2511  	defer st.Unlock()
  2512  
  2513  	var ws []*state.Warning
  2514  	if all {
  2515  		ws = stateAllWarnings(st)
  2516  	} else {
  2517  		ws, _ = statePendingWarnings(st)
  2518  	}
  2519  	if len(ws) == 0 {
  2520  		// no need to confuse the issue
  2521  		return SyncResponse([]state.Warning{}, nil)
  2522  	}
  2523  
  2524  	return SyncResponse(ws, nil)
  2525  }