github.com/tompreston/snapd@v0.0.0-20210817193607-954edfcb9611/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 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 that 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 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 IgnoreRunning bool `long:"ignore-running" hidden:"yes"` 479 Positional struct { 480 Snaps []remoteSnapName `positional-arg-name:"<snap>"` 481 } `positional-args:"yes" required:"yes"` 482 } 483 484 func (x *cmdInstall) installOne(nameOrPath, desiredName string, opts *client.SnapOptions) error { 485 var err error 486 var changeID string 487 var snapName string 488 var path string 489 490 if strings.Contains(nameOrPath, "/") || strings.HasSuffix(nameOrPath, ".snap") || strings.Contains(nameOrPath, ".snap.") { 491 path = nameOrPath 492 changeID, err = x.client.InstallPath(path, x.Name, opts) 493 } else { 494 snapName = nameOrPath 495 if desiredName != "" { 496 return errors.New(i18n.G("cannot use explicit name when installing from store")) 497 } 498 changeID, err = x.client.Install(snapName, opts) 499 } 500 if err != nil { 501 msg, err := errorToCmdMessage(nameOrPath, err, opts) 502 if err != nil { 503 return err 504 } 505 fmt.Fprintln(Stderr, msg) 506 return nil 507 } 508 509 chg, err := x.wait(changeID) 510 if err != nil { 511 if err == noWait { 512 return nil 513 } 514 return err 515 } 516 517 // extract the snapName from the change, important for sideloaded 518 if path != "" { 519 if err := chg.Get("snap-name", &snapName); err != nil { 520 return fmt.Errorf("cannot extract the snap-name from local file %q: %s", nameOrPath, err) 521 } 522 } 523 524 // TODO: mention details of the install (e.g. like switch does) 525 return showDone(x.client, []string{snapName}, "install", opts, x.getEscapes()) 526 } 527 528 func (x *cmdInstall) installMany(names []string, opts *client.SnapOptions) error { 529 // sanity check 530 for _, name := range names { 531 if strings.Contains(name, "/") || strings.HasSuffix(name, ".snap") || strings.Contains(name, ".snap.") { 532 return fmt.Errorf("only one snap file can be installed at a time") 533 } 534 } 535 536 changeID, err := x.client.InstallMany(names, opts) 537 if err != nil { 538 var snapName string 539 if err, ok := err.(*client.Error); ok { 540 snapName, _ = err.Value.(string) 541 } 542 msg, err := errorToCmdMessage(snapName, err, opts) 543 if err != nil { 544 return err 545 } 546 fmt.Fprintln(Stderr, msg) 547 return nil 548 } 549 550 chg, err := x.wait(changeID) 551 if err != nil { 552 if err == noWait { 553 return nil 554 } 555 return err 556 } 557 558 var installed []string 559 if err := chg.Get("snap-names", &installed); err != nil && err != client.ErrNoData { 560 return err 561 } 562 563 if len(installed) > 0 { 564 if err := showDone(x.client, installed, "install", opts, x.getEscapes()); err != nil { 565 return err 566 } 567 } 568 569 // show skipped 570 seen := make(map[string]bool) 571 for _, name := range installed { 572 seen[name] = true 573 } 574 for _, name := range names { 575 if !seen[name] { 576 // FIXME: this is the only reason why a name can be 577 // skipped, but it does feel awkward 578 fmt.Fprintf(Stdout, i18n.G("%s already installed\n"), name) 579 } 580 } 581 582 return nil 583 } 584 585 func (x *cmdInstall) Execute([]string) error { 586 if err := x.setChannelFromCommandline(); err != nil { 587 return err 588 } 589 if err := x.validateMode(); err != nil { 590 return err 591 } 592 593 dangerous := x.Dangerous || x.ForceDangerous 594 opts := &client.SnapOptions{ 595 Channel: x.Channel, 596 Revision: x.Revision, 597 Dangerous: dangerous, 598 Unaliased: x.Unaliased, 599 CohortKey: x.Cohort, 600 IgnoreRunning: x.IgnoreRunning, 601 } 602 x.setModes(opts) 603 604 names := remoteSnapNames(x.Positional.Snaps) 605 if len(names) == 0 { 606 return errors.New(i18n.G("cannot install zero snaps")) 607 } 608 for _, name := range names { 609 if len(name) == 0 { 610 return errors.New(i18n.G("cannot install snap with empty name")) 611 } 612 } 613 614 if len(names) == 1 { 615 return x.installOne(names[0], x.Name, opts) 616 } 617 618 if x.asksForMode() || x.asksForChannel() { 619 return errors.New(i18n.G("a single snap name is needed to specify mode or channel flags")) 620 } 621 622 if x.Name != "" { 623 return errors.New(i18n.G("cannot use instance name when installing multiple snaps")) 624 } 625 return x.installMany(names, nil) 626 } 627 628 type cmdRefresh struct { 629 colorMixin 630 timeMixin 631 waitMixin 632 channelMixin 633 modeMixin 634 635 Amend bool `long:"amend"` 636 Revision string `long:"revision"` 637 Cohort string `long:"cohort"` 638 LeaveCohort bool `long:"leave-cohort"` 639 List bool `long:"list"` 640 Time bool `long:"time"` 641 IgnoreValidation bool `long:"ignore-validation"` 642 IgnoreRunning bool `long:"ignore-running" hidden:"yes"` 643 Positional struct { 644 Snaps []installedSnapName `positional-arg-name:"<snap>"` 645 } `positional-args:"yes"` 646 } 647 648 func (x *cmdRefresh) refreshMany(snaps []string, opts *client.SnapOptions) error { 649 changeID, err := x.client.RefreshMany(snaps, opts) 650 if err != nil { 651 return err 652 } 653 654 chg, err := x.wait(changeID) 655 if err != nil { 656 if err == noWait { 657 return nil 658 } 659 return err 660 } 661 662 var refreshed []string 663 if err := chg.Get("snap-names", &refreshed); err != nil && err != client.ErrNoData { 664 return err 665 } 666 667 if len(refreshed) > 0 { 668 return showDone(x.client, refreshed, "refresh", opts, x.getEscapes()) 669 } 670 671 fmt.Fprintln(Stderr, i18n.G("All snaps up to date.")) 672 673 return nil 674 } 675 676 func (x *cmdRefresh) refreshOne(name string, opts *client.SnapOptions) error { 677 changeID, err := x.client.Refresh(name, opts) 678 if err != nil { 679 msg, err := errorToCmdMessage(name, err, opts) 680 if err != nil { 681 return err 682 } 683 fmt.Fprintln(Stderr, msg) 684 return nil 685 } 686 687 if _, err := x.wait(changeID); err != nil { 688 if err == noWait { 689 return nil 690 } 691 return err 692 } 693 694 // TODO: this doesn't really tell about all the things you 695 // could set while refreshing (something switch does) 696 return showDone(x.client, []string{name}, "refresh", opts, x.getEscapes()) 697 } 698 699 func parseSysinfoTime(s string) time.Time { 700 t, err := time.Parse(time.RFC3339, s) 701 if err != nil { 702 return time.Time{} 703 } 704 return t 705 } 706 707 func (x *cmdRefresh) showRefreshTimes() error { 708 sysinfo, err := x.client.SysInfo() 709 if err != nil { 710 return err 711 } 712 713 if sysinfo.Refresh.Timer != "" { 714 fmt.Fprintf(Stdout, "timer: %s\n", sysinfo.Refresh.Timer) 715 } else if sysinfo.Refresh.Schedule != "" { 716 fmt.Fprintf(Stdout, "schedule: %s\n", sysinfo.Refresh.Schedule) 717 } else { 718 return errors.New("internal error: both refresh.timer and refresh.schedule are empty") 719 } 720 last := parseSysinfoTime(sysinfo.Refresh.Last) 721 hold := parseSysinfoTime(sysinfo.Refresh.Hold) 722 next := parseSysinfoTime(sysinfo.Refresh.Next) 723 724 if !last.IsZero() { 725 fmt.Fprintf(Stdout, "last: %s\n", x.fmtTime(last)) 726 } else { 727 fmt.Fprintf(Stdout, "last: n/a\n") 728 } 729 if !hold.IsZero() { 730 fmt.Fprintf(Stdout, "hold: %s\n", x.fmtTime(hold)) 731 } 732 // only show "next" if its after "hold" to not confuse users 733 if !next.IsZero() { 734 // Snapstate checks for holdTime.After(limitTime) so we need 735 // to check for before or equal here to be fully correct. 736 if next.Before(hold) || next.Equal(hold) { 737 fmt.Fprintf(Stdout, "next: %s (but held)\n", x.fmtTime(next)) 738 } else { 739 fmt.Fprintf(Stdout, "next: %s\n", x.fmtTime(next)) 740 } 741 } else { 742 fmt.Fprintf(Stdout, "next: n/a\n") 743 } 744 return nil 745 } 746 747 func (x *cmdRefresh) listRefresh() error { 748 snaps, _, err := x.client.Find(&client.FindOptions{ 749 Refresh: true, 750 }) 751 if err != nil { 752 return err 753 } 754 if len(snaps) == 0 { 755 fmt.Fprintln(Stderr, i18n.G("All snaps up to date.")) 756 return nil 757 } 758 759 sort.Sort(snapsByName(snaps)) 760 761 esc := x.getEscapes() 762 w := tabWriter() 763 defer w.Flush() 764 765 // TRANSLATORS: the %s is to insert a filler escape sequence (please keep it flush to the column header, with no extra spaces) 766 fmt.Fprintf(w, i18n.G("Name\tVersion\tRev\tPublisher%s\tNotes\n"), fillerPublisher(esc)) 767 for _, snap := range snaps { 768 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)) 769 } 770 771 return nil 772 } 773 774 func (x *cmdRefresh) Execute([]string) error { 775 if err := x.setChannelFromCommandline(); err != nil { 776 return err 777 } 778 if err := x.validateMode(); err != nil { 779 return err 780 } 781 782 if x.Time { 783 if x.asksForMode() || x.asksForChannel() { 784 return errors.New(i18n.G("--time does not take mode or channel flags")) 785 } 786 return x.showRefreshTimes() 787 } 788 789 if x.List { 790 if len(x.Positional.Snaps) > 0 || x.asksForMode() || x.asksForChannel() { 791 return errors.New(i18n.G("--list does not accept additional arguments")) 792 } 793 794 return x.listRefresh() 795 } 796 797 if len(x.Positional.Snaps) == 0 && os.Getenv("SNAP_REFRESH_FROM_TIMER") == "1" { 798 fmt.Fprintf(Stdout, "Ignoring `snap refresh` from the systemd timer") 799 return nil 800 } 801 802 names := installedSnapNames(x.Positional.Snaps) 803 if len(names) == 1 { 804 opts := &client.SnapOptions{ 805 Amend: x.Amend, 806 Channel: x.Channel, 807 IgnoreValidation: x.IgnoreValidation, 808 IgnoreRunning: x.IgnoreRunning, 809 Revision: x.Revision, 810 CohortKey: x.Cohort, 811 LeaveCohort: x.LeaveCohort, 812 } 813 x.setModes(opts) 814 return x.refreshOne(names[0], opts) 815 } 816 817 if x.asksForMode() || x.asksForChannel() { 818 return errors.New(i18n.G("a single snap name is needed to specify mode or channel flags")) 819 } 820 821 if x.IgnoreValidation { 822 return errors.New(i18n.G("a single snap name must be specified when ignoring validation")) 823 } 824 if x.IgnoreRunning { 825 return errors.New(i18n.G("a single snap name must be specified when ignoring running apps and hooks")) 826 } 827 828 return x.refreshMany(names, nil) 829 } 830 831 type cmdTry struct { 832 waitMixin 833 834 modeMixin 835 Positional struct { 836 SnapDir string `positional-arg-name:"<snap-dir>"` 837 } `positional-args:"yes"` 838 } 839 840 func hasSnapcraftYaml() bool { 841 for _, loc := range []string{ 842 "snap/snapcraft.yaml", 843 "snapcraft.yaml", 844 ".snapcraft.yaml", 845 } { 846 if osutil.FileExists(loc) { 847 return true 848 } 849 } 850 851 return false 852 } 853 854 func (x *cmdTry) Execute([]string) error { 855 if err := x.validateMode(); err != nil { 856 return err 857 } 858 name := x.Positional.SnapDir 859 opts := &client.SnapOptions{} 860 x.setModes(opts) 861 862 if name == "" { 863 if hasSnapcraftYaml() && osutil.IsDirectory("prime") { 864 name = "prime" 865 } else { 866 if osutil.FileExists("meta/snap.yaml") { 867 name = "./" 868 } 869 } 870 if name == "" { 871 return fmt.Errorf(i18n.G("error: the `<snap-dir>` argument was not provided and couldn't be inferred")) 872 } 873 } 874 875 path, err := filepath.Abs(name) 876 if err != nil { 877 // TRANSLATORS: %q gets what the user entered, %v gets the resulting error message 878 return fmt.Errorf(i18n.G("cannot get full path for %q: %v"), name, err) 879 } 880 881 changeID, err := x.client.Try(path, opts) 882 if err != nil { 883 msg, err := errorToCmdMessage(name, err, opts) 884 if err != nil { 885 return err 886 } 887 fmt.Fprintln(Stderr, msg) 888 return nil 889 } 890 891 chg, err := x.wait(changeID) 892 if err != nil { 893 if err == noWait { 894 return nil 895 } 896 return err 897 } 898 899 // extract the snap name 900 var snapName string 901 if err := chg.Get("snap-name", &snapName); err != nil { 902 // TRANSLATORS: %q gets the snap name, %v gets the resulting error message 903 return fmt.Errorf(i18n.G("cannot extract the snap-name from local file %q: %v"), name, err) 904 } 905 name = snapName 906 907 // show output as speced 908 snaps, err := x.client.List([]string{name}, nil) 909 if err != nil { 910 return err 911 } 912 if len(snaps) != 1 { 913 // TRANSLATORS: %q gets the snap name, %v the list of things found when trying to list it 914 return fmt.Errorf(i18n.G("cannot get data for %q: %v"), name, snaps) 915 } 916 snap := snaps[0] 917 // TRANSLATORS: 1. snap name, 2. snap version (keep those together please). the 3rd %s is a path (where it's mounted from). 918 fmt.Fprintf(Stdout, i18n.G("%s %s mounted from %s\n"), name, snap.Version, path) 919 return nil 920 } 921 922 type cmdEnable struct { 923 waitMixin 924 925 Positional struct { 926 Snap installedSnapName `positional-arg-name:"<snap>"` 927 } `positional-args:"yes" required:"yes"` 928 } 929 930 func (x *cmdEnable) Execute([]string) error { 931 name := string(x.Positional.Snap) 932 opts := &client.SnapOptions{} 933 changeID, err := x.client.Enable(name, opts) 934 if err != nil { 935 return err 936 } 937 938 if _, err := x.wait(changeID); err != nil { 939 if err == noWait { 940 return nil 941 } 942 return err 943 } 944 945 fmt.Fprintf(Stdout, i18n.G("%s enabled\n"), name) 946 return nil 947 } 948 949 type cmdDisable struct { 950 waitMixin 951 952 Positional struct { 953 Snap installedSnapName `positional-arg-name:"<snap>"` 954 } `positional-args:"yes" required:"yes"` 955 } 956 957 func (x *cmdDisable) Execute([]string) error { 958 name := string(x.Positional.Snap) 959 opts := &client.SnapOptions{} 960 changeID, err := x.client.Disable(name, opts) 961 if err != nil { 962 return err 963 } 964 965 if _, err := x.wait(changeID); err != nil { 966 if err == noWait { 967 return nil 968 } 969 return err 970 } 971 972 fmt.Fprintf(Stdout, i18n.G("%s disabled\n"), name) 973 return nil 974 } 975 976 type cmdRevert struct { 977 waitMixin 978 979 modeMixin 980 Revision string `long:"revision"` 981 IgnoreRunning bool `long:"ignore-running" hidden:"yes"` 982 Positional struct { 983 Snap installedSnapName `positional-arg-name:"<snap>"` 984 } `positional-args:"yes" required:"yes"` 985 } 986 987 var shortRevertHelp = i18n.G("Reverts the given snap to the previous state") 988 var longRevertHelp = i18n.G(` 989 The revert command reverts the given snap to its state before 990 the latest refresh. This will reactivate the previous snap revision, 991 and will use the original data that was associated with that revision, 992 discarding any data changes that were done by the latest revision. As 993 an exception, data which the snap explicitly chooses to share across 994 revisions is not touched by the revert process. 995 `) 996 997 func (x *cmdRevert) Execute(args []string) error { 998 if len(args) > 0 { 999 return ErrExtraArgs 1000 } 1001 1002 if err := x.validateMode(); err != nil { 1003 return err 1004 } 1005 1006 name := string(x.Positional.Snap) 1007 opts := &client.SnapOptions{ 1008 Revision: x.Revision, 1009 IgnoreRunning: x.IgnoreRunning, 1010 } 1011 x.setModes(opts) 1012 changeID, err := x.client.Revert(name, opts) 1013 if err != nil { 1014 return err 1015 } 1016 1017 if _, err := x.wait(changeID); err != nil { 1018 if err == noWait { 1019 return nil 1020 } 1021 return err 1022 } 1023 1024 return showDone(x.client, []string{name}, "revert", nil, nil) 1025 } 1026 1027 var shortSwitchHelp = i18n.G("Switches snap to a different channel") 1028 var longSwitchHelp = i18n.G(` 1029 The switch command switches the given snap to a different channel without 1030 doing a refresh. 1031 `) 1032 1033 type cmdSwitch struct { 1034 waitMixin 1035 channelMixin 1036 1037 Cohort string `long:"cohort"` 1038 LeaveCohort bool `long:"leave-cohort"` 1039 1040 Positional struct { 1041 Snap installedSnapName `positional-arg-name:"<snap>" required:"1"` 1042 } `positional-args:"yes" required:"yes"` 1043 } 1044 1045 func (x cmdSwitch) Execute(args []string) error { 1046 if err := x.setChannelFromCommandline(); err != nil { 1047 return err 1048 } 1049 1050 name := string(x.Positional.Snap) 1051 channel := string(x.Channel) 1052 1053 switchCohort := x.Cohort != "" 1054 switchChannel := x.Channel != "" 1055 1056 // we have three boolean things to check, meaning 2³=8 possibilities 1057 // of which 3 are errors (which is why we look at the errors first). 1058 // the 5 valid cases are handled by showDone. 1059 if switchCohort && x.LeaveCohort { 1060 // this one counts as two (no channel filter) 1061 return fmt.Errorf(i18n.G("cannot specify both --cohort and --leave-cohort")) 1062 } 1063 if !switchCohort && !x.LeaveCohort && !switchChannel { 1064 return fmt.Errorf(i18n.G("nothing to switch; specify --channel (and/or one of --cohort/--leave-cohort)")) 1065 } 1066 1067 opts := &client.SnapOptions{ 1068 Channel: channel, 1069 CohortKey: x.Cohort, 1070 LeaveCohort: x.LeaveCohort, 1071 } 1072 changeID, err := x.client.Switch(name, opts) 1073 if err != nil { 1074 return err 1075 } 1076 1077 if _, err := x.wait(changeID); err != nil { 1078 if err == noWait { 1079 return nil 1080 } 1081 return err 1082 } 1083 1084 return showDone(x.client, []string{name}, "switch", opts, nil) 1085 } 1086 1087 func init() { 1088 addCommand("remove", shortRemoveHelp, longRemoveHelp, func() flags.Commander { return &cmdRemove{} }, 1089 waitDescs.also(map[string]string{ 1090 // TRANSLATORS: This should not start with a lowercase letter. 1091 "revision": i18n.G("Remove only the given revision"), 1092 // TRANSLATORS: This should not start with a lowercase letter. 1093 "purge": i18n.G("Remove the snap without saving a snapshot of its data"), 1094 }), nil) 1095 addCommand("install", shortInstallHelp, longInstallHelp, func() flags.Commander { return &cmdInstall{} }, 1096 colorDescs.also(waitDescs).also(channelDescs).also(modeDescs).also(map[string]string{ 1097 // TRANSLATORS: This should not start with a lowercase letter. 1098 "revision": i18n.G("Install the given revision of a snap, to which you must have developer access"), 1099 // TRANSLATORS: This should not start with a lowercase letter. 1100 "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)"), 1101 // TRANSLATORS: This should not start with a lowercase letter. 1102 "force-dangerous": i18n.G("Alias for --dangerous (DEPRECATED)"), 1103 // TRANSLATORS: This should not start with a lowercase letter. 1104 "unaliased": i18n.G("Install the given snap without enabling its automatic aliases"), 1105 // TRANSLATORS: This should not start with a lowercase letter. 1106 "name": i18n.G("Install the snap file under the given instance name"), 1107 // TRANSLATORS: This should not start with a lowercase letter. 1108 "cohort": i18n.G("Install the snap in the given cohort"), 1109 // TRANSLATORS: This should not start with a lowercase letter. 1110 "ignore-running": i18n.G("Ignore running hooks or applications blocking the installation"), 1111 }), nil) 1112 addCommand("refresh", shortRefreshHelp, longRefreshHelp, func() flags.Commander { return &cmdRefresh{} }, 1113 colorDescs.also(waitDescs).also(channelDescs).also(modeDescs).also(timeDescs).also(map[string]string{ 1114 // TRANSLATORS: This should not start with a lowercase letter. 1115 "amend": i18n.G("Allow refresh attempt on snap unknown to the store"), 1116 // TRANSLATORS: This should not start with a lowercase letter. 1117 "revision": i18n.G("Refresh to the given revision, to which you must have developer access"), 1118 // TRANSLATORS: This should not start with a lowercase letter. 1119 "list": i18n.G("Show the new versions of snaps that would be updated with the next refresh"), 1120 // TRANSLATORS: This should not start with a lowercase letter. 1121 "time": i18n.G("Show auto refresh information but do not perform a refresh"), 1122 // TRANSLATORS: This should not start with a lowercase letter. 1123 "ignore-validation": i18n.G("Ignore validation by other snaps blocking the refresh"), 1124 // TRANSLATORS: This should not start with a lowercase letter. 1125 "ignore-running": i18n.G("Ignore running hooks or applications blocking the refresh"), 1126 // TRANSLATORS: This should not start with a lowercase letter. 1127 "cohort": i18n.G("Refresh the snap into the given cohort"), 1128 // TRANSLATORS: This should not start with a lowercase letter. 1129 "leave-cohort": i18n.G("Refresh the snap out of its cohort"), 1130 }), nil) 1131 addCommand("try", shortTryHelp, longTryHelp, func() flags.Commander { return &cmdTry{} }, waitDescs.also(modeDescs), nil) 1132 addCommand("enable", shortEnableHelp, longEnableHelp, func() flags.Commander { return &cmdEnable{} }, waitDescs, nil) 1133 addCommand("disable", shortDisableHelp, longDisableHelp, func() flags.Commander { return &cmdDisable{} }, waitDescs, nil) 1134 addCommand("revert", shortRevertHelp, longRevertHelp, func() flags.Commander { return &cmdRevert{} }, waitDescs.also(modeDescs).also(map[string]string{ 1135 // TRANSLATORS: This should not start with a lowercase letter. 1136 "revision": i18n.G("Revert to the given revision"), 1137 // TRANSLATORS: This should not start with a lowercase letter. 1138 "ignore-running": i18n.G("Ignore running hooks or applications blocking the revert"), 1139 }), nil) 1140 addCommand("switch", shortSwitchHelp, longSwitchHelp, func() flags.Commander { return &cmdSwitch{} }, waitDescs.also(channelDescs).also(map[string]string{ 1141 // TRANSLATORS: This should not start with a lowercase letter. 1142 "cohort": i18n.G("Switch the snap into the given cohort"), 1143 // TRANSLATORS: This should not start with a lowercase letter. 1144 "leave-cohort": i18n.G("Switch the snap out of its cohort"), 1145 }), nil) 1146 }