github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/cmd/snap/cmd_snap_op.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-2017 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 main
    21  
    22  import (
    23  	"errors"
    24  	"fmt"
    25  	"os"
    26  	"path/filepath"
    27  	"sort"
    28  	"strings"
    29  	"time"
    30  	"unicode/utf8"
    31  
    32  	"github.com/jessevdk/go-flags"
    33  
    34  	"github.com/snapcore/snapd/client"
    35  	"github.com/snapcore/snapd/dirs"
    36  	"github.com/snapcore/snapd/i18n"
    37  	"github.com/snapcore/snapd/osutil"
    38  	"github.com/snapcore/snapd/release"
    39  	"github.com/snapcore/snapd/snap/channel"
    40  	"github.com/snapcore/snapd/strutil"
    41  )
    42  
    43  var (
    44  	shortInstallHelp = i18n.G("Install snaps on the system")
    45  	shortRemoveHelp  = i18n.G("Remove snaps from the system")
    46  	shortRefreshHelp = i18n.G("Refresh snaps in the system")
    47  	shortTryHelp     = i18n.G("Test an unpacked snap in the system")
    48  	shortEnableHelp  = i18n.G("Enable a snap in the system")
    49  	shortDisableHelp = i18n.G("Disable a snap in the system")
    50  )
    51  
    52  var longInstallHelp = i18n.G(`
    53  The install command installs the named snaps on the system.
    54  
    55  To install multiple instances of the same snap, append an underscore and a
    56  unique identifier (for each instance) to a snap's name.
    57  
    58  With no further options, the snaps are installed tracking the stable channel,
    59  with strict security confinement.
    60  
    61  Revision choice via the --revision override requires the the user to
    62  have developer access to the snap, either directly or through the
    63  store's collaboration feature, and to be logged in (see 'snap help login').
    64  
    65  Note a later refresh will typically undo a revision override, taking the snap
    66  back to the current revision of the channel it's tracking.
    67  
    68  Use --name to set the instance name when installing from snap file.
    69  `)
    70  
    71  var longRemoveHelp = i18n.G(`
    72  The remove command removes the named snap instance from the system.
    73  
    74  By default all the snap revisions are removed, including their data and the
    75  common data directory. When a --revision option is passed only the specified
    76  revision is removed.
    77  
    78  Unless automatic snapshots are disabled, a snapshot of all data for the snap is 
    79  saved upon removal, which is then available for future restoration with snap
    80  restore. The --purge option disables automatically creating snapshots.
    81  `)
    82  
    83  var longRefreshHelp = i18n.G(`
    84  The refresh command updates the specified snaps, or all snaps in the system if
    85  none are specified.
    86  
    87  With no further options, the snaps are refreshed to the current revision of the
    88  channel they're tracking, preserving their confinement options.
    89  
    90  Revision choice via the --revision override requires the the user to
    91  have developer access to the snap, either directly or through the
    92  store's collaboration feature, and to be logged in (see 'snap help login').
    93  
    94  Note a later refresh will typically undo a revision override.
    95  `)
    96  
    97  var longTryHelp = i18n.G(`
    98  The try command installs an unpacked snap into the system for testing purposes.
    99  The unpacked snap content continues to be used even after installation, so
   100  non-metadata changes there go live instantly. Metadata changes such as those
   101  performed in snap.yaml will require reinstallation to go live.
   102  
   103  If snap-dir argument is omitted, the try command will attempt to infer it if
   104  either snapcraft.yaml file and prime directory or meta/snap.yaml file can be
   105  found relative to current working directory.
   106  `)
   107  
   108  var longEnableHelp = i18n.G(`
   109  The enable command enables a snap that was previously disabled.
   110  `)
   111  
   112  var longDisableHelp = i18n.G(`
   113  The disable command disables a snap. The binaries and services of the
   114  snap will no longer be available, but all the data is still available
   115  and the snap can easily be enabled again.
   116  `)
   117  
   118  type cmdRemove struct {
   119  	waitMixin
   120  
   121  	Revision   string `long:"revision"`
   122  	Purge      bool   `long:"purge"`
   123  	Positional struct {
   124  		Snaps []installedSnapName `positional-arg-name:"<snap>" required:"1"`
   125  	} `positional-args:"yes" required:"yes"`
   126  }
   127  
   128  func (x *cmdRemove) removeOne(opts *client.SnapOptions) error {
   129  	name := string(x.Positional.Snaps[0])
   130  
   131  	changeID, err := x.client.Remove(name, opts)
   132  	if err != nil {
   133  		msg, err := errorToCmdMessage(name, err, opts)
   134  		if err != nil {
   135  			return err
   136  		}
   137  		fmt.Fprintln(Stderr, msg)
   138  		return nil
   139  	}
   140  
   141  	if _, err := x.wait(changeID); err != nil {
   142  		if err == noWait {
   143  			return nil
   144  		}
   145  		return err
   146  	}
   147  
   148  	if opts.Revision != "" {
   149  		fmt.Fprintf(Stdout, i18n.G("%s (revision %s) removed\n"), name, opts.Revision)
   150  	} else {
   151  		fmt.Fprintf(Stdout, i18n.G("%s removed\n"), name)
   152  	}
   153  	return nil
   154  }
   155  
   156  func (x *cmdRemove) removeMany(opts *client.SnapOptions) error {
   157  	names := installedSnapNames(x.Positional.Snaps)
   158  	changeID, err := x.client.RemoveMany(names, opts)
   159  	if err != nil {
   160  		return err
   161  	}
   162  
   163  	chg, err := x.wait(changeID)
   164  	if err != nil {
   165  		if err == noWait {
   166  			return nil
   167  		}
   168  		return err
   169  	}
   170  
   171  	var removed []string
   172  	if err := chg.Get("snap-names", &removed); err != nil && err != client.ErrNoData {
   173  		return err
   174  	}
   175  
   176  	seen := make(map[string]bool)
   177  	for _, name := range removed {
   178  		fmt.Fprintf(Stdout, i18n.G("%s removed\n"), name)
   179  		seen[name] = true
   180  	}
   181  	for _, name := range names {
   182  		if !seen[name] {
   183  			// FIXME: this is the only reason why a name can be
   184  			// skipped, but it does feel awkward
   185  			fmt.Fprintf(Stdout, i18n.G("%s not installed\n"), name)
   186  		}
   187  	}
   188  
   189  	return nil
   190  
   191  }
   192  
   193  func (x *cmdRemove) Execute([]string) error {
   194  	opts := &client.SnapOptions{Revision: x.Revision, Purge: x.Purge}
   195  	if len(x.Positional.Snaps) == 1 {
   196  		return x.removeOne(opts)
   197  	}
   198  
   199  	if x.Purge || x.Revision != "" {
   200  		return errors.New(i18n.G("a single snap name is needed to specify options"))
   201  	}
   202  	return x.removeMany(nil)
   203  }
   204  
   205  type channelMixin struct {
   206  	Channel string `long:"channel"`
   207  
   208  	// shortcuts
   209  	EdgeChannel      bool `long:"edge"`
   210  	BetaChannel      bool `long:"beta"`
   211  	CandidateChannel bool `long:"candidate"`
   212  	StableChannel    bool `long:"stable" `
   213  }
   214  
   215  type mixinDescs map[string]string
   216  
   217  func (mxd mixinDescs) also(m map[string]string) mixinDescs {
   218  	n := make(map[string]string, len(mxd)+len(m))
   219  	for k, v := range mxd {
   220  		n[k] = v
   221  	}
   222  	for k, v := range m {
   223  		n[k] = v
   224  	}
   225  	return n
   226  }
   227  
   228  var channelDescs = mixinDescs{
   229  	// TRANSLATORS: This should not start with a lowercase letter.
   230  	"channel": i18n.G("Use this channel instead of stable"),
   231  	// TRANSLATORS: This should not start with a lowercase letter.
   232  	"beta": i18n.G("Install from the beta channel"),
   233  	// TRANSLATORS: This should not start with a lowercase letter.
   234  	"edge": i18n.G("Install from the edge channel"),
   235  	// TRANSLATORS: This should not start with a lowercase letter.
   236  	"candidate": i18n.G("Install from the candidate channel"),
   237  	// TRANSLATORS: This should not start with a lowercase letter.
   238  	"stable": i18n.G("Install from the stable channel"),
   239  }
   240  
   241  func (mx *channelMixin) setChannelFromCommandline() error {
   242  	for _, ch := range []struct {
   243  		enabled bool
   244  		chName  string
   245  	}{
   246  		{mx.StableChannel, "stable"},
   247  		{mx.CandidateChannel, "candidate"},
   248  		{mx.BetaChannel, "beta"},
   249  		{mx.EdgeChannel, "edge"},
   250  	} {
   251  		if !ch.enabled {
   252  			continue
   253  		}
   254  		if mx.Channel != "" {
   255  			return fmt.Errorf("Please specify a single channel")
   256  		}
   257  		mx.Channel = ch.chName
   258  	}
   259  
   260  	if mx.Channel != "" {
   261  		if _, err := channel.Parse(mx.Channel, ""); err != nil {
   262  			full, er := channel.Full(mx.Channel)
   263  			if er != nil {
   264  				// the parse error has more detailed info
   265  				return err
   266  			}
   267  
   268  			// TODO: get escapes in here so we can bold the Warning
   269  			head := i18n.G("Warning:")
   270  			msg := i18n.G("Specifying a channel %q is relying on undefined behaviour. Interpreting it as %q for now, but this will be an error later.\n")
   271  			warn := fill(fmt.Sprintf(msg, mx.Channel, full), utf8.RuneCountInString(head)+1) // +1 for the space
   272  			fmt.Fprint(Stderr, head, " ", warn, "\n\n")
   273  			mx.Channel = full // so a malformed-but-eh channel will always be full, i.e. //stable// -> latest/stable
   274  		}
   275  	}
   276  
   277  	return nil
   278  }
   279  
   280  // isSnapInPath checks whether the snap binaries dir (e.g. /snap/bin)
   281  // is in $PATH.
   282  //
   283  // TODO: consider symlinks
   284  func isSnapInPath() bool {
   285  	paths := filepath.SplitList(os.Getenv("PATH"))
   286  	for _, path := range paths {
   287  		if filepath.Clean(path) == dirs.SnapBinariesDir {
   288  			return true
   289  		}
   290  	}
   291  	return false
   292  }
   293  
   294  func isSameRisk(tracking, current string) (bool, error) {
   295  	if tracking == current {
   296  		return true, nil
   297  	}
   298  	var trackingRisk, currentRisk string
   299  	if tracking != "" {
   300  		traCh, err := channel.Parse(tracking, "")
   301  		if err != nil {
   302  			return false, err
   303  		}
   304  		trackingRisk = traCh.Risk
   305  	}
   306  	if current != "" {
   307  		curCh, err := channel.Parse(current, "")
   308  		if err != nil {
   309  			return false, err
   310  		}
   311  		currentRisk = curCh.Risk
   312  	}
   313  	return trackingRisk == currentRisk, nil
   314  }
   315  
   316  func maybeWithSudoSecurePath() bool {
   317  	// Some distributions set secure_path to a known list of paths in
   318  	// /etc/sudoers. The snapd package currently has no means of overwriting
   319  	// or extending that setting.
   320  	if _, isSet := os.LookupEnv("SUDO_UID"); !isSet {
   321  		return false
   322  	}
   323  	// Known distros setting secure_path that does not include
   324  	// $SNAP_MOUNT_DIR/bin:
   325  	return release.DistroLike("fedora", "opensuse", "debian")
   326  }
   327  
   328  // show what has been done
   329  func showDone(cli *client.Client, names []string, op string, opts *client.SnapOptions, esc *escapes) error {
   330  	snaps, err := cli.List(names, nil)
   331  	if err != nil {
   332  		return err
   333  	}
   334  
   335  	needsPathWarning := !isSnapInPath() && !maybeWithSudoSecurePath()
   336  	for _, snap := range snaps {
   337  		channelStr := ""
   338  		if snap.Channel != "" {
   339  			ch, err := channel.Parse(snap.Channel, "")
   340  			if err != nil {
   341  				return err
   342  			}
   343  			if ch.Name != "stable" {
   344  				channelStr = fmt.Sprintf(" (%s)", ch.Name)
   345  			}
   346  		}
   347  		switch op {
   348  		case "install":
   349  			if needsPathWarning {
   350  				head := i18n.G("Warning:")
   351  				warn := fill(fmt.Sprintf(i18n.G("%s was not found in your $PATH. If you've not restarted your session since you installed snapd, try doing that. Please see https://forum.snapcraft.io/t/9469 for more details."), dirs.SnapBinariesDir), utf8.RuneCountInString(head)+1) // +1 for the space
   352  				fmt.Fprint(Stderr, esc.bold, head, esc.end, " ", warn, "\n\n")
   353  				needsPathWarning = false
   354  			}
   355  
   356  			if opts != nil && opts.Classic && snap.Confinement != client.ClassicConfinement {
   357  				// requested classic but the snap is not classic
   358  				head := i18n.G("Warning:")
   359  				// TRANSLATORS: the arg is a snap name (e.g. "some-snap")
   360  				warn := fill(fmt.Sprintf(i18n.G("flag --classic ignored for strictly confined snap %s"), snap.Name), utf8.RuneCountInString(head)+1) // +1 for the space
   361  				fmt.Fprint(Stderr, esc.bold, head, esc.end, " ", warn, "\n\n")
   362  			}
   363  
   364  			if snap.Publisher != nil {
   365  				// TRANSLATORS: the args are a snap name optionally followed by a channel, then a version, then the developer name (e.g. "some-snap (beta) 1.3 from Alice installed")
   366  				fmt.Fprintf(Stdout, i18n.G("%s%s %s from %s installed\n"), snap.Name, channelStr, snap.Version, longPublisher(esc, snap.Publisher))
   367  			} else {
   368  				// TRANSLATORS: the args are a snap name optionally followed by a channel, then a version (e.g. "some-snap (beta) 1.3 installed")
   369  				fmt.Fprintf(Stdout, i18n.G("%s%s %s installed\n"), snap.Name, channelStr, snap.Version)
   370  			}
   371  		case "refresh":
   372  			if snap.Publisher != nil {
   373  				// TRANSLATORS: the args are a snap name optionally followed by a channel, then a version, then the developer name (e.g. "some-snap (beta) 1.3 from Alice refreshed")
   374  				fmt.Fprintf(Stdout, i18n.G("%s%s %s from %s refreshed\n"), snap.Name, channelStr, snap.Version, longPublisher(esc, snap.Publisher))
   375  			} else {
   376  				// TRANSLATORS: the args are a snap name optionally followed by a channel, then a version (e.g. "some-snap (beta) 1.3 refreshed")
   377  				fmt.Fprintf(Stdout, i18n.G("%s%s %s refreshed\n"), snap.Name, channelStr, snap.Version)
   378  			}
   379  		case "revert":
   380  			// TRANSLATORS: first %s is a snap name, second %s is a revision
   381  			fmt.Fprintf(Stdout, i18n.G("%s reverted to %s\n"), snap.Name, snap.Version)
   382  		case "switch":
   383  			switchCohort := opts.CohortKey != ""
   384  			switchChannel := opts.Channel != ""
   385  			var msg string
   386  			// we have three boolean things to check, meaning 2³=8 possibilities,
   387  			// minus 3 error cases which are handled before the call to showDone.
   388  			switch {
   389  			case switchCohort && !opts.LeaveCohort && !switchChannel:
   390  				// TRANSLATORS: the first %q will be the (quoted) snap name, the second an ellipted cohort string
   391  				msg = fmt.Sprintf(i18n.G("%q switched to the %q cohort\n"), snap.Name, strutil.ElliptLeft(opts.CohortKey, 10))
   392  			case switchCohort && !opts.LeaveCohort && switchChannel:
   393  				// TRANSLATORS: the first %q will be the (quoted) snap name, the second a channel, the third an ellipted cohort string
   394  				msg = fmt.Sprintf(i18n.G("%q switched to the %q channel and the %q cohort\n"), snap.Name, snap.TrackingChannel, strutil.ElliptLeft(opts.CohortKey, 10))
   395  			case !switchCohort && !opts.LeaveCohort && switchChannel:
   396  				// TRANSLATORS: the first %q will be the (quoted) snap name, the second a channel
   397  				msg = fmt.Sprintf(i18n.G("%q switched to the %q channel\n"), snap.Name, snap.TrackingChannel)
   398  			case !switchCohort && opts.LeaveCohort && switchChannel:
   399  				// TRANSLATORS: the first %q will be the (quoted) snap name, the second a channel
   400  				msg = fmt.Sprintf(i18n.G("%q left the cohort, and switched to the %q channel"), snap.Name, snap.TrackingChannel)
   401  			case !switchCohort && opts.LeaveCohort && !switchChannel:
   402  				// TRANSLATORS: %q will be the (quoted) snap name
   403  				msg = fmt.Sprintf(i18n.G("%q left the cohort"), snap.Name)
   404  			}
   405  			fmt.Fprintln(Stdout, msg)
   406  		default:
   407  			fmt.Fprintf(Stdout, "internal error: unknown op %q", op)
   408  		}
   409  		if op == "install" || op == "refresh" {
   410  			if snap.TrackingChannel != snap.Channel && snap.Channel != "" {
   411  				if sameRisk, err := isSameRisk(snap.TrackingChannel, snap.Channel); err == nil && !sameRisk {
   412  					// TRANSLATORS: first %s is a channel name, following %s is a snap name, last %s is a channel name again.
   413  					fmt.Fprintf(Stdout, i18n.G("Channel %s for %s is closed; temporarily forwarding to %s.\n"), snap.TrackingChannel, snap.Name, snap.Channel)
   414  				}
   415  			}
   416  		}
   417  	}
   418  
   419  	return nil
   420  }
   421  
   422  func (mx *channelMixin) asksForChannel() bool {
   423  	return mx.Channel != ""
   424  }
   425  
   426  type modeMixin struct {
   427  	DevMode  bool `long:"devmode"`
   428  	JailMode bool `long:"jailmode"`
   429  	Classic  bool `long:"classic"`
   430  }
   431  
   432  var modeDescs = mixinDescs{
   433  	// TRANSLATORS: This should not start with a lowercase letter.
   434  	"classic": i18n.G("Put snap in classic mode and disable security confinement"),
   435  	// TRANSLATORS: This should not start with a lowercase letter.
   436  	"devmode": i18n.G("Put snap in development mode and disable security confinement"),
   437  	// TRANSLATORS: This should not start with a lowercase letter.
   438  	"jailmode": i18n.G("Put snap in enforced confinement mode"),
   439  }
   440  
   441  var errModeConflict = errors.New(i18n.G("cannot use devmode and jailmode flags together"))
   442  
   443  func (mx modeMixin) validateMode() error {
   444  	if mx.DevMode && mx.JailMode {
   445  		return errModeConflict
   446  	}
   447  	return nil
   448  }
   449  
   450  func (mx modeMixin) asksForMode() bool {
   451  	return mx.DevMode || mx.JailMode || mx.Classic
   452  }
   453  
   454  func (mx modeMixin) setModes(opts *client.SnapOptions) {
   455  	opts.DevMode = mx.DevMode
   456  	opts.JailMode = mx.JailMode
   457  	opts.Classic = mx.Classic
   458  }
   459  
   460  type cmdInstall struct {
   461  	colorMixin
   462  	waitMixin
   463  
   464  	channelMixin
   465  	modeMixin
   466  	Revision string `long:"revision"`
   467  
   468  	Dangerous bool `long:"dangerous"`
   469  	// alias for --dangerous, deprecated but we need to support it
   470  	// because we released 2.14.2 with --force-dangerous
   471  	ForceDangerous bool `long:"force-dangerous" hidden:"yes"`
   472  
   473  	Unaliased bool `long:"unaliased"`
   474  
   475  	Name string `long:"name"`
   476  
   477  	Cohort     string `long:"cohort"`
   478  	Positional struct {
   479  		Snaps []remoteSnapName `positional-arg-name:"<snap>"`
   480  	} `positional-args:"yes" required:"yes"`
   481  }
   482  
   483  func (x *cmdInstall) installOne(nameOrPath, desiredName string, opts *client.SnapOptions) error {
   484  	var err error
   485  	var changeID string
   486  	var snapName string
   487  	var path string
   488  
   489  	if strings.Contains(nameOrPath, "/") || strings.HasSuffix(nameOrPath, ".snap") || strings.Contains(nameOrPath, ".snap.") {
   490  		path = nameOrPath
   491  		changeID, err = x.client.InstallPath(path, x.Name, opts)
   492  	} else {
   493  		snapName = nameOrPath
   494  		if desiredName != "" {
   495  			return errors.New(i18n.G("cannot use explicit name when installing from store"))
   496  		}
   497  		changeID, err = x.client.Install(snapName, opts)
   498  	}
   499  	if err != nil {
   500  		msg, err := errorToCmdMessage(nameOrPath, err, opts)
   501  		if err != nil {
   502  			return err
   503  		}
   504  		fmt.Fprintln(Stderr, msg)
   505  		return nil
   506  	}
   507  
   508  	chg, err := x.wait(changeID)
   509  	if err != nil {
   510  		if err == noWait {
   511  			return nil
   512  		}
   513  		return err
   514  	}
   515  
   516  	// extract the snapName from the change, important for sideloaded
   517  	if path != "" {
   518  		if err := chg.Get("snap-name", &snapName); err != nil {
   519  			return fmt.Errorf("cannot extract the snap-name from local file %q: %s", nameOrPath, err)
   520  		}
   521  	}
   522  
   523  	// TODO: mention details of the install (e.g. like switch does)
   524  	return showDone(x.client, []string{snapName}, "install", opts, x.getEscapes())
   525  }
   526  
   527  func (x *cmdInstall) installMany(names []string, opts *client.SnapOptions) error {
   528  	// sanity check
   529  	for _, name := range names {
   530  		if strings.Contains(name, "/") || strings.HasSuffix(name, ".snap") || strings.Contains(name, ".snap.") {
   531  			return fmt.Errorf("only one snap file can be installed at a time")
   532  		}
   533  	}
   534  
   535  	changeID, err := x.client.InstallMany(names, opts)
   536  	if err != nil {
   537  		var snapName string
   538  		if err, ok := err.(*client.Error); ok {
   539  			snapName, _ = err.Value.(string)
   540  		}
   541  		msg, err := errorToCmdMessage(snapName, err, opts)
   542  		if err != nil {
   543  			return err
   544  		}
   545  		fmt.Fprintln(Stderr, msg)
   546  		return nil
   547  	}
   548  
   549  	chg, err := x.wait(changeID)
   550  	if err != nil {
   551  		if err == noWait {
   552  			return nil
   553  		}
   554  		return err
   555  	}
   556  
   557  	var installed []string
   558  	if err := chg.Get("snap-names", &installed); err != nil && err != client.ErrNoData {
   559  		return err
   560  	}
   561  
   562  	if len(installed) > 0 {
   563  		if err := showDone(x.client, installed, "install", opts, x.getEscapes()); err != nil {
   564  			return err
   565  		}
   566  	}
   567  
   568  	// show skipped
   569  	seen := make(map[string]bool)
   570  	for _, name := range installed {
   571  		seen[name] = true
   572  	}
   573  	for _, name := range names {
   574  		if !seen[name] {
   575  			// FIXME: this is the only reason why a name can be
   576  			// skipped, but it does feel awkward
   577  			fmt.Fprintf(Stdout, i18n.G("%s already installed\n"), name)
   578  		}
   579  	}
   580  
   581  	return nil
   582  }
   583  
   584  func (x *cmdInstall) Execute([]string) error {
   585  	if err := x.setChannelFromCommandline(); err != nil {
   586  		return err
   587  	}
   588  	if err := x.validateMode(); err != nil {
   589  		return err
   590  	}
   591  
   592  	dangerous := x.Dangerous || x.ForceDangerous
   593  	opts := &client.SnapOptions{
   594  		Channel:   x.Channel,
   595  		Revision:  x.Revision,
   596  		Dangerous: dangerous,
   597  		Unaliased: x.Unaliased,
   598  		CohortKey: x.Cohort,
   599  	}
   600  	x.setModes(opts)
   601  
   602  	names := remoteSnapNames(x.Positional.Snaps)
   603  	if len(names) == 0 {
   604  		return errors.New(i18n.G("cannot install zero snaps"))
   605  	}
   606  	for _, name := range names {
   607  		if len(name) == 0 {
   608  			return errors.New(i18n.G("cannot install snap with empty name"))
   609  		}
   610  	}
   611  
   612  	if len(names) == 1 {
   613  		return x.installOne(names[0], x.Name, opts)
   614  	}
   615  
   616  	if x.asksForMode() || x.asksForChannel() {
   617  		return errors.New(i18n.G("a single snap name is needed to specify mode or channel flags"))
   618  	}
   619  
   620  	if x.Name != "" {
   621  		return errors.New(i18n.G("cannot use instance name when installing multiple snaps"))
   622  	}
   623  	return x.installMany(names, nil)
   624  }
   625  
   626  type cmdRefresh struct {
   627  	colorMixin
   628  	timeMixin
   629  	waitMixin
   630  	channelMixin
   631  	modeMixin
   632  
   633  	Amend            bool   `long:"amend"`
   634  	Revision         string `long:"revision"`
   635  	Cohort           string `long:"cohort"`
   636  	LeaveCohort      bool   `long:"leave-cohort"`
   637  	List             bool   `long:"list"`
   638  	Time             bool   `long:"time"`
   639  	IgnoreValidation bool   `long:"ignore-validation"`
   640  	Positional       struct {
   641  		Snaps []installedSnapName `positional-arg-name:"<snap>"`
   642  	} `positional-args:"yes"`
   643  }
   644  
   645  func (x *cmdRefresh) refreshMany(snaps []string, opts *client.SnapOptions) error {
   646  	changeID, err := x.client.RefreshMany(snaps, opts)
   647  	if err != nil {
   648  		return err
   649  	}
   650  
   651  	chg, err := x.wait(changeID)
   652  	if err != nil {
   653  		if err == noWait {
   654  			return nil
   655  		}
   656  		return err
   657  	}
   658  
   659  	var refreshed []string
   660  	if err := chg.Get("snap-names", &refreshed); err != nil && err != client.ErrNoData {
   661  		return err
   662  	}
   663  
   664  	if len(refreshed) > 0 {
   665  		return showDone(x.client, refreshed, "refresh", opts, x.getEscapes())
   666  	}
   667  
   668  	fmt.Fprintln(Stderr, i18n.G("All snaps up to date."))
   669  
   670  	return nil
   671  }
   672  
   673  func (x *cmdRefresh) refreshOne(name string, opts *client.SnapOptions) error {
   674  	changeID, err := x.client.Refresh(name, opts)
   675  	if err != nil {
   676  		msg, err := errorToCmdMessage(name, err, opts)
   677  		if err != nil {
   678  			return err
   679  		}
   680  		fmt.Fprintln(Stderr, msg)
   681  		return nil
   682  	}
   683  
   684  	if _, err := x.wait(changeID); err != nil {
   685  		if err == noWait {
   686  			return nil
   687  		}
   688  		return err
   689  	}
   690  
   691  	// TODO: this doesn't really tell about all the things you
   692  	// could set while refreshing (something switch does)
   693  	return showDone(x.client, []string{name}, "refresh", opts, x.getEscapes())
   694  }
   695  
   696  func parseSysinfoTime(s string) time.Time {
   697  	t, err := time.Parse(time.RFC3339, s)
   698  	if err != nil {
   699  		return time.Time{}
   700  	}
   701  	return t
   702  }
   703  
   704  func (x *cmdRefresh) showRefreshTimes() error {
   705  	sysinfo, err := x.client.SysInfo()
   706  	if err != nil {
   707  		return err
   708  	}
   709  
   710  	if sysinfo.Refresh.Timer != "" {
   711  		fmt.Fprintf(Stdout, "timer: %s\n", sysinfo.Refresh.Timer)
   712  	} else if sysinfo.Refresh.Schedule != "" {
   713  		fmt.Fprintf(Stdout, "schedule: %s\n", sysinfo.Refresh.Schedule)
   714  	} else {
   715  		return errors.New("internal error: both refresh.timer and refresh.schedule are empty")
   716  	}
   717  	last := parseSysinfoTime(sysinfo.Refresh.Last)
   718  	hold := parseSysinfoTime(sysinfo.Refresh.Hold)
   719  	next := parseSysinfoTime(sysinfo.Refresh.Next)
   720  
   721  	if !last.IsZero() {
   722  		fmt.Fprintf(Stdout, "last: %s\n", x.fmtTime(last))
   723  	} else {
   724  		fmt.Fprintf(Stdout, "last: n/a\n")
   725  	}
   726  	if !hold.IsZero() {
   727  		fmt.Fprintf(Stdout, "hold: %s\n", x.fmtTime(hold))
   728  	}
   729  	// only show "next" if its after "hold" to not confuse users
   730  	if !next.IsZero() {
   731  		// Snapstate checks for holdTime.After(limitTime) so we need
   732  		// to check for before or equal here to be fully correct.
   733  		if next.Before(hold) || next.Equal(hold) {
   734  			fmt.Fprintf(Stdout, "next: %s (but held)\n", x.fmtTime(next))
   735  		} else {
   736  			fmt.Fprintf(Stdout, "next: %s\n", x.fmtTime(next))
   737  		}
   738  	} else {
   739  		fmt.Fprintf(Stdout, "next: n/a\n")
   740  	}
   741  	return nil
   742  }
   743  
   744  func (x *cmdRefresh) listRefresh() error {
   745  	snaps, _, err := x.client.Find(&client.FindOptions{
   746  		Refresh: true,
   747  	})
   748  	if err != nil {
   749  		return err
   750  	}
   751  	if len(snaps) == 0 {
   752  		fmt.Fprintln(Stderr, i18n.G("All snaps up to date."))
   753  		return nil
   754  	}
   755  
   756  	sort.Sort(snapsByName(snaps))
   757  
   758  	esc := x.getEscapes()
   759  	w := tabWriter()
   760  	defer w.Flush()
   761  
   762  	// TRANSLATORS: the %s is to insert a filler escape sequence (please keep it flush to the column header, with no extra spaces)
   763  	fmt.Fprintf(w, i18n.G("Name\tVersion\tRev\tPublisher%s\tNotes\n"), fillerPublisher(esc))
   764  	for _, snap := range snaps {
   765  		fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", snap.Name, snap.Version, snap.Revision, shortPublisher(esc, snap.Publisher), NotesFromRemote(snap, nil))
   766  	}
   767  
   768  	return nil
   769  }
   770  
   771  func (x *cmdRefresh) Execute([]string) error {
   772  	if err := x.setChannelFromCommandline(); err != nil {
   773  		return err
   774  	}
   775  	if err := x.validateMode(); err != nil {
   776  		return err
   777  	}
   778  
   779  	if x.Time {
   780  		if x.asksForMode() || x.asksForChannel() {
   781  			return errors.New(i18n.G("--time does not take mode or channel flags"))
   782  		}
   783  		return x.showRefreshTimes()
   784  	}
   785  
   786  	if x.List {
   787  		if len(x.Positional.Snaps) > 0 || x.asksForMode() || x.asksForChannel() {
   788  			return errors.New(i18n.G("--list does not accept additional arguments"))
   789  		}
   790  
   791  		return x.listRefresh()
   792  	}
   793  
   794  	if len(x.Positional.Snaps) == 0 && os.Getenv("SNAP_REFRESH_FROM_TIMER") == "1" {
   795  		fmt.Fprintf(Stdout, "Ignoring `snap refresh` from the systemd timer")
   796  		return nil
   797  	}
   798  
   799  	names := installedSnapNames(x.Positional.Snaps)
   800  	if len(names) == 1 {
   801  		opts := &client.SnapOptions{
   802  			Amend:            x.Amend,
   803  			Channel:          x.Channel,
   804  			IgnoreValidation: x.IgnoreValidation,
   805  			Revision:         x.Revision,
   806  			CohortKey:        x.Cohort,
   807  			LeaveCohort:      x.LeaveCohort,
   808  		}
   809  		x.setModes(opts)
   810  		return x.refreshOne(names[0], opts)
   811  	}
   812  
   813  	if x.asksForMode() || x.asksForChannel() {
   814  		return errors.New(i18n.G("a single snap name is needed to specify mode or channel flags"))
   815  	}
   816  
   817  	if x.IgnoreValidation {
   818  		return errors.New(i18n.G("a single snap name must be specified when ignoring validation"))
   819  	}
   820  
   821  	return x.refreshMany(names, nil)
   822  }
   823  
   824  type cmdTry struct {
   825  	waitMixin
   826  
   827  	modeMixin
   828  	Positional struct {
   829  		SnapDir string `positional-arg-name:"<snap-dir>"`
   830  	} `positional-args:"yes"`
   831  }
   832  
   833  func hasSnapcraftYaml() bool {
   834  	for _, loc := range []string{
   835  		"snap/snapcraft.yaml",
   836  		"snapcraft.yaml",
   837  		".snapcraft.yaml",
   838  	} {
   839  		if osutil.FileExists(loc) {
   840  			return true
   841  		}
   842  	}
   843  
   844  	return false
   845  }
   846  
   847  func (x *cmdTry) Execute([]string) error {
   848  	if err := x.validateMode(); err != nil {
   849  		return err
   850  	}
   851  	name := x.Positional.SnapDir
   852  	opts := &client.SnapOptions{}
   853  	x.setModes(opts)
   854  
   855  	if name == "" {
   856  		if hasSnapcraftYaml() && osutil.IsDirectory("prime") {
   857  			name = "prime"
   858  		} else {
   859  			if osutil.FileExists("meta/snap.yaml") {
   860  				name = "./"
   861  			}
   862  		}
   863  		if name == "" {
   864  			return fmt.Errorf(i18n.G("error: the `<snap-dir>` argument was not provided and couldn't be inferred"))
   865  		}
   866  	}
   867  
   868  	path, err := filepath.Abs(name)
   869  	if err != nil {
   870  		// TRANSLATORS: %q gets what the user entered, %v gets the resulting error message
   871  		return fmt.Errorf(i18n.G("cannot get full path for %q: %v"), name, err)
   872  	}
   873  
   874  	changeID, err := x.client.Try(path, opts)
   875  	if err != nil {
   876  		msg, err := errorToCmdMessage(name, err, opts)
   877  		if err != nil {
   878  			return err
   879  		}
   880  		fmt.Fprintln(Stderr, msg)
   881  		return nil
   882  	}
   883  
   884  	chg, err := x.wait(changeID)
   885  	if err != nil {
   886  		if err == noWait {
   887  			return nil
   888  		}
   889  		return err
   890  	}
   891  
   892  	// extract the snap name
   893  	var snapName string
   894  	if err := chg.Get("snap-name", &snapName); err != nil {
   895  		// TRANSLATORS: %q gets the snap name, %v gets the resulting error message
   896  		return fmt.Errorf(i18n.G("cannot extract the snap-name from local file %q: %v"), name, err)
   897  	}
   898  	name = snapName
   899  
   900  	// show output as speced
   901  	snaps, err := x.client.List([]string{name}, nil)
   902  	if err != nil {
   903  		return err
   904  	}
   905  	if len(snaps) != 1 {
   906  		// TRANSLATORS: %q gets the snap name, %v the list of things found when trying to list it
   907  		return fmt.Errorf(i18n.G("cannot get data for %q: %v"), name, snaps)
   908  	}
   909  	snap := snaps[0]
   910  	// TRANSLATORS: 1. snap name, 2. snap version (keep those together please). the 3rd %s is a path (where it's mounted from).
   911  	fmt.Fprintf(Stdout, i18n.G("%s %s mounted from %s\n"), name, snap.Version, path)
   912  	return nil
   913  }
   914  
   915  type cmdEnable struct {
   916  	waitMixin
   917  
   918  	Positional struct {
   919  		Snap installedSnapName `positional-arg-name:"<snap>"`
   920  	} `positional-args:"yes" required:"yes"`
   921  }
   922  
   923  func (x *cmdEnable) Execute([]string) error {
   924  	name := string(x.Positional.Snap)
   925  	opts := &client.SnapOptions{}
   926  	changeID, err := x.client.Enable(name, opts)
   927  	if err != nil {
   928  		return err
   929  	}
   930  
   931  	if _, err := x.wait(changeID); err != nil {
   932  		if err == noWait {
   933  			return nil
   934  		}
   935  		return err
   936  	}
   937  
   938  	fmt.Fprintf(Stdout, i18n.G("%s enabled\n"), name)
   939  	return nil
   940  }
   941  
   942  type cmdDisable struct {
   943  	waitMixin
   944  
   945  	Positional struct {
   946  		Snap installedSnapName `positional-arg-name:"<snap>"`
   947  	} `positional-args:"yes" required:"yes"`
   948  }
   949  
   950  func (x *cmdDisable) Execute([]string) error {
   951  	name := string(x.Positional.Snap)
   952  	opts := &client.SnapOptions{}
   953  	changeID, err := x.client.Disable(name, opts)
   954  	if err != nil {
   955  		return err
   956  	}
   957  
   958  	if _, err := x.wait(changeID); err != nil {
   959  		if err == noWait {
   960  			return nil
   961  		}
   962  		return err
   963  	}
   964  
   965  	fmt.Fprintf(Stdout, i18n.G("%s disabled\n"), name)
   966  	return nil
   967  }
   968  
   969  type cmdRevert struct {
   970  	waitMixin
   971  
   972  	modeMixin
   973  	Revision   string `long:"revision"`
   974  	Positional struct {
   975  		Snap installedSnapName `positional-arg-name:"<snap>"`
   976  	} `positional-args:"yes" required:"yes"`
   977  }
   978  
   979  var shortRevertHelp = i18n.G("Reverts the given snap to the previous state")
   980  var longRevertHelp = i18n.G(`
   981  The revert command reverts the given snap to its state before
   982  the latest refresh. This will reactivate the previous snap revision,
   983  and will use the original data that was associated with that revision,
   984  discarding any data changes that were done by the latest revision. As
   985  an exception, data which the snap explicitly chooses to share across
   986  revisions is not touched by the revert process.
   987  `)
   988  
   989  func (x *cmdRevert) Execute(args []string) error {
   990  	if len(args) > 0 {
   991  		return ErrExtraArgs
   992  	}
   993  
   994  	if err := x.validateMode(); err != nil {
   995  		return err
   996  	}
   997  
   998  	name := string(x.Positional.Snap)
   999  	opts := &client.SnapOptions{Revision: x.Revision}
  1000  	x.setModes(opts)
  1001  	changeID, err := x.client.Revert(name, opts)
  1002  	if err != nil {
  1003  		return err
  1004  	}
  1005  
  1006  	if _, err := x.wait(changeID); err != nil {
  1007  		if err == noWait {
  1008  			return nil
  1009  		}
  1010  		return err
  1011  	}
  1012  
  1013  	return showDone(x.client, []string{name}, "revert", nil, nil)
  1014  }
  1015  
  1016  var shortSwitchHelp = i18n.G("Switches snap to a different channel")
  1017  var longSwitchHelp = i18n.G(`
  1018  The switch command switches the given snap to a different channel without
  1019  doing a refresh.
  1020  `)
  1021  
  1022  type cmdSwitch struct {
  1023  	waitMixin
  1024  	channelMixin
  1025  
  1026  	Cohort      string `long:"cohort"`
  1027  	LeaveCohort bool   `long:"leave-cohort"`
  1028  
  1029  	Positional struct {
  1030  		Snap installedSnapName `positional-arg-name:"<snap>" required:"1"`
  1031  	} `positional-args:"yes" required:"yes"`
  1032  }
  1033  
  1034  func (x cmdSwitch) Execute(args []string) error {
  1035  	if err := x.setChannelFromCommandline(); err != nil {
  1036  		return err
  1037  	}
  1038  
  1039  	name := string(x.Positional.Snap)
  1040  	channel := string(x.Channel)
  1041  
  1042  	switchCohort := x.Cohort != ""
  1043  	switchChannel := x.Channel != ""
  1044  
  1045  	// we have three boolean things to check, meaning 2³=8 possibilities
  1046  	// of which 3 are errors (which is why we look at the errors first).
  1047  	// the 5 valid cases are handled by showDone.
  1048  	if switchCohort && x.LeaveCohort {
  1049  		// this one counts as two (no channel filter)
  1050  		return fmt.Errorf(i18n.G("cannot specify both --cohort and --leave-cohort"))
  1051  	}
  1052  	if !switchCohort && !x.LeaveCohort && !switchChannel {
  1053  		return fmt.Errorf(i18n.G("nothing to switch; specify --channel (and/or one of --cohort/--leave-cohort)"))
  1054  	}
  1055  
  1056  	opts := &client.SnapOptions{
  1057  		Channel:     channel,
  1058  		CohortKey:   x.Cohort,
  1059  		LeaveCohort: x.LeaveCohort,
  1060  	}
  1061  	changeID, err := x.client.Switch(name, opts)
  1062  	if err != nil {
  1063  		return err
  1064  	}
  1065  
  1066  	if _, err := x.wait(changeID); err != nil {
  1067  		if err == noWait {
  1068  			return nil
  1069  		}
  1070  		return err
  1071  	}
  1072  
  1073  	return showDone(x.client, []string{name}, "switch", opts, nil)
  1074  }
  1075  
  1076  func init() {
  1077  	addCommand("remove", shortRemoveHelp, longRemoveHelp, func() flags.Commander { return &cmdRemove{} },
  1078  		waitDescs.also(map[string]string{
  1079  			// TRANSLATORS: This should not start with a lowercase letter.
  1080  			"revision": i18n.G("Remove only the given revision"),
  1081  			// TRANSLATORS: This should not start with a lowercase letter.
  1082  			"purge": i18n.G("Remove the snap without saving a snapshot of its data"),
  1083  		}), nil)
  1084  	addCommand("install", shortInstallHelp, longInstallHelp, func() flags.Commander { return &cmdInstall{} },
  1085  		colorDescs.also(waitDescs).also(channelDescs).also(modeDescs).also(map[string]string{
  1086  			// TRANSLATORS: This should not start with a lowercase letter.
  1087  			"revision": i18n.G("Install the given revision of a snap, to which you must have developer access"),
  1088  			// TRANSLATORS: This should not start with a lowercase letter.
  1089  			"dangerous": i18n.G("Install the given snap file even if there are no pre-acknowledged signatures for it, meaning it was not verified and could be dangerous (--devmode implies this)"),
  1090  			// TRANSLATORS: This should not start with a lowercase letter.
  1091  			"force-dangerous": i18n.G("Alias for --dangerous (DEPRECATED)"),
  1092  			// TRANSLATORS: This should not start with a lowercase letter.
  1093  			"unaliased": i18n.G("Install the given snap without enabling its automatic aliases"),
  1094  			// TRANSLATORS: This should not start with a lowercase letter.
  1095  			"name": i18n.G("Install the snap file under the given instance name"),
  1096  			// TRANSLATORS: This should not start with a lowercase letter.
  1097  			"cohort": i18n.G("Install the snap in the given cohort"),
  1098  		}), nil)
  1099  	addCommand("refresh", shortRefreshHelp, longRefreshHelp, func() flags.Commander { return &cmdRefresh{} },
  1100  		colorDescs.also(waitDescs).also(channelDescs).also(modeDescs).also(timeDescs).also(map[string]string{
  1101  			// TRANSLATORS: This should not start with a lowercase letter.
  1102  			"amend": i18n.G("Allow refresh attempt on snap unknown to the store"),
  1103  			// TRANSLATORS: This should not start with a lowercase letter.
  1104  			"revision": i18n.G("Refresh to the given revision, to which you must have developer access"),
  1105  			// TRANSLATORS: This should not start with a lowercase letter.
  1106  			"list": i18n.G("Show the new versions of snaps that would be updated with the next refresh"),
  1107  			// TRANSLATORS: This should not start with a lowercase letter.
  1108  			"time": i18n.G("Show auto refresh information but do not perform a refresh"),
  1109  			// TRANSLATORS: This should not start with a lowercase letter.
  1110  			"ignore-validation": i18n.G("Ignore validation by other snaps blocking the refresh"),
  1111  			// TRANSLATORS: This should not start with a lowercase letter.
  1112  			"cohort": i18n.G("Refresh the snap into the given cohort"),
  1113  			// TRANSLATORS: This should not start with a lowercase letter.
  1114  			"leave-cohort": i18n.G("Refresh the snap out of its cohort"),
  1115  		}), nil)
  1116  	addCommand("try", shortTryHelp, longTryHelp, func() flags.Commander { return &cmdTry{} }, waitDescs.also(modeDescs), nil)
  1117  	addCommand("enable", shortEnableHelp, longEnableHelp, func() flags.Commander { return &cmdEnable{} }, waitDescs, nil)
  1118  	addCommand("disable", shortDisableHelp, longDisableHelp, func() flags.Commander { return &cmdDisable{} }, waitDescs, nil)
  1119  	addCommand("revert", shortRevertHelp, longRevertHelp, func() flags.Commander { return &cmdRevert{} }, waitDescs.also(modeDescs).also(map[string]string{
  1120  		// TRANSLATORS: This should not start with a lowercase letter.
  1121  		"revision": i18n.G("Revert to the given revision"),
  1122  	}), nil)
  1123  	addCommand("switch", shortSwitchHelp, longSwitchHelp, func() flags.Commander { return &cmdSwitch{} }, waitDescs.also(channelDescs).also(map[string]string{
  1124  		// TRANSLATORS: This should not start with a lowercase letter.
  1125  		"cohort": i18n.G("Switch the snap into the given cohort"),
  1126  		// TRANSLATORS: This should not start with a lowercase letter.
  1127  		"leave-cohort": i18n.G("Switch the snap out of its cohort"),
  1128  	}), nil)
  1129  }