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 }