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