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 }