diff -Nru snapd-2.32.3.2~14.04/advisor/backend.go snapd-2.37~rc1~14.04/advisor/backend.go --- snapd-2.32.3.2~14.04/advisor/backend.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/advisor/backend.go 2019-01-16 08:36:51.000000000 +0000 @@ -20,14 +20,16 @@ package advisor import ( + "encoding/json" "os" - "strings" + "path/filepath" "time" "github.com/snapcore/bolt" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/strutil" ) var ( @@ -36,6 +38,7 @@ ) type writer struct { + fn string db *bolt.DB tx *bolt.Tx cmdBucket *bolt.Bucket @@ -45,7 +48,7 @@ type CommandDB interface { // AddSnap adds the entries for commands pointing to the given // snap name to the commands database. - AddSnap(snapName, summary string, commands []string) error + AddSnap(snapName, version, summary string, commands []string) error // Commit persist the changes, and closes the database. If the // database has already been committed/rollbacked, does nothing. Commit() error @@ -61,32 +64,24 @@ // these closes the database again. func Create() (CommandDB, error) { var err error - t := &writer{} + t := &writer{ + fn: dirs.SnapCommandsDB + "." + strutil.MakeRandomString(12) + "~", + } - t.db, err = bolt.Open(dirs.SnapCommandsDB, 0644, &bolt.Options{Timeout: 1 * time.Second}) + t.db, err = bolt.Open(t.fn, 0644, &bolt.Options{Timeout: 1 * time.Second}) if err != nil { return nil, err } t.tx, err = t.db.Begin(true) if err == nil { - err := t.tx.DeleteBucket(cmdBucketKey) - if err == nil || err == bolt.ErrBucketNotFound { - t.cmdBucket, err = t.tx.CreateBucket(cmdBucketKey) + t.cmdBucket, err = t.tx.CreateBucket(cmdBucketKey) + if err == nil { + t.pkgBucket, err = t.tx.CreateBucket(pkgBucketKey) } + if err != nil { t.tx.Rollback() - - } - - if err == nil { - err := t.tx.DeleteBucket(pkgBucketKey) - if err == nil || err == bolt.ErrBucketNotFound { - t.pkgBucket, err = t.tx.CreateBucket(pkgBucketKey) - } - if err != nil { - t.tx.Rollback() - } } } @@ -98,23 +93,38 @@ return t, nil } -func (t *writer) AddSnap(snapName, summary string, commands []string) error { - bname := []byte(snapName) - +func (t *writer) AddSnap(snapName, version, summary string, commands []string) error { for _, cmd := range commands { + var sil []Package + bcmd := []byte(cmd) row := t.cmdBucket.Get(bcmd) - if row == nil { - row = bname - } else { - row = append(append(row, ','), bname...) + if row != nil { + if err := json.Unmarshal(row, &sil); err != nil { + return err + } + } + // For the mapping of command->snap we do not need the summary, nothing is using that. + sil = append(sil, Package{Snap: snapName, Version: version}) + row, err := json.Marshal(sil) + if err != nil { + return err } if err := t.cmdBucket.Put(bcmd, row); err != nil { return err } } - if err := t.pkgBucket.Put([]byte(snapName), []byte(summary)); err != nil { + // TODO: use json here as well and put the version information here + bj, err := json.Marshal(Package{ + Snap: snapName, + Version: version, + Summary: summary, + }) + if err != nil { + return err + } + if err := t.pkgBucket.Put([]byte(snapName), bj); err != nil { return err } @@ -122,11 +132,35 @@ } func (t *writer) Commit() error { - return t.done(true) + // either everything worked, and therefore this will fail, or something + // will fail, and that error is more important than this one if this one + // then fails as well. So, ignore the error. + defer os.Remove(t.fn) + + if err := t.done(true); err != nil { + return err + } + + dir, err := os.Open(filepath.Dir(dirs.SnapCommandsDB)) + if err != nil { + return err + } + defer dir.Close() + + if err := os.Rename(t.fn, dirs.SnapCommandsDB); err != nil { + return err + } + + return dir.Sync() } func (t *writer) Rollback() error { - return t.done(false) + e1 := t.done(false) + e2 := os.Remove(t.fn) + if e1 == nil { + return e2 + } + return e1 } func (t *writer) done(commit bool) error { @@ -154,7 +188,7 @@ // DumpCommands returns the whole database as a map. For use in // testing and debugging. -func DumpCommands() (map[string][]string, error) { +func DumpCommands() (map[string]string, error) { db, err := bolt.Open(dirs.SnapCommandsDB, 0644, &bolt.Options{ ReadOnly: true, Timeout: 1 * time.Second, @@ -175,10 +209,10 @@ return nil, nil } - m := map[string][]string{} + m := map[string]string{} c := b.Cursor() for k, v := c.First(); k != nil; k, v = c.Next() { - m[string(k)] = strings.Split(string(v), ",") + m[string(k)] = string(v) } return m, nil @@ -190,6 +224,10 @@ // Open the database for reading. func Open() (Finder, error) { + // Check for missing file manually to workaround bug in bolt. + // bolt.Open() is using os.OpenFile(.., os.O_RDONLY | + // os.O_CREATE) even if ReadOnly mode is used. So we would get + // a misleading "permission denied" error without this check. if !osutil.FileExists(dirs.SnapCommandsDB) { return nil, os.ErrNotExist } @@ -220,12 +258,15 @@ if buf == nil { return nil, nil } - - snaps := strings.Split(string(buf), ",") - cmds := make([]Command, len(snaps)) - for i, snap := range snaps { + var sil []Package + if err := json.Unmarshal(buf, &sil); err != nil { + return nil, err + } + cmds := make([]Command, len(sil)) + for i, si := range sil { cmds[i] = Command{ - Snap: snap, + Snap: si.Snap, + Version: si.Version, Command: command, } } @@ -245,10 +286,15 @@ return nil, nil } - bsummary := b.Get([]byte(pkgName)) - if bsummary == nil { + bj := b.Get([]byte(pkgName)) + if bj == nil { return nil, nil } + var si Package + err = json.Unmarshal(bj, &si) + if err != nil { + return nil, err + } - return &Package{Snap: pkgName, Summary: string(bsummary)}, nil + return &Package{Snap: pkgName, Version: si.Version, Summary: si.Summary}, nil } diff -Nru snapd-2.32.3.2~14.04/advisor/cmdfinder.go snapd-2.37~rc1~14.04/advisor/cmdfinder.go --- snapd-2.32.3.2~14.04/advisor/cmdfinder.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/advisor/cmdfinder.go 2019-01-16 08:36:51.000000000 +0000 @@ -25,6 +25,7 @@ type Command struct { Snap string + Version string `json:"Version,omitempty"` Command string } diff -Nru snapd-2.32.3.2~14.04/advisor/cmdfinder_test.go snapd-2.37~rc1~14.04/advisor/cmdfinder_test.go --- snapd-2.32.3.2~14.04/advisor/cmdfinder_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/advisor/cmdfinder_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -44,8 +44,8 @@ db, err := advisor.Create() c.Assert(err, IsNil) - c.Assert(db.AddSnap("foo", "foo summary", []string{"foo", "meh"}), IsNil) - c.Assert(db.AddSnap("bar", "bar summary", []string{"bar", "meh"}), IsNil) + c.Assert(db.AddSnap("foo", "1.0", "foo summary", []string{"foo", "meh"}), IsNil) + c.Assert(db.AddSnap("bar", "2.0", "bar summary", []string{"bar", "meh"}), IsNil) c.Assert(db.Commit(), IsNil) } @@ -99,8 +99,8 @@ cmds, err := advisor.FindCommand("meh") c.Assert(err, IsNil) c.Check(cmds, DeepEquals, []advisor.Command{ - {Snap: "foo", Command: "meh"}, - {Snap: "bar", Command: "meh"}, + {Snap: "foo", Version: "1.0", Command: "meh"}, + {Snap: "bar", Version: "2.0", Command: "meh"}, }) } @@ -114,8 +114,8 @@ cmds, err := advisor.FindMisspelledCommand("moh") c.Assert(err, IsNil) c.Check(cmds, DeepEquals, []advisor.Command{ - {Snap: "foo", Command: "meh"}, - {Snap: "bar", Command: "meh"}, + {Snap: "foo", Version: "1.0", Command: "meh"}, + {Snap: "bar", Version: "2.0", Command: "meh"}, }) } @@ -128,10 +128,10 @@ func (s *cmdfinderSuite) TestDumpCommands(c *C) { cmds, err := advisor.DumpCommands() c.Assert(err, IsNil) - c.Check(cmds, DeepEquals, map[string][]string{ - "foo": {"foo"}, - "bar": {"bar"}, - "meh": {"foo", "bar"}, + c.Check(cmds, DeepEquals, map[string]string{ + "foo": `[{"snap":"foo","version":"1.0"}]`, + "bar": `[{"snap":"bar","version":"2.0"}]`, + "meh": `[{"snap":"foo","version":"1.0"},{"snap":"bar","version":"2.0"}]`, }) } diff -Nru snapd-2.32.3.2~14.04/advisor/pkgfinder.go snapd-2.37~rc1~14.04/advisor/pkgfinder.go --- snapd-2.32.3.2~14.04/advisor/pkgfinder.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/advisor/pkgfinder.go 2019-01-16 08:36:51.000000000 +0000 @@ -24,8 +24,9 @@ ) type Package struct { - Snap string - Summary string + Snap string `json:"snap"` + Version string `json:"version"` + Summary string `json:"summary,omitempty"` } func FindPackage(pkgName string) (*Package, error) { diff -Nru snapd-2.32.3.2~14.04/advisor/pkgfinder_test.go snapd-2.37~rc1~14.04/advisor/pkgfinder_test.go --- snapd-2.32.3.2~14.04/advisor/pkgfinder_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/advisor/pkgfinder_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,40 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package advisor_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/advisor" +) + +func (s *cmdfinderSuite) TestFindPackageHit(c *C) { + pkg, err := advisor.FindPackage("foo") + c.Assert(err, IsNil) + c.Check(pkg, DeepEquals, &advisor.Package{ + Snap: "foo", Version: "1.0", Summary: "foo summary", + }) +} + +func (s *cmdfinderSuite) TestFindPackageMiss(c *C) { + pkg, err := advisor.FindPackage("moh") + c.Assert(err, IsNil) + c.Check(pkg, IsNil) +} diff -Nru snapd-2.32.3.2~14.04/arch/arch.go snapd-2.37~rc1~14.04/arch/arch.go --- snapd-2.32.3.2~14.04/arch/arch.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/arch/arch.go 2019-01-16 08:36:51.000000000 +0000 @@ -22,9 +22,8 @@ import ( "log" "runtime" - "syscall" - "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/osutil" ) // ArchitectureType is the type for a supported snappy architecture @@ -78,8 +77,7 @@ // arch mapping. The Go arch sadly doesn't map this out // for us so we have to fallback to uname here. if goarch == "arm" { - machineName := release.Machine() - if machineName == "armv6l" { + if osutil.MachineName() == "armv6l" { return "armel" } } @@ -97,24 +95,7 @@ // UbuntuArchitecture - however there maybe cases that you run e.g. // a snapd:i386 on an amd64 kernel. func UbuntuKernelArchitecture() string { - var utsname syscall.Utsname - if err := syscall.Uname(&utsname); err != nil { - log.Panicf("cannot get kernel architecture: %v", err) - } - - // syscall.Utsname{} is using [65]int8 for all char[] inside it, - // this makes converting it so awkward. The alternative would be - // to use a unsafe.Pointer() to cast it to a [65]byte slice. - // see https://github.com/golang/go/issues/20753 - kernelArch := make([]byte, 0, len(utsname.Machine)) - for _, c := range utsname.Machine { - if c == 0 { - break - } - kernelArch = append(kernelArch, byte(c)) - } - - return ubuntuArchFromKernelArch(string(kernelArch)) + return ubuntuArchFromKernelArch(osutil.MachineName()) } // ubuntuArchFromkernelArch maps the kernel architecture as reported diff -Nru snapd-2.32.3.2~14.04/asserts/account.go snapd-2.37~rc1~14.04/asserts/account.go --- snapd-2.32.3.2~14.04/asserts/account.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/account.go 2019-01-16 08:36:51.000000000 +0000 @@ -26,8 +26,6 @@ ) var ( - accountValidationCertified = "certified" - // account ids look like snap-ids or a nice identifier validAccountID = regexp.MustCompile("^(?:[a-z0-9A-Z]{32}|[-a-z0-9]{2,28})$") ) @@ -36,8 +34,8 @@ // to its identifier and provides the authority's confidence in the name's validity. type Account struct { assertionBase - certified bool - timestamp time.Time + validation string + timestamp time.Time } // AccountID returns the account-id of the account. @@ -55,9 +53,12 @@ return acc.HeaderString("display-name") } -// IsCertified returns true if the authority has confidence in the account's name. -func (acc *Account) IsCertified() bool { - return acc.certified +// Validation returns the level of confidence of the authority in the +// account's identity, expected to be "unproven" or "verified", and +// for forward compatibility any value != "unproven" can be considered +// at least "verified". +func (acc *Account) Validation() string { + return acc.validation } // Timestamp returns the time when the account was issued. @@ -82,11 +83,17 @@ return nil, err } - _, err = checkNotEmptyString(assert.headers, "validation") + validation, err := checkNotEmptyString(assert.headers, "validation") if err != nil { return nil, err } - certified := assert.headers["validation"] == accountValidationCertified + // backward compatibility with the hard-coded trusted account + // assertions + // TODO: generate revision 1 of them with validation + // s/certified/verified/ + if validation == "certified" { + validation = "verified" + } timestamp, err := checkRFC3339Date(assert.headers, "timestamp") if err != nil { @@ -100,7 +107,7 @@ return &Account{ assertionBase: assert, - certified: certified, + validation: validation, timestamp: timestamp, }, nil } diff -Nru snapd-2.32.3.2~14.04/asserts/account_test.go snapd-2.37~rc1~14.04/asserts/account_test.go --- snapd-2.32.3.2~14.04/asserts/account_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/account_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -47,7 +47,7 @@ "account-id: abc-123\n" + "display-name: Nice User\n" + "username: nice\n" + - "validation: certified\n" + + "validation: verified\n" + "TSLINE" + "body-length: 0\n" + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + @@ -65,7 +65,7 @@ c.Check(account.AccountID(), Equals, "abc-123") c.Check(account.DisplayName(), Equals, "Nice User") c.Check(account.Username(), Equals, "nice") - c.Check(account.IsCertified(), Equals, true) + c.Check(account.Validation(), Equals, "verified") } func (s *accountSuite) TestOptional(c *C) { @@ -83,12 +83,13 @@ } } -func (s *accountSuite) TestIsCertified(c *C) { +func (s *accountSuite) TestValidation(c *C) { tests := []struct { - value string - isCertified bool + value string + isVerified bool }{ - {"certified", true}, + {"certified", true}, // backward compat for hard-coded trusted assertions + {"verified", true}, {"unproven", false}, {"nonsense", false}, } @@ -97,14 +98,18 @@ for _, test := range tests { encoded := strings.Replace( template, - "validation: certified\n", + "validation: verified\n", fmt.Sprintf("validation: %s\n", test.value), 1, ) assert, err := asserts.Decode([]byte(encoded)) c.Assert(err, IsNil) account := assert.(*asserts.Account) - c.Check(account.IsCertified(), Equals, test.isCertified) + expected := test.value + if test.isVerified { + expected = "verified" + } + c.Check(account.Validation(), Equals, expected) } } @@ -121,8 +126,8 @@ {"display-name: Nice User\n", "", `"display-name" header is mandatory`}, {"display-name: Nice User\n", "display-name: \n", `"display-name" header should not be empty`}, {"username: nice\n", "username:\n - foo\n - bar\n", `"username" header must be a string`}, - {"validation: certified\n", "", `"validation" header is mandatory`}, - {"validation: certified\n", "validation: \n", `"validation" header should not be empty`}, + {"validation: verified\n", "", `"validation" header is mandatory`}, + {"validation: verified\n", "validation: \n", `"validation" header should not be empty`}, {s.tsLine, "", `"timestamp" header is mandatory`}, {s.tsLine, "timestamp: \n", `"timestamp" header should not be empty`}, {s.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, diff -Nru snapd-2.32.3.2~14.04/asserts/asserts.go snapd-2.37~rc1~14.04/asserts/asserts.go --- snapd-2.32.3.2~14.04/asserts/asserts.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/asserts.go 2019-01-16 08:36:51.000000000 +0000 @@ -124,7 +124,8 @@ // 1: plugs and slots // 2: support for $SLOT()/$PLUG()/$MISSING - maxSupportedFormat[SnapDeclarationType.Name] = 2 + // 3: support for on-store/on-brand/on-model device scope constraints + maxSupportedFormat[SnapDeclarationType.Name] = 3 } func MockMaxSupportedFormat(assertType *AssertionType, maxFormat int) (restore func()) { diff -Nru snapd-2.32.3.2~14.04/asserts/assertstest/assertstest.go snapd-2.37~rc1~14.04/asserts/assertstest/assertstest.go --- snapd-2.32.3.2~14.04/asserts/assertstest/assertstest.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/assertstest/assertstest.go 2019-01-16 08:36:51.000000000 +0000 @@ -313,7 +313,7 @@ ts := time.Now().Format(time.RFC3339) trustedAcct := NewAccount(rootSigning, authorityID, map[string]interface{}{ "account-id": authorityID, - "validation": "certified", + "validation": "verified", "timestamp": ts, }, "") trustedKey := NewAccountKey(rootSigning, trustedAcct, map[string]interface{}{ @@ -324,7 +324,7 @@ genericAcct := NewAccount(rootSigning, "generic", map[string]interface{}{ "account-id": "generic", - "validation": "certified", + "validation": "verified", "timestamp": ts, }, "") diff -Nru snapd-2.32.3.2~14.04/asserts/assertstest/assertstest_test.go snapd-2.37~rc1~14.04/asserts/assertstest/assertstest_test.go --- snapd-2.32.3.2~14.04/asserts/assertstest/assertstest_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/assertstest/assertstest_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -78,13 +78,13 @@ store := assertstest.NewStoreStack("super", nil) c.Check(store.TrustedAccount.AccountID(), Equals, "super") - c.Check(store.TrustedAccount.IsCertified(), Equals, true) + c.Check(store.TrustedAccount.Validation(), Equals, "verified") c.Check(store.TrustedKey.AccountID(), Equals, "super") c.Check(store.TrustedKey.Name(), Equals, "root") c.Check(store.GenericAccount.AccountID(), Equals, "generic") - c.Check(store.GenericAccount.IsCertified(), Equals, true) + c.Check(store.GenericAccount.Validation(), Equals, "verified") db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ Backstore: asserts.NewMemoryBackstore(), @@ -128,7 +128,7 @@ acct := assertstest.NewAccount(store, "devel1", nil, "") c.Check(acct.Username(), Equals, "devel1") c.Check(acct.AccountID(), HasLen, 32) - c.Check(acct.IsCertified(), Equals, false) + c.Check(acct.Validation(), Equals, "unproven") err = db.Add(storeAccKey) c.Assert(err, IsNil) diff -Nru snapd-2.32.3.2~14.04/asserts/database_test.go snapd-2.37~rc1~14.04/asserts/database_test.go --- snapd-2.32.3.2~14.04/asserts/database_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/database_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -60,7 +60,7 @@ "authority-id": "canonical", "account-id": "trusted", "display-name": "Trusted", - "validation": "certified", + "validation": "verified", "timestamp": "2015-01-01T14:00:00Z", } acct, err := asserts.AssembleAndSignInTest(asserts.AccountType, headers, nil, testPrivKey0) @@ -339,7 +339,7 @@ "type": "account", "authority-id": "canonical", "account-id": "predefined", - "validation": "certified", + "validation": "verified", "display-name": "Predef", "timestamp": time.Now().Format(time.RFC3339), } @@ -931,7 +931,7 @@ "type": "account", "authority-id": "canonical", "account-id": "predefined", - "validation": "certified", + "validation": "verified", "display-name": "Predef", "timestamp": time.Now().Format(time.RFC3339), } @@ -1046,7 +1046,7 @@ "type": "account", "authority-id": "canonical", "account-id": "predefined", - "validation": "certified", + "validation": "verified", "display-name": "Predef", "timestamp": time.Now().Format(time.RFC3339), } diff -Nru snapd-2.32.3.2~14.04/asserts/device_asserts.go snapd-2.37~rc1~14.04/asserts/device_asserts.go --- snapd-2.32.3.2~14.04/asserts/device_asserts.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/device_asserts.go 2019-01-16 08:36:51.000000000 +0000 @@ -24,6 +24,8 @@ "regexp" "strings" "time" + + "github.com/snapcore/snapd/strutil" ) // Model holds a model assertion, which is a statement by a brand @@ -71,14 +73,45 @@ return mod.HeaderString("architecture") } +// snapWithTrack represents a snap that includes optional track +// information like `snapName=trackName` +type snapWithTrack string + +func (s snapWithTrack) Snap() string { + return strings.SplitN(string(s), "=", 2)[0] +} + +func (s snapWithTrack) Track() string { + l := strings.SplitN(string(s), "=", 2) + if len(l) > 1 { + return l[1] + } + return "" +} + // Gadget returns the gadget snap the model uses. func (mod *Model) Gadget() string { - return mod.HeaderString("gadget") + return snapWithTrack(mod.HeaderString("gadget")).Snap() +} + +// GadgetTrack returns the gadget track the model uses. +func (mod *Model) GadgetTrack() string { + return snapWithTrack(mod.HeaderString("gadget")).Track() } // Kernel returns the kernel snap the model uses. func (mod *Model) Kernel() string { - return mod.HeaderString("kernel") + return snapWithTrack(mod.HeaderString("kernel")).Snap() +} + +// KernelTrack returns the kernel track the model uses. +func (mod *Model) KernelTrack() string { + return snapWithTrack(mod.HeaderString("kernel")).Track() +} + +// Base returns the base snap the model uses. +func (mod *Model) Base() string { + return mod.HeaderString("base") } // Store returns the snap store the model uses. @@ -113,11 +146,40 @@ // limit model to only lowercase for now var validModel = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$") +func checkSnapWithTrackHeader(header string, headers map[string]interface{}) error { + _, ok := headers[header] + if !ok { + return nil + } + value, ok := headers[header].(string) + if !ok { + return fmt.Errorf(`%q header must be a string`, header) + } + l := strings.SplitN(value, "=", 2) + + if err := validateSnapName(l[0], header); err != nil { + return err + } + if len(l) == 1 { + return nil + } + track := l[1] + if strings.Count(track, "/") != 0 { + return fmt.Errorf(`%q channel selector must be a track name only`, header) + } + channelRisks := []string{"stable", "candidate", "beta", "edge"} + if strutil.ListContains(channelRisks, track) { + return fmt.Errorf(`%q channel selector must be a track name`, header) + } + return nil +} + func checkModel(headers map[string]interface{}) (string, error) { s, err := checkStringMatches(headers, "model", validModel) if err != nil { return "", err } + // TODO: support the concept of case insensitive/preserving string headers if strings.ToLower(s) != s { return "", fmt.Errorf(`"model" header cannot contain uppercase letters`) @@ -160,6 +222,29 @@ classicModelOptional = []string{"architecture", "gadget"} ) +var almostValidName = regexp.MustCompile("^[a-z0-9-]*[a-z][a-z0-9-]*$") + +// validateSnapName checks whether the name can be used as a snap name +// +// This function should be synchronized with the reference implementation +// snap.ValidateName() in snap/validate.go +func validateSnapName(name string, headerName string) error { + isValidName := func() bool { + if !almostValidName.MatchString(name) { + return false + } + if name[0] == '-' || name[len(name)-1] == '-' || strings.Contains(name, "--") { + return false + } + return true + } + + if len(name) > 40 || !isValidName() { + return fmt.Errorf("invalid snap name in %q header: %s", headerName, name) + } + return nil +} + func assembleModel(assert assertionBase) (Assertion, error) { err := checkAuthorityMatchesBrand(&assert) if err != nil { @@ -180,6 +265,9 @@ if _, ok := assert.headers["kernel"]; ok { return nil, fmt.Errorf("cannot specify a kernel with a classic model") } + if _, ok := assert.headers["base"]; ok { + return nil, fmt.Errorf("cannot specify a base with a classic model") + } } checker := checkNotEmptyString @@ -195,6 +283,25 @@ } } + // kernel/gadget must be valid snap names and can have (optional) tracks + // - validate those + if err := checkSnapWithTrackHeader("kernel", assert.headers); err != nil { + return nil, err + } + if err := checkSnapWithTrackHeader("gadget", assert.headers); err != nil { + return nil, err + } + // base, if provided, must be a valid snap name too + base, err := checkOptionalString(assert.headers, "base") + if err != nil { + return nil, err + } + if base != "" { + if err := validateSnapName(base, "base"); err != nil { + return nil, err + } + } + // store is optional but must be a string, defaults to the ubuntu store _, err = checkOptionalString(assert.headers, "store") if err != nil { @@ -207,10 +314,16 @@ return nil, err } + // required snap must be valid snap names reqSnaps, err := checkStringList(assert.headers, "required-snaps") if err != nil { return nil, err } + for _, name := range reqSnaps { + if err := validateSnapName(name, "required-snaps"); err != nil { + return nil, err + } + } sysUserAuthority, err := checkOptionalSystemUserAuthority(assert.headers, assert.HeaderString("brand-id")) if err != nil { diff -Nru snapd-2.32.3.2~14.04/asserts/device_asserts_test.go snapd-2.37~rc1~14.04/asserts/device_asserts_test.go --- snapd-2.32.3.2~14.04/asserts/device_asserts_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/device_asserts_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -20,6 +20,7 @@ package asserts_test import ( + "fmt" "strings" "time" @@ -58,6 +59,7 @@ "display-name: Baz 3000\n" + "architecture: amd64\n" + "gadget: brand-gadget\n" + + "base: core18\n" + "kernel: baz-linux\n" + "store: brand-store\n" + sysUserAuths + @@ -100,7 +102,10 @@ c.Check(model.DisplayName(), Equals, "Baz 3000") c.Check(model.Architecture(), Equals, "amd64") c.Check(model.Gadget(), Equals, "brand-gadget") + c.Check(model.GadgetTrack(), Equals, "") c.Check(model.Kernel(), Equals, "baz-linux") + c.Check(model.KernelTrack(), Equals, "") + c.Check(model.Base(), Equals, "core18") c.Check(model.Store(), Equals, "brand-store") c.Check(model.RequiredSnaps(), DeepEquals, []string{"foo", "bar"}) c.Check(model.SystemUserAuthority(), HasLen, 0) @@ -121,6 +126,21 @@ c.Check(model.Store(), Equals, "") } +func (mods *modelSuite) TestDecodeBaseIsOptional(c *C) { + withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + encoded := strings.Replace(withTimestamp, "base: core18\n", "base: \n", 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model := a.(*asserts.Model) + c.Check(model.Base(), Equals, "") + + encoded = strings.Replace(withTimestamp, "base: core18\n", "", 1) + a, err = asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model = a.(*asserts.Model) + c.Check(model.Base(), Equals, "") +} + func (mods *modelSuite) TestDecodeDisplayNameIsOptional(c *C) { withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) encoded := strings.Replace(withTimestamp, "display-name: Baz 3000\n", "display-name: \n", 1) @@ -147,6 +167,92 @@ c.Check(model.RequiredSnaps(), HasLen, 0) } +func (mods *modelSuite) TestDecodeValidatesSnapNames(c *C) { + withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + encoded := strings.Replace(withTimestamp, reqSnaps, "required-snaps:\n - foo_bar\n - bar\n", 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(a, IsNil) + c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "required-snaps" header: foo_bar`) + + encoded = strings.Replace(withTimestamp, reqSnaps, "required-snaps:\n - foo\n - bar-;;''\n", 1) + a, err = asserts.Decode([]byte(encoded)) + c.Assert(a, IsNil) + c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "required-snaps" header: bar-;;''`) + + encoded = strings.Replace(withTimestamp, "kernel: baz-linux\n", "kernel: baz-linux_instance\n", 1) + a, err = asserts.Decode([]byte(encoded)) + c.Assert(a, IsNil) + c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "kernel" header: baz-linux_instance`) + + encoded = strings.Replace(withTimestamp, "gadget: brand-gadget\n", "gadget: brand-gadget_instance\n", 1) + a, err = asserts.Decode([]byte(encoded)) + c.Assert(a, IsNil) + c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "gadget" header: brand-gadget_instance`) + + encoded = strings.Replace(withTimestamp, "base: core18\n", "base: core18_instance\n", 1) + a, err = asserts.Decode([]byte(encoded)) + c.Assert(a, IsNil) + c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "base" header: core18_instance`) +} + +func (mods modelSuite) TestDecodeValidSnapNames(c *C) { + // reuse test cases for snap.ValidateName() + + withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + + validNames := []string{ + "a", "aa", "aaa", "aaaa", + "a-a", "aa-a", "a-aa", "a-b-c", + "a0", "a-0", "a-0a", + "01game", "1-or-2", + // a regexp stresser + "u-94903713687486543234157734673284536758", + } + for _, name := range validNames { + encoded := strings.Replace(withTimestamp, "kernel: baz-linux\n", fmt.Sprintf("kernel: %s\n", name), 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model := a.(*asserts.Model) + c.Check(model.Kernel(), Equals, name) + } + invalidNames := []string{ + // name cannot be empty, never reaches snap name validation + "", + // names cannot be too long + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx", + "1111111111111111111111111111111111111111x", + "x1111111111111111111111111111111111111111", + "x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x", + // a regexp stresser + "u-9490371368748654323415773467328453675-", + // dashes alone are not a name + "-", "--", + // double dashes in a name are not allowed + "a--a", + // name should not end with a dash + "a-", + // name cannot have any spaces in it + "a ", " a", "a a", + // a number alone is not a name + "0", "123", + // identifier must be plain ASCII + "日本語", "한글", "ру́сский язы́к", + // instance names are invalid too + "foo_bar", "x_1", + } + for _, name := range invalidNames { + encoded := strings.Replace(withTimestamp, "kernel: baz-linux\n", fmt.Sprintf("kernel: %s\n", name), 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(a, IsNil) + if name != "" { + c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "kernel" header: .*`) + } else { + c.Assert(err, ErrorMatches, `assertion model: "kernel" header should not be empty`) + } + } +} + func (mods *modelSuite) TestDecodeSystemUserAuthorityIsOptional(c *C) { withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) encoded := strings.Replace(withTimestamp, sysUserAuths, "", 1) @@ -163,6 +269,26 @@ c.Check(model.SystemUserAuthority(), DeepEquals, []string{"foo", "bar"}) } +func (mods *modelSuite) TestDecodeKernelTrack(c *C) { + withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + encoded := strings.Replace(withTimestamp, "kernel: baz-linux\n", "kernel: baz-linux=18\n", 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model := a.(*asserts.Model) + c.Check(model.Kernel(), Equals, "baz-linux") + c.Check(model.KernelTrack(), Equals, "18") +} + +func (mods *modelSuite) TestDecodeGadgetTrack(c *C) { + withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + encoded := strings.Replace(withTimestamp, "gadget: brand-gadget\n", "gadget: brand-gadget=18\n", 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model := a.(*asserts.Model) + c.Check(model.Gadget(), Equals, "brand-gadget") + c.Check(model.GadgetTrack(), Equals, "18") +} + const ( modelErrPrefix = "assertion model: " ) @@ -186,8 +312,16 @@ {"architecture: amd64\n", "architecture: \n", `"architecture" header should not be empty`}, {"gadget: brand-gadget\n", "", `"gadget" header is mandatory`}, {"gadget: brand-gadget\n", "gadget: \n", `"gadget" header should not be empty`}, + {"gadget: brand-gadget\n", "gadget: brand-gadget=x/x/x\n", `"gadget" channel selector must be a track name only`}, + {"gadget: brand-gadget\n", "gadget: brand-gadget=stable\n", `"gadget" channel selector must be a track name`}, + {"gadget: brand-gadget\n", "gadget: brand-gadget=18/beta\n", `"gadget" channel selector must be a track name only`}, + {"gadget: brand-gadget\n", "gadget:\n - xyz \n", `"gadget" header must be a string`}, {"kernel: baz-linux\n", "", `"kernel" header is mandatory`}, {"kernel: baz-linux\n", "kernel: \n", `"kernel" header should not be empty`}, + {"kernel: baz-linux\n", "kernel: baz-linux=x/x/x\n", `"kernel" channel selector must be a track name only`}, + {"kernel: baz-linux\n", "kernel: baz-linux=stable\n", `"kernel" channel selector must be a track name`}, + {"kernel: baz-linux\n", "kernel: baz-linux=18/beta\n", `"kernel" channel selector must be a track name only`}, + {"kernel: baz-linux\n", "kernel:\n - xyz \n", `"kernel" header must be a string`}, {"store: brand-store\n", "store:\n - xyz\n", `"store" header must be a string`}, {mods.tsLine, "", `"timestamp" header is mandatory`}, {mods.tsLine, "timestamp: \n", `"timestamp" header should not be empty`}, @@ -255,6 +389,7 @@ c.Check(model.Architecture(), Equals, "amd64") c.Check(model.Gadget(), Equals, "brand-gadget") c.Check(model.Kernel(), Equals, "") + c.Check(model.KernelTrack(), Equals, "") c.Check(model.Store(), Equals, "brand-store") c.Check(model.RequiredSnaps(), DeepEquals, []string{"foo", "bar"}) } @@ -267,6 +402,7 @@ {"architecture: amd64\n", "architecture:\n - foo\n", `"architecture" header must be a string`}, {"gadget: brand-gadget\n", "gadget:\n - foo\n", `"gadget" header must be a string`}, {"gadget: brand-gadget\n", "kernel: brand-kernel\n", `cannot specify a kernel with a classic model`}, + {"gadget: brand-gadget\n", "base: some-base\n", `cannot specify a base with a classic model`}, } for _, test := range invalidTests { diff -Nru snapd-2.32.3.2~14.04/asserts/export_test.go snapd-2.37~rc1~14.04/asserts/export_test.go --- snapd-2.32.3.2~14.04/asserts/export_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/export_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -55,7 +55,7 @@ "type": "account", "authority-id": authorityID, "account-id": authorityID, - "validation": "certified", + "validation": "verified", }, }, timestamp: time.Now().UTC(), diff -Nru snapd-2.32.3.2~14.04/asserts/header_checks.go snapd-2.37~rc1~14.04/asserts/header_checks.go --- snapd-2.32.3.2~14.04/asserts/header_checks.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/header_checks.go 2019-01-16 08:36:51.000000000 +0000 @@ -196,8 +196,10 @@ return b, nil } -var anyString = regexp.MustCompile("") - +// checkStringListInMap returns the `name` entry in the `m` map as a (possibly nil) `[]string` +// if `m` has an entry for `name` and it isn't a `[]string`, an error is returned +// if pattern is not nil, all the strings must match that pattern, otherwise an error is returned +// `what` is a descriptor, used for error messages func checkStringListInMap(m map[string]interface{}, name, what string, pattern *regexp.Regexp) ([]string, error) { value, ok := m[name] if !ok { @@ -216,7 +218,7 @@ if !ok { return nil, fmt.Errorf("%s must be a list of strings", what) } - if !pattern.MatchString(s) { + if pattern != nil && !pattern.MatchString(s) { return nil, fmt.Errorf("%s contains an invalid element: %q", what, s) } res[i] = s @@ -225,7 +227,7 @@ } func checkStringList(headers map[string]interface{}, name string) ([]string, error) { - return checkStringListMatches(headers, name, anyString) + return checkStringListMatches(headers, name, nil) } func checkStringListMatches(headers map[string]interface{}, name string, pattern *regexp.Regexp) ([]string, error) { diff -Nru snapd-2.32.3.2~14.04/asserts/ifacedecls.go snapd-2.37~rc1~14.04/asserts/ifacedecls.go --- snapd-2.32.3.2~14.04/asserts/ifacedecls.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/ifacedecls.go 2019-01-16 08:36:51.000000000 +0000 @@ -37,6 +37,8 @@ const ( // feature label for $SLOT()/$PLUG()/$MISSING dollarAttrConstraintsFeature = "dollar-attr-constraints" + // feature label for on-store/on-brand/on-model + deviceScopeConstraintsFeature = "device-scope-constraints" ) type attrMatcher interface { @@ -150,6 +152,14 @@ func (matcher mapAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) error { switch x := v.(type) { + case Attrer: + // we get Atter from root-level Check (apath is "") + for k, matcher1 := range matcher { + v, _ := x.Lookup(k) + if err := matchEntry("", k, matcher1, v, ctx); err != nil { + return err + } + } case map[string]interface{}: // maps in attributes look like this for k, matcher1 := range matcher { if err := matchEntry(apath, k, matcher1, x[k], ctx); err != nil { @@ -340,9 +350,14 @@ NeverMatchAttributes = &AttributeConstraints{matcher: fixedAttrMatcher{errors.New("not allowed")}} ) +// Attrer reflects part of the Attrer interface (see interfaces.Attrer). +type Attrer interface { + Lookup(path string) (interface{}, bool) +} + // Check checks whether attrs don't match the constraints. -func (c *AttributeConstraints) Check(attrs map[string]interface{}, ctx AttrMatchContext) error { - return c.matcher.match("", attrs, ctx) +func (c *AttributeConstraints) Check(attrer Attrer, ctx AttrMatchContext) error { + return c.matcher.match("", attrer, ctx) } // OnClassicConstraint specifies a constraint based whether the system is classic and optional specific distros' sets. @@ -351,6 +366,66 @@ SystemIDs []string } +// DeviceScopeConstraint specifies a constraints based on which brand +// store, brand or model the device belongs to. +type DeviceScopeConstraint struct { + Store []string + Brand []string + // Model is a list of precise "/" constraints + Model []string +} + +var ( + validStoreID = regexp.MustCompile("^[-A-Z0-9a-z_]+$") + validBrandSlashModel = regexp.MustCompile("^(" + + strings.Trim(validAccountID.String(), "^$") + + ")/(" + + strings.Trim(validModel.String(), "^$") + + ")$") + deviceScopeConstraints = map[string]*regexp.Regexp{ + "on-store": validStoreID, + "on-brand": validAccountID, + // on-model constraints are of the form list of + // / strings where are account + // IDs as they appear in the respective model assertion + "on-model": validBrandSlashModel, + } +) + +func detectDeviceScopeConstraint(cMap map[string]interface{}) bool { + // for consistency and simplicity we support all of on-store, + // on-brand, and on-model to appear together. The interpretation + // layer will AND them as usual + for field := range deviceScopeConstraints { + if cMap[field] != nil { + return true + } + } + return false +} + +func compileDeviceScopeConstraint(cMap map[string]interface{}, context string) (constr *DeviceScopeConstraint, err error) { + // initial map size of 2: we expect usual cases to have just one of the + // constraints or rarely 2 + deviceConstr := make(map[string][]string, 2) + for field, validRegexp := range deviceScopeConstraints { + vals, err := checkStringListInMap(cMap, field, fmt.Sprintf("%s in %s", field, context), validRegexp) + if err != nil { + return nil, err + } + deviceConstr[field] = vals + } + + if len(deviceConstr) == 0 { + return nil, fmt.Errorf("internal error: misdetected device scope constraints in %s", context) + } + return &DeviceScopeConstraint{ + Store: deviceConstr["on-store"], + Brand: deviceConstr["on-brand"], + Model: deviceConstr["on-model"], + }, nil +} + // rules var ( @@ -388,6 +463,7 @@ setAttributeConstraints(field string, cstrs *AttributeConstraints) setIDConstraints(field string, cstrs []string) setOnClassicConstraint(onClassic *OnClassicConstraint) + setDeviceScopeConstraint(deviceScope *DeviceScopeConstraint) } func baseCompileConstraints(context string, cDef constraintsDef, target constraintsHolder, attrConstraints, idConstraints []string) error { @@ -452,8 +528,21 @@ } target.setOnClassicConstraint(c) } - if defaultUsed == len(attributeConstraints)+len(idConstraints)+1 { - return fmt.Errorf("%s must specify at least one of %s, %s, on-classic", context, strings.Join(attrConstraints, ", "), strings.Join(idConstraints, ", ")) + if !detectDeviceScopeConstraint(cMap) { + defaultUsed++ + } else { + c, err := compileDeviceScopeConstraint(cMap, context) + if err != nil { + return err + } + target.setDeviceScopeConstraint(c) + } + // checks whether defaults have been used for everything, which is not + // well-formed + // +1+1 accounts for defaults for missing on-classic plus missing + // on-store/on-brand/on-model + if defaultUsed == len(attributeConstraints)+len(idConstraints)+1+1 { + return fmt.Errorf("%s must specify at least one of %s, %s, on-classic, on-store, on-brand, on-model", context, strings.Join(attrConstraints, ", "), strings.Join(idConstraints, ", ")) } return nil } @@ -624,9 +713,14 @@ PlugAttributes *AttributeConstraints OnClassic *OnClassicConstraint + + DeviceScope *DeviceScopeConstraint } func (c *PlugInstallationConstraints) feature(flabel string) bool { + if flabel == deviceScopeConstraintsFeature { + return c.DeviceScope != nil + } return c.PlugAttributes.feature(flabel) } @@ -652,6 +746,10 @@ c.OnClassic = onClassic } +func (c *PlugInstallationConstraints) setDeviceScopeConstraint(deviceScope *DeviceScopeConstraint) { + c.DeviceScope = deviceScope +} + func compilePlugInstallationConstraints(context string, cDef constraintsDef) (constraintsHolder, error) { plugInstCstrs := &PlugInstallationConstraints{} err := baseCompileConstraints(context, cDef, plugInstCstrs, []string{"plug-attributes"}, []string{"plug-snap-type"}) @@ -673,9 +771,14 @@ SlotAttributes *AttributeConstraints OnClassic *OnClassicConstraint + + DeviceScope *DeviceScopeConstraint } func (c *PlugConnectionConstraints) feature(flabel string) bool { + if flabel == deviceScopeConstraintsFeature { + return c.DeviceScope != nil + } return c.PlugAttributes.feature(flabel) || c.SlotAttributes.feature(flabel) } @@ -707,6 +810,10 @@ c.OnClassic = onClassic } +func (c *PlugConnectionConstraints) setDeviceScopeConstraint(deviceScope *DeviceScopeConstraint) { + c.DeviceScope = deviceScope +} + var ( attributeConstraints = []string{"plug-attributes", "slot-attributes"} plugIDConstraints = []string{"slot-snap-type", "slot-publisher-id", "slot-snap-id"} @@ -856,9 +963,14 @@ SlotAttributes *AttributeConstraints OnClassic *OnClassicConstraint + + DeviceScope *DeviceScopeConstraint } func (c *SlotInstallationConstraints) feature(flabel string) bool { + if flabel == deviceScopeConstraintsFeature { + return c.DeviceScope != nil + } return c.SlotAttributes.feature(flabel) } @@ -884,6 +996,10 @@ c.OnClassic = onClassic } +func (c *SlotInstallationConstraints) setDeviceScopeConstraint(deviceScope *DeviceScopeConstraint) { + c.DeviceScope = deviceScope +} + func compileSlotInstallationConstraints(context string, cDef constraintsDef) (constraintsHolder, error) { slotInstCstrs := &SlotInstallationConstraints{} err := baseCompileConstraints(context, cDef, slotInstCstrs, []string{"slot-attributes"}, []string{"slot-snap-type"}) @@ -905,9 +1021,14 @@ PlugAttributes *AttributeConstraints OnClassic *OnClassicConstraint + + DeviceScope *DeviceScopeConstraint } func (c *SlotConnectionConstraints) feature(flabel string) bool { + if flabel == deviceScopeConstraintsFeature { + return c.DeviceScope != nil + } return c.PlugAttributes.feature(flabel) || c.SlotAttributes.feature(flabel) } @@ -943,6 +1064,10 @@ c.OnClassic = onClassic } +func (c *SlotConnectionConstraints) setDeviceScopeConstraint(deviceScope *DeviceScopeConstraint) { + c.DeviceScope = deviceScope +} + func compileSlotConnectionConstraints(context string, cDef constraintsDef) (constraintsHolder, error) { slotConnCstrs := &SlotConnectionConstraints{} err := baseCompileConstraints(context, cDef, slotConnCstrs, attributeConstraints, slotIDConstraints) diff -Nru snapd-2.32.3.2~14.04/asserts/ifacedecls_test.go snapd-2.37~rc1~14.04/asserts/ifacedecls_test.go --- snapd-2.32.3.2~14.04/asserts/ifacedecls_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/ifacedecls_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -41,7 +41,14 @@ testutil.BaseTest } -func attrs(yml string) map[string]interface{} { +type attrerObject map[string]interface{} + +func (o attrerObject) Lookup(path string) (interface{}, bool) { + v, ok := o[path] + return v, ok +} + +func attrs(yml string) *attrerObject { var attrs map[string]interface{} err := yaml.Unmarshal([]byte(yml), &attrs) if err != nil { @@ -56,11 +63,17 @@ if err != nil { panic(err) } + + // NOTE: it's important to go through snap yaml here even though we're really interested in Attrs only, + // as InfoFromSnapYaml normalizes yaml values. info, err := snap.InfoFromSnapYaml(snapYaml) if err != nil { panic(err) } - return info.Plugs["plug"].Attrs + + var ao attrerObject + ao = info.Plugs["plug"].Attrs + return &ao } func (s *attrConstraintsSuite) SetUpTest(c *C) { @@ -81,24 +94,27 @@ cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) c.Assert(err, IsNil) - err = cstrs.Check(map[string]interface{}{ + plug := attrerObject(map[string]interface{}{ "foo": "FOO", "bar": "BAR", "baz": "BAZ", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, IsNil) - err = cstrs.Check(map[string]interface{}{ + plug = attrerObject(map[string]interface{}{ "foo": "FOO", "bar": "BAZ", "baz": "BAZ", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, ErrorMatches, `attribute "bar" value "BAZ" does not match \^\(BAR\)\$`) - err = cstrs.Check(map[string]interface{}{ + plug = attrerObject(map[string]interface{}{ "foo": "FOO", "baz": "BAZ", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, ErrorMatches, `attribute "bar" has constraints but is unset`) } @@ -110,29 +126,34 @@ cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) c.Assert(err, IsNil) - err = cstrs.Check(map[string]interface{}{ + plug := attrerObject(map[string]interface{}{ "bar": "BAR", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, IsNil) - err = cstrs.Check(map[string]interface{}{ + plug = attrerObject(map[string]interface{}{ "bar": "BARR", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, ErrorMatches, `attribute "bar" value "BARR" does not match \^\(BAR|BAZ\)\$`) - err = cstrs.Check(map[string]interface{}{ + plug = attrerObject(map[string]interface{}{ "bar": "BBAZ", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, ErrorMatches, `attribute "bar" value "BAZZ" does not match \^\(BAR|BAZ\)\$`) - err = cstrs.Check(map[string]interface{}{ + plug = attrerObject(map[string]interface{}{ "bar": "BABAZ", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, ErrorMatches, `attribute "bar" value "BABAZ" does not match \^\(BAR|BAZ\)\$`) - err = cstrs.Check(map[string]interface{}{ + plug = attrerObject(map[string]interface{}{ "bar": "BARAZ", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, ErrorMatches, `attribute "bar" value "BARAZ" does not match \^\(BAR|BAZ\)\$`) } @@ -199,25 +220,28 @@ cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].([]interface{})) c.Assert(err, IsNil) - err = cstrs.Check(map[string]interface{}{ + plug := attrerObject(map[string]interface{}{ "foo": "FOO", "bar": "BAR", "baz": "BAZ", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, IsNil) - err = cstrs.Check(map[string]interface{}{ + plug = attrerObject(map[string]interface{}{ "foo": "FOO", "bar": "BAZ", "baz": "BAZ", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, IsNil) - err = cstrs.Check(map[string]interface{}{ + plug = attrerObject(map[string]interface{}{ "foo": "FOO", "bar": "BARR", "baz": "BAR", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, ErrorMatches, `no alternative matches: attribute "bar" value "BARR" does not match \^\(BAR\)\$`) } @@ -274,10 +298,11 @@ `), nil) c.Check(err, IsNil) - err = cstrs.Check(map[string]interface{}{ + plug := attrerObject(map[string]interface{}{ "foo": int64(1), "bar": true, - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, IsNil) } @@ -461,12 +486,14 @@ type plugSlotRulesSuite struct{} func checkAttrs(c *C, attrs *asserts.AttributeConstraints, witness, expected string) { - c.Check(attrs.Check(map[string]interface{}{ + plug := attrerObject(map[string]interface{}{ witness: "XYZ", - }, nil), ErrorMatches, fmt.Sprintf(`attribute "%s".*does not match.*`, witness)) - c.Check(attrs.Check(map[string]interface{}{ + }) + c.Check(attrs.Check(plug, nil), ErrorMatches, fmt.Sprintf(`attribute "%s".*does not match.*`, witness)) + plug = attrerObject(map[string]interface{}{ witness: expected, - }, nil), IsNil) + }) + c.Check(attrs.Check(plug, nil), IsNil) } func checkBoolPlugConnConstraints(c *C, cstrs []*asserts.PlugConnectionConstraints, always bool) { @@ -750,6 +777,65 @@ c.Check(rule.AllowInstallation[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true, SystemIDs: []string{"ubuntu", "debian"}}) } +func (s *plugSlotRulesSuite) TestCompilePlugRuleInstallationConstraintsDeviceScope(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-installation: true`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].DeviceScope, IsNil) + + tests := []struct { + rule string + expected asserts.DeviceScopeConstraint + }{ + {`iface: + allow-installation: + on-store: + - my-store`, asserts.DeviceScopeConstraint{Store: []string{"my-store"}}}, + {`iface: + allow-installation: + on-store: + - my-store + - other-store`, asserts.DeviceScopeConstraint{Store: []string{"my-store", "other-store"}}}, + {`iface: + allow-installation: + on-brand: + - my-brand + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT`, asserts.DeviceScopeConstraint{Brand: []string{"my-brand", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT"}}}, + {`iface: + allow-installation: + on-model: + - my-brand/bar + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz`, asserts.DeviceScopeConstraint{Model: []string{"my-brand/bar", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz"}}}, + {`iface: + allow-installation: + on-store: + - store1 + - store2 + on-brand: + - my-brand + on-model: + - my-brand/bar + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz`, asserts.DeviceScopeConstraint{ + Store: []string{"store1", "store2"}, + Brand: []string{"my-brand"}, + Model: []string{"my-brand/bar", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz"}}}, + } + + for _, t := range tests { + m, err = asserts.ParseHeaders([]byte(t.rule)) + c.Assert(err, IsNil) + + rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].DeviceScope, DeepEquals, &t.expected) + } +} + func (s *plugSlotRulesSuite) TestCompilePlugRuleConnectionConstraintsIDConstraints(c *C) { rule, err := asserts.CompilePlugRule("iface", map[string]interface{}{ "allow-connection": map[string]interface{}{ @@ -811,6 +897,65 @@ c.Check(rule.AllowConnection[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true, SystemIDs: []string{"ubuntu", "debian"}}) } +func (s *plugSlotRulesSuite) TestCompilePlugRuleConnectionConstraintsDeviceScope(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-connection: true`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].DeviceScope, IsNil) + + tests := []struct { + rule string + expected asserts.DeviceScopeConstraint + }{ + {`iface: + allow-connection: + on-store: + - my-store`, asserts.DeviceScopeConstraint{Store: []string{"my-store"}}}, + {`iface: + allow-connection: + on-store: + - my-store + - other-store`, asserts.DeviceScopeConstraint{Store: []string{"my-store", "other-store"}}}, + {`iface: + allow-connection: + on-brand: + - my-brand + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT`, asserts.DeviceScopeConstraint{Brand: []string{"my-brand", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT"}}}, + {`iface: + allow-connection: + on-model: + - my-brand/bar + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz`, asserts.DeviceScopeConstraint{Model: []string{"my-brand/bar", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz"}}}, + {`iface: + allow-connection: + on-store: + - store1 + - store2 + on-brand: + - my-brand + on-model: + - my-brand/bar + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz`, asserts.DeviceScopeConstraint{ + Store: []string{"store1", "store2"}, + Brand: []string{"my-brand"}, + Model: []string{"my-brand/bar", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz"}}}, + } + + for _, t := range tests { + m, err = asserts.ParseHeaders([]byte(t.rule)) + c.Assert(err, IsNil) + + rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].DeviceScope, DeepEquals, &t.expected) + } +} + func (s *plugSlotRulesSuite) TestCompilePlugRuleConnectionConstraintsAttributesDefault(c *C) { rule, err := asserts.CompilePlugRule("iface", map[string]interface{}{ "allow-connection": map[string]interface{}{ @@ -873,21 +1018,51 @@ {`iface: allow-connection: slot-snap-ids: - - foo`, `allow-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic`}, + - foo`, `allow-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic, on-store, on-brand, on-model`}, {`iface: deny-connection: slot-snap-ids: - - foo`, `deny-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic`}, + - foo`, `deny-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic, on-store, on-brand, on-model`}, {`iface: allow-auto-connection: slot-snap-ids: - - foo`, `allow-auto-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic`}, + - foo`, `allow-auto-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic, on-store, on-brand, on-model`}, {`iface: deny-auto-connection: slot-snap-ids: - - foo`, `deny-auto-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic`}, + - foo`, `deny-auto-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic, on-store, on-brand, on-model`}, {`iface: allow-connect: true`, `plug rule for interface "iface" must specify at least one of allow-installation, deny-installation, allow-connection, deny-connection, allow-auto-connection, deny-auto-connection`}, + {`iface: + allow-installation: + on-store: true`, `on-store in allow-installation in plug rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-installation: + on-store: store1`, `on-store in allow-installation in plug rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-installation: + on-store: + - zoom!`, `on-store in allow-installation in plug rule for interface \"iface\" contains an invalid element: \"zoom!\"`}, + {`iface: + allow-connection: + on-brand: true`, `on-brand in allow-connection in plug rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-connection: + on-brand: brand1`, `on-brand in allow-connection in plug rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-connection: + on-brand: + - zoom!`, `on-brand in allow-connection in plug rule for interface \"iface\" contains an invalid element: \"zoom!\"`}, + {`iface: + allow-auto-connection: + on-model: true`, `on-model in allow-auto-connection in plug rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-auto-connection: + on-model: foo/bar`, `on-model in allow-auto-connection in plug rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-auto-connection: + on-model: + - foo/!qz`, `on-model in allow-auto-connection in plug rule for interface \"iface\" contains an invalid element: \"foo/!qz"`}, } for _, t := range tests { @@ -899,6 +1074,14 @@ } } +var ( + deviceScopeConstrs = map[string][]interface{}{ + "on-store": {"store"}, + "on-brand": {"brand"}, + "on-model": {"brand/model"}, + } +) + func (s *plugSlotRulesSuite) TestPlugRuleFeatures(c *C) { combos := []struct { subrule string @@ -928,6 +1111,8 @@ c.Assert(err, IsNil) c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, false, Commentf("%v", ruleMap)) + c.Check(asserts.RuleFeature(rule, "device-scope-constraints"), Equals, false, Commentf("%v", ruleMap)) + attrConstraintMap["a"] = "$MISSING" rule, err = asserts.CompilePlugRule("iface", ruleMap) c.Assert(err, IsNil) @@ -939,6 +1124,20 @@ c.Assert(err, IsNil) c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, true, Commentf("%v", ruleMap)) + c.Check(asserts.RuleFeature(rule, "device-scope-constraints"), Equals, false, Commentf("%v", ruleMap)) + + } + + for deviceScopeConstr, value := range deviceScopeConstrs { + ruleMap := map[string]interface{}{ + combo.subrule: map[string]interface{}{ + deviceScopeConstr: value, + }, + } + + rule, err := asserts.CompilePlugRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "device-scope-constraints"), Equals, true, Commentf("%v", ruleMap)) } } } @@ -1196,6 +1395,65 @@ c.Check(rule.AllowInstallation[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true, SystemIDs: []string{"ubuntu", "debian"}}) } +func (s *plugSlotRulesSuite) TestCompileSlotRuleInstallationConstraintsDeviceScope(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-installation: true`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].DeviceScope, IsNil) + + tests := []struct { + rule string + expected asserts.DeviceScopeConstraint + }{ + {`iface: + allow-installation: + on-store: + - my-store`, asserts.DeviceScopeConstraint{Store: []string{"my-store"}}}, + {`iface: + allow-installation: + on-store: + - my-store + - other-store`, asserts.DeviceScopeConstraint{Store: []string{"my-store", "other-store"}}}, + {`iface: + allow-installation: + on-brand: + - my-brand + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT`, asserts.DeviceScopeConstraint{Brand: []string{"my-brand", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT"}}}, + {`iface: + allow-installation: + on-model: + - my-brand/bar + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz`, asserts.DeviceScopeConstraint{Model: []string{"my-brand/bar", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz"}}}, + {`iface: + allow-installation: + on-store: + - store1 + - store2 + on-brand: + - my-brand + on-model: + - my-brand/bar + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz`, asserts.DeviceScopeConstraint{ + Store: []string{"store1", "store2"}, + Brand: []string{"my-brand"}, + Model: []string{"my-brand/bar", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz"}}}, + } + + for _, t := range tests { + m, err = asserts.ParseHeaders([]byte(t.rule)) + c.Assert(err, IsNil) + + rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].DeviceScope, DeepEquals, &t.expected) + } +} + func (s *plugSlotRulesSuite) TestCompileSlotRuleConnectionConstraintsIDConstraints(c *C) { rule, err := asserts.CompileSlotRule("iface", map[string]interface{}{ "allow-connection": map[string]interface{}{ @@ -1256,6 +1514,65 @@ c.Check(rule.AllowConnection[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true, SystemIDs: []string{"ubuntu", "debian"}}) } +func (s *plugSlotRulesSuite) TestCompileSlotRuleConnectionConstraintsDeviceScope(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-connection: true`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].DeviceScope, IsNil) + + tests := []struct { + rule string + expected asserts.DeviceScopeConstraint + }{ + {`iface: + allow-connection: + on-store: + - my-store`, asserts.DeviceScopeConstraint{Store: []string{"my-store"}}}, + {`iface: + allow-connection: + on-store: + - my-store + - other-store`, asserts.DeviceScopeConstraint{Store: []string{"my-store", "other-store"}}}, + {`iface: + allow-connection: + on-brand: + - my-brand + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT`, asserts.DeviceScopeConstraint{Brand: []string{"my-brand", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT"}}}, + {`iface: + allow-connection: + on-model: + - my-brand/bar + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz`, asserts.DeviceScopeConstraint{Model: []string{"my-brand/bar", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz"}}}, + {`iface: + allow-connection: + on-store: + - store1 + - store2 + on-brand: + - my-brand + on-model: + - my-brand/bar + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz`, asserts.DeviceScopeConstraint{ + Store: []string{"store1", "store2"}, + Brand: []string{"my-brand"}, + Model: []string{"my-brand/bar", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz"}}}, + } + + for _, t := range tests { + m, err = asserts.ParseHeaders([]byte(t.rule)) + c.Assert(err, IsNil) + + rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].DeviceScope, DeepEquals, &t.expected) + } +} + func (s *plugSlotRulesSuite) TestCompileSlotRuleErrors(c *C) { tests := []struct { stanza string @@ -1312,21 +1629,51 @@ {`iface: allow-connection: plug-snap-ids: - - foo`, `allow-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic`}, + - foo`, `allow-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic, on-store, on-brand, on-model`}, {`iface: deny-connection: plug-snap-ids: - - foo`, `deny-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic`}, + - foo`, `deny-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic, on-store, on-brand, on-model`}, {`iface: allow-auto-connection: plug-snap-ids: - - foo`, `allow-auto-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic`}, + - foo`, `allow-auto-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic, on-store, on-brand, on-model`}, {`iface: deny-auto-connection: plug-snap-ids: - - foo`, `deny-auto-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic`}, + - foo`, `deny-auto-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic, on-store, on-brand, on-model`}, {`iface: allow-connect: true`, `slot rule for interface "iface" must specify at least one of allow-installation, deny-installation, allow-connection, deny-connection, allow-auto-connection, deny-auto-connection`}, + {`iface: + allow-installation: + on-store: true`, `on-store in allow-installation in slot rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-installation: + on-store: store1`, `on-store in allow-installation in slot rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-installation: + on-store: + - zoom!`, `on-store in allow-installation in slot rule for interface \"iface\" contains an invalid element: \"zoom!\"`}, + {`iface: + allow-connection: + on-brand: true`, `on-brand in allow-connection in slot rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-connection: + on-brand: brand1`, `on-brand in allow-connection in slot rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-connection: + on-brand: + - zoom!`, `on-brand in allow-connection in slot rule for interface \"iface\" contains an invalid element: \"zoom!\"`}, + {`iface: + allow-auto-connection: + on-model: true`, `on-model in allow-auto-connection in slot rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-auto-connection: + on-model: foo/bar`, `on-model in allow-auto-connection in slot rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-auto-connection: + on-model: + - foo//bar`, `on-model in allow-auto-connection in slot rule for interface \"iface\" contains an invalid element: \"foo//bar"`}, } for _, t := range tests { @@ -1365,11 +1712,101 @@ c.Assert(err, IsNil) c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, false, Commentf("%v", ruleMap)) + c.Check(asserts.RuleFeature(rule, "device-scope-constraints"), Equals, false, Commentf("%v", ruleMap)) + attrConstraintMap["a"] = "$PLUG(a)" rule, err = asserts.CompileSlotRule("iface", ruleMap) c.Assert(err, IsNil) c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, true, Commentf("%v", ruleMap)) + c.Check(asserts.RuleFeature(rule, "device-scope-constraints"), Equals, false, Commentf("%v", ruleMap)) + } + + for deviceScopeConstr, value := range deviceScopeConstrs { + ruleMap := map[string]interface{}{ + combo.subrule: map[string]interface{}{ + deviceScopeConstr: value, + }, + } + + rule, err := asserts.CompileSlotRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "device-scope-constraints"), Equals, true, Commentf("%v", ruleMap)) + } + } +} + +func (s *plugSlotRulesSuite) TestValidOnStoreBrandModel(c *C) { + tests := []struct { + constr string + value string + valid bool + }{ + {"on-store", "", false}, + {"on-store", "foo", true}, + {"on-store", "F_o-O88", true}, + {"on-store", "foo!", false}, + {"on-store", "foo.", false}, + {"on-store", "foo/", false}, + {"on-brand", "", false}, + // custom set brands (length 2-28) + {"on-brand", "dwell", true}, + {"on-brand", "Dwell", false}, + {"on-brand", "dwell-88", true}, + {"on-brand", "dwell_88", false}, + {"on-brand", "dwell.88", false}, + {"on-brand", "dwell:88", false}, + {"on-brand", "dwell!88", false}, + {"on-brand", "a", false}, + {"on-brand", "ab", true}, + {"on-brand", "0123456789012345678901234567", true}, + // snappy id brands (fixed length 32) + {"on-brand", "01234567890123456789012345678", false}, + {"on-brand", "012345678901234567890123456789", false}, + {"on-brand", "0123456789012345678901234567890", false}, + {"on-brand", "01234567890123456789012345678901", true}, + {"on-brand", "abcdefghijklmnopqrstuvwxyz678901", true}, + {"on-brand", "ABCDEFGHIJKLMNOPQRSTUVWCYZ678901", true}, + {"on-brand", "ABCDEFGHIJKLMNOPQRSTUVWCYZ678901X", false}, + {"on-brand", "ABCDEFGHIJKLMNOPQ!STUVWCYZ678901", false}, + {"on-brand", "ABCDEFGHIJKLMNOPQ_STUVWCYZ678901", false}, + {"on-brand", "ABCDEFGHIJKLMNOPQ-STUVWCYZ678901", false}, + {"on-model", "", false}, + {"on-model", "/", false}, + {"on-model", "dwell/dwell1", true}, + {"on-model", "dwell", false}, + {"on-model", "dwell/", false}, + {"on-model", "dwell//dwell1", false}, + {"on-model", "dwell/-dwell1", false}, + {"on-model", "dwell/dwell1-", false}, + {"on-model", "dwell/dwell1-23", true}, + {"on-model", "dwell/dwell1!", false}, + {"on-model", "dwell/dwe_ll1", false}, + {"on-model", "dwell/dwe.ll1", false}, + } + + check := func(constr, value string, valid bool) { + ruleMap := map[string]interface{}{ + "allow-auto-connection": map[string]interface{}{ + constr: []interface{}{value}, + }, + } + + _, err := asserts.CompilePlugRule("iface", ruleMap) + if valid { + c.Check(err, IsNil, Commentf("%v", ruleMap)) + } else { + c.Check(err, ErrorMatches, fmt.Sprintf(`%s in allow-auto-connection in plug rule for interface "iface" contains an invalid element: %q`, constr, value), Commentf("%v", ruleMap)) + } + } + + for _, t := range tests { + check(t.constr, t.value, t.valid) + + if t.constr == "on-brand" { + // reuse and double check all brands also in the context of on-model! + + check("on-model", t.value+"/foo", t.valid) } } } diff -Nru snapd-2.32.3.2~14.04/asserts/snapasserts/snapasserts.go snapd-2.37~rc1~14.04/asserts/snapasserts/snapasserts.go --- snapd-2.32.3.2~14.04/asserts/snapasserts/snapasserts.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/snapasserts/snapasserts.go 2019-01-16 08:36:51.000000000 +0000 @@ -53,34 +53,34 @@ return snapDecl, nil } -// CrossCheck tries to cross check the name, hash digest and size of a snap plus its metadata in a SideInfo with the relevant snap assertions in a database that should have been populated with them. -func CrossCheck(name, snapSHA3_384 string, snapSize uint64, si *snap.SideInfo, db Finder) error { +// CrossCheck tries to cross check the instance name, hash digest and size of a snap plus its metadata in a SideInfo with the relevant snap assertions in a database that should have been populated with them. +func CrossCheck(instanceName, snapSHA3_384 string, snapSize uint64, si *snap.SideInfo, db Finder) error { // get relevant assertions and do cross checks a, err := db.Find(asserts.SnapRevisionType, map[string]string{ "snap-sha3-384": snapSHA3_384, }) if err != nil { - return fmt.Errorf("internal error: cannot find pre-populated snap-revision assertion for %q: %s", name, snapSHA3_384) + return fmt.Errorf("internal error: cannot find pre-populated snap-revision assertion for %q: %s", instanceName, snapSHA3_384) } snapRev := a.(*asserts.SnapRevision) if snapRev.SnapSize() != snapSize { - return fmt.Errorf("snap %q file does not have expected size according to signatures (download is broken or tampered): %d != %d", name, snapSize, snapRev.SnapSize()) + return fmt.Errorf("snap %q file does not have expected size according to signatures (download is broken or tampered): %d != %d", instanceName, snapSize, snapRev.SnapSize()) } snapID := si.SnapID if snapRev.SnapID() != snapID || snapRev.SnapRevision() != si.Revision.N { - return fmt.Errorf("snap %q does not have expected ID or revision according to assertions (metadata is broken or tampered): %s / %s != %d / %s", name, si.Revision, snapID, snapRev.SnapRevision(), snapRev.SnapID()) + return fmt.Errorf("snap %q does not have expected ID or revision according to assertions (metadata is broken or tampered): %s / %s != %d / %s", instanceName, si.Revision, snapID, snapRev.SnapRevision(), snapRev.SnapID()) } - snapDecl, err := findSnapDeclaration(snapID, name, db) + snapDecl, err := findSnapDeclaration(snapID, instanceName, db) if err != nil { return err } - if snapDecl.SnapName() != name { - return fmt.Errorf("cannot install snap %q that is undergoing a rename to %q", name, snapDecl.SnapName()) + if snapDecl.SnapName() != snap.InstanceSnap(instanceName) { + return fmt.Errorf("cannot install %q, snap %q is undergoing a rename to %q", instanceName, snap.InstanceSnap(instanceName), snapDecl.SnapName()) } return nil @@ -142,4 +142,14 @@ } return f.Fetch(ref) +} + +// FetchStore fetches the store assertion and its prerequisites for the given store id using the given fetcher. +func FetchStore(f asserts.Fetcher, storeID string) error { + ref := &asserts.Ref{ + Type: asserts.StoreType, + PrimaryKey: []string{storeID}, + } + + return f.Fetch(ref) } diff -Nru snapd-2.32.3.2~14.04/asserts/snapasserts/snapasserts_test.go snapd-2.37~rc1~14.04/asserts/snapasserts/snapasserts_test.go --- snapd-2.32.3.2~14.04/asserts/snapasserts/snapasserts_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/snapasserts/snapasserts_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -119,9 +119,12 @@ Revision: snap.R(12), } - // everything cross checks + // everything cross checks, with the regular snap name err = snapasserts.CrossCheck("foo", digest, size, si, s.localDB) c.Check(err, IsNil) + // and a snap instance name + err = snapasserts.CrossCheck("foo_instance", digest, size, si, s.localDB) + c.Check(err, IsNil) } func (s *snapassertsSuite) TestCrossCheckErrors(c *C) { @@ -148,6 +151,8 @@ // different size err = snapasserts.CrossCheck("foo", digest, size+1, si, s.localDB) c.Check(err, ErrorMatches, fmt.Sprintf(`snap "foo" file does not have expected size according to signatures \(download is broken or tampered\): %d != %d`, size+1, size)) + err = snapasserts.CrossCheck("foo_instance", digest, size+1, si, s.localDB) + c.Check(err, ErrorMatches, fmt.Sprintf(`snap "foo_instance" file does not have expected size according to signatures \(download is broken or tampered\): %d != %d`, size+1, size)) // mismatched revision vs what we got from store original info err = snapasserts.CrossCheck("foo", digest, size, &snap.SideInfo{ @@ -155,6 +160,11 @@ Revision: snap.R(21), }, s.localDB) c.Check(err, ErrorMatches, `snap "foo" does not have expected ID or revision according to assertions \(metadata is broken or tampered\): 21 / snap-id-1 != 12 / snap-id-1`) + err = snapasserts.CrossCheck("foo_instance", digest, size, &snap.SideInfo{ + SnapID: "snap-id-1", + Revision: snap.R(21), + }, s.localDB) + c.Check(err, ErrorMatches, `snap "foo_instance" does not have expected ID or revision according to assertions \(metadata is broken or tampered\): 21 / snap-id-1 != 12 / snap-id-1`) // mismatched snap id vs what we got from store original info err = snapasserts.CrossCheck("foo", digest, size, &snap.SideInfo{ @@ -162,10 +172,17 @@ Revision: snap.R(12), }, s.localDB) c.Check(err, ErrorMatches, `snap "foo" does not have expected ID or revision according to assertions \(metadata is broken or tampered\): 12 / snap-id-other != 12 / snap-id-1`) + err = snapasserts.CrossCheck("foo_instance", digest, size, &snap.SideInfo{ + SnapID: "snap-id-other", + Revision: snap.R(12), + }, s.localDB) + c.Check(err, ErrorMatches, `snap "foo_instance" does not have expected ID or revision according to assertions \(metadata is broken or tampered\): 12 / snap-id-other != 12 / snap-id-1`) // changed name err = snapasserts.CrossCheck("baz", digest, size, si, s.localDB) - c.Check(err, ErrorMatches, `cannot install snap "baz" that is undergoing a rename to "foo"`) + c.Check(err, ErrorMatches, `cannot install "baz", snap "baz" is undergoing a rename to "foo"`) + err = snapasserts.CrossCheck("baz_instance", digest, size, si, s.localDB) + c.Check(err, ErrorMatches, `cannot install "baz_instance", snap "baz" is undergoing a rename to "foo"`) } @@ -206,6 +223,8 @@ err = snapasserts.CrossCheck("foo", digest, size, si, s.localDB) c.Check(err, ErrorMatches, `cannot install snap "foo" with a revoked snap declaration`) + err = snapasserts.CrossCheck("foo_instance", digest, size, si, s.localDB) + c.Check(err, ErrorMatches, `cannot install snap "foo_instance" with a revoked snap declaration`) } func (s *snapassertsSuite) TestDeriveSideInfoHappy(c *C) { diff -Nru snapd-2.32.3.2~14.04/asserts/snap_asserts.go snapd-2.37~rc1~14.04/asserts/snap_asserts.go --- snapd-2.32.3.2~14.04/asserts/snap_asserts.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/snap_asserts.go 2019-01-16 08:36:51.000000000 +0000 @@ -152,7 +152,13 @@ if !(plugsOk || slotsOk) { return 0, nil } + formatnum = 1 + setFormatNum := func(num int) { + if num > formatnum { + formatnum = num + } + } plugs, err := checkMap(headers, "plugs") if err != nil { @@ -160,7 +166,10 @@ } err = compilePlugRules(plugs, func(_ string, rule *PlugRule) { if rule.feature(dollarAttrConstraintsFeature) { - formatnum = 2 + setFormatNum(2) + } + if rule.feature(deviceScopeConstraintsFeature) { + setFormatNum(3) } }) if err != nil { @@ -173,7 +182,10 @@ } err = compileSlotRules(slots, func(_ string, rule *SlotRule) { if rule.feature(dollarAttrConstraintsFeature) { - formatnum = 2 + setFormatNum(2) + } + if rule.feature(deviceScopeConstraintsFeature) { + setFormatNum(3) } }) if err != nil { diff -Nru snapd-2.32.3.2~14.04/asserts/snap_asserts_test.go snapd-2.37~rc1~14.04/asserts/snap_asserts_test.go --- snapd-2.32.3.2~14.04/asserts/snap_asserts_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/snap_asserts_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -49,6 +49,12 @@ tsLine string } +type emptyAttrerObject struct{} + +func (o emptyAttrerObject) Lookup(path string) (interface{}, bool) { + return nil, false +} + func (sds *snapDeclSuite) SetUpSuite(c *C) { sds.ts = time.Now().Truncate(time.Second).UTC() sds.tsLine = "timestamp: " + sds.ts.Format(time.RFC3339) + "\n" @@ -298,23 +304,27 @@ c.Assert(plugRule1.DenyInstallation, HasLen, 1) c.Check(plugRule1.DenyInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes) c.Assert(plugRule1.AllowAutoConnection, HasLen, 1) - c.Check(plugRule1.AllowAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "a1".*`) - c.Check(plugRule1.AllowAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "b1".*`) + + plug := emptyAttrerObject{} + slot := emptyAttrerObject{} + + c.Check(plugRule1.AllowAutoConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.AllowAutoConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "b1".*`) c.Check(plugRule1.AllowAutoConnection[0].SlotSnapTypes, DeepEquals, []string{"app"}) c.Check(plugRule1.AllowAutoConnection[0].SlotPublisherIDs, DeepEquals, []string{"acme"}) c.Assert(plugRule1.DenyAutoConnection, HasLen, 1) - c.Check(plugRule1.DenyAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "a1".*`) - c.Check(plugRule1.DenyAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "b1".*`) + c.Check(plugRule1.DenyAutoConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.DenyAutoConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "b1".*`) plugRule2 := snapDecl.PlugRule("interface2") c.Assert(plugRule2, NotNil) c.Assert(plugRule2.AllowInstallation, HasLen, 1) c.Check(plugRule2.AllowInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes) c.Assert(plugRule2.AllowConnection, HasLen, 1) - c.Check(plugRule2.AllowConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "a2".*`) - c.Check(plugRule2.AllowConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "b2".*`) + c.Check(plugRule2.AllowConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.AllowConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "b2".*`) c.Assert(plugRule2.DenyConnection, HasLen, 1) - c.Check(plugRule2.DenyConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "a2".*`) - c.Check(plugRule2.DenyConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "b2".*`) + c.Check(plugRule2.DenyConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.DenyConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "b2".*`) c.Check(plugRule2.DenyConnection[0].SlotSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) slotRule3 := snapDecl.SlotRule("interface3") @@ -322,24 +332,24 @@ c.Assert(slotRule3.DenyInstallation, HasLen, 1) c.Check(slotRule3.DenyInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes) c.Assert(slotRule3.AllowAutoConnection, HasLen, 1) - c.Check(slotRule3.AllowAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "c1".*`) - c.Check(slotRule3.AllowAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "d1".*`) + c.Check(slotRule3.AllowAutoConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.AllowAutoConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "d1".*`) c.Check(slotRule3.AllowAutoConnection[0].PlugSnapTypes, DeepEquals, []string{"app"}) c.Check(slotRule3.AllowAutoConnection[0].PlugPublisherIDs, DeepEquals, []string{"acme"}) c.Assert(slotRule3.DenyAutoConnection, HasLen, 1) - c.Check(slotRule3.DenyAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "c1".*`) - c.Check(slotRule3.DenyAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "d1".*`) + c.Check(slotRule3.DenyAutoConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.DenyAutoConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "d1".*`) slotRule4 := snapDecl.SlotRule("interface4") c.Assert(slotRule4, NotNil) c.Assert(slotRule4.AllowAutoConnection, HasLen, 1) - c.Check(slotRule4.AllowConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "c2".*`) - c.Check(slotRule4.AllowConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "d2".*`) + c.Check(slotRule4.AllowConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.AllowConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "d2".*`) c.Assert(slotRule4.DenyAutoConnection, HasLen, 1) - c.Check(slotRule4.DenyConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "c2".*`) - c.Check(slotRule4.DenyConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "d2".*`) + c.Check(slotRule4.DenyConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.DenyConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "d2".*`) c.Check(slotRule4.DenyConnection[0].PlugSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) c.Assert(slotRule4.AllowInstallation, HasLen, 1) - c.Check(slotRule4.AllowInstallation[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "e1".*`) + c.Check(slotRule4.AllowInstallation[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "e1".*`) c.Check(slotRule4.AllowInstallation[0].SlotSnapTypes, DeepEquals, []string{"app"}) } @@ -396,6 +406,87 @@ c.Assert(err, IsNil) c.Check(fmtnum, Equals, 2) + // combinations with on-store/on-brand/on-model => format 3 + for _, side := range []string{"plugs", "slots"} { + for k, vals := range deviceScopeConstrs { + + headers := map[string]interface{}{ + side: map[string]interface{}{ + "interface3": map[string]interface{}{ + "allow-installation": map[string]interface{}{ + k: vals, + }, + }, + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 3) + + for _, conn := range []string{"connection", "auto-connection"} { + + headers = map[string]interface{}{ + side: map[string]interface{}{ + "interface3": map[string]interface{}{ + "allow-" + conn: map[string]interface{}{ + k: vals, + }, + }, + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 3) + } + } + } + + // higher format features win + + headers = map[string]interface{}{ + "plugs": map[string]interface{}{ + "interface3": map[string]interface{}{ + "allow-auto-connection": map[string]interface{}{ + "on-store": []interface{}{"store"}, + }, + }, + }, + "slots": map[string]interface{}{ + "interface4": map[string]interface{}{ + "allow-auto-connection": map[string]interface{}{ + "plug-attributes": map[string]interface{}{ + "x": "$SLOT(x)", + }, + }, + }, + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 3) + + headers = map[string]interface{}{ + "plugs": map[string]interface{}{ + "interface4": map[string]interface{}{ + "allow-auto-connection": map[string]interface{}{ + "slot-attributes": map[string]interface{}{ + "x": "$SLOT(x)", + }, + }, + }, + }, + "slots": map[string]interface{}{ + "interface3": map[string]interface{}{ + "allow-auto-connection": map[string]interface{}{ + "on-store": []interface{}{"store"}, + }, + }, + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 3) + // errors headers = map[string]interface{}{ "plugs": "what", @@ -1197,28 +1288,31 @@ c.Check(baseDecl.PlugRule("interfaceX"), IsNil) c.Check(baseDecl.SlotRule("interfaceX"), IsNil) + plug := emptyAttrerObject{} + slot := emptyAttrerObject{} + plugRule1 := baseDecl.PlugRule("interface1") c.Assert(plugRule1, NotNil) c.Assert(plugRule1.DenyInstallation, HasLen, 1) c.Check(plugRule1.DenyInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes) c.Assert(plugRule1.AllowAutoConnection, HasLen, 1) - c.Check(plugRule1.AllowAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "a1".*`) - c.Check(plugRule1.AllowAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "b1".*`) + c.Check(plugRule1.AllowAutoConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.AllowAutoConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "b1".*`) c.Check(plugRule1.AllowAutoConnection[0].SlotSnapTypes, DeepEquals, []string{"app"}) c.Check(plugRule1.AllowAutoConnection[0].SlotPublisherIDs, DeepEquals, []string{"acme"}) c.Assert(plugRule1.DenyAutoConnection, HasLen, 1) - c.Check(plugRule1.DenyAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "a1".*`) - c.Check(plugRule1.DenyAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "b1".*`) + c.Check(plugRule1.DenyAutoConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.DenyAutoConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "b1".*`) plugRule2 := baseDecl.PlugRule("interface2") c.Assert(plugRule2, NotNil) c.Assert(plugRule2.AllowInstallation, HasLen, 1) c.Check(plugRule2.AllowInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes) c.Assert(plugRule2.AllowConnection, HasLen, 1) - c.Check(plugRule2.AllowConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "a2".*`) - c.Check(plugRule2.AllowConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "b2".*`) + c.Check(plugRule2.AllowConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.AllowConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "b2".*`) c.Assert(plugRule2.DenyConnection, HasLen, 1) - c.Check(plugRule2.DenyConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "a2".*`) - c.Check(plugRule2.DenyConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "b2".*`) + c.Check(plugRule2.DenyConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.DenyConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "b2".*`) c.Check(plugRule2.DenyConnection[0].SlotSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) slotRule3 := baseDecl.SlotRule("interface3") @@ -1226,24 +1320,24 @@ c.Assert(slotRule3.DenyInstallation, HasLen, 1) c.Check(slotRule3.DenyInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes) c.Assert(slotRule3.AllowAutoConnection, HasLen, 1) - c.Check(slotRule3.AllowAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "c1".*`) - c.Check(slotRule3.AllowAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "d1".*`) + c.Check(slotRule3.AllowAutoConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.AllowAutoConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "d1".*`) c.Check(slotRule3.AllowAutoConnection[0].PlugSnapTypes, DeepEquals, []string{"app"}) c.Check(slotRule3.AllowAutoConnection[0].PlugPublisherIDs, DeepEquals, []string{"acme"}) c.Assert(slotRule3.DenyAutoConnection, HasLen, 1) - c.Check(slotRule3.DenyAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "c1".*`) - c.Check(slotRule3.DenyAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "d1".*`) + c.Check(slotRule3.DenyAutoConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.DenyAutoConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "d1".*`) slotRule4 := baseDecl.SlotRule("interface4") c.Assert(slotRule4, NotNil) c.Assert(slotRule4.AllowConnection, HasLen, 1) - c.Check(slotRule4.AllowConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "c2".*`) - c.Check(slotRule4.AllowConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "d2".*`) + c.Check(slotRule4.AllowConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.AllowConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "d2".*`) c.Assert(slotRule4.DenyConnection, HasLen, 1) - c.Check(slotRule4.DenyConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "c2".*`) - c.Check(slotRule4.DenyConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "d2".*`) + c.Check(slotRule4.DenyConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.DenyConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "d2".*`) c.Check(slotRule4.DenyConnection[0].PlugSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) c.Assert(slotRule4.AllowInstallation, HasLen, 1) - c.Check(slotRule4.AllowInstallation[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "e1".*`) + c.Check(slotRule4.AllowInstallation[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "e1".*`) c.Check(slotRule4.AllowInstallation[0].SlotSnapTypes, DeepEquals, []string{"app"}) } diff -Nru snapd-2.32.3.2~14.04/asserts/store_asserts.go snapd-2.37~rc1~14.04/asserts/store_asserts.go --- snapd-2.32.3.2~14.04/asserts/store_asserts.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/store_asserts.go 2019-01-16 08:36:51.000000000 +0000 @@ -26,11 +26,12 @@ ) // Store holds a store assertion, defining the configuration needed to connect -// a device to the store. +// a device to the store or relative to a non-default store. type Store struct { assertionBase - url *url.URL - timestamp time.Time + url *url.URL + friendlyStores []string + timestamp time.Time } // Store returns the identifying name of the operator's store. @@ -48,6 +49,12 @@ return store.url } +// FriendlyStores returns stores holding snaps that are also exposed +// through this one. +func (store *Store) FriendlyStores() []string { + return store.friendlyStores +} + // Location returns a summary of the store's location/purpose. func (store *Store) Location() string { return store.HeaderString("location") @@ -59,7 +66,9 @@ } func (store *Store) checkConsistency(db RODatabase, acck *AccountKey) error { - // Will be applied to a system's snapd so must be signed by a trusted authority. + // Will be applied to a system's snapd or influence snapd + // policy decisions (via friendly-stores) so must be signed by a trusted + // authority! if !db.IsTrustedAccount(store.AuthorityID()) { return fmt.Errorf("store assertion %q is not signed by a directly trusted authority: %s", store.Store(), store.AuthorityID()) @@ -129,6 +138,11 @@ return nil, err } + friendlyStores, err := checkStringList(assert.headers, "friendly-stores") + if err != nil { + return nil, err + } + _, err = checkOptionalString(assert.headers, "location") if err != nil { return nil, err @@ -140,8 +154,9 @@ } return &Store{ - assertionBase: assert, - url: url, - timestamp: timestamp, + assertionBase: assert, + url: url, + friendlyStores: friendlyStores, + timestamp: timestamp, }, nil } diff -Nru snapd-2.32.3.2~14.04/asserts/store_asserts_test.go snapd-2.37~rc1~14.04/asserts/store_asserts_test.go --- snapd-2.32.3.2~14.04/asserts/store_asserts_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/store_asserts_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -63,6 +63,7 @@ c.Check(store.URL().String(), Equals, "https://store.example.com") c.Check(store.Location(), Equals, "upstairs") c.Check(store.Timestamp().Equal(s.ts), Equals, true) + c.Check(store.FriendlyStores(), HasLen, 0) } var storeErrPrefix = "assertion store: " @@ -78,6 +79,7 @@ {s.tsLine, "", `"timestamp" header is mandatory`}, {s.tsLine, "timestamp: \n", `"timestamp" header should not be empty`}, {s.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + {"url: https://store.example.com\n", "friendly-stores: foo\n", `"friendly-stores" header must be a list of strings`}, } for _, test := range tests { @@ -187,6 +189,19 @@ c.Assert(err, IsNil) } +func (s *storeSuite) TestFriendlyStores(c *C) { + encoded := strings.Replace(s.validExample, "url: https://store.example.com\n", `friendly-stores: + - store1 + - store2 + - store3 +`, 1) + assert, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + store := assert.(*asserts.Store) + c.Check(store.URL(), IsNil) + c.Check(store.FriendlyStores(), DeepEquals, []string{"store1", "store2", "store3"}) +} + func (s *storeSuite) TestCheckOperatorAccount(c *C) { storeDB, db := makeStoreAndCheckDB(c) diff -Nru snapd-2.32.3.2~14.04/asserts/sysdb/sysdb_test.go snapd-2.37~rc1~14.04/asserts/sysdb/sysdb_test.go --- snapd-2.32.3.2~14.04/asserts/sysdb/sysdb_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/sysdb/sysdb_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -54,7 +54,7 @@ trustedAcct := assertstest.NewAccount(signingDB, "can0nical", map[string]interface{}{ "account-id": "can0nical", - "validation": "certified", + "validation": "verified", "timestamp": "2015-11-20T15:04:00Z", }, "") @@ -68,7 +68,7 @@ otherAcct := assertstest.NewAccount(signingDB, "gener1c", map[string]interface{}{ "account-id": "gener1c", - "validation": "certified", + "validation": "verified", "timestamp": "2015-11-20T15:04:00Z", }, "") @@ -152,6 +152,8 @@ }) c.Assert(err, IsNil) + c.Check(trustedAcc.(*asserts.Account).Validation(), Equals, "verified") + err = db.Check(trustedAcc) c.Check(err, IsNil) @@ -166,6 +168,8 @@ }) c.Assert(err, IsNil) + c.Check(genericAcc.(*asserts.Account).Validation(), Equals, "verified") + err = db.Check(genericAcc) c.Check(err, IsNil) diff -Nru snapd-2.32.3.2~14.04/asserts/systestkeys/trusted.go snapd-2.37~rc1~14.04/asserts/systestkeys/trusted.go --- snapd-2.32.3.2~14.04/asserts/systestkeys/trusted.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/systestkeys/trusted.go 2019-01-16 08:36:51.000000000 +0000 @@ -89,21 +89,21 @@ authority-id: testrootorg account-id: testrootorg display-name: Testrootorg -timestamp: 2016-08-11T18:30:57+02:00 +timestamp: 2018-06-27T14:25:40+02:00 username: testrootorg -validation: certified +validation: verified sign-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR -AcLBUgQAAQoABgUCV6yoQQAAelEQAEdSECpdmV5a2G5VMBzJFuHQUU1FzgZ7gPQjc3l0BibDWm8O -rDi7IT3L80OkqS2AoQgHS5KtEvKqEmhyfcdzcXgvCkHR5kucRBJJaPy8z6gGMhzZIPlc+EqY+Cvb -/MQPLvtYYvtAxq1vWz+aDGGwk2Z/dFUG+wofvNWodz400gYTZeFOCZwStBD84S7iY/3pMQgC3+SO -QMr/VI+bgmOukFqZL0cX4ReiuUs2W45V6EC81UGBjk+k7AVTEXMR1Xo8f0yiRzlLoEdKQMCOC45Q -n4eedjCToGRPFcktM0QhgfbcpPIQKHNqKGGvtQQXvW5PIZ7AS4rTfQScXTn1dqDsL/ZVdasvOpCP -5o4WvoWMoU8+Hm4n6ckw4sXn//PZIQrtnkp2DO+9JXXZasIPg4k1mvUQ5Kb9qCcBbaM+OO1izOoC -3PY8xHNQNfHNHwBMewhnU2NpdTS0mTepN/8iFsDT1vSZ28OE2hgbu1ltqx4AsRkCVyFFx6N6OYm2 -UDNozU9K5w0NY4u9HSTDz4KrBIalAaKY72CIUqeVsmAcYatXglbj7dVTZTw75M0v1thQiSoKFqHw -CHykZ6BJRgminY1FqOg7tvqTwzYM7lwaE3K8JpAyzie7v+OSLSxy1vlwUmT2lT+h1i28/w+r+R3Q -C0QC8xuHSvOv3YRtzKna3smAfRlB +AcLBUgQAAQoABgUCWzOCRAAA31gQAE5QgyBuxF3DGlMP32+3G5soq0uDLKG+sqFIEj/8j1dwLG0u +ut7UPEf5iTZFDqqyAaFBRUPxx1cGB/6WFrks3X3/325hVzv5DYA9d4508BXdlNBA++t7tTdb4rU6 +G57aVgbpMCdwdjRbRMv1LLVWnli1pj8Cvt/jiTMbJUwQ/CAO0UZA6EH+fAeHGB53NNvedAM1goWi +pS3XvruDtv8qTbVW9jNSIX1ADcLAbmM2xV2Vo54lfgN5NJd/4K4S7sPSsX7QLBghFkB0m9i5g/Qu +PecvJ9njebFF48yvG4W1owBNBfxD2oHNhK/GdtxsREDKgDXuIrhziXBzWNeYto8lCZ1D520k+xp+ +2rL1TYSy9IixOzAf2qBhUTQdXsoVfmBOyExlYVQDIFO+X4ufbhLzy2pTE4KWvFvF58HzGradbix6 +oUD5hiEjw1YoV8FKdMLDobcvGzgm+Kx/FQo2Iqm5GmfzPW/K3SntoptuHIDSk3B12F/F/EDoYiGS +MDWJJ4NMbFLMemJhvEI23IuZOTBEt27sGcgOju4wYkcsaHPEeXTGUBUgQADugBTwJtWmuybkmovM +aLn1kVYpht+0cZeAQR5L3nSOK7T3V+QvWSXt6PAiHJv+HnemrYarSmGVTDcpj1QuWXyX026RnkvP +SD73HCe5QPTjrvFvIa6o6n9khFgs ` TestRootKeyID = "hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR" diff -Nru snapd-2.32.3.2~14.04/asserts/user.go snapd-2.37~rc1~14.04/asserts/user.go --- snapd-2.32.3.2~14.04/asserts/user.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/user.go 2019-01-16 08:36:51.000000000 +0000 @@ -39,6 +39,8 @@ sshKeys []string since time.Time until time.Time + + forcePasswordChange bool } // BrandID returns the brand identifier that signed this assertion. @@ -77,6 +79,12 @@ return su.HeaderString("password") } +// ForcePasswordChange returns true if the user needs to change the password +// after the first login. +func (su *SystemUser) ForcePasswordChange() bool { + return su.forcePasswordChange +} + // SSHKeys returns the ssh keys for the user. func (su *SystemUser) SSHKeys() []string { return su.sshKeys @@ -150,6 +158,9 @@ }, nil } +// see crypt(3) for the legal chars +var isValidSaltAndHash = regexp.MustCompile(`^[a-zA-Z0-9./]+$`).MatchString + func checkHashedPassword(headers map[string]interface{}, name string) (string, error) { pw, err := checkOptionalString(headers, name) if err != nil { @@ -189,12 +200,10 @@ } } - // see crypt(3) for the legal chars - validSaltAndHash := regexp.MustCompile(`^[a-zA-Z0-9./]+$`) - if !validSaltAndHash.MatchString(shd.Salt) { + if !isValidSaltAndHash(shd.Salt) { return "", fmt.Errorf("%q header has invalid chars in salt %q", name, shd.Salt) } - if !validSaltAndHash.MatchString(shd.Hash) { + if !isValidSaltAndHash(shd.Hash) { return "", fmt.Errorf("%q header has invalid chars in hash %q", name, shd.Hash) } @@ -227,9 +236,17 @@ if _, err := checkStringMatches(assert.headers, "username", validSystemUserUsernames); err != nil { return nil, err } - if _, err := checkHashedPassword(assert.headers, "password"); err != nil { + password, err := checkHashedPassword(assert.headers, "password") + if err != nil { return nil, err } + forcePasswordChange, err := checkOptionalBool(assert.headers, "force-password-change") + if err != nil { + return nil, err + } + if forcePasswordChange && password == "" { + return nil, fmt.Errorf(`cannot use "force-password-change" with an empty "password"`) + } sshKeys, err := checkStringList(assert.headers, "ssh-keys") if err != nil { @@ -253,11 +270,12 @@ } return &SystemUser{ - assertionBase: assert, - series: series, - models: models, - sshKeys: sshKeys, - since: since, - until: until, + assertionBase: assert, + series: series, + models: models, + sshKeys: sshKeys, + since: since, + until: until, + forcePasswordChange: forcePasswordChange, }, nil } diff -Nru snapd-2.32.3.2~14.04/asserts/user_test.go snapd-2.37~rc1~14.04/asserts/user_test.go --- snapd-2.32.3.2~14.04/asserts/user_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/asserts/user_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -102,6 +102,18 @@ } } +func (s *systemUserSuite) TestDecodeForcePasswdChange(c *C) { + + old := "password: $6$salt$hash\n" + new := "password: $6$salt$hash\nforce-password-change: true\n" + + valid := strings.Replace(s.systemUserStr, old, new, 1) + a, err := asserts.Decode([]byte(valid)) + c.Check(err, IsNil) + systemUser := a.(*asserts.SystemUser) + c.Check(systemUser.ForcePasswordChange(), Equals, true) +} + func (s *systemUserSuite) TestValidAt(c *C) { a, err := asserts.Decode([]byte(s.systemUserStr)) c.Assert(err, IsNil) @@ -164,6 +176,8 @@ {"password: $6$salt$hash\n", "password: $8$rounds=xxx$salt$hash\n", `"password" header has invalid number of rounds:.*`}, {"password: $6$salt$hash\n", "password: $8$rounds=1$salt$hash\n", `"password" header rounds parameter out of bounds: 1`}, {"password: $6$salt$hash\n", "password: $8$rounds=1999999999$salt$hash\n", `"password" header rounds parameter out of bounds: 1999999999`}, + {"password: $6$salt$hash\n", "force-password-change: true\n", `cannot use "force-password-change" with an empty "password"`}, + {"password: $6$salt$hash\n", "password: $6$salt$hash\nforce-password-change: xxx\n", `"force-password-change" header must be 'true' or 'false'`}, {s.sinceLine, "since: \n", `"since" header should not be empty`}, {s.sinceLine, "since: 12:30\n", `"since" header is not a RFC3339 date: .*`}, {s.untilLine, "until: \n", `"until" header should not be empty`}, diff -Nru snapd-2.32.3.2~14.04/boot/kernel_os.go snapd-2.37~rc1~14.04/boot/kernel_os.go --- snapd-2.32.3.2~14.04/boot/kernel_os.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/boot/kernel_os.go 2019-01-16 08:36:51.000000000 +0000 @@ -22,7 +22,6 @@ import ( "fmt" "os" - "os/exec" "path/filepath" "github.com/snapcore/snapd/logger" @@ -49,13 +48,6 @@ return nil } -func copyAll(src, dst string) error { - if output, err := exec.Command("cp", "-aLv", src, dst).CombinedOutput(); err != nil { - return fmt.Errorf("cannot copy %q -> %q: %s (%s)", src, dst, err, output) - } - return nil -} - // ExtractKernelAssets extracts kernel/initrd/dtb data from the given // kernel snap, if required, to a versioned bootloader directory so // that the bootloader can use it. @@ -100,14 +92,16 @@ return dir.Sync() } -// SetNextBoot will schedule the given OS or kernel snap to be used in -// the next boot +// SetNextBoot will schedule the given OS or base or kernel snap to be +// used in the next boot. For base snaps it up to the caller to select +// the right bootable base (from the model assertion). func SetNextBoot(s *snap.Info) error { if release.OnClassic { - return nil + return fmt.Errorf("cannot set next boot on classic systems") } - if s.Type != snap.TypeOS && s.Type != snap.TypeKernel { - return nil + + if s.Type != snap.TypeOS && s.Type != snap.TypeKernel && s.Type != snap.TypeBase { + return fmt.Errorf("cannot set next boot to snap %q with type %q", s.SnapName(), s.Type) } bootloader, err := partition.FindBootloader() @@ -117,7 +111,7 @@ var nextBoot, goodBoot string switch s.Type { - case snap.TypeOS: + case snap.TypeOS, snap.TypeBase: nextBoot = "snap_try_core" goodBoot = "snap_core" case snap.TypeKernel: @@ -128,11 +122,22 @@ // check if we actually need to do anything, i.e. the exact same // kernel/core revision got installed again (e.g. firstboot) - m, err := bootloader.GetBootVars(goodBoot) + // and we are not in any special boot mode + m, err := bootloader.GetBootVars("snap_mode", goodBoot) if err != nil { return err } if m[goodBoot] == blobName { + // If we were in anything but default ("") mode before + // and now switch to the good core/kernel again, make + // sure to clean the snap_mode here. This also + // mitigates https://forum.snapcraft.io/t/5253 + if m["snap_mode"] != "" { + return bootloader.SetBootVars(map[string]string{ + "snap_mode": "", + nextBoot: "", + }) + } return nil } @@ -142,9 +147,10 @@ }) } -// KernelOrOsRebootRequired returns whether a reboot is required to swith to the given OS or kernel snap. -func KernelOrOsRebootRequired(s *snap.Info) bool { - if s.Type != snap.TypeKernel && s.Type != snap.TypeOS { +// ChangeRequiresReboot returns whether a reboot is required to switch +// to the given OS, base or kernel snap. +func ChangeRequiresReboot(s *snap.Info) bool { + if s.Type != snap.TypeKernel && s.Type != snap.TypeOS && s.Type != snap.TypeBase { return false } @@ -159,7 +165,7 @@ case snap.TypeKernel: nextBoot = "snap_try_kernel" goodBoot = "snap_kernel" - case snap.TypeOS: + case snap.TypeOS, snap.TypeBase: nextBoot = "snap_try_core" goodBoot = "snap_core" } diff -Nru snapd-2.32.3.2~14.04/boot/kernel_os_test.go snapd-2.37~rc1~14.04/boot/kernel_os_test.go --- snapd-2.32.3.2~14.04/boot/kernel_os_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/boot/kernel_os_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -156,7 +156,7 @@ // Create a fake OS snap that we try to update snapInfo := snaptest.MockSnap(c, "name: os\ntype: os", &snap.SideInfo{Revision: snap.R(42)}) err := boot.SetNextBoot(snapInfo) - c.Assert(err, IsNil) + c.Assert(err, ErrorMatches, "cannot set next boot on classic systems") c.Assert(s.bootloader.BootVars, HasLen, 0) } @@ -178,7 +178,27 @@ "snap_mode": "try", }) - c.Check(boot.KernelOrOsRebootRequired(info), Equals, true) + c.Check(boot.ChangeRequiresReboot(info), Equals, true) +} + +func (s *kernelOSSuite) TestSetNextBootWithBaseForCore(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + info := &snap.Info{} + info.Type = snap.TypeBase + info.RealName = "core18" + info.Revision = snap.R(1818) + + err := boot.SetNextBoot(info) + c.Assert(err, IsNil) + + c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ + "snap_try_core": "core18_1818.snap", + "snap_mode": "try", + }) + + c.Check(boot.ChangeRequiresReboot(info), Equals, true) } func (s *kernelOSSuite) TestSetNextBootForKernel(c *C) { @@ -200,11 +220,11 @@ s.bootloader.BootVars["snap_kernel"] = "krnl_40.snap" s.bootloader.BootVars["snap_try_kernel"] = "krnl_42.snap" - c.Check(boot.KernelOrOsRebootRequired(info), Equals, true) + c.Check(boot.ChangeRequiresReboot(info), Equals, true) // simulate good boot s.bootloader.BootVars["snap_kernel"] = "krnl_42.snap" - c.Check(boot.KernelOrOsRebootRequired(info), Equals, false) + c.Check(boot.ChangeRequiresReboot(info), Equals, false) } func (s *kernelOSSuite) TestSetNextBootForKernelForTheSameKernel(c *C) { @@ -226,6 +246,29 @@ }) } +func (s *kernelOSSuite) TestSetNextBootForKernelForTheSameKernelTryMode(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + info := &snap.Info{} + info.Type = snap.TypeKernel + info.RealName = "krnl" + info.Revision = snap.R(40) + + s.bootloader.BootVars["snap_kernel"] = "krnl_40.snap" + s.bootloader.BootVars["snap_try_kernel"] = "krnl_99.snap" + s.bootloader.BootVars["snap_mode"] = "try" + + err := boot.SetNextBoot(info) + c.Assert(err, IsNil) + + c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ + "snap_kernel": "krnl_40.snap", + "snap_try_kernel": "", + "snap_mode": "", + }) +} + func (s *kernelOSSuite) TestInUse(c *C) { for _, t := range []struct { bootVarKey string diff -Nru snapd-2.32.3.2~14.04/client/apps.go snapd-2.37~rc1~14.04/client/apps.go --- snapd-2.32.3.2~14.04/client/apps.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/client/apps.go 2019-01-16 08:36:51.000000000 +0000 @@ -31,14 +31,26 @@ "time" ) +// AppActivator is a thing that activates the app that is a service in the +// system. +type AppActivator struct { + Name string + // Type describes the type of the unit, either timer or socket + Type string + Active bool + Enabled bool +} + // AppInfo describes a single snap application. type AppInfo struct { - Snap string `json:"snap,omitempty"` - Name string `json:"name"` - DesktopFile string `json:"desktop-file,omitempty"` - Daemon string `json:"daemon,omitempty"` - Enabled bool `json:"enabled,omitempty"` - Active bool `json:"active,omitempty"` + Snap string `json:"snap,omitempty"` + Name string `json:"name"` + DesktopFile string `json:"desktop-file,omitempty"` + Daemon string `json:"daemon,omitempty"` + Enabled bool `json:"enabled,omitempty"` + Active bool `json:"active,omitempty"` + CommonID string `json:"common-id,omitempty"` + Activators []AppActivator `json:"activators,omitempty"` } // IsService returns true if the application is a background daemon. @@ -112,6 +124,15 @@ return nil, err } + if rsp.StatusCode != 200 { + var r response + defer rsp.Body.Close() + if err := decodeInto(rsp.Body, &r); err != nil { + return nil, err + } + return nil, r.err(client) + } + ch := make(chan Log, 20) go func() { // logs come in application/json-seq, described in RFC7464: it's diff -Nru snapd-2.32.3.2~14.04/client/apps_test.go snapd-2.37~rc1~14.04/client/apps_test.go --- snapd-2.32.3.2~14.04/client/apps_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/client/apps_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -87,6 +87,22 @@ } } +func (cs *clientSuite) TestClientAppCommonID(c *check.C) { + expected := []*client.AppInfo{{ + Snap: "foo", + Name: "foo", + CommonID: "org.foo", + }} + buf, err := json.Marshal(expected) + c.Assert(err, check.IsNil) + cs.rsp = fmt.Sprintf(`{"type": "sync", "result": %s}`, buf) + for _, chkr := range appcheckers { + actual, err := chkr(cs, c) + c.Assert(err, check.IsNil) + c.Check(actual, check.DeepEquals, expected) + } +} + func testClientLogs(cs *clientSuite, c *check.C) ([]client.Log, error) { ch, err := cs.cli.Logs([]string{"foo", "bar"}, client.LogOptions{N: -1, Follow: false}) c.Check(cs.req.URL.Path, check.Equals, "/v2/logs") @@ -188,6 +204,14 @@ } } +func (cs *clientSuite) TestClientLogsNotFound(c *check.C) { + cs.rsp = `{"type":"error","status-code":404,"status":"Not Found","result":{"message":"snap \"foo\" not found","kind":"snap-not-found","value":"foo"}}` + cs.status = 404 + actual, err := testClientLogs(cs, c) + c.Assert(err, check.ErrorMatches, `snap "foo" not found`) + c.Check(actual, check.HasLen, 0) +} + func (cs *clientSuite) TestClientServiceStart(c *check.C) { cs.rsp = `{"type": "async", "status-code": 202, "change": "24"}` diff -Nru snapd-2.32.3.2~14.04/client/buy.go snapd-2.37~rc1~14.04/client/buy.go --- snapd-2.32.3.2~14.04/client/buy.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/client/buy.go 2019-01-16 08:36:51.000000000 +0000 @@ -22,13 +22,23 @@ import ( "bytes" "encoding/json" - - "github.com/snapcore/snapd/store" ) -func (client *Client) Buy(opts *store.BuyOptions) (*store.BuyResult, error) { +// BuyOptions specifies parameters to buy from the store. +type BuyOptions struct { + SnapID string `json:"snap-id"` + Price float64 `json:"price"` + Currency string `json:"currency"` // ISO 4217 code as string +} + +// BuyResult holds the state of a buy attempt. +type BuyResult struct { + State string `json:"state,omitempty"` +} + +func (client *Client) Buy(opts *BuyOptions) (*BuyResult, error) { if opts == nil { - opts = &store.BuyOptions{} + opts = &BuyOptions{} } var body bytes.Buffer @@ -36,7 +46,7 @@ return nil, err } - var result store.BuyResult + var result BuyResult _, err := client.doSync("POST", "/v2/buy", nil, nil, &body, &result) if err != nil { diff -Nru snapd-2.32.3.2~14.04/client/change.go snapd-2.37~rc1~14.04/client/change.go --- snapd-2.32.3.2~14.04/client/change.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/client/change.go 2019-01-16 08:36:51.000000000 +0000 @@ -78,7 +78,7 @@ Data map[string]*json.RawMessage `json:"data"` } -// Change fetches information about a Change given its ID +// Change fetches information about a Change given its ID. func (client *Client) Change(id string) (*Change, error) { var chgd changeAndData _, err := client.doSync("GET", "/v2/changes/"+id, nil, nil, nil, &chgd) diff -Nru snapd-2.32.3.2~14.04/client/change_test.go snapd-2.37~rc1~14.04/client/change_test.go --- snapd-2.32.3.2~14.04/client/change_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/client/change_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -81,6 +81,24 @@ c.Assert(err, check.Equals, client.ErrNoData) } +func (cs *clientSuite) TestClientChangeRestartingState(c *check.C) { + cs.rsp = `{"type": "sync", "result": { + "id": "uno", + "kind": "foo", + "summary": "...", + "status": "Do", + "ready": false +}, + "maintenance": {"kind": "system-restart", "message": "system is restarting"} +}` + + chg, err := cs.cli.Change("uno") + c.Check(chg, check.NotNil) + c.Check(chg.ID, check.Equals, "uno") + c.Check(err, check.IsNil) + c.Check(cs.cli.Maintenance(), check.ErrorMatches, `system is restarting`) +} + func (cs *clientSuite) TestClientChangeError(c *check.C) { cs.rsp = `{"type": "sync", "result": { "id": "uno", diff -Nru snapd-2.32.3.2~14.04/client/client.go snapd-2.37~rc1~14.04/client/client.go --- snapd-2.32.3.2~14.04/client/client.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/client/client.go 2019-01-16 08:36:51.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2015-2016 Canonical Ltd + * Copyright (C) 2015-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -66,6 +66,10 @@ // Socket is the path to the unix socket to use Socket string + + // DisableKeepAlive indicates whether the connections should not be kept + // alive for later reuse + DisableKeepAlive bool } // A Client knows how to talk to the snappy daemon. @@ -75,6 +79,11 @@ disableAuth bool interactive bool + + maintenance error + + warningCount int + warningTimestamp time.Time } // New returns a new instance of Client @@ -85,14 +94,13 @@ // By default talk over an UNIX socket. if config.BaseURL == "" { + transport := &http.Transport{Dial: unixDialer(config.Socket), DisableKeepAlives: config.DisableKeepAlive} return &Client{ baseURL: url.URL{ Scheme: "http", Host: "localhost", }, - doer: &http.Client{ - Transport: &http.Transport{Dial: unixDialer(config.Socket)}, - }, + doer: &http.Client{Transport: transport}, disableAuth: config.DisableAuth, interactive: config.Interactive, } @@ -104,12 +112,24 @@ } return &Client{ baseURL: *baseURL, - doer: &http.Client{}, + doer: &http.Client{Transport: &http.Transport{DisableKeepAlives: config.DisableKeepAlive}}, disableAuth: config.DisableAuth, interactive: config.Interactive, } } +// Maintenance returns an error reflecting the daemon maintenance status or nil. +func (client *Client) Maintenance() error { + return client.maintenance +} + +// WarningsSummary returns the number of warnings that are ready to be shown to +// the user, and the timestamp of the most recently added warning (useful for +// silencing the warning alerts, and OKing the returned warnings). +func (client *Client) WarningsSummary() (count int, timestamp time.Time) { + return client.warningCount, client.warningTimestamp +} + func (client *Client) WhoAmI() (string, error) { user, err := readAuthData() if os.IsNotExist(err) { @@ -216,6 +236,19 @@ } } +type hijacked struct { + do func(*http.Request) (*http.Response, error) +} + +func (h hijacked) Do(req *http.Request) (*http.Response, error) { + return h.do(req) +} + +// Hijack lets the caller take over the raw http request +func (client *Client) Hijack(f func(*http.Request) (*http.Response, error)) { + client.doer = hijacked{f} +} + // do performs a request and decodes the resulting json into the given // value. It's low-level, for testing/experimenting only; you should // usually use a higher level interface that builds on this. @@ -243,29 +276,37 @@ defer rsp.Body.Close() if v != nil { - dec := json.NewDecoder(rsp.Body) - if err := dec.Decode(v); err != nil { - r := dec.Buffered() - buf, err1 := ioutil.ReadAll(r) - if err1 != nil { - buf = []byte(fmt.Sprintf("error reading buffered response body: %s", err1)) - } - return fmt.Errorf("cannot decode %q: %s", buf, err) + if err := decodeInto(rsp.Body, v); err != nil { + return err } } return nil } +func decodeInto(reader io.Reader, v interface{}) error { + dec := json.NewDecoder(reader) + if err := dec.Decode(v); err != nil { + r := dec.Buffered() + buf, err1 := ioutil.ReadAll(r) + if err1 != nil { + buf = []byte(fmt.Sprintf("error reading buffered response body: %s", err1)) + } + return fmt.Errorf("cannot decode %q: %s", buf, err) + } + return nil +} + // doSync performs a request to the given path using the specified HTTP method. // It expects a "sync" response from the API and on success decodes the JSON -// response payload into the given value. +// response payload into the given value using the "UseNumber" json decoding +// which produces json.Numbers instead of float64 types for numbers. func (client *Client) doSync(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) (*ResultInfo, error) { var rsp response if err := client.do(method, path, query, headers, body, &rsp); err != nil { return nil, err } - if err := rsp.err(); err != nil { + if err := rsp.err(client); err != nil { return nil, err } if rsp.Type != "sync" { @@ -278,29 +319,37 @@ } } + client.warningCount = rsp.WarningCount + client.warningTimestamp = rsp.WarningTimestamp + return &rsp.ResultInfo, nil } func (client *Client) doAsync(method, path string, query url.Values, headers map[string]string, body io.Reader) (changeID string, err error) { + _, changeID, err = client.doAsyncFull(method, path, query, headers, body) + return +} + +func (client *Client) doAsyncFull(method, path string, query url.Values, headers map[string]string, body io.Reader) (result json.RawMessage, changeID string, err error) { var rsp response if err := client.do(method, path, query, headers, body, &rsp); err != nil { - return "", err + return nil, "", err } - if err := rsp.err(); err != nil { - return "", err + if err := rsp.err(client); err != nil { + return nil, "", err } if rsp.Type != "async" { - return "", fmt.Errorf("expected async response for %q on %q, got %q", method, path, rsp.Type) + return nil, "", fmt.Errorf("expected async response for %q on %q, got %q", method, path, rsp.Type) } if rsp.StatusCode != 202 { - return "", fmt.Errorf("operation not accepted") + return nil, "", fmt.Errorf("operation not accepted") } if rsp.Change == "" { - return "", fmt.Errorf("async response without change reference") + return nil, "", fmt.Errorf("async response without change reference") } - return rsp.Change, nil + return rsp.Result, rsp.Change, nil } type ServerVersion struct { @@ -339,7 +388,12 @@ Type string `json:"type"` Change string `json:"change"` + WarningCount int `json:"warning-count"` + WarningTimestamp time.Time `json:"warning-timestamp"` + ResultInfo + + Maintenance *Error `json:"maintenance"` } // Error is the real value of response.Result when an error occurs. @@ -359,6 +413,7 @@ ErrorKindTwoFactorRequired = "two-factor-required" ErrorKindTwoFactorFailed = "two-factor-failed" ErrorKindLoginRequired = "login-required" + ErrorKindInvalidAuthData = "invalid-auth-data" ErrorKindTermsNotAccepted = "terms-not-accepted" ErrorKindNoPaymentMethods = "no-payment-methods" ErrorKindPaymentDeclined = "payment-declined" @@ -367,18 +422,44 @@ ErrorKindSnapAlreadyInstalled = "snap-already-installed" ErrorKindSnapNotInstalled = "snap-not-installed" ErrorKindSnapNotFound = "snap-not-found" + ErrorKindAppNotFound = "app-not-found" ErrorKindSnapLocal = "snap-local" ErrorKindSnapNeedsDevMode = "snap-needs-devmode" ErrorKindSnapNeedsClassic = "snap-needs-classic" ErrorKindSnapNeedsClassicSystem = "snap-needs-classic-system" + ErrorKindSnapNotClassic = "snap-not-classic" ErrorKindNoUpdateAvailable = "snap-no-update-available" + ErrorKindRevisionNotAvailable = "snap-revision-not-available" + ErrorKindChannelNotAvailable = "snap-channel-not-available" + ErrorKindArchitectureNotAvailable = "snap-architecture-not-available" + + ErrorKindChangeConflict = "snap-change-conflict" + ErrorKindNotSnap = "snap-not-a-snap" - ErrorKindNetworkTimeout = "network-timeout" + ErrorKindNetworkTimeout = "network-timeout" + ErrorKindDNSFailure = "dns-failure" + ErrorKindInterfacesUnchanged = "interfaces-unchanged" + + ErrorKindBadQuery = "bad-query" + ErrorKindConfigNoSuchOption = "option-not-found" + + ErrorKindSystemRestart = "system-restart" + ErrorKindDaemonRestart = "daemon-restart" ) +// IsRetryable returns true if the given error is an error +// that can be retried later. +func IsRetryable(err error) bool { + switch e := err.(type) { + case *Error: + return e.Kind == ErrorKindChangeConflict + } + return false +} + // IsTwoFactorError returns whether the given error is due to problems // in two-factor authentication. func IsTwoFactorError(err error) bool { @@ -421,17 +502,28 @@ type SysInfo struct { Series string `json:"series,omitempty"` Version string `json:"version,omitempty"` + BuildID string `json:"build-id"` OSRelease OSRelease `json:"os-release"` OnClassic bool `json:"on-classic"` Managed bool `json:"managed"` KernelVersion string `json:"kernel-version,omitempty"` - Refresh RefreshInfo `json:"refresh,omitempty"` - Confinement string `json:"confinement"` + Refresh RefreshInfo `json:"refresh,omitempty"` + Confinement string `json:"confinement"` + SandboxFeatures map[string][]string `json:"sandbox-features,omitempty"` } -func (rsp *response) err() error { +func (rsp *response) err(cli *Client) error { + if cli != nil { + maintErr := rsp.Maintenance + // avoid setting to (*client.Error)(nil) + if maintErr != nil { + cli.maintenance = maintErr + } else { + cli.maintenance = nil + } + } if rsp.Type != "error" { return nil } @@ -456,7 +548,7 @@ return fmt.Errorf("cannot unmarshal error: %v", err) } - err := rsp.err() + err := rsp.err(nil) if err == nil { return fmt.Errorf("server error: %q", r.Status) } diff -Nru snapd-2.32.3.2~14.04/client/client_test.go snapd-2.37~rc1~14.04/client/client_test.go --- snapd-2.32.3.2~14.04/client/client_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/client/client_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -51,6 +51,7 @@ doCalls int header http.Header status int + restore func() } var _ = Suite(&clientSuite{}) @@ -70,10 +71,13 @@ cs.doCalls = 0 dirs.SetRootDir(c.MkDir()) + + cs.restore = client.MockDoRetry(time.Millisecond, 10*time.Millisecond) } func (cs *clientSuite) TearDownTest(c *C) { os.Unsetenv(client.TestAuthFileEnvKey) + cs.restore() } func (cs *clientSuite) Do(req *http.Request) (*http.Response, error) { @@ -99,8 +103,6 @@ } func (cs *clientSuite) TestClientDoReportsErrors(c *C) { - restore := client.MockDoRetry(10*time.Millisecond, 100*time.Millisecond) - defer restore() cs.err = errors.New("ouchie") err := cs.cli.Do("GET", "/", nil, nil, nil) c.Check(err, ErrorMatches, "cannot communicate with server: ouchie") @@ -216,7 +218,9 @@ "version": "2", "os-release": {"id": "ubuntu", "version-id": "16.04"}, "on-classic": true, - "confinement": "strict"}}` + "build-id": "1234", + "confinement": "strict", + "sandbox-features": {"backend": ["feature-1", "feature-2"]}}}` sysInfo, err := cs.cli.SysInfo() c.Check(err, IsNil) c.Check(sysInfo, DeepEquals, &client.SysInfo{ @@ -228,6 +232,10 @@ }, OnClassic: true, Confinement: "strict", + SandboxFeatures: map[string][]string{ + "backend": {"feature-1", "feature-2"}, + }, + BuildID: "1234", }) } @@ -343,6 +351,36 @@ c.Check(err, ErrorMatches, `.*cannot unmarshal.*`) } +func (cs *clientSuite) TestClientMaintenance(c *C) { + cs.rsp = `{"type":"sync", "result":{"series":"42"}, "maintenance": {"kind": "system-restart", "message": "system is restarting"}}` + _, err := cs.cli.SysInfo() + c.Assert(err, IsNil) + c.Check(cs.cli.Maintenance().(*client.Error), DeepEquals, &client.Error{ + Kind: client.ErrorKindSystemRestart, + Message: "system is restarting", + }) + + cs.rsp = `{"type":"sync", "result":{"series":"42"}}` + _, err = cs.cli.SysInfo() + c.Assert(err, IsNil) + c.Check(cs.cli.Maintenance(), Equals, error(nil)) +} + +func (cs *clientSuite) TestClientAsyncOpMaintenance(c *C) { + cs.rsp = `{"type":"async", "status-code": 202, "change": "42", "maintenance": {"kind": "system-restart", "message": "system is restarting"}}` + _, err := cs.cli.Install("foo", nil) + c.Assert(err, IsNil) + c.Check(cs.cli.Maintenance().(*client.Error), DeepEquals, &client.Error{ + Kind: client.ErrorKindSystemRestart, + Message: "system is restarting", + }) + + cs.rsp = `{"type":"async", "status-code": 202, "change": "42"}` + _, err = cs.cli.Install("foo", nil) + c.Assert(err, IsNil) + c.Check(cs.cli.Maintenance(), Equals, error(nil)) +} + func (cs *clientSuite) TestParseError(c *C) { resp := &http.Response{ Status: "404 Not Found", @@ -384,6 +422,15 @@ c.Check(client.IsTwoFactorError((*client.Error)(nil)), Equals, false) } +func (cs *clientSuite) TestIsRetryable(c *C) { + // unhappy + c.Check(client.IsRetryable(nil), Equals, false) + c.Check(client.IsRetryable(errors.New("some-error")), Equals, false) + c.Check(client.IsRetryable(&client.Error{Kind: "something-else"}), Equals, false) + // happy + c.Check(client.IsRetryable(&client.Error{Kind: client.ErrorKindChangeConflict}), Equals, true) +} + func (cs *clientSuite) TestClientCreateUser(c *C) { _, err := cs.cli.CreateUser(&client.CreateUserOptions{}) c.Assert(err, ErrorMatches, "cannot create a user without providing an email") diff -Nru snapd-2.32.3.2~14.04/client/conf.go snapd-2.37~rc1~14.04/client/conf.go --- snapd-2.32.3.2~14.04/client/conf.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/client/conf.go 2019-01-16 08:36:51.000000000 +0000 @@ -36,6 +36,8 @@ } // Conf asks for a snap's current configuration. +// +// Note that the configuration may include json.Numbers. func (client *Client) Conf(snapName string, keys []string) (configuration map[string]interface{}, err error) { // Prepare query query := url.Values{} diff -Nru snapd-2.32.3.2~14.04/client/export_test.go snapd-2.37~rc1~14.04/client/export_test.go --- snapd-2.32.3.2~14.04/client/export_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/client/export_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -20,6 +20,7 @@ package client import ( + "encoding/json" "io" "net/url" ) @@ -43,3 +44,8 @@ var TestStoreAuthFilename = storeAuthDataFilename var TestAuthFileEnvKey = authFileEnvKey + +func UnmarshalSnapshotAction(body io.Reader) (act snapshotAction, err error) { + err = json.NewDecoder(body).Decode(&act) + return +} diff -Nru snapd-2.32.3.2~14.04/client/icons.go snapd-2.37~rc1~14.04/client/icons.go --- snapd-2.32.3.2~14.04/client/icons.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/client/icons.go 2019-01-16 08:36:51.000000000 +0000 @@ -31,6 +31,8 @@ Content []byte } +var contentDispositionMatcher = regexp.MustCompile(`attachment; filename=(.+)`).FindStringSubmatch + // Icon returns the Icon belonging to an installed snap func (c *Client) Icon(pkgID string) (*Icon, error) { const errPrefix = "cannot retrieve icon" @@ -45,8 +47,7 @@ return nil, fmt.Errorf("%s: Not Found", errPrefix) } - re := regexp.MustCompile(`attachment; filename=(.+)`) - matches := re.FindStringSubmatch(response.Header.Get("Content-Disposition")) + matches := contentDispositionMatcher(response.Header.Get("Content-Disposition")) if matches == nil || matches[1] == "" { return nil, fmt.Errorf("%s: cannot determine filename", errPrefix) diff -Nru snapd-2.32.3.2~14.04/client/packages.go snapd-2.37~rc1~14.04/client/packages.go --- snapd-2.32.3.2~14.04/client/packages.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/client/packages.go 2019-01-16 08:36:51.000000000 +0000 @@ -32,18 +32,21 @@ // Snap holds the data for a snap as obtained from snapd. type Snap struct { - ID string `json:"id"` - Title string `json:"title,omitempty"` - Summary string `json:"summary"` - Description string `json:"description"` - DownloadSize int64 `json:"download-size,omitempty"` - Icon string `json:"icon,omitempty"` - InstalledSize int64 `json:"installed-size,omitempty"` - InstallDate time.Time `json:"install-date,omitempty"` - Name string `json:"name"` + ID string `json:"id"` + Title string `json:"title,omitempty"` + Summary string `json:"summary"` + Description string `json:"description"` + DownloadSize int64 `json:"download-size,omitempty"` + Icon string `json:"icon,omitempty"` + InstalledSize int64 `json:"installed-size,omitempty"` + InstallDate time.Time `json:"install-date,omitempty"` + Name string `json:"name"` + Publisher *snap.StoreAccount `json:"publisher,omitempty"` + // Developer is also the publisher's username for historic reasons. Developer string `json:"developer"` Status string `json:"status"` Type string `json:"type"` + Base string `json:"base,omitempty"` Version string `json:"version"` Channel string `json:"channel"` TrackingChannel string `json:"tracking-channel,omitempty"` @@ -58,9 +61,12 @@ Broken string `json:"broken,omitempty"` Contact string `json:"contact"` License string `json:"license,omitempty"` + CommonIDs []string `json:"common-ids,omitempty"` + MountedFrom string `json:"mounted-from,omitempty"` - Prices map[string]float64 `json:"prices,omitempty"` - Screenshots []Screenshot `json:"screenshots,omitempty"` + Prices map[string]float64 `json:"prices,omitempty"` + Screenshots []snap.ScreenshotInfo `json:"screenshots,omitempty"` + Media snap.MediaInfos `json:"media,omitempty"` // The flattended channel map with $track/$risk Channels map[string]*snap.ChannelSnapInfo `json:"channels,omitempty"` @@ -84,12 +90,6 @@ return json.Marshal(&m) } -type Screenshot struct { - URL string `json:"url"` - Width int64 `json:"width,omitempty"` - Height int64 `json:"height,omitempty"` -} - // Statuses and types a snap may have. const ( StatusAvailable = "available" @@ -122,6 +122,7 @@ Prefix bool Query string Section string + Scope string } var ErrNoSnapsInstalled = errors.New("no snaps installed") @@ -191,6 +192,9 @@ if opts.Section != "" { q.Set("section", opts.Section) } + if opts.Scope != "" { + q.Set("scope", opts.Scope) + } return client.snapsFromPath("/v2/find", q) } diff -Nru snapd-2.32.3.2~14.04/client/packages_test.go snapd-2.37~rc1~14.04/client/packages_test.go --- snapd-2.32.3.2~14.04/client/packages_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/client/packages_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -28,6 +28,7 @@ "gopkg.in/check.v1" "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/snap" ) func (cs *clientSuite) TestClientSnapsCallsEndpoint(c *check.C) { @@ -81,6 +82,17 @@ c.Check(cs.req.URL.Query().Get("select"), check.Equals, "private") } +func (cs *clientSuite) TestClientFindWithScopeSetsQuery(c *check.C) { + _, _, _ = cs.cli.Find(&client.FindOptions{ + Scope: "mouthwash", + }) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/find") + c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{ + "q": []string{""}, "scope": []string{"mouthwash"}, + }) +} + func (cs *clientSuite) TestClientSnapsInvalidSnapsJSON(c *check.C) { cs.rsp = `{ "type": "sync", @@ -116,12 +128,19 @@ "license": "GPL-3.0", "name": "hello-world", "developer": "canonical", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, "resource": "/v2/snaps/hello-world.canonical", "status": "available", "type": "app", "version": "1.0.18", "confinement": "strict", - "private": true + "private": true, + "common-ids": ["org.funky.snap"] }], "suggested-currency": "GBP" }` @@ -138,12 +157,19 @@ License: "GPL-3.0", Name: "hello-world", Developer: "canonical", - Status: client.StatusAvailable, - Type: client.TypeApp, - Version: "1.0.18", - Confinement: client.StrictConfinement, - Private: true, - DevMode: false, + Publisher: &snap.StoreAccount{ + ID: "canonical", + Username: "canonical", + DisplayName: "Canonical", + Validation: "verified", + }, + Status: client.StatusAvailable, + Type: client.TypeApp, + Version: "1.0.18", + Confinement: client.StrictConfinement, + Private: true, + DevMode: false, + CommonIDs: []string{"org.funky.snap"}, }}) } @@ -186,10 +212,17 @@ "license": "GPL-3.0", "name": "chatroom", "developer": "ogra", + "publisher": { + "id": "ogra-id", + "username": "ogra", + "display-name": "Ogra", + "validation": "unproven" + }, "resource": "/v2/snaps/chatroom.ogra", "status": "active", "type": "app", "version": "0.1-8", + "revision": 42, "confinement": "strict", "private": true, "devmode": true, @@ -197,7 +230,13 @@ "screenshots": [ {"url":"http://example.com/shot1.png", "width":640, "height":480}, {"url":"http://example.com/shot2.png"} - ] + ], + "media": [ + {"type": "icon", "url":"http://example.com/icon.png"}, + {"type": "screenshot", "url":"http://example.com/shot1.png", "width":640, "height":480}, + {"type": "screenshot", "url":"http://example.com/shot2.png"} + ], + "common-ids": ["org.funky.snap"] } }` pkg, _, err := cs.cli.Snap(pkgName) @@ -216,17 +255,30 @@ License: "GPL-3.0", Name: "chatroom", Developer: "ogra", - Status: client.StatusActive, - Type: client.TypeApp, - Version: "0.1-8", - Confinement: client.StrictConfinement, - Private: true, - DevMode: true, - TryMode: true, - Screenshots: []client.Screenshot{ + Publisher: &snap.StoreAccount{ + ID: "ogra-id", + Username: "ogra", + DisplayName: "Ogra", + Validation: "unproven", + }, + Status: client.StatusActive, + Type: client.TypeApp, + Version: "0.1-8", + Revision: snap.R(42), + Confinement: client.StrictConfinement, + Private: true, + DevMode: true, + TryMode: true, + Screenshots: []snap.ScreenshotInfo{ {URL: "http://example.com/shot1.png", Width: 640, Height: 480}, {URL: "http://example.com/shot2.png"}, }, + Media: []snap.MediaInfo{ + {Type: "icon", URL: "http://example.com/icon.png"}, + {Type: "screenshot", URL: "http://example.com/shot1.png", Width: 640, Height: 480}, + {Type: "screenshot", URL: "http://example.com/shot2.png"}, + }, + CommonIDs: []string{"org.funky.snap"}, }) } diff -Nru snapd-2.32.3.2~14.04/client/snap_op.go snapd-2.37~rc1~14.04/client/snap_op.go --- snapd-2.32.3.2~14.04/client/snap_op.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/client/snap_op.go 2019-01-16 08:36:51.000000000 +0000 @@ -39,6 +39,15 @@ Dangerous bool `json:"dangerous,omitempty"` IgnoreValidation bool `json:"ignore-validation,omitempty"` Unaliased bool `json:"unaliased,omitempty"` + + Users []string `json:"users,omitempty"` +} + +func writeFieldBool(mw *multipart.Writer, key string, val bool) error { + if !val { + return nil + } + return mw.WriteField(key, "true") } func (opts *SnapOptions) writeModeFields(mw *multipart.Writer) error { @@ -52,11 +61,7 @@ {"dangerous", opts.Dangerous}, } for _, o := range fields { - if !o.b { - continue - } - err := mw.WriteField(o.f, "true") - if err != nil { + if err := writeFieldBool(mw, o.f, o.b); err != nil { return err } } @@ -64,6 +69,10 @@ return nil } +func (opts *SnapOptions) writeOptionFields(mw *multipart.Writer) error { + return writeFieldBool(mw, "unaliased", opts.Unaliased) +} + type actionData struct { Action string `json:"action"` Name string `json:"name,omitempty"` @@ -74,6 +83,7 @@ type multiActionData struct { Action string `json:"action"` Snaps []string `json:"snaps,omitempty"` + Users []string `json:"users,omitempty"` } // Install adds the snap with the given name from the given channel (or @@ -123,6 +133,24 @@ return client.doSnapAction("switch", name, options) } +// SnapshotMany snapshots many snaps (all, if names empty) for many users (all, if users is empty). +func (client *Client) SnapshotMany(names []string, users []string) (setID uint64, changeID string, err error) { + result, changeID, err := client.doMultiSnapActionFull("snapshot", names, &SnapOptions{Users: users}) + if err != nil { + return 0, "", err + } + if len(result) == 0 { + return 0, "", fmt.Errorf("server result does not contain snapshot set identifier") + } + var x struct { + SetID uint64 `json:"set-id"` + } + if err := json.Unmarshal(result, &x); err != nil { + return 0, "", err + } + return x.SetID, changeID, nil +} + var ErrDangerousNotApplicable = fmt.Errorf("dangerous option only meaningful when installing from a local file") func (client *Client) doSnapAction(actionName string, snapName string, options *SnapOptions) (changeID string, err error) { @@ -150,25 +178,34 @@ if options != nil { return "", fmt.Errorf("cannot use options for multi-action") // (yet) } + _, changeID, err = client.doMultiSnapActionFull(actionName, snaps, options) + + return changeID, err +} + +func (client *Client) doMultiSnapActionFull(actionName string, snaps []string, options *SnapOptions) (result json.RawMessage, changeID string, err error) { action := multiActionData{ Action: actionName, Snaps: snaps, } + if options != nil { + action.Users = options.Users + } data, err := json.Marshal(&action) if err != nil { - return "", fmt.Errorf("cannot marshal multi-snap action: %s", err) + return nil, "", fmt.Errorf("cannot marshal multi-snap action: %s", err) } headers := map[string]string{ "Content-Type": "application/json", } - return client.doAsync("POST", "/v2/snaps", nil, headers, bytes.NewBuffer(data)) + return client.doAsyncFull("POST", "/v2/snaps", nil, headers, bytes.NewBuffer(data)) } -// InstallPath sideloads the snap with the given path, returning the UUID -// of the background operation upon success. -func (client *Client) InstallPath(path string, options *SnapOptions) (changeID string, err error) { +// InstallPath sideloads the snap with the given path under optional provided name, +// returning the UUID of the background operation upon success. +func (client *Client) InstallPath(path, name string, options *SnapOptions) (changeID string, err error) { f, err := os.Open(path) if err != nil { return "", fmt.Errorf("cannot open: %q", path) @@ -176,6 +213,7 @@ action := actionData{ Action: "install", + Name: name, SnapPath: path, SnapOptions: options, } @@ -243,6 +281,11 @@ pw.CloseWithError(err) return } + + if err := action.writeOptionFields(mw); err != nil { + pw.CloseWithError(err) + return + } fw, err := mw.CreateFormFile("snap", filepath.Base(snapPath)) if err != nil { diff -Nru snapd-2.32.3.2~14.04/client/snap_op_test.go snapd-2.37~rc1~14.04/client/snap_op_test.go --- snapd-2.32.3.2~14.04/client/snap_op_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/client/snap_op_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -72,6 +72,8 @@ _, err := s.op(cs.cli, nil, nil) c.Check(err, check.ErrorMatches, `.*fail`, check.Commentf(s.action)) } + _, _, err := cs.cli.SnapshotMany(nil, nil) + c.Check(err, check.ErrorMatches, `.*fail`) } func (cs *clientSuite) TestClientOpSnapResponseError(c *check.C) { @@ -88,6 +90,8 @@ _, err := s.op(cs.cli, nil, nil) c.Check(err, check.ErrorMatches, `.*server error: "potatoes"`, check.Commentf(s.action)) } + _, _, err := cs.cli.SnapshotMany(nil, nil) + c.Check(err, check.ErrorMatches, `.*server error: "potatoes"`) } func (cs *clientSuite) TestClientOpSnapBadType(c *check.C) { @@ -153,6 +157,7 @@ "type": "async" }` for _, s := range multiOps { + // Note body is essentially the same as TestClientMultiSnapshot; keep in sync id, err := s.op(cs.cli, []string{pkgName}, nil) c.Assert(err, check.IsNil) @@ -172,6 +177,31 @@ } } +func (cs *clientSuite) TestClientMultiSnapshot(c *check.C) { + // Note body is essentially the same as TestClientMultiOpSnap; keep in sync + cs.rsp = `{ + "result": {"set-id": 42}, + "change": "d728", + "status-code": 202, + "type": "async" + }` + setID, changeID, err := cs.cli.SnapshotMany([]string{pkgName}, nil) + c.Assert(err, check.IsNil) + c.Check(cs.req.Header.Get("Content-Type"), check.Equals, "application/json") + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + jsonBody := make(map[string]interface{}) + err = json.Unmarshal(body, &jsonBody) + c.Assert(err, check.IsNil) + c.Check(jsonBody["action"], check.Equals, "snapshot") + c.Check(jsonBody["snaps"], check.DeepEquals, []interface{}{pkgName}) + c.Check(jsonBody, check.HasLen, 2) + c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps") + c.Check(setID, check.Equals, uint64(42)) + c.Check(changeID, check.Equals, "d728") +} + func (cs *clientSuite) TestClientOpInstallPath(c *check.C) { cs.rsp = `{ "change": "66b3", @@ -184,7 +214,7 @@ err := ioutil.WriteFile(snap, bodyData, 0644) c.Assert(err, check.IsNil) - id, err := cs.cli.InstallPath(snap, nil) + id, err := cs.cli.InstallPath(snap, "", nil) c.Assert(err, check.IsNil) body, err := ioutil.ReadAll(cs.req.Body) @@ -199,6 +229,34 @@ c.Check(id, check.Equals, "66b3") } +func (cs *clientSuite) TestClientOpInstallPathInstance(c *check.C) { + cs.rsp = `{ + "change": "66b3", + "status-code": 202, + "type": "async" + }` + bodyData := []byte("snap-data") + + snap := filepath.Join(c.MkDir(), "foo.snap") + err := ioutil.WriteFile(snap, bodyData, 0644) + c.Assert(err, check.IsNil) + + id, err := cs.cli.InstallPath(snap, "foo_bar", nil) + c.Assert(err, check.IsNil) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + + c.Assert(string(body), check.Matches, "(?s).*\r\nsnap-data\r\n.*") + c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*") + c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"name\"\r\n\r\nfoo_bar\r\n.*") + + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps")) + c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*") + c.Check(id, check.Equals, "66b3") +} + func (cs *clientSuite) TestClientOpInstallDangerous(c *check.C) { cs.rsp = `{ "change": "66b3", @@ -216,7 +274,7 @@ } // InstallPath takes Dangerous - _, err = cs.cli.InstallPath(snap, &opts) + _, err = cs.cli.InstallPath(snap, "", &opts) c.Assert(err, check.IsNil) body, err := ioutil.ReadAll(cs.req.Body) @@ -235,6 +293,41 @@ c.Assert(err, check.NotNil) } +func (cs *clientSuite) TestClientOpInstallUnaliased(c *check.C) { + cs.rsp = `{ + "change": "66b3", + "status-code": 202, + "type": "async" + }` + bodyData := []byte("snap-data") + + snap := filepath.Join(c.MkDir(), "foo.snap") + err := ioutil.WriteFile(snap, bodyData, 0644) + c.Assert(err, check.IsNil) + + opts := client.SnapOptions{ + Unaliased: true, + } + + _, err = cs.cli.Install("foo", &opts) + c.Assert(err, check.IsNil) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + jsonBody := make(map[string]interface{}) + err = json.Unmarshal(body, &jsonBody) + c.Assert(err, check.IsNil, check.Commentf("body: %v", string(body))) + c.Check(jsonBody["unaliased"], check.Equals, true, check.Commentf("body: %v", string(body))) + + _, err = cs.cli.InstallPath(snap, "", &opts) + c.Assert(err, check.IsNil) + + body, err = ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + + c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"unaliased\"\r\n\r\ntrue\r\n.*") +} + func formToMap(c *check.C, mr *multipart.Reader) map[string]string { formData := map[string]string{} for { diff -Nru snapd-2.32.3.2~14.04/client/snapshot.go snapd-2.37~rc1~14.04/client/snapshot.go --- snapd-2.32.3.2~14.04/client/snapshot.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/client/snapshot.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,175 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package client + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/url" + "strconv" + "strings" + "time" + + "github.com/snapcore/snapd/snap" +) + +var ( + ErrSnapshotSetNotFound = errors.New("no snapshot set with the given ID") + ErrSnapshotSnapsNotFound = errors.New("no snapshot for the requested snaps found in the set with the given ID") +) + +// A snapshotAction is used to request an operation on a snapshot. +type snapshotAction struct { + SetID uint64 `json:"set"` + Action string `json:"action"` + Snaps []string `json:"snaps,omitempty"` + Users []string `json:"users,omitempty"` +} + +// A Snapshot is a collection of archives with a simple metadata json file +// (and hashsums of everything). +type Snapshot struct { + // SetID is the ID of the snapshot set (a snapshot set is the result of a "snap save" invocation) + SetID uint64 `json:"set"` + // the time this snapshot's data collection was started + Time time.Time `json:"time"` + + // information about the snap this data is for + Snap string `json:"snap"` + Revision snap.Revision `json:"revision"` + SnapID string `json:"snap-id,omitempty"` + Epoch snap.Epoch `json:"epoch,omitempty"` + Summary string `json:"summary"` + Version string `json:"version"` + + // the snap's configuration at snapshot time + Conf map[string]interface{} `json:"conf,omitempty"` + + // the hash of the archives' data, keyed by archive path + // (either 'archive.tgz' for the system archive, or + // user/.tgz for each user) + SHA3_384 map[string]string `json:"sha3-384"` + // the sum of the archive sizes + Size int64 `json:"size,omitempty"` + // if the snapshot failed to open this will be the reason why + Broken string `json:"broken,omitempty"` +} + +// IsValid checks whether the snapshot is missing information that +// should be there for a snapshot that's just been opened. +func (sh *Snapshot) IsValid() bool { + return !(sh == nil || sh.SetID == 0 || sh.Snap == "" || sh.Revision.Unset() || len(sh.SHA3_384) == 0 || sh.Time.IsZero()) +} + +// A SnapshotSet is a set of snapshots created by a single "snap save". +type SnapshotSet struct { + ID uint64 `json:"id"` + Snapshots []*Snapshot `json:"snapshots"` +} + +// Time returns the earliest time in the set. +func (ss SnapshotSet) Time() time.Time { + if len(ss.Snapshots) == 0 { + return time.Time{} + } + mint := ss.Snapshots[0].Time + for _, sh := range ss.Snapshots { + if sh.Time.Before(mint) { + mint = sh.Time + } + } + return mint +} + +// Size returns the sum of the set's sizes. +func (ss SnapshotSet) Size() int64 { + var sum int64 + for _, sh := range ss.Snapshots { + sum += sh.Size + } + return sum +} + +// SnapshotSets lists the snapshot sets in the system that belong to the +// given set (if non-zero) and are for the given snaps (if non-empty). +func (client *Client) SnapshotSets(setID uint64, snapNames []string) ([]SnapshotSet, error) { + q := make(url.Values) + if setID > 0 { + q.Add("set", strconv.FormatUint(setID, 10)) + } + if len(snapNames) > 0 { + q.Add("snaps", strings.Join(snapNames, ",")) + } + + var snapshotSets []SnapshotSet + _, err := client.doSync("GET", "/v2/snapshots", q, nil, nil, &snapshotSets) + return snapshotSets, err +} + +// ForgetSnapshots permanently removes the snapshot set, limited to the +// given snaps (if non-empty). +func (client *Client) ForgetSnapshots(setID uint64, snaps []string) (changeID string, err error) { + return client.snapshotAction(&snapshotAction{ + SetID: setID, + Action: "forget", + Snaps: snaps, + }) +} + +// CheckSnapshots verifies the archive checksums in the given snapshot set. +// +// If snaps or users are non-empty, limit to checking only those +// archives of the snapshot. +func (client *Client) CheckSnapshots(setID uint64, snaps []string, users []string) (changeID string, err error) { + return client.snapshotAction(&snapshotAction{ + SetID: setID, + Action: "check", + Snaps: snaps, + Users: users, + }) +} + +// RestoreSnapshots extracts the given snapshot set. +// +// If snaps or users are non-empty, limit to checking only those +// archives of the snapshot. +func (client *Client) RestoreSnapshots(setID uint64, snaps []string, users []string) (changeID string, err error) { + return client.snapshotAction(&snapshotAction{ + SetID: setID, + Action: "restore", + Snaps: snaps, + Users: users, + }) +} + +func (client *Client) snapshotAction(action *snapshotAction) (changeID string, err error) { + data, err := json.Marshal(action) + if err != nil { + return "", fmt.Errorf("cannot marshal snapshot action: %v", err) + } + + headers := map[string]string{ + "Content-Type": "application/json", + } + + return client.doAsync("POST", "/v2/snapshots", nil, headers, bytes.NewBuffer(data)) +} diff -Nru snapd-2.32.3.2~14.04/client/snapshot_test.go snapd-2.37~rc1~14.04/client/snapshot_test.go --- snapd-2.32.3.2~14.04/client/snapshot_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/client/snapshot_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,138 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package client_test + +import ( + "net/url" + "time" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/snap" +) + +func (cs *clientSuite) TestClientSnapshotIsValid(c *check.C) { + now := time.Now() + revno := snap.R(1) + sums := map[string]string{"user/foo.tgz": "some long hash"} + c.Check((&client.Snapshot{ + SetID: 42, + Time: now, + Snap: "asnap", + Revision: revno, + SHA3_384: sums, + }).IsValid(), check.Equals, true) + + for desc, snapshot := range map[string]*client.Snapshot{ + "nil": nil, + "empty": {}, + "no id": { /*SetID: 42,*/ Time: now, Snap: "asnap", Revision: revno, SHA3_384: sums}, + "no time": {SetID: 42 /*Time: now,*/, Snap: "asnap", Revision: revno, SHA3_384: sums}, + "no snap": {SetID: 42, Time: now /*Snap: "asnap",*/, Revision: revno, SHA3_384: sums}, + "no rev": {SetID: 42, Time: now, Snap: "asnap" /*Revision: revno,*/, SHA3_384: sums}, + "no sums": {SetID: 42, Time: now, Snap: "asnap", Revision: revno /*SHA3_384: sums*/}, + } { + c.Check(snapshot.IsValid(), check.Equals, false, check.Commentf("%s", desc)) + } + +} + +func (cs *clientSuite) TestClientSnapshotSetTime(c *check.C) { + // if set is empty, it doesn't explode (and returns the zero time) + c.Check(client.SnapshotSet{}.Time().IsZero(), check.Equals, true) + // if not empty, returns the earliest one + c.Check(client.SnapshotSet{Snapshots: []*client.Snapshot{ + {Time: time.Unix(3, 0)}, + {Time: time.Unix(1, 0)}, + {Time: time.Unix(2, 0)}, + }}.Time(), check.DeepEquals, time.Unix(1, 0)) +} + +func (cs *clientSuite) TestClientSnapshotSetSize(c *check.C) { + // if set is empty, doesn't explode (and returns 0) + c.Check(client.SnapshotSet{}.Size(), check.Equals, int64(0)) + // if not empty, returns the sum + c.Check(client.SnapshotSet{Snapshots: []*client.Snapshot{ + {Size: 1}, + {Size: 2}, + {Size: 3}, + }}.Size(), check.DeepEquals, int64(6)) +} + +func (cs *clientSuite) TestClientSnapshotSets(c *check.C) { + cs.rsp = `{ + "type": "sync", + "result": [{"id": 1}, {"id":2}] +}` + sets, err := cs.cli.SnapshotSets(42, []string{"foo", "bar"}) + c.Assert(err, check.IsNil) + c.Check(sets, check.DeepEquals, []client.SnapshotSet{{ID: 1}, {ID: 2}}) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/snapshots") + c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{ + "set": []string{"42"}, + "snaps": []string{"foo,bar"}, + }) +} + +func (cs *clientSuite) testClientSnapshotActionFull(c *check.C, action string, users []string, f func() (string, error)) { + cs.rsp = `{ + "status-code": 202, + "type": "async", + "change": "1too3" + }` + id, err := f() + c.Assert(err, check.IsNil) + c.Check(id, check.Equals, "1too3") + + c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json") + + act, err := client.UnmarshalSnapshotAction(cs.req.Body) + c.Assert(err, check.IsNil) + c.Check(act.SetID, check.Equals, uint64(42)) + c.Check(act.Action, check.Equals, action) + c.Check(act.Snaps, check.DeepEquals, []string{"asnap", "bsnap"}) + c.Check(act.Users, check.DeepEquals, users) + + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/snapshots") + c.Check(cs.req.URL.Query(), check.HasLen, 0) +} + +func (cs *clientSuite) TestClientForgetSnapshot(c *check.C) { + cs.testClientSnapshotActionFull(c, "forget", nil, func() (string, error) { + return cs.cli.ForgetSnapshots(42, []string{"asnap", "bsnap"}) + }) +} + +func (cs *clientSuite) testClientSnapshotAction(c *check.C, action string, f func(uint64, []string, []string) (string, error)) { + cs.testClientSnapshotActionFull(c, action, []string{"auser", "buser"}, func() (string, error) { + return f(42, []string{"asnap", "bsnap"}, []string{"auser", "buser"}) + }) +} + +func (cs *clientSuite) TestClientCheckSnapshots(c *check.C) { + cs.testClientSnapshotAction(c, "check", cs.cli.CheckSnapshots) +} + +func (cs *clientSuite) TestClientRestoreSnapshots(c *check.C) { + cs.testClientSnapshotAction(c, "restore", cs.cli.RestoreSnapshots) +} diff -Nru snapd-2.32.3.2~14.04/client/warnings.go snapd-2.37~rc1~14.04/client/warnings.go --- snapd-2.32.3.2~14.04/client/warnings.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/client/warnings.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,89 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package client + +import ( + "bytes" + "encoding/json" + "net/url" + "time" +) + +// A Warning is a short messages that's meant to alert about system events. +// There'll only ever be one Warning with the same message, and it can be +// silenced for a while before repeating. After a (supposedly longer) while +// it'll go away on its own (unless it recurrs). +type Warning struct { + Message string `json:"message"` + FirstAdded time.Time `json:"first-added"` + LastAdded time.Time `json:"last-added"` + LastShown time.Time `json:"last-shown,omitempty"` + ExpireAfter time.Duration `json:"expire-after,omitempty"` + RepeatAfter time.Duration `json:"repeat-after,omitempty"` +} + +type jsonWarning struct { + Warning + ExpireAfter string `json:"expire-after,omitempty"` + RepeatAfter string `json:"repeat-after,omitempty"` +} + +// WarningsOptions contains options for querying snapd for warnings +// supported options: +// - All: return all warnings, instead of only the un-okayed ones. +type WarningsOptions struct { + All bool +} + +// Warnings returns the list of un-okayed warnings. +func (client *Client) Warnings(opts WarningsOptions) ([]*Warning, error) { + var jws []*jsonWarning + q := make(url.Values) + if opts.All { + q.Add("select", "all") + } + _, err := client.doSync("GET", "/v2/warnings", q, nil, nil, &jws) + + ws := make([]*Warning, len(jws)) + for i, jw := range jws { + ws[i] = &jw.Warning + ws[i].ExpireAfter, _ = time.ParseDuration(jw.ExpireAfter) + ws[i].RepeatAfter, _ = time.ParseDuration(jw.RepeatAfter) + } + + return ws, err +} + +type warningsAction struct { + Action string `json:"action"` + Timestamp time.Time `json:"timestamp"` +} + +// Okay asks snapd to chill about the warnings that would have been returned by +// Warnings at the given time. +func (client *Client) Okay(t time.Time) error { + var body bytes.Buffer + var op = warningsAction{Action: "okay", Timestamp: t} + if err := json.NewEncoder(&body).Encode(op); err != nil { + return err + } + _, err := client.doSync("POST", "/v2/warnings", nil, nil, &body, nil) + return err +} diff -Nru snapd-2.32.3.2~14.04/client/warnings_test.go snapd-2.37~rc1~14.04/client/warnings_test.go --- snapd-2.32.3.2~14.04/client/warnings_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/client/warnings_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,121 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package client_test + +import ( + "encoding/json" + "time" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" +) + +func (cs *clientSuite) testWarnings(c *check.C, all bool) { + t1 := time.Date(2018, 9, 19, 12, 41, 18, 505007495, time.UTC) + t2 := time.Date(2018, 9, 19, 12, 44, 19, 680362867, time.UTC) + cs.rsp = `{ + "result": [ + { + "expire-after": "672h0m0s", + "first-added": "2018-09-19T12:41:18.505007495Z", + "last-added": "2018-09-19T12:41:18.505007495Z", + "message": "hello world number one", + "repeat-after": "24h0m0s" + }, + { + "expire-after": "672h0m0s", + "first-added": "2018-09-19T12:44:19.680362867Z", + "last-added": "2018-09-19T12:44:19.680362867Z", + "message": "hello world number two", + "repeat-after": "24h0m0s" + } + ], + "status": "OK", + "status-code": 200, + "type": "sync", + "warning-count": 2, + "warning-timestamp": "2018-09-19T12:44:19.680362867Z" + }` + + ws, err := cs.cli.Warnings(client.WarningsOptions{All: all}) + c.Assert(err, check.IsNil) + c.Check(ws, check.DeepEquals, []*client.Warning{ + { + Message: "hello world number one", + FirstAdded: t1, + LastAdded: t1, + ExpireAfter: time.Hour * 24 * 28, + RepeatAfter: time.Hour * 24, + }, + { + Message: "hello world number two", + FirstAdded: t2, + LastAdded: t2, + ExpireAfter: time.Hour * 24 * 28, + RepeatAfter: time.Hour * 24, + }, + }) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/warnings") + query := cs.req.URL.Query() + if all { + c.Check(query, check.HasLen, 1) + c.Check(query.Get("select"), check.Equals, "all") + } else { + c.Check(query, check.HasLen, 0) + } + + // this could be done at the end of any sync method + count, stamp := cs.cli.WarningsSummary() + c.Check(count, check.Equals, 2) + c.Check(stamp, check.Equals, t2) +} + +func (cs *clientSuite) TestWarningsAll(c *check.C) { + cs.testWarnings(c, true) +} + +func (cs *clientSuite) TestWarnings(c *check.C) { + cs.testWarnings(c, false) +} + +func (cs *clientSuite) TestOkay(c *check.C) { + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": { } + }` + t0 := time.Now() + err := cs.cli.Okay(t0) + c.Assert(err, check.IsNil) + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Query(), check.HasLen, 0) + var body map[string]interface{} + c.Assert(json.NewDecoder(cs.req.Body).Decode(&body), check.IsNil) + c.Check(body, check.HasLen, 2) + c.Check(body["action"], check.Equals, "okay") + c.Check(body["timestamp"], check.Equals, t0.Format(time.RFC3339Nano)) + + // note there's no warnings summary in the response + count, stamp := cs.cli.WarningsSummary() + c.Check(count, check.Equals, 0) + c.Check(stamp, check.Equals, time.Time{}) +} diff -Nru snapd-2.32.3.2~14.04/cmd/appinfo.go snapd-2.37~rc1~14.04/cmd/appinfo.go --- snapd-2.32.3.2~14.04/cmd/appinfo.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/appinfo.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,133 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package cmd + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/progress" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/systemd" +) + +func ClientAppInfoNotes(app *client.AppInfo) string { + if !app.IsService() { + return "-" + } + + var notes = make([]string, 0, 2) + var seenTimer, seenSocket bool + for _, act := range app.Activators { + switch act.Type { + case "timer": + seenTimer = true + case "socket": + seenSocket = true + } + } + if seenTimer { + notes = append(notes, "timer-activated") + } + if seenSocket { + notes = append(notes, "socket-activated") + } + if len(notes) == 0 { + return "-" + } + return strings.Join(notes, ",") +} + +func ClientAppInfosFromSnapAppInfos(apps []*snap.AppInfo) ([]client.AppInfo, error) { + // TODO: pass in an actual notifier here instead of null + // (Status doesn't _need_ it, but benefits from it) + sysd := systemd.New(dirs.GlobalRootDir, progress.Null) + + out := make([]client.AppInfo, 0, len(apps)) + for _, app := range apps { + appInfo := client.AppInfo{ + Snap: app.Snap.InstanceName(), + Name: app.Name, + CommonID: app.CommonID, + } + if fn := app.DesktopFile(); osutil.FileExists(fn) { + appInfo.DesktopFile = fn + } + + appInfo.Daemon = app.Daemon + if !app.IsService() || !app.Snap.IsActive() { + out = append(out, appInfo) + continue + } + + // collect all services for a single call to systemctl + serviceNames := make([]string, 0, 1+len(app.Sockets)+1) + serviceNames = append(serviceNames, app.ServiceName()) + + sockSvcFileToName := make(map[string]string, len(app.Sockets)) + for _, sock := range app.Sockets { + sockUnit := filepath.Base(sock.File()) + sockSvcFileToName[sockUnit] = sock.Name + serviceNames = append(serviceNames, sockUnit) + } + if app.Timer != nil { + timerUnit := filepath.Base(app.Timer.File()) + serviceNames = append(serviceNames, timerUnit) + } + + // sysd.Status() makes sure that we get only the units we asked + // for and raises an error otherwise + sts, err := sysd.Status(serviceNames...) + if err != nil { + return nil, fmt.Errorf("cannot get status of services of app %q: %v", app.Name, err) + } + if len(sts) != len(serviceNames) { + return nil, fmt.Errorf("cannot get status of services of app %q: expected %v results, got %v", app.Name, len(serviceNames), len(sts)) + } + for _, st := range sts { + switch filepath.Ext(st.UnitName) { + case ".service": + appInfo.Enabled = st.Enabled + appInfo.Active = st.Active + case ".timer": + appInfo.Activators = append(appInfo.Activators, client.AppActivator{ + Name: app.Name, + Enabled: st.Enabled, + Active: st.Active, + Type: "timer", + }) + case ".socket": + appInfo.Activators = append(appInfo.Activators, client.AppActivator{ + Name: sockSvcFileToName[st.UnitName], + Enabled: st.Enabled, + Active: st.Active, + Type: "socket", + }) + } + } + out = append(out, appInfo) + } + + return out, nil +} diff -Nru snapd-2.32.3.2~14.04/cmd/appinfo_test.go snapd-2.37~rc1~14.04/cmd/appinfo_test.go --- snapd-2.32.3.2~14.04/cmd/appinfo_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/appinfo_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,71 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package cmd_test + +import ( + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/cmd" +) + +func (*cmdSuite) TestAppStatusNotes(c *check.C) { + ai := client.AppInfo{} + c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "-") + + ai = client.AppInfo{ + Daemon: "oneshot", + } + c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "-") + + ai = client.AppInfo{ + Daemon: "oneshot", + Activators: []client.AppActivator{ + {Type: "timer"}, + }, + } + c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "timer-activated") + + ai = client.AppInfo{ + Daemon: "oneshot", + Activators: []client.AppActivator{ + {Type: "socket"}, + }, + } + c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "socket-activated") + + // check that the output is stable regardless of the order of activators + ai = client.AppInfo{ + Daemon: "oneshot", + Activators: []client.AppActivator{ + {Type: "timer"}, + {Type: "socket"}, + }, + } + c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "timer-activated,socket-activated") + ai = client.AppInfo{ + Daemon: "oneshot", + Activators: []client.AppActivator{ + {Type: "socket"}, + {Type: "timer"}, + }, + } + c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "timer-activated,socket-activated") +} diff -Nru snapd-2.32.3.2~14.04/cmd/autogen.sh snapd-2.37~rc1~14.04/cmd/autogen.sh --- snapd-2.32.3.2~14.04/cmd/autogen.sh 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/autogen.sh 2019-01-16 08:36:51.000000000 +0000 @@ -27,7 +27,7 @@ . /etc/os-release case "$ID" in arch) - extra_opts="--libexecdir=/usr/lib/snapd --with-snap-mount-dir=/var/lib/snapd/snap --disable-apparmor --enable-nvidia-biarch --enable-merged-usr" + extra_opts="--libexecdir=/usr/lib/snapd --with-snap-mount-dir=/var/lib/snapd/snap --enable-apparmor --enable-nvidia-biarch --enable-merged-usr" ;; debian) extra_opts="--libexecdir=/usr/lib/snapd" @@ -41,8 +41,8 @@ fedora|centos|rhel) extra_opts="--libexecdir=/usr/libexec/snapd --with-snap-mount-dir=/var/lib/snapd/snap --enable-merged-usr --disable-apparmor" ;; - opensuse) - extra_opts="--libexecdir=/usr/lib/snapd" + opensuse|opensuse-tumbleweed) + extra_opts="--libexecdir=/usr/lib/snapd --enable-nvidia-biarch --with-32bit-libdir=/usr/lib --enable-merged-usr" ;; solus) extra_opts="--enable-nvidia-biarch" diff -Nru snapd-2.32.3.2~14.04/cmd/cmd.go snapd-2.37~rc1~14.04/cmd/cmd.go --- snapd-2.32.3.2~14.04/cmd/cmd.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/cmd.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,205 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- - -/* - * Copyright (C) 2016 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package cmd - -import ( - "io/ioutil" - "log" - "os" - "path/filepath" - "regexp" - "strings" - "syscall" - - "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/logger" - "github.com/snapcore/snapd/osutil" - "github.com/snapcore/snapd/release" - "github.com/snapcore/snapd/strutil" -) - -// The SNAP_REEXEC environment variable controls whether the command -// will attempt to re-exec itself from inside an ubuntu-core snap -// present on the system. If not present in the environ it's assumed -// to be set to 1 (do re-exec); that is: set it to 0 to disable. -const reExecKey = "SNAP_REEXEC" - -var ( - // newCore is the place to look for the core snap; everything in this - // location will be new enough to re-exec into. - newCore = "/snap/core/current" - - // oldCore is the previous location of the core snap. Only things - // newer than minOldRevno will be ok to re-exec into. - oldCore = "/snap/ubuntu-core/current" - - // selfExe is the path to a symlink pointing to the current executable - selfExe = "/proc/self/exe" - - syscallExec = syscall.Exec - osReadlink = os.Readlink -) - -// distroSupportsReExec returns true if the distribution we are running on can use re-exec. -// -// This is true by default except for a "core/all" snap system where it makes -// no sense and in certain distributions that we don't want to enable re-exec -// yet because of missing validation or other issues. -func distroSupportsReExec() bool { - if !release.OnClassic { - return false - } - if !release.DistroLike("debian", "ubuntu") { - logger.Debugf("re-exec not supported on distro %q yet", release.ReleaseInfo.ID) - return false - } - return true -} - -// coreSupportsReExec returns true if the given core snap should be used as re-exec target. -// -// Ensure we do not use older version of snapd, look for info file and ignore -// version of core that do not yet have it. -func coreSupportsReExec(corePath string) bool { - fullInfo := filepath.Join(corePath, filepath.Join(dirs.CoreLibExecDir, "info")) - if !osutil.FileExists(fullInfo) { - return false - } - content, err := ioutil.ReadFile(fullInfo) - if err != nil { - logger.Noticef("cannot read snapd info file %q: %s", fullInfo, err) - return false - } - ver := regexp.MustCompile("(?m)^VERSION=(.*)$").FindStringSubmatch(string(content)) - if len(ver) != 2 { - logger.Noticef("cannot find snapd version information in %q", content) - return false - } - // > 0 means our Version is bigger than the version of snapd in core - res, err := strutil.VersionCompare(Version, ver[1]) - if err != nil { - logger.Debugf("cannot version compare %q and %q: %s", Version, ver[1], res) - return false - } - if res > 0 { - logger.Debugf("core snap (at %q) is older (%q) than distribution package (%q)", corePath, ver[1], Version) - return false - } - return true -} - -// InternalToolPath returns the path of an internal snapd tool. The tool -// *must* be located inside /usr/lib/snapd/. -// -// The return value is either the path of the tool in the current distribution -// or in the core snap (or the ubuntu-core snap). This handles spiritual -// "re-exec" where we run the tool from the core snap if the environment allows -// us to do so. -func InternalToolPath(tool string) string { - distroTool := filepath.Join(dirs.DistroLibExecDir, tool) - - // find the internal path relative to the running snapd, this - // ensure we don't rely on the state of the system (like - // having a valid "current" symlink). - exe, err := osReadlink("/proc/self/exe") - if err != nil { - logger.Noticef("cannot read /proc/self/exe: %v, using tool outside core", err) - return distroTool - } - - // ensure we never use this helper from anything but - if !strings.HasSuffix(exe, "/snapd") && !strings.HasSuffix(exe, ".test") { - log.Panicf("InternalToolPath can only be used from snapd, got: %s", exe) - } - - if !strings.HasPrefix(exe, dirs.SnapMountDir) { - logger.Debugf("exe doesn't have snap mount dir prefix: %q vs %q", exe, dirs.SnapMountDir) - return distroTool - } - - // if we are re-execed, then the tool is at the same location - // as snapd - return filepath.Join(filepath.Dir(exe), tool) -} - -// mustUnsetenv will unset the given environment key or panic if it -// cannot do that -func mustUnsetenv(key string) { - if err := os.Unsetenv(key); err != nil { - log.Panicf("cannot unset %s: %s", key, err) - } -} - -// ExecInCoreSnap makes sure you're executing the binary that ships in -// the core snap. -func ExecInCoreSnap() { - // Which executable are we? - exe, err := os.Readlink(selfExe) - if err != nil { - logger.Noticef("cannot read /proc/self/exe: %v", err) - return - } - - // Special case for snapd re-execing from 2.21. In this - // version of snap/snapd we did set SNAP_REEXEC=0 when we - // re-execed. In this case we need to unset the reExecKey to - // ensure that subsequent run of snap/snapd (e.g. when using - // classic confinement) will *not* prevented from re-execing. - if strings.HasPrefix(exe, dirs.SnapMountDir) && !osutil.GetenvBool(reExecKey, true) { - mustUnsetenv(reExecKey) - return - } - - // If we are asked not to re-execute use distribution packages. This is - // "spiritual" re-exec so use the same environment variable to decide. - if !osutil.GetenvBool(reExecKey, true) { - logger.Debugf("re-exec disabled by user") - return - } - - // Did we already re-exec? - if strings.HasPrefix(exe, dirs.SnapMountDir) { - return - } - - // If the distribution doesn't support re-exec or run-from-core then don't do it. - if !distroSupportsReExec() { - return - } - - // Is this executable in the core snap too? - corePath := newCore - full := filepath.Join(newCore, exe) - if !osutil.FileExists(full) { - corePath = oldCore - full = filepath.Join(oldCore, exe) - if !osutil.FileExists(full) { - return - } - } - - // If the core snap doesn't support re-exec or run-from-core then don't do it. - if !coreSupportsReExec(corePath) { - return - } - - logger.Debugf("restarting into %q", full) - panic(syscallExec(full, os.Args, os.Environ())) -} diff -Nru snapd-2.32.3.2~14.04/cmd/cmd_linux.go snapd-2.37~rc1~14.04/cmd/cmd_linux.go --- snapd-2.32.3.2~14.04/cmd/cmd_linux.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/cmd_linux.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,214 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package cmd + +import ( + "bytes" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + "syscall" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/strutil" +) + +// The SNAP_REEXEC environment variable controls whether the command +// will attempt to re-exec itself from inside an ubuntu-core snap +// present on the system. If not present in the environ it's assumed +// to be set to 1 (do re-exec); that is: set it to 0 to disable. +const reExecKey = "SNAP_REEXEC" + +var ( + // snapdSnap is the place to look for the snapd snap; we will re-exec + // here + snapdSnap = "/snap/snapd/current" + + // coreSnap is the place to look for the core snap; we will re-exec + // here if there is no snapd snap + coreSnap = "/snap/core/current" + + // selfExe is the path to a symlink pointing to the current executable + selfExe = "/proc/self/exe" + + syscallExec = syscall.Exec + osReadlink = os.Readlink +) + +// distroSupportsReExec returns true if the distribution we are running on can use re-exec. +// +// This is true by default except for a "core/all" snap system where it makes +// no sense and in certain distributions that we don't want to enable re-exec +// yet because of missing validation or other issues. +func distroSupportsReExec() bool { + if !release.OnClassic { + return false + } + if !release.DistroLike("debian", "ubuntu") { + logger.Debugf("re-exec not supported on distro %q yet", release.ReleaseInfo.ID) + return false + } + return true +} + +// coreSupportsReExec returns true if the given core snap should be used as re-exec target. +// +// Ensure we do not use older version of snapd, look for info file and ignore +// version of core that do not yet have it. +func coreSupportsReExec(corePath string) bool { + fullInfo := filepath.Join(corePath, filepath.Join(dirs.CoreLibExecDir, "info")) + content, err := ioutil.ReadFile(fullInfo) + if err != nil { + if !os.IsNotExist(err) { + logger.Noticef("cannot open snapd info file %q: %s", fullInfo, err) + } + return false + } + + if !bytes.HasPrefix(content, []byte("VERSION=")) { + idx := bytes.Index(content, []byte("\nVERSION=")) + if idx < 0 { + logger.Noticef("cannot find snapd version information in %q", content) + return false + } + content = content[idx+1:] + } + content = content[8:] + idx := bytes.IndexByte(content, '\n') + if idx > -1 { + content = content[:idx] + } + ver := string(content) + // > 0 means our Version is bigger than the version of snapd in core + res, err := strutil.VersionCompare(Version, ver) + if err != nil { + logger.Debugf("cannot version compare %q and %q: %v", Version, ver, err) + return false + } + if res > 0 { + logger.Debugf("core snap (at %q) is older (%q) than distribution package (%q)", corePath, ver, Version) + return false + } + return true +} + +// InternalToolPath returns the path of an internal snapd tool. The tool +// *must* be located inside /usr/lib/snapd/. +// +// The return value is either the path of the tool in the current distribution +// or in the core snap (or the ubuntu-core snap). This handles spiritual +// "re-exec" where we run the tool from the core snap if the environment allows +// us to do so. +func InternalToolPath(tool string) string { + distroTool := filepath.Join(dirs.DistroLibExecDir, tool) + + // find the internal path relative to the running snapd, this + // ensure we don't rely on the state of the system (like + // having a valid "current" symlink). + exe, err := osReadlink("/proc/self/exe") + if err != nil { + logger.Noticef("cannot read /proc/self/exe: %v, using tool outside core", err) + return distroTool + } + + // ensure we never use this helper from anything but + if !strings.HasSuffix(exe, "/snapd") && !strings.HasSuffix(exe, ".test") { + log.Panicf("InternalToolPath can only be used from snapd, got: %s", exe) + } + + if !strings.HasPrefix(exe, dirs.SnapMountDir) { + logger.Debugf("exe doesn't have snap mount dir prefix: %q vs %q", exe, dirs.SnapMountDir) + return distroTool + } + + // if we are re-execed, then the tool is at the same location + // as snapd + return filepath.Join(filepath.Dir(exe), tool) +} + +// mustUnsetenv will unset the given environment key or panic if it +// cannot do that +func mustUnsetenv(key string) { + if err := os.Unsetenv(key); err != nil { + log.Panicf("cannot unset %s: %s", key, err) + } +} + +// ExecInSnapdOrCoreSnap makes sure you're executing the binary that ships in +// the snapd/core snap. +func ExecInSnapdOrCoreSnap() { + // Which executable are we? + exe, err := os.Readlink(selfExe) + if err != nil { + logger.Noticef("cannot read /proc/self/exe: %v", err) + return + } + + // Special case for snapd re-execing from 2.21. In this + // version of snap/snapd we did set SNAP_REEXEC=0 when we + // re-execed. In this case we need to unset the reExecKey to + // ensure that subsequent run of snap/snapd (e.g. when using + // classic confinement) will *not* prevented from re-execing. + if strings.HasPrefix(exe, dirs.SnapMountDir) && !osutil.GetenvBool(reExecKey, true) { + mustUnsetenv(reExecKey) + return + } + + // If we are asked not to re-execute use distribution packages. This is + // "spiritual" re-exec so use the same environment variable to decide. + if !osutil.GetenvBool(reExecKey, true) { + logger.Debugf("re-exec disabled by user") + return + } + + // Did we already re-exec? + if strings.HasPrefix(exe, dirs.SnapMountDir) { + return + } + + // If the distribution doesn't support re-exec or run-from-core then don't do it. + if !distroSupportsReExec() { + return + } + + // Is this executable in the core snap too? + corePath := snapdSnap + full := filepath.Join(snapdSnap, exe) + if !osutil.FileExists(full) { + corePath = coreSnap + full = filepath.Join(coreSnap, exe) + if !osutil.FileExists(full) { + return + } + } + + // If the core snap doesn't support re-exec or run-from-core then don't do it. + if !coreSupportsReExec(corePath) { + return + } + + logger.Debugf("restarting into %q", full) + panic(syscallExec(full, os.Args, os.Environ())) +} diff -Nru snapd-2.32.3.2~14.04/cmd/cmd_linux_test.go snapd-2.37~rc1~14.04/cmd/cmd_linux_test.go --- snapd-2.32.3.2~14.04/cmd/cmd_linux_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/cmd_linux_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,117 @@ +package cmd + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/snapcore/snapd/dirs" +) + +const dataOK = `one line +another line +yadda yadda +VERSION=42 +potatoes +` + +const dataNOK = `a line +another +this is a very long line +that wasn't long what are you talking about long lines are like, so long you need to add things like commas to them for them to even make sense +a short one +and another +what is this +why +no +stop +` + +const dataHuge = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. +Quisque euismod ac elit ac auctor. +Proin malesuada diam ac tellus maximus aliquam. +Aenean tincidunt mi et tortor bibendum fringilla. +Phasellus finibus, urna id convallis vestibulum, metus metus venenatis massa, et efficitur nisi elit in massa. +Mauris at nisl leo. +Nulla ullamcorper risus venenatis massa venenatis, ac finibus lacus aliquam. +Nunc tempor convallis cursus. +Maecenas id rhoncus orci, eget pretium eros. + +Donec et consectetur lacus. +Nam nec mattis elit, id sollicitudin magna. +Aenean sit amet diam vitae tellus finibus tristique. +Duis et pharetra tortor, id pharetra erat. +Suspendisse commodo venenatis blandit. +Morbi tellus est, iaculis et tincidunt nec, semper ut ipsum. +Mauris quis condimentum risus. +Lorem ipsum dolor sit amet, consectetur adipiscing elit. +Mauris gravida turpis ut urna laoreet, sit amet tempor odio porttitor. + +Aliquam nibh libero, venenatis ac vehicula at, blandit id odio. +Etiam malesuada consectetur porta. +Fusce consectetur ligula et metus interdum sollicitudin. +Pellentesque odio neque, pharetra et gravida non, vestibulum nec lorem. +Sed condimentum velit ex, sit amet viverra lectus aliquet quis. +Aliquam tincidunt eu elit at condimentum. +Donec feugiat urna tortor, pellentesque tincidunt quam congue eu. + +Phasellus vel libero molestie, semper erat at, suscipit nisi. +Nullam euismod neque ut turpis molestie, eu fringilla elit volutpat. +Phasellus maximus, urna eget porta congue, diam enim volutpat diam, nec ultrices lorem risus ac metus. +Vivamus convallis eros non nunc pretium bibendum. +Maecenas consectetur metus metus. +Morbi scelerisque urna at arcu tristique feugiat. +Vestibulum condimentum odio sed tortor vulputate, eget hendrerit mi consequat. +Integer egestas finibus augue, ac scelerisque ex pretium aliquam. +Aliquam erat volutpat. +Suspendisse a nulla ultrices, porttitor tellus ut, bibendum diam. +In nibh dui, tempus eget vestibulum in, euismod in ex. +In tempus felis lectus. + +Maecenas suscipit turpis eget velit molestie, quis luctus nibh placerat. +Nulla semper eleifend nisi ut dignissim. +Donec eu massa maximus, blandit massa ac, lobortis risus. +Donec id condimentum libero, vel fringilla diam. +Praesent ultrices, ante congue sollicitudin sagittis, orci ex maximus ipsum, at convallis nunc nisl nec lorem. +Duis iaculis finibus fermentum. +Curabitur quis pharetra metus. +Donec nisl ipsum, faucibus vitae odio sed, mattis feugiat nisl. +Pellentesque nec justo in magna volutpat accumsan. +Pellentesque porttitor justo non velit porta rhoncus. +Nulla ut lectus quis lectus rutrum dignissim. +Pellentesque posuere sagittis felis, quis varius purus pharetra eu. +Nam blandit diam ullamcorper, auctor massa at, aliquet dui. +Aliquam erat volutpat. +Nullam sit amet augue nec diam sollicitudin ullamcorper a vitae neque. +VERSION=42 +` + +func benchmarkCSRE(b *testing.B, data string) { + tempdir, err := ioutil.TempDir("", "") + if err != nil { + b.Fatalf("tempdir: %v", err) + } + defer os.RemoveAll(tempdir) + if err = os.MkdirAll(filepath.Join(tempdir, dirs.CoreLibExecDir), 0755); err != nil { + b.Fatalf("mkdirall: %v", err) + } + + if err = ioutil.WriteFile(filepath.Join(tempdir, dirs.CoreLibExecDir, "info"), []byte(data), 0600); err != nil { + b.Fatalf("%v", err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + coreSupportsReExec(tempdir) + } +} + +func BenchmarkCSRE_fakeOK(b *testing.B) { benchmarkCSRE(b, dataOK) } +func BenchmarkCSRE_fakeNOK(b *testing.B) { benchmarkCSRE(b, dataNOK) } +func BenchmarkCSRE_fakeHuge(b *testing.B) { benchmarkCSRE(b, dataHuge) } + +func BenchmarkCSRE_real(b *testing.B) { + for i := 0; i < b.N; i++ { + coreSupportsReExec("/snap/core/current") + } +} diff -Nru snapd-2.32.3.2~14.04/cmd/cmd_other.go snapd-2.37~rc1~14.04/cmd/cmd_other.go --- snapd-2.32.3.2~14.04/cmd/cmd_other.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/cmd_other.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,28 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +build !linux + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package cmd + +// ExecInSnapdOrCoreSnap makes sure you're executing the binary that ships in +// the snapd/core snap. +// On this OS this is a stub. +func ExecInSnapdOrCoreSnap() { + return +} diff -Nru snapd-2.32.3.2~14.04/cmd/cmd_test.go snapd-2.37~rc1~14.04/cmd/cmd_test.go --- snapd-2.32.3.2~14.04/cmd/cmd_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/cmd_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -44,8 +44,8 @@ lastExecArgv []string lastExecEnvv []string fakeroot string - newCore string - oldCore string + snapdPath string + corePath string } var _ = Suite(&cmdSuite{}) @@ -59,8 +59,8 @@ s.lastExecEnvv = nil s.fakeroot = c.MkDir() dirs.SetRootDir(s.fakeroot) - s.newCore = filepath.Join(dirs.SnapMountDir, "/core/42") - s.oldCore = filepath.Join(dirs.SnapMountDir, "/ubuntu-core/21") + s.snapdPath = filepath.Join(dirs.SnapMountDir, "/snapd/42") + s.corePath = filepath.Join(dirs.SnapMountDir, "/core/21") c.Assert(os.MkdirAll(filepath.Join(s.fakeroot, "proc/self"), 0755), IsNil) } @@ -95,7 +95,7 @@ restore := []func(){ release.MockOnClassic(true), release.MockReleaseInfo(&release.OS{ID: "ubuntu"}), - cmd.MockCorePaths(s.oldCore, s.newCore), + cmd.MockCoreSnapdPaths(s.corePath, s.snapdPath), cmd.MockVersion("2"), } @@ -148,7 +148,7 @@ // no distro supports re-exec when not on classic :-) for _, id := range []string{ "fedora", "centos", "rhel", "opensuse", "suse", "poky", - "debian", "ubuntu", "arch", + "debian", "ubuntu", "arch", "archlinux", } { restore = release.MockReleaseInfo(&release.OS{ID: id}) defer restore() @@ -163,41 +163,41 @@ func (s *cmdSuite) TestCoreSupportsReExecBadInfo(c *C) { // can't read snapd/info if it's a directory - p := s.newCore + "/usr/lib/snapd/info" + p := s.snapdPath + "/usr/lib/snapd/info" c.Assert(os.MkdirAll(p, 0755), IsNil) - c.Check(cmd.CoreSupportsReExec(s.newCore), Equals, false) + c.Check(cmd.CoreSupportsReExec(s.snapdPath), Equals, false) } func (s *cmdSuite) TestCoreSupportsReExecBadInfoContent(c *C) { // can't understand snapd/info if all it holds are potatoes - p := s.newCore + "/usr/lib/snapd" + p := s.snapdPath + "/usr/lib/snapd" c.Assert(os.MkdirAll(p, 0755), IsNil) c.Assert(ioutil.WriteFile(p+"/info", []byte("potatoes"), 0644), IsNil) - c.Check(cmd.CoreSupportsReExec(s.newCore), Equals, false) + c.Check(cmd.CoreSupportsReExec(s.snapdPath), Equals, false) } func (s *cmdSuite) TestCoreSupportsReExecBadVersion(c *C) { // can't understand snapd/info if all its version is gibberish - s.fakeCoreVersion(c, s.newCore, "0:") + s.fakeCoreVersion(c, s.snapdPath, "0:") - c.Check(cmd.CoreSupportsReExec(s.newCore), Equals, false) + c.Check(cmd.CoreSupportsReExec(s.snapdPath), Equals, false) } func (s *cmdSuite) TestCoreSupportsReExecOldVersion(c *C) { // can't re-exec if core version is too old defer cmd.MockVersion("2")() - s.fakeCoreVersion(c, s.newCore, "0") + s.fakeCoreVersion(c, s.snapdPath, "0") - c.Check(cmd.CoreSupportsReExec(s.newCore), Equals, false) + c.Check(cmd.CoreSupportsReExec(s.snapdPath), Equals, false) } func (s *cmdSuite) TestCoreSupportsReExec(c *C) { defer cmd.MockVersion("2")() - s.fakeCoreVersion(c, s.newCore, "9999") + s.fakeCoreVersion(c, s.snapdPath, "9999") - c.Check(cmd.CoreSupportsReExec(s.newCore), Equals, true) + c.Check(cmd.CoreSupportsReExec(s.snapdPath), Equals, true) } func (s *cmdSuite) TestInternalToolPathNoReexec(c *C) { @@ -210,13 +210,13 @@ } func (s *cmdSuite) TestInternalToolPathWithReexec(c *C) { - s.fakeInternalTool(c, s.newCore, "potato") + s.fakeInternalTool(c, s.snapdPath, "potato") restore := cmd.MockOsReadlink(func(string) (string, error) { - return filepath.Join(s.newCore, "/usr/lib/snapd/snapd"), nil + return filepath.Join(s.snapdPath, "/usr/lib/snapd/snapd"), nil }) defer restore() - c.Check(cmd.InternalToolPath("potato"), Equals, filepath.Join(dirs.SnapMountDir, "core/42/usr/lib/snapd/potato")) + c.Check(cmd.InternalToolPath("potato"), Equals, filepath.Join(dirs.SnapMountDir, "snapd/42/usr/lib/snapd/potato")) } func (s *cmdSuite) TestInternalToolPathFromIncorrectHelper(c *C) { @@ -228,80 +228,80 @@ c.Check(func() { cmd.InternalToolPath("potato") }, PanicMatches, "InternalToolPath can only be used from snapd, got: /usr/bin/potato") } -func (s *cmdSuite) TestExecInCoreSnap(c *C) { - defer s.mockReExecFor(c, s.newCore, "potato")() +func (s *cmdSuite) TestExecInSnapdOrCoreSnap(c *C) { + defer s.mockReExecFor(c, s.snapdPath, "potato")() - c.Check(cmd.ExecInCoreSnap, PanicMatches, `>exec of "[^"]+/potato" in tests<`) + c.Check(cmd.ExecInSnapdOrCoreSnap, PanicMatches, `>exec of "[^"]+/potato" in tests<`) c.Check(s.execCalled, Equals, 1) - c.Check(s.lastExecArgv0, Equals, filepath.Join(s.newCore, "/usr/lib/snapd/potato")) + c.Check(s.lastExecArgv0, Equals, filepath.Join(s.snapdPath, "/usr/lib/snapd/potato")) c.Check(s.lastExecArgv, DeepEquals, os.Args) } func (s *cmdSuite) TestExecInOldCoreSnap(c *C) { - defer s.mockReExecFor(c, s.oldCore, "potato")() + defer s.mockReExecFor(c, s.corePath, "potato")() - c.Check(cmd.ExecInCoreSnap, PanicMatches, `>exec of "[^"]+/potato" in tests<`) + c.Check(cmd.ExecInSnapdOrCoreSnap, PanicMatches, `>exec of "[^"]+/potato" in tests<`) c.Check(s.execCalled, Equals, 1) - c.Check(s.lastExecArgv0, Equals, filepath.Join(s.oldCore, "/usr/lib/snapd/potato")) + c.Check(s.lastExecArgv0, Equals, filepath.Join(s.corePath, "/usr/lib/snapd/potato")) c.Check(s.lastExecArgv, DeepEquals, os.Args) } -func (s *cmdSuite) TestExecInCoreSnapBailsNoCoreSupport(c *C) { - defer s.mockReExecFor(c, s.newCore, "potato")() +func (s *cmdSuite) TestExecInSnapdOrCoreSnapBailsNoCoreSupport(c *C) { + defer s.mockReExecFor(c, s.snapdPath, "potato")() // no "info" -> no core support: - c.Assert(os.Remove(filepath.Join(s.newCore, "/usr/lib/snapd/info")), IsNil) + c.Assert(os.Remove(filepath.Join(s.snapdPath, "/usr/lib/snapd/info")), IsNil) - cmd.ExecInCoreSnap() + cmd.ExecInSnapdOrCoreSnap() c.Check(s.execCalled, Equals, 0) } -func (s *cmdSuite) TestExecInCoreSnapMissingExe(c *C) { - defer s.mockReExecFor(c, s.newCore, "potato")() +func (s *cmdSuite) TestExecInSnapdOrCoreSnapMissingExe(c *C) { + defer s.mockReExecFor(c, s.snapdPath, "potato")() // missing exe: - c.Assert(os.Remove(filepath.Join(s.newCore, "/usr/lib/snapd/potato")), IsNil) + c.Assert(os.Remove(filepath.Join(s.snapdPath, "/usr/lib/snapd/potato")), IsNil) - cmd.ExecInCoreSnap() + cmd.ExecInSnapdOrCoreSnap() c.Check(s.execCalled, Equals, 0) } -func (s *cmdSuite) TestExecInCoreSnapBadSelfExe(c *C) { - defer s.mockReExecFor(c, s.newCore, "potato")() +func (s *cmdSuite) TestExecInSnapdOrCoreSnapBadSelfExe(c *C) { + defer s.mockReExecFor(c, s.snapdPath, "potato")() // missing self/exe: c.Assert(os.Remove(filepath.Join(s.fakeroot, "proc/self/exe")), IsNil) - cmd.ExecInCoreSnap() + cmd.ExecInSnapdOrCoreSnap() c.Check(s.execCalled, Equals, 0) } -func (s *cmdSuite) TestExecInCoreSnapBailsNoDistroSupport(c *C) { - defer s.mockReExecFor(c, s.newCore, "potato")() +func (s *cmdSuite) TestExecInSnapdOrCoreSnapBailsNoDistroSupport(c *C) { + defer s.mockReExecFor(c, s.snapdPath, "potato")() // no distro support: defer release.MockOnClassic(false)() - cmd.ExecInCoreSnap() + cmd.ExecInSnapdOrCoreSnap() c.Check(s.execCalled, Equals, 0) } -func (s *cmdSuite) TestExecInCoreSnapNoDouble(c *C) { +func (s *cmdSuite) TestExecInSnapdOrCoreSnapNoDouble(c *C) { selfExe := filepath.Join(s.fakeroot, "proc/self/exe") err := os.Symlink(filepath.Join(s.fakeroot, "/snap/core/42/usr/lib/snapd"), selfExe) c.Assert(err, IsNil) cmd.MockSelfExe(selfExe) - cmd.ExecInCoreSnap() + cmd.ExecInSnapdOrCoreSnap() c.Check(s.execCalled, Equals, 0) } -func (s *cmdSuite) TestExecInCoreSnapDisabled(c *C) { - defer s.mockReExecFor(c, s.newCore, "potato")() +func (s *cmdSuite) TestExecInSnapdOrCoreSnapDisabled(c *C) { + defer s.mockReExecFor(c, s.snapdPath, "potato")() os.Setenv("SNAP_REEXEC", "0") defer os.Unsetenv("SNAP_REEXEC") - cmd.ExecInCoreSnap() + cmd.ExecInSnapdOrCoreSnap() c.Check(s.execCalled, Equals, 0) } diff -Nru snapd-2.32.3.2~14.04/cmd/configure.ac snapd-2.37~rc1~14.04/cmd/configure.ac --- snapd-2.32.3.2~14.04/cmd/configure.ac 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/configure.ac 2019-01-16 08:36:51.000000000 +0000 @@ -228,5 +228,13 @@ AC_SUBST(HOST_ARCH32_TRIPLET) AC_DEFINE_UNQUOTED([HOST_ARCH32_TRIPLET], "${HOST_ARCH32_TRIPLET}", [Arch triplet for 32bit libraries]) +SYSTEMD_SYSTEM_GENERATOR_DIR="$($PKG_CONFIG --variable=systemdsystemgeneratordir systemd)" +AS_IF([test "x$SYSTEMD_SYSTEM_GENERATOR_DIR" = "x"], [SYSTEMD_SYSTEM_GENERATOR_DIR=/lib/systemd/system-generators]) +AC_SUBST(SYSTEMD_SYSTEM_GENERATOR_DIR) + +# FIXME: get this via something like pkgconf once it is defined there +SYSTEMD_SYSTEM_ENV_GENERATOR_DIR="${prefix}/lib/systemd/system-environment-generators" +AC_SUBST(SYSTEMD_SYSTEM_ENV_GENERATOR_DIR) + AC_CONFIG_FILES([Makefile]) AC_OUTPUT diff -Nru snapd-2.32.3.2~14.04/cmd/export_test.go snapd-2.37~rc1~14.04/cmd/export_test.go --- snapd-2.32.3.2~14.04/cmd/export_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/export_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -24,14 +24,14 @@ CoreSupportsReExec = coreSupportsReExec ) -func MockCorePaths(newOldCore, newNewCore string) func() { - oldOldCore := oldCore - oldNewCore := newCore - newCore = newNewCore - oldCore = newOldCore +func MockCoreSnapdPaths(newCoreSnap, newSnapdSnap string) func() { + oldOldCore := coreSnap + oldNewCore := snapdSnap + snapdSnap = newSnapdSnap + coreSnap = newCoreSnap return func() { - newCore = oldNewCore - oldCore = oldOldCore + snapdSnap = oldNewCore + coreSnap = oldOldCore } } diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/apparmor-support.c snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/apparmor-support.c --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/apparmor-support.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/apparmor-support.c 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "apparmor-support.h" + +#include +#include +#ifdef HAVE_APPARMOR +#include +#endif // ifdef HAVE_APPARMOR + +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/utils.h" + +// NOTE: Those constants map exactly what apparmor is returning and cannot be +// changed without breaking apparmor functionality. +#define SC_AA_ENFORCE_STR "enforce" +#define SC_AA_COMPLAIN_STR "complain" +#define SC_AA_MIXED_STR "mixed" +#define SC_AA_UNCONFINED_STR "unconfined" + +void sc_init_apparmor_support(struct sc_apparmor *apparmor) +{ +#ifdef HAVE_APPARMOR + // Use aa_is_enabled() to see if apparmor is available in the kernel and + // enabled at boot time. If it isn't log a diagnostic message and assume + // we're not confined. + if (aa_is_enabled() != true) { + switch (errno) { + case ENOSYS: + debug + ("apparmor extensions to the system are not available"); + break; + case ECANCELED: + debug + ("apparmor is available on the system but has been disabled at boot"); + break; + case ENOENT: + debug + ("apparmor is available but the interface but the interface is not available"); + break; + case EPERM: + // NOTE: fall-through + case EACCES: + debug + ("insufficient permissions to determine if apparmor is enabled"); + break; + default: + debug("apparmor is not enabled: %s", strerror(errno)); + break; + } + apparmor->is_confined = false; + apparmor->mode = SC_AA_NOT_APPLICABLE; + return; + } + // Use aa_getcon() to check the label of the current process and + // confinement type. Note that the returned label must be released with + // free() but the mode is a constant string that must not be freed. + char *label SC_CLEANUP(sc_cleanup_string) = NULL; + char *mode = NULL; + if (aa_getcon(&label, &mode) < 0) { + die("cannot query current apparmor profile"); + } + debug("apparmor label on snap-confine is: %s", label); + debug("apparmor mode is: %s", mode); + // The label has a special value "unconfined" that is applied to all + // processes without a dedicated profile. If that label is used then the + // current process is not confined. All other labels imply confinement. + if (label != NULL && strcmp(label, SC_AA_UNCONFINED_STR) == 0) { + apparmor->is_confined = false; + } else { + apparmor->is_confined = true; + } + // There are several possible results for the confinement type (mode) that + // are checked for below. + if (mode != NULL && strcmp(mode, SC_AA_COMPLAIN_STR) == 0) { + apparmor->mode = SC_AA_COMPLAIN; + } else if (mode != NULL && strcmp(mode, SC_AA_ENFORCE_STR) == 0) { + apparmor->mode = SC_AA_ENFORCE; + } else if (mode != NULL && strcmp(mode, SC_AA_MIXED_STR) == 0) { + apparmor->mode = SC_AA_MIXED; + } else { + apparmor->mode = SC_AA_INVALID; + } +#else + apparmor->mode = SC_AA_NOT_APPLICABLE; + apparmor->is_confined = false; +#endif // ifdef HAVE_APPARMOR +} + +void +sc_maybe_aa_change_onexec(struct sc_apparmor *apparmor, const char *profile) +{ +#ifdef HAVE_APPARMOR + if (apparmor->mode == SC_AA_NOT_APPLICABLE) { + return; + } + debug("requesting changing of apparmor profile on next exec to %s", + profile); + if (aa_change_onexec(profile) < 0) { + if (secure_getenv("SNAPPY_LAUNCHER_INSIDE_TESTS") == NULL) { + die("cannot change profile for the next exec call"); + } + } +#endif // ifdef HAVE_APPARMOR +} + +void +sc_maybe_aa_change_hat(struct sc_apparmor *apparmor, + const char *subprofile, unsigned long magic_token) +{ +#ifdef HAVE_APPARMOR + if (apparmor->mode == SC_AA_NOT_APPLICABLE) { + return; + } + if (apparmor->is_confined) { + debug("changing apparmor hat to %s", subprofile); + if (aa_change_hat(subprofile, magic_token) < 0) { + die("cannot change apparmor hat"); + } + } +#endif // ifdef HAVE_APPARMOR +} diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/apparmor-support.h snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/apparmor-support.h --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/apparmor-support.h 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/apparmor-support.h 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef SNAP_CONFINE_APPARMOR_SUPPORT_H +#define SNAP_CONFINE_APPARMOR_SUPPORT_H + +#include + +/** + * Type of apparmor confinement. + **/ +enum sc_apparmor_mode { + // The enforcement mode was not recognized. + SC_AA_INVALID = -1, + // The enforcement mode is not applicable because apparmor is disabled. + SC_AA_NOT_APPLICABLE = 0, + // The enforcement mode is "enforcing" + SC_AA_ENFORCE = 1, + // The enforcement mode is "complain" + SC_AA_COMPLAIN, + // The enforcement mode is "mixed" + SC_AA_MIXED, +}; + +/** + * Data required to manage apparmor wrapper. + **/ +struct sc_apparmor { + // The mode of enforcement. In addition to the two apparmor defined modes + // can be also SC_AA_INVALID (unknown mode reported by apparmor) and + // SC_AA_NOT_APPLICABLE (when we're not linked with apparmor). + enum sc_apparmor_mode mode; + // Flag indicating that the current process is confined. + bool is_confined; +}; + +/** + * Initialize apparmor support. + * + * This operation should be done even when apparmor support is disabled at + * compile time. Internally the supplied structure is initialized based on the + * information returned from aa_getcon(2) or if apparmor is disabled at compile + * time, with built-in constants. + * + * The main action performed here is to check if snap-confine is currently + * confined, this information is used later in sc_maybe_change_apparmor_hat() + * + * As with many functions in the snap-confine tree, all errors result in + * process termination. + **/ +void sc_init_apparmor_support(struct sc_apparmor *apparmor); + +/** + * Maybe call aa_change_onexec(2) + * + * This function does nothing when apparmor support is not enabled at compile + * time. If apparmor is enabled then profile change request is attempted. + * + * As with many functions in the snap-confine tree, all errors result in + * process termination. As an exception, when SNAPPY_LAUNCHER_INSIDE_TESTS + * environment variable is set then the process is not terminated. + **/ +void +sc_maybe_aa_change_onexec(struct sc_apparmor *apparmor, const char *profile); + +/** + * Maybe call aa_change_hat(2) + * + * This function does nothing when apparmor support is not enabled at compile + * time. If apparmor is enabled then hat change is attempted. + * + * As with many functions in the snap-confine tree, all errors result in + * process termination. + **/ +void +sc_maybe_aa_change_hat(struct sc_apparmor *apparmor, + const char *subprofile, unsigned long magic_token); + +#endif diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/classic.c snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/classic.c --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/classic.c 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/classic.c 2019-01-16 08:36:51.000000000 +0000 @@ -1,25 +1,63 @@ #include "config.h" #include "classic.h" #include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/string-utils.h" -#include +#include #include +#include #include -char *os_release = "/etc/os-release"; +static const char *os_release = "/etc/os-release"; +static const char *meta_snap_yaml = "/meta/snap.yaml"; -bool is_running_on_classic_distribution() +sc_distro sc_classify_distro(void) { FILE *f SC_CLEANUP(sc_cleanup_file) = fopen(os_release, "r"); if (f == NULL) { - return true; + return SC_DISTRO_CLASSIC; } + bool is_core = false; + int core_version = 0; char buf[255] = { 0 }; + while (fgets(buf, sizeof buf, f) != NULL) { - if (strcmp(buf, "ID=ubuntu-core\n") == 0) { - return false; + size_t len = strlen(buf); + if (len > 0 && buf[len - 1] == '\n') { + buf[len - 1] = '\0'; + } + if (sc_streq(buf, "ID=\"ubuntu-core\"") + || sc_streq(buf, "ID=ubuntu-core")) { + is_core = true; + } else if (sc_streq(buf, "VERSION_ID=\"16\"") + || sc_streq(buf, "VERSION_ID=16")) { + core_version = 16; + } else if (sc_streq(buf, "VARIANT_ID=\"snappy\"") + || sc_streq(buf, "VARIANT_ID=snappy")) { + is_core = true; + } + } + + if (!is_core) { + /* Since classic systems don't have a /meta/snap.yaml file the simple + presence of that file qualifies as SC_DISTRO_CORE_OTHER. */ + if (access(meta_snap_yaml, F_OK) == 0) { + is_core = true; } } - return true; + + if (is_core) { + if (core_version == 16) { + return SC_DISTRO_CORE16; + } + return SC_DISTRO_CORE_OTHER; + } else { + return SC_DISTRO_CLASSIC; + } +} + +bool sc_should_use_normal_mode(sc_distro distro, const char *base_snap_name) +{ + return distro != SC_DISTRO_CORE16 || !sc_streq(base_snap_name, "core"); } diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/classic.h snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/classic.h --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/classic.h 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/classic.h 2019-01-16 08:36:51.000000000 +0000 @@ -22,6 +22,14 @@ // Location of the host filesystem directory in the core snap. #define SC_HOSTFS_DIR "/var/lib/snapd/hostfs" -bool is_running_on_classic_distribution(void); +typedef enum sc_distro { + SC_DISTRO_CORE16, // As present in both "core" and later on in "core16" + SC_DISTRO_CORE_OTHER, // Any core distribution. + SC_DISTRO_CLASSIC, // Any classic distribution. +} sc_distro; + +sc_distro sc_classify_distro(void); + +bool sc_should_use_normal_mode(sc_distro distro, const char *base_snap_name); #endif diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/classic-test.c snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/classic-test.c --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/classic-test.c 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/classic-test.c 2019-01-16 08:36:51.000000000 +0000 @@ -20,32 +20,104 @@ #include -const char *os_release_classic = "" +/* restore_os_release is an internal helper for mock_os_release */ +static void restore_os_release(gpointer * old) +{ + unlink(os_release); + os_release = (const char *)old; +} + +/* mock_os_release replaces the presence and contents of /etc/os-release + as seen by classic.c. The mocked value may be NULL to have the code refer + to an absent file. */ +static void mock_os_release(const char *mocked) +{ + const char *old = os_release; + if (mocked != NULL) { + os_release = "os-release.test"; + g_file_set_contents(os_release, mocked, -1, NULL); + } else { + os_release = "os-release.missing"; + } + g_test_queue_destroy((GDestroyNotify) restore_os_release, + (gpointer) old); +} + +/* restore_meta_snap_yaml is an internal helper for mock_meta_snap_yaml */ +static void restore_meta_snap_yaml(gpointer * old) +{ + unlink(meta_snap_yaml); + meta_snap_yaml = (const char *)old; +} + +/* mock_meta_snap_yaml replaces the presence and contents of /meta/snap.yaml + as seen by classic.c. The mocked value may be NULL to have the code refer + to an absent file. */ +static void mock_meta_snap_yaml(const char *mocked) +{ + const char *old = meta_snap_yaml; + if (mocked != NULL) { + meta_snap_yaml = "snap-yaml.test"; + g_file_set_contents(meta_snap_yaml, mocked, -1, NULL); + } else { + meta_snap_yaml = "snap-yaml.missing"; + } + g_test_queue_destroy((GDestroyNotify) restore_meta_snap_yaml, + (gpointer) old); +} + +static const char *os_release_classic = "" "NAME=\"Ubuntu\"\n" "VERSION=\"17.04 (Zesty Zapus)\"\n" "ID=ubuntu\n" "ID_LIKE=debian\n"; static void test_is_on_classic(void) { - g_file_set_contents("os-release.classic", os_release_classic, - strlen(os_release_classic), NULL); - os_release = "os-release.classic"; - g_assert_true(is_running_on_classic_distribution()); - unlink("os-release.classic"); + mock_os_release(os_release_classic); + mock_meta_snap_yaml(NULL); + g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CLASSIC); +} + +static const char *os_release_core16 = "" + "NAME=\"Ubuntu Core\"\n" "VERSION_ID=\"16\"\n" "ID=ubuntu-core\n"; + +static const char *meta_snap_yaml_core16 = "" + "name: core\n" + "version: 16-something\n" "type: core\n" "architectures: [amd64]\n"; + +static void test_is_on_core_on16(void) +{ + mock_os_release(os_release_core16); + mock_meta_snap_yaml(meta_snap_yaml_core16); + g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CORE16); } -const char *os_release_core = "" - "NAME=\"Ubuntu Core\"\n" "VERSION=\"16\"\n" "ID=ubuntu-core\n"; +static const char *os_release_core18 = "" + "NAME=\"Ubuntu Core\"\n" "VERSION_ID=\"18\"\n" "ID=ubuntu-core\n"; + +static const char *meta_snap_yaml_core18 = "" + "name: core18\n" "type: base\n" "architectures: [amd64]\n"; -static void test_is_on_core(void) +static void test_is_on_core_on18(void) { - g_file_set_contents("os-release.core", os_release_core, - strlen(os_release_core), NULL); - os_release = "os-release.core"; - g_assert_false(is_running_on_classic_distribution()); - unlink("os-release.core"); + mock_os_release(os_release_core18); + mock_meta_snap_yaml(meta_snap_yaml_core18); + g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CORE_OTHER); } -const char *os_release_classic_with_long_line = "" +const char *os_release_core20 = "" + "NAME=\"Ubuntu Core\"\n" "VERSION_ID=\"20\"\n" "ID=ubuntu-core\n"; + +static const char *meta_snap_yaml_core20 = "" + "name: core20\n" "type: base\n" "architectures: [amd64]\n"; + +static void test_is_on_core_on20(void) +{ + mock_os_release(os_release_core20); + mock_meta_snap_yaml(meta_snap_yaml_core20); + g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CORE_OTHER); +} + +static const char *os_release_classic_with_long_line = "" "NAME=\"Ubuntu\"\n" "VERSION=\"17.04 (Zesty Zapus)\"\n" "ID=ubuntu\n" @@ -54,12 +126,66 @@ static void test_is_on_classic_with_long_line(void) { - g_file_set_contents("os-release.classic-with-long-line", - os_release_classic, strlen(os_release_classic), - NULL); - os_release = "os-release.classic-with-long-line"; - g_assert_true(is_running_on_classic_distribution()); - unlink("os-release.classic-with-long-line"); + mock_os_release(os_release_classic_with_long_line); + mock_meta_snap_yaml(NULL); + g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CLASSIC); +} + +static const char *os_release_fedora_base = "" + "NAME=Fedora\nID=fedora\nVARIANT_ID=snappy\n"; + +static const char *meta_snap_yaml_fedora_base = "" + "name: fedora29\n" "type: base\n" "architectures: [amd64]\n"; + +static void test_is_on_fedora_base(void) +{ + mock_os_release(os_release_fedora_base); + mock_meta_snap_yaml(meta_snap_yaml_fedora_base); + g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CORE_OTHER); +} + +static const char *os_release_fedora_ws = "" + "NAME=Fedora\nID=fedora\nVARIANT_ID=workstation\n"; + +static void test_is_on_fedora_ws(void) +{ + mock_os_release(os_release_fedora_ws); + mock_meta_snap_yaml(NULL); + g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CLASSIC); +} + +static const char *os_release_custom = "" + "NAME=\"Custom Distribution\"\nID=custom\n"; + +static const char *meta_snap_yaml_custom = "" + "name: custom\n" + "version: rolling\n" + "summary: Runtime environment based on Custom Distribution\n" + "type: base\n" "architectures: [amd64]\n"; + +static void test_is_on_custom_base(void) +{ + mock_os_release(os_release_custom); + + /* Without /meta/snap.yaml we treat "Custom Distribution" as classic. */ + mock_meta_snap_yaml(NULL); + g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CLASSIC); + + /* With /meta/snap.yaml we treat it as core instead. */ + mock_meta_snap_yaml(meta_snap_yaml_custom); + g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CORE_OTHER); +} + +static void test_should_use_normal_mode(void) +{ + g_assert_false(sc_should_use_normal_mode(SC_DISTRO_CORE16, "core")); + g_assert_true(sc_should_use_normal_mode(SC_DISTRO_CORE_OTHER, "core")); + g_assert_true(sc_should_use_normal_mode(SC_DISTRO_CLASSIC, "core")); + + g_assert_true(sc_should_use_normal_mode(SC_DISTRO_CORE16, "core18")); + g_assert_true(sc_should_use_normal_mode + (SC_DISTRO_CORE_OTHER, "core18")); + g_assert_true(sc_should_use_normal_mode(SC_DISTRO_CLASSIC, "core18")); } static void __attribute__ ((constructor)) init(void) @@ -67,5 +193,12 @@ g_test_add_func("/classic/on-classic", test_is_on_classic); g_test_add_func("/classic/on-classic-with-long-line", test_is_on_classic_with_long_line); - g_test_add_func("/classic/on-core", test_is_on_core); + g_test_add_func("/classic/on-core-on16", test_is_on_core_on16); + g_test_add_func("/classic/on-core-on18", test_is_on_core_on18); + g_test_add_func("/classic/on-core-on20", test_is_on_core_on20); + g_test_add_func("/classic/on-fedora-base", test_is_on_fedora_base); + g_test_add_func("/classic/on-fedora-ws", test_is_on_fedora_ws); + g_test_add_func("/classic/on-custom-base", test_is_on_custom_base); + g_test_add_func("/classic/should-use-normal-mode", + test_should_use_normal_mode); } diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/feature.c snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/feature.c --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/feature.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/feature.c 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#define _GNU_SOURCE + +#include "feature.h" + +#include +#include +#include +#include +#include + +#include "cleanup-funcs.h" +#include "utils.h" + +static const char *feature_flag_dir = "/var/lib/snapd/features"; + +bool sc_feature_enabled(sc_feature_flag flag) +{ + const char *file_name; + switch (flag) { + case SC_PER_USER_MOUNT_NAMESPACE: + file_name = "per-user-mount-namespace"; + break; + default: + die("unknown feature flag code %d", flag); + } + + int dirfd SC_CLEANUP(sc_cleanup_close) = -1; + dirfd = open(feature_flag_dir, O_CLOEXEC | O_DIRECTORY | O_NOFOLLOW | O_PATH); + if (dirfd < 0 && errno == ENOENT) { + return false; + } + if (dirfd < 0) { + die("cannot open path %s", feature_flag_dir); + } + + struct stat file_info; + if (fstatat(dirfd, file_name, &file_info, AT_SYMLINK_NOFOLLOW) < 0) { + if (errno == ENOENT) { + return false; + } + die("cannot inspect file %s/%s", feature_flag_dir, file_name); + } + + return S_ISREG(file_info.st_mode); +} diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/feature.h snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/feature.h --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/feature.h 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/feature.h 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef SNAP_CONFINE_FEATURE_H +#define SNAP_CONFINE_FEATURE_H + +#include + +typedef enum sc_feature_flag { + SC_PER_USER_MOUNT_NAMESPACE, +} sc_feature_flag; + +/** + * sc_feature_enabled returns true if a given feature flag has been activated + * by the user via "snap set core experimental.xxx=true". This is determined by + * testing the presence of a file in /var/lib/snapd/features/ that is named + * after the flag name. +**/ +bool sc_feature_enabled(sc_feature_flag flag); + +#endif diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/feature-test.c snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/feature-test.c --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/feature-test.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/feature-test.c 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "feature.h" +#include "feature.c" + +#include + +#include + +#include "string-utils.h" +#include "test-utils.h" + +static char *sc_testdir(void) +{ + char *d = g_dir_make_tmp(NULL, NULL); + g_assert_nonnull(d); + g_test_queue_free(d); + g_test_queue_destroy((GDestroyNotify) rm_rf_tmp, d); + return d; +} + +// Set the feature flag directory to given value, useful for cleanup handlers. +static void set_feature_flag_dir(const char *dir) +{ + feature_flag_dir = dir; +} + +// Mock the location of the feature flag directory. +static void sc_mock_feature_flag_dir(const char *d) +{ + g_test_queue_destroy((GDestroyNotify) set_feature_flag_dir, + (void *)feature_flag_dir); + set_feature_flag_dir(d); +} + +static void test_feature_enabled__missing_dir(void) +{ + const char *d = sc_testdir(); + char subd[PATH_MAX]; + sc_must_snprintf(subd, sizeof subd, "%s/absent", d); + sc_mock_feature_flag_dir(subd); + g_assert(!sc_feature_enabled(SC_PER_USER_MOUNT_NAMESPACE)); +} + +static void test_feature_enabled__missing_file(void) +{ + const char *d = sc_testdir(); + sc_mock_feature_flag_dir(d); + g_assert(!sc_feature_enabled(SC_PER_USER_MOUNT_NAMESPACE)); +} + +static void test_feature_enabled__present_file(void) +{ + const char *d = sc_testdir(); + sc_mock_feature_flag_dir(d); + char pname[PATH_MAX]; + sc_must_snprintf(pname, sizeof pname, "%s/per-user-mount-namespace", d); + g_file_set_contents(pname, "", -1, NULL); + + g_assert(sc_feature_enabled(SC_PER_USER_MOUNT_NAMESPACE)); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/feature/missing_dir", + test_feature_enabled__missing_dir); + g_test_add_func("/feature/missing_file", + test_feature_enabled__missing_file); + g_test_add_func("/feature/present_file", + test_feature_enabled__present_file); +} diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/locking.c snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/locking.c --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/locking.c 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/locking.c 2019-01-16 08:36:51.000000000 +0000 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 Canonical Ltd + * Copyright (C) 2017-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -21,6 +21,7 @@ #include "locking.h" +#include #include #include #include @@ -84,7 +85,7 @@ static const char *sc_lock_dir = SC_LOCK_DIR; -int sc_lock(const char *scope) +static int get_lock_directory(void) { // Create (if required) and open the lock directory. debug("creating lock directory %s (if missing)", sc_lock_dir); @@ -92,54 +93,107 @@ die("cannot create lock directory %s", sc_lock_dir); } debug("opening lock directory %s", sc_lock_dir); - int dir_fd SC_CLEANUP(sc_cleanup_close) = -1; - dir_fd = + int dir_fd = open(sc_lock_dir, O_DIRECTORY | O_PATH | O_CLOEXEC | O_NOFOLLOW); if (dir_fd < 0) { die("cannot open lock directory"); } - // Construct the name of the lock file. + return dir_fd; +} + +static void get_lock_name(char *lock_fname, size_t size, const char *scope, + uid_t uid) +{ + if (uid == 0) { + // The root user doesn't have a per-user mount namespace. + // Doing so would be confusing for services which use $SNAP_DATA + // as home, and not in $SNAP_USER_DATA. + sc_must_snprintf(lock_fname, size, "%s.lock", scope ? : ""); + } else { + sc_must_snprintf(lock_fname, size, "%s.%d.lock", + scope ? : "", uid); + } +} + +static int open_lock(const char *scope, uid_t uid) +{ + int dir_fd SC_CLEANUP(sc_cleanup_close) = -1; char lock_fname[PATH_MAX] = { 0 }; - sc_must_snprintf(lock_fname, sizeof lock_fname, "%s/%s.lock", - sc_lock_dir, scope ? : ""); + int lock_fd; + + dir_fd = get_lock_directory(); + get_lock_name(lock_fname, sizeof lock_fname, scope, uid); // Open the lock file and acquire an exclusive lock. - debug("opening lock file: %s", lock_fname); - int lock_fd = openat(dir_fd, lock_fname, - O_CREAT | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600); + debug("opening lock file: %s/%s", sc_lock_dir, lock_fname); + lock_fd = openat(dir_fd, lock_fname, + O_CREAT | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600); if (lock_fd < 0) { - die("cannot open lock file: %s", lock_fname); + die("cannot open lock file: %s/%s", sc_lock_dir, lock_fname); } + return lock_fd; +} +static int sc_lock_generic(const char *scope, uid_t uid) +{ + int lock_fd = open_lock(scope, uid); sc_enable_sanity_timeout(); - debug("acquiring exclusive lock (scope %s)", scope ? : "(global)"); + debug("acquiring exclusive lock (scope %s, uid %d)", + scope ? : "(global)", uid); if (flock(lock_fd, LOCK_EX) < 0) { sc_disable_sanity_timeout(); close(lock_fd); - die("cannot acquire exclusive lock (scope %s)", - scope ? : "(global)"); + die("cannot acquire exclusive lock (scope %s, uid %d)", + scope ? : "(global)", uid); } else { sc_disable_sanity_timeout(); } return lock_fd; } -void sc_unlock(const char *scope, int lock_fd) +int sc_lock_global(void) { - // Release the lock and finish. - debug("releasing lock (scope: %s)", scope ? : "(global)"); - if (flock(lock_fd, LOCK_UN) < 0) { - die("cannot release lock (scope: %s)", scope ? : "(global)"); + return sc_lock_generic(NULL, 0); +} + +int sc_lock_snap(const char *snap_name) +{ + return sc_lock_generic(snap_name, 0); +} + +void sc_verify_snap_lock(const char *snap_name) +{ + int lock_fd, retval; + + lock_fd = open_lock(snap_name, 0); + debug("trying to verify whether exclusive lock over snap %s is held", + snap_name); + retval = flock(lock_fd, LOCK_EX | LOCK_NB); + if (retval == 0) { + /* We managed to grab the lock, the lock was not held! */ + flock(lock_fd, LOCK_UN); + close(lock_fd); + errno = 0; + die("unexpectedly managed to acquire exclusive lock over snap %s", snap_name); } - close(lock_fd); + if (retval < 0 && errno != EWOULDBLOCK) { + die("cannot verify exclusive lock over snap %s", snap_name); + } + /* We tried but failed to grab the lock because the file is already locked. + * Good, this is what we expected. */ } -int sc_lock_global(void) +int sc_lock_snap_user(const char *snap_name, uid_t uid) { - return sc_lock(NULL); + return sc_lock_generic(snap_name, uid); } -void sc_unlock_global(int lock_fd) +void sc_unlock(int lock_fd) { - return sc_unlock(NULL, lock_fd); + // Release the lock and finish. + debug("releasing lock %d", lock_fd); + if (flock(lock_fd, LOCK_UN) < 0) { + die("cannot release lock %d", lock_fd); + } + close(lock_fd); } diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/locking.h snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/locking.h --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/locking.h 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/locking.h 2019-01-16 08:36:51.000000000 +0000 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 Canonical Ltd + * Copyright (C) 2017-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -17,17 +17,19 @@ #ifndef SNAP_CONFINE_LOCKING_H #define SNAP_CONFINE_LOCKING_H +// Include config.h which pulls in _GNU_SOURCE which in turn allows sys/types.h +// to define O_PATH. Since locking.h is included from locking.c this is +// required to see O_PATH there. +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + /** - * Obtain a flock-based, exclusive lock. + * Obtain a flock-based, exclusive, globally scoped, lock. * - * The scope may be the name of a snap or NULL (global lock). Each subsequent - * argument is of type sc_locked_fn and gets called with the scope argument. - * The function guarantees that a filesystem lock is reliably acquired and - * released on call to sc_unlock() immediately upon process death. - * - * The actual lock is placed in "/run/snapd/ns" and is either called - * "/run/snapd/ns/.lock" if scope is NULL or - * "/run/snapd/ns/$scope.lock" otherwise. + * The actual lock is placed in "/run/snap/ns/.lock" * * If the lock cannot be acquired for three seconds (via * sc_enable_sanity_timeout) then the function fails and the process dies. @@ -35,29 +37,50 @@ * The return value needs to be passed to sc_unlock(), there is no need to * check for errors as the function will die() on any problem. **/ -int sc_lock(const char *scope); +int sc_lock_global(void); /** - * Release a flock-based lock. + * Obtain a flock-based, exclusive, snap-scoped, lock. + * + * The actual lock is placed in "/run/snapd/ns/$SNAP_NAME.lock" + * It should be acquired only when holding the global lock. * - * This function simply unlocks the lock and closes the file descriptor. + * If the lock cannot be acquired for three seconds (via + * sc_enable_sanity_timeout) then the function fails and the process dies. + * + * The return value needs to be passed to sc_unlock(), there is no need to + * check for errors as the function will die() on any problem. **/ -void sc_unlock(const char *scope, int lock_fd); +int sc_lock_snap(const char *snap_name); /** - * Obtain a flock-based, exclusive, globally scoped, lock. + * Verify that a flock-based, exclusive, snap-scoped, lock is held. * - * This function is exactly like sc_lock(NULL), that is the acquired lock is - * not specific to any snap but global. + * If the lock is not held the process dies. The details about the lock + * are exactly the same as for sc_lock_snap(). **/ -int sc_lock_global(void); +void sc_verify_snap_lock(const char *snap_name); /** - * Release a flock-based, globally scoped, lock + * Obtain a flock-based, exclusive, snap-scoped, lock. + * + * The actual lock is placed in "/run/snapd/ns/$SNAP_NAME.$UID.lock" + * It should be acquired only when holding the snap-specific lock. + * + * If the lock cannot be acquired for three seconds (via + * sc_enable_sanity_timeout) then the function fails and the process dies. + * The return value needs to be passed to sc_unlock(), there is no need to + * check for errors as the function will die() on any problem. + **/ +int sc_lock_snap_user(const char *snap_name, uid_t uid); + +/** + * Release a flock-based lock. * - * This function is exactly like sc_unlock(NULL, lock_fd). + * All kinds of locks can be unlocked the same way. This function simply + * unlocks the lock and closes the file descriptor. **/ -void sc_unlock_global(int lock_fd); +void sc_unlock(int lock_fd); /** * Enable a sanity-check timeout. diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/locking-test.c snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/locking-test.c --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/locking-test.c 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/locking-test.c 2019-01-16 08:36:51.000000000 +0000 @@ -32,6 +32,12 @@ sc_lock_dir = dir; } +// A variant of unsetenv that is compatible with GDestroyNotify +static void my_unsetenv(const char *k) +{ + unsetenv(k); +} + // Use temporary directory for locking. // // The directory is automatically reset to the real value at the end of the @@ -50,7 +56,7 @@ g_test_queue_free(lock_dir); g_assert_cmpint(setenv("SNAP_CONFINE_LOCK_DIR", lock_dir, 0), ==, 0); - g_test_queue_destroy((GDestroyNotify) unsetenv, + g_test_queue_destroy((GDestroyNotify) my_unsetenv, "SNAP_CONFINE_LOCK_DIR"); g_test_queue_destroy((GDestroyNotify) rm_rf_tmp, lock_dir); } @@ -63,10 +69,10 @@ static void test_sc_lock_unlock(void) { const char *lock_dir = sc_test_use_fake_lock_dir(); - int fd = sc_lock("foo"); + int fd = sc_lock_generic("foo", 123); // Construct the name of the lock file char *lock_file SC_CLEANUP(sc_cleanup_string) = NULL; - lock_file = g_strdup_printf("%s/foo.lock", lock_dir); + lock_file = g_strdup_printf("%s/foo.123.lock", lock_dir); // Open the lock file again to obtain a separate file descriptor. // According to flock(2) locks are associated with an open file table entry // so this descriptor will be separate and can compete for the same lock. @@ -80,12 +86,35 @@ g_assert_cmpint(err, ==, -1); g_assert_cmpint(saved_errno, ==, EWOULDBLOCK); // Unlock the lock. - sc_unlock("foo", fd); + sc_unlock(fd); // Re-attempt the locking operation. This time it should succeed. err = flock(lock_fd, LOCK_EX | LOCK_NB); g_assert_cmpint(err, ==, 0); } +// Check that holding a lock is properly detected. +static void test_sc_verify_snap_lock__locked(void) +{ + (void)sc_test_use_fake_lock_dir(); + int fd = sc_lock_snap("foo"); + sc_verify_snap_lock("foo"); + sc_unlock(fd); +} + +// Check that holding a lock is properly detected. +static void test_sc_verify_snap_lock__unlocked(void) +{ + (void)sc_test_use_fake_lock_dir(); + if (g_test_subprocess()) { + sc_verify_snap_lock("foo"); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("unexpectedly managed to acquire exclusive lock over snap foo\n"); +} + static void test_sc_enable_sanity_timeout(void) { if (g_test_subprocess()) { @@ -106,4 +135,8 @@ g_test_add_func("/locking/sc_lock_unlock", test_sc_lock_unlock); g_test_add_func("/locking/sc_enable_sanity_timeout", test_sc_enable_sanity_timeout); + g_test_add_func("/locking/sc_verify_snap_lock__locked", + test_sc_verify_snap_lock__locked); + g_test_add_func("/locking/sc_verify_snap_lock__unlocked", + test_sc_verify_snap_lock__unlocked); } diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/mount-opt.c snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/mount-opt.c --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/mount-opt.c 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/mount-opt.c 2019-01-16 08:36:51.000000000 +0000 @@ -256,9 +256,10 @@ "(disabled) use debug build to see details"; #endif -void sc_do_mount(const char *source, const char *target, - const char *fs_type, unsigned long mountflags, - const void *data) +static bool sc_do_mount_ex(const char *source, const char *target, + const char *fs_type, + unsigned long mountflags, const void *data, + bool optional) { char buf[10000] = { 0 }; const char *mount_cmd = NULL; @@ -274,9 +275,11 @@ } if (sc_faulty("mount", NULL) || mount(source, target, fs_type, mountflags, data) < 0) { - // Save errno as ensure can clobber it. int saved_errno = errno; - + if (optional && saved_errno == ENOENT) { + // The special-cased value that is allowed to fail. + return false; + } // Drop privileges so that we can compute our nice error message // without risking an attack on one of the string functions there. sc_privs_drop(); @@ -288,6 +291,21 @@ errno = saved_errno; die("cannot perform operation: %s", mount_cmd); } + return true; +} + +void sc_do_mount(const char *source, const char *target, + const char *fs_type, unsigned long mountflags, + const void *data) +{ + (void)sc_do_mount_ex(source, target, fs_type, mountflags, data, false); +} + +bool sc_do_optional_mount(const char *source, const char *target, + const char *fs_type, unsigned long mountflags, + const void *data) +{ + return sc_do_mount_ex(source, target, fs_type, mountflags, data, true); } void sc_do_umount(const char *target, int flags) diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/mount-opt.h snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/mount-opt.h --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/mount-opt.h 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/mount-opt.h 2019-01-16 08:36:51.000000000 +0000 @@ -18,6 +18,7 @@ #ifndef SNAP_CONFINE_MOUNT_OPT_H #define SNAP_CONFINE_MOUNT_OPT_H +#include #include /** @@ -68,6 +69,19 @@ const void *data); /** + * A thin wrapper around mount(2) with logging and error checks. + * + * This variant is allowed to silently fail when mount fails with ENOENT. + * That is, it can be used to perform mount operations and if either the source + * or the destination is not present, carry on as if nothing had happened. + * + * The return value indicates if the operation was successful or not. + **/ +bool sc_do_optional_mount(const char *source, const char *target, + const char *fs_type, unsigned long mountflags, + const void *data); + +/** * A thin wrapper around umount(2) with logging and error checks. **/ void sc_do_umount(const char *target, int flags); diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/mount-opt-test.c snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/mount-opt-test.c --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/mount-opt-test.c 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/mount-opt-test.c 2019-01-16 08:36:51.000000000 +0000 @@ -275,6 +275,50 @@ } } +static bool missing_mount(struct sc_fault_state *state, void *ptr) +{ + errno = ENOENT; + return true; +} + +static void test_sc_do_optional_mount_missing(void) +{ + sc_break("mount", missing_mount); + bool ok = sc_do_optional_mount("/foo", "/bar", "ext4", MS_RDONLY, NULL); + g_assert_false(ok); + sc_reset_faults(); +} + +static void test_sc_do_optional_mount_failure(gconstpointer snap_debug) +{ + if (g_test_subprocess()) { + sc_break("mount", broken_mount); + if (GPOINTER_TO_INT(snap_debug) == 1) { + g_setenv("SNAP_CONFINE_DEBUG", "1", true); + } + (void)sc_do_optional_mount("/foo", "/bar", "ext4", MS_RDONLY, + NULL); + + g_test_message("expected sc_do_mount not to return"); + sc_reset_faults(); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + if (GPOINTER_TO_INT(snap_debug) == 0) { + g_test_trap_assert_stderr + ("cannot perform operation: mount -t ext4 -o ro /foo /bar: Permission denied\n"); + } else { + /* with snap_debug the debug output hides the actual mount commands *but* + * they are still shown if there was an error + */ + g_test_trap_assert_stderr + ("DEBUG: performing operation: (disabled) use debug build to see details\n" + "cannot perform operation: mount -t ext4 -o ro /foo /bar: Permission denied\n"); + } +} + static void __attribute__ ((constructor)) init(void) { g_test_add_func("/mount/sc_mount_opt2str", test_sc_mount_opt2str); @@ -288,4 +332,12 @@ GINT_TO_POINTER(1), test_sc_do_mount); g_test_add_data_func("/mount/sc_do_umount_with_debug", GINT_TO_POINTER(1), test_sc_do_umount); + g_test_add_func("/mount/sc_do_optional_mount_missing", + test_sc_do_optional_mount_missing); + g_test_add_data_func("/mount/sc_do_optional_mount_failure", + GINT_TO_POINTER(0), + test_sc_do_optional_mount_failure); + g_test_add_data_func("/mount/sc_do_optional_mount_failure_with_debug", + GINT_TO_POINTER(1), + test_sc_do_optional_mount_failure); } diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/snap.c snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/snap.c --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/snap.c 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/snap.c 2019-01-16 08:36:51.000000000 +0000 @@ -22,6 +22,7 @@ #include #include #include +#include #include "utils.h" #include "string-utils.h" @@ -30,7 +31,7 @@ bool verify_security_tag(const char *security_tag, const char *snap_name) { const char *whitelist_re = - "^snap\\.([a-z0-9](-?[a-z0-9])*)\\.([a-zA-Z0-9](-?[a-zA-Z0-9])*|hook\\.[a-z](-?[a-z])*)$"; + "^snap\\.([a-z0-9](-?[a-z0-9])*(_[a-z0-9]{1,10})?)\\.([a-zA-Z0-9](-?[a-zA-Z0-9])*|hook\\.[a-z](-?[a-z])*)$"; regex_t re; if (regcomp(&re, whitelist_re, REG_EXTENDED) != 0) die("can not compile regex %s", whitelist_re); @@ -56,7 +57,7 @@ bool sc_is_hook_security_tag(const char *security_tag) { const char *whitelist_re = - "^snap\\.[a-z](-?[a-z0-9])*\\.(hook\\.[a-z](-?[a-z])*)$"; + "^snap\\.[a-z](-?[a-z0-9])*(_[a-z0-9]{1,10})?\\.(hook\\.[a-z](-?[a-z])*)$"; regex_t re; if (regcomp(&re, whitelist_re, REG_EXTENDED | REG_NOSUB) != 0) @@ -97,6 +98,94 @@ return 0; } +void sc_instance_name_validate(const char *instance_name, + struct sc_error **errorp) +{ + // NOTE: This function should be synchronized with the two other + // implementations: validate_instance_name and snap.ValidateInstanceName. + struct sc_error *err = NULL; + + // Ensure that name is not NULL + if (instance_name == NULL) { + err = + sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_NAME, + "snap instance name cannot be NULL"); + goto out; + } + // 40 char snap_name + '_' + 10 char instance_key + 1 extra overflow + 1 + // NULL + char s[53] = { 0 }; + strncpy(s, instance_name, sizeof(s) - 1); + + char *t = s; + const char *snap_name = strsep(&t, "_"); + const char *instance_key = strsep(&t, "_"); + const char *third_separator = strsep(&t, "_"); + if (third_separator != NULL) { + err = + sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_NAME, + "snap instance name can contain only one underscore"); + goto out; + } + + sc_snap_name_validate(snap_name, &err); + if (err != NULL) { + goto out; + } + // When the instance_name is a normal snap name, instance_key will be + // NULL, so only validate instance_key when we found one. + if (instance_key != NULL) { + sc_instance_key_validate(instance_key, &err); + } + + out: + sc_error_forward(errorp, err); +} + +void sc_instance_key_validate(const char *instance_key, + struct sc_error **errorp) +{ + // NOTE: see snap.ValidateInstanceName for reference of a valid instance key + // format + struct sc_error *err = NULL; + + // Ensure that name is not NULL + if (instance_key == NULL) { + err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME, + "instance key cannot be NULL"); + goto out; + } + // This is a regexp-free routine hand-coding the following pattern: + // + // "^[a-z]{1,10}$" + // + // The only motivation for not using regular expressions is so that we don't + // run untrusted input against a potentially complex regular expression + // engine. + int i = 0; + for (i = 0; instance_key[i] != '\0'; i++) { + if (islower(instance_key[i]) || isdigit(instance_key[i])) { + continue; + } + err = + sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_KEY, + "instance key must use lower case letters or digits"); + goto out; + } + + if (i == 0) { + err = + sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_KEY, + "instance key must contain at least one letter or digit"); + } else if (i > 10) { + err = + sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_KEY, + "instance key must be shorter than 10 characters"); + } + out: + sc_error_forward(errorp, err); +} + void sc_snap_name_validate(const char *snap_name, struct sc_error **errorp) { // NOTE: This function should be synchronized with the two other @@ -123,15 +212,19 @@ goto out; } bool got_letter = false; + int n = 0, m; for (; *p != '\0';) { - if (skip_lowercase_letters(&p) > 0) { + if ((m = skip_lowercase_letters(&p)) > 0) { + n += m; got_letter = true; continue; } - if (skip_digits(&p) > 0) { + if ((m = skip_digits(&p)) > 0) { + n += m; continue; } if (skip_one_char(&p, '-') > 0) { + n++; if (*p == '\0') { err = sc_error_init(SC_SNAP_DOMAIN, @@ -155,8 +248,67 @@ if (!got_letter) { err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME, "snap name must contain at least one letter"); + goto out; + } + if (n < 2) { + err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME, + "snap name must be longer than 1 character"); + goto out; + } + if (n > 40) { + err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME, + "snap name must be shorter than 40 characters"); + goto out; } out: sc_error_forward(errorp, err); } + +void sc_snap_drop_instance_key(const char *instance_name, char *snap_name, + size_t snap_name_size) +{ + sc_snap_split_instance_name(instance_name, snap_name, snap_name_size, + NULL, 0); +} + +void sc_snap_split_instance_name(const char *instance_name, char *snap_name, + size_t snap_name_size, char *instance_key, + size_t instance_key_size) +{ + if (instance_name == NULL) { + die("internal error: cannot split instance name when it is unset"); + } + if (snap_name == NULL && instance_key == NULL) { + die("internal error: cannot split instance name when both snap name and instance key are unset"); + } + + const char *pos = strchr(instance_name, '_'); + const char *instance_key_start = ""; + size_t snap_name_len = 0; + size_t instance_key_len = 0; + if (pos == NULL) { + snap_name_len = strlen(instance_name); + } else { + snap_name_len = pos - instance_name; + instance_key_start = pos + 1; + instance_key_len = strlen(instance_key_start); + } + + if (snap_name != NULL) { + if (snap_name_len >= snap_name_size) { + die("snap name buffer too small"); + } + + memcpy(snap_name, instance_name, snap_name_len); + snap_name[snap_name_len] = '\0'; + } + + if (instance_key != NULL) { + if (instance_key_len >= instance_key_size) { + die("instance key buffer too small"); + } + memcpy(instance_key, instance_key_start, instance_key_len); + instance_key[instance_key_len] = '\0'; + } +} diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/snap.h snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/snap.h --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/snap.h 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/snap.h 2019-01-16 08:36:51.000000000 +0000 @@ -19,6 +19,7 @@ #define SNAP_CONFINE_SNAP_H #include +#include #include "error.h" @@ -30,6 +31,10 @@ enum { /** The name of the snap is not valid. */ SC_SNAP_INVALID_NAME = 1, + /** The instance key of the snap is not valid. */ + SC_SNAP_INVALID_INSTANCE_KEY = 2, + /** The instance of the snap is not valid. */ + SC_SNAP_INVALID_INSTANCE_NAME = 3, }; /** @@ -44,6 +49,31 @@ void sc_snap_name_validate(const char *snap_name, struct sc_error **errorp); /** + * Validate the given instance key. + * + * Valid instance key cannot be NULL and must match a regular expression + * describing the strict naming requirements. Please refer to snapd source code + * for details. + * + * The error protocol is observed so if the caller doesn't provide an outgoing + * error pointer the function will die on any error. + **/ +void sc_instance_key_validate(const char *instance_key, + struct sc_error **errorp); + +/** + * Validate the given snap instance name. + * + * Valid instance name must be composed of a valid snap name and a valid + * instance key. + * + * The error protocol is observed so if the caller doesn't provide an outgoing + * error pointer the function will die on any error. + **/ +void sc_instance_name_validate(const char *instance_name, + struct sc_error **errorp); + +/** * Validate security tag against strict naming requirements and snap name. * * The executable name is of form: @@ -58,4 +88,32 @@ bool sc_is_hook_security_tag(const char *security_tag); +/** + * Extract snap name out of an instance name. + * + * A snap may be installed multiple times in parallel under distinct instance names. + * This function extracts the snap name out of a name that possibly contains a snap + * instance key. + * + * For example: snap_instance => snap, just-snap => just-snap + **/ +void sc_snap_drop_instance_key(const char *instance_name, char *snap_name, + size_t snap_name_size); + +/** + * Extract snap name and instance key out of an instance name. + * + * A snap may be installed multiple times in parallel under distinct instance + * names. This function extracts the snap name and instance key out of the + * instance name. One of snap_name, instance_key must be non-NULL. + * + * For example: + * name_instance => "name" & "instance" + * just-name => "just-name" & "" + * + **/ +void sc_snap_split_instance_name(const char *instance_name, char *snap_name, + size_t snap_name_size, char *instance_key, + size_t instance_key_size); + #endif diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/snap-test.c snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/snap-test.c --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/snap-test.c 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/snap-test.c 2019-01-16 08:36:51.000000000 +0000 @@ -30,6 +30,12 @@ g_assert_true(verify_security_tag("snap.f00.bar-baz1", "f00")); g_assert_true(verify_security_tag("snap.foo.hook.bar", "foo")); g_assert_true(verify_security_tag("snap.foo.hook.bar-baz", "foo")); + g_assert_true(verify_security_tag + ("snap.foo_instance.bar-baz", "foo_instance")); + g_assert_true(verify_security_tag + ("snap.foo_instance.hook.bar-baz", "foo_instance")); + g_assert_true(verify_security_tag + ("snap.foo_bar.hook.bar-baz", "foo_bar")); // Now, test the names we know are bad g_assert_false(verify_security_tag @@ -62,31 +68,63 @@ g_assert_false(verify_security_tag("snap..name.app", ".name")); g_assert_false(verify_security_tag("snap.name..app", "name.")); g_assert_false(verify_security_tag("snap.name.app..", "name")); + // These contain invalid instance key + g_assert_false(verify_security_tag("snap.foo_.bar-baz", "foo")); + g_assert_false(verify_security_tag + ("snap.foo_toolonginstance.bar-baz", "foo")); + g_assert_false(verify_security_tag + ("snap.foo_inst@nace.bar-baz", "foo")); + g_assert_false(verify_security_tag + ("snap.foo_in-stan-ce.bar-baz", "foo")); + g_assert_false(verify_security_tag("snap.foo_in stan.bar-baz", "foo")); // Test names that are both good, but snap name doesn't match security tag g_assert_false(verify_security_tag("snap.foo.hook.bar", "fo")); g_assert_false(verify_security_tag("snap.foo.hook.bar", "fooo")); g_assert_false(verify_security_tag("snap.foo.hook.bar", "snap")); g_assert_false(verify_security_tag("snap.foo.hook.bar", "bar")); + g_assert_false(verify_security_tag("snap.foo_instance.bar", "foo_bar")); // Regression test 12to8 g_assert_true(verify_security_tag("snap.12to8.128to8", "12to8")); g_assert_true(verify_security_tag("snap.123test.123test", "123test")); g_assert_true(verify_security_tag ("snap.123test.hook.configure", "123test")); +} +static void test_sc_is_hook_security_tag(void) +{ + // First, test the names we know are good + g_assert_true(sc_is_hook_security_tag("snap.foo.hook.bar")); + g_assert_true(sc_is_hook_security_tag("snap.foo.hook.bar-baz")); + g_assert_true(sc_is_hook_security_tag + ("snap.foo_instance.hook.bar-baz")); + g_assert_true(sc_is_hook_security_tag("snap.foo_bar.hook.bar-baz")); + + // Now, test the names we know are not valid hook security tags + g_assert_false(sc_is_hook_security_tag("snap.foo_instance.bar-baz")); + g_assert_false(sc_is_hook_security_tag("snap.name.app!hook.foo")); + g_assert_false(sc_is_hook_security_tag("snap.name.app.hook!foo")); + g_assert_false(sc_is_hook_security_tag("snap.name.app.hook.-foo")); + g_assert_false(sc_is_hook_security_tag("snap.name.app.hook.f00")); } -static void test_sc_snap_name_validate(void) +static void test_sc_snap_or_instance_name_validate(gconstpointer data) { + typedef void (*validate_func_t) (const char *, struct sc_error **); + + validate_func_t validate = (validate_func_t) data; + bool is_instance = + (validate == sc_instance_name_validate) ? true : false; + struct sc_error *err = NULL; // Smoke test, a valid snap name - sc_snap_name_validate("hello-world", &err); + validate("hello-world", &err); g_assert_null(err); // Smoke test: invalid character - sc_snap_name_validate("hello world", &err); + validate("hello world", &err); g_assert_nonnull(err); g_assert_true(sc_error_match (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); @@ -95,7 +133,7 @@ sc_error_free(err); // Smoke test: no letters - sc_snap_name_validate("", &err); + validate("", &err); g_assert_nonnull(err); g_assert_true(sc_error_match (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); @@ -104,7 +142,7 @@ sc_error_free(err); // Smoke test: leading dash - sc_snap_name_validate("-foo", &err); + validate("-foo", &err); g_assert_nonnull(err); g_assert_true(sc_error_match (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); @@ -113,7 +151,7 @@ sc_error_free(err); // Smoke test: trailing dash - sc_snap_name_validate("foo-", &err); + validate("foo-", &err); g_assert_nonnull(err); g_assert_true(sc_error_match (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); @@ -122,7 +160,7 @@ sc_error_free(err); // Smoke test: double dash - sc_snap_name_validate("f--oo", &err); + validate("f--oo", &err); g_assert_nonnull(err); g_assert_true(sc_error_match (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); @@ -131,27 +169,46 @@ sc_error_free(err); // Smoke test: NULL name is not valid - sc_snap_name_validate(NULL, &err); + validate(NULL, &err); g_assert_nonnull(err); - g_assert_true(sc_error_match - (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); - g_assert_cmpstr(sc_error_msg(err), ==, "snap name cannot be NULL"); + // the only case when instance name validation diverges from snap name + // validation + if (!is_instance) { + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name cannot be NULL"); + } else { + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, + SC_SNAP_INVALID_INSTANCE_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap instance name cannot be NULL"); + } sc_error_free(err); const char *valid_names[] = { - "a", "aa", "aaa", "aaaa", + "aa", "aaa", "aaaa", "a-a", "aa-a", "a-aa", "a-b-c", "a0", "a-0", "a-0a", "01game", "1-or-2" }; for (size_t i = 0; i < sizeof valid_names / sizeof *valid_names; ++i) { g_test_message("checking valid snap name: %s", valid_names[i]); - sc_snap_name_validate(valid_names[i], &err); + validate(valid_names[i], &err); g_assert_null(err); } const char *invalid_names[] = { // name cannot be empty "", + // too short + "a", + // names cannot be too long + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx", + "1111111111111111111111111111111111111111x", + "x1111111111111111111111111111111111111111", + "x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x", // dashes alone are not a name "-", "--", // double dashes in a name are not allowed @@ -161,7 +218,7 @@ // name cannot have any spaces in it "a ", " a", "a a", // a number alone is not a name - "0", "123", + "0", "123", "1-2-3", // identifier must be plain ASCII "日本語", "한글", "ру́сский язы́к", }; @@ -169,18 +226,29 @@ ++i) { g_test_message("checking invalid snap name: >%s<", invalid_names[i]); - sc_snap_name_validate(invalid_names[i], &err); + validate(invalid_names[i], &err); g_assert_nonnull(err); g_assert_true(sc_error_match (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); sc_error_free(err); } // Regression test: 12to8 and 123test - sc_snap_name_validate("12to8", &err); + validate("12to8", &err); g_assert_null(err); - sc_snap_name_validate("123test", &err); + validate("123test", &err); g_assert_null(err); + // In case we switch to a regex, here's a test that could break things. + const char good_bad_name[] = "u-94903713687486543234157734673284536758"; + char varname[sizeof good_bad_name] = { 0 }; + for (size_t i = 3; i <= sizeof varname - 1; i++) { + g_assert_nonnull(memcpy(varname, good_bad_name, i)); + varname[i] = 0; + g_test_message("checking valid snap name: >%s<", varname); + validate(varname, &err); + g_assert_null(err); + sc_error_free(err); + } } static void test_sc_snap_name_validate__respects_error_protocol(void) @@ -197,11 +265,304 @@ ("snap name must use lower case letters, digits or dashes\n"); } +static void test_sc_instance_name_validate(void) +{ + struct sc_error *err = NULL; + + sc_instance_name_validate("hello-world", &err); + g_assert_null(err); + sc_instance_name_validate("hello-world_foo", &err); + g_assert_null(err); + + // just the separator + sc_instance_name_validate("_", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name must contain at least one letter"); + sc_error_free(err); + + // just name, with separator, missing instance key + sc_instance_name_validate("hello-world_", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_KEY)); + g_assert_cmpstr(sc_error_msg(err), ==, + "instance key must contain at least one letter or digit"); + sc_error_free(err); + + // only separator and instance key, missing name + sc_instance_name_validate("_bar", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name must contain at least one letter"); + sc_error_free(err); + + sc_instance_name_validate("", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name must contain at least one letter"); + sc_error_free(err); + + // third separator + sc_instance_name_validate("foo_bar_baz", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap instance name can contain only one underscore"); + sc_error_free(err); + + const char *valid_names[] = { + "aa", "aaa", "aaaa", + "aa_a", "aa_1", "aa_123", "aa_0123456789", + }; + for (size_t i = 0; i < sizeof valid_names / sizeof *valid_names; ++i) { + g_test_message("checking valid instance name: %s", + valid_names[i]); + sc_instance_name_validate(valid_names[i], &err); + g_assert_null(err); + } + const char *invalid_names[] = { + // too short + "a", + // only letters and digits in the instance key + "a_--23))", "a_ ", "a_091234#", "a_123_456", + // up to 10 characters for the instance key + "a_01234567891", "a_0123456789123", + // snap name must not be more than 40 characters, regardless of instance + // key + "01234567890123456789012345678901234567890_foobar", + "01234567890123456789-01234567890123456789_foobar", + // instance key must be plain ASCII + "foobar_日本語", + // way too many underscores + "foobar_baz_zed_daz", + "foobar______", + }; + for (size_t i = 0; i < sizeof invalid_names / sizeof *invalid_names; + ++i) { + g_test_message("checking invalid instance name: >%s<", + invalid_names[i]); + sc_instance_name_validate(invalid_names[i], &err); + g_assert_nonnull(err); + sc_error_free(err); + } +} + +static void test_sc_snap_drop_instance_key_no_dest(void) +{ + if (g_test_subprocess()) { + sc_snap_drop_instance_key("foo_bar", NULL, 0); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + +} + +static void test_sc_snap_drop_instance_key_short_dest(void) +{ + if (g_test_subprocess()) { + char dest[10] = { 0 }; + sc_snap_drop_instance_key("foo-foo-foo-foo-foo_bar", dest, + sizeof dest); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); +} + +static void test_sc_snap_drop_instance_key_short_dest2(void) +{ + if (g_test_subprocess()) { + char dest[3] = { 0 }; // "foo" sans the nil byte + sc_snap_drop_instance_key("foo", dest, sizeof dest); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); +} + +static void test_sc_snap_drop_instance_key_no_name(void) +{ + if (g_test_subprocess()) { + char dest[10] = { 0 }; + sc_snap_drop_instance_key(NULL, dest, sizeof dest); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); +} + +static void test_sc_snap_drop_instance_key_basic(void) +{ + char name[41] = { 0xff }; + + sc_snap_drop_instance_key("foo_bar", name, sizeof name); + g_assert_cmpstr(name, ==, "foo"); + + memset(name, 0xff, sizeof name); + sc_snap_drop_instance_key("foo-bar_bar", name, sizeof name); + g_assert_cmpstr(name, ==, "foo-bar"); + + memset(name, 0xff, sizeof name); + sc_snap_drop_instance_key("foo-bar", name, sizeof name); + g_assert_cmpstr(name, ==, "foo-bar"); + + memset(name, 0xff, sizeof name); + sc_snap_drop_instance_key("_baz", name, sizeof name); + g_assert_cmpstr(name, ==, ""); + + memset(name, 0xff, sizeof name); + sc_snap_drop_instance_key("foo", name, sizeof name); + g_assert_cmpstr(name, ==, "foo"); +} + +static void test_sc_snap_split_instance_name_trailing_nil(void) +{ + if (g_test_subprocess()) { + char dest[3] = { 0 }; + // pretend there is no place for trailing \0 + sc_snap_split_instance_name("_", NULL, 0, dest, 0); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); +} + +static void test_sc_snap_split_instance_name_short_instance_dest(void) +{ + if (g_test_subprocess()) { + char dest[10] = { 0 }; + sc_snap_split_instance_name("foo_barbarbarbar", NULL, 0, + dest, sizeof dest); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); +} + +static void test_sc_snap_split_instance_name_basic(void) +{ + char name[41] = { 0xff }; + char instance[20] = { 0xff }; + + sc_snap_split_instance_name("foo_bar", name, sizeof name, instance, + sizeof instance); + g_assert_cmpstr(name, ==, "foo"); + g_assert_cmpstr(instance, ==, "bar"); + + memset(name, 0xff, sizeof name); + memset(instance, 0xff, sizeof instance); + sc_snap_split_instance_name("foo-bar_bar", name, sizeof name, instance, + sizeof instance); + g_assert_cmpstr(name, ==, "foo-bar"); + g_assert_cmpstr(instance, ==, "bar"); + + memset(name, 0xff, sizeof name); + memset(instance, 0xff, sizeof instance); + sc_snap_split_instance_name("foo-bar", name, sizeof name, instance, + sizeof instance); + g_assert_cmpstr(name, ==, "foo-bar"); + g_assert_cmpstr(instance, ==, ""); + + memset(name, 0xff, sizeof name); + memset(instance, 0xff, sizeof instance); + sc_snap_split_instance_name("_baz", name, sizeof name, instance, + sizeof instance); + g_assert_cmpstr(name, ==, ""); + g_assert_cmpstr(instance, ==, "baz"); + + memset(name, 0xff, sizeof name); + memset(instance, 0xff, sizeof instance); + sc_snap_split_instance_name("foo", name, sizeof name, instance, + sizeof instance); + g_assert_cmpstr(name, ==, "foo"); + g_assert_cmpstr(instance, ==, ""); + + memset(name, 0xff, sizeof name); + sc_snap_split_instance_name("foo_bar", name, sizeof name, NULL, 0); + g_assert_cmpstr(name, ==, "foo"); + + memset(instance, 0xff, sizeof instance); + sc_snap_split_instance_name("foo_bar", NULL, 0, instance, + sizeof instance); + g_assert_cmpstr(instance, ==, "bar"); + + memset(name, 0xff, sizeof name); + memset(instance, 0xff, sizeof instance); + sc_snap_split_instance_name("hello_world_surprise", name, sizeof name, + instance, sizeof instance); + g_assert_cmpstr(name, ==, "hello"); + g_assert_cmpstr(instance, ==, "world_surprise"); + + memset(name, 0xff, sizeof name); + memset(instance, 0xff, sizeof instance); + sc_snap_split_instance_name("", name, sizeof name, instance, + sizeof instance); + g_assert_cmpstr(name, ==, ""); + g_assert_cmpstr(instance, ==, ""); + + memset(name, 0xff, sizeof name); + memset(instance, 0xff, sizeof instance); + sc_snap_split_instance_name("_", name, sizeof name, instance, + sizeof instance); + g_assert_cmpstr(name, ==, ""); + g_assert_cmpstr(instance, ==, ""); + + memset(name, 0xff, sizeof name); + memset(instance, 0xff, sizeof instance); + sc_snap_split_instance_name("foo_", name, sizeof name, instance, + sizeof instance); + g_assert_cmpstr(name, ==, "foo"); + g_assert_cmpstr(instance, ==, ""); +} + static void __attribute__ ((constructor)) init(void) { g_test_add_func("/snap/verify_security_tag", test_verify_security_tag); - g_test_add_func("/snap/sc_snap_name_validate", - test_sc_snap_name_validate); + g_test_add_func("/snap/sc_is_hook_security_tag", + test_sc_is_hook_security_tag); + + g_test_add_data_func("/snap/sc_snap_name_validate", + sc_snap_name_validate, + test_sc_snap_or_instance_name_validate); g_test_add_func("/snap/sc_snap_name_validate/respects_error_protocol", test_sc_snap_name_validate__respects_error_protocol); + + g_test_add_data_func("/snap/sc_instance_name_validate/just_name", + sc_instance_name_validate, + test_sc_snap_or_instance_name_validate); + g_test_add_func("/snap/sc_instance_name_validate/full", + test_sc_instance_name_validate); + + g_test_add_func("/snap/sc_snap_drop_instance_key/basic", + test_sc_snap_drop_instance_key_basic); + g_test_add_func("/snap/sc_snap_drop_instance_key/no_dest", + test_sc_snap_drop_instance_key_no_dest); + g_test_add_func("/snap/sc_snap_drop_instance_key/no_name", + test_sc_snap_drop_instance_key_no_name); + g_test_add_func("/snap/sc_snap_drop_instance_key/short_dest", + test_sc_snap_drop_instance_key_short_dest); + g_test_add_func("/snap/sc_snap_drop_instance_key/short_dest2", + test_sc_snap_drop_instance_key_short_dest2); + + g_test_add_func("/snap/sc_snap_split_instance_name/basic", + test_sc_snap_split_instance_name_basic); + g_test_add_func("/snap/sc_snap_split_instance_name/trailing_nil", + test_sc_snap_split_instance_name_trailing_nil); + g_test_add_func("/snap/sc_snap_split_instance_name/short_instance_dest", + test_sc_snap_split_instance_name_short_instance_dest); } diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/string-utils.c snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/string-utils.c --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/string-utils.c 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/string-utils.c 2019-01-16 08:36:51.000000000 +0000 @@ -56,6 +56,22 @@ return strncmp(str - xlen + slen, suffix, xlen) == 0; } +char *sc_strdup(const char *str) +{ + size_t len; + char *copy; + if (str == NULL) { + die("cannot duplicate NULL string"); + } + len = strlen(str); + copy = malloc(len + 1); + if (copy == NULL) { + die("cannot allocate string copy (len: %zd)", len); + } + memcpy(copy, str, len + 1); + return copy; +} + int sc_must_snprintf(char *str, size_t size, const char *format, ...) { int n; diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/string-utils.h snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/string-utils.h --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/string-utils.h 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/string-utils.h 2019-01-16 08:36:51.000000000 +0000 @@ -32,6 +32,11 @@ bool sc_endswith(const char *str, const char *suffix); /** + * Allocate and return a copy of a string. +**/ +char *sc_strdup(const char *str); + +/** * Safer version of snprintf. * * This version dies on any error condition. diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/string-utils-test.c snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/string-utils-test.c --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/string-utils-test.c 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/string-utils-test.c 2019-01-16 08:36:51.000000000 +0000 @@ -780,6 +780,14 @@ #undef DQ } +static void test_sc_strdup(void) +{ + char *s = sc_strdup("snap install everything"); + g_assert_nonnull(s); + g_assert_cmpstr(s, ==, "snap install everything"); + free(s); +} + static void __attribute__ ((constructor)) init(void) { g_test_add_func("/string-utils/sc_streq", test_sc_streq); @@ -836,4 +844,5 @@ ("/string-utils/sc_string_append_char_pair__uninitialized_buf", test_sc_string_append_char_pair__uninitialized_buf); g_test_add_func("/string-utils/sc_string_quote", test_sc_string_quote); + g_test_add_func("/string-utils/sc_strdup", test_sc_strdup); } diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/tool.c snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/tool.c --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/tool.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/tool.c 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "tool.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "../libsnap-confine-private/apparmor-support.h" +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/utils.h" + +/** + * sc_open_snapd_tool returns a file descriptor of the given internal executable. + * + * The executable is located based on the location of the currently executing process. + * The returning file descriptor can be used with fexecve function, like in sc_call_snapd_tool. +**/ +static int sc_open_snapd_tool(const char *tool_name); + +/** + * sc_call_snapd_tool calls a snapd tool by file descriptor. + * + * The idea with calling with an open file descriptor is to allow calling executables + * across mount namespaces, where the executable may not be visible in the new filesystem + * anymore. The caller establishes an open file descriptor in one namespace and later on + * performs the call in another mount namespace. + * + * The environment vector has special support for expanding the string "SNAPD_DEBUG=x". + * If such string is present, the "x" is replaced with either "0" or "1" depending on + * the result of is_sc_debug_enabled(). + **/ +static void sc_call_snapd_tool(int tool_fd, const char *tool_name, char **argv, + char **envp); + +/** + * sc_call_snapd_tool_with_apparmor calls a snapd tool by file descriptor, + * possibly confining the program with a specific apparmor profile. +**/ +static void sc_call_snapd_tool_with_apparmor(int tool_fd, const char *tool_name, + struct sc_apparmor *apparmor, + const char *aa_profile, + char **argv, char **envp); + +int sc_open_snap_update_ns(void) +{ + return sc_open_snapd_tool("snap-update-ns"); +} + +void sc_call_snap_update_ns(int snap_update_ns_fd, const char *snap_name, + struct sc_apparmor *apparmor) +{ + char *snap_name_copy SC_CLEANUP(sc_cleanup_string) = NULL; + snap_name_copy = sc_strdup(snap_name); + + char aa_profile[PATH_MAX] = { 0 }; + sc_must_snprintf(aa_profile, sizeof aa_profile, "snap-update-ns.%s", + snap_name); + + char *argv[] = { + "snap-update-ns", + /* This tells snap-update-ns we are calling from snap-confine and locking is in place */ + "--from-snap-confine", + snap_name_copy, NULL + }; + char *envp[] = { "SNAPD_DEBUG=x", NULL }; + sc_call_snapd_tool_with_apparmor(snap_update_ns_fd, + "snap-update-ns", apparmor, + aa_profile, argv, envp); +} + +void sc_call_snap_update_ns_as_user(int snap_update_ns_fd, + const char *snap_name, + struct sc_apparmor *apparmor) +{ + char *snap_name_copy SC_CLEANUP(sc_cleanup_string) = NULL; + snap_name_copy = sc_strdup(snap_name); + + char aa_profile[PATH_MAX] = { 0 }; + sc_must_snprintf(aa_profile, sizeof aa_profile, "snap-update-ns.%s", + snap_name); + + const char *xdg_runtime_dir = getenv("XDG_RUNTIME_DIR"); + char xdg_runtime_dir_env[PATH_MAX+strlen("XDG_RUNTIME_DIR=")]; + if (xdg_runtime_dir != NULL) { + sc_must_snprintf(xdg_runtime_dir_env, + sizeof(xdg_runtime_dir_env), + "XDG_RUNTIME_DIR=%s", xdg_runtime_dir); + } + + char *argv[] = { + "snap-update-ns", + /* This tells snap-update-ns we are calling from snap-confine and locking is in place */ + /* TODO: enable this in sync with snap-update-ns changes, "--from-snap-confine", */ + /* This tells snap-update-ns that we want to process the per-user profile */ + "--user-mounts", snap_name_copy, NULL + }; + char *envp[] = { + /* SNAPD_DEBUG=x is replaced by sc_call_snapd_tool_with_apparmor + * with either SNAPD_DEBUG=0 or SNAPD_DEBUG=1, see that function + * for details. */ + "SNAPD_DEBUG=x", + xdg_runtime_dir_env, NULL + }; + sc_call_snapd_tool_with_apparmor(snap_update_ns_fd, + "snap-update-ns", apparmor, + aa_profile, argv, envp); +} + +int sc_open_snap_discard_ns(void) +{ + return sc_open_snapd_tool("snap-discard-ns"); +} + +void sc_call_snap_discard_ns(int snap_discard_ns_fd, const char *snap_name) +{ + char *snap_name_copy SC_CLEANUP(sc_cleanup_string) = NULL; + snap_name_copy = sc_strdup(snap_name); + char *argv[] = + { "snap-discard-ns", "--from-snap-confine", snap_name_copy, NULL }; + /* SNAPD_DEBUG=x is replaced by sc_call_snapd_tool_with_apparmor with + * either SNAPD_DEBUG=0 or SNAPD_DEBUG=1, see that function for details. */ + char *envp[] = { "SNAPD_DEBUG=x", NULL }; + sc_call_snapd_tool(snap_discard_ns_fd, "snap-discard-ns", argv, envp); +} + +static int sc_open_snapd_tool(const char *tool_name) +{ + // +1 is for the case where the link is exactly PATH_MAX long but we also + // want to store the terminating '\0'. The readlink system call doesn't add + // terminating null, but our initialization of buf handles this for us. + char buf[PATH_MAX + 1] = { 0 }; + if (readlink("/proc/self/exe", buf, sizeof buf) < 0) { + die("cannot readlink /proc/self/exe"); + } + if (buf[0] != '/') { // this shouldn't happen, but make sure have absolute path + die("readlink /proc/self/exe returned relative path"); + } + char *dir_name = dirname(buf); + int dir_fd SC_CLEANUP(sc_cleanup_close) = 1; + dir_fd = open(dir_name, O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (dir_fd < 0) { + die("cannot open path %s", dir_name); + } + int tool_fd = -1; + tool_fd = openat(dir_fd, tool_name, O_PATH | O_NOFOLLOW | O_CLOEXEC); + if (tool_fd < 0) { + die("cannot open path %s/%s", dir_name, tool_name); + } + debug("opened %s executable as file descriptor %d", tool_name, tool_fd); + return tool_fd; +} + +static void sc_call_snapd_tool(int tool_fd, const char *tool_name, char **argv, + char **envp) +{ + sc_call_snapd_tool_with_apparmor(tool_fd, tool_name, NULL, NULL, argv, + envp); +} + +static void sc_call_snapd_tool_with_apparmor(int tool_fd, const char *tool_name, + struct sc_apparmor *apparmor, + const char *aa_profile, + char **argv, char **envp) +{ + debug("calling snapd tool %s", tool_name); + pid_t child = fork(); + if (child < 0) { + die("cannot fork to run snapd tool %s", tool_name); + } + if (child == 0) { + /* If the caller provided template environment entry for SNAPD_DEBUG + * then expand it to the actual value. */ + for (char **env = envp; + /* Mama mia, that's a spicy meatball. */ + env != NULL && *env != NULL && **env != '\0'; env++) { + if (sc_streq(*env, "SNAPD_DEBUG=x")) { + /* NOTE: this is not released, on purpose. */ + char *entry = sc_strdup(*env); + entry[strlen("SNAPD_DEBUG=x") - 1] = + sc_is_debug_enabled()? '1' : '0'; + *env = entry; + } + } + /* Switch apparmor profile for the process after exec. */ + if (apparmor != NULL && aa_profile != NULL) { + sc_maybe_aa_change_onexec(apparmor, aa_profile); + } + fexecve(tool_fd, argv, envp); + die("cannot execute snapd tool %s", tool_name); + } else { + int status = 0; + debug("waiting for snapd tool %s to terminate", tool_name); + if (waitpid(child, &status, 0) < 0) { + die("cannot get snapd tool %s termination status via waitpid", tool_name); + } + if (WIFEXITED(status) && WEXITSTATUS(status) != 0) { + die("%s failed with code %i", tool_name, + WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) { + die("%s killed by signal %i", tool_name, + WTERMSIG(status)); + } + debug("%s finished successfully", tool_name); + } +} diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/tool.h snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/tool.h --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/tool.h 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/tool.h 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef SNAP_CONFINE_TOOL_H +#define SNAP_CONFINE_TOOL_H + +/* Forward declaration, for real see apparmor-support.h */ +struct sc_apparmor; + +/** + * sc_open_snap_update_ns returns a file descriptor for the snap-update-ns tool. +**/ +int sc_open_snap_update_ns(void); + +/** + * sc_call_snap_update_ns calls snap-update-ns from snap-confine + **/ +void sc_call_snap_update_ns(int snap_update_ns_fd, const char *snap_name, + struct sc_apparmor *apparmor); + +/** + * sc_call_snap_update_ns calls snap-update-ns --user-mounts from snap-confine + **/ +void sc_call_snap_update_ns_as_user(int snap_update_ns_fd, + const char *snap_name, + struct sc_apparmor *apparmor); + +/** + * sc_open_snap_update_ns returns a file descriptor for the snap-discard-ns tool. +**/ +int sc_open_snap_discard_ns(void); + +/** + * sc_call_snap_discard_ns calls the snap-discard-ns from snap confine. +**/ +void sc_call_snap_discard_ns(int snap_discard_ns_fd, const char *snap_name); + +#endif diff -Nru snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/utils-test.c snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/utils-test.c --- snapd-2.32.3.2~14.04/cmd/libsnap-confine-private/utils-test.c 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/libsnap-confine-private/utils-test.c 2019-01-16 08:36:51.000000000 +0000 @@ -99,6 +99,22 @@ g_test_trap_assert_stderr("death message: Operation not permitted\n"); } +// A variant of rmdir that is compatible with GDestroyNotify +static void my_rmdir(const char *path) +{ + if (rmdir(path) != 0) { + die("cannot rmdir %s", path); + } +} + +// A variant of chdir that is compatible with GDestroyNotify +static void my_chdir(const char *path) +{ + if (chdir(path) != 0) { + die("cannot change dir to %s", path); + } +} + /** * Perform the rest of testing in a ephemeral directory. * @@ -114,9 +130,9 @@ g_assert_cmpint(err, ==, 0); g_test_queue_free(temp_dir); - g_test_queue_destroy((GDestroyNotify) rmdir, temp_dir); + g_test_queue_destroy((GDestroyNotify) my_rmdir, temp_dir); g_test_queue_free(orig_dir); - g_test_queue_destroy((GDestroyNotify) chdir, orig_dir); + g_test_queue_destroy((GDestroyNotify) my_chdir, orig_dir); } /** @@ -130,7 +146,7 @@ G_FILE_TEST_IS_DIR)); // Use sc_nonfatal_mkpath to create the directory and ensure that it worked // as expected. - g_test_queue_destroy((GDestroyNotify) rmdir, (char *)dirname); + g_test_queue_destroy((GDestroyNotify) my_rmdir, (char *)dirname); int err = sc_nonfatal_mkpath(dirname, 0755); g_assert_cmpint(err, ==, 0); g_assert_cmpint(errno, ==, 0); @@ -143,7 +159,7 @@ g_assert_cmpint(errno, ==, EEXIST); // Now create a sub-directory of the original directory and observe the // results. We should no longer see errno of EEXIST! - g_test_queue_destroy((GDestroyNotify) rmdir, (char *)subdirname); + g_test_queue_destroy((GDestroyNotify) my_rmdir, (char *)subdirname); err = sc_nonfatal_mkpath(subdirname, 0755); g_assert_cmpint(err, ==, 0); g_assert_cmpint(errno, ==, 0); diff -Nru snapd-2.32.3.2~14.04/cmd/Makefile.am snapd-2.37~rc1~14.04/cmd/Makefile.am --- snapd-2.32.3.2~14.04/cmd/Makefile.am 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/Makefile.am 2019-01-16 08:36:51.000000000 +0000 @@ -15,7 +15,15 @@ CHECK_CFLAGS += -Werror endif -subdirs = snap-confine snap-discard-ns system-shutdown libsnap-confine-private snap-gdb-shim snapd-generator +subdirs = \ + libsnap-confine-private \ + snap-confine \ + snap-discard-ns \ + snap-gdb-shim \ + snap-update-ns \ + snapd-env-generator \ + snapd-generator \ + system-shutdown # Run check-syntax when checking # TODO: conver those to autotools-style tests later @@ -39,26 +47,31 @@ if WITH_UNIT_TESTS check-unit-tests: snap-confine/unit-tests system-shutdown/unit-tests libsnap-confine-private/unit-tests $(HAVE_VALGRIND) ./libsnap-confine-private/unit-tests - $(HAVE_VALGRIND) ./snap-confine/unit-tests + SNAP_DEVICE_HELPER=$(srcdir)/snap-confine/snap-device-helper $(HAVE_VALGRIND) ./snap-confine/unit-tests $(HAVE_VALGRIND) ./system-shutdown/unit-tests else check-unit-tests: echo "unit tests are disabled (rebuild with --enable-unit-tests)" endif +new_format = snap-discard-ns/snap-discard-ns.c .PHONY: fmt -fmt: $(foreach dir,$(subdirs),$(wildcard $(srcdir)/$(dir)/*.[ch])) +fmt:: $(filter $(addprefix %,$(new_format)),$(foreach dir,$(subdirs),$(wildcard $(srcdir)/$(dir)/*.[ch]))) + clang-format -style='{BasedOnStyle: Google, IndentWidth: 4, ColumnLimit: 120}' -i $^ + +fmt:: $(filter-out $(addprefix %,$(new_format)),$(foreach dir,$(subdirs),$(wildcard $(srcdir)/$(dir)/*.[ch]))) HOME=$(srcdir) indent $^ # The hack target helps devlopers work on snap-confine on their live system by # installing a fresh copy of snap confine and the appropriate apparmor profile. .PHONY: hack -hack: snap-confine/snap-confine snap-confine/snap-confine.apparmor snap-update-ns/snap-update-ns snap-seccomp/snap-seccomp - sudo install -D -m 6755 snap-confine/snap-confine $(DESTDIR)$(libexecdir)/snap-confine +hack: snap-confine/snap-confine-debug snap-confine/snap-confine.apparmor snap-update-ns/snap-update-ns snap-seccomp/snap-seccomp snap-discard-ns/snap-discard-ns + sudo install -D -m 6755 snap-confine/snap-confine-debug $(DESTDIR)$(libexecdir)/snap-confine sudo install -m 644 snap-confine/snap-confine.apparmor $(DESTDIR)/etc/apparmor.d/$(patsubst .%,%,$(subst /,.,$(libexecdir))).snap-confine.real sudo install -d -m 755 $(DESTDIR)/var/lib/snapd/apparmor/snap-confine/ sudo apparmor_parser -r snap-confine/snap-confine.apparmor sudo install -m 755 snap-update-ns/snap-update-ns $(DESTDIR)$(libexecdir)/snap-update-ns + sudo install -m 755 snap-discard-ns/snap-discard-ns $(DESTDIR)$(libexecdir)/snap-discard-ns sudo install -m 755 snap-seccomp/snap-seccomp $(DESTDIR)$(libexecdir)/snap-seccomp # for the hack target also: @@ -74,6 +87,8 @@ noinst_LIBRARIES += libsnap-confine-private.a libsnap_confine_private_a_SOURCES = \ + libsnap-confine-private/apparmor-support.c \ + libsnap-confine-private/apparmor-support.h \ libsnap-confine-private/cgroup-freezer-support.c \ libsnap-confine-private/cgroup-freezer-support.h \ libsnap-confine-private/classic.c \ @@ -84,6 +99,8 @@ libsnap-confine-private/error.h \ libsnap-confine-private/fault-injection.c \ libsnap-confine-private/fault-injection.h \ + libsnap-confine-private/feature.c \ + libsnap-confine-private/feature.h \ libsnap-confine-private/locking.c \ libsnap-confine-private/locking.h \ libsnap-confine-private/mount-opt.c \ @@ -98,6 +115,8 @@ libsnap-confine-private/snap.h \ libsnap-confine-private/string-utils.c \ libsnap-confine-private/string-utils.h \ + libsnap-confine-private/tool.c \ + libsnap-confine-private/tool.h \ libsnap-confine-private/utils.c \ libsnap-confine-private/utils.h libsnap_confine_private_a_CFLAGS = $(CHECK_CFLAGS) @@ -113,6 +132,7 @@ libsnap-confine-private/cleanup-funcs-test.c \ libsnap-confine-private/error-test.c \ libsnap-confine-private/fault-injection-test.c \ + libsnap-confine-private/feature-test.c \ libsnap-confine-private/locking-test.c \ libsnap-confine-private/mount-opt-test.c \ libsnap-confine-private/mountinfo-test.c \ @@ -120,8 +140,8 @@ libsnap-confine-private/secure-getenv-test.c \ libsnap-confine-private/snap-test.c \ libsnap-confine-private/string-utils-test.c \ - libsnap-confine-private/test-utils.c \ libsnap-confine-private/test-utils-test.c \ + libsnap-confine-private/test-utils.c \ libsnap-confine-private/unit-tests-main.c \ libsnap-confine-private/unit-tests.c \ libsnap-confine-private/unit-tests.h \ @@ -178,15 +198,13 @@ libexec_PROGRAMS += snap-confine/snap-confine if HAVE_RST2MAN -dist_man_MANS += snap-confine/snap-confine.1 -CLEANFILES += snap-confine/snap-confine.1 +dist_man_MANS += snap-confine/snap-confine.8 +CLEANFILES += snap-confine/snap-confine.8 endif EXTRA_DIST += snap-confine/snap-confine.rst EXTRA_DIST += snap-confine/snap-confine.apparmor.in snap_confine_snap_confine_SOURCES = \ - snap-confine/apparmor-support.c \ - snap-confine/apparmor-support.h \ snap-confine/cookie-support.c \ snap-confine/cookie-support.h \ snap-confine/mount-support-nvidia.c \ @@ -195,8 +213,6 @@ snap-confine/mount-support.h \ snap-confine/ns-support.c \ snap-confine/ns-support.h \ - snap-confine/quirks.c \ - snap-confine/quirks.h \ snap-confine/snap-confine-args.c \ snap-confine/snap-confine-args.h \ snap-confine/snap-confine.c \ @@ -242,7 +258,7 @@ snap-confine/seccomp-support.h snap_confine_snap_confine_CFLAGS += $(SECCOMP_CFLAGS) if STATIC_LIBSECCOMP -snap_confine_snap_confine_STATIC += $(shell pkg-config --static --libs libseccomp) +snap_confine_snap_confine_STATIC += $(shell $(PKG_CONFIG) --static --libs libseccomp) else snap_confine_snap_confine_extra_libs += $(SECCOMP_LIBS) endif # STATIC_LIBSECCOMP @@ -251,7 +267,7 @@ if APPARMOR snap_confine_snap_confine_CFLAGS += $(APPARMOR_CFLAGS) if STATIC_LIBAPPARMOR -snap_confine_snap_confine_STATIC += $(shell pkg-config --static --libs libapparmor) +snap_confine_snap_confine_STATIC += $(shell $(PKG_CONFIG) --static --libs libapparmor) else snap_confine_snap_confine_extra_libs += $(APPARMOR_LIBS) endif # STATIC_LIBAPPARMOR @@ -281,14 +297,11 @@ libsnap-confine-private/unit-tests-main.c \ libsnap-confine-private/unit-tests.c \ libsnap-confine-private/unit-tests.h \ - snap-confine/apparmor-support.c \ - snap-confine/apparmor-support.h \ snap-confine/cookie-support-test.c \ snap-confine/mount-support-test.c \ snap-confine/ns-support-test.c \ - snap-confine/quirks.c \ - snap-confine/quirks.h \ - snap-confine/snap-confine-args-test.c + snap-confine/snap-confine-args-test.c \ + snap-confine/snap-device-helper-test.c snap_confine_unit_tests_CFLAGS = $(snap_confine_snap_confine_CFLAGS) $(GLIB_CFLAGS) snap_confine_unit_tests_LDADD = $(snap_confine_snap_confine_LDADD) $(GLIB_LIBS) snap_confine_unit_tests_LDFLAGS = $(snap_confine_snap_confine_LDFLAGS) @@ -303,8 +316,7 @@ endif # WITH_UNIT_TESTS if HAVE_RST2MAN -snap-confine/%.1: snap-confine/%.rst - mkdir -p snap-confine +%.8: %.rst $(HAVE_RST2MAN) $^ > $@ endif @@ -340,7 +352,12 @@ ## snap-mgmt ## -libexec_PROGRAMS += snap-mgmt/snap-mgmt +libexec_SCRIPTS = snap-mgmt/snap-mgmt +CLEANFILES += snap-mgmt/$(am__dirstamp) snap-mgmt/snap-mgmt + +snap-mgmt/$(am__dirstamp): + mkdir -p $$(dirname $@) + touch $@ snap-mgmt/snap-mgmt: snap-mgmt/snap-mgmt.sh.in Makefile snap-mgmt/$(am__dirstamp) sed -e 's,[@]SNAP_MOUNT_DIR[@],$(SNAP_MOUNT_DIR),' <$< >$@ @@ -375,37 +392,18 @@ libexec_PROGRAMS += snap-discard-ns/snap-discard-ns if HAVE_RST2MAN -dist_man_MANS += snap-discard-ns/snap-discard-ns.5 -CLEANFILES += snap-discard-ns/snap-discard-ns.5 +dist_man_MANS += snap-discard-ns/snap-discard-ns.8 +CLEANFILES += snap-discard-ns/snap-discard-ns.8 endif EXTRA_DIST += snap-discard-ns/snap-discard-ns.rst snap_discard_ns_snap_discard_ns_SOURCES = \ - snap-confine/ns-support.c \ - snap-confine/ns-support.h \ - snap-confine/apparmor-support.c \ - snap-confine/apparmor-support.h \ snap-discard-ns/snap-discard-ns.c snap_discard_ns_snap_discard_ns_CFLAGS = $(CHECK_CFLAGS) $(AM_CFLAGS) snap_discard_ns_snap_discard_ns_LDFLAGS = $(AM_LDFLAGS) snap_discard_ns_snap_discard_ns_LDADD = libsnap-confine-private.a snap_discard_ns_snap_discard_ns_STATIC = -if APPARMOR -snap_discard_ns_snap_discard_ns_CFLAGS += $(APPARMOR_CFLAGS) -if STATIC_LIBAPPARMOR -snap_discard_ns_snap_discard_ns_STATIC += $(shell pkg-config --static --libs libapparmor) -else -snap_discard_ns_snap_discard_ns_LDADD += $(APPARMOR_LIBS) -endif # STATIC_LIBAPPARMOR -endif # APPARMOR - -if STATIC_LIBCAP -snap_discard_ns_snap_discard_ns_STATIC += -lcap -else -snap_discard_ns_snap_discard_ns_LDADD += -lcap -endif # STATIC_LIBCAP - # Use a hacked rule if we're doing static build. This allows us to inject the LIBS += .. rule below. snap-discard-ns/snap-discard-ns$(EXEEXT): $(snap_discard_ns_snap_discard_ns_OBJECTS) $(snap_discard_ns_snap_discard_ns_DEPENDENCIES) $(EXTRA_snap_discard_ns_snap_discard_ns_DEPENDENCIES) snap-discard-ns/$(am__dirstamp) @rm -f snap-discard-ns/snap-discard-ns$(EXEEXT) @@ -413,12 +411,6 @@ snap-discard-ns/snap-discard-ns$(EXEEXT): LIBS += -Wl,-Bstatic $(snap_discard_ns_snap_discard_ns_STATIC) -Wl,-Bdynamic -pthread -if HAVE_RST2MAN -snap-discard-ns/%.5: snap-discard-ns/%.rst - mkdir -p snap-discard-ns - $(HAVE_RST2MAN) $^ > $@ -endif - ## ## system-shutdown ## @@ -459,7 +451,36 @@ ## snapd-generator ## -libexec_PROGRAMS += snapd-generator/snapd-generator +systemdsystemgeneratordir = $(SYSTEMD_SYSTEM_GENERATOR_DIR) +systemdsystemgenerator_PROGRAMS = snapd-generator/snapd-generator snapd_generator_snapd_generator_SOURCES = snapd-generator/main.c snapd_generator_snapd_generator_LDADD = libsnap-confine-private.a + +## +## snapd-env-generator +## + +systemdsystemenvgeneratordir=$(SYSTEMD_SYSTEM_ENV_GENERATOR_DIR) +systemdsystemenvgenerator_PROGRAMS = snapd-env-generator/snapd-env-generator + +snapd_env_generator_snapd_env_generator_SOURCES = snapd-env-generator/main.c +snapd_env_generator_snapd_env_generator_LDADD = libsnap-confine-private.a +EXTRA_DIST += snapd-env-generator/snapd-env-generator.rst + +if HAVE_RST2MAN +dist_man_MANS += snapd-env-generator/snapd-env-generator.8 +CLEANFILES += snapd-env-generator/snapd-env-generator.8 +endif + +## +## snapd-apparmor +## + +EXTRA_DIST += snapd-apparmor/snapd-apparmor + +install-exec-local:: + install -d -m 755 $(DESTDIR)$(libexecdir) +if APPARMOR + install -m 755 $(srcdir)/snapd-apparmor/snapd-apparmor $(DESTDIR)$(libexecdir) +endif diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_abort.go snapd-2.37~rc1~14.04/cmd/snap/cmd_abort.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_abort.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_abort.go 2019-01-16 08:36:51.000000000 +0000 @@ -50,11 +50,13 @@ return ErrExtraArgs } - cli := Client() - id, err := x.GetChangeID(cli) + id, err := x.GetChangeID() if err != nil { + if err == noChangeFoundOK { + return nil + } return err } - _, err = cli.Abort(id) + _, err = x.client.Abort(id) return err } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_abort_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_abort_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_abort_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_abort_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,89 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestAbortLast(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes") + fmt.Fprintln(w, mockChangesJSON) + case 2: + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/changes/two") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{"action": "abort"}) + fmt.Fprintln(w, mockChangeJSON) + default: + c.Errorf("expected 2 queries, currently on %d", n) + } + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"abort", "--last=install"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") + + c.Assert(n, check.Equals, 2) +} + +func (s *SnapSuite) TestAbortLastQuestionmark(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + c.Check(r.Method, check.Equals, "GET") + c.Assert(r.URL.Path, check.Equals, "/v2/changes") + switch n { + case 1, 2: + fmt.Fprintln(w, `{"type": "sync", "result": []}`) + case 3, 4: + fmt.Fprintln(w, mockChangesJSON) + default: + c.Errorf("expected 4 calls, now on %d", n) + } + }) + for i := 0; i < 2; i++ { + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"abort", "--last=foobar?"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, "") + c.Check(s.Stderr(), check.Equals, "") + + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"abort", "--last=foobar"}) + if i == 0 { + c.Assert(err, check.ErrorMatches, `no changes found`) + } else { + c.Assert(err, check.ErrorMatches, `no changes of type "foobar" found`) + } + } + + c.Check(n, check.Equals, 4) +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_ack.go snapd-2.37~rc1~14.04/cmd/snap/cmd_ack.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_ack.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_ack.go 2019-01-16 08:36:51.000000000 +0000 @@ -23,22 +23,24 @@ "fmt" "io/ioutil" - "github.com/snapcore/snapd/i18n" - "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" ) type cmdAck struct { + clientMixin AckOptions struct { AssertionFile flags.Filename } `positional-args:"true" required:"true"` } -var shortAckHelp = i18n.G("Adds an assertion to the system") +var shortAckHelp = i18n.G("Add an assertion to the system") var longAckHelp = i18n.G(` The ack command tries to add an assertion to the system assertion database. -The assertion may also be a newer revision of a preexisting assertion that it +The assertion may also be a newer revision of a pre-existing assertion that it will replace. To succeed the assertion must be valid, its signature verified with a known @@ -50,27 +52,27 @@ addCommand("ack", shortAckHelp, longAckHelp, func() flags.Commander { return &cmdAck{} }, nil, []argDesc{{ - // TRANSLATORS: This needs to be wrapped in <>s. + // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), - // TRANSLATORS: This should probably not start with a lowercase letter. + // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Assertion file"), }}) } -func ackFile(assertFile string) error { +func ackFile(cli *client.Client, assertFile string) error { assertData, err := ioutil.ReadFile(assertFile) if err != nil { return err } - return Client().Ack(assertData) + return cli.Ack(assertData) } func (x *cmdAck) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } - if err := ackFile(string(x.AckOptions.AssertionFile)); err != nil { + if err := ackFile(x.client, string(x.AckOptions.AssertionFile)); err != nil { return fmt.Errorf("cannot assert: %v", err) } return nil diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_advise.go snapd-2.37~rc1~14.04/cmd/snap/cmd_advise.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_advise.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_advise.go 2019-01-16 08:36:51.000000000 +0000 @@ -20,56 +20,84 @@ package main import ( + "bufio" "encoding/json" "fmt" + "io" + "net" + "os" + "strconv" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/advisor" "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/osutil" ) type cmdAdviseSnap struct { Positionals struct { - CommandOrPkg string `required:"yes"` + CommandOrPkg string } `positional-args:"true"` - Format string `long:"format" default:"pretty"` - Command bool `long:"command"` + Format string `long:"format" default:"pretty" choice:"pretty" choice:"json"` + // Command makes advise try to find snaps that provide this command + Command bool `long:"command"` + + // FromApt tells advise that it got started from an apt hook + // and needs to communicate over a socket + FromApt bool `long:"from-apt"` } -var shortAdviseSnapHelp = i18n.G("Advise on available snaps.") +var shortAdviseSnapHelp = i18n.G("Advise on available snaps") var longAdviseSnapHelp = i18n.G(` -The advise-snap command shows what snaps with the given command are -available. +The advise-snap command searches for and suggests the installation of snaps. + +If --command is given, it suggests snaps that provide the given command. +Otherwise it suggests snaps with the given name. `) func init() { cmd := addCommand("advise-snap", shortAdviseSnapHelp, longAdviseSnapHelp, func() flags.Commander { return &cmdAdviseSnap{} }, map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. "command": i18n.G("Advise on snaps that provide the given command"), - "format": i18n.G("Use the given output format (pretty or json)"), + // TRANSLATORS: This should not start with a lowercase letter. + "from-apt": i18n.G("Advise will talk to apt via an apt hook"), + // TRANSLATORS: This should not start with a lowercase letter. + "format": i18n.G("Use the given output format"), }, []argDesc{ - {name: ""}, + // TRANSLATORS: This needs to begin with < and end with > + {name: i18n.G("")}, }) cmd.hidden = true } func outputAdviseExactText(command string, result []advisor.Command) error { - fmt.Fprintf(Stdout, i18n.G("The program %q can be found in the following snaps:\n"), command) + fmt.Fprintf(Stdout, "\n") + // TRANSLATORS: %q is a command name (like "gimp" or "loimpress") + fmt.Fprintf(Stdout, i18n.G("Command %q not found, but can be installed with:\n"), command) + fmt.Fprintf(Stdout, "\n") for _, snap := range result { - fmt.Fprintf(Stdout, " * %s\n", snap.Snap) + fmt.Fprintf(Stdout, "sudo snap install %s\n", snap.Snap) } - fmt.Fprintf(Stdout, i18n.G("Try: snap install \n")) + fmt.Fprintf(Stdout, "\n") + fmt.Fprintln(Stdout, i18n.G("See 'snap info ' for additional versions.")) + fmt.Fprintf(Stdout, "\n") return nil } func outputAdviseMisspellText(command string, result []advisor.Command) error { - fmt.Fprintf(Stdout, i18n.G("No command %q found, did you mean:\n"), command) + fmt.Fprintf(Stdout, "\n") + fmt.Fprintf(Stdout, i18n.G("Command %q not found, did you mean:\n"), command) + fmt.Fprintf(Stdout, "\n") for _, snap := range result { - fmt.Fprintf(Stdout, " Command %q from snap %q\n", snap.Command, snap.Snap) + fmt.Fprintf(Stdout, i18n.G(" command %q from snap %q\n"), snap.Command, snap.Snap) } + fmt.Fprintf(Stdout, "\n") + fmt.Fprintln(Stdout, i18n.G("See 'snap info ' for additional versions.")) + fmt.Fprintf(Stdout, "\n") return nil } @@ -79,11 +107,126 @@ return nil } +type jsonRPC struct { + JsonRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params struct { + Command string `json:"command"` + SearchTerms []string `json:"search-terms"` + UnknownPackages []string `json:"unknown-packages"` + } +} + +// readRpc reads a apt json rpc protocol 0.1 message as described in +// https://salsa.debian.org/apt-team/apt/blob/master/doc/json-hooks-protocol.md#wire-protocol +func readRpc(r *bufio.Reader) (*jsonRPC, error) { + line, err := r.ReadBytes('\n') + if err != nil && err != io.EOF { + return nil, fmt.Errorf("cannot read json-rpc: %v", err) + } + if osutil.GetenvBool("SNAP_APT_HOOK_DEBUG") { + fmt.Fprintf(os.Stderr, "%s\n", line) + } + + var rpc jsonRPC + if err := json.Unmarshal(line, &rpc); err != nil { + return nil, err + } + // empty \n + emptyNL, _, err := r.ReadLine() + if err != nil { + return nil, err + } + if string(emptyNL) != "" { + return nil, fmt.Errorf("unexpected line: %q (empty)", emptyNL) + } + + return &rpc, nil +} + +func adviseViaAptHook() error { + sockFd := os.Getenv("APT_HOOK_SOCKET") + if sockFd == "" { + return fmt.Errorf("cannot find APT_HOOK_SOCKET env") + } + fd, err := strconv.Atoi(sockFd) + if err != nil { + return fmt.Errorf("expected APT_HOOK_SOCKET to be a decimal integer, found %q", sockFd) + } + + f := os.NewFile(uintptr(fd), "apt-hook-socket") + if f == nil { + return fmt.Errorf("cannot open file descriptor %v", fd) + } + defer f.Close() + + conn, err := net.FileConn(f) + if err != nil { + return fmt.Errorf("cannot connect to %v: %v", fd, err) + } + defer conn.Close() + + r := bufio.NewReader(conn) + + // handshake + rpc, err := readRpc(r) + if err != nil { + return err + } + if rpc.Method != "org.debian.apt.hooks.hello" { + return fmt.Errorf("expected 'hello' method, got: %v", rpc.Method) + } + if _, err := conn.Write([]byte(`{"jsonrpc":"2.0","id":0,"result":{"version":"0.1"}}` + "\n\n")); err != nil { + return err + } + + // payload + rpc, err = readRpc(r) + if err != nil { + return err + } + if rpc.Method == "org.debian.apt.hooks.install.fail" { + for _, pkgName := range rpc.Params.UnknownPackages { + match, err := advisor.FindPackage(pkgName) + if err == nil && match != nil { + fmt.Fprintf(Stdout, "\n") + fmt.Fprintf(Stdout, i18n.G("No apt package %q, but there is a snap with that name.\n"), pkgName) + fmt.Fprintf(Stdout, i18n.G("Try \"snap install %s\"\n"), pkgName) + fmt.Fprintf(Stdout, "\n") + } + } + + } + // if rpc.Method == "org.debian.apt.hooks.search.post" { + // // FIXME: do a snap search here + // // FIXME2: figure out why apt does not tell us the search results + // } + + // bye + rpc, err = readRpc(r) + if err != nil { + return err + } + if rpc.Method != "org.debian.apt.hooks.bye" { + return fmt.Errorf("expected 'bye' method, got: %v", rpc.Method) + } + + return nil +} + func (x *cmdAdviseSnap) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } + if x.FromApt { + return adviseViaAptHook() + } + + if len(x.Positionals.CommandOrPkg) == 0 { + return fmt.Errorf("the required argument `` was not provided") + } + if x.Command { return adviseCommand(x.Positionals.CommandOrPkg, x.Format) } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_advise_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_advise_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_advise_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_advise_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -20,7 +20,14 @@ package main_test import ( + "bufio" + "bytes" "fmt" + "net" + "os" + "strconv" + "strings" + "syscall" . "gopkg.in/check.v1" @@ -65,13 +72,17 @@ restore := advisor.ReplaceCommandsFinder(mkSillyFinder) defer restore() - rest, err := snap.Parser().ParseArgs([]string{"advise-snap", "--command", "hello"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"advise-snap", "--command", "hello"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) - c.Assert(s.Stdout(), Equals, `The program "hello" can be found in the following snaps: - * hello - * hello-wcm -Try: snap install + c.Assert(s.Stdout(), Equals, ` +Command "hello" not found, but can be installed with: + +sudo snap install hello +sudo snap install hello-wcm + +See 'snap info ' for additional versions. + `) c.Assert(s.Stderr(), Equals, "") } @@ -80,7 +91,7 @@ restore := advisor.ReplaceCommandsFinder(mkSillyFinder) defer restore() - rest, err := snap.Parser().ParseArgs([]string{"advise-snap", "--command", "--format=json", "hello"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"advise-snap", "--command", "--format=json", "hello"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Assert(s.Stdout(), Equals, `[{"Snap":"hello","Command":"hello"},{"Snap":"hello-wcm","Command":"hello"}]`+"\n") @@ -94,9 +105,14 @@ for _, misspelling := range []string{"helo", "0hello", "hell0", "hello0"} { err := snap.AdviseCommand(misspelling, "pretty") c.Assert(err, IsNil) - c.Assert(s.Stdout(), Equals, fmt.Sprintf(`No command "%s" found, did you mean: - Command "hello" from snap "hello" - Command "hello" from snap "hello-wcm" + c.Assert(s.Stdout(), Equals, fmt.Sprintf(` +Command "%s" not found, did you mean: + + command "hello" from snap "hello" + command "hello" from snap "hello-wcm" + +See 'snap info ' for additional versions. + `, misspelling)) c.Assert(s.Stderr(), Equals, "") @@ -104,3 +120,124 @@ s.stderr.Reset() } } + +func (s *SnapSuite) TestAdviseFromAptIntegrationNoAptPackage(c *C) { + restore := advisor.ReplaceCommandsFinder(mkSillyFinder) + defer restore() + + fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) + c.Assert(err, IsNil) + + os.Setenv("APT_HOOK_SOCKET", strconv.Itoa(int(fds[1]))) + // note we don't close fds[1] ourselves; adviseViaAptHook might, or we might leak it + // (we don't close it here to avoid accidentally closing an arbitrary file descriptor that reused the number) + + done := make(chan bool, 1) + go func() { + f := os.NewFile(uintptr(fds[0]), "advise-sock") + conn, err := net.FileConn(f) + c.Assert(err, IsNil) + defer conn.Close() + defer f.Close() + + // handshake + _, err = conn.Write([]byte(`{"jsonrpc":"2.0","method":"org.debian.apt.hooks.hello","id":0,"params":{"versions":["0.1"]}}` + "\n\n")) + c.Assert(err, IsNil) + + // reply from snap + r := bufio.NewReader(conn) + buf, _, err := r.ReadLine() + c.Assert(err, IsNil) + c.Assert(string(buf), Equals, `{"jsonrpc":"2.0","id":0,"result":{"version":"0.1"}}`) + // plus empty line + buf, _, err = r.ReadLine() + c.Assert(err, IsNil) + c.Assert(string(buf), Equals, ``) + + // payload + _, err = conn.Write([]byte(`{"jsonrpc":"2.0","method":"org.debian.apt.hooks.install.fail","params":{"command":"install","search-terms":["aws-cli"],"unknown-packages":["hello"],"packages":[]}}` + "\n\n")) + c.Assert(err, IsNil) + + // bye + _, err = conn.Write([]byte(`{"jsonrpc":"2.0","method":"org.debian.apt.hooks.bye","params":{}}` + "\n\n")) + c.Assert(err, IsNil) + + done <- true + }() + + cmd := snap.CmdAdviseSnap() + cmd.FromApt = true + err = cmd.Execute(nil) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, ` +No apt package "hello", but there is a snap with that name. +Try "snap install hello" + +`) + c.Assert(s.Stderr(), Equals, "") + c.Assert(<-done, Equals, true) +} + +func (s *SnapSuite) TestReadRpc(c *C) { + rpc := strings.Replace(` +{ + "jsonrpc": "2.0", + "method": "org.debian.apt.hooks.install.pre-prompt", + "params": { + "command": "install", + "packages": [ + { + "architecture": "amd64", + "automatic": false, + "id": 38033, + "mode": "install", + "name": "hello", + "versions": { + "candidate": { + "architecture": "amd64", + "id": 22712, + "pin": 500, + "version": "4:17.12.3-1ubuntu1" + }, + "install": { + "architecture": "amd64", + "id": 22712, + "pin": 500, + "version": "4:17.12.3-1ubuntu1" + } + } + }, + { + "architecture": "amd64", + "automatic": true, + "id": 38202, + "mode": "install", + "name": "hello-kpart", + "versions": { + "candidate": { + "architecture": "amd64", + "id": 22713, + "pin": 500, + "version": "4:17.12.3-1ubuntu1" + }, + "install": { + "architecture": "amd64", + "id": 22713, + "pin": 500, + "version": "4:17.12.3-1ubuntu1" + } + } + } + ], + "search-terms": [ + "hello" + ], + "unknown-packages": [] + } +}`, "\n", "", -1) + // all apt rpc ends with \n\n + rpc = rpc + "\n\n" + // this can be parsed without errors + _, err := snap.ReadRpc(bufio.NewReader(bytes.NewBufferString(rpc))) + c.Assert(err, IsNil) +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_aliases.go snapd-2.37~rc1~14.04/cmd/snap/cmd_aliases.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_aliases.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_aliases.go 2019-01-16 08:36:51.000000000 +0000 @@ -31,12 +31,13 @@ ) type cmdAliases struct { + clientMixin Positionals struct { Snap installedSnapName `positional-arg-name:""` } `positional-args:"true"` } -var shortAliasesHelp = i18n.G("Lists aliases in the system") +var shortAliasesHelp = i18n.G("List aliases in the system") var longAliasesHelp = i18n.G(` The aliases command lists all aliases available in the system and their status. @@ -45,8 +46,8 @@ Lists only the aliases defined by the specified snap. An alias noted as undefined means it was explicitly enabled or disabled but is -not defined in the current revision of the snap; possibly temporarely (e.g -because of a revert), if not this can be cleared with snap alias --reset. +not defined in the current revision of the snap, possibly temporarily (e.g. +because of a revert). This can cleared with 'snap alias --reset'. `) func init() { @@ -89,7 +90,7 @@ return ErrExtraArgs } - allStatuses, err := Client().Aliases() + allStatuses, err := x.client.Aliases() if err != nil { return err } @@ -140,7 +141,7 @@ } else { fmt.Fprintln(Stderr, i18n.G("No aliases are currently defined.")) } - fmt.Fprintln(Stderr, i18n.G("\nUse snap alias --help to learn how to create aliases manually.")) + fmt.Fprintln(Stderr, i18n.G("\nUse 'snap help alias' to learn how to create aliases manually.")) } return nil } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_aliases_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_aliases_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_aliases_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_aliases_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -31,7 +31,7 @@ func (s *SnapSuite) TestAliasesHelp(c *C) { msg := `Usage: - snap.test [OPTIONS] aliases [] + snap.test aliases [] The aliases command lists all aliases available in the system and their status. @@ -40,18 +40,10 @@ Lists only the aliases defined by the specified snap. An alias noted as undefined means it was explicitly enabled or disabled but is -not defined in the current revision of the snap; possibly temporarely (e.g -because of a revert), if not this can be cleared with snap alias --reset. - -Application Options: - --version Print the version and exit - -Help Options: - -h, --help Show this help message +not defined in the current revision of the snap, possibly temporarily (e.g. +because of a revert). This can cleared with 'snap alias --reset'. ` - rest, err := Parser().ParseArgs([]string{"aliases", "--help"}) - c.Assert(err.Error(), Equals, msg) - c.Assert(rest, DeepEquals, []string{}) + s.testSubCommandHelp(c, "aliases", msg) } func (s *SnapSuite) TestAliases(c *C) { @@ -76,7 +68,7 @@ }, }) }) - rest, err := Parser().ParseArgs([]string{"aliases"}) + rest, err := Parser(Client()).ParseArgs([]string{"aliases"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + @@ -111,7 +103,7 @@ }, }) }) - rest, err := Parser().ParseArgs([]string{"aliases", "foo"}) + rest, err := Parser(Client()).ParseArgs([]string{"aliases", "foo"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + @@ -134,10 +126,10 @@ "result": map[string]map[string]client.AliasStatus{}, }) }) - _, err := Parser().ParseArgs([]string{"aliases"}) + _, err := Parser(Client()).ParseArgs([]string{"aliases"}) c.Assert(err, IsNil) c.Assert(s.Stdout(), Equals, "") - c.Assert(s.Stderr(), Equals, "No aliases are currently defined.\n\nUse snap alias --help to learn how to create aliases manually.\n") + c.Assert(s.Stderr(), Equals, "No aliases are currently defined.\n\nUse 'snap help alias' to learn how to create aliases manually.\n") } func (s *SnapSuite) TestAliasesNoneFilterSnap(c *C) { @@ -155,10 +147,10 @@ }}, }) }) - _, err := Parser().ParseArgs([]string{"aliases", "not-bar"}) + _, err := Parser(Client()).ParseArgs([]string{"aliases", "not-bar"}) c.Assert(err, IsNil) c.Assert(s.Stdout(), Equals, "") - c.Assert(s.Stderr(), Equals, "No aliases are currently defined for snap \"not-bar\".\n\nUse snap alias --help to learn how to create aliases manually.\n") + c.Assert(s.Stderr(), Equals, "No aliases are currently defined for snap \"not-bar\".\n\nUse 'snap help alias' to learn how to create aliases manually.\n") } func (s *SnapSuite) TestAliasesSorting(c *C) { diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_alias.go snapd-2.37~rc1~14.04/cmd/snap/cmd_alias.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_alias.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_alias.go 2019-01-16 08:36:51.000000000 +0000 @@ -32,6 +32,7 @@ ) type cmdAlias struct { + waitMixin Positionals struct { SnapApp appName `required:"yes"` Alias string `required:"yes"` @@ -40,19 +41,20 @@ // TODO: implement a completer for snapApp -var shortAliasHelp = i18n.G("Sets up a manual alias") +var shortAliasHelp = i18n.G("Set up a manual alias") var longAliasHelp = i18n.G(` The alias command aliases the given snap application to the given alias. -Once this manual alias is setup the respective application command can be invoked just using the alias. +Once this manual alias is setup the respective application command can be +invoked just using the alias. `) func init() { addCommand("alias", shortAliasHelp, longAliasHelp, func() flags.Commander { return &cmdAlias{} - }, nil, []argDesc{ + }, waitDescs, []argDesc{ {name: ""}, - // TRANSLATORS: This needs to be wrapped in <>s. + // TRANSLATORS: This needs to begin with < and end with > {name: i18n.G("")}, }) } @@ -65,21 +67,19 @@ snapName, appName := snap.SplitSnapApp(string(x.Positionals.SnapApp)) alias := x.Positionals.Alias - cli := Client() - id, err := cli.Alias(snapName, appName, alias) + id, err := x.client.Alias(snapName, appName, alias) if err != nil { return err } - - chg, err := wait(cli, id) + chg, err := x.wait(id) if err != nil { - return err - } - if err := showAliasChanges(chg); err != nil { + if err == noWait { + return nil + } return err } - return nil + return showAliasChanges(chg) } type changedAlias struct { @@ -98,9 +98,11 @@ } w := tabwriter.NewWriter(Stdout, 2, 2, 1, ' ', 0) if len(added) != 0 { + // TRANSLATORS: this is used to introduce a list of aliases that were added printChangedAliases(w, i18n.G("Added"), added) } if len(removed) != 0 { + // TRANSLATORS: this is used to introduce a list of aliases that were removed printChangedAliases(w, i18n.G("Removed"), removed) } w.Flush() @@ -110,6 +112,7 @@ func printChangedAliases(w io.Writer, label string, changed []*changedAlias) { fmt.Fprintf(w, "%s:\n", label) for _, a := range changed { - fmt.Fprintf(w, "\t- %s as %s\n", snap.JoinSnapApp(a.Snap, a.App), a.Alias) + // TRANSLATORS: the first %s is a snap command (e.g. "hello-world.echo"), the second is the alias + fmt.Fprintf(w, i18n.G("\t- %s as %s\n"), snap.JoinSnapApp(a.Snap, a.App), a.Alias) } } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_alias_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_alias_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_alias_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_alias_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -30,22 +30,18 @@ func (s *SnapSuite) TestAliasHelp(c *C) { msg := `Usage: - snap.test [OPTIONS] alias [] [] + snap.test alias [alias-OPTIONS] [] [] The alias command aliases the given snap application to the given alias. Once this manual alias is setup the respective application command can be invoked just using the alias. -Application Options: - --version Print the version and exit - -Help Options: - -h, --help Show this help message +[alias command options] + --no-wait Do not wait for the operation to finish but just print + the change id. ` - rest, err := Parser().ParseArgs([]string{"alias", "--help"}) - c.Assert(err.Error(), Equals, msg) - c.Assert(rest, DeepEquals, []string{}) + s.testSubCommandHelp(c, "alias", msg) } func (s *SnapSuite) TestAlias(c *C) { @@ -67,7 +63,7 @@ c.Fatalf("unexpected path %q", r.URL.Path) } }) - rest, err := Parser().ParseArgs([]string{"alias", "alias-snap.cmd1", "alias1"}) + rest, err := Parser(Client()).ParseArgs([]string{"alias", "alias-snap.cmd1", "alias1"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Assert(s.Stdout(), Equals, ""+ diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_auto_import.go snapd-2.37~rc1~14.04/cmd/snap/cmd_auto_import.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_auto_import.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_auto_import.go 2019-01-16 08:36:51.000000000 +0000 @@ -127,7 +127,7 @@ return osutil.CopyFile(src, dst, osutil.CopyFlagOverwrite) } -func autoImportFromSpool() (added int, err error) { +func autoImportFromSpool(cli *client.Client) (added int, err error) { files, err := ioutil.ReadDir(dirs.SnapAssertsSpoolDir) if os.IsNotExist(err) { return 0, nil @@ -138,7 +138,7 @@ for _, fi := range files { cand := filepath.Join(dirs.SnapAssertsSpoolDir, fi.Name()) - if err := ackFile(cand); err != nil { + if err := ackFile(cli, cand); err != nil { logger.Noticef("error: cannot import %s: %s", cand, err) continue } else { @@ -154,7 +154,7 @@ return added, nil } -func autoImportFromAllMounts() (int, error) { +func autoImportFromAllMounts(cli *client.Client) (int, error) { cands, err := autoImportCandidates() if err != nil { return 0, err @@ -162,7 +162,7 @@ added := 0 for _, cand := range cands { - err := ackFile(cand) + err := ackFile(cli, cand) // the server is not ready yet if _, ok := err.(client.ConnectionError); ok { logger.Noticef("queuing for later %s", cand) @@ -214,23 +214,24 @@ } type cmdAutoImport struct { + clientMixin Mount []string `long:"mount" arg-name:""` ForceClassic bool `long:"force-classic"` } -var shortAutoImportHelp = i18n.G("Inspects devices for actionable information") +var shortAutoImportHelp = i18n.G("Inspect devices for actionable information") var longAutoImportHelp = i18n.G(` The auto-import command searches available mounted devices looking for assertions that are signed by trusted authorities, and potentially performs system changes based on them. -If one or more device paths are provided via --mount, these are temporariy +If one or more device paths are provided via --mount, these are temporarily mounted to be inspected as well. Even in that case the command will still consider all available mounted devices for inspection. -Imported assertions must be made available in the auto-import.assert file +Assertions to be imported must be made available in the auto-import.assert file in the root of the filesystem. `) @@ -241,15 +242,19 @@ func() flags.Commander { return &cmdAutoImport{} }, map[string]string{ - "mount": i18n.G("Temporarily mount device before inspecting"), + // TRANSLATORS: This should not start with a lowercase letter. + "mount": i18n.G("Temporarily mount device before inspecting"), + // TRANSLATORS: This should not start with a lowercase letter. "force-classic": i18n.G("Force import on classic systems"), }, nil) cmd.hidden = true } -func autoAddUsers() error { +func (x *cmdAutoImport) autoAddUsers() error { cmd := cmdCreateUser{ - Known: true, Sudoer: true, + clientMixin: x.clientMixin, + Known: true, + Sudoer: true, } return cmd.Execute(nil) } @@ -282,18 +287,18 @@ defer doUmount(mp) } - added1, err := autoImportFromSpool() + added1, err := autoImportFromSpool(x.client) if err != nil { return err } - added2, err := autoImportFromAllMounts() + added2, err := autoImportFromAllMounts(x.client) if err != nil { return err } if added1+added2 > 0 { - return autoAddUsers() + return x.autoAddUsers() } return nil diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_auto_import_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_auto_import_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_auto_import_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_auto_import_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -88,7 +88,7 @@ logbuf, restore := logger.MockLogger() defer restore() - rest, err := snap.Parser().ParseArgs([]string{"auto-import"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Check(s.Stdout(), Equals, `created user "foo"`+"\n") @@ -120,7 +120,7 @@ restore = snap.MockMountInfoPath(makeMockMountInfo(c, content)) defer restore() - rest, err := snap.Parser().ParseArgs([]string{"auto-import"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Check(s.Stdout(), Equals, "") @@ -175,7 +175,7 @@ restore = snap.MockMountInfoPath(makeMockMountInfo(c, content)) defer restore() - rest, err := snap.Parser().ParseArgs([]string{"auto-import"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Check(s.Stdout(), Equals, "") @@ -204,7 +204,7 @@ restore = snap.MockMountInfoPath(makeMockMountInfo(c, content)) defer restore() - rest, err := snap.Parser().ParseArgs([]string{"auto-import"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Check(s.Stdout(), Equals, "") @@ -261,7 +261,7 @@ logbuf, restore := logger.MockLogger() defer restore() - rest, err := snap.Parser().ParseArgs([]string{"auto-import"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Check(s.Stdout(), Equals, `created user "foo"`+"\n") @@ -297,6 +297,6 @@ restore = snap.MockMountInfoPath(makeMockMountInfo(c, content)) defer restore() - _, err = snap.Parser().ParseArgs([]string{"auto-import"}) + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"}) c.Assert(err, ErrorMatches, "cannot queue .*, file size too big: 656384") } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_blame_generated.go snapd-2.37~rc1~14.04/cmd/snap/cmd_blame_generated.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_blame_generated.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_blame_generated.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,7 @@ +package main + +// generated by mkauthors.sh; do not edit + +func init() { + authors = []string{"Mark Shuttleworth", "Gustavo Niemeyer", "Sergio Schvezov", "Simon Fels", "Kyle Fazzari", "Leo Arias", "Sergio Cazzolato", "Gustavo Niemeyer", "Federico Gimenez", "Maciej Borzecki", "Jamie Strandboge", "Pawel Stolowski", "John R. Lenton", "Samuele Pedroni", "Zygmunt Krynicki", "Michael Vogt"} +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_blame.go snapd-2.37~rc1~14.04/cmd/snap/cmd_blame.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_blame.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_blame.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,55 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main + +//go:generate mkauthors.sh + +import ( + "fmt" + "math/rand" + + "github.com/jessevdk/go-flags" +) + +type cmdBlame struct{} + +var authors []string + +func init() { + cmd := addCommand("blame", + "", + "", + func() flags.Commander { + return &cmdBlame{} + }, nil, nil) + cmd.hidden = true +} + +func (x *cmdBlame) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + if len(authors) == 0 { + return nil + } + + fmt.Fprintf(Stdout, "It's all %s's fault.\n", authors[rand.Intn(len(authors))]) + return nil +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_booted.go snapd-2.37~rc1~14.04/cmd/snap/cmd_booted.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_booted.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_booted.go 2019-01-16 08:36:51.000000000 +0000 @@ -29,8 +29,8 @@ func init() { cmd := addCommand("booted", - "internal", - "internal", + "Internal", + "The booted command is only retained for backwards compatibility.", func() flags.Commander { return &cmdBooted{} }, nil, nil) diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_buy.go snapd-2.37~rc1~14.04/cmd/snap/cmd_buy.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_buy.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_buy.go 2019-01-16 08:36:51.000000000 +0000 @@ -25,30 +25,31 @@ "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" - "github.com/snapcore/snapd/store" "github.com/jessevdk/go-flags" ) -var shortBuyHelp = i18n.G("Buys a snap") +var shortBuyHelp = i18n.G("Buy a snap") var longBuyHelp = i18n.G(` The buy command buys a snap from the store. `) type cmdBuy struct { + clientMixin Positional struct { SnapName remoteSnapName } `positional-args:"yes" required:"yes"` } func init() { - addCommand("buy", shortBuyHelp, longBuyHelp, func() flags.Commander { + cmd := addCommand("buy", shortBuyHelp, longBuyHelp, func() flags.Commander { return &cmdBuy{} }, map[string]string{}, []argDesc{{ name: "", - // TRANSLATORS: This should probably not start with a lowercase letter. + // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Snap name"), }}) + cmd.hidden = true } func (x *cmdBuy) Execute(args []string) error { @@ -56,12 +57,10 @@ return ErrExtraArgs } - return buySnap(string(x.Positional.SnapName)) + return buySnap(x.client, string(x.Positional.SnapName)) } -func buySnap(snapName string) error { - cli := Client() - +func buySnap(cli *client.Client, snapName string) error { user := cli.LoggedInUser() if user == nil { return fmt.Errorf(i18n.G("You need to be logged in to purchase software. Please run 'snap login' and try again.")) @@ -76,7 +75,7 @@ return err } - opts := &store.BuyOptions{ + opts := &client.BuyOptions{ SnapID: snap.ID, Currency: resultInfo.SuggestedCurrency, } @@ -109,10 +108,10 @@ // TRANSLATORS: %q, %q and %s are the snap name, developer, and price. Please wrap the translation at 80 characters. fmt.Fprintf(Stdout, i18n.G(`Please re-enter your Ubuntu One password to purchase %q from %q -for %s. Press ctrl-c to cancel.`), snap.Name, snap.Developer, formatPrice(opts.Price, opts.Currency)) +for %s. Press ctrl-c to cancel.`), snap.Name, snap.Publisher.Username, formatPrice(opts.Price, opts.Currency)) fmt.Fprint(Stdout, "\n") - err = requestLogin(user.Email) + err = requestLogin(cli, user.Email) if err != nil { return err } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_buy_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_buy_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_buy_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_buy_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -89,7 +89,7 @@ } func (s *BuySnapSuite) TestBuyHelp(c *check.C) { - _, err := snap.Parser().ParseArgs([]string{"buy"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy"}) c.Assert(err, check.NotNil) c.Check(err.Error(), check.Equals, "the required argument `` was not provided") c.Check(s.Stdout(), check.Equals, "") @@ -97,13 +97,13 @@ } func (s *BuySnapSuite) TestBuyInvalidCharacters(c *check.C) { - _, err := snap.Parser().ParseArgs([]string{"buy", "a:b"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "a:b"}) c.Assert(err, check.NotNil) c.Check(err.Error(), check.Equals, "cannot buy snap: invalid characters in name") c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") - _, err = snap.Parser().ParseArgs([]string{"buy", "c*d"}) + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"buy", "c*d"}) c.Assert(err, check.NotNil) c.Check(err.Error(), check.Equals, "cannot buy snap: invalid characters in name") c.Check(s.Stdout(), check.Equals, "") @@ -121,6 +121,12 @@ "confinement": "strict", "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", "developer": "canonical", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, "download-size": 65536, "icon": "", "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", @@ -155,7 +161,7 @@ defer mockServer.checkCounts() s.RedirectClientToTestServer(mockServer.serveHttp) - rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "hello"}) c.Assert(err, check.NotNil) c.Check(err.Error(), check.Equals, "cannot buy snap: snap is free") c.Assert(rest, check.DeepEquals, []string{"hello"}) @@ -174,6 +180,12 @@ "confinement": "strict", "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", "developer": "canonical", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, "download-size": 65536, "icon": "", "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", @@ -294,7 +306,7 @@ // Confirm the purchase. s.password = "the password" - rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "hello"}) c.Check(err, check.IsNil) c.Check(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `Please re-enter your Ubuntu One password to purchase "hello" from "canonical" @@ -355,7 +367,7 @@ // Confirm the purchase. s.password = "the password" - rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "hello"}) c.Assert(err, check.NotNil) c.Check(err.Error(), check.Equals, `Sorry, your payment method has been declined by the issuer. Please review your payment details at https://my.ubuntu.com/payment/edit and try again.`) @@ -392,7 +404,7 @@ defer mockServer.checkCounts() s.RedirectClientToTestServer(mockServer.serveHttp) - rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "hello"}) c.Assert(err, check.NotNil) c.Check(err.Error(), check.Equals, `You need to have a payment method associated with your account in order to buy a snap, please visit https://my.ubuntu.com/payment/edit to add one. @@ -427,7 +439,7 @@ defer mockServer.checkCounts() s.RedirectClientToTestServer(mockServer.serveHttp) - rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "hello"}) c.Assert(err, check.NotNil) c.Check(err.Error(), check.Equals, `In order to buy "hello", you need to agree to the latest terms and conditions. Please visit https://my.ubuntu.com/payment/edit to do this. @@ -441,7 +453,7 @@ // We don't login here s.Logout(c) - rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "hello"}) c.Check(err, check.NotNil) c.Check(err.Error(), check.Equals, "You need to be logged in to purchase software. Please run 'snap login' and try again.") c.Check(rest, check.DeepEquals, []string{"hello"}) diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_can_manage_refreshes.go snapd-2.37~rc1~14.04/cmd/snap/cmd_can_manage_refreshes.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_can_manage_refreshes.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_can_manage_refreshes.go 2019-01-16 08:36:51.000000000 +0000 @@ -25,7 +25,9 @@ "github.com/jessevdk/go-flags" ) -type cmdCanManageRefreshes struct{} +type cmdCanManageRefreshes struct { + clientMixin +} func init() { cmd := addDebugCommand("can-manage-refreshes", @@ -33,7 +35,7 @@ "(internal) return if refresh.schedule=managed can be used", func() flags.Commander { return &cmdCanManageRefreshes{} - }) + }, nil, nil) cmd.hidden = true } @@ -43,7 +45,7 @@ } var resp bool - if err := Client().Debug("can-manage-refreshes", nil, &resp); err != nil { + if err := x.client.Debug("can-manage-refreshes", nil, &resp); err != nil { return err } fmt.Fprintf(Stdout, "%v\n", resp) diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_changes.go snapd-2.37~rc1~14.04/cmd/snap/cmd_changes.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_changes.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_changes.go 2019-01-16 08:36:51.000000000 +0000 @@ -23,7 +23,6 @@ "fmt" "regexp" "sort" - "time" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" @@ -34,24 +33,32 @@ var shortChangesHelp = i18n.G("List system changes") var shortTasksHelp = i18n.G("List a change's tasks") var longChangesHelp = i18n.G(` -The changes command displays a summary of the recent system changes performed.`) +The changes command displays a summary of system changes performed recently. +`) var longTasksHelp = i18n.G(` -The tasks command displays a summary of tasks associated to an individual change.`) +The tasks command displays a summary of tasks associated with an individual +change. +`) type cmdChanges struct { + clientMixin + timeMixin Positional struct { Snap string `positional-arg-name:""` } `positional-args:"yes"` } -type cmdTasks struct{ changeIDMixin } +type cmdTasks struct { + timeMixin + changeIDMixin +} func init() { addCommand("changes", shortChangesHelp, longChangesHelp, - func() flags.Commander { return &cmdChanges{} }, nil, nil) + func() flags.Commander { return &cmdChanges{} }, timeDescs, nil) addCommand("tasks", shortTasksHelp, longTasksHelp, func() flags.Commander { return &cmdTasks{} }, - changeIDMixinOptDesc, + changeIDMixinOptDesc.also(timeDescs), changeIDMixinArgDesc).alias = "change" } @@ -63,14 +70,25 @@ var allDigits = regexp.MustCompile(`^[0-9]+$`).MatchString +func queryChanges(cli *client.Client, opts *client.ChangesOptions) ([]*client.Change, error) { + chgs, err := cli.Changes(opts) + if err != nil { + return nil, err + } + if err := warnMaintenance(cli); err != nil { + return nil, err + } + return chgs, nil +} + func (c *cmdChanges) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } if allDigits(c.Positional.Snap) { - // TRANSLATORS: the %s is the argument given by the user to "snap changes" - return fmt.Errorf(i18n.G(`"snap changes" command expects a snap name, try: "snap tasks %s"`), c.Positional.Snap) + // TRANSLATORS: the %s is the argument given by the user to 'snap changes' + return fmt.Errorf(i18n.G(`'snap changes' command expects a snap name, try 'snap tasks %s'`), c.Positional.Snap) } if c.Positional.Snap == "everything" { @@ -83,8 +101,7 @@ Selector: client.ChangesAll, } - cli := Client() - changes, err := cli.Changes(&opts) + changes, err := queryChanges(c.client, &opts) if err != nil { return err } @@ -99,8 +116,8 @@ fmt.Fprintf(w, i18n.G("ID\tStatus\tSpawn\tReady\tSummary\n")) for _, chg := range changes { - spawnTime := chg.SpawnTime.UTC().Format(time.RFC3339) - readyTime := chg.ReadyTime.UTC().Format(time.RFC3339) + spawnTime := c.fmtTime(chg.SpawnTime) + readyTime := c.fmtTime(chg.ReadyTime) if chg.ReadyTime.IsZero() { readyTime = "-" } @@ -114,18 +131,31 @@ } func (c *cmdTasks) Execute([]string) error { - cli := Client() - id, err := c.GetChangeID(cli) + chid, err := c.GetChangeID() if err != nil { + if err == noChangeFoundOK { + return nil + } return err } - return showChange(cli, id) + return c.showChange(chid) } -func showChange(cli *client.Client, chid string) error { +func queryChange(cli *client.Client, chid string) (*client.Change, error) { chg, err := cli.Change(chid) if err != nil { + return nil, err + } + if err := warnMaintenance(cli); err != nil { + return nil, err + } + return chg, nil +} + +func (c *cmdTasks) showChange(chid string) error { + chg, err := queryChange(c.client, chid) + if err != nil { return err } @@ -133,8 +163,8 @@ fmt.Fprintf(w, i18n.G("Status\tSpawn\tReady\tSummary\n")) for _, t := range chg.Tasks { - spawnTime := t.SpawnTime.UTC().Format(time.RFC3339) - readyTime := t.ReadyTime.UTC().Format(time.RFC3339) + spawnTime := c.fmtTime(t.SpawnTime) + readyTime := c.fmtTime(t.ReadyTime) if t.ReadyTime.IsZero() { readyTime = "-" } @@ -166,3 +196,14 @@ } const line = "......................................................................" + +func warnMaintenance(cli *client.Client) error { + if maintErr := cli.Maintenance(); maintErr != nil { + msg, err := errorToCmdMessage("", maintErr, nil) + if err != nil { + return err + } + fmt.Fprintf(Stderr, "WARNING: %s\n", msg) + } + return nil +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_changes_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_changes_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_changes_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_changes_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -22,6 +22,7 @@ import ( "fmt" "net/http" + "strings" "gopkg.in/check.v1" @@ -55,19 +56,38 @@ expectedChange := `(?ms)Status +Spawn +Ready +Summary Do +2016-04-21T01:02:03Z +2016-04-21T01:02:04Z +some summary ` - rest, err := snap.Parser().ParseArgs([]string{"change", "42"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"change", "--abs-time", "42"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, expectedChange) c.Check(s.Stderr(), check.Equals, "") - rest, err = snap.Parser().ParseArgs([]string{"tasks", "42"}) + rest, err = snap.Parser(snap.Client()).ParseArgs([]string{"tasks", "--abs-time", "42"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, expectedChange) c.Check(s.Stderr(), check.Equals, "") } +func (s *SnapSuite) TestChangeSimpleRebooting(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + if n < 2 { + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, strings.Replace(mockChangeJSON, `"type": "sync"`, `"type": "sync", "maintenance": {"kind": "system-restart", "message": "system is restarting"}`, 1)) + } else { + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"change", "42"}) + c.Assert(err, check.IsNil) + c.Check(s.Stderr(), check.Equals, "WARNING: snapd is about to reboot the system\n") +} + var mockChangesJSON = `{"type": "sync", "result": [ { "id": "four", @@ -124,23 +144,56 @@ expectedChange := `(?ms)Status +Spawn +Ready +Summary Do +2016-04-21T01:02:03Z +2016-04-21T01:02:04Z +some summary ` - rest, err := snap.Parser().ParseArgs([]string{"tasks", "--last=install"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"tasks", "--abs-time", "--last=install"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, expectedChange) c.Check(s.Stderr(), check.Equals, "") - _, err = snap.Parser().ParseArgs([]string{"tasks", "--last=foobar"}) + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"tasks", "--abs-time", "--last=foobar"}) c.Assert(err, check.NotNil) c.Assert(err, check.ErrorMatches, `no changes of type "foobar" found`) } +func (s *SnapSuite) TestTasksLastQuestionmark(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + c.Check(r.Method, check.Equals, "GET") + c.Assert(r.URL.Path, check.Equals, "/v2/changes") + switch n { + case 1, 2: + fmt.Fprintln(w, `{"type": "sync", "result": []}`) + case 3, 4: + fmt.Fprintln(w, mockChangesJSON) + default: + c.Errorf("expected 4 calls, now on %d", n) + } + }) + for i := 0; i < 2; i++ { + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"tasks", "--last=foobar?"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, "") + c.Check(s.Stderr(), check.Equals, "") + + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"tasks", "--last=foobar"}) + if i == 0 { + c.Assert(err, check.ErrorMatches, `no changes found`) + } else { + c.Assert(err, check.ErrorMatches, `no changes of type "foobar" found`) + } + } + + c.Check(n, check.Equals, 4) +} + func (s *SnapSuite) TestTasksSyntaxError(c *check.C) { - _, err := snap.Parser().ParseArgs([]string{"tasks", "--last=install", "42"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"tasks", "--abs-time", "--last=install", "42"}) c.Assert(err, check.NotNil) c.Assert(err, check.ErrorMatches, `cannot use change ID and type together`) - _, err = snap.Parser().ParseArgs([]string{"tasks"}) + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"tasks"}) c.Assert(err, check.NotNil) c.Assert(err, check.ErrorMatches, `please provide change ID or type with --last=`) } @@ -170,7 +223,7 @@ n++ }) - rest, err := snap.Parser().ParseArgs([]string{"change", "42"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"change", "--abs-time", "42"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?ms)Status +Spawn +Ready +Summary diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_confinement.go snapd-2.37~rc1~14.04/cmd/snap/cmd_confinement.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_confinement.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_confinement.go 2019-01-16 08:36:51.000000000 +0000 @@ -27,16 +27,20 @@ "github.com/jessevdk/go-flags" ) -var shortConfinementHelp = i18n.G("Prints the confinement mode the system operates in") +var shortConfinementHelp = i18n.G("Print the confinement mode the system operates in") var longConfinementHelp = i18n.G(` -The confinement command will print the confinement mode (strict, partial or none) -the system operates in. +The confinement command will print the confinement mode (strict, +partial or none) the system operates in. `) -type cmdConfinement struct{} +type cmdConfinement struct { + clientMixin +} func init() { - addDebugCommand("confinement", shortConfinementHelp, longConfinementHelp, func() flags.Commander { return &cmdConfinement{} }) + addDebugCommand("confinement", shortConfinementHelp, longConfinementHelp, func() flags.Commander { + return &cmdConfinement{} + }, nil, nil) } func (cmd cmdConfinement) Execute(args []string) error { @@ -44,8 +48,7 @@ return ErrExtraArgs } - cli := Client() - sysInfo, err := cli.SysInfo() + sysInfo, err := cmd.client.SysInfo() if err != nil { return err } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_confinement_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_confinement_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_confinement_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_confinement_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -32,7 +32,7 @@ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"type": "sync", "result": {"confinement": "strict"}}`) }) - _, err := snap.Parser().ParseArgs([]string{"debug", "confinement"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "confinement"}) c.Assert(err, IsNil) c.Assert(s.Stdout(), Equals, "strict\n") c.Assert(s.Stderr(), Equals, "") diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_connect.go snapd-2.37~rc1~14.04/cmd/snap/cmd_connect.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_connect.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_connect.go 2019-01-16 08:36:51.000000000 +0000 @@ -26,13 +26,14 @@ ) type cmdConnect struct { + waitMixin Positionals struct { PlugSpec connectPlugSpec `required:"yes"` SlotSpec connectSlotSpec } `positional-args:"true"` } -var shortConnectHelp = i18n.G("Connects a plug to a slot") +var shortConnectHelp = i18n.G("Connect a plug to a slot") var longConnectHelp = i18n.G(` The connect command connects a plug to a slot. It may be called in the following ways: @@ -56,10 +57,10 @@ func init() { addCommand("connect", shortConnectHelp, longConnectHelp, func() flags.Commander { return &cmdConnect{} - }, nil, []argDesc{ - // TRANSLATORS: This needs to be wrapped in <>s. + }, waitDescs, []argDesc{ + // TRANSLATORS: This needs to begin with < and end with > {name: i18n.G(":")}, - // TRANSLATORS: This needs to be wrapped in <>s. + // TRANSLATORS: This needs to begin with < and end with > {name: i18n.G(":")}, }) } @@ -76,12 +77,17 @@ x.Positionals.PlugSpec.Snap = "" } - cli := Client() - id, err := cli.Connect(x.Positionals.PlugSpec.Snap, x.Positionals.PlugSpec.Name, x.Positionals.SlotSpec.Snap, x.Positionals.SlotSpec.Name) + id, err := x.client.Connect(x.Positionals.PlugSpec.Snap, x.Positionals.PlugSpec.Name, x.Positionals.SlotSpec.Snap, x.Positionals.SlotSpec.Name) if err != nil { return err } - _, err = wait(cli, id) - return err + if _, err := x.wait(id); err != nil { + if err == noWait { + return nil + } + return err + } + + return nil } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_connectivity_check.go snapd-2.37~rc1~14.04/cmd/snap/cmd_connectivity_check.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_connectivity_check.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_connectivity_check.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,64 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "fmt" + + "github.com/jessevdk/go-flags" +) + +type cmdConnectivityCheck struct { + clientMixin +} + +func init() { + addDebugCommand("connectivity", + "Check network connectivity status", + "The connectivity command checks the network connectivity of snapd.", + func() flags.Commander { + return &cmdConnectivityCheck{} + }, nil, nil) +} + +func (x *cmdConnectivityCheck) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + var status struct { + Connectivity bool + Unreachable []string + } + if err := x.client.Debug("connectivity", nil, &status); err != nil { + return err + } + + fmt.Fprintf(Stdout, "Connectivity status:\n") + if len(status.Unreachable) == 0 { + fmt.Fprintf(Stdout, " * PASS\n") + return nil + } + + for _, uri := range status.Unreachable { + fmt.Fprintf(Stdout, " * %s: unreachable\n", uri) + } + return fmt.Errorf("%v servers unreachable", len(status.Unreachable)) +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_connectivity_check_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_connectivity_check_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_connectivity_check_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_connectivity_check_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,84 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "net/http" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestConnectivityHappy(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/debug") + c.Check(r.URL.RawQuery, check.Equals, "") + data, err := ioutil.ReadAll(r.Body) + c.Check(err, check.IsNil) + c.Check(data, check.DeepEquals, []byte(`{"action":"connectivity"}`)) + fmt.Fprintln(w, `{"type": "sync", "result": {}}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "connectivity"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `Connectivity status: + * PASS +`) + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestConnectivityUnhappy(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/debug") + c.Check(r.URL.RawQuery, check.Equals, "") + data, err := ioutil.ReadAll(r.Body) + c.Check(err, check.IsNil) + c.Check(data, check.DeepEquals, []byte(`{"action":"connectivity"}`)) + fmt.Fprintln(w, `{"type": "sync", "result": {"connectivity":false,"unreachable":["foo.bar.com"]}}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "connectivity"}) + c.Assert(err, check.ErrorMatches, "1 servers unreachable") + // note that only the unreachable hosts are displayed + c.Check(s.Stdout(), check.Equals, `Connectivity status: + * foo.bar.com: unreachable +`) + c.Check(s.Stderr(), check.Equals, "") +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_connect_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_connect_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_connect_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_connect_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -33,7 +33,7 @@ func (s *SnapSuite) TestConnectHelp(c *C) { msg := `Usage: - snap.test [OPTIONS] connect [:] [:] + snap.test connect [connect-OPTIONS] [:] [:] The connect command connects a plug to a slot. It may be called in the following ways: @@ -53,15 +53,11 @@ Connects the provided plug to the slot in the core snap with a name matching the plug name. -Application Options: - --version Print the version and exit - -Help Options: - -h, --help Show this help message +[connect command options] + --no-wait Do not wait for the operation to finish but just print + the change id. ` - rest, err := Parser().ParseArgs([]string{"connect", "--help"}) - c.Assert(err.Error(), Equals, msg) - c.Assert(rest, DeepEquals, []string{}) + s.testSubCommandHelp(c, "connect", msg) } func (s *SnapSuite) TestConnectExplicitEverything(c *C) { @@ -92,7 +88,7 @@ c.Fatalf("unexpected path %q", r.URL.Path) } }) - rest, err := Parser().ParseArgs([]string{"connect", "producer:plug", "consumer:slot"}) + rest, err := Parser(Client()).ParseArgs([]string{"connect", "producer:plug", "consumer:slot"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) } @@ -125,7 +121,7 @@ c.Fatalf("unexpected path %q", r.URL.Path) } }) - rest, err := Parser().ParseArgs([]string{"connect", "producer:plug", "consumer"}) + rest, err := Parser(Client()).ParseArgs([]string{"connect", "producer:plug", "consumer"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) } @@ -158,7 +154,7 @@ c.Fatalf("unexpected path %q", r.URL.Path) } }) - rest, err := Parser().ParseArgs([]string{"connect", "plug", "consumer:slot"}) + rest, err := Parser(Client()).ParseArgs([]string{"connect", "plug", "consumer:slot"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) } @@ -191,7 +187,7 @@ c.Fatalf("unexpected path %q", r.URL.Path) } }) - rest, err := Parser().ParseArgs([]string{"connect", "plug", "consumer"}) + rest, err := Parser(Client()).ParseArgs([]string{"connect", "plug", "consumer"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) } @@ -294,7 +290,7 @@ defer os.Unsetenv("GO_FLAGS_COMPLETION") expected := []flags.Completion{} - parser := Parser() + parser := Parser(Client()) parser.CompletionHandler = func(obtained []flags.Completion) { c.Check(obtained, DeepEquals, expected) } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_create_key.go snapd-2.37~rc1~14.04/cmd/snap/cmd_create_key.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_create_key.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_create_key.go 2019-01-16 08:36:51.000000000 +0000 @@ -39,13 +39,16 @@ func init() { cmd := addCommand("create-key", i18n.G("Create cryptographic key pair"), - i18n.G("Create a cryptographic key pair that can be used for signing assertions."), + i18n.G(` +The create-key command creates a cryptographic key pair that can be +used for signing assertions. +`), func() flags.Commander { return &cmdCreateKey{} }, nil, []argDesc{{ - // TRANSLATORS: This needs to be wrapped in <>s. + // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), - // TRANSLATORS: This should probably not start with a lowercase letter. + // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Name of key to create; defaults to 'default'"), }}) cmd.hidden = true diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_create_key_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_create_key_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_create_key_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_create_key_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -26,7 +26,7 @@ ) func (s *SnapSuite) TestCreateKeyInvalidCharacters(c *C) { - _, err := snap.Parser().ParseArgs([]string{"create-key", "a b"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-key", "a b"}) c.Assert(err, NotNil) c.Check(err.Error(), Equals, "key name \"a b\" is not valid; only ASCII letters, digits, and hyphens are allowed") c.Check(s.Stdout(), Equals, "") diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_create_user.go snapd-2.37~rc1~14.04/cmd/snap/cmd_create_user.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_create_user.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_create_user.go 2019-01-16 08:36:51.000000000 +0000 @@ -29,7 +29,7 @@ "github.com/jessevdk/go-flags" ) -var shortCreateUserHelp = i18n.G("Creates a local system user") +var shortCreateUserHelp = i18n.G("Create a local system user") var longCreateUserHelp = i18n.G(` The create-user command creates a local system user with the username and SSH keys registered on the store account identified by the provided email address. @@ -38,6 +38,7 @@ `) type cmdCreateUser struct { + clientMixin Positional struct { Email string } `positional-args:"yes"` @@ -51,14 +52,18 @@ func init() { cmd := addCommand("create-user", shortCreateUserHelp, longCreateUserHelp, func() flags.Commander { return &cmdCreateUser{} }, map[string]string{ - "json": i18n.G("Output results in JSON format"), - "sudoer": i18n.G("Grant sudo access to the created user"), - "known": i18n.G("Use known assertions for user creation"), + // TRANSLATORS: This should not start with a lowercase letter. + "json": i18n.G("Output results in JSON format"), + // TRANSLATORS: This should not start with a lowercase letter. + "sudoer": i18n.G("Grant sudo access to the created user"), + // TRANSLATORS: This should not start with a lowercase letter. + "known": i18n.G("Use known assertions for user creation"), + // TRANSLATORS: This should not start with a lowercase letter. "force-managed": i18n.G("Force adding the user, even if the device is already managed"), }, []argDesc{{ - // TRANSLATORS: This is a noun, and it needs to be wrapped in <>s. + // TRANSLATORS: This is a noun and it needs to begin with < and end with > name: i18n.G(""), - // TRANSLATORS: This should probably not start with a lowercase letter. Also, note users on login.ubuntu.com can have multiple email addresses. + // TRANSLATORS: This should not start with a lowercase letter (unless it's "login.ubuntu.com"). Also, note users on login.ubuntu.com can have multiple email addresses. desc: i18n.G("An email of a user on login.ubuntu.com"), }}) cmd.hidden = true @@ -69,8 +74,6 @@ return ErrExtraArgs } - cli := Client() - options := client.CreateUserOptions{ Email: x.Positional.Email, Sudoer: x.Sudoer, @@ -83,9 +86,9 @@ var err error if options.Email == "" && options.Known { - results, err = cli.CreateUsers([]*client.CreateUserOptions{&options}) + results, err = x.client.CreateUsers([]*client.CreateUserOptions{&options}) } else { - result, err = cli.CreateUser(&options) + result, err = x.client.CreateUser(&options) if err == nil { results = append(results, result) } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_create_user_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_create_user_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_create_user_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_create_user_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -71,7 +71,7 @@ n := 0 s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "one@email.com", false, false)) - rest, err := snap.Parser().ParseArgs([]string{"create-user", "one@email.com"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-user", "one@email.com"}) c.Assert(err, check.IsNil) c.Check(rest, check.DeepEquals, []string{}) c.Check(n, check.Equals, 1) @@ -83,7 +83,7 @@ n := 0 s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "one@email.com", true, false)) - rest, err := snap.Parser().ParseArgs([]string{"create-user", "--sudoer", "one@email.com"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-user", "--sudoer", "one@email.com"}) c.Assert(err, check.IsNil) c.Check(rest, check.DeepEquals, []string{}) c.Check(n, check.Equals, 1) @@ -101,7 +101,7 @@ } actualResponse := &client.CreateUserResult{} - rest, err := snap.Parser().ParseArgs([]string{"create-user", "--json", "one@email.com"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-user", "--json", "one@email.com"}) c.Assert(err, check.IsNil) c.Check(rest, check.DeepEquals, []string{}) c.Check(n, check.Equals, 1) @@ -120,7 +120,7 @@ }} var actualResponse []*client.CreateUserResult - rest, err := snap.Parser().ParseArgs([]string{"create-user", "--json", "--known"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-user", "--json", "--known"}) c.Assert(err, check.IsNil) c.Check(rest, check.DeepEquals, []string{}) c.Check(n, check.Equals, 1) @@ -133,7 +133,7 @@ n := 0 s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "one@email.com", false, true)) - rest, err := snap.Parser().ParseArgs([]string{"create-user", "--known", "one@email.com"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-user", "--known", "one@email.com"}) c.Assert(err, check.IsNil) c.Check(rest, check.DeepEquals, []string{}) c.Check(n, check.Equals, 1) @@ -143,7 +143,7 @@ n := 0 s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "", false, true)) - rest, err := snap.Parser().ParseArgs([]string{"create-user", "--known"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-user", "--known"}) c.Assert(err, check.IsNil) c.Check(rest, check.DeepEquals, []string{}) c.Check(n, check.Equals, 1) diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_debug.go snapd-2.37~rc1~14.04/cmd/snap/cmd_debug.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_debug.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_debug.go 2019-01-16 08:36:51.000000000 +0000 @@ -25,7 +25,7 @@ type cmdDebug struct{} -var shortDebugHelp = i18n.G("Runs debug commands") +var shortDebugHelp = i18n.G("Run debug commands") var longDebugHelp = i18n.G(` The debug command contains a selection of additional sub-commands. diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_delete_key.go snapd-2.37~rc1~14.04/cmd/snap/cmd_delete_key.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_delete_key.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_delete_key.go 2019-01-16 08:36:51.000000000 +0000 @@ -35,13 +35,16 @@ func init() { cmd := addCommand("delete-key", i18n.G("Delete cryptographic key pair"), - i18n.G("Delete the local cryptographic key pair with the given name."), + i18n.G(` +The delete-key command deletes the local cryptographic key pair with +the given name. +`), func() flags.Commander { return &cmdDeleteKey{} }, nil, []argDesc{{ - // TRANSLATORS: This needs to be wrapped in <>s. + // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), - // TRANSLATORS: This should probably not start with a lowercase letter. + // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Name of key to delete"), }}) cmd.hidden = true diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_delete_key_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_delete_key_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_delete_key_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_delete_key_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -28,7 +28,7 @@ ) func (s *SnapKeysSuite) TestDeleteKeyRequiresName(c *C) { - _, err := snap.Parser().ParseArgs([]string{"delete-key"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"delete-key"}) c.Assert(err, NotNil) c.Check(err.Error(), Equals, "the required argument `` was not provided") c.Check(s.Stdout(), Equals, "") @@ -36,7 +36,7 @@ } func (s *SnapKeysSuite) TestDeleteKeyNonexistent(c *C) { - _, err := snap.Parser().ParseArgs([]string{"delete-key", "nonexistent"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"delete-key", "nonexistent"}) c.Assert(err, NotNil) c.Check(err.Error(), Equals, "cannot find key named \"nonexistent\" in GPG keyring") c.Check(s.Stdout(), Equals, "") @@ -44,12 +44,12 @@ } func (s *SnapKeysSuite) TestDeleteKey(c *C) { - rest, err := snap.Parser().ParseArgs([]string{"delete-key", "another"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"delete-key", "another"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Check(s.Stdout(), Equals, "") c.Check(s.Stderr(), Equals, "") - _, err = snap.Parser().ParseArgs([]string{"keys", "--json"}) + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"keys", "--json"}) c.Assert(err, IsNil) expectedResponse := []snap.Key{ { diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_disconnect.go snapd-2.37~rc1~14.04/cmd/snap/cmd_disconnect.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_disconnect.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_disconnect.go 2019-01-16 08:36:51.000000000 +0000 @@ -29,13 +29,14 @@ ) type cmdDisconnect struct { + waitMixin Positionals struct { Offer disconnectSlotOrPlugSpec `required:"true"` Use disconnectSlotSpec } `positional-args:"true"` } -var shortDisconnectHelp = i18n.G("Disconnects a plug from a slot") +var shortDisconnectHelp = i18n.G("Disconnect a plug from a slot") var longDisconnectHelp = i18n.G(` The disconnect command disconnects a plug from a slot. It may be called in the following ways: @@ -53,10 +54,10 @@ func init() { addCommand("disconnect", shortDisconnectHelp, longDisconnectHelp, func() flags.Commander { return &cmdDisconnect{} - }, nil, []argDesc{ - // TRANSLATORS: This needs to be wrapped in <>s. + }, waitDescs, []argDesc{ + // TRANSLATORS: This needs to begin with < and end with > {name: i18n.G(":")}, - // TRANSLATORS: This needs to be wrapped in <>s. + // TRANSLATORS: This needs to begin with < and end with > {name: i18n.G(":")}, }) } @@ -79,8 +80,7 @@ return fmt.Errorf("please provide the plug or slot name to disconnect from snap %q", use.Snap) } - cli := Client() - id, err := cli.Disconnect(offer.Snap, offer.Name, use.Snap, use.Name) + id, err := x.client.Disconnect(offer.Snap, offer.Name, use.Snap, use.Name) if err != nil { if client.IsInterfacesUnchangedError(err) { fmt.Fprintf(Stdout, i18n.G("No connections to disconnect")) @@ -90,6 +90,12 @@ return err } - _, err = wait(cli, id) - return err + if _, err := x.wait(id); err != nil { + if err == noWait { + return nil + } + return err + } + + return nil } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_disconnect_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_disconnect_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_disconnect_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_disconnect_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -32,7 +32,7 @@ func (s *SnapSuite) TestDisconnectHelp(c *C) { msg := `Usage: - snap.test [OPTIONS] disconnect [:] [:] + snap.test disconnect [disconnect-OPTIONS] [:] [:] The disconnect command disconnects a plug from a slot. It may be called in the following ways: @@ -46,15 +46,11 @@ Disconnects everything from the provided plug or slot. The snap name may be omitted for the core snap. -Application Options: - --version Print the version and exit - -Help Options: - -h, --help Show this help message +[disconnect command options] + --no-wait Do not wait for the operation to finish but just print + the change id. ` - rest, err := Parser().ParseArgs([]string{"disconnect", "--help"}) - c.Assert(err.Error(), Equals, msg) - c.Assert(rest, DeepEquals, []string{}) + s.testSubCommandHelp(c, "disconnect", msg) } func (s *SnapSuite) TestDisconnectExplicitEverything(c *C) { @@ -85,7 +81,7 @@ c.Fatalf("unexpected path %q", r.URL.Path) } }) - rest, err := Parser().ParseArgs([]string{"disconnect", "producer:plug", "consumer:slot"}) + rest, err := Parser(Client()).ParseArgs([]string{"disconnect", "producer:plug", "consumer:slot"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Assert(s.Stdout(), Equals, "") @@ -120,7 +116,7 @@ c.Fatalf("unexpected path %q", r.URL.Path) } }) - rest, err := Parser().ParseArgs([]string{"disconnect", "consumer:slot"}) + rest, err := Parser(Client()).ParseArgs([]string{"disconnect", "consumer:slot"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Assert(s.Stdout(), Equals, "") @@ -155,7 +151,7 @@ c.Fatalf("unexpected path %q", r.URL.Path) } }) - rest, err := Parser().ParseArgs([]string{"disconnect", "consumer:plug-or-slot"}) + rest, err := Parser(Client()).ParseArgs([]string{"disconnect", "consumer:plug-or-slot"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Assert(s.Stdout(), Equals, "") @@ -166,7 +162,7 @@ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Fatalf("expected nothing to reach the server") }) - rest, err := Parser().ParseArgs([]string{"disconnect", "consumer"}) + rest, err := Parser(Client()).ParseArgs([]string{"disconnect", "consumer"}) c.Assert(err, ErrorMatches, `please provide the plug or slot name to disconnect from snap "consumer"`) c.Assert(rest, DeepEquals, []string{"consumer"}) c.Assert(s.Stdout(), Equals, "") @@ -190,7 +186,7 @@ defer os.Unsetenv("GO_FLAGS_COMPLETION") expected := []flags.Completion{} - parser := Parser() + parser := Parser(Client()) parser.CompletionHandler = func(obtained []flags.Completion) { c.Check(obtained, DeepEquals, expected) } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_download.go snapd-2.37~rc1~14.04/cmd/snap/cmd_download.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_download.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_download.go 2019-01-16 08:36:51.000000000 +0000 @@ -43,10 +43,10 @@ } `positional-args:"true" required:"true"` } -var shortDownloadHelp = i18n.G("Downloads the given snap") +var shortDownloadHelp = i18n.G("Download the given snap") var longDownloadHelp = i18n.G(` The download command downloads the given snap and its supporting assertions -to the current directory under .snap and .assert file extensions, respectively. +to the current directory with .snap and .assert file extensions, respectively. `) func init() { @@ -56,7 +56,7 @@ "revision": i18n.G("Download the given revision of a snap, to which you must have developer access"), }), []argDesc{{ name: "", - // TRANSLATORS: This should probably not start with a lowercase letter. + // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Snap name"), }}) } @@ -100,6 +100,9 @@ if x.Revision == "" { revision = snap.R(0) } else { + if x.Channel != "" { + return fmt.Errorf(i18n.G("cannot specify both channel and revision")) + } var err error revision, err = snap.ParseRevision(x.Revision) if err != nil { diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_ensure_state_soon.go snapd-2.37~rc1~14.04/cmd/snap/cmd_ensure_state_soon.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_ensure_state_soon.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_ensure_state_soon.go 2019-01-16 08:36:51.000000000 +0000 @@ -23,15 +23,17 @@ "github.com/jessevdk/go-flags" ) -type cmdEnsureStateSoon struct{} +type cmdEnsureStateSoon struct { + clientMixin +} func init() { cmd := addDebugCommand("ensure-state-soon", - "(internal) trigger an ensure runn in the state engine", - "(internal) trigger an ensure runn in the state engine", + "(internal) trigger an ensure run in the state engine", + "(internal) trigger an ensure run in the state engine", func() flags.Commander { return &cmdEnsureStateSoon{} - }) + }, nil, nil) cmd.hidden = true } @@ -40,5 +42,5 @@ return ErrExtraArgs } - return Client().Debug("ensure-state-soon", nil, nil) + return x.client.Debug("ensure-state-soon", nil, nil) } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_ensure_state_soon_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_ensure_state_soon_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_ensure_state_soon_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_ensure_state_soon_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -47,7 +47,7 @@ n++ }) - rest, err := snap.Parser().ParseArgs([]string{"debug", "ensure-state-soon"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "ensure-state-soon"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, "") diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_export_key.go snapd-2.37~rc1~14.04/cmd/snap/cmd_export_key.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_export_key.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_export_key.go 2019-01-16 08:36:51.000000000 +0000 @@ -39,15 +39,18 @@ func init() { cmd := addCommand("export-key", i18n.G("Export cryptographic public key"), - i18n.G("Export a public key assertion body that may be imported by other systems."), + i18n.G(` +The export-key command exports a public key assertion body that may be +imported by other systems. +`), func() flags.Commander { return &cmdExportKey{} }, map[string]string{ "account": i18n.G("Format public key material as a request for an account-key for this account-id"), }, []argDesc{{ - // TRANSLATORS: This needs to be wrapped in <>s. + // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), - // TRANSLATORS: This should probably not start with a lowercase letter. + // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Name of key to export"), }}) cmd.hidden = true diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_export_key_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_export_key_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_export_key_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_export_key_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -30,7 +30,7 @@ ) func (s *SnapKeysSuite) TestExportKeyNonexistent(c *C) { - _, err := snap.Parser().ParseArgs([]string{"export-key", "nonexistent"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"export-key", "nonexistent"}) c.Assert(err, NotNil) c.Check(err.Error(), Equals, "cannot find key named \"nonexistent\" in GPG keyring") c.Check(s.Stdout(), Equals, "") @@ -38,7 +38,7 @@ } func (s *SnapKeysSuite) TestExportKeyDefault(c *C) { - rest, err := snap.Parser().ParseArgs([]string{"export-key"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"export-key"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) pubKey, err := asserts.DecodePublicKey(s.stdout.Bytes()) @@ -48,7 +48,7 @@ } func (s *SnapKeysSuite) TestExportKeyNonDefault(c *C) { - rest, err := snap.Parser().ParseArgs([]string{"export-key", "another"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"export-key", "another"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) pubKey, err := asserts.DecodePublicKey(s.stdout.Bytes()) @@ -61,7 +61,7 @@ storeSigning := assertstest.NewStoreStack("canonical", nil) manager := asserts.NewGPGKeypairManager() assertstest.NewAccount(storeSigning, "developer1", nil, "") - rest, err := snap.Parser().ParseArgs([]string{"export-key", "another", "--account=developer1"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"export-key", "another", "--account=developer1"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) assertion, err := asserts.Decode(s.stdout.Bytes()) diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_find.go snapd-2.37~rc1~14.04/cmd/snap/cmd_find.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_find.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_find.go 2019-01-16 08:36:51.000000000 +0000 @@ -36,9 +36,17 @@ "github.com/snapcore/snapd/strutil" ) -var shortFindHelp = i18n.G("Finds packages to install") +var shortFindHelp = i18n.G("Find packages to install") var longFindHelp = i18n.G(` The find command queries the store for available packages in the stable channel. + +With the --private flag, which requires the user to be logged-in to the store +(see 'snap help login'), it instead searches for private snaps that the user +has developer access to, either directly or through the store's collaboration +feature. + +A green check mark (given color and unicode support) after a publisher name +indicates that the publisher has been verified. `) func getPrice(prices map[string]float64, currency string) (float64, string, error) { @@ -78,7 +86,7 @@ return ret } - cli := Client() + cli := mkClient() sections, err := cli.Sections() if err != nil { return nil @@ -92,28 +100,42 @@ return ret } -func getSections() (sections []string, err error) { - if cachedSections, err := os.Open(dirs.SnapSectionsFile); err == nil { - // try loading from cached sections file - defer cachedSections.Close() - - r := bufio.NewScanner(cachedSections) - sections = make([]string, 0, 10) - for r.Scan() { - sections = append(sections, r.Text()) - } - if r.Err() == nil && len(sections) > 0 { - return sections, nil +func cachedSections() (sections []string, err error) { + cachedSections, err := os.Open(dirs.SnapSectionsFile) + if err != nil { + if os.IsNotExist(err) { + return nil, nil } + return nil, err + } + defer cachedSections.Close() + + r := bufio.NewScanner(cachedSections) + for r.Scan() { + sections = append(sections, r.Text()) + } + if r.Err() != nil { + return nil, r.Err() } + return sections, nil +} + +func getSections(cli *client.Client) (sections []string, err error) { + // try loading from cached sections file + sections, err = cachedSections() + if err != nil { + return nil, err + } + if sections != nil { + return sections, nil + } // fallback to listing from the daemon - cli := Client() return cli.Sections() } -func showSections() error { - sections, err := getSections() +func showSections(cli *client.Client) error { + sections, err := getSections(cli) if err != nil { return err } @@ -123,28 +145,36 @@ for _, sec := range sections { fmt.Fprintf(Stdout, " * %s\n", sec) } - fmt.Fprintf(Stdout, i18n.G("Please try: snap find --section=\n")) + fmt.Fprintf(Stdout, i18n.G("Please try 'snap find --section='\n")) return nil } type cmdFind struct { + clientMixin Private bool `long:"private"` + Narrow bool `long:"narrow"` Section SectionName `long:"section" optional:"true" optional-value:"show-all-sections-please" default:"no-section-specified"` Positional struct { Query string } `positional-args:"yes"` + colorMixin } func init() { addCommand("find", shortFindHelp, longFindHelp, func() flags.Commander { return &cmdFind{} - }, map[string]string{ + }, colorDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. "private": i18n.G("Search private snaps"), + // TRANSLATORS: This should not start with a lowercase letter. + "narrow": i18n.G("Only search for snaps in “stable”"), + // TRANSLATORS: This should not start with a lowercase letter. "section": i18n.G("Restrict the search to a given section"), - }, []argDesc{{ - // TRANSLATORS: This needs to be wrapped in <>s. + }), []argDesc{{ + // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), }}).alias = "search" + } func (x *cmdFind) Execute(args []string) error { @@ -164,7 +194,7 @@ // the commandline at all switch x.Section { case "show-all-sections-please": - return showSections() + return showSections(x.client) case "no-section-specified": x.Section = "" } @@ -175,16 +205,21 @@ x.Section = "featured" } - cli := Client() - if x.Section != "" && x.Section != "featured" { - sections, err := getSections() + sections, err := cachedSections() if err != nil { return err } if !strutil.ListContains(sections, string(x.Section)) { - // TRANSLATORS: the %q is the (quoted) name of the section the user entered - return fmt.Errorf(i18n.G("No matching section %q, use --section to list existing sections"), x.Section) + // try the store just in case it was added in the last 24 hours + sections, err = x.client.Sections() + if err != nil { + return err + } + if !strutil.ListContains(sections, string(x.Section)) { + // TRANSLATORS: the %q is the (quoted) name of the section the user entered + return fmt.Errorf(i18n.G("No matching section %q, use --section to list existing sections"), x.Section) + } } } @@ -193,8 +228,13 @@ Section: string(x.Section), Query: x.Positional.Query, } - snaps, resInfo, err := cli.Find(opts) - if e, ok := err.(*client.Error); ok && e.Kind == client.ErrorKindNetworkTimeout { + + if !x.Narrow { + opts.Scope = "wide" + } + + snaps, resInfo, err := x.client.Find(opts) + if e, ok := err.(*client.Error); ok && (e.Kind == client.ErrorKindNetworkTimeout || e.Kind == client.ErrorKindDNSFailure) { logger.Debugf("cannot list snaps: %v", e) return fmt.Errorf("unable to contact snap store") } @@ -216,19 +256,19 @@ // show featured header *after* we checked for errors from the find if showFeatured { - fmt.Fprintf(Stdout, i18n.G("No search term specified. Here are some interesting snaps:\n\n")) + fmt.Fprint(Stdout, i18n.G("No search term specified. Here are some interesting snaps:\n\n")) } + esc := x.getEscapes() w := tabWriter() - fmt.Fprintln(w, i18n.G("Name\tVersion\tDeveloper\tNotes\tSummary")) + // TRANSLATORS: the %s is to insert a filler escape sequence (please keep it flush to the column header, with no extra spaces) + fmt.Fprintf(w, i18n.G("Name\tVersion\tPublisher%s\tNotes\tSummary\n"), fillerPublisher(esc)) for _, snap := range snaps { - // TODO: get snap.Publisher, so we can only show snap.Developer if it's different - fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", snap.Name, snap.Version, snap.Developer, NotesFromRemote(snap, resInfo), snap.Summary) + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", snap.Name, snap.Version, shortPublisher(esc, snap.Publisher), NotesFromRemote(snap, resInfo), snap.Summary) } w.Flush() - if showFeatured { - fmt.Fprintf(Stdout, i18n.G("\nProvide a search term for more specific results.\n")) + fmt.Fprint(Stdout, i18n.G("\nProvide a search term for more specific results.\n")) } return nil } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_find_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_find_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_find_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_find_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -44,6 +44,12 @@ "confinement": "strict", "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", "developer": "canonical", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, "download-size": 65536, "icon": "", "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", @@ -61,6 +67,12 @@ "confinement": "strict", "description": "This is a simple hello world example.", "developer": "canonical", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, "download-size": 20480, "icon": "", "id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", @@ -78,6 +90,12 @@ "confinement": "strict", "description": "1.0GB", "developer": "noise", + "publisher": { + "id": "noise-id", + "username": "noise", + "display-name": "Bret", + "validation": "unproven" + }, "download-size": 512004096, "icon": "", "id": "asXOGCreK66DIAdyXmucwspTMgqA4rne", @@ -119,14 +137,14 @@ n++ }) - rest, err := snap.Parser().ParseArgs([]string{"find", "hello"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "hello"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `Name +Version +Developer +Notes +Summary -hello +2.10 +canonical +- +GNU Hello, the "hello world" snap -hello-world +6.1 +canonical +- +Hello world example + c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +Notes +Summary +hello +2.10 +canonical\* +- +GNU Hello, the "hello world" snap +hello-world +6.1 +canonical\* +- +Hello world example hello-huge +1.0 +noise +- +a really big snap `) c.Check(s.Stderr(), check.Equals, "") @@ -145,6 +163,12 @@ "confinement": "strict", "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", "developer": "canonical", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, "download-size": 65536, "icon": "", "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", @@ -162,6 +186,12 @@ "confinement": "strict", "description": "1.0GB", "developer": "noise", + "publisher": { + "id": "noise-id", + "username": "noise", + "display-name": "Bret", + "validation": "unproven" + }, "download-size": 512004096, "icon": "", "id": "asXOGCreK66DIAdyXmucwspTMgqA4rne", @@ -190,7 +220,9 @@ c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/find") q := r.URL.Query() + c.Check(q, check.HasLen, 2) c.Check(q.Get("q"), check.Equals, "hello") + c.Check(q.Get("scope"), check.Equals, "wide") fmt.Fprintln(w, findHelloJSON) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) @@ -198,11 +230,38 @@ n++ }) - rest, err := snap.Parser().ParseArgs([]string{"find", "hello"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "hello"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `Name +Version +Developer +Notes +Summary -hello +2.10 +canonical +- +GNU Hello, the "hello world" snap + c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +Notes +Summary +hello +2.10 +canonical\* +- +GNU Hello, the "hello world" snap +hello-huge +1.0 +noise +- +a really big snap +`) + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestFindHelloNarrow(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + q := r.URL.Query() + c.Check(q, check.HasLen, 1) + c.Check(q.Get("q"), check.Equals, "hello") + fmt.Fprintln(w, findHelloJSON) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "--narrow", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +Notes +Summary +hello +2.10 +canonical\* +- +GNU Hello, the "hello world" snap hello-huge +1.0 +noise +- +a really big snap `) c.Check(s.Stderr(), check.Equals, "") @@ -219,6 +278,12 @@ "confinement": "strict", "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", "developer": "canonical", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, "download-size": 65536, "icon": "", "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", @@ -255,11 +320,11 @@ n++ }) - rest, err := snap.Parser().ParseArgs([]string{"find", "hello"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "hello"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `Name +Version +Developer +Notes +Summary -hello +2.10 +canonical +1.99GBP +GNU Hello, the "hello world" snap + c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +Notes +Summary +hello +2.10 +canonical\* +1.99GBP +GNU Hello, the "hello world" snap `) c.Check(s.Stderr(), check.Equals, "") } @@ -275,6 +340,12 @@ "confinement": "strict", "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", "developer": "canonical", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, "download-size": 65536, "icon": "", "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", @@ -310,11 +381,11 @@ n++ }) - rest, err := snap.Parser().ParseArgs([]string{"find", "hello"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "hello"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `Name +Version +Developer +Notes +Summary -hello +2.10 +canonical +bought +GNU Hello, the "hello world" snap + c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +Notes +Summary +hello +2.10 +canonical\* +bought +GNU Hello, the "hello world" snap `) c.Check(s.Stderr(), check.Equals, "") } @@ -334,7 +405,7 @@ n++ }) - _, err := snap.Parser().ParseArgs([]string{"find"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"find"}) c.Assert(err, check.IsNil) c.Check(s.Stderr(), check.Equals, "") c.Check(n, check.Equals, 1) @@ -392,7 +463,7 @@ n++ }) - _, err := snap.Parser().ParseArgs([]string{"find", "hello"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "hello"}) c.Assert(err, check.ErrorMatches, `unable to contact snap store`) c.Check(s.Stdout(), check.Equals, "") } @@ -414,7 +485,7 @@ n++ }) - rest, err := snap.Parser().ParseArgs([]string{"find", "--section"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "--section"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) @@ -422,7 +493,7 @@ c.Check(s.Stdout(), check.Equals, `No section specified. Available sections: * sec1 * sec2 -Please try: snap find --section= +Please try 'snap find --section=' `) c.Check(s.Stderr(), check.Equals, "") @@ -446,7 +517,7 @@ n++ }) - _, err := snap.Parser().ParseArgs([]string{"find", "--section=foobar", "hello"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "--section=foobar", "hello"}) c.Assert(err, check.ErrorMatches, `No matching section "foobar", use --section to list existing sections`) } @@ -477,7 +548,7 @@ n++ }) - _, err := snap.Parser().ParseArgs([]string{"find", "--section=foobar", "hello"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "--section=foobar", "hello"}) c.Assert(err, check.IsNil) c.Check(s.Stderr(), check.Equals, "No matching snaps for \"hello\" in section \"foobar\"\n") c.Check(s.Stdout(), check.Equals, "") @@ -486,20 +557,27 @@ } func (s *SnapSuite) TestFindSnapCachedSection(c *check.C) { + numHits := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { - c.Fatalf("not expecting any requests") + numHits++ + c.Check(numHits, check.Equals, 1) + c.Check(r.URL.Path, check.Equals, "/v2/sections") + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": []string{"sec1", "sec2", "sec3"}, + }) }) os.MkdirAll(path.Dir(dirs.SnapSectionsFile), 0755) ioutil.WriteFile(dirs.SnapSectionsFile, []byte("sec1\nsec2\nsec3"), 0644) - _, err := snap.Parser().ParseArgs([]string{"find", "--section=foobar", "hello"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "--section=foobar", "hello"}) c.Logf("stdout: %s", s.Stdout()) c.Assert(err, check.ErrorMatches, `No matching section "foobar", use --section to list existing sections`) s.ResetStdStreams() - rest, err := snap.Parser().ParseArgs([]string{"find", "--section"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "--section"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) @@ -508,8 +586,9 @@ * sec1 * sec2 * sec3 -Please try: snap find --section= +Please try 'snap find --section=' `) s.ResetStdStreams() + c.Check(numHits, check.Equals, 1) } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_first_boot.go snapd-2.37~rc1~14.04/cmd/snap/cmd_first_boot.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_first_boot.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_first_boot.go 2019-01-16 08:36:51.000000000 +0000 @@ -29,8 +29,9 @@ func init() { cmd := addCommand("firstboot", - "internal", - "internal", func() flags.Commander { + "Internal", + "The firstboot command is only retained for backwards compatibility.", + func() flags.Commander { return &cmdInternalFirstBoot{} }, nil, nil) cmd.hidden = true diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_get_base_declaration.go snapd-2.37~rc1~14.04/cmd/snap/cmd_get_base_declaration.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_get_base_declaration.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_get_base_declaration.go 2019-01-16 08:36:51.000000000 +0000 @@ -25,15 +25,18 @@ "github.com/jessevdk/go-flags" ) -type cmdGetBaseDeclaration struct{} +type cmdGetBaseDeclaration struct { + clientMixin +} func init() { - addDebugCommand("get-base-declaration", + cmd := addDebugCommand("get-base-declaration", "(internal) obtain the base declaration for all interfaces", "(internal) obtain the base declaration for all interfaces", func() flags.Commander { return &cmdGetBaseDeclaration{} - }) + }, nil, nil) + cmd.hidden = true } func (x *cmdGetBaseDeclaration) Execute(args []string) error { @@ -43,7 +46,7 @@ var resp struct { BaseDeclaration string `json:"base-declaration"` } - if err := Client().Debug("get-base-declaration", nil, &resp); err != nil { + if err := x.client.Debug("get-base-declaration", nil, &resp); err != nil { return err } fmt.Fprintf(Stdout, "%s\n", resp.BaseDeclaration) diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_get_base_declaration_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_get_base_declaration_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_get_base_declaration_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_get_base_declaration_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -47,7 +47,7 @@ n++ }) - rest, err := snap.Parser().ParseArgs([]string{"debug", "get-base-declaration"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "get-base-declaration"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, "hello\n") diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_get.go snapd-2.37~rc1~14.04/cmd/snap/cmd_get.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_get.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_get.go 2019-01-16 08:36:51.000000000 +0000 @@ -22,17 +22,15 @@ import ( "encoding/json" "fmt" - "os" "sort" "strings" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/i18n" - "golang.org/x/crypto/ssh/terminal" ) -var shortGetHelp = i18n.G("Prints configuration options") +var shortGetHelp = i18n.G("Print configuration options") var longGetHelp = i18n.G(` The get command prints configuration options for the provided snap. @@ -54,6 +52,7 @@ `) type cmdGet struct { + clientMixin Positional struct { Snap installedSnapName `required:"yes"` Keys []string @@ -67,19 +66,22 @@ func init() { addCommand("get", shortGetHelp, longGetHelp, func() flags.Commander { return &cmdGet{} }, map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. "d": i18n.G("Always return document, even with single key"), + // TRANSLATORS: This should not start with a lowercase letter. "l": i18n.G("Always return list, even with single key"), + // TRANSLATORS: This should not start with a lowercase letter. "t": i18n.G("Strict typing with nulls and quoted strings"), }, []argDesc{ { name: "", - // TRANSLATORS: This should probably not start with a lowercase letter. + // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("The snap whose conf is being requested"), }, { - // TRANSLATORS: This needs to be wrapped in <>s. + // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), - // TRANSLATORS: This should probably not start with a lowercase letter. + // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Key of interest within the configuration"), }, }) @@ -176,10 +178,6 @@ return nil } -var isTerminal = func() bool { - return terminal.IsTerminal(int(os.Stdin.Fd())) -} - // outputDefault will be used when no commandline switch to override the // output where used. The output follows the following rules: // - a single key with a string value is printed directly @@ -204,13 +202,13 @@ // conf looks like a map if cfg, ok := confToPrint.(map[string]interface{}); ok { - if isTerminal() { + if isStdinTTY { return x.outputList(cfg) } // TODO: remove this conditional and the warning below // after a transition period. - fmt.Fprintf(Stderr, i18n.G(`WARNING: The output of "snap get" will become a list with columns - use -d or -l to force the output format.\n`)) + fmt.Fprintf(Stderr, i18n.G(`WARNING: The output of 'snap get' will become a list with columns - use -d or -l to force the output format.\n`)) return x.outputJson(confToPrint) } @@ -245,8 +243,7 @@ snapName := string(x.Positional.Snap) confKeys := x.Positional.Keys - cli := Client() - conf, err := cli.Conf(snapName, confKeys) + conf, err := x.client.Conf(snapName, confKeys) if err != nil { return err } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_get_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_get_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_get_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_get_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -66,7 +66,7 @@ stdout: "Key Value\ntest-key1 test-value1\ntest-key2 2\n", }, { args: "get snapname document", - stderr: `WARNING: The output of "snap get" will become a list with columns - use -d or -l to force the output format.\n`, + stderr: `WARNING: The output of 'snap get' will become a list with columns - use -d or -l to force the output format.\n`, stdout: "{\n\t\"document\": {\n\t\t\"key1\": \"value1\",\n\t\t\"key2\": \"value2\"\n\t}\n}\n", }, { isTerminal: true, @@ -98,7 +98,7 @@ isTerminal: false, args: "get snapname test-key1 test-key2", stdout: "{\n\t\"test-key1\": \"test-value1\",\n\t\"test-key2\": 2\n}\n", - stderr: `WARNING: The output of "snap get" will become a list with columns - use -d or -l to force the output format.\n`, + stderr: `WARNING: The output of 'snap get' will become a list with columns - use -d or -l to force the output format.\n`, }, } @@ -109,10 +109,10 @@ c.Logf("Test: %s", test.args) - restore := snapset.MockIsTerminal(test.isTerminal) + restore := snapset.MockIsStdinTTY(test.isTerminal) defer restore() - _, err := snapset.Parser().ParseArgs(strings.Fields(test.args)) + _, err := snapset.Parser(snapset.Client()).ParseArgs(strings.Fields(test.args)) if test.error != "" { c.Check(err, ErrorMatches, test.error) } else { diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_handle_link.go snapd-2.37~rc1~14.04/cmd/snap/cmd_handle_link.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_handle_link.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_handle_link.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,91 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "fmt" + "os" + "syscall" + "time" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/userd/ui" +) + +type cmdHandleLink struct { + waitMixin + + Positional struct { + Uri string `positional-arg-name:""` + } `positional-args:"yes" required:"yes"` +} + +func init() { + cmd := addCommand("handle-link", + i18n.G("Handle a snap:// URI"), + i18n.G("The handle-link command installs the snap-store snap and then invokes it."), + func() flags.Commander { + return &cmdHandleLink{} + }, nil, nil) + cmd.hidden = true +} + +func (x *cmdHandleLink) ensureSnapStoreInstalled() error { + // If the snap-store snap is installed, our work is done + if _, _, err := x.client.Snap("snap-store"); err == nil { + return nil + } + + dialog, err := ui.New() + if err != nil { + return err + } + answeredYes := dialog.YesNo( + i18n.G("Install snap-aware Snap Store snap?"), + i18n.G("The Snap Store is required to open snaps from a web browser."), + &ui.DialogOptions{ + Timeout: 5 * time.Minute, + Footer: i18n.G("This dialog will close automatically after 5 minutes of inactivity."), + }) + if !answeredYes { + return fmt.Errorf(i18n.G("Snap Store required")) + } + + changeID, err := x.client.Install("snap-store", nil) + if err != nil { + return err + } + _, err = x.wait(changeID) + if err != nil && err != noWait { + return err + } + return nil +} + +func (x *cmdHandleLink) Execute([]string) error { + if err := x.ensureSnapStoreInstalled(); err != nil { + return err + } + + argv := []string{"snap", "run", "snap-store", x.Positional.Uri} + return syscall.Exec("/proc/self/exe", argv, os.Environ()) +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_help.go snapd-2.37~rc1~14.04/cmd/snap/cmd_help.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_help.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_help.go 2019-01-16 08:36:51.000000000 +0000 @@ -20,43 +20,257 @@ package main import ( - "os" - - "github.com/snapcore/snapd/i18n" + "bytes" + "fmt" + "strings" + "unicode/utf8" "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" ) -var shortHelpHelp = i18n.G("Help") +var shortHelpHelp = i18n.G("Show help about a command") var longHelpHelp = i18n.G(` -The help command shows helpful information. Unlike this. ;-) +The help command displays information about snap commands. `) +// addHelp adds --help like what go-flags would do for us, but hidden +func addHelp(parser *flags.Parser) error { + var help struct { + ShowHelp func() error `short:"h" long:"help"` + } + help.ShowHelp = func() error { + // this function is called via --help (or -h). In that + // case, parser.Command.Active should be the command + // on which help is being requested (like "snap foo + // --help", active is foo), or nil in the toplevel. + if parser.Command.Active == nil { + // toplevel --help will get handled via ErrCommandRequired + return nil + } + // not toplevel, so ask for regular help + return &flags.Error{Type: flags.ErrHelp} + } + hlpgrp, err := parser.AddGroup("Help Options", "", &help) + if err != nil { + return err + } + hlpgrp.Hidden = true + hlp := parser.FindOptionByLongName("help") + hlp.Description = i18n.G("Show this help message") + hlp.Hidden = true + + return nil +} + type cmdHelp struct { - Manpage bool `long:"man"` - parser *flags.Parser + All bool `long:"all"` + Manpage bool `long:"man" hidden:"true"` + Positional struct { + // TODO: find a way to make Command tab-complete + Sub string `positional-arg-name:""` + } `positional-args:"yes"` + parser *flags.Parser } func init() { addCommand("help", shortHelpHelp, longHelpHelp, func() flags.Commander { return &cmdHelp{} }, - map[string]string{"man": i18n.G("Generate the manpage")}, nil) + map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "all": i18n.G("Show a short summary of all commands"), + // TRANSLATORS: This should not start with a lowercase letter. + "man": i18n.G("Generate the manpage"), + }, nil) } func (cmd *cmdHelp) setParser(parser *flags.Parser) { cmd.parser = parser } +// manfixer is a hackish way to get the generated manpage into section 8 +// (go-flags doesn't have an option for this; I'll be proposing something +// there soon, but still waiting on some other PRs to make it through) +type manfixer struct { + done bool +} + +func (w *manfixer) Write(buf []byte) (int, error) { + if !w.done { + w.done = true + if bytes.HasPrefix(buf, []byte(".TH snap 1 ")) { + // io.Writer.Write must not modify the buffer, even temporarily + n, _ := Stdout.Write(buf[:9]) + Stdout.Write([]byte{'8'}) + m, err := Stdout.Write(buf[10:]) + return n + m + 1, err + } + } + return Stdout.Write(buf) +} + func (cmd cmdHelp) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } - if cmd.Manpage { - cmd.parser.WriteManPage(Stdout) - os.Exit(0) + // you shouldn't try to to combine --man with --all nor a + // subcommand, but --man is hidden so no real need to check. + cmd.parser.WriteManPage(&manfixer{}) + return nil + } + if cmd.All { + if cmd.Positional.Sub != "" { + return fmt.Errorf(i18n.G("help accepts a command, or '--all', but not both.")) + } + printLongHelp(cmd.parser) + return nil + } + + if cmd.Positional.Sub != "" { + subcmd := cmd.parser.Find(cmd.Positional.Sub) + if subcmd == nil { + return fmt.Errorf(i18n.G("Unknown command %q. Try 'snap help'."), cmd.Positional.Sub) + } + // this makes "snap help foo" work the same as "snap foo --help" + cmd.parser.Command.Active = subcmd + return &flags.Error{Type: flags.ErrHelp} + } + + return &flags.Error{Type: flags.ErrCommandRequired} +} + +type helpCategory struct { + Label string + Description string + Commands []string +} + +// helpCategories helps us by grouping commands +var helpCategories = []helpCategory{ + { + Label: i18n.G("Basics"), + Description: i18n.G("basic snap management"), + Commands: []string{"find", "info", "install", "list", "remove"}, + }, { + Label: i18n.G("...more"), + Description: i18n.G("slightly more advanced snap management"), + Commands: []string{"refresh", "revert", "switch", "disable", "enable"}, + }, { + Label: i18n.G("History"), + Description: i18n.G("manage system change transactions"), + Commands: []string{"changes", "tasks", "abort", "watch"}, + }, { + Label: i18n.G("Daemons"), + Description: i18n.G("manage services"), + Commands: []string{"services", "start", "stop", "restart", "logs"}, + }, { + Label: i18n.G("Commands"), + Description: i18n.G("manage aliases"), + Commands: []string{"alias", "aliases", "unalias", "prefer"}, + }, { + Label: i18n.G("Configuration"), + Description: i18n.G("system administration and configuration"), + Commands: []string{"get", "set", "wait"}, + }, { + Label: i18n.G("Account"), + Description: i18n.G("authentication to snapd and the snap store"), + Commands: []string{"login", "logout", "whoami"}, + }, { + Label: i18n.G("Permissions"), + Description: i18n.G("manage permissions"), + Commands: []string{"interfaces", "interface", "connect", "disconnect"}, + }, { + Label: i18n.G("Snapshots"), + Description: i18n.G("archives of snap data"), + Commands: []string{"saved", "save", "check-snapshot", "restore", "forget"}, + }, { + Label: i18n.G("Other"), + Description: i18n.G("miscellanea"), + Commands: []string{"version", "warnings", "okay"}, + }, { + Label: i18n.G("Development"), + Description: i18n.G("developer-oriented features"), + Commands: []string{"run", "pack", "try", "ack", "known", "download"}, + }, +} + +var ( + longSnapDescription = strings.TrimSpace(i18n.G(` +The snap command lets you install, configure, refresh and remove snaps. +Snaps are packages that work across many different Linux distributions, +enabling secure delivery and operation of the latest apps and utilities. +`)) + snapUsage = i18n.G("Usage: snap [...]") + snapHelpCategoriesIntro = i18n.G("Commands can be classified as follows:") + snapHelpAllFooter = i18n.G("For more information about a command, run 'snap help '.") + snapHelpFooter = i18n.G("For a short summary of all commands, run 'snap help --all'.") +) + +func printHelpHeader() { + fmt.Fprintln(Stdout, longSnapDescription) + fmt.Fprintln(Stdout) + fmt.Fprintln(Stdout, snapUsage) + fmt.Fprintln(Stdout) + fmt.Fprintln(Stdout, snapHelpCategoriesIntro) + fmt.Fprintln(Stdout) +} + +func printHelpAllFooter() { + fmt.Fprintln(Stdout) + fmt.Fprintln(Stdout, snapHelpAllFooter) +} + +func printHelpFooter() { + printHelpAllFooter() + fmt.Fprintln(Stdout, snapHelpFooter) +} + +// this is called when the Execute returns a flags.Error with ErrCommandRequired +func printShortHelp() { + printHelpHeader() + maxLen := 0 + for _, categ := range helpCategories { + if l := utf8.RuneCountInString(categ.Label); l > maxLen { + maxLen = l + } + } + for _, categ := range helpCategories { + fmt.Fprintf(Stdout, "%*s: %s\n", maxLen+2, categ.Label, strings.Join(categ.Commands, ", ")) + } + printHelpFooter() +} + +// this is "snap help --all" +func printLongHelp(parser *flags.Parser) { + printHelpHeader() + maxLen := 0 + for _, categ := range helpCategories { + for _, command := range categ.Commands { + if l := len(command); l > maxLen { + maxLen = l + } + } + } + + // flags doesn't have a LookupCommand? + commands := parser.Commands() + cmdLookup := make(map[string]*flags.Command, len(commands)) + for _, cmd := range commands { + cmdLookup[cmd.Name] = cmd } - return &flags.Error{ - Type: flags.ErrHelp, + for _, categ := range helpCategories { + fmt.Fprintln(Stdout) + fmt.Fprintf(Stdout, " %s (%s):\n", categ.Label, categ.Description) + for _, name := range categ.Commands { + cmd := cmdLookup[name] + if cmd == nil { + fmt.Fprintf(Stderr, "??? Cannot find command %q mentioned in help categories, please report!\n", name) + } else { + fmt.Fprintf(Stdout, " %*s %s\n", -maxLen, name, cmd.ShortDescription) + } + } } + printHelpAllFooter() } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_help_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_help_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_help_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_help_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -20,8 +20,13 @@ package main_test import ( + "bytes" + "fmt" "os" + "regexp" + "strings" + "github.com/jessevdk/go-flags" "gopkg.in/check.v1" snap "github.com/snapcore/snapd/cmd/snap" @@ -32,50 +37,140 @@ defer func() { os.Args = origArgs }() for _, cmdLine := range [][]string{ + {"snap"}, {"snap", "help"}, {"snap", "--help"}, {"snap", "-h"}, } { + s.ResetStdStreams() + os.Args = cmdLine + comment := check.Commentf("%q", cmdLine) err := snap.RunMain() - c.Assert(err, check.IsNil) - c.Check(s.Stdout(), check.Matches, `(?smU)Usage: - +snap \[OPTIONS\] + c.Assert(err, check.IsNil, comment) + c.Check(s.Stdout(), check.Matches, "(?s)"+strings.Join([]string{ + snap.LongSnapDescription, + "", + regexp.QuoteMeta(snap.SnapUsage), + "", ".*", "", + snap.SnapHelpAllFooter, + snap.SnapHelpFooter, + }, "\n")+`\s*`, comment) + c.Check(s.Stderr(), check.Equals, "", comment) + } +} -Install, configure, refresh and remove snap packages. Snaps are -'universal' packages that work across many different Linux systems, -enabling secure distribution of the latest apps and utilities for -cloud, servers, desktops and the internet of things. +func (s *SnapSuite) TestHelpAllPrintsLongHelp(c *check.C) { + origArgs := os.Args + defer func() { os.Args = origArgs }() -This is the CLI for snapd, a background service that takes care of -snaps on the system. Start with 'snap list' to see installed snaps. + os.Args = []string{"snap", "help", "--all"} + err := snap.RunMain() + c.Assert(err, check.IsNil) + c.Check(s.Stdout(), check.Matches, "(?sm)"+strings.Join([]string{ + snap.LongSnapDescription, + "", + regexp.QuoteMeta(snap.SnapUsage), + "", + snap.SnapHelpCategoriesIntro, + "", ".*", "", + snap.SnapHelpAllFooter, + }, "\n")+`\s*`) + c.Check(s.Stderr(), check.Equals, "") +} -Application Options: - +--version +Print the version and exit +func nonHiddenCommands() map[string]bool { + parser := snap.Parser(snap.Client()) + commands := parser.Commands() + names := make(map[string]bool, len(commands)) + for _, cmd := range commands { + if cmd.Hidden { + continue + } + names[cmd.Name] = true + } + return names +} + +func (s *SnapSuite) testSubCommandHelp(c *check.C, sub, expected string) { + parser := snap.Parser(snap.Client()) + rest, err := parser.ParseArgs([]string{sub, "--help"}) + c.Assert(err, check.DeepEquals, &flags.Error{Type: flags.ErrHelp}) + c.Assert(rest, check.HasLen, 0) + var buf bytes.Buffer + parser.WriteHelp(&buf) + c.Check(buf.String(), check.Equals, expected) +} + +func (s *SnapSuite) TestSubCommandHelpPrintsHelp(c *check.C) { + origArgs := os.Args + defer func() { os.Args = origArgs }() -Help Options: - +-h, --help +Show this help message + for cmd := range nonHiddenCommands() { + s.ResetStdStreams() + os.Args = []string{"snap", cmd, "--help"} -Available commands: - +abort.* -`) - c.Check(s.Stderr(), check.Equals, "") + err := snap.RunMain() + comment := check.Commentf("%q", cmd) + c.Assert(err, check.IsNil, comment) + // regexp matches "Usage: snap " plus an arbitrary + // number of [] plus an arbitrary number of + // <> optionally ending in ellipsis + c.Check(s.Stdout(), check.Matches, fmt.Sprintf(`(?sm)Usage:\s+snap %s(?: \[[^][]+\])*(?:(?: <[^<>]+>)+(?:\.\.\.)?)?$.*`, cmd), comment) + c.Check(s.Stderr(), check.Equals, "", comment) } } -func (s *SnapSuite) TestSubCommandHelpPrintsHelp(c *check.C) { +func (s *SnapSuite) TestHelpCategories(c *check.C) { + // non-hidden commands that are not expected to appear in the help summary + excluded := []string{ + "help", + } + all := nonHiddenCommands() + categorised := make(map[string]bool, len(all)+len(excluded)) + for _, cmd := range excluded { + categorised[cmd] = true + } + seen := make(map[string]string, len(all)) + for _, categ := range snap.HelpCategories { + for _, cmd := range categ.Commands { + categorised[cmd] = true + if seen[cmd] != "" { + c.Errorf("duplicated: %q in %q and %q", cmd, seen[cmd], categ.Label) + } + seen[cmd] = categ.Label + } + } + for cmd := range all { + if !categorised[cmd] { + c.Errorf("uncategorised: %q", cmd) + } + } + for cmd := range categorised { + if !all[cmd] { + c.Errorf("unknown (hidden?): %q", cmd) + } + } +} + +func (s *SnapSuite) TestHelpCommandAllFails(c *check.C) { origArgs := os.Args defer func() { os.Args = origArgs }() + os.Args = []string{"snap", "help", "interfaces", "--all"} + + err := snap.RunMain() + c.Assert(err, check.ErrorMatches, "help accepts a command, or '--all', but not both.") +} - os.Args = []string{"snap", "install", "--help"} +func (s *SnapSuite) TestManpageInSection8(c *check.C) { + origArgs := os.Args + defer func() { os.Args = origArgs }() + os.Args = []string{"snap", "help", "--man"} err := snap.RunMain() c.Assert(err, check.IsNil) - c.Check(s.Stdout(), check.Matches, `(?smU)Usage: - +snap \[OPTIONS\] install \[install-OPTIONS\] ... -.* -`) - c.Check(s.Stderr(), check.Equals, "") + + c.Check(s.Stdout(), check.Matches, `\.TH snap 8 (?s).*`) } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_info.go snapd-2.37~rc1~14.04/cmd/snap/cmd_info.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_info.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_info.go 2019-01-16 08:36:51.000000000 +0000 @@ -20,13 +20,14 @@ package main import ( - "bytes" "fmt" "io" "path/filepath" "strings" "text/tabwriter" "time" + "unicode" + "unicode/utf8" "github.com/jessevdk/go-flags" "gopkg.in/yaml.v2" @@ -40,15 +41,25 @@ ) type infoCmd struct { + clientMixin + colorMixin + timeMixin + Verbose bool `long:"verbose"` Positional struct { Snaps []anySnapName `positional-arg-name:"" required:"1"` } `positional-args:"yes" required:"yes"` } -var shortInfoHelp = i18n.G("Show detailed information about a snap") +var shortInfoHelp = i18n.G("Show detailed information about snaps") var longInfoHelp = i18n.G(` -The info command shows detailed information about a snap, be it by name or by path.`) +The info command shows detailed information about snaps. + +The snaps can be specified by name or by path; names are looked for both in the +store and in the installed snaps; paths can refer to a .snap file, or to a +directory that contains an unpacked snap suitable for 'snap try' (an example +of this would be the 'prime' directory snapcraft produces). +`) func init() { addCommand("info", @@ -56,9 +67,10 @@ longInfoHelp, func() flags.Commander { return &infoCmd{} - }, map[string]string{ - "verbose": i18n.G("Include a verbose list of a snap's notes (otherwise, summarise notes)"), - }, nil) + }, colorDescs.also(timeDescs).also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "verbose": i18n.G("Include more details on the snap (expanded notes, base, etc.)"), + }), nil) } func norm(path string) string { @@ -100,6 +112,12 @@ } } +func maybePrintBase(w io.Writer, base string, verbose bool) { + if verbose && base != "" { + fmt.Fprintf(w, "base:\t%s\n", base) + } +} + func tryDirect(w io.Writer, path string, verbose bool) bool { path = norm(path) @@ -122,7 +140,7 @@ return false } fmt.Fprintf(w, "path:\t%q\n", path) - fmt.Fprintf(w, "name:\t%s\n", info.Name()) + fmt.Fprintf(w, "name:\t%s\n", info.InstanceName()) fmt.Fprintf(w, "summary:\t%s\n", formatSummary(info.Summary())) var notes *Notes @@ -140,6 +158,7 @@ } fmt.Fprintf(w, "version:\t%s %s\n", info.Version, notes) maybePrintType(w, string(info.Type)) + maybePrintBase(w, info.Base, verbose) if sha3_384 != "" { fmt.Fprintf(w, "sha3-384:\t%s\n", sha3_384) } @@ -156,27 +175,90 @@ return nil } -// formatDescr formats a given string (typically a snap description) +// runesTrimRightSpace returns text, with any trailing whitespace dropped. +func runesTrimRightSpace(text []rune) []rune { + j := len(text) + for j > 0 && unicode.IsSpace(text[j-1]) { + j-- + } + return text[:j] +} + +// runesLastIndexSpace returns the index of the last whitespace rune +// in the text. If the text has no whitespace, returns -1. +func runesLastIndexSpace(text []rune) int { + for i := len(text) - 1; i >= 0; i-- { + if unicode.IsSpace(text[i]) { + return i + } + } + return -1 +} + +// wrapLine wraps a line to fit into width, preserving the line's indent, and +// writes it out prepending padding to each line. +func wrapLine(out io.Writer, text []rune, pad string, width int) error { + // Note: this is _wrong_ for much of unicode (because the width of a rune on + // the terminal is anything between 0 and 2, not always 1 as this code + // assumes) but fixing that is Hard. Long story short, you can get close + // using a couple of big unicode tables (which is what wcwidth + // does). Getting it 100% requires a terminfo-alike of unicode behaviour. + // However, before this we'd count bytes instead of runes, so we'd be + // even more broken. Think of it as successive approximations... at least + // with this work we share tabwriter's opinion on the width of things! + + // This (and possibly printDescr below) should move to strutil once + // we're happy with it getting wider (heh heh) use. + + // discard any trailing whitespace + text = runesTrimRightSpace(text) + // establish the indent of the whole block + idx := 0 + for idx < len(text) && unicode.IsSpace(text[idx]) { + idx++ + } + indent := pad + string(text[:idx]) + text = text[idx:] + width -= idx + utf8.RuneCountInString(pad) + var err error + for len(text) > width && err == nil { + // find a good place to chop the text + idx = runesLastIndexSpace(text[:width+1]) + if idx < 0 { + // there's no whitespace; just chop at line width + idx = width + } + _, err = fmt.Fprint(out, indent, string(text[:idx]), "\n") + // prune any remaining whitespace before the start of the next line + for idx < len(text) && unicode.IsSpace(text[idx]) { + idx++ + } + text = text[idx:] + } + if err != nil { + return err + } + _, err = fmt.Fprint(out, indent, string(text), "\n") + return err +} + +// printDescr formats a given string (typically a snap description) // in a user friendly way. // // The rules are (intentionally) very simple: -// - trim whitespace -// - word wrap at "max" chars -// - keep \n intact and break here -// - ignore \r -func formatDescr(descr string, max int) string { - out := bytes.NewBuffer(nil) - for _, line := range strings.Split(strings.TrimSpace(descr), "\n") { - if len(line) > max { - for _, chunk := range strutil.WordWrap(line, max) { - fmt.Fprintf(out, " %s\n", chunk) - } - } else { - fmt.Fprintf(out, " %s\n", line) +// - trim trailing whitespace +// - word wrap at "max" chars preserving line indent +// - keep \n intact and break there +func printDescr(w io.Writer, descr string, max int) error { + var err error + descr = strings.TrimRightFunc(descr, unicode.IsSpace) + for _, line := range strings.Split(descr, "\n") { + err = wrapLine(w, []rune(line), " ", max) + if err != nil { + break } } - - return strings.TrimSuffix(out.String(), "\n") + return err } func maybePrintCommands(w io.Writer, snapName string, allApps []client.AppInfo, n int) { @@ -240,9 +322,19 @@ var channelRisks = []string{"stable", "candidate", "beta", "edge"} // displayChannels displays channels and tracks in the right order -func displayChannels(w io.Writer, remote *client.Snap) { - // \t\t\t so we get "installed" lined up with "channels" - fmt.Fprintf(w, "channels:\t\t\t\n") +func (x *infoCmd) displayChannels(w io.Writer, chantpl string, esc *escapes, remote *client.Snap, revLen, sizeLen int) (maxRevLen, maxSizeLen int) { + fmt.Fprintln(w, "channels:") + + releasedfmt := "2006-01-02" + if x.AbsTime { + releasedfmt = time.RFC3339 + } + + type chInfoT struct { + name, version, released, revision, size, notes string + } + var chInfos []*chInfoT + maxRevLen, maxSizeLen = revLen, sizeLen // order by tracks for _, tr := range remote.Tracks { @@ -253,23 +345,36 @@ if tr == "latest" { chName = risk } - var version, revision, size, notes string + chInfo := chInfoT{name: chName} if ok { - version = ch.Version - revision = fmt.Sprintf("(%s)", ch.Revision) - size = strutil.SizeToStr(ch.Size) - notes = NotesFromChannelSnapInfo(ch).String() + chInfo.version = ch.Version + chInfo.revision = fmt.Sprintf("(%s)", ch.Revision) + if len(chInfo.revision) > maxRevLen { + maxRevLen = len(chInfo.revision) + } + chInfo.released = ch.ReleasedAt.Format(releasedfmt) + chInfo.size = strutil.SizeToStr(ch.Size) + if len(chInfo.size) > maxSizeLen { + maxSizeLen = len(chInfo.size) + } + chInfo.notes = NotesFromChannelSnapInfo(ch).String() trackHasOpenChannel = true } else { if trackHasOpenChannel { - version = "↑" + chInfo.version = esc.uparrow } else { - version = "–" // that's an en dash (so yaml is happy) + chInfo.version = esc.dash } } - fmt.Fprintf(w, " %s:\t%s\t%s\t%s\t%s\n", chName, version, revision, size, notes) + chInfos = append(chInfos, &chInfo) } } + + for _, chInfo := range chInfos { + fmt.Fprintf(w, " "+chantpl, chInfo.name, chInfo.version, chInfo.released, maxRevLen, chInfo.revision, maxSizeLen, chInfo.size, chInfo.notes) + } + + return maxRevLen, maxSizeLen } func formatSummary(raw string) string { @@ -281,8 +386,6 @@ } func (x *infoCmd) Execute([]string) error { - cli := Client() - termWidth, _ := termSize() termWidth -= 3 if termWidth > 100 { @@ -290,6 +393,7 @@ termWidth = 100 } + esc := x.getEscapes() w := tabwriter.NewWriter(Stdout, 2, 2, 1, ' ', 0) noneOK := true @@ -298,13 +402,17 @@ if i > 0 { fmt.Fprintln(w, "---") } + if snapName == "system" { + fmt.Fprintln(w, "system: You can't have it.") + continue + } if tryDirect(w, snapName, x.Verbose) { noneOK = false continue } - remote, resInfo, _ := cli.FindOne(snapName) - local, _, _ := cli.Snap(snapName) + remote, resInfo, _ := x.client.FindOne(snapName) + local, _, _ := x.client.Snap(snapName) both := coalesce(local, remote) @@ -320,19 +428,18 @@ fmt.Fprintf(w, "name:\t%s\n", both.Name) fmt.Fprintf(w, "summary:\t%s\n", formatSummary(both.Summary)) - // TODO: have publisher; use publisher here, - // and additionally print developer if publisher != developer - fmt.Fprintf(w, "publisher:\t%s\n", both.Developer) + fmt.Fprintf(w, "publisher:\t%s\n", longPublisher(esc, both.Publisher)) if both.Contact != "" { fmt.Fprintf(w, "contact:\t%s\n", strings.TrimPrefix(both.Contact, "mailto:")) } license := both.License if license == "" { - license = "unknown" + license = "unset" } fmt.Fprintf(w, "license:\t%s\n", license) maybePrintPrice(w, remote, resInfo) - fmt.Fprintf(w, "description: |\n%s\n", formatDescr(both.Description, termWidth)) + fmt.Fprintln(w, "description: |") + printDescr(w, both.Description, termWidth) maybePrintCommands(w, snapName, both.Apps, termWidth) maybePrintServices(w, snapName, both.Apps, termWidth) @@ -364,18 +471,33 @@ // stops the notes etc trying to be aligned with channels w.Flush() maybePrintType(w, both.Type) + maybePrintBase(w, both.Base, x.Verbose) maybePrintID(w, both) + var localRev, localSize string + var revLen, sizeLen int if local != nil { - fmt.Fprintf(w, "tracking:\t%s\n", local.TrackingChannel) - fmt.Fprintf(w, "refreshed:\t%s\n", local.InstallDate.Format(time.RFC3339)) - } - w.Flush() - if local != nil { - fmt.Fprintf(w, "installed:\t%s\t(%s)\t%s\t%s\n", local.Version, local.Revision, strutil.SizeToStr(local.InstalledSize), notes) + if local.TrackingChannel != "" { + fmt.Fprintf(w, "tracking:\t%s\n", local.TrackingChannel) + } + if !local.InstallDate.IsZero() { + fmt.Fprintf(w, "refresh-date:\t%s\n", x.fmtTime(local.InstallDate)) + } + localRev = fmt.Sprintf("(%s)", local.Revision) + revLen = len(localRev) + localSize = strutil.SizeToStr(local.InstalledSize) + sizeLen = len(localSize) } + chantpl := "%s:\t%s %s%*s %*s %s\n" if remote != nil && remote.Channels != nil && remote.Tracks != nil { - displayChannels(w, remote) + chantpl = "%s:\t%s\t%s\t%*s\t%*s\t%s\n" + + w.Flush() + revLen, sizeLen = x.displayChannels(w, chantpl, esc, remote, revLen, sizeLen) + } + if local != nil { + fmt.Fprintf(w, chantpl, + "installed", local.Version, "", revLen, localRev, sizeLen, localSize, notes) } } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_info_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_info_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_info_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_info_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -23,6 +23,7 @@ "bytes" "fmt" "net/http" + "time" "gopkg.in/check.v1" @@ -48,7 +49,13 @@ var mixedAppInfos = append(append([]client.AppInfo(nil), cmdAppInfos...), svcAppInfos...) -func (s *SnapSuite) TestMaybePrintServices(c *check.C) { +type infoSuite struct { + BaseSnapSuite +} + +var _ = check.Suite(&infoSuite{}) + +func (s *infoSuite) TestMaybePrintServices(c *check.C) { for _, infos := range [][]client.AppInfo{svcAppInfos, mixedAppInfos} { var buf bytes.Buffer snap.MaybePrintServices(&buf, "foo", infos, -1) @@ -60,7 +67,7 @@ } } -func (s *SnapSuite) TestMaybePrintServicesNoServices(c *check.C) { +func (s *infoSuite) TestMaybePrintServicesNoServices(c *check.C) { for _, infos := range [][]client.AppInfo{cmdAppInfos, nil} { var buf bytes.Buffer snap.MaybePrintServices(&buf, "foo", infos, -1) @@ -69,7 +76,7 @@ } } -func (s *SnapSuite) TestMaybePrintCommands(c *check.C) { +func (s *infoSuite) TestMaybePrintCommands(c *check.C) { for _, infos := range [][]client.AppInfo{cmdAppInfos, mixedAppInfos} { var buf bytes.Buffer snap.MaybePrintCommands(&buf, "foo", infos, -1) @@ -81,7 +88,7 @@ } } -func (s *SnapSuite) TestMaybePrintCommandsNoCommands(c *check.C) { +func (s *infoSuite) TestMaybePrintCommandsNoCommands(c *check.C) { for _, infos := range [][]client.AppInfo{svcAppInfos, nil} { var buf bytes.Buffer snap.MaybePrintCommands(&buf, "foo", infos, -1) @@ -90,7 +97,7 @@ } } -func (s *SnapSuite) TestInfoPriced(c *check.C) { +func (s *infoSuite) TestInfoPriced(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { @@ -108,12 +115,12 @@ n++ }) - rest, err := snap.Parser().ParseArgs([]string{"info", "hello"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "hello"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `name: hello summary: GNU Hello, the "hello world" snap -publisher: canonical +publisher: Canonical* license: Proprietary price: 1.99GBP description: | @@ -135,6 +142,12 @@ "confinement": "strict", "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", "developer": "canonical", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, "download-size": 65536, "icon": "", "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", @@ -156,7 +169,55 @@ } ` -func (s *SnapSuite) TestInfoUnquoted(c *check.C) { +const mockInfoJSONWithChannels = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": [ + { + "channel": "stable", + "confinement": "strict", + "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", + "developer": "canonical", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, + "download-size": 65536, + "icon": "", + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "name": "hello", + "private": false, + "resource": "/v2/snaps/hello", + "revision": "1", + "status": "available", + "summary": "The GNU Hello snap", + "type": "app", + "version": "2.10", + "license": "MIT", + "channels": { + "1/stable": { + "revision": "1", + "version": "2.10", + "channel": "1/stable", + "size": 65536, + "released-at": "2018-12-18T15:16:56.723501Z" + } + }, + "tracks": ["1"] + } + ], + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +func (s *infoSuite) TestInfoUnquoted(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { @@ -169,17 +230,17 @@ c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello") fmt.Fprintln(w, "{}") default: - c.Fatalf("expected to get 1 requests, now on %d (%v)", n+1, r) + c.Fatalf("expected to get 2 requests, now on %d (%v)", n+1, r) } n++ }) - rest, err := snap.Parser().ParseArgs([]string{"info", "hello"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "hello"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `name: hello summary: The GNU Hello snap -publisher: canonical +publisher: Canonical* license: MIT description: | GNU hello prints a friendly greeting. This is part of the snapcraft tour at @@ -199,7 +260,15 @@ "confinement": "strict", "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", "developer": "canonical", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "install-date": "2006-01-02T22:04:07.123456789Z", + "installed-size": 1024, "name": "hello", "private": false, "resource": "/v2/snaps/hello", @@ -209,8 +278,7 @@ "type": "app", "version": "2.10", "license": "BSD-3", - "tracking-channel": "beta", - "installed-size": 1024 + "tracking-channel": "beta" } } ` @@ -224,23 +292,30 @@ "confinement": "strict", "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", "developer": "canonical", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "install-date": "2006-01-02T22:04:07.123456789Z", + "installed-size": 1024, "name": "hello", "private": false, "resource": "/v2/snaps/hello", - "revision": "1", + "revision": "100", "status": "available", "summary": "The GNU Hello snap", "type": "app", "version": "2.10", "license": "", - "tracking-channel": "beta", - "installed-size": 1024 + "tracking-channel": "beta" } } ` -func (s *SnapSuite) TestInfoWithLocalDifferentLicense(c *check.C) { +func (s *infoSuite) TestInfoWithLocalDifferentLicense(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { @@ -253,30 +328,30 @@ c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello") fmt.Fprintln(w, mockInfoJSONOtherLicense) default: - c.Fatalf("expected to get 1 requests, now on %d (%v)", n+1, r) + c.Fatalf("expected to get 2 requests, now on %d (%v)", n+1, r) } n++ }) - rest, err := snap.Parser().ParseArgs([]string{"info", "hello"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "--abs-time", "hello"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `name: hello summary: The GNU Hello snap -publisher: canonical +publisher: Canonical* license: BSD-3 description: | GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/ -snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 -tracking: beta -refreshed: 0001-01-01T00:00:00Z -installed: 2.10 (1) 1kB disabled +snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 +tracking: beta +refresh-date: 2006-01-02T22:04:07Z +installed: 2.10 (1) 1kB disabled `) c.Check(s.Stderr(), check.Equals, "") } -func (s *SnapSuite) TestInfoWithLocalNoLicense(c *check.C) { +func (s *infoSuite) TestInfoWithLocalNoLicense(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { @@ -289,25 +364,189 @@ c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello") fmt.Fprintln(w, mockInfoJSONNoLicense) default: - c.Fatalf("expected to get 1 requests, now on %d (%v)", n+1, r) + c.Fatalf("expected to get 2 requests, now on %d (%v)", n+1, r) } n++ }) - rest, err := snap.Parser().ParseArgs([]string{"info", "hello"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "--abs-time", "hello"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `name: hello summary: The GNU Hello snap -publisher: canonical -license: unknown +publisher: Canonical* +license: unset description: | GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/ -snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 -tracking: beta -refreshed: 0001-01-01T00:00:00Z -installed: 2.10 (1) 1kB disabled +snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 +tracking: beta +refresh-date: 2006-01-02T22:04:07Z +installed: 2.10 (100) 1kB disabled `) c.Check(s.Stderr(), check.Equals, "") } + +func (s *infoSuite) TestInfoWithChannelsAndLocal(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0, 2, 4: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + fmt.Fprintln(w, mockInfoJSONWithChannels) + case 1, 3, 5: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello") + fmt.Fprintln(w, mockInfoJSONNoLicense) + default: + c.Fatalf("expected to get 6 requests, now on %d (%v)", n+1, r) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "--abs-time", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `name: hello +summary: The GNU Hello snap +publisher: Canonical* +license: unset +description: | + GNU hello prints a friendly greeting. This is part of the snapcraft tour at + https://snapcraft.io/ +snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 +tracking: beta +refresh-date: 2006-01-02T22:04:07Z +channels: + 1/stable: 2.10 2018-12-18T15:16:56Z (1) 65kB - + 1/candidate: ^ + 1/beta: ^ + 1/edge: ^ +installed: 2.10 (100) 1kB disabled +`) + c.Check(s.Stderr(), check.Equals, "") + c.Check(n, check.Equals, 2) + + // now the same but without abs-time + s.ResetStdStreams() + rest, err = snap.Parser(snap.Client()).ParseArgs([]string{"info", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `name: hello +summary: The GNU Hello snap +publisher: Canonical* +license: unset +description: | + GNU hello prints a friendly greeting. This is part of the snapcraft tour at + https://snapcraft.io/ +snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 +tracking: beta +refresh-date: 2006-01-02 +channels: + 1/stable: 2.10 2018-12-18 (1) 65kB - + 1/candidate: ^ + 1/beta: ^ + 1/edge: ^ +installed: 2.10 (100) 1kB disabled +`) + c.Check(s.Stderr(), check.Equals, "") + c.Check(n, check.Equals, 4) + + // now the same but with unicode on + s.ResetStdStreams() + rest, err = snap.Parser(snap.Client()).ParseArgs([]string{"info", "--unicode=always", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `name: hello +summary: The GNU Hello snap +publisher: Canonical✓ +license: unset +description: | + GNU hello prints a friendly greeting. This is part of the snapcraft tour at + https://snapcraft.io/ +snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 +tracking: beta +refresh-date: 2006-01-02 +channels: + 1/stable: 2.10 2018-12-18 (1) 65kB - + 1/candidate: ↑ + 1/beta: ↑ + 1/edge: ↑ +installed: 2.10 (100) 1kB disabled +`) + c.Check(s.Stderr(), check.Equals, "") + c.Check(n, check.Equals, 6) +} + +func (s *infoSuite) TestInfoHumanTimes(c *check.C) { + // checks that tiemutil.Human is called when no --abs-time is given + restore := snap.MockTimeutilHuman(func(time.Time) string { return "TOTALLY NOT A ROBOT" }) + defer restore() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + fmt.Fprintln(w, "{}") + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello") + fmt.Fprintln(w, mockInfoJSONNoLicense) + default: + c.Fatalf("expected to get 2 requests, now on %d (%v)", n+1, r) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `name: hello +summary: The GNU Hello snap +publisher: Canonical* +license: unset +description: | + GNU hello prints a friendly greeting. This is part of the snapcraft tour at + https://snapcraft.io/ +snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 +tracking: beta +refresh-date: TOTALLY NOT A ROBOT +installed: 2.10 (100) 1kB disabled +`) + c.Check(s.Stderr(), check.Equals, "") +} + +func (infoSuite) TestDescr(c *check.C) { + for k, v := range map[string]string{ + "": " \n", + `one: + * two three four five six + * seven height nine ten +`: ` one: + * two three four + five six + * seven height + nine ten +`, + "abcdefghijklm nopqrstuvwxyz ABCDEFGHIJKLMNOPQR STUVWXYZ": ` + abcdefghijklm + nopqrstuvwxyz + ABCDEFGHIJKLMNOPQR + STUVWXYZ +`[1:], + // not much we can do when it won't fit + "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ": ` + abcdefghijklmnopqr + stuvwxyz + ABCDEFGHIJKLMNOPQR + STUVWXYZ +`[1:], + } { + var buf bytes.Buffer + snap.PrintDescr(&buf, k, 20) + c.Check(buf.String(), check.Equals, v, check.Commentf("%q", k)) + } +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_interface.go snapd-2.37~rc1~14.04/cmd/snap/cmd_interface.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_interface.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_interface.go 2019-01-16 08:36:51.000000000 +0000 @@ -20,6 +20,7 @@ package main import ( + "encoding/json" "fmt" "io" "sort" @@ -32,6 +33,7 @@ ) type cmdInterface struct { + clientMixin ShowAttrs bool `long:"attrs"` ShowAll bool `long:"all"` Positionals struct { @@ -39,7 +41,7 @@ } `positional-args:"true"` } -var shortInterfaceHelp = i18n.G("Lists snap interfaces") +var shortInterfaceHelp = i18n.G("Show details of snap interfaces") var longInterfaceHelp = i18n.G(` The interface command shows details of snap interfaces. @@ -51,12 +53,14 @@ addCommand("interface", shortInterfaceHelp, longInterfaceHelp, func() flags.Commander { return &cmdInterface{} }, map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. "attrs": i18n.G("Show interface attributes"), - "all": i18n.G("Include unused interfaces"), + // TRANSLATORS: This should not start with a lowercase letter. + "all": i18n.G("Include unused interfaces"), }, []argDesc{{ - // TRANSLATORS: This needs to be wrapped in <>s. + // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), - // TRANSLATORS: This should probably not start with a lowercase letter. + // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Show details of a specific interface"), }}) } @@ -69,7 +73,7 @@ if x.Positionals.Interface != "" { // Show one interface in detail. name := string(x.Positionals.Interface) - ifaces, err := Client().Interfaces(&client.InterfaceOptions{ + ifaces, err := x.client.Interfaces(&client.InterfaceOptions{ Names: []string{name}, Doc: true, Plugs: true, @@ -84,7 +88,7 @@ x.showOneInterface(ifaces[0]) } else { // Show an overview of available interfaces. - ifaces, err := Client().Interfaces(&client.InterfaceOptions{ + ifaces, err := x.client.Interfaces(&client.InterfaceOptions{ Connected: !x.ShowAll, }) if err != nil { @@ -179,7 +183,7 @@ for _, name := range names { value := attrs[name] switch value.(type) { - case string, int, bool: + case string, bool, json.Number: fmt.Fprintf(w, "%s %s:\t%v\n", indent, name, value) } } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_interfaces.go snapd-2.37~rc1~14.04/cmd/snap/cmd_interfaces.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_interfaces.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_interfaces.go 2019-01-16 08:36:51.000000000 +0000 @@ -28,18 +28,19 @@ ) type cmdInterfaces struct { + clientMixin Interface string `short:"i"` Positionals struct { Query interfacesSlotOrPlugSpec `skip-help:"true"` } `positional-args:"true"` } -var shortInterfacesHelp = i18n.G("Lists interfaces in the system") +var shortInterfacesHelp = i18n.G("List interfaces' slots and plugs") var longInterfacesHelp = i18n.G(` The interfaces command lists interfaces available in the system. By default all slots and plugs, used and offered by all snaps, are displayed. - + $ snap interfaces : Lists only the specified slot or plug. @@ -50,18 +51,20 @@ $ snap interfaces -i= [] -Filters the complete output so only plugs and/or slots matching the provided details are listed. +Filters the complete output so only plugs and/or slots matching the provided +details are listed. `) func init() { addCommand("interfaces", shortInterfacesHelp, longInterfacesHelp, func() flags.Commander { return &cmdInterfaces{} }, map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. "i": i18n.G("Constrain listing to specific interfaces"), }, []argDesc{{ - // TRANSLATORS: This needs to be wrapped in <>s. + // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(":"), - // TRANSLATORS: This should probably not start with a lowercase letter. + // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Constrain listing to a specific snap or snap:name"), }}) } @@ -71,7 +74,7 @@ return ErrExtraArgs } - ifaces, err := Client().Connections() + ifaces, err := x.client.Connections() if err != nil { return err } @@ -81,11 +84,32 @@ w := tabWriter() defer w.Flush() fmt.Fprintln(w, i18n.G("Slot\tPlug")) + + wantedSnap := x.Positionals.Query.Snap + for _, slot := range ifaces.Slots { - if wanted := x.Positionals.Query.Snap; wanted != "" { - ok := wanted == slot.Snap + if wantedSnap != "" { + var ok bool + if wantedSnap == slot.Snap { + ok = true + } + // Normally snap nicknames are handled internally in the snapd + // layer. This specific command is an exception as it does + // client-side filtering. As a special case, when the user asked + // for the snap "core" but we see the "system" nickname or the + // "snapd" snap, treat that as a match. + // + // The system nickname was returned in 2.35. + // The snapd snap is returned by 2.36+ if snapd snap is installed + // and is the host for implicit interfaces. + if (wantedSnap == "core" || wantedSnap == "snapd" || wantedSnap == "system") && (slot.Snap == "core" || slot.Snap == "snapd" || slot.Snap == "system") { + ok = true + } + for i := 0; i < len(slot.Connections) && !ok; i++ { - ok = wanted == slot.Connections[i].Snap + if wantedSnap == slot.Connections[i].Snap { + ok = true + } } if !ok { continue @@ -97,9 +121,10 @@ if x.Interface != "" && slot.Interface != x.Interface { continue } - // The OS snap is special and enable abbreviated - // display syntax on the slot-side of the connection. - if slot.Snap == "core" || slot.Snap == "ubuntu-core" { + // There are two special snaps, the "core" and "snapd" snaps are + // abbreviated to an empty snap name. The "system" snap name is still + // here in case we talk to older snapd for some reason. + if slot.Snap == "core" || slot.Snap == "snapd" || slot.Snap == "system" { fmt.Fprintf(w, ":%s\t", slot.Name) } else { fmt.Fprintf(w, "%s:%s\t", slot.Snap, slot.Name) @@ -123,8 +148,10 @@ // Plugs are treated differently. Since the loop above already printed each connected // plug, the loop below focuses on printing just the disconnected plugs. for _, plug := range ifaces.Plugs { - if x.Positionals.Query.Snap != "" && x.Positionals.Query.Snap != plug.Snap { - continue + if wantedSnap != "" { + if wantedSnap != plug.Snap { + continue + } } if x.Positionals.Query.Name != "" && x.Positionals.Query.Name != plug.Name { continue diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_interfaces_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_interfaces_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_interfaces_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_interfaces_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -50,7 +50,7 @@ }, }) }) - rest, err := Parser().ParseArgs([]string{"interfaces"}) + rest, err := Parser(Client()).ParseArgs([]string{"interfaces"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + @@ -81,7 +81,7 @@ }, }) }) - rest, err := Parser().ParseArgs([]string{"interfaces"}) + rest, err := Parser(Client()).ParseArgs([]string{"interfaces"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + @@ -132,7 +132,7 @@ }, }) }) - rest, err := Parser().ParseArgs([]string{"interfaces"}) + rest, err := Parser(Client()).ParseArgs([]string{"interfaces"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + @@ -143,7 +143,7 @@ s.SetUpTest(c) // should be the same - rest, err = Parser().ParseArgs([]string{"interfaces", "canonical-pi2"}) + rest, err = Parser(Client()).ParseArgs([]string{"interfaces", "canonical-pi2"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Assert(s.Stdout(), Equals, expectedStdout) @@ -151,7 +151,7 @@ s.SetUpTest(c) // and the same again - rest, err = Parser().ParseArgs([]string{"interfaces", "keyboard-lights"}) + rest, err = Parser(Client()).ParseArgs([]string{"interfaces", "keyboard-lights"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Assert(s.Stdout(), Equals, expectedStdout) @@ -189,7 +189,7 @@ }, }) }) - rest, err := Parser().ParseArgs([]string{"interfaces"}) + rest, err := Parser(Client()).ParseArgs([]string{"interfaces"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + @@ -256,7 +256,7 @@ }, }) }) - rest, err := Parser().ParseArgs([]string{"interfaces"}) + rest, err := Parser(Client()).ParseArgs([]string{"interfaces"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + @@ -278,7 +278,7 @@ "result": client.Connections{ Slots: []client.Slot{ { - Snap: "core", + Snap: "system", Name: "network-listening", Interface: "network-listening", Label: "Ability to be a network service", @@ -302,7 +302,7 @@ Label: "Ability to be a network service", Connections: []client.SlotRef{ { - Snap: "core", + Snap: "system", Name: "network-listening", }, }, @@ -314,7 +314,7 @@ Label: "Ability to be a network service", Connections: []client.SlotRef{ { - Snap: "core", + Snap: "system", Name: "network-listening", }, }, @@ -323,7 +323,7 @@ }, }) }) - rest, err := Parser().ParseArgs([]string{"interfaces"}) + rest, err := Parser(Client()).ParseArgs([]string{"interfaces"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + @@ -372,7 +372,7 @@ }, }) }) - rest, err := Parser().ParseArgs([]string{"interfaces", "-i=serial-port"}) + rest, err := Parser(Client()).ParseArgs([]string{"interfaces", "-i=serial-port"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + @@ -415,7 +415,7 @@ }, }) }) - rest, err := Parser().ParseArgs([]string{"interfaces", "wake-up-alarm"}) + rest, err := Parser(Client()).ParseArgs([]string{"interfaces", "wake-up-alarm"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + @@ -426,6 +426,61 @@ c.Assert(s.Stderr(), Equals, "") } +func (s *SnapSuite) TestConnectionsOfSystemNicknameSnap(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Connections{ + Slots: []client.Slot{ + { + Snap: "system", + Name: "core-support", + Interface: "some-iface", + Connections: []client.PlugRef{{Snap: "core", Name: "core-support-plug"}}, + }, { + Snap: "foo", + Name: "foo-slot", + Interface: "foo-slot-iface", + }, + }, + Plugs: []client.Plug{ + { + Snap: "core", + Name: "core-support-plug", + Interface: "some-iface", + Connections: []client.SlotRef{{Snap: "system", Name: "core-support"}}, + }, + }, + }, + }) + }) + rest, err := Parser(Client()).ParseArgs([]string{"interfaces", "system"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + ":core-support core:core-support-plug\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") + + s.ResetStdStreams() + + // when called with system nickname we get the same output + rest, err = Parser(Client()).ParseArgs([]string{"interfaces", "system"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdoutSystem := "" + + "Slot Plug\n" + + ":core-support core:core-support-plug\n" + c.Assert(s.Stdout(), Equals, expectedStdoutSystem) + c.Assert(s.Stderr(), Equals, "") +} + func (s *SnapSuite) TestConnectionsOfSpecificSnapAndSlot(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") @@ -459,7 +514,7 @@ }, }) }) - rest, err := Parser().ParseArgs([]string{"interfaces", "wake-up-alarm:snooze"}) + rest, err := Parser(Client()).ParseArgs([]string{"interfaces", "wake-up-alarm:snooze"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + @@ -481,7 +536,7 @@ "result": client.Connections{}, }) }) - rest, err := Parser().ParseArgs([]string{"interfaces"}) + rest, err := Parser(Client()).ParseArgs([]string{"interfaces"}) c.Assert(err, ErrorMatches, "no interfaces found") // XXX: not sure why this is returned, I guess that's what happens when a // command Execute returns an error. @@ -523,7 +578,7 @@ }, }) }) - rest, err := Parser().ParseArgs([]string{"interfaces", "-i", "bool-file"}) + rest, err := Parser(Client()).ParseArgs([]string{"interfaces", "-i", "bool-file"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + @@ -552,7 +607,7 @@ defer os.Unsetenv("GO_FLAGS_COMPLETION") expected := []flags.Completion{} - parser := Parser() + parser := Parser(Client()) parser.CompletionHandler = func(obtained []flags.Completion) { c.Check(obtained, DeepEquals, expected) } @@ -572,3 +627,48 @@ c.Assert(s.Stdout(), Equals, "") c.Assert(s.Stderr(), Equals, "") } + +func (s *SnapSuite) TestConnectionsCoreNicknamedSystem(c *C) { + s.checkConnectionsSystemCoreRemapping(c, "core", "system") +} + +func (s *SnapSuite) TestConnectionsSnapdNicknamedSystem(c *C) { + s.checkConnectionsSystemCoreRemapping(c, "snapd", "system") +} + +func (s *SnapSuite) TestConnectionsSnapdNicknamedCore(c *C) { + s.checkConnectionsSystemCoreRemapping(c, "snapd", "core") +} + +func (s *SnapSuite) TestConnectionsCoreSnap(c *C) { + s.checkConnectionsSystemCoreRemapping(c, "core", "core") +} + +func (s *SnapSuite) checkConnectionsSystemCoreRemapping(c *C, apiSnapName, cliSnapName string) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Connections{ + Slots: []client.Slot{ + { + Snap: apiSnapName, + Name: "network", + }, + }, + }, + }) + }) + rest, err := Parser(Client()).ParseArgs([]string{"interfaces", cliSnapName}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + ":network -\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_interface_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_interface_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_interface_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_interface_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -33,29 +33,21 @@ func (s *SnapSuite) TestInterfaceHelp(c *C) { msg := `Usage: - snap.test [OPTIONS] interface [interface-OPTIONS] [] + snap.test interface [interface-OPTIONS] [] The interface command shows details of snap interfaces. If no interface name is provided, a list of interface names with at least one connection is shown, or a list of all interfaces if --all is provided. -Application Options: - --version Print the version and exit - -Help Options: - -h, --help Show this help message - [interface command options] - --attrs Show interface attributes - --all Include unused interfaces + --attrs Show interface attributes + --all Include unused interfaces [interface command arguments] - : Show details of a specific interface + : Show details of a specific interface ` - rest, err := Parser().ParseArgs([]string{"interface", "--help"}) - c.Assert(err.Error(), Equals, msg) - c.Assert(rest, DeepEquals, []string{}) + s.testSubCommandHelp(c, "interface", msg) } func (s *SnapSuite) TestInterfaceListEmpty(c *C) { @@ -71,7 +63,7 @@ "result": []*client.Interface{}, }) }) - rest, err := Parser().ParseArgs([]string{"interface"}) + rest, err := Parser(Client()).ParseArgs([]string{"interface"}) c.Assert(err, ErrorMatches, "no interfaces currently connected") c.Assert(rest, DeepEquals, []string{"interface"}) c.Assert(s.Stdout(), Equals, "") @@ -91,7 +83,7 @@ "result": []*client.Interface{}, }) }) - rest, err := Parser().ParseArgs([]string{"interface", "--all"}) + rest, err := Parser(Client()).ParseArgs([]string{"interface", "--all"}) c.Assert(err, ErrorMatches, "no interfaces found") c.Assert(rest, DeepEquals, []string{"--all"}) // XXX: feels like a bug in go-flags. c.Assert(s.Stdout(), Equals, "") @@ -117,7 +109,7 @@ }}, }) }) - rest, err := Parser().ParseArgs([]string{"interface"}) + rest, err := Parser(Client()).ParseArgs([]string{"interface"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + @@ -150,7 +142,7 @@ }}, }) }) - rest, err := Parser().ParseArgs([]string{"interface", "--all"}) + rest, err := Parser(Client()).ParseArgs([]string{"interface", "--all"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + @@ -180,11 +172,11 @@ {Snap: "deepin-music", Name: "network"}, {Snap: "http", Name: "network"}, }, - Slots: []client.Slot{{Snap: "core", Name: "network"}}, + Slots: []client.Slot{{Snap: "system", Name: "network"}}, }}, }) }) - rest, err := Parser().ParseArgs([]string{"interface", "network"}) + rest, err := Parser(Client()).ParseArgs([]string{"interface", "network"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + @@ -195,7 +187,7 @@ " - deepin-music\n" + " - http\n" + "slots:\n" + - " - core\n" + " - system\n" c.Assert(s.Stdout(), Equals, expectedStdout) c.Assert(s.Stderr(), Equals, "") } @@ -224,12 +216,13 @@ "header": "pin-array", "location": "internal", "path": "/dev/ttyS0", + "number": 1, }, }}, }}, }) }) - rest, err := Parser().ParseArgs([]string{"interface", "--attrs", "serial-port"}) + rest, err := Parser(Client()).ParseArgs([]string{"interface", "--attrs", "serial-port"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedStdout := "" + @@ -241,6 +234,7 @@ " - gizmo-gadget:debug-serial-port (serial port for debugging):\n" + " header: pin-array\n" + " location: internal\n" + + " number: 1\n" + " path: /dev/ttyS0\n" c.Assert(s.Stdout(), Equals, expectedStdout) c.Assert(s.Stderr(), Equals, "") @@ -266,7 +260,7 @@ defer os.Unsetenv("GO_FLAGS_COMPLETION") expected := []flags.Completion{} - parser := Parser() + parser := Parser(Client()) parser.CompletionHandler = func(obtained []flags.Completion) { c.Check(obtained, DeepEquals, expected) } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_keys.go snapd-2.37~rc1~14.04/cmd/snap/cmd_keys.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_keys.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_keys.go 2019-01-16 08:36:51.000000000 +0000 @@ -36,10 +36,16 @@ func init() { cmd := addCommand("keys", i18n.G("List cryptographic keys"), - i18n.G("List cryptographic keys that can be used for signing assertions."), + i18n.G(` +The keys command lists cryptographic keys that can be used for signing +assertions. +`), func() flags.Commander { return &cmdKeys{} - }, map[string]string{"json": i18n.G("Output results in JSON format")}, nil) + }, map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "json": i18n.G("Output results in JSON format"), + }, nil) cmd.hidden = true } @@ -60,7 +66,7 @@ func outputText(keys []Key) error { if len(keys) == 0 { - fmt.Fprintf(Stdout, "No keys registered, see `snapcraft create-key`") + fmt.Fprintf(Stderr, "No keys registered, see `snapcraft create-key`\n") return nil } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_keys_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_keys_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_keys_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_keys_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -25,6 +25,7 @@ "io/ioutil" "os" "path/filepath" + "testing" . "gopkg.in/check.v1" @@ -60,6 +61,9 @@ `) func (s *SnapKeysSuite) SetUpTest(c *C) { + if testing.Short() && s.GnupgCmd == "/usr/bin/gpg2" { + c.Skip("gpg2 does not do short tests") + } s.BaseSnapSuite.SetUpTest(c) s.tempdir = c.MkDir() @@ -87,7 +91,7 @@ } func (s *SnapKeysSuite) TestKeys(c *C) { - rest, err := snap.Parser().ParseArgs([]string{"keys"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"keys"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Check(s.Stdout(), Matches, `Name +SHA3-384 @@ -102,15 +106,15 @@ err := os.RemoveAll(s.tempdir) c.Assert(err, IsNil) - rest, err := snap.Parser().ParseArgs([]string{"keys"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"keys"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) - c.Check(s.Stdout(), Equals, "No keys registered, see `snapcraft create-key`") - c.Check(s.Stderr(), Equals, "") + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "No keys registered, see `snapcraft create-key`\n") } func (s *SnapKeysSuite) TestKeysJSON(c *C) { - rest, err := snap.Parser().ParseArgs([]string{"keys", "--json"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"keys", "--json"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) expectedResponse := []snap.Key{ @@ -132,7 +136,7 @@ func (s *SnapKeysSuite) TestKeysJSONEmpty(c *C) { err := os.RemoveAll(os.Getenv("SNAP_GNUPG_HOME")) c.Assert(err, IsNil) - rest, err := snap.Parser().ParseArgs([]string{"keys", "--json"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"keys", "--json"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Check(s.Stdout(), Equals, "[]\n") diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_known.go snapd-2.37~rc1~14.04/cmd/snap/cmd_known.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_known.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_known.go 2019-01-16 08:36:51.000000000 +0000 @@ -32,6 +32,7 @@ ) type cmdKnown struct { + clientMixin KnownOptions struct { // XXX: how to get a list of assert types for completion? AssertTypeName assertTypeName `required:"true"` @@ -41,7 +42,7 @@ Remote bool `long:"remote"` } -var shortKnownHelp = i18n.G("Shows known assertions of the provided type") +var shortKnownHelp = i18n.G("Show known assertions of the provided type") var longKnownHelp = i18n.G(` The known command shows known assertions of the provided type. If header=value pairs are provided after the assertion type, the assertions @@ -53,14 +54,14 @@ return &cmdKnown{} }, nil, []argDesc{ { - // TRANSLATORS: This needs to be wrapped in <>s. + // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), - // TRANSLATORS: This should probably not start with a lowercase letter. + // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Assertion type name"), }, { - // TRANSLATORS: This needs to be wrapped in <>s. + // TRANSLATORS: This needs to begin with < and end with > name: i18n.G("
"), - // TRANSLATORS: This should probably not start with a lowercase letter. + // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Constrain listing to those matching header=value"), }, }) @@ -112,7 +113,7 @@ if x.Remote { assertions, err = downloadAssertion(string(x.KnownOptions.AssertTypeName), headers) } else { - assertions, err = Client().Known(string(x.KnownOptions.AssertTypeName), headers) + assertions, err = x.client.Known(string(x.KnownOptions.AssertTypeName), headers) } if err != nil { return err diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_known_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_known_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_known_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_known_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -78,7 +78,7 @@ n++ })) - rest, err := snap.Parser().ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical", "model=pi99"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical", "model=pi99"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, mockModelAssertion) @@ -86,7 +86,7 @@ } func (s *SnapSuite) TestKnownRemoteMissingPrimaryKey(c *check.C) { - _, err := snap.Parser().ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical"}) c.Assert(err, check.ErrorMatches, `cannot query remote assertion: must provide primary key: model`) } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_list.go snapd-2.37~rc1~14.04/cmd/snap/cmd_list.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_list.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_list.go 2019-01-16 08:36:51.000000000 +0000 @@ -35,19 +35,28 @@ var shortListHelp = i18n.G("List installed snaps") var longListHelp = i18n.G(` -The list command displays a summary of snaps installed in the current system.`) +The list command displays a summary of snaps installed in the current system. + +A green check mark (given color and unicode support) after a publisher name +indicates that the publisher has been verified. +`) type cmdList struct { + clientMixin Positional struct { Snaps []installedSnapName `positional-arg-name:""` } `positional-args:"yes"` All bool `long:"all"` + colorMixin } func init() { addCommand("list", shortListHelp, longListHelp, func() flags.Commander { return &cmdList{} }, - map[string]string{"all": i18n.G("Show all revisions")}, nil) + colorDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "all": i18n.G("Show all revisions"), + }), nil) } type snapsByName []*client.Snap @@ -56,14 +65,6 @@ func (s snapsByName) Less(i, j int) bool { return s[i].Name < s[j].Name } func (s snapsByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } -func (x *cmdList) Execute(args []string) error { - if len(args) > 0 { - return ErrExtraArgs - } - - return listSnaps(installedSnapNames(x.Positional.Snaps), x.All) -} - var ErrNoMatchingSnaps = errors.New(i18n.G("no matching snaps installed")) // snapd will give us and we want @@ -102,13 +103,17 @@ return ch } -func listSnaps(names []string, all bool) error { - cli := Client() - snaps, err := cli.List(names, &client.ListOptions{All: all}) +func (x *cmdList) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + names := installedSnapNames(x.Positional.Snaps) + snaps, err := x.client.List(names, &client.ListOptions{All: x.All}) if err != nil { if err == client.ErrNoSnapsInstalled { if len(names) == 0 { - fmt.Fprintln(Stderr, i18n.G("No snaps are installed yet. Try \"snap install hello-world\".")) + fmt.Fprintln(Stderr, i18n.G("No snaps are installed yet. Try 'snap install hello-world'.")) return nil } else { return ErrNoMatchingSnaps @@ -120,28 +125,25 @@ } sort.Sort(snapsByName(snaps)) + esc := x.getEscapes() w := tabWriter() - defer w.Flush() - fmt.Fprintln(w, i18n.G("Name\tVersion\tRev\tTracking\tDeveloper\tNotes")) + // TRANSLATORS: the %s is to insert a filler escape sequence (please keep it flush to the column header, with no extra spaces) + fmt.Fprintf(w, i18n.G("Name\tVersion\tRev\tTracking\tPublisher%s\tNotes\n"), fillerPublisher(esc)) for _, snap := range snaps { - // Aid parsing of the output by not leaving the field empty. - dev := snap.Developer - if dev == "" { - dev = "-" - } // doing it this way because otherwise it's a sea of %s\t%s\t%s line := []string{ snap.Name, snap.Version, snap.Revision.String(), fmtChannel(snap.TrackingChannel), - dev, + shortPublisher(esc, snap.Publisher), NotesFromLocal(snap).String(), } fmt.Fprintln(w, strings.Join(line, "\t")) } + w.Flush() return nil } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_list_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_list_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_list_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_list_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -30,22 +30,21 @@ func (s *SnapSuite) TestListHelp(c *check.C) { msg := `Usage: - snap.test [OPTIONS] list [list-OPTIONS] [...] + snap.test list [list-OPTIONS] [...] The list command displays a summary of snaps installed in the current system. -Application Options: - --version Print the version and exit - -Help Options: - -h, --help Show this help message +A green check mark (given color and unicode support) after a publisher name +indicates that the publisher has been verified. [list command options] - --all Show all revisions + --all Show all revisions + --color=[auto|never|always] Use a little bit of color to highlight + some things. (default: auto) + --unicode=[auto|never|always] Use a little bit of Unicode to improve + legibility. (default: auto) ` - rest, err := snap.Parser().ParseArgs([]string{"list", "--help"}) - c.Assert(err.Error(), check.Equals, msg) - c.Assert(rest, check.DeepEquals, []string{}) + s.testSubCommandHelp(c, "list", msg) } func (s *SnapSuite) TestList(c *check.C) { @@ -56,17 +55,17 @@ c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/snaps") c.Check(r.URL.RawQuery, check.Equals, "") - fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "revision":17, "tracking-channel": "potatoes"}]}`) + fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":17, "tracking-channel": "potatoes"}]}`) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) } n++ }) - rest, err := snap.Parser().ParseArgs([]string{"list"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"list"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Tracking +Developer +Notes + c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Tracking +Publisher +Notes foo +4.2 +17 +potatoes +bar +- `) c.Check(s.Stderr(), check.Equals, "") @@ -80,17 +79,17 @@ c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/snaps") c.Check(r.URL.RawQuery, check.Equals, "select=all") - fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "revision":17, "tracking-channel": "stable"}]}`) + fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":17, "tracking-channel": "stable"}]}`) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) } n++ }) - rest, err := snap.Parser().ParseArgs([]string{"list", "--all"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"list", "--all"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Tracking +Developer +Notes + c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Tracking +Publisher +Notes foo +4.2 +17 +stable +bar +- `) c.Check(s.Stderr(), check.Equals, "") @@ -110,11 +109,11 @@ n++ }) - rest, err := snap.Parser().ParseArgs([]string{"list"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"list"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, "") - c.Check(s.Stderr(), check.Equals, "No snaps are installed yet. Try \"snap install hello-world\".\n") + c.Check(s.Stderr(), check.Equals, "No snaps are installed yet. Try 'snap install hello-world'.\n") } func (s *SnapSuite) TestListEmptyWithQuery(c *check.C) { @@ -131,7 +130,7 @@ n++ }) - _, err := snap.Parser().ParseArgs([]string{"list", "quux"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"list", "quux"}) c.Assert(err, check.ErrorMatches, `no matching snaps installed`) } @@ -150,7 +149,7 @@ n++ }) - _, err := snap.Parser().ParseArgs([]string{"list", "quux"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"list", "quux"}) c.Assert(err, check.ErrorMatches, "no matching snaps installed") } @@ -162,17 +161,17 @@ c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/snaps") c.Check(r.URL.Query().Get("snaps"), check.Equals, "foo") - fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "revision":17, "tracking-channel": "1.10/stable/fix1234"}]}`) + fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":17, "tracking-channel": "1.10/stable/fix1234"}]}`) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) } n++ }) - rest, err := snap.Parser().ParseArgs([]string{"list", "foo"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"list", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Tracking +Developer +Notes + c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Tracking +Publisher +Notes foo +4.2 +17 +1\.10/stable/… +bar +- `) c.Check(s.Stderr(), check.Equals, "") @@ -188,7 +187,7 @@ c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/snaps") fmt.Fprintln(w, `{"type": "sync", "result": [ -{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "revision":17, "trymode": true} +{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":17, "trymode": true} ,{"name": "dm1", "status": "active", "version": "5", "revision":1, "devmode": true, "confinement": "devmode"} ,{"name": "dm2", "status": "active", "version": "5", "revision":1, "devmode": true, "confinement": "strict"} ,{"name": "cf1", "status": "active", "version": "6", "revision":2, "confinement": "devmode", "jailmode": true} @@ -199,10 +198,10 @@ n++ }) - rest, err := snap.Parser().ParseArgs([]string{"list"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"list"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `(?ms)^Name +Version +Rev +Tracking +Developer +Notes$`) + c.Check(s.Stdout(), check.Matches, `(?ms)^Name +Version +Rev +Tracking +Publisher +Notes$`) c.Check(s.Stdout(), check.Matches, `(?ms).*^foo +4.2 +17 +- +bar +try$`) c.Check(s.Stdout(), check.Matches, `(?ms).*^dm1 +.* +devmode$`) c.Check(s.Stdout(), check.Matches, `(?ms).*^dm2 +.* +devmode$`) diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_login.go snapd-2.37~rc1~14.04/cmd/snap/cmd_login.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_login.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_login.go 2019-01-16 08:36:51.000000000 +0000 @@ -31,21 +31,24 @@ ) type cmdLogin struct { + clientMixin Positional struct { Email string } `positional-args:"yes"` } -var shortLoginHelp = i18n.G("Authenticates on snapd and the store") +var shortLoginHelp = i18n.G("Authenticate to snapd and the store") var longLoginHelp = i18n.G(` -The login command authenticates on snapd and the snap store and saves credentials -into the ~/.snap/auth.json file. Further communication with snapd will then be made -using those credentials. +The login command authenticates the user to snapd and the snap store, and saves +credentials into the ~/.snap/auth.json file. Further communication with snapd +will then be made using those credentials. + +It's not necessary to log in to interact with snapd. Doing so, however, enables +purchasing of snaps using 'snap buy', as well as some some developer-oriented +features as detailed in the help for the find, install and refresh commands. -Login only works for local users in the sudo, admin or wheel groups. - -An account can be setup at https://login.ubuntu.com +An account can be set up at https://login.ubuntu.com `) func init() { @@ -55,14 +58,14 @@ func() flags.Commander { return &cmdLogin{} }, nil, []argDesc{{ - // TRANSLATORS: This is a noun, and it needs to be wrapped in <>s. + // TRANSLATORS: This is a noun, and it needs to begin with < and end with > name: i18n.G(""), - // TRANSLATORS: This should probably not start with a lowercase letter. + // TRANSLATORS: This should not start with a lowercase letter (unless it's "login.ubuntu.com") desc: i18n.G("The login.ubuntu.com email to login as"), }}) } -func requestLoginWith2faRetry(email, password string) error { +func requestLoginWith2faRetry(cli *client.Client, email, password string) error { var otp []byte var err error @@ -72,7 +75,6 @@ i18n.G("Wrong again. Once more: "), } - cli := Client() reader := bufio.NewReader(nil) for i := 0; ; i++ { @@ -92,7 +94,7 @@ } } -func requestLogin(email string) error { +func requestLogin(cli *client.Client, email string) error { fmt.Fprint(Stdout, fmt.Sprintf(i18n.G("Password of %q: "), email)) password, err := ReadPassword(0) fmt.Fprint(Stdout, "\n") @@ -101,7 +103,7 @@ } // strings.TrimSpace needed because we get \r from the pty in the tests - return requestLoginWith2faRetry(email, strings.TrimSpace(string(password))) + return requestLoginWith2faRetry(cli, email, strings.TrimSpace(string(password))) } func (x *cmdLogin) Execute(args []string) error { @@ -109,6 +111,10 @@ return ErrExtraArgs } + //TRANSLATORS: after the "... at" follows a URL in the next line + fmt.Fprint(Stdout, i18n.G("Personal information is handled as per our privacy notice at\n")) + fmt.Fprint(Stdout, "https://www.ubuntu.com/legal/dataprivacy/snap-store\n\n") + email := x.Positional.Email if email == "" { fmt.Fprint(Stdout, i18n.G("Email address: ")) @@ -119,7 +125,7 @@ email = string(in) } - err := requestLogin(email) + err := requestLogin(x.client, email) if err != nil { return err } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_login_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_login_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_login_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_login_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -54,10 +54,13 @@ // send the password s.password = "some-password\n" - rest, err := snap.Parser().ParseArgs([]string{"login", "foo@example.com"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"login", "foo@example.com"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) - c.Check(s.Stdout(), Equals, `Password of "foo@example.com": + c.Check(s.Stdout(), Equals, `Personal information is handled as per our privacy notice at +https://www.ubuntu.com/legal/dataprivacy/snap-store + +Password of "foo@example.com": Login successful `) c.Check(s.Stderr(), Equals, "") @@ -73,13 +76,16 @@ // send the password s.password = "some-password" - rest, err := snap.Parser().ParseArgs([]string{"login"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"login"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) // test slightly ugly, on a real system STDOUT will be: // Email address: foo@example.com\n // because the input to stdin is echoed - c.Check(s.Stdout(), Equals, `Email address: Password of "foo@example.com": + c.Check(s.Stdout(), Equals, `Personal information is handled as per our privacy notice at +https://www.ubuntu.com/legal/dataprivacy/snap-store + +Email address: Password of "foo@example.com": Login successful `) c.Check(s.Stderr(), Equals, "") diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_logout.go snapd-2.37~rc1~14.04/cmd/snap/cmd_logout.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_logout.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_logout.go 2019-01-16 08:36:51.000000000 +0000 @@ -25,11 +25,15 @@ "github.com/snapcore/snapd/i18n" ) -type cmdLogout struct{} +type cmdLogout struct { + clientMixin +} -var shortLogoutHelp = i18n.G("Log out of the store") +var shortLogoutHelp = i18n.G("Log out of snapd and the store") -var longLogoutHelp = i18n.G("This command logs the current user out of the store") +var longLogoutHelp = i18n.G(` +The logout command logs the current user out of snapd and the store. +`) func init() { addCommand("logout", @@ -45,5 +49,5 @@ return ErrExtraArgs } - return Client().Logout() + return cmd.client.Logout() } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_managed.go snapd-2.37~rc1~14.04/cmd/snap/cmd_managed.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_managed.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_managed.go 2019-01-16 08:36:51.000000000 +0000 @@ -27,13 +27,15 @@ "github.com/jessevdk/go-flags" ) -var shortIsManagedHelp = i18n.G("Prints whether system is managed") +var shortIsManagedHelp = i18n.G("Print whether the system is managed") var longIsManagedHelp = i18n.G(` The managed command will print true or false informing whether snapd has registered users. `) -type cmdIsManaged struct{} +type cmdIsManaged struct { + clientMixin +} func init() { cmd := addCommand("managed", shortIsManagedHelp, longIsManagedHelp, func() flags.Commander { return &cmdIsManaged{} }, nil, nil) @@ -45,7 +47,7 @@ return ErrExtraArgs } - sysinfo, err := Client().SysInfo() + sysinfo, err := cmd.client.SysInfo() if err != nil { return err } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_managed_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_managed_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_managed_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_managed_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -39,7 +39,7 @@ fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {"managed":%v}}`, managed) }) - _, err := snap.Parser().ParseArgs([]string{"managed"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"managed"}) c.Assert(err, IsNil) c.Check(s.Stdout(), Equals, fmt.Sprintf("%v\n", managed)) } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_pack.go snapd-2.37~rc1~14.04/cmd/snap/cmd_pack.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_pack.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_pack.go 2019-01-16 08:36:51.000000000 +0000 @@ -21,34 +21,66 @@ import ( "fmt" + "path/filepath" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/pack" ) type packCmd struct { - Positional struct { + CheckSkeleton bool `long:"check-skeleton"` + Filename string `long:"filename"` + Positional struct { SnapDir string `positional-arg-name:""` TargetDir string `positional-arg-name:""` } `positional-args:"yes"` } -var shortPackHelp = i18n.G("Pack the given target dir as a snap") +var shortPackHelp = i18n.G("Pack the given directory as a snap") var longPackHelp = i18n.G(` -The pack command packs the given snap-dir as a snap.`) +The pack command packs the given snap-dir as a snap and writes the result to +target-dir. If target-dir is omitted, the result is written to current +directory. If both source-dir and target-dir are omitted, the pack command packs +the current directory. + +The default file name for a snap can be derived entirely from its snap.yaml, but +in some situations it's simpler for a script to feed the filename in. In those +cases, --filename can be given to override the default. If this filename is +not absolute it will be taken as relative to target-dir. + +When used with --check-skeleton, pack only checks whether snap-dir contains +valid snap metadata and raises an error otherwise. Application commands listed +in snap metadata file, but appearing with incorrect permission bits result in an +error. Commands that are missing from snap-dir are listed in diagnostic +messages. +`) func init() { - addCommand("pack", + cmd := addCommand("pack", shortPackHelp, longPackHelp, func() flags.Commander { return &packCmd{} - }, nil, nil) + }, map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "check-skeleton": i18n.G("Validate snap-dir metadata only"), + // TRANSLATORS: This should not start with a lowercase letter. + "filename": i18n.G("Output to this filename"), + }, nil) + cmd.extra = func(cmd *flags.Command) { + // TRANSLATORS: this describes the default filename for a snap, e.g. core_16-2.35.2_amd64.snap + cmd.FindOptionByLongName("filename").DefaultMask = i18n.G("__.snap") + } } func (x *packCmd) Execute([]string) error { + if x.Positional.TargetDir != "" && x.Filename != "" && filepath.IsAbs(x.Filename) { + return fmt.Errorf(i18n.G("you can't specify an absolute filename while also specifying target dir.")) + } + if x.Positional.SnapDir == "" { x.Positional.SnapDir = "." } @@ -56,11 +88,22 @@ x.Positional.TargetDir = "." } - snapPath, err := pack.Snap(x.Positional.SnapDir, x.Positional.TargetDir) + if x.CheckSkeleton { + err := pack.CheckSkeleton(x.Positional.SnapDir) + if err == snap.ErrMissingPaths { + return nil + } + return err + } + + snapPath, err := pack.Snap(x.Positional.SnapDir, x.Positional.TargetDir, x.Filename) if err != nil { - return fmt.Errorf("cannot pack %q: %v", x.Positional.SnapDir, err) + // TRANSLATORS: the %q is the snap-dir (the first positional + // argument to the command); the %v is an error + return fmt.Errorf(i18n.G("cannot pack %q: %v"), x.Positional.SnapDir, err) } - fmt.Fprintf(Stdout, "built: %s\n", snapPath) + // TRANSLATORS: %s is the path to the built snap file + fmt.Fprintf(Stdout, i18n.G("built: %s\n"), snapPath) return nil } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_pack_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_pack_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_pack_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_pack_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,103 @@ +package main_test + +import ( + "io/ioutil" + "os" + "path/filepath" + + "gopkg.in/check.v1" + + snaprun "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/logger" +) + +const packSnapYaml = `name: hello +version: 1.0.1 +apps: + app: + command: bin/hello +` + +func makeSnapDirForPack(c *check.C, snapYaml string) string { + tempdir := c.MkDir() + c.Assert(os.Chmod(tempdir, 0755), check.IsNil) + + // use meta/snap.yaml + metaDir := filepath.Join(tempdir, "meta") + err := os.Mkdir(metaDir, 0755) + c.Assert(err, check.IsNil) + err = ioutil.WriteFile(filepath.Join(metaDir, "snap.yaml"), []byte(snapYaml), 0644) + c.Assert(err, check.IsNil) + + return tempdir +} + +func (s *SnapSuite) TestPackCheckSkeletonNoAppFiles(c *check.C) { + _, r := logger.MockLogger() + defer r() + + snapDir := makeSnapDirForPack(c, packSnapYaml) + + // check-skeleton does not fail due to missing files + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", "--check-skeleton", snapDir}) + c.Assert(err, check.IsNil) +} + +func (s *SnapSuite) TestPackCheckSkeletonBadMeta(c *check.C) { + // no snap name + snapYaml := ` +version: foobar +apps: +` + snapDir := makeSnapDirForPack(c, snapYaml) + + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", "--check-skeleton", snapDir}) + c.Assert(err, check.ErrorMatches, `cannot validate snap "": snap name cannot be empty`) +} + +func (s *SnapSuite) TestPackCheckSkeletonConflictingCommonID(c *check.C) { + // conflicting common-id + snapYaml := `name: foo +version: foobar +apps: + foo: + common-id: org.foo.foo + bar: + common-id: org.foo.foo +` + snapDir := makeSnapDirForPack(c, snapYaml) + + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", "--check-skeleton", snapDir}) + c.Assert(err, check.ErrorMatches, `cannot validate snap "foo": application ("bar" common-id "org.foo.foo" must be unique, already used by application "foo"|"foo" common-id "org.foo.foo" must be unique, already used by application "bar")`) +} + +func (s *SnapSuite) TestPackPacksFailsForMissingPaths(c *check.C) { + _, r := logger.MockLogger() + defer r() + + snapDir := makeSnapDirForPack(c, packSnapYaml) + + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", snapDir, snapDir}) + c.Assert(err, check.ErrorMatches, ".* snap is unusable due to missing files") +} + +func (s *SnapSuite) TestPackPacksASnap(c *check.C) { + snapDir := makeSnapDirForPack(c, packSnapYaml) + + const helloBinContent = `#!/bin/sh +printf "hello world" +` + // an example binary + binDir := filepath.Join(snapDir, "bin") + err := os.Mkdir(binDir, 0755) + c.Assert(err, check.IsNil) + err = ioutil.WriteFile(filepath.Join(binDir, "hello"), []byte(helloBinContent), 0755) + c.Assert(err, check.IsNil) + + _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", snapDir, snapDir}) + c.Assert(err, check.IsNil) + + matches, err := filepath.Glob(snapDir + "/hello*.snap") + c.Assert(err, check.IsNil) + c.Assert(matches, check.HasLen, 1) +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_paths.go snapd-2.37~rc1~14.04/cmd/snap/cmd_paths.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_paths.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_paths.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,62 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "fmt" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n" +) + +var pathsHelp = i18n.G("Print system paths") +var longPathsHelp = i18n.G(` +The paths command prints the list of paths detected and used by snapd. +`) + +type cmdPaths struct{} + +func init() { + addDebugCommand("paths", pathsHelp, longPathsHelp, func() flags.Commander { + return &cmdPaths{} + }, nil, nil) +} + +func (cmd cmdPaths) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + // TODO: include paths reported by snap-confine + for _, p := range []struct { + name string + path string + }{ + {"SNAPD_MOUNT", dirs.SnapMountDir}, + {"SNAPD_BIN", dirs.SnapBinariesDir}, + {"SNAPD_LIBEXEC", dirs.DistroLibExecDir}, + } { + fmt.Fprintf(Stdout, "%s=%s\n", p.name, p.path) + } + + return nil +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_paths_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_paths_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_paths_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_paths_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,90 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main_test + +import ( + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/release" +) + +func (s *SnapSuite) TestPathsUbuntu(c *C) { + restore := release.MockReleaseInfo(&release.OS{ID: "ubuntu"}) + defer restore() + defer dirs.SetRootDir("/") + + dirs.SetRootDir("/") + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "paths"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, ""+ + "SNAPD_MOUNT=/snap\n"+ + "SNAPD_BIN=/snap/bin\n"+ + "SNAPD_LIBEXEC=/usr/lib/snapd\n") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestPathsFedora(c *C) { + restore := release.MockReleaseInfo(&release.OS{ID: "fedora"}) + defer restore() + defer dirs.SetRootDir("/") + + dirs.SetRootDir("/") + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "paths"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, ""+ + "SNAPD_MOUNT=/var/lib/snapd/snap\n"+ + "SNAPD_BIN=/var/lib/snapd/snap/bin\n"+ + "SNAPD_LIBEXEC=/usr/libexec/snapd\n") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestPathsArch(c *C) { + defer dirs.SetRootDir("/") + + // old /etc/os-release contents + restore := release.MockReleaseInfo(&release.OS{ID: "arch", IDLike: []string{"archlinux"}}) + defer restore() + + dirs.SetRootDir("/") + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "paths"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, ""+ + "SNAPD_MOUNT=/var/lib/snapd/snap\n"+ + "SNAPD_BIN=/var/lib/snapd/snap/bin\n"+ + "SNAPD_LIBEXEC=/usr/lib/snapd\n") + c.Assert(s.Stderr(), Equals, "") + + s.ResetStdStreams() + + // new contents, as set by filesystem-2018.12-1 + restore = release.MockReleaseInfo(&release.OS{ID: "archlinux"}) + defer restore() + + dirs.SetRootDir("/") + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"debug", "paths"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, ""+ + "SNAPD_MOUNT=/var/lib/snapd/snap\n"+ + "SNAPD_BIN=/var/lib/snapd/snap/bin\n"+ + "SNAPD_LIBEXEC=/usr/lib/snapd\n") + c.Assert(s.Stderr(), Equals, "") +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_prefer.go snapd-2.37~rc1~14.04/cmd/snap/cmd_prefer.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_prefer.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_prefer.go 2019-01-16 08:36:51.000000000 +0000 @@ -26,22 +26,23 @@ ) type cmdPrefer struct { + waitMixin Positionals struct { Snap installedSnapName `required:"yes"` } `positional-args:"true"` } -var shortPreferHelp = i18n.G("Prefer aliases from a snap and disable conflicts") +var shortPreferHelp = i18n.G("Enable aliases from a snap, disabling any conflicting aliases") var longPreferHelp = i18n.G(` The prefer command enables all aliases of the given snap in preference to conflicting aliases of other snaps whose aliases will be disabled -(removed for manual ones). +(or removed, for manual ones). `) func init() { addCommand("prefer", shortPreferHelp, longPreferHelp, func() flags.Commander { return &cmdPrefer{} - }, nil, []argDesc{ + }, waitDescs, []argDesc{ {name: ""}, }) } @@ -51,19 +52,17 @@ return ErrExtraArgs } - cli := Client() - id, err := cli.Prefer(string(x.Positionals.Snap)) + id, err := x.client.Prefer(string(x.Positionals.Snap)) if err != nil { return err } - - chg, err := wait(cli, id) + chg, err := x.wait(id) if err != nil { - return err - } - if err := showAliasChanges(chg); err != nil { + if err == noWait { + return nil + } return err } - return nil + return showAliasChanges(chg) } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_prefer_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_prefer_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_prefer_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_prefer_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -30,21 +30,17 @@ func (s *SnapSuite) TestPreferHelp(c *C) { msg := `Usage: - snap.test [OPTIONS] prefer [] + snap.test prefer [prefer-OPTIONS] [] The prefer command enables all aliases of the given snap in preference to conflicting aliases of other snaps whose aliases will be disabled -(removed for manual ones). +(or removed, for manual ones). -Application Options: - --version Print the version and exit - -Help Options: - -h, --help Show this help message +[prefer command options] + --no-wait Do not wait for the operation to finish but just print the + change id. ` - rest, err := Parser().ParseArgs([]string{"prefer", "--help"}) - c.Assert(err.Error(), Equals, msg) - c.Assert(rest, DeepEquals, []string{}) + s.testSubCommandHelp(c, "prefer", msg) } func (s *SnapSuite) TestPrefer(c *C) { @@ -64,7 +60,7 @@ c.Fatalf("unexpected path %q", r.URL.Path) } }) - rest, err := Parser().ParseArgs([]string{"prefer", "some-snap"}) + rest, err := Parser(Client()).ParseArgs([]string{"prefer", "some-snap"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Assert(s.Stdout(), Equals, ""+ diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_prepare_image.go snapd-2.37~rc1~14.04/cmd/snap/cmd_prepare_image.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_prepare_image.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_prepare_image.go 2019-01-16 08:36:51.000000000 +0000 @@ -40,23 +40,28 @@ func init() { cmd := addCommand("prepare-image", - i18n.G("Prepare a snappy image"), - i18n.G("Prepare a snappy image"), + i18n.G("Prepare a core device image"), + i18n.G(` +The prepare-image command performs some of the steps necessary for creating +core device images. +`), func() flags.Commander { return &cmdPrepareImage{} }, map[string]string{ - "extra-snaps": "Extra snaps to be installed", - "channel": "The channel to use", + // TRANSLATORS: This should not start with a lowercase letter. + "extra-snaps": i18n.G("Extra snaps to be installed"), + // TRANSLATORS: This should not start with a lowercase letter. + "channel": i18n.G("The channel to use"), }, []argDesc{ { - // TRANSLATORS: This needs to be wrapped in <>s. + // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), - // TRANSLATORS: This should probably not start with a lowercase letter. + // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("The model assertion name"), }, { - // TRANSLATORS: This needs to be wrapped in <>s. + // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), - // TRANSLATORS: This should probably not start with a lowercase letter. + // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("The output directory"), }, }) diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_repair_repairs.go snapd-2.37~rc1~14.04/cmd/snap/cmd_repair_repairs.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_repair_repairs.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_repair_repairs.go 2019-01-16 08:36:51.000000000 +0000 @@ -54,7 +54,7 @@ } `positional-args:"yes"` } -var shortRepairHelp = i18n.G("Shows specific repairs") +var shortRepairHelp = i18n.G("Show specific repairs") var longRepairHelp = i18n.G(` The repair command shows the details about one or multiple repairs. `) diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_repair_repairs_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_repair_repairs_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_repair_repairs_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_repair_repairs_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -45,7 +45,7 @@ mockSnapRepair := mockSnapRepair(c) defer mockSnapRepair.Restore() - _, err := snap.Parser().ParseArgs([]string{"repair", "canonical-1"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"repair", "canonical-1"}) c.Assert(err, IsNil) c.Check(mockSnapRepair.Calls(), DeepEquals, [][]string{ {"snap-repair", "show", "canonical-1"}, @@ -59,7 +59,7 @@ mockSnapRepair := mockSnapRepair(c) defer mockSnapRepair.Restore() - _, err := snap.Parser().ParseArgs([]string{"repairs"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"repairs"}) c.Assert(err, IsNil) c.Check(mockSnapRepair.Calls(), DeepEquals, [][]string{ {"snap-repair", "list"}, diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_run.go snapd-2.37~rc1~14.04/cmd/snap/cmd_run.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_run.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_run.go 2019-01-16 08:36:51.000000000 +0000 @@ -23,6 +23,7 @@ "bufio" "fmt" "io" + "io/ioutil" "os" "os/exec" "os/user" @@ -33,36 +34,47 @@ "syscall" "time" + "github.com/godbus/dbus" "github.com/jessevdk/go-flags" + "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/osutil/strace" + "github.com/snapcore/snapd/selinux" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snapenv" + "github.com/snapcore/snapd/strutil/shlex" "github.com/snapcore/snapd/timeutil" "github.com/snapcore/snapd/x11" ) var ( - syscallExec = syscall.Exec - userCurrent = user.Current - osGetenv = os.Getenv - timeNow = time.Now + syscallExec = syscall.Exec + userCurrent = user.Current + osGetenv = os.Getenv + timeNow = time.Now + selinuxIsEnabled = selinux.IsEnabled + selinuxVerifyPathContext = selinux.VerifyPathContext + selinuxRestoreContext = selinux.RestoreContext ) type cmdRun struct { + clientMixin Command string `long:"command" hidden:"yes"` HookName string `long:"hook" hidden:"yes"` Revision string `short:"r" default:"unset" hidden:"yes"` Shell bool `long:"shell" ` + // This options is both a selector (use or don't use strace) and it // can also carry extra options for strace. This is why there is // "default" and "optional-value" to distinguish this. - Strace string `long:"strace" optional:"true" optional-value:"with-strace" default:"no-strace" default-mask:"-"` - Gdb bool `long:"gdb"` + Strace string `long:"strace" optional:"true" optional-value:"with-strace" default:"no-strace" default-mask:"-"` + Gdb bool `long:"gdb"` + TraceExec bool `long:"trace-exec"` // not a real option, used to check if cmdRun is initialized by // the parser @@ -73,22 +85,34 @@ func init() { addCommand("run", i18n.G("Run the given snap command"), - i18n.G("Run the given snap command with the right confinement and environment"), + i18n.G(` +The run command executes the given snap command with the right confinement +and environment. +`), func() flags.Commander { return &cmdRun{} }, map[string]string{ - "command": i18n.G("Alternative command to run"), - "hook": i18n.G("Hook to run"), - "r": i18n.G("Use a specific snap revision when running hook"), - "shell": i18n.G("Run a shell instead of the command (useful for debugging)"), - "strace": i18n.G("Run the command under strace (useful for debugging). Extra strace options can be specified as well here."), - "gdb": i18n.G("Run the command with gdb"), - "timer": i18n.G("Run as a timer service with given schedule"), + // TRANSLATORS: This should not start with a lowercase letter. + "command": i18n.G("Alternative command to run"), + // TRANSLATORS: This should not start with a lowercase letter. + "hook": i18n.G("Hook to run"), + // TRANSLATORS: This should not start with a lowercase letter. + "r": i18n.G("Use a specific snap revision when running hook"), + // TRANSLATORS: This should not start with a lowercase letter. + "shell": i18n.G("Run a shell instead of the command (useful for debugging)"), + // TRANSLATORS: This should not start with a lowercase letter. + "strace": i18n.G("Run the command under strace (useful for debugging). Extra strace options can be specified as well here. Pass --raw to strace early snap helpers."), + // TRANSLATORS: This should not start with a lowercase letter. + "gdb": i18n.G("Run the command with gdb"), + // TRANSLATORS: This should not start with a lowercase letter. + "timer": i18n.G("Run as a timer service with given schedule"), + // TRANSLATORS: This should not start with a lowercase letter. + "trace-exec": i18n.G("Display exec calls timing data"), "parser-ran": "", }, nil) } -func maybeWaitForSecurityProfileRegeneration() error { +func maybeWaitForSecurityProfileRegeneration(cli *client.Client) error { // check if the security profiles key has changed, if so, we need // to wait for snapd to re-generate all profiles mismatch, err := interfaces.SystemKeyMismatch() @@ -121,7 +145,6 @@ } } - cli := Client() for i := 0; i < timeout; i++ { if _, err := cli.SysInfo(); err == nil { return nil @@ -159,7 +182,7 @@ return fmt.Errorf(i18n.G("too many arguments for hook %q: %s"), x.HookName, strings.Join(args, " ")) } - if err := maybeWaitForSecurityProfileRegeneration(); err != nil { + if err := maybeWaitForSecurityProfileRegeneration(x.client); err != nil { return err } @@ -233,28 +256,16 @@ return actualApp, argsOut } -func getSnapInfo(snapName string, revision snap.Revision) (*snap.Info, error) { +func getSnapInfo(snapName string, revision snap.Revision) (info *snap.Info, err error) { if revision.Unset() { - curFn := filepath.Join(dirs.SnapMountDir, snapName, "current") - realFn, err := os.Readlink(curFn) - if err != nil { - return nil, fmt.Errorf("cannot find current revision for snap %s: %s", snapName, err) - } - rev := filepath.Base(realFn) - revision, err = snap.ParseRevision(rev) - if err != nil { - return nil, fmt.Errorf("cannot read revision %s: %s", rev, err) - } - } - - info, err := snap.ReadInfo(snapName, &snap.SideInfo{ - Revision: revision, - }) - if err != nil { - return nil, err + info, err = snap.ReadCurrentInfo(snapName) + } else { + info, err = snap.ReadInfo(snapName, &snap.SideInfo{ + Revision: revision, + }) } - return info, nil + return info, err } func createOrUpdateUserDataSymlink(info *snap.Info, usr *user.User) error { @@ -308,35 +319,82 @@ } // see snapenv.User - userData := info.UserDataDir(usr.HomeDir) - commonUserData := info.UserCommonDataDir(usr.HomeDir) - for _, d := range []string{userData, commonUserData} { + instanceUserData := info.UserDataDir(usr.HomeDir) + instanceCommonUserData := info.UserCommonDataDir(usr.HomeDir) + createDirs := []string{instanceUserData, instanceCommonUserData} + if info.InstanceKey != "" { + // parallel instance snaps get additional mapping in their mount + // namespace, namely /home/joe/snap/foo_bar -> + // /home/joe/snap/foo, make sure that the mount point exists and + // is owned by the user + snapUserDir := snap.UserSnapDir(usr.HomeDir, info.SnapName()) + createDirs = append(createDirs, snapUserDir) + } + for _, d := range createDirs { if err := os.MkdirAll(d, 0755); err != nil { // TRANSLATORS: %q is the directory whose creation failed, %v the error message return fmt.Errorf(i18n.G("cannot create %q: %v"), d, err) } } - return createOrUpdateUserDataSymlink(info, usr) + if err := createOrUpdateUserDataSymlink(info, usr); err != nil { + return err + } + + return maybeRestoreSecurityContext(usr) +} + +// maybeRestoreSecurityContext attempts to restore security context of ~/snap on +// systems where it's applicable +func maybeRestoreSecurityContext(usr *user.User) error { + snapUserHome := filepath.Join(usr.HomeDir, dirs.UserHomeSnapDir) + enabled, err := selinuxIsEnabled() + if err != nil { + return fmt.Errorf("cannot determine SELinux status: %v", err) + } + if !enabled { + logger.Debugf("SELinux not enabled") + return nil + } + + match, err := selinuxVerifyPathContext(snapUserHome) + if err != nil { + return fmt.Errorf("failed to verify SELinux context of %v: %v", snapUserHome, err) + } + if match { + return nil + } + logger.Noticef("restoring default SELinux context of %v", snapUserHome) + + if err := selinuxRestoreContext(snapUserHome, selinux.RestoreMode{Recursive: true}); err != nil { + return fmt.Errorf("cannot restore SELinux context of %v: %v", snapUserHome, err) + } + return nil } func (x *cmdRun) useStrace() bool { return x.ParserRan == 1 && x.Strace != "no-strace" } -func (x *cmdRun) straceOpts() []string { +func (x *cmdRun) straceOpts() (opts []string, raw bool, err error) { if x.Strace == "with-strace" { - return nil + return nil, false, nil } - var opts []string - // TODO: use shlex? - for _, opt := range strings.Split(x.Strace, " ") { - if strings.TrimSpace(opt) != "" { - opts = append(opts, opt) + split, err := shlex.Split(x.Strace) + if err != nil { + return nil, false, err + } + + opts = make([]string, 0, len(split)) + for _, opt := range split { + if opt == "--raw" { + raw = true + continue } + opts = append(opts, opt) } - return opts + return opts, raw, nil } func (x *cmdRun) snapRunApp(snapApp string, args []string) error { @@ -532,47 +590,99 @@ return targetPath, nil } -func straceCmd() ([]string, error) { - current, err := user.Current() - if err != nil { - return nil, err +func activateXdgDocumentPortal(info *snap.Info, snapApp, hook string) error { + // Don't do anything for apps or hooks that don't plug the + // desktop interface + // + // NOTE: This check is imperfect because we don't really know + // if the interface is connected or not but this is an + // acceptable compromise for not having to communicate with + // snapd in snap run. In a typical desktop session the + // document portal can be in use by many applications, not + // just by snaps, so this is at most, pre-emptively using some + // extra memory. + var plugs map[string]*snap.PlugInfo + if hook != "" { + plugs = info.Hooks[hook].Plugs + } else { + _, appName := snap.SplitSnapApp(snapApp) + plugs = info.Apps[appName].Plugs + } + plugsDesktop := false + for _, plug := range plugs { + if plug.Interface == "desktop" { + plugsDesktop = true + break + } + } + if !plugsDesktop { + return nil } - sudoPath, err := exec.LookPath("sudo") + + u, err := userCurrent() if err != nil { - return nil, fmt.Errorf("cannot use strace without sudo: %s", err) + return fmt.Errorf(i18n.G("cannot get the current user: %s"), err) + } + xdgRuntimeDir := filepath.Join(dirs.XdgRuntimeDirBase, u.Uid) + + // If $XDG_RUNTIME_DIR/doc appears to be a mount point, assume + // that the document portal is up and running. + expectedMountPoint := filepath.Join(xdgRuntimeDir, "doc") + if mounted, err := osutil.IsMounted(expectedMountPoint); err != nil { + logger.Noticef("Could not check document portal mount state: %s", err) + } else if mounted { + return nil + } + + // If there is no session bus, our job is done. We check this + // manually to avoid dbus.SessionBus() auto-launching a new + // bus. + busAddress := osGetenv("DBUS_SESSION_BUS_ADDRESS") + if len(busAddress) == 0 { + return nil } - // Try strace from the snap first, we use new syscalls like - // "_newselect" that are known to not work with the strace of e.g. - // ubuntu 14.04. + // We've previously tried to start the document portal and + // were told the service is unknown: don't bother connecting + // to the session bus again. // - // TODO: some architectures do not have some syscalls (e.g. - // s390x does not have _newselect). In - // https://github.com/strace/strace/issues/57 options are - // discussed. We could use "-e trace=?syscall" but that is - // only available since strace 4.17 which is not even in - // ubutnu 17.10. - var stracePath string - cand := filepath.Join(dirs.SnapMountDir, "strace-static", "current", "bin", "strace") - if osutil.FileExists(cand) { - stracePath = cand + // As the file is in $XDG_RUNTIME_DIR, it will be cleared over + // full logout/login or reboot cycles. + portalsUnavailableFile := filepath.Join(xdgRuntimeDir, ".portals-unavailable") + if osutil.FileExists(portalsUnavailableFile) { + return nil } - if stracePath == "" { - stracePath, err = exec.LookPath("strace") - if err != nil { - return nil, fmt.Errorf("cannot find an installed strace, please try: `snap install strace-static`") + + conn, err := dbus.SessionBus() + if err != nil { + return err + } + + portal := conn.Object("org.freedesktop.portal.Documents", + "/org/freedesktop/portal/documents") + var mountPoint []byte + if err := portal.Call("org.freedesktop.portal.Documents.GetMountPoint", 0).Store(&mountPoint); err != nil { + // It is not considered an error if + // xdg-document-portal is not available on the system. + if dbusErr, ok := err.(dbus.Error); ok && dbusErr.Name == "org.freedesktop.DBus.Error.ServiceUnknown" { + // We ignore errors here: if writing the file + // fails, we'll just try connecting to D-Bus + // again next time. + if err = ioutil.WriteFile(portalsUnavailableFile, []byte(""), 0644); err != nil { + logger.Noticef("WARNING: cannot write file at %s: %s", portalsUnavailableFile, err) + } + return nil } + return err } - return []string{ - sudoPath, "-E", - stracePath, - "-u", current.Username, - "-f", - // these syscalls are excluded because they make strace hang - // on all or some architectures (gettimeofday on arm64) - "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday", - }, nil + // Sanity check to make sure the document portal is exposed + // where we think it is. + actualMountPoint := strings.TrimRight(string(mountPoint), "\x00") + if actualMountPoint != expectedMountPoint { + return fmt.Errorf(i18n.G("Expected portal at %#v, got %#v"), expectedMountPoint, actualMountPoint) + } + return nil } func (x *cmdRun) runCmdUnderGdb(origCmd, env []string) error { @@ -589,21 +699,76 @@ return gcmd.Run() } +func (x *cmdRun) runCmdWithTraceExec(origCmd, env []string) error { + // setup private tmp dir with strace fifo + straceTmp, err := ioutil.TempDir("", "exec-trace") + if err != nil { + return err + } + defer os.RemoveAll(straceTmp) + straceLog := filepath.Join(straceTmp, "strace.fifo") + if err := syscall.Mkfifo(straceLog, 0640); err != nil { + return err + } + // ensure we have one writer on the fifo so that if strace fails + // nothing blocks + fw, err := os.OpenFile(straceLog, os.O_RDWR, 0640) + if err != nil { + return err + } + defer fw.Close() + + // read strace data from fifo async + var slg *strace.ExecveTiming + var straceErr error + doneCh := make(chan bool, 1) + go func() { + // FIXME: make this configurable? + nSlowest := 10 + slg, straceErr = strace.TraceExecveTimings(straceLog, nSlowest) + close(doneCh) + }() + + cmd, err := strace.TraceExecCommand(straceLog, origCmd...) + if err != nil { + return err + } + // run + cmd.Env = env + cmd.Stdin = Stdin + cmd.Stdout = Stdout + cmd.Stderr = Stderr + err = cmd.Run() + // ensure we close the fifo here so that the strace.TraceExecCommand() + // helper gets a EOF from the fifo (i.e. all writers must be closed + // for this) + fw.Close() + + // wait for strace reader + <-doneCh + if straceErr == nil { + slg.Display(Stderr) + } else { + logger.Noticef("cannot extract runtime data: %v", straceErr) + } + return err +} + func (x *cmdRun) runCmdUnderStrace(origCmd, env []string) error { - // prepend strace magic - cmd, err := straceCmd() + extraStraceOpts, raw, err := x.straceOpts() + if err != nil { + return err + } + cmd, err := strace.Command(extraStraceOpts, origCmd...) if err != nil { return err } - cmd = append(cmd, x.straceOpts()...) - cmd = append(cmd, origCmd...) // run with filter - gcmd := exec.Command(cmd[0], cmd[1:]...) - gcmd.Env = env - gcmd.Stdin = Stdin - gcmd.Stdout = Stdout - stderr, err := gcmd.StderrPipe() + cmd.Env = env + cmd.Stdin = Stdin + cmd.Stdout = Stdout + stderr, err := cmd.StderrPipe() if err != nil { return err } @@ -611,6 +776,15 @@ go func() { defer func() { filterDone <- true }() + if raw { + // Passing --strace='--raw' disables the filtering of + // early strace output. This is useful when tracking + // down issues with snap helpers such as snap-confine, + // snap-exec ... + io.Copy(Stderr, stderr) + return + } + r := bufio.NewReader(stderr) // The first thing from strace if things work is @@ -658,31 +832,39 @@ } io.Copy(Stderr, r) }() - if err := gcmd.Start(); err != nil { + if err := cmd.Start(); err != nil { return err } <-filterDone - err = gcmd.Wait() + err = cmd.Wait() return err } func (x *cmdRun) runSnapConfine(info *snap.Info, securityTag, snapApp, hook string, args []string) error { snapConfine := filepath.Join(dirs.DistroLibExecDir, "snap-confine") - // if we re-exec, we must run the snap-confine from the core snap + // if we re-exec, we must run the snap-confine from the core/snapd snap // as well, if they get out of sync, havoc will happen if isReexeced() { - // run snap-confine from the core snap. that will work because - // snap-confine on the core snap is mostly statically linked - // (except libudev and libc) - snapConfine = filepath.Join(dirs.SnapMountDir, "core/current", dirs.CoreLibExecDir, "snap-confine") + // exe is something like /snap/{snapd,core}/123/usr/bin/snap + exe, err := osReadlink("/proc/self/exe") + if err != nil { + return err + } + // snapBase will be "/snap/{core,snapd}/$rev/" because + // the snap binary is always at $root/usr/bin/snap + snapBase := filepath.Clean(filepath.Join(filepath.Dir(exe), "..", "..")) + // Run snap-confine from the core/snapd snap. That + // will work because snap-confine on the core/snapd snap is + // mostly statically linked (except libudev and libc) + snapConfine = filepath.Join(snapBase, dirs.CoreLibExecDir, "snap-confine") } if !osutil.FileExists(snapConfine) { if hook != "" { - logger.Noticef("WARNING: skipping running hook %q of snap %q: missing snap-confine", hook, info.Name()) + logger.Noticef("WARNING: skipping running hook %q of snap %q: missing snap-confine", hook, info.InstanceName()) return nil } - return fmt.Errorf(i18n.G("missing snap-confine: try updating your snapd package")) + return fmt.Errorf(i18n.G("missing snap-confine: try updating your core/snapd package")) } if err := createUserDataDirs(info); err != nil { @@ -694,6 +876,10 @@ logger.Noticef("WARNING: cannot copy user Xauthority file: %s", err) } + if err := activateXdgDocumentPortal(info, snapApp, hook); err != nil { + logger.Noticef("WARNING: cannot start document portal: %s", err) + } + cmd := []string{snapConfine} if info.NeedsClassic() { cmd = append(cmd, "--classic") @@ -745,7 +931,9 @@ } env := snapenv.ExecEnv(info, extraEnv) - if x.Gdb { + if x.TraceExec { + return x.runCmdWithTraceExec(cmd, env) + } else if x.Gdb { return x.runCmdUnderGdb(cmd, env) } else if x.useStrace() { return x.runCmdUnderStrace(cmd, env) diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_run_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_run_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_run_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_run_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -20,6 +20,7 @@ package main_test import ( + "errors" "fmt" "os" "os/user" @@ -31,7 +32,9 @@ snaprun "github.com/snapcore/snapd/cmd/snap" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/selinux" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/testutil" @@ -48,38 +51,39 @@ `) func (s *SnapSuite) TestInvalidParameters(c *check.C) { - invalidParameters := []string{"run", "--hook=configure", "--command=command-name", "snap-name"} - _, err := snaprun.Parser().ParseArgs(invalidParameters) + invalidParameters := []string{"run", "--hook=configure", "--command=command-name", "--", "snap-name"} + _, err := snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters) c.Check(err, check.ErrorMatches, ".*you can only use one of --hook, --command, and --timer.*") - invalidParameters = []string{"run", "--hook=configure", "--timer=10:00-12:00", "snap-name"} - _, err = snaprun.Parser().ParseArgs(invalidParameters) + invalidParameters = []string{"run", "--hook=configure", "--timer=10:00-12:00", "--", "snap-name"} + _, err = snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters) c.Check(err, check.ErrorMatches, ".*you can only use one of --hook, --command, and --timer.*") - invalidParameters = []string{"run", "--command=command-name", "--timer=10:00-12:00", "snap-name"} - _, err = snaprun.Parser().ParseArgs(invalidParameters) + invalidParameters = []string{"run", "--command=command-name", "--timer=10:00-12:00", "--", "snap-name"} + _, err = snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters) c.Check(err, check.ErrorMatches, ".*you can only use one of --hook, --command, and --timer.*") - invalidParameters = []string{"run", "-r=1", "--command=command-name", "snap-name"} - _, err = snaprun.Parser().ParseArgs(invalidParameters) + invalidParameters = []string{"run", "-r=1", "--command=command-name", "--", "snap-name"} + _, err = snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters) c.Check(err, check.ErrorMatches, ".*-r can only be used with --hook.*") - invalidParameters = []string{"run", "-r=1", "snap-name"} - _, err = snaprun.Parser().ParseArgs(invalidParameters) + invalidParameters = []string{"run", "-r=1", "--", "snap-name"} + _, err = snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters) c.Check(err, check.ErrorMatches, ".*-r can only be used with --hook.*") - invalidParameters = []string{"run", "--hook=configure", "foo", "bar", "snap-name"} - _, err = snaprun.Parser().ParseArgs(invalidParameters) + invalidParameters = []string{"run", "--hook=configure", "--", "foo", "bar", "snap-name"} + _, err = snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters) c.Check(err, check.ErrorMatches, ".*too many arguments for hook \"configure\": bar.*") } func (s *SnapSuite) TestSnapRunWhenMissingConfine(c *check.C) { + _, r := logger.MockLogger() + defer r() + // mock installed snap - si := snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R("x2"), }) - err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) - c.Assert(err, check.IsNil) // redirect exec var execs [][]string @@ -91,10 +95,10 @@ // and run it! // a regular run will fail - _, err = snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) - c.Assert(err, check.ErrorMatches, `.* your snapd package`) + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.ErrorMatches, `.* your core/snapd package`) // a hook run will not fail - _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "snapname"}) + _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "--", "snapname"}) c.Assert(err, check.IsNil) // but nothing is run ever @@ -105,11 +109,9 @@ defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap - si := snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R("x2"), }) - err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) - c.Assert(err, check.IsNil) // redirect exec execArg0 := "" @@ -124,7 +126,7 @@ defer restorer() // and run it! - rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) @@ -140,11 +142,9 @@ defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap - si := snaptest.MockSnap(c, string(mockYaml)+"confinement: classic\n", &snap.SideInfo{ + snaptest.MockSnapCurrent(c, string(mockYaml)+"confinement: classic\n", &snap.SideInfo{ Revision: snap.R("x2"), }) - err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) - c.Assert(err, check.IsNil) // redirect exec execArg0 := "" @@ -159,7 +159,7 @@ defer restorer() // and run it! - rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) @@ -179,11 +179,9 @@ defer mockSnapConfine(mountedCoreLibExecPath)() // mock installed snap - si := snaptest.MockSnap(c, string(mockYaml)+"confinement: classic\n", &snap.SideInfo{ + snaptest.MockSnapCurrent(c, string(mockYaml)+"confinement: classic\n", &snap.SideInfo{ Revision: snap.R("x2"), }) - err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) - c.Assert(err, check.IsNil) restore := snaprun.MockOsReadlink(func(name string) (string, error) { // pretend 'snap' is reexeced from 'core' @@ -197,7 +195,7 @@ return nil }) defer restorer() - rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) c.Check(execArgs, check.DeepEquals, []string{ @@ -211,11 +209,9 @@ defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap - si := snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R(42), }) - err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) - c.Assert(err, check.IsNil) // redirect exec execArg0 := "" @@ -230,7 +226,7 @@ defer restorer() // and run it! - _, err = snaprun.Parser().ParseArgs([]string{"run", "--command=my-command", "snapname.app", "arg1", "arg2"}) + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--command=my-command", "--", "snapname.app", "arg1", "arg2"}) c.Assert(err, check.IsNil) c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) c.Check(execArgs, check.DeepEquals, []string{ @@ -258,15 +254,37 @@ c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname/common")), check.Equals, true) } +func (s *SnapSuite) TestParallelInstanceSnapRunCreateDataDirs(c *check.C) { + info, err := snap.InfoFromSnapYaml(mockYaml) + c.Assert(err, check.IsNil) + info.SideInfo.Revision = snap.R(42) + info.InstanceKey = "foo" + + fakeHome := c.MkDir() + restorer := snaprun.MockUserCurrent(func() (*user.User, error) { + return &user.User{HomeDir: fakeHome}, nil + }) + defer restorer() + + err = snaprun.CreateUserDataDirs(info) + c.Assert(err, check.IsNil) + c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname_foo/42")), check.Equals, true) + c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname_foo/common")), check.Equals, true) + // mount point for snap instance mapping has been created + c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname")), check.Equals, true) + // and it's empty inside + m, err := filepath.Glob(filepath.Join(fakeHome, "/snap/snapname/*")) + c.Assert(err, check.IsNil) + c.Assert(m, check.HasLen, 0) +} + func (s *SnapSuite) TestSnapRunHookIntegration(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap - si := snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R(42), }) - err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) - c.Assert(err, check.IsNil) // redirect exec execArg0 := "" @@ -281,7 +299,7 @@ defer restorer() // Run a hook from the active revision - _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "snapname"}) + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "--", "snapname"}) c.Assert(err, check.IsNil) c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) c.Check(execArgs, check.DeepEquals, []string{ @@ -296,11 +314,9 @@ defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap - si := snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R(42), }) - err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) - c.Assert(err, check.IsNil) // redirect exec execArg0 := "" @@ -315,7 +331,7 @@ defer restorer() // Specifically pass "unset" which would use the active version. - _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=unset", "snapname"}) + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "-r=unset", "--", "snapname"}) c.Assert(err, check.IsNil) c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) c.Check(execArgs, check.DeepEquals, []string{ @@ -351,7 +367,7 @@ defer restorer() // Run a hook on revision 41 - _, err := snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=41", "snapname"}) + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "-r=41", "--", "snapname"}) c.Assert(err, check.IsNil) c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) c.Check(execArgs, check.DeepEquals, []string{ @@ -364,11 +380,9 @@ func (s *SnapSuite) TestSnapRunHookMissingRevisionIntegration(c *check.C) { // Only create revision 42 - si := snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R(42), }) - err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) - c.Assert(err, check.IsNil) // redirect exec restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { @@ -377,24 +391,22 @@ defer restorer() // Attempt to run a hook on revision 41, which doesn't exist - _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=41", "snapname"}) + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "-r=41", "--", "snapname"}) c.Assert(err, check.NotNil) c.Check(err, check.ErrorMatches, "cannot find .*") } func (s *SnapSuite) TestSnapRunHookInvalidRevisionIntegration(c *check.C) { - _, err := snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=invalid", "snapname"}) + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "-r=invalid", "--", "snapname"}) c.Assert(err, check.NotNil) c.Check(err, check.ErrorMatches, "invalid snap revision: \"invalid\"") } func (s *SnapSuite) TestSnapRunHookMissingHookIntegration(c *check.C) { // Only create revision 42 - si := snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R(42), }) - err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) - c.Assert(err, check.IsNil) // redirect exec called := false @@ -404,23 +416,23 @@ }) defer restorer() - _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=missing-hook", "snapname"}) + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=missing-hook", "--", "snapname"}) c.Assert(err, check.ErrorMatches, `cannot find hook "missing-hook" in "snapname"`) c.Check(called, check.Equals, false) } func (s *SnapSuite) TestSnapRunErorsForUnknownRunArg(c *check.C) { - _, err := snaprun.Parser().ParseArgs([]string{"run", "--unknown", "snapname.app", "--arg1", "arg2"}) + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--unknown", "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.ErrorMatches, "unknown flag `unknown'") } func (s *SnapSuite) TestSnapRunErorsForMissingApp(c *check.C) { - _, err := snaprun.Parser().ParseArgs([]string{"run", "--command=shell"}) + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--command=shell"}) c.Assert(err, check.ErrorMatches, "need the application to run as argument") } func (s *SnapSuite) TestSnapRunErorrForUnavailableApp(c *check.C) { - _, err := snaprun.Parser().ParseArgs([]string{"run", "not-there"}) + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "not-there"}) c.Assert(err, check.ErrorMatches, fmt.Sprintf("cannot find current revision for snap not-there: readlink %s/not-there/current: no such file or directory", dirs.SnapMountDir)) } @@ -428,11 +440,9 @@ defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap - si := snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R(42), }) - err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) - c.Assert(err, check.IsNil) // redirect exec execEnv := []string{} @@ -452,7 +462,7 @@ defer os.Unsetenv("SNAP_THE_WORLD") // and ensure those SNAP_ vars get overridden - rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") @@ -481,14 +491,12 @@ } func (s *SnapSuite) TestSnapRunAppIntegrationFromCore(c *check.C) { - defer mockSnapConfine(filepath.Join(dirs.SnapMountDir, "core", "current", dirs.CoreLibExecDir))() + defer mockSnapConfine(filepath.Join(dirs.SnapMountDir, "core", "111", dirs.CoreLibExecDir))() // mock installed snap - si := snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R("x2"), }) - err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) - c.Assert(err, check.IsNil) // pretend to be running from core restorer := snaprun.MockOsReadlink(func(string) (string, error) { @@ -509,12 +517,51 @@ defer restorer() // and run it! - rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(execArg0, check.Equals, filepath.Join(dirs.SnapMountDir, "/core/111", dirs.CoreLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.SnapMountDir, "/core/111", dirs.CoreLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "snapname.app", "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") +} + +func (s *SnapSuite) TestSnapRunAppIntegrationFromSnapd(c *check.C) { + defer mockSnapConfine(filepath.Join(dirs.SnapMountDir, "snapd", "222", dirs.CoreLibExecDir))() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + + // pretend to be running from snapd + restorer := snaprun.MockOsReadlink(func(string) (string, error) { + return filepath.Join(dirs.SnapMountDir, "snapd/222/usr/bin/snap"), nil + }) + defer restorer() + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer = snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // and run it! + rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) - c.Check(execArg0, check.Equals, filepath.Join(dirs.SnapMountDir, "/core/current", dirs.CoreLibExecDir, "snap-confine")) + c.Check(execArg0, check.Equals, filepath.Join(dirs.SnapMountDir, "/snapd/222", dirs.CoreLibExecDir, "snap-confine")) c.Check(execArgs, check.DeepEquals, []string{ - filepath.Join(dirs.SnapMountDir, "/core/current", dirs.CoreLibExecDir, "snap-confine"), + filepath.Join(dirs.SnapMountDir, "/snapd/222", dirs.CoreLibExecDir, "snap-confine"), "snap.snapname.app", filepath.Join(dirs.CoreLibExecDir, "snap-exec"), "snapname.app", "--arg1", "arg2"}) @@ -533,11 +580,9 @@ // mock installed snap; happily this also gives us a directory // below /tmp which the Xauthority migration expects. - si := snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R("x2"), }) - err = os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) - c.Assert(err, check.IsNil) // redirect exec execArg0 := "" @@ -563,7 +608,7 @@ })() // and run it! - rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app"}) + rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app"}) c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) @@ -660,11 +705,9 @@ defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap - si := snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R("x2"), }) - err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) - c.Assert(err, check.IsNil) // pretend we have sudo and simulate some useful output that would // normally come from strace @@ -688,7 +731,7 @@ c.Assert(err, check.IsNil) // and run it under strace - rest, err := snaprun.Parser().ParseArgs([]string{"run", "--strace", "snapname.app", "--arg1", "arg2"}) + rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--strace", "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) c.Check(sudoCmd.Calls(), check.DeepEquals, [][]string{ @@ -697,7 +740,7 @@ filepath.Join(straceCmd.BinDir(), "strace"), "-u", user.Username, "-f", - "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday", + "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep", filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "snap.snapname.app", filepath.Join(dirs.CoreLibExecDir, "snap-exec"), @@ -706,17 +749,45 @@ }) c.Check(s.Stdout(), check.Equals, "stdout output 1\nstdout output 2\n") c.Check(s.Stderr(), check.Equals, fmt.Sprintf("execve(%q)\ninteressting strace output\nand more\n", filepath.Join(dirs.SnapMountDir, "snapName/x2/bin/foo"))) + + s.ResetStdStreams() + sudoCmd.ForgetCalls() + + // try again without filtering + rest, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--strace=--raw", "--", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(sudoCmd.Calls(), check.DeepEquals, [][]string{ + { + "sudo", "-E", + filepath.Join(straceCmd.BinDir(), "strace"), + "-u", user.Username, + "-f", + "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep", + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "snapname.app", "--arg1", "arg2", + }, + }) + c.Check(s.Stdout(), check.Equals, "stdout output 1\nstdout output 2\n") + expectedFullFmt := `execve("/path/to/snap-confine") +snap-confine/snap-exec strace stuff +getuid() = 1000 +execve("%s/snapName/x2/bin/foo") +interessting strace output +and more +` + c.Check(s.Stderr(), check.Equals, fmt.Sprintf(expectedFullFmt, dirs.SnapMountDir)) } func (s *SnapSuite) TestSnapRunAppWithStraceOptions(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap - si := snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R("x2"), }) - err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) - c.Assert(err, check.IsNil) // pretend we have sudo sudoCmd := testutil.MockCommand(c, "sudo", "") @@ -730,7 +801,7 @@ c.Assert(err, check.IsNil) // and run it under strace - rest, err := snaprun.Parser().ParseArgs([]string{"run", "--strace=-tt", "snapname.app", "--arg1", "arg2"}) + rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", `--strace=-tt --raw -o "file with spaces"`, "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) c.Check(sudoCmd.Calls(), check.DeepEquals, [][]string{ @@ -739,8 +810,10 @@ filepath.Join(straceCmd.BinDir(), "strace"), "-u", user.Username, "-f", - "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday", + "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep", "-tt", + "-o", + "file with spaces", filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "snap.snapname.app", filepath.Join(dirs.CoreLibExecDir, "snap-exec"), @@ -753,11 +826,9 @@ defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap - si := snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R("x2"), }) - err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) - c.Assert(err, check.IsNil) // redirect exec execArg0 := "" @@ -772,7 +843,7 @@ defer restorer() // and run it! - rest, err := snaprun.Parser().ParseArgs([]string{"run", "--shell", "snapname.app", "--arg1", "arg2"}) + rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--shell", "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) @@ -788,11 +859,9 @@ defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap - si := snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R("x2"), }) - err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) - c.Assert(err, check.IsNil) // redirect exec execArg0 := "" @@ -814,7 +883,7 @@ defer restorer() // pretend we are outside of timer range - rest, err := snaprun.Parser().ParseArgs([]string{"run", `--timer="mon,10:00~12:00,,fri,13:00"`, "snapname.app", "--arg1", "arg2"}) + rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", `--timer="mon,10:00~12:00,,fri,13:00"`, "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) c.Assert(execCalled, check.Equals, false) @@ -830,7 +899,7 @@ defer restorer() // and run it under strace - _, err = snaprun.Parser().ParseArgs([]string{"run", `--timer="mon,10:00~12:00,,fri,13:00"`, "snapname.app", "--arg1", "arg2"}) + _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", `--timer="mon,10:00~12:00,,fri,13:00"`, "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(execCalled, check.Equals, true) c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) @@ -840,3 +909,203 @@ filepath.Join(dirs.CoreLibExecDir, "snap-exec"), "snapname.app", "--arg1", "arg2"}) } + +func (s *SnapSuite) TestRunCmdWithTraceExecUnhappy(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("1"), + }) + + // pretend we have sudo + sudoCmd := testutil.MockCommand(c, "sudo", "echo unhappy; exit 12") + defer sudoCmd.Restore() + + // pretend we have strace + straceCmd := testutil.MockCommand(c, "strace", "") + defer straceCmd.Restore() + + rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--trace-exec", "--", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.ErrorMatches, "exit status 12") + c.Assert(rest, check.DeepEquals, []string{"--", "snapname.app", "--arg1", "arg2"}) + c.Check(s.Stdout(), check.Equals, "unhappy\n") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestSnapRunRestoreSecurityContextHappy(c *check.C) { + logbuf, restorer := logger.MockLogger() + defer restorer() + + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + + fakeHome := c.MkDir() + restorer = snaprun.MockUserCurrent(func() (*user.User, error) { + return &user.User{HomeDir: fakeHome}, nil + }) + defer restorer() + + // redirect exec + execCalled := 0 + restorer = snaprun.MockSyscallExec(func(_ string, args []string, envv []string) error { + execCalled++ + return nil + }) + defer restorer() + + verifyCalls := 0 + restoreCalls := 0 + isEnabledCalls := 0 + enabled := false + verify := true + + snapUserDir := filepath.Join(fakeHome, dirs.UserHomeSnapDir) + + restorer = snaprun.MockSELinuxVerifyPathContext(func(what string) (bool, error) { + c.Check(what, check.Equals, snapUserDir) + verifyCalls++ + return verify, nil + }) + defer restorer() + + restorer = snaprun.MockSELinuxRestoreContext(func(what string, mode selinux.RestoreMode) error { + c.Check(mode, check.Equals, selinux.RestoreMode{Recursive: true}) + c.Check(what, check.Equals, snapUserDir) + restoreCalls++ + return nil + }) + defer restorer() + + restorer = snaprun.MockSELinuxIsEnabled(func() (bool, error) { + isEnabledCalls++ + return enabled, nil + }) + defer restorer() + + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) + c.Assert(err, check.IsNil) + c.Check(execCalled, check.Equals, 1) + c.Check(isEnabledCalls, check.Equals, 1) + c.Check(verifyCalls, check.Equals, 0) + c.Check(restoreCalls, check.Equals, 0) + + // pretend SELinux is on + enabled = true + + _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) + c.Assert(err, check.IsNil) + c.Check(execCalled, check.Equals, 2) + c.Check(isEnabledCalls, check.Equals, 2) + c.Check(verifyCalls, check.Equals, 1) + c.Check(restoreCalls, check.Equals, 0) + + // pretend the context does not match + verify = false + + logbuf.Reset() + + _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) + c.Assert(err, check.IsNil) + c.Check(execCalled, check.Equals, 3) + c.Check(isEnabledCalls, check.Equals, 3) + c.Check(verifyCalls, check.Equals, 2) + c.Check(restoreCalls, check.Equals, 1) + + // and we let the user know what we're doing + c.Check(logbuf.String(), testutil.Contains, fmt.Sprintf("restoring default SELinux context of %s", snapUserDir)) +} + +func (s *SnapSuite) TestSnapRunRestoreSecurityContextFail(c *check.C) { + logbuf, restorer := logger.MockLogger() + defer restorer() + + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + + fakeHome := c.MkDir() + restorer = snaprun.MockUserCurrent(func() (*user.User, error) { + return &user.User{HomeDir: fakeHome}, nil + }) + defer restorer() + + // redirect exec + execCalled := 0 + restorer = snaprun.MockSyscallExec(func(_ string, args []string, envv []string) error { + execCalled++ + return nil + }) + defer restorer() + + verifyCalls := 0 + restoreCalls := 0 + isEnabledCalls := 0 + enabledErr := errors.New("enabled failed") + verifyErr := errors.New("verify failed") + restoreErr := errors.New("restore failed") + + snapUserDir := filepath.Join(fakeHome, dirs.UserHomeSnapDir) + + restorer = snaprun.MockSELinuxVerifyPathContext(func(what string) (bool, error) { + c.Check(what, check.Equals, snapUserDir) + verifyCalls++ + return false, verifyErr + }) + defer restorer() + + restorer = snaprun.MockSELinuxRestoreContext(func(what string, mode selinux.RestoreMode) error { + c.Check(mode, check.Equals, selinux.RestoreMode{Recursive: true}) + c.Check(what, check.Equals, snapUserDir) + restoreCalls++ + return restoreErr + }) + defer restorer() + + restorer = snaprun.MockSELinuxIsEnabled(func() (bool, error) { + isEnabledCalls++ + return enabledErr == nil, enabledErr + }) + defer restorer() + + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) + // these errors are only logged, but we still run the snap + c.Assert(err, check.IsNil) + c.Check(execCalled, check.Equals, 1) + c.Check(logbuf.String(), testutil.Contains, "cannot determine SELinux status: enabled failed") + c.Check(isEnabledCalls, check.Equals, 1) + c.Check(verifyCalls, check.Equals, 0) + c.Check(restoreCalls, check.Equals, 0) + // pretend selinux is on + enabledErr = nil + + logbuf.Reset() + + _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) + c.Assert(err, check.IsNil) + c.Check(execCalled, check.Equals, 2) + c.Check(logbuf.String(), testutil.Contains, fmt.Sprintf("failed to verify SELinux context of %s: verify failed", snapUserDir)) + c.Check(isEnabledCalls, check.Equals, 2) + c.Check(verifyCalls, check.Equals, 1) + c.Check(restoreCalls, check.Equals, 0) + + // pretend the context does not match + verifyErr = nil + + logbuf.Reset() + + _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) + c.Assert(err, check.IsNil) + c.Check(execCalled, check.Equals, 3) + c.Check(logbuf.String(), testutil.Contains, fmt.Sprintf("cannot restore SELinux context of %s: restore failed", snapUserDir)) + c.Check(isEnabledCalls, check.Equals, 3) + c.Check(verifyCalls, check.Equals, 2) + c.Check(restoreCalls, check.Equals, 1) +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_sandbox_features.go snapd-2.37~rc1~14.04/cmd/snap/cmd_sandbox_features.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_sandbox_features.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_sandbox_features.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,88 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "fmt" + "sort" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" +) + +var shortSandboxFeaturesHelp = i18n.G("Print sandbox features available on the system") +var longSandboxFeaturesHelp = i18n.G(` +The sandbox command prints tags describing features of individual sandbox +components used by snapd on a given system. +`) + +type cmdSandboxFeatures struct { + clientMixin + Required []string `long:"required" arg-name:""` +} + +func init() { + addDebugCommand("sandbox-features", shortSandboxFeaturesHelp, longSandboxFeaturesHelp, func() flags.Commander { + return &cmdSandboxFeatures{} + }, map[string]string{ + "required": i18n.G("Ensure that given backend:feature is available"), + }, nil) +} + +func (cmd cmdSandboxFeatures) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + sysInfo, err := cmd.client.SysInfo() + if err != nil { + return err + } + + sandboxFeatures := sysInfo.SandboxFeatures + + if len(cmd.Required) > 0 { + avail := make(map[string]bool) + for backend := range sandboxFeatures { + for _, feature := range sandboxFeatures[backend] { + avail[fmt.Sprintf("%s:%s", backend, feature)] = true + } + } + for _, required := range cmd.Required { + if !avail[required] { + return fmt.Errorf("sandbox feature not available: %q", required) + } + } + } else { + backends := make([]string, 0, len(sandboxFeatures)) + for backend := range sandboxFeatures { + backends = append(backends, backend) + } + sort.Strings(backends) + w := tabWriter() + defer w.Flush() + for _, backend := range backends { + fmt.Fprintf(w, "%s:\t%s\n", backend, strings.Join(sandboxFeatures[backend], " ")) + } + } + return nil +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_sandbox_features_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_sandbox_features_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_sandbox_features_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_sandbox_features_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,61 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestSandboxFeatures(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "sync", "result": {"sandbox-features": {"apparmor": ["a", "b", "c"], "selinux": ["1", "2", "3"]}}}`) + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "sandbox-features"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, ""+ + "apparmor: a b c\n"+ + "selinux: 1 2 3\n") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestSandboxFeaturesRequired(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "sync", "result": {"sandbox-features": {"apparmor": ["a", "b", "c"], "selinux": ["1", "2", "3"]}}}`) + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "sandbox-features", "--required=apparmor:a", "--required=selinux:2"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestSandboxFeaturesRequiredButMissing(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "sync", "result": {"sandbox-features": {"apparmor": ["a", "b", "c"], "selinux": ["1", "2", "3"]}}}`) + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "sandbox-features", "--required=magic:thing"}) + c.Assert(err, ErrorMatches, `sandbox feature not available: "magic:thing"`) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_services.go snapd-2.37~rc1~14.04/cmd/snap/cmd_services.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_services.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_services.go 2019-01-16 08:36:51.000000000 +0000 @@ -26,16 +26,19 @@ "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/cmd" "github.com/snapcore/snapd/i18n" ) type svcStatus struct { + clientMixin Positional struct { ServiceNames []serviceName } `positional-args:"yes"` } type svcLogs struct { + clientMixin N string `short:"n" default:"10"` Follow bool `short:"f"` Positional struct { @@ -46,11 +49,13 @@ var ( shortServicesHelp = i18n.G("Query the status of services") longServicesHelp = i18n.G(` -The services command lists information about the services specified, or about the services in all currently installed snaps. +The services command lists information about the services specified, or about +the services in all currently installed snaps. `) - shortLogsHelp = i18n.G("Retrieve logs of services") + shortLogsHelp = i18n.G("Retrieve logs for services") longLogsHelp = i18n.G(` -The logs command fetches logs of the given services and displays them in chronological order. +The logs command fetches logs of the given services and displays them in +chronological order. `) shortStartHelp = i18n.G("Start services") longStartHelp = i18n.G(` @@ -64,33 +69,40 @@ longRestartHelp = i18n.G(` The restart command restarts the given services. -If the --reload option is given, for each service whose app has a reload command, a reload is performed instead of a restart. +If the --reload option is given, for each service whose app has a reload +command, a reload is performed instead of a restart. `) ) func init() { argdescs := []argDesc{{ - // TRANSLATORS: This needs to be wrapped in <>s. + // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("A service specification, which can be just a snap name (for all services in the snap), or . for a single service."), }} addCommand("services", shortServicesHelp, longServicesHelp, func() flags.Commander { return &svcStatus{} }, nil, argdescs) addCommand("logs", shortLogsHelp, longLogsHelp, func() flags.Commander { return &svcLogs{} }, map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. "n": i18n.G("Show only the given number of lines, or 'all'."), + // TRANSLATORS: This should not start with a lowercase letter. "f": i18n.G("Wait for new lines and print them as they come in."), }, argdescs) addCommand("start", shortStartHelp, longStartHelp, func() flags.Commander { return &svcStart{} }, waitDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. "enable": i18n.G("As well as starting the service now, arrange for it to be started on boot."), }), argdescs) addCommand("stop", shortStopHelp, longStopHelp, func() flags.Commander { return &svcStop{} }, waitDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. "disable": i18n.G("As well as stopping the service now, arrange for it to no longer be started on boot."), }), argdescs) addCommand("restart", shortRestartHelp, longRestartHelp, func() flags.Commander { return &svcRestart{} }, waitDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. "reload": i18n.G("If the service has a reload command, use it instead of restarting."), }), argdescs) } @@ -108,15 +120,20 @@ return ErrExtraArgs } - services, err := Client().Apps(svcNames(s.Positional.ServiceNames), client.AppOptions{Service: true}) + services, err := s.client.Apps(svcNames(s.Positional.ServiceNames), client.AppOptions{Service: true}) if err != nil { return err } + if len(services) == 0 { + fmt.Fprintln(Stderr, i18n.G("There are no services provided by installed snaps.")) + return nil + } + w := tabWriter() defer w.Flush() - fmt.Fprintln(w, i18n.G("Service\tStartup\tCurrent")) + fmt.Fprintln(w, i18n.G("Service\tStartup\tCurrent\tNotes")) for _, svc := range services { startup := i18n.G("disabled") @@ -127,7 +144,7 @@ if svc.Active { current = i18n.G("active") } - fmt.Fprintf(w, "%s.%s\t%s\t%s\n", svc.Snap, svc.Name, startup, current) + fmt.Fprintf(w, "%s.%s\t%s\t%s\t%s\n", svc.Snap, svc.Name, startup, current, cmd.ClientAppInfoNotes(svc)) } return nil @@ -147,7 +164,7 @@ sN = int(n) } - logs, err := Client().Logs(svcNames(s.Positional.ServiceNames), client.LogOptions{N: sN, Follow: s.Follow}) + logs, err := s.client.Logs(svcNames(s.Positional.ServiceNames), client.LogOptions{N: sN, Follow: s.Follow}) if err != nil { return err } @@ -171,13 +188,12 @@ if len(args) > 0 { return ErrExtraArgs } - cli := Client() names := svcNames(s.Positional.ServiceNames) - changeID, err := cli.Start(names, client.StartOptions{Enable: s.Enable}) + changeID, err := s.client.Start(names, client.StartOptions{Enable: s.Enable}) if err != nil { return err } - if _, err := s.wait(cli, changeID); err != nil { + if _, err := s.wait(changeID); err != nil { if err == noWait { return nil } @@ -201,13 +217,12 @@ if len(args) > 0 { return ErrExtraArgs } - cli := Client() names := svcNames(s.Positional.ServiceNames) - changeID, err := cli.Stop(names, client.StopOptions{Disable: s.Disable}) + changeID, err := s.client.Stop(names, client.StopOptions{Disable: s.Disable}) if err != nil { return err } - if _, err := s.wait(cli, changeID); err != nil { + if _, err := s.wait(changeID); err != nil { if err == noWait { return nil } @@ -231,13 +246,12 @@ if len(args) > 0 { return ErrExtraArgs } - cli := Client() names := svcNames(s.Positional.ServiceNames) - changeID, err := cli.Restart(names, client.RestartOptions{Reload: s.Reload}) + changeID, err := s.client.Restart(names, client.RestartOptions{Reload: s.Reload}) if err != nil { return err } - if _, err := s.wait(cli, changeID); err != nil { + if _, err := s.wait(changeID); err != nil { if err == noWait { return nil } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_services_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_services_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_services_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_services_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -20,6 +20,7 @@ package main_test import ( + "encoding/json" "fmt" "net/http" "time" @@ -83,7 +84,7 @@ func (s *appOpSuite) testOpNoArgs(c *check.C, op string) { s.RedirectClientToTestServer(nil) - _, err := snap.Parser().ParseArgs([]string{op}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{op}) c.Assert(err, check.ErrorMatches, `.* required argument .* not provided`) } @@ -105,7 +106,7 @@ n++ }) - _, err := snap.Parser().ParseArgs(s.args(op, names, extra, noWait)) + _, err := snap.Parser(snap.Client()).ParseArgs(s.args(op, names, extra, noWait)) c.Assert(err, check.ErrorMatches, "error") c.Check(n, check.Equals, 1) } @@ -135,7 +136,7 @@ n++ }) - rest, err := snap.Parser().ParseArgs(s.args(op, names, extra, noWait)) + rest, err := snap.Parser(snap.Client()).ParseArgs(s.args(op, names, extra, noWait)) c.Assert(err, check.IsNil) c.Assert(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") @@ -169,3 +170,85 @@ } } } + +func (s *appOpSuite) TestAppStatus(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.URL.Path, check.Equals, "/v2/apps") + c.Check(r.URL.Query(), check.HasLen, 1) + c.Check(r.URL.Query().Get("select"), check.Equals, "service") + c.Check(r.Method, check.Equals, "GET") + w.WriteHeader(200) + enc := json.NewEncoder(w) + enc.Encode(map[string]interface{}{ + "type": "sync", + "result": []map[string]interface{}{ + {"snap": "foo", "name": "bar", "daemon": "oneshot", + "active": false, "enabled": true, + "activators": []map[string]interface{}{ + {"name": "bar", "type": "timer", "active": true, "enabled": true}, + }, + }, {"snap": "foo", "name": "baz", "daemon": "oneshot", + "active": false, "enabled": true, + "activators": []map[string]interface{}{ + {"name": "baz-sock1", "type": "socket", "active": true, "enabled": true}, + {"name": "baz-sock2", "type": "socket", "active": false, "enabled": true}, + }, + }, {"snap": "foo", "name": "zed", + "active": true, "enabled": true, + }, + }, + "status": "OK", + "status-code": 200, + }) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"services"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.HasLen, 0) + c.Check(s.Stderr(), check.Equals, "") + c.Check(s.Stdout(), check.Equals, `Service Startup Current Notes +foo.bar enabled inactive timer-activated +foo.baz enabled inactive socket-activated +foo.zed enabled active - +`) + // ensure that the fake server api was actually hit + c.Check(n, check.Equals, 1) +} + +func (s *appOpSuite) TestAppStatusNoServices(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.URL.Path, check.Equals, "/v2/apps") + c.Check(r.URL.Query(), check.HasLen, 1) + c.Check(r.URL.Query().Get("select"), check.Equals, "service") + c.Check(r.Method, check.Equals, "GET") + w.WriteHeader(200) + enc := json.NewEncoder(w) + enc.Encode(map[string]interface{}{ + "type": "sync", + "result": []map[string]interface{}{}, + "status": "OK", + "status-code": 200, + }) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"services"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.HasLen, 0) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "There are no services provided by installed snaps.\n") + // ensure that the fake server api was actually hit + c.Check(n, check.Equals, 1) +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_set.go snapd-2.37~rc1~14.04/cmd/snap/cmd_set.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_set.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_set.go 2019-01-16 08:36:51.000000000 +0000 @@ -29,7 +29,7 @@ "github.com/snapcore/snapd/jsonutil" ) -var shortSetHelp = i18n.G("Changes configuration options") +var shortSetHelp = i18n.G("Change configuration options") var longSetHelp = i18n.G(` The set command changes the provided configuration options as requested. @@ -44,6 +44,7 @@ `) type cmdSet struct { + waitMixin Positional struct { Snap installedSnapName ConfValues []string `required:"1"` @@ -51,15 +52,15 @@ } func init() { - addCommand("set", shortSetHelp, longSetHelp, func() flags.Commander { return &cmdSet{} }, nil, []argDesc{ + addCommand("set", shortSetHelp, longSetHelp, func() flags.Commander { return &cmdSet{} }, waitDescs, []argDesc{ { name: "", - // TRANSLATORS: This should probably not start with a lowercase letter. + // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("The snap to configure (e.g. hello-world)"), }, { - // TRANSLATORS: This needs to be wrapped in <>s. + // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), - // TRANSLATORS: This should probably not start with a lowercase letter. + // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Configuration value (key=value)"), }, }) @@ -81,16 +82,18 @@ } } - return configure(string(x.Positional.Snap), patchValues) -} - -func configure(snapName string, patchValues map[string]interface{}) error { - cli := Client() - id, err := cli.SetConf(snapName, patchValues) + snapName := string(x.Positional.Snap) + id, err := x.client.SetConf(snapName, patchValues) if err != nil { return err } - _, err = wait(cli, id) - return err + if _, err := x.wait(id); err != nil { + if err == noWait { + return nil + } + return err + } + + return nil } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_set_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_set_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_set_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_set_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -39,7 +39,7 @@ func (s *SnapSuite) TestInvalidSetParameters(c *check.C) { invalidParameters := []string{"set", "snap-name", "key", "value"} - _, err := snapset.Parser().ParseArgs(invalidParameters) + _, err := snapset.Parser(snapset.Client()).ParseArgs(invalidParameters) c.Check(err, check.ErrorMatches, ".*invalid configuration:.*(want key=value).*") } @@ -53,7 +53,7 @@ s.mockSetConfigServer(c, "value") // Set a config value for the active snap - _, err := snapset.Parser().ParseArgs([]string{"set", "snapname", "key=value"}) + _, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"set", "snapname", "key=value"}) c.Assert(err, check.IsNil) } @@ -67,7 +67,7 @@ s.mockSetConfigServer(c, json.Number("1.2")) // Set a config value for the active snap - _, err := snapset.Parser().ParseArgs([]string{"set", "snapname", "key=1.2"}) + _, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"set", "snapname", "key=1.2"}) c.Assert(err, check.IsNil) } @@ -80,7 +80,7 @@ s.mockSetConfigServer(c, json.Number("1234567890")) // Set a config value for the active snap - _, err := snapset.Parser().ParseArgs([]string{"set", "snapname", "key=1234567890"}) + _, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"set", "snapname", "key=1234567890"}) c.Assert(err, check.IsNil) } @@ -94,7 +94,7 @@ s.mockSetConfigServer(c, map[string]interface{}{"subkey": "value"}) // Set a config value for the active snap - _, err := snapset.Parser().ParseArgs([]string{"set", "snapname", `key={"subkey":"value"}`}) + _, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"set", "snapname", `key={"subkey":"value"}`}) c.Assert(err, check.IsNil) } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_sign_build.go snapd-2.37~rc1~14.04/cmd/snap/cmd_sign_build.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_sign_build.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_sign_build.go 2019-01-16 08:36:51.000000000 +0000 @@ -43,8 +43,11 @@ Grade string `long:"grade" choice:"devel" choice:"stable" default:"stable"` } -var shortSignBuildHelp = i18n.G("Create snap build assertion") -var longSignBuildHelp = i18n.G("Create snap-build assertion for the provided snap file.") +var shortSignBuildHelp = i18n.G("Create a snap-build assertion") +var longSignBuildHelp = i18n.G(` +The sign-build command creates a snap-build assertion for the provided +snap file. +`) func init() { cmd := addCommand("sign-build", @@ -53,14 +56,18 @@ func() flags.Commander { return &cmdSignBuild{} }, map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. "developer-id": i18n.G("Identifier of the signer"), - "snap-id": i18n.G("Identifier of the snap package associated with the build"), - "k": i18n.G("Name of the GnuPG key to use (defaults to 'default' as key name)"), - "grade": i18n.G("Grade states the build quality of the snap (defaults to 'stable')"), + // TRANSLATORS: This should not start with a lowercase letter. + "snap-id": i18n.G("Identifier of the snap package associated with the build"), + // TRANSLATORS: This should not start with a lowercase letter. + "k": i18n.G("Name of the GnuPG key to use (defaults to 'default' as key name)"), + // TRANSLATORS: This should not start with a lowercase letter. + "grade": i18n.G("Grade states the build quality of the snap (defaults to 'stable')"), }, []argDesc{{ - // TRANSLATORS: This needs to be wrapped in <>s. + // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), - // TRANSLATORS: This should probably not start with a lowercase letter. + // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Filename of the snap you want to assert a build for"), }}) cmd.hidden = true diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_sign_build_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_sign_build_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_sign_build_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_sign_build_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -38,7 +38,7 @@ var _ = Suite(&SnapSignBuildSuite{}) func (s *SnapSignBuildSuite) TestSignBuildMandatoryFlags(c *C) { - _, err := snap.Parser().ParseArgs([]string{"sign-build", "foo_1_amd64.snap"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"sign-build", "foo_1_amd64.snap"}) c.Assert(err, NotNil) c.Check(err.Error(), Equals, "the required flags `--developer-id' and `--snap-id' were not specified") c.Check(s.Stdout(), Equals, "") @@ -46,7 +46,7 @@ } func (s *SnapSignBuildSuite) TestSignBuildMissingSnap(c *C) { - _, err := snap.Parser().ParseArgs([]string{"sign-build", "foo_1_amd64.snap", "--developer-id", "dev-id1", "--snap-id", "snap-id-1"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"sign-build", "foo_1_amd64.snap", "--developer-id", "dev-id1", "--snap-id", "snap-id-1"}) c.Assert(err, NotNil) c.Check(err.Error(), Equals, "cannot compute snap \"foo_1_amd64.snap\" digest: open foo_1_amd64.snap: no such file or directory") c.Check(s.Stdout(), Equals, "") @@ -63,7 +63,7 @@ os.Setenv("SNAP_GNUPG_HOME", tempdir) defer os.Unsetenv("SNAP_GNUPG_HOME") - _, err := snap.Parser().ParseArgs([]string{"sign-build", snapFilename, "--developer-id", "dev-id1", "--snap-id", "snap-id-1"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"sign-build", snapFilename, "--developer-id", "dev-id1", "--snap-id", "snap-id-1"}) c.Assert(err, NotNil) c.Check(err.Error(), Equals, "cannot use \"default\" key: cannot find key named \"default\" in GPG keyring") c.Check(s.Stdout(), Equals, "") @@ -87,7 +87,7 @@ os.Setenv("SNAP_GNUPG_HOME", tempdir) defer os.Unsetenv("SNAP_GNUPG_HOME") - _, err := snap.Parser().ParseArgs([]string{"sign-build", snapFilename, "--developer-id", "dev-id1", "--snap-id", "snap-id-1"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"sign-build", snapFilename, "--developer-id", "dev-id1", "--snap-id", "snap-id-1"}) c.Assert(err, IsNil) assertion, err := asserts.Decode([]byte(s.Stdout())) @@ -122,7 +122,7 @@ os.Setenv("SNAP_GNUPG_HOME", tempdir) defer os.Unsetenv("SNAP_GNUPG_HOME") - _, err := snap.Parser().ParseArgs([]string{"sign-build", snapFilename, "--developer-id", "dev-id1", "--snap-id", "snap-id-1", "--grade", "devel"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"sign-build", snapFilename, "--developer-id", "dev-id1", "--snap-id", "snap-id-1", "--grade", "devel"}) c.Assert(err, IsNil) assertion, err := asserts.Decode([]byte(s.Stdout())) c.Assert(err, IsNil) diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_sign.go snapd-2.37~rc1~14.04/cmd/snap/cmd_sign.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_sign.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_sign.go 2019-01-16 08:36:51.000000000 +0000 @@ -31,7 +31,10 @@ ) var shortSignHelp = i18n.G("Sign an assertion") -var longSignHelp = i18n.G(`Sign an assertion using the specified key, using the input for headers from a JSON mapping provided through stdin, the body of the assertion can be specified through a "body" pseudo-header. +var longSignHelp = i18n.G(` +The sign command signs an assertion using the specified key, using the +input for headers from a JSON mapping provided through stdin. The body +of the assertion can be specified through a "body" pseudo-header. `) type cmdSign struct { @@ -41,7 +44,10 @@ func init() { cmd := addCommand("sign", shortSignHelp, longSignHelp, func() flags.Commander { return &cmdSign{} - }, map[string]string{"k": i18n.G("Name of the key to use, otherwise use the default key")}, nil) + }, map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "k": i18n.G("Name of the key to use, otherwise use the default key"), + }, nil) cmd.hidden = true } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_sign_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_sign_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_sign_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_sign_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -43,7 +43,7 @@ func (s *SnapKeysSuite) TestHappyDefaultKey(c *C) { s.stdin.Write(statement) - rest, err := snap.Parser().ParseArgs([]string{"sign"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"sign"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) @@ -55,7 +55,7 @@ func (s *SnapKeysSuite) TestHappyNonDefaultKey(c *C) { s.stdin.Write(statement) - rest, err := snap.Parser().ParseArgs([]string{"sign", "-k", "another"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"sign", "-k", "another"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_snap_op.go snapd-2.37~rc1~14.04/cmd/snap/cmd_snap_op.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_snap_op.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_snap_op.go 2019-01-16 08:36:51.000000000 +0000 @@ -23,11 +23,9 @@ "errors" "fmt" "os" - "os/signal" "path/filepath" "sort" "strings" - "syscall" "time" "github.com/jessevdk/go-flags" @@ -35,141 +33,56 @@ "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/osutil" - "github.com/snapcore/snapd/progress" ) -func lastLogStr(logs []string) string { - if len(logs) == 0 { - return "" - } - return logs[len(logs)-1] -} - var ( - maxGoneTime = 5 * time.Second - pollTime = 100 * time.Millisecond + shortInstallHelp = i18n.G("Install snaps on the system") + shortRemoveHelp = i18n.G("Remove snaps from the system") + shortRefreshHelp = i18n.G("Refresh snaps in the system") + shortTryHelp = i18n.G("Test an unpacked snap in the system") + shortEnableHelp = i18n.G("Enable a snap in the system") + shortDisableHelp = i18n.G("Disable a snap in the system") ) -type waitMixin struct { - NoWait bool `long:"no-wait" hidden:"true"` -} - -// TODO: use waitMixin outside of cmd_snap_op.go - -var waitDescs = mixinDescs{ - "no-wait": i18n.G("Do not wait for the operation to finish but just print the change id."), -} - -var noWait = errors.New("no wait for op") - -func (wmx *waitMixin) wait(cli *client.Client, id string) (*client.Change, error) { - if wmx.NoWait { - fmt.Fprintf(Stdout, "%s\n", id) - return nil, noWait - } - return wait(cli, id) -} - -func wait(cli *client.Client, id string) (*client.Change, error) { - pb := progress.MakeProgressBar() - defer func() { - pb.Finished() - }() - - tMax := time.Time{} - - var lastID string - lastLog := map[string]string{} - for { - chg, err := cli.Change(id) - if err != nil { - // a client.Error means we were able to communicate with - // the server (got an answer) - if e, ok := err.(*client.Error); ok { - return nil, e - } - - // an non-client error here means the server most - // likely went away - // XXX: it actually can be a bunch of other things; fix client to expose it better - now := time.Now() - if tMax.IsZero() { - tMax = now.Add(maxGoneTime) - } - if now.After(tMax) { - return nil, err - } - pb.Spin(i18n.G("Waiting for server to restart")) - time.Sleep(pollTime) - continue - } - if !tMax.IsZero() { - pb.Finished() - tMax = time.Time{} - } - - for _, t := range chg.Tasks { - switch { - case t.Status != "Doing": - continue - case t.Progress.Total == 1: - pb.Spin(t.Summary) - nowLog := lastLogStr(t.Log) - if lastLog[t.ID] != nowLog { - pb.Notify(nowLog) - lastLog[t.ID] = nowLog - } - case t.ID == lastID: - pb.Set(float64(t.Progress.Done)) - default: - pb.Start(t.Summary, float64(t.Progress.Total)) - lastID = t.ID - } - break - } +var longInstallHelp = i18n.G(` +The install command installs the named snaps on the system. - if chg.Ready { - if chg.Status == "Done" { - return chg, nil - } +To install multiple instances of the same snap, append an underscore and a +unique identifier (for each instance) to a snap's name. - if chg.Err != "" { - return chg, errors.New(chg.Err) - } +With no further options, the snaps are installed tracking the stable channel, +with strict security confinement. - return nil, fmt.Errorf(i18n.G("change finished in status %q with no error message"), chg.Status) - } +Revision choice via the --revision override requires the the user to +have developer access to the snap, either directly or through the +store's collaboration feature, and to be logged in (see 'snap help login'). - // note this very purposely is not a ticker; we want - // to sleep 100ms between calls, not call once every - // 100ms. - time.Sleep(pollTime) - } -} +Note a later refresh will typically undo a revision override, taking the snap +back to the current revision of the channel it's tracking. -var ( - shortInstallHelp = i18n.G("Installs a snap to the system") - shortRemoveHelp = i18n.G("Removes a snap from the system") - shortRefreshHelp = i18n.G("Refreshes a snap in the system") - shortTryHelp = i18n.G("Tests a snap in the system") - shortEnableHelp = i18n.G("Enables a snap in the system") - shortDisableHelp = i18n.G("Disables a snap in the system") -) - -var longInstallHelp = i18n.G(` -The install command installs the named snap in the system. +Use --name to set the instance name when installing from snap file. `) var longRemoveHelp = i18n.G(` -The remove command removes the named snap from the system. +The remove command removes the named snap instance from the system. -By default all the snap revisions are removed, including their data and the common -data directory. When a --revision option is passed only the specified revision is -removed. +By default all the snap revisions are removed, including their data and the +common data directory. When a --revision option is passed only the specified +revision is removed. `) var longRefreshHelp = i18n.G(` -The refresh command refreshes (updates) the named snap. +The refresh command updates the specified snaps, or all snaps in the system if +none are specified. + +With no further options, the snaps are refreshed to the current revision of the +channel they're tracking, preserving their confinement options. + +Revision choice via the --revision override requires the the user to +have developer access to the snap, either directly or through the +store's collaboration feature, and to be logged in (see 'snap help login'). + +Note a later refresh will typically undo a revision override. `) var longTryHelp = i18n.G(` @@ -189,7 +102,7 @@ var longDisableHelp = i18n.G(` The disable command disables a snap. The binaries and services of the -snap will no longer be available. But all the data is still available +snap will no longer be available, but all the data is still available and the snap can easily be enabled again. `) @@ -205,8 +118,7 @@ func (x *cmdRemove) removeOne(opts *client.SnapOptions) error { name := string(x.Positional.Snaps[0]) - cli := Client() - changeID, err := cli.Remove(name, opts) + changeID, err := x.client.Remove(name, opts) if err != nil { msg, err := errorToCmdMessage(name, err, opts) if err != nil { @@ -216,31 +128,33 @@ return nil } - _, err = x.wait(cli, changeID) - if err == noWait { - return nil - } - if err != nil { + if _, err := x.wait(changeID); err != nil { + if err == noWait { + return nil + } return err } - fmt.Fprintf(Stdout, i18n.G("%s removed\n"), name) + if opts.Revision != "" { + fmt.Fprintf(Stdout, i18n.G("%s (revision %s) removed\n"), name, opts.Revision) + } else { + fmt.Fprintf(Stdout, i18n.G("%s removed\n"), name) + } return nil } func (x *cmdRemove) removeMany(opts *client.SnapOptions) error { names := installedSnapNames(x.Positional.Snaps) - cli := Client() - changeID, err := cli.RemoveMany(names, opts) + changeID, err := x.client.RemoveMany(names, opts) if err != nil { return err } - chg, err := x.wait(cli, changeID) - if err == noWait { - return nil - } + chg, err := x.wait(changeID) if err != nil { + if err == noWait { + return nil + } return err } @@ -302,11 +216,16 @@ } var channelDescs = mixinDescs{ - "channel": i18n.G("Use this channel instead of stable"), - "beta": i18n.G("Install from the beta channel"), - "edge": i18n.G("Install from the edge channel"), + // TRANSLATORS: This should not start with a lowercase letter. + "channel": i18n.G("Use this channel instead of stable"), + // TRANSLATORS: This should not start with a lowercase letter. + "beta": i18n.G("Install from the beta channel"), + // TRANSLATORS: This should not start with a lowercase letter. + "edge": i18n.G("Install from the edge channel"), + // TRANSLATORS: This should not start with a lowercase letter. "candidate": i18n.G("Install from the candidate channel"), - "stable": i18n.G("Install from the stable channel"), + // TRANSLATORS: This should not start with a lowercase letter. + "stable": i18n.G("Install from the stable channel"), } func (mx *channelMixin) setChannelFromCommandline() error { @@ -338,8 +257,7 @@ } // show what has been done -func showDone(names []string, op string) error { - cli := Client() +func showDone(cli *client.Client, names []string, op string, esc *escapes) error { snaps, err := cli.List(names, nil) if err != nil { return err @@ -352,17 +270,17 @@ } switch op { case "install": - if snap.Developer != "" { - // 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") - fmt.Fprintf(Stdout, i18n.G("%s%s %s from '%s' installed\n"), snap.Name, channelStr, snap.Version, snap.Developer) + if snap.Publisher != nil { + // 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") + fmt.Fprintf(Stdout, i18n.G("%s%s %s from %s installed\n"), snap.Name, channelStr, snap.Version, longPublisher(esc, snap.Publisher)) } else { // TRANSLATORS: the args are a snap name optionally followed by a channel, then a version (e.g. "some-snap (beta) 1.3 installed") fmt.Fprintf(Stdout, i18n.G("%s%s %s installed\n"), snap.Name, channelStr, snap.Version) } case "refresh": - if snap.Developer != "" { - // 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") - fmt.Fprintf(Stdout, i18n.G("%s%s %s from '%s' refreshed\n"), snap.Name, channelStr, snap.Version, snap.Developer) + if snap.Publisher != nil { + // 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") + fmt.Fprintf(Stdout, i18n.G("%s%s %s from %s refreshed\n"), snap.Name, channelStr, snap.Version, longPublisher(esc, snap.Publisher)) } else { // TRANSLATORS: the args are a snap name optionally followed by a channel, then a version (e.g. "some-snap (beta) 1.3 refreshed") fmt.Fprintf(Stdout, i18n.G("%s%s %s refreshed\n"), snap.Name, channelStr, snap.Version) @@ -373,9 +291,9 @@ default: fmt.Fprintf(Stdout, "internal error: unknown op %q", op) } - if snap.TrackingChannel != snap.Channel { - // TRANSLATORS: first %s is a snap name, following %s is a channel name - fmt.Fprintf(Stdout, i18n.G("Snap %s is no longer tracking %s.\n"), snap.Name, snap.TrackingChannel) + if snap.TrackingChannel != snap.Channel && snap.Channel != "" { + // TRANSLATORS: first %s is a channel name, following %s is a snap name, last %s is a channel name again. + fmt.Fprintf(Stdout, i18n.G("Channel %s for %s is closed; temporarily forwarding to %s.\n"), snap.TrackingChannel, snap.Name, snap.Channel) } } @@ -393,8 +311,11 @@ } var modeDescs = mixinDescs{ - "classic": i18n.G("Put snap in classic mode and disable security confinement"), - "devmode": i18n.G("Put snap in development mode and disable security confinement"), + // TRANSLATORS: This should not start with a lowercase letter. + "classic": i18n.G("Put snap in classic mode and disable security confinement"), + // TRANSLATORS: This should not start with a lowercase letter. + "devmode": i18n.G("Put snap in development mode and disable security confinement"), + // TRANSLATORS: This should not start with a lowercase letter. "jailmode": i18n.G("Put snap in enforced confinement mode"), } @@ -418,6 +339,7 @@ } type cmdInstall struct { + colorMixin waitMixin channelMixin @@ -431,39 +353,31 @@ Unaliased bool `long:"unaliased"` + Name string `long:"name"` + Positional struct { Snaps []remoteSnapName `positional-arg-name:""` } `positional-args:"yes" required:"yes"` } -func setupAbortHandler(changeId string) { - // Intercept sigint - c := make(chan os.Signal, 2) - signal.Notify(c, syscall.SIGINT) - go func() { - <-c - cli := Client() - _, err := cli.Abort(changeId) - if err != nil { - fmt.Fprintf(Stderr, err.Error()+"\n") - } - }() -} - -func (x *cmdInstall) installOne(name string, opts *client.SnapOptions) error { +func (x *cmdInstall) installOne(nameOrPath, desiredName string, opts *client.SnapOptions) error { var err error - var installFromFile bool var changeID string + var snapName string + var path string - cli := Client() - if strings.Contains(name, "/") || strings.HasSuffix(name, ".snap") || strings.Contains(name, ".snap.") { - installFromFile = true - changeID, err = cli.InstallPath(name, opts) + if strings.Contains(nameOrPath, "/") || strings.HasSuffix(nameOrPath, ".snap") || strings.Contains(nameOrPath, ".snap.") { + path = nameOrPath + changeID, err = x.client.InstallPath(path, x.Name, opts) } else { - changeID, err = cli.Install(name, opts) + snapName = nameOrPath + if desiredName != "" { + return errors.New(i18n.G("cannot use explicit name when installing from store")) + } + changeID, err = x.client.Install(snapName, opts) } if err != nil { - msg, err := errorToCmdMessage(name, err, opts) + msg, err := errorToCmdMessage(nameOrPath, err, opts) if err != nil { return err } @@ -471,27 +385,22 @@ return nil } - setupAbortHandler(changeID) - - chg, err := x.wait(cli, changeID) - if err == noWait { - return nil - } + chg, err := x.wait(changeID) if err != nil { + if err == noWait { + return nil + } return err } // extract the snapName from the change, important for sideloaded - var snapName string - - if installFromFile { + if path != "" { if err := chg.Get("snap-name", &snapName); err != nil { - return fmt.Errorf("cannot extract the snap-name from local file %q: %s", name, err) + return fmt.Errorf("cannot extract the snap-name from local file %q: %s", nameOrPath, err) } - name = snapName } - return showDone([]string{name}, "install") + return showDone(x.client, []string{snapName}, "install", x.getEscapes()) } func (x *cmdInstall) installMany(names []string, opts *client.SnapOptions) error { @@ -502,8 +411,7 @@ } } - cli := Client() - changeID, err := cli.InstallMany(names, opts) + changeID, err := x.client.InstallMany(names, opts) if err != nil { var snapName string if err, ok := err.(*client.Error); ok { @@ -517,13 +425,11 @@ return nil } - setupAbortHandler(changeID) - - chg, err := x.wait(cli, changeID) - if err == noWait { - return nil - } + chg, err := x.wait(changeID) if err != nil { + if err == noWait { + return nil + } return err } @@ -533,7 +439,7 @@ } if len(installed) > 0 { - if err := showDone(installed, "install"); err != nil { + if err := showDone(x.client, installed, "install", x.getEscapes()); err != nil { return err } } @@ -572,20 +478,33 @@ x.setModes(opts) names := remoteSnapNames(x.Positional.Snaps) + if len(names) == 0 { + return errors.New(i18n.G("cannot install zero snaps")) + } + for _, name := range names { + if len(name) == 0 { + return errors.New(i18n.G("cannot install snap with empty name")) + } + } + if len(names) == 1 { - return x.installOne(names[0], opts) + return x.installOne(names[0], x.Name, opts) } if x.asksForMode() || x.asksForChannel() { return errors.New(i18n.G("a single snap name is needed to specify mode or channel flags")) } + if x.Name != "" { + return errors.New(i18n.G("cannot use instance name when installing multiple snaps")) + } return x.installMany(names, nil) } type cmdRefresh struct { + colorMixin + timeMixin waitMixin - channelMixin modeMixin @@ -600,17 +519,16 @@ } func (x *cmdRefresh) refreshMany(snaps []string, opts *client.SnapOptions) error { - cli := Client() - changeID, err := cli.RefreshMany(snaps, opts) + changeID, err := x.client.RefreshMany(snaps, opts) if err != nil { return err } - chg, err := x.wait(cli, changeID) - if err == noWait { - return nil - } + chg, err := x.wait(changeID) if err != nil { + if err == noWait { + return nil + } return err } @@ -620,7 +538,7 @@ } if len(refreshed) > 0 { - return showDone(refreshed, "refresh") + return showDone(x.client, refreshed, "refresh", x.getEscapes()) } fmt.Fprintln(Stderr, i18n.G("All snaps up to date.")) @@ -629,8 +547,7 @@ } func (x *cmdRefresh) refreshOne(name string, opts *client.SnapOptions) error { - cli := Client() - changeID, err := cli.Refresh(name, opts) + changeID, err := x.client.Refresh(name, opts) if err != nil { msg, err := errorToCmdMessage(name, err, opts) if err != nil { @@ -640,20 +557,26 @@ return nil } - _, err = x.wait(cli, changeID) - if err == noWait { - return nil - } - if err != nil { + if _, err := x.wait(changeID); err != nil { + if err == noWait { + return nil + } return err } - return showDone([]string{name}, "refresh") + return showDone(x.client, []string{name}, "refresh", x.getEscapes()) +} + +func parseSysinfoTime(s string) time.Time { + t, err := time.Parse(time.RFC3339, s) + if err != nil { + return time.Time{} + } + return t } func (x *cmdRefresh) showRefreshTimes() error { - cli := Client() - sysinfo, err := cli.SysInfo() + sysinfo, err := x.client.SysInfo() if err != nil { return err } @@ -665,16 +588,27 @@ } else { return errors.New("internal error: both refresh.timer and refresh.schedule are empty") } - if sysinfo.Refresh.Last != "" { - fmt.Fprintf(Stdout, "last: %s\n", sysinfo.Refresh.Last) + last := parseSysinfoTime(sysinfo.Refresh.Last) + hold := parseSysinfoTime(sysinfo.Refresh.Hold) + next := parseSysinfoTime(sysinfo.Refresh.Next) + + if !last.IsZero() { + fmt.Fprintf(Stdout, "last: %s\n", x.fmtTime(last)) } else { fmt.Fprintf(Stdout, "last: n/a\n") } - if sysinfo.Refresh.Hold != "" { - fmt.Fprintf(Stdout, "hold: %s\n", sysinfo.Refresh.Hold) + if !hold.IsZero() { + fmt.Fprintf(Stdout, "hold: %s\n", x.fmtTime(hold)) } - if sysinfo.Refresh.Next != "" { - fmt.Fprintf(Stdout, "next: %s\n", sysinfo.Refresh.Next) + // only show "next" if its after "hold" to not confuse users + if !next.IsZero() { + // Snapstate checks for holdTime.After(limitTime) so we need + // to check for before or equal here to be fully correct. + if next.Before(hold) || next.Equal(hold) { + fmt.Fprintf(Stdout, "next: %s (but held)\n", x.fmtTime(next)) + } else { + fmt.Fprintf(Stdout, "next: %s\n", x.fmtTime(next)) + } } else { fmt.Fprintf(Stdout, "next: n/a\n") } @@ -682,8 +616,7 @@ } func (x *cmdRefresh) listRefresh() error { - cli := Client() - snaps, _, err := cli.Find(&client.FindOptions{ + snaps, _, err := x.client.Find(&client.FindOptions{ Refresh: true, }) if err != nil { @@ -696,12 +629,14 @@ sort.Sort(snapsByName(snaps)) + esc := x.getEscapes() w := tabWriter() defer w.Flush() - fmt.Fprintln(w, i18n.G("Name\tVersion\tRev\tDeveloper\tNotes")) + // TRANSLATORS: the %s is to insert a filler escape sequence (please keep it flush to the column header, with no extra spaces) + fmt.Fprintf(w, i18n.G("Name\tVersion\tRev\tPublisher%s\tNotes\n"), fillerPublisher(esc)) for _, snap := range snaps { - fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", snap.Name, snap.Version, snap.Revision, snap.Developer, NotesFromRemote(snap, nil)) + 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)) } return nil @@ -717,14 +652,14 @@ if x.Time { if x.asksForMode() || x.asksForChannel() { - return errors.New(i18n.G("--time does not take mode nor channel flags")) + return errors.New(i18n.G("--time does not take mode or channel flags")) } return x.showRefreshTimes() } if x.List { - if x.asksForMode() || x.asksForChannel() { - return errors.New(i18n.G("--list does not take mode nor channel flags")) + if len(x.Positional.Snaps) > 0 || x.asksForMode() || x.asksForChannel() { + return errors.New(i18n.G("--list does not accept additional arguments")) } return x.listRefresh() @@ -785,7 +720,6 @@ if err := x.validateMode(); err != nil { return err } - cli := Client() name := x.Positional.SnapDir opts := &client.SnapOptions{} x.setModes(opts) @@ -809,21 +743,21 @@ return fmt.Errorf(i18n.G("cannot get full path for %q: %v"), name, err) } - changeID, err := cli.Try(path, opts) - if e, ok := err.(*client.Error); ok && e.Kind == client.ErrorKindNotSnap { - return fmt.Errorf(i18n.G(`%q does not contain an unpacked snap. - -Try "snapcraft prime" in your project directory, then "snap try" again.`), path) - } + changeID, err := x.client.Try(path, opts) if err != nil { - return err - } - - chg, err := x.wait(cli, changeID) - if err == noWait { + msg, err := errorToCmdMessage(name, err, opts) + if err != nil { + return err + } + fmt.Fprintln(Stderr, msg) return nil } + + chg, err := x.wait(changeID) if err != nil { + if err == noWait { + return nil + } return err } @@ -836,7 +770,7 @@ name = snapName // show output as speced - snaps, err := cli.List([]string{name}, nil) + snaps, err := x.client.List([]string{name}, nil) if err != nil { return err } @@ -859,19 +793,17 @@ } func (x *cmdEnable) Execute([]string) error { - cli := Client() name := string(x.Positional.Snap) opts := &client.SnapOptions{} - changeID, err := cli.Enable(name, opts) + changeID, err := x.client.Enable(name, opts) if err != nil { return err } - _, err = x.wait(cli, changeID) - if err == noWait { - return nil - } - if err != nil { + if _, err := x.wait(changeID); err != nil { + if err == noWait { + return nil + } return err } @@ -888,19 +820,17 @@ } func (x *cmdDisable) Execute([]string) error { - cli := Client() name := string(x.Positional.Snap) opts := &client.SnapOptions{} - changeID, err := cli.Disable(name, opts) + changeID, err := x.client.Disable(name, opts) if err != nil { return err } - _, err = x.wait(cli, changeID) - if err == noWait { - return nil - } - if err != nil { + if _, err := x.wait(changeID); err != nil { + if err == noWait { + return nil + } return err } @@ -937,24 +867,22 @@ return err } - cli := Client() name := string(x.Positional.Snap) opts := &client.SnapOptions{Revision: x.Revision} x.setModes(opts) - changeID, err := cli.Revert(name, opts) + changeID, err := x.client.Revert(name, opts) if err != nil { return err } - _, err = x.wait(cli, changeID) - if err == noWait { - return nil - } - if err != nil { + if _, err := x.wait(changeID); err != nil { + if err == noWait { + return nil + } return err } - return showDone([]string{name}, "revert") + return showDone(x.client, []string{name}, "revert", nil) } var shortSwitchHelp = i18n.G("Switches snap to a different channel") @@ -964,6 +892,7 @@ `) type cmdSwitch struct { + waitMixin channelMixin Positional struct { @@ -979,18 +908,20 @@ return fmt.Errorf("missing --channel= parameter") } - cli := Client() name := string(x.Positional.Snap) channel := string(x.Channel) opts := &client.SnapOptions{ Channel: channel, } - changeID, err := cli.Switch(name, opts) + changeID, err := x.client.Switch(name, opts) if err != nil { return err } - if _, err = wait(cli, changeID); err != nil { + if _, err := x.wait(changeID); err != nil { + if err == noWait { + return nil + } return err } @@ -1000,28 +931,42 @@ func init() { addCommand("remove", shortRemoveHelp, longRemoveHelp, func() flags.Commander { return &cmdRemove{} }, - waitDescs.also(map[string]string{"revision": i18n.G("Remove only the given revision")}), nil) + waitDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "revision": i18n.G("Remove only the given revision"), + }), nil) addCommand("install", shortInstallHelp, longInstallHelp, func() flags.Commander { return &cmdInstall{} }, - waitDescs.also(channelDescs).also(modeDescs).also(map[string]string{ - "revision": i18n.G("Install the given revision of a snap, to which you must have developer access"), - "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)"), + colorDescs.also(waitDescs).also(channelDescs).also(modeDescs).also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "revision": i18n.G("Install the given revision of a snap, to which you must have developer access"), + // TRANSLATORS: This should not start with a lowercase letter. + "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)"), + // TRANSLATORS: This should not start with a lowercase letter. "force-dangerous": i18n.G("Alias for --dangerous (DEPRECATED)"), - "unaliased": i18n.G("Install the given snap without enabling its automatic aliases"), + // TRANSLATORS: This should not start with a lowercase letter. + "unaliased": i18n.G("Install the given snap without enabling its automatic aliases"), + // TRANSLATORS: This should not start with a lowercase letter. + "name": i18n.G("Install the snap file under the given instance name"), }), nil) addCommand("refresh", shortRefreshHelp, longRefreshHelp, func() flags.Commander { return &cmdRefresh{} }, - waitDescs.also(channelDescs).also(modeDescs).also(map[string]string{ - "amend": i18n.G("Allow refresh attempt on snap unknown to the store"), - "revision": i18n.G("Refresh to the given revision"), - "list": i18n.G("Show available snaps for refresh but do not perform a refresh"), - "time": i18n.G("Show auto refresh information but do not perform a refresh"), + colorDescs.also(waitDescs).also(channelDescs).also(modeDescs).also(timeDescs).also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "amend": i18n.G("Allow refresh attempt on snap unknown to the store"), + // TRANSLATORS: This should not start with a lowercase letter. + "revision": i18n.G("Refresh to the given revision, to which you must have developer access"), + // TRANSLATORS: This should not start with a lowercase letter. + "list": i18n.G("Show the new versions of snaps that would be updated with the next refresh"), + // TRANSLATORS: This should not start with a lowercase letter. + "time": i18n.G("Show auto refresh information but do not perform a refresh"), + // TRANSLATORS: This should not start with a lowercase letter. "ignore-validation": i18n.G("Ignore validation by other snaps blocking the refresh"), }), nil) addCommand("try", shortTryHelp, longTryHelp, func() flags.Commander { return &cmdTry{} }, waitDescs.also(modeDescs), nil) addCommand("enable", shortEnableHelp, longEnableHelp, func() flags.Commander { return &cmdEnable{} }, waitDescs, nil) addCommand("disable", shortDisableHelp, longDisableHelp, func() flags.Commander { return &cmdDisable{} }, waitDescs, nil) addCommand("revert", shortRevertHelp, longRevertHelp, func() flags.Commander { return &cmdRevert{} }, waitDescs.also(modeDescs).also(map[string]string{ - "revision": "Revert to the given revision", + // TRANSLATORS: This should not start with a lowercase letter. + "revision": i18n.G("Revert to the given revision"), }), nil) - addCommand("switch", shortSwitchHelp, longSwitchHelp, func() flags.Commander { return &cmdSwitch{} }, nil, nil) - + addCommand("switch", shortSwitchHelp, longSwitchHelp, func() flags.Commander { return &cmdSwitch{} }, waitDescs.also(channelDescs), nil) } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_snap_op_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_snap_op_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_snap_op_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_snap_op_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -28,6 +28,7 @@ "net/http/httptest" "path/filepath" "regexp" + "strings" "time" "gopkg.in/check.v1" @@ -37,15 +38,18 @@ "github.com/snapcore/snapd/progress" "github.com/snapcore/snapd/progress/progresstest" "github.com/snapcore/snapd/testutil" + "os" ) type snapOpTestServer struct { c *check.C - checker func(r *http.Request) - n int - total int - channel string + checker func(r *http.Request) + n int + total int + channel string + rebooting bool + snap string } var _ = check.Suite(&SnapOpSuite{}) @@ -54,21 +58,29 @@ switch t.n { case 0: t.checker(r) - t.c.Check(r.Method, check.Equals, "POST") + method := "POST" + if strings.HasSuffix(r.URL.Path, "/conf") { + method = "PUT" + } + t.c.Check(r.Method, check.Equals, method) w.WriteHeader(202) fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`) case 1: t.c.Check(r.Method, check.Equals, "GET") t.c.Check(r.URL.Path, check.Equals, "/v2/changes/42") - fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`) + if !t.rebooting { + fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`) + } else { + fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}, "maintenance": {"kind": "system-restart", "message": "system is restarting"}}}`) + } case 2: t.c.Check(r.Method, check.Equals, "GET") t.c.Check(r.URL.Path, check.Equals, "/v2/changes/42") - fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"snap-name": "foo"}}}`) + fmt.Fprintf(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"snap-name": "%s"}}}\n`, t.snap) case 3: t.c.Check(r.Method, check.Equals, "GET") t.c.Check(r.URL.Path, check.Equals, "/v2/snaps") - fmt.Fprintf(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "1.0", "developer": "bar", "revision":42, "channel":"%s"}]}\n`, t.channel) + fmt.Fprintf(w, `{"type": "sync", "result": [{"name": "%s", "status": "active", "version": "1.0", "developer": "bar", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":42, "channel":"%s"}]}\n`, t.snap, t.channel) default: t.c.Fatalf("expected to get %d requests, now on %d", t.total, t.n+1) } @@ -96,6 +108,7 @@ s.srv = snapOpTestServer{ c: c, total: 4, + snap: "foo", } } @@ -147,6 +160,31 @@ c.Check(meter.Labels, testutil.Contains, "Waiting for server to restart") } +func (s *SnapOpSuite) TestWaitRebooting(c *check.C) { + meter := &progresstest.Meter{} + defer progress.MockMeter(meter)() + restore := snap.MockMaxGoneTime(time.Millisecond) + defer restore() + + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "sync", +"result": { +"ready": false, +"status": "Doing", +"tasks": [{"kind": "bar", "summary": "...", "status": "Doing", "progress": {"done": 1, "total": 1}, "log": ["INFO: info"]}] +}, +"maintenance": {"kind": "system-restart", "message": "system is restarting"}}`) + }) + + cli := snap.Client() + chg, err := snap.Wait(cli, "x") + c.Assert(chg, check.IsNil) + c.Assert(err, check.DeepEquals, &client.Error{Kind: client.ErrorKindSystemRestart, Message: "system is restarting"}) + + // last available info is still displayed + c.Check(meter.Notices, testutil.Contains, "INFO: info") +} + func (s *SnapOpSuite) TestInstall(c *check.C) { s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") @@ -158,10 +196,10 @@ } s.RedirectClientToTestServer(s.srv.handle) - rest, err := snap.Parser().ParseArgs([]string{"install", "--channel", "candidate", "foo"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "candidate", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(candidate\) 1.0 from 'bar' installed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(candidate\) 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) @@ -179,10 +217,10 @@ s.RedirectClientToTestServer(s.srv.handle) // snap install --channel=3.4 means 3.4/stable, this is what we test here - rest, err := snap.Parser().ParseArgs([]string{"install", "--channel", "3.4", "foo"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "3.4", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(3.4/stable\) 1.0 from 'bar' installed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(3.4/stable\) 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) @@ -199,10 +237,10 @@ } s.RedirectClientToTestServer(s.srv.handle) - rest, err := snap.Parser().ParseArgs([]string{"install", "--channel", "3.4/hotfix-1", "foo"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "3.4/hotfix-1", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(3.4/hotfix-1\) 1.0 from 'bar' installed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(3.4/hotfix-1\) 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) @@ -220,10 +258,10 @@ } s.RedirectClientToTestServer(s.srv.handle) - rest, err := snap.Parser().ParseArgs([]string{"install", "--channel", "beta", "--devmode", "foo"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "beta", "--devmode", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(beta\) 1.0 from 'bar' installed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(beta\) 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) @@ -239,10 +277,10 @@ } s.RedirectClientToTestServer(s.srv.handle) - rest, err := snap.Parser().ParseArgs([]string{"install", "--classic", "foo"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--classic", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) @@ -258,15 +296,323 @@ } s.RedirectClientToTestServer(s.srv.handle) - rest, err := snap.Parser().ParseArgs([]string{"install", "--unaliased", "foo"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--unaliased", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } +func (s *SnapOpSuite) TestInstallSnapNotFound(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "snap not found", "value": "foo", "kind": "snap-not-found"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("error: %v\n", err), check.Equals, `error: snap "foo" not found +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailable(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision available as specified", "value": "foo", "kind": "snap-revision-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: snap "foo" not available as specified (see 'snap info foo') +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableOnChannel(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision available as specified", "value": "foo", "kind": "snap-revision-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=mytrack", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: snap "foo" not available on channel "mytrack/stable" (see 'snap info + foo') +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableAtRevision(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision available as specified", "value": "foo", "kind": "snap-revision-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--revision=2", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: snap "foo" revision 2 not available (see 'snap info foo') +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelTrackOK(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { + "snap-name": "foo", + "action": "install", + "architecture": "amd64", + "channel": "stable", + "releases": [{"architecture": "amd64", "channel": "beta"}, + {"architecture": "amd64", "channel": "edge"}] +}, "kind": "snap-channel-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: snap "foo" is not available on stable but is available to install on the + following channels: + + beta snap install --beta foo + edge snap install --edge foo + + Please be mindful pre-release channels may include features not + completely tested or implemented. Get more information with 'snap info + foo'. +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelTrackOKPrerelOK(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { + "snap-name": "foo", + "action": "install", + "architecture": "amd64", + "channel": "candidate", + "releases": [{"architecture": "amd64", "channel": "beta"}, + {"architecture": "amd64", "channel": "edge"}] +}, "kind": "snap-channel-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--candidate", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: snap "foo" is not available on candidate but is available to install on + the following channels: + + beta snap install --beta foo + edge snap install --edge foo + + Get more information with 'snap info foo'. +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelTrackOther(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { + "snap-name": "foo", + "action": "install", + "architecture": "amd64", + "channel": "stable", + "releases": [{"architecture": "amd64", "channel": "1.0/stable"}, + {"architecture": "amd64", "channel": "2.0/stable"}] +}, "kind": "snap-channel-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: snap "foo" is not available on latest/stable but is available to install + on the following tracks: + + 1.0/stable snap install --channel=1.0 foo + 2.0/stable snap install --channel=2.0 foo + + Please be mindful that different tracks may include different features. + Get more information with 'snap info foo'. +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelTrackLatestStable(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { + "snap-name": "foo", + "action": "install", + "architecture": "amd64", + "channel": "2.0/stable", + "releases": [{"architecture": "amd64", "channel": "stable"}] +}, "kind": "snap-channel-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=2.0/stable", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: snap "foo" is not available on 2.0/stable but is available to install on + the following tracks: + + latest/stable snap install --stable foo + + Please be mindful that different tracks may include different features. + Get more information with 'snap info foo'. +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelTrackAndRiskOther(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { + "snap-name": "foo", + "action": "install", + "architecture": "amd64", + "channel": "2.0/stable", + "releases": [{"architecture": "amd64", "channel": "1.0/edge"}] +}, "kind": "snap-channel-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=2.0/stable", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: snap "foo" is not available on 2.0/stable but other tracks exist. + + Please be mindful that different tracks may include different features. + Get more information with 'snap info foo'. +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForArchitectureTrackAndRiskOK(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified architecture", "value": { + "snap-name": "foo", + "action": "install", + "architecture": "arm64", + "channel": "stable", + "releases": [{"architecture": "amd64", "channel": "stable"}, + {"architecture": "s390x", "channel": "stable"}] +}, "kind": "snap-architecture-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: snap "foo" is not available on stable for this architecture (arm64) but + exists on other architectures (amd64, s390x). +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForArchitectureTrackAndRiskOther(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified architecture", "value": { + "snap-name": "foo", + "action": "install", + "architecture": "arm64", + "channel": "1.0/stable", + "releases": [{"architecture": "amd64", "channel": "stable"}, + {"architecture": "s390x", "channel": "stable"}] +}, "kind": "snap-architecture-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=1.0/stable", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: snap "foo" is not available on this architecture (arm64) but exists on + other architectures (amd64, s390x). +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableInvalidChannel(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { + "snap-name": "foo", + "action": "install", + "architecture": "amd64", + "channel": "a/b/c/d", + "releases": [{"architecture": "amd64", "channel": "stable"}] +}, "kind": "snap-channel-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=a/b/c/d", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: requested channel "a/b/c/d" is not valid (see 'snap info foo' for valid + ones) +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelNonExistingBranchOnMainChannel(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { + "snap-name": "foo", + "action": "install", + "architecture": "amd64", + "channel": "stable/baz", + "releases": [{"architecture": "amd64", "channel": "stable"}] +}, "kind": "snap-channel-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=stable/baz", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: requested a non-existing branch on latest/stable for snap "foo": baz +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelNonExistingBranch(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { + "snap-name": "foo", + "action": "install", + "architecture": "amd64", + "channel": "stable/baz", + "releases": [{"architecture": "amd64", "channel": "edge"}] +}, "kind": "snap-channel-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=stable/baz", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: requested a non-existing branch for snap "foo": latest/stable/baz +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + func testForm(r *http.Request, c *check.C) *multipart.Form { contentType := r.Header.Get("Content-Type") mediaType, params, err := mime.ParseMediaType(contentType) @@ -321,10 +667,10 @@ err := ioutil.WriteFile(snapPath, snapBody, 0644) c.Assert(err, check.IsNil) - rest, err := snap.Parser().ParseArgs([]string{"install", snapPath}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", snapPath}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) @@ -352,10 +698,10 @@ err := ioutil.WriteFile(snapPath, snapBody, 0644) c.Assert(err, check.IsNil) - rest, err := snap.Parser().ParseArgs([]string{"install", "--devmode", snapPath}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--devmode", snapPath}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) @@ -383,10 +729,10 @@ err := ioutil.WriteFile(snapPath, snapBody, 0644) c.Assert(err, check.IsNil) - rest, err := snap.Parser().ParseArgs([]string{"install", "--classic", snapPath}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--classic", snapPath}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) @@ -414,15 +760,60 @@ err := ioutil.WriteFile(snapPath, snapBody, 0644) c.Assert(err, check.IsNil) - rest, err := snap.Parser().ParseArgs([]string{"install", "--dangerous", snapPath}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--dangerous", snapPath}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } +func (s *SnapOpSuite) TestInstallPathInstance(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + + form := testForm(r, c) + defer form.RemoveAll() + + c.Check(form.Value["action"], check.DeepEquals, []string{"install"}) + c.Check(form.Value["name"], check.DeepEquals, []string{"foo_bar"}) + c.Check(form.Value["devmode"], check.IsNil) + c.Check(form.Value["snap-path"], check.NotNil) + c.Check(form.Value, check.HasLen, 3) + + name, _, body := formFile(form, c) + c.Check(name, check.Equals, "snap") + c.Check(string(body), check.Equals, "snap-data") + } + + snapBody := []byte("snap-data") + s.RedirectClientToTestServer(s.srv.handle) + // instance is named foo_bar + s.srv.snap = "foo_bar" + snapPath := filepath.Join(c.MkDir(), "foo.snap") + err := ioutil.WriteFile(snapPath, snapBody, 0644) + c.Assert(err, check.IsNil) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", snapPath, "--name", "foo_bar"}) + c.Assert(rest, check.DeepEquals, []string{}) + c.Assert(err, check.IsNil) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo_bar 1.0 from Bar installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapSuite) TestInstallWithInstanceNoPath(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--name", "foo_bar", "some-snap"}) + c.Assert(err, check.ErrorMatches, "cannot use explicit name when installing from store") +} + +func (s *SnapSuite) TestInstallManyWithInstance(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--name", "foo_bar", "some-snap-1", "some-snap-2"}) + c.Assert(err, check.ErrorMatches, "cannot use instance name when installing multiple snaps") +} + func (s *SnapOpSuite) TestRevertRunthrough(c *check.C) { s.srv.total = 4 s.srv.channel = "potato" @@ -434,12 +825,12 @@ } s.RedirectClientToTestServer(s.srv.handle) - rest, err := snap.Parser().ParseArgs([]string{"revert", "foo"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"revert", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) // tracking channel is "" in the test server c.Check(s.Stdout(), check.Equals, `foo reverted to 1.0 -Snap foo is no longer tracking . +Channel for foo is closed; temporarily forwarding to potato. `) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit @@ -484,7 +875,7 @@ } } - rest, err := snap.Parser().ParseArgs(cmd) + rest, err := snap.Parser(snap.Client()).ParseArgs(cmd) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, "foo reverted to 1.0\n") @@ -510,11 +901,25 @@ } func (s *SnapOpSuite) TestRevertMissingName(c *check.C) { - _, err := snap.Parser().ParseArgs([]string{"revert"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"revert"}) c.Assert(err, check.NotNil) c.Assert(err, check.ErrorMatches, "the required argument `` was not provided") } +func (s *SnapSuite) TestRefreshListLessOptions(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Fatal("expected to get 0 requests") + }) + + for _, flag := range []string{"--beta", "--channel=potato", "--classic"} { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--list", flag}) + c.Assert(err, check.ErrorMatches, "--list does not accept additional arguments") + + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--list", flag, "some-snap"}) + c.Assert(err, check.ErrorMatches, "--list does not accept additional arguments") + } +} + func (s *SnapSuite) TestRefreshList(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { @@ -523,17 +928,17 @@ c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/find") c.Check(r.URL.Query().Get("select"), check.Equals, "refresh") - fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2update1", "developer": "bar", "revision":17,"summary":"some summary"}]}`) + fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2update1", "developer": "bar", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":17,"summary":"some summary"}]}`) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) } n++ }) - rest, err := snap.Parser().ParseArgs([]string{"refresh", "--list"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--list"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Developer +Notes + c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Publisher +Notes foo +4.2update1 +17 +bar +-.* `) c.Check(s.Stderr(), check.Equals, "") @@ -548,19 +953,19 @@ case 0: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/system-info") - fmt.Fprintln(w, `{"type": "sync", "status-code": 200, "result": {"refresh": {"schedule": "00:00-04:59/5:00-10:59/11:00-16:59/17:00-23:59", "last": "2017-04-25T17:35:00+0200", "next": "2017-04-26T00:58:00+0200"}}}`) + fmt.Fprintln(w, `{"type": "sync", "status-code": 200, "result": {"refresh": {"schedule": "00:00-04:59/5:00-10:59/11:00-16:59/17:00-23:59", "last": "2017-04-25T17:35:00+02:00", "next": "2017-04-26T00:58:00+02:00"}}}`) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) } n++ }) - rest, err := snap.Parser().ParseArgs([]string{"refresh", "--time"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--time", "--abs-time"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `schedule: 00:00-04:59/5:00-10:59/11:00-16:59/17:00-23:59 -last: 2017-04-25T17:35:00+0200 -next: 2017-04-26T00:58:00+0200 +last: 2017-04-25T17:35:00+02:00 +next: 2017-04-26T00:58:00+02:00 `) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit @@ -574,19 +979,19 @@ case 0: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/system-info") - fmt.Fprintln(w, `{"type": "sync", "status-code": 200, "result": {"refresh": {"timer": "0:00-24:00/4", "last": "2017-04-25T17:35:00+0200", "next": "2017-04-26T00:58:00+0200"}}}`) + fmt.Fprintln(w, `{"type": "sync", "status-code": 200, "result": {"refresh": {"timer": "0:00-24:00/4", "last": "2017-04-25T17:35:00+02:00", "next": "2017-04-26T00:58:00+02:00"}}}`) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) } n++ }) - rest, err := snap.Parser().ParseArgs([]string{"refresh", "--time"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--time", "--abs-time"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `timer: 0:00-24:00/4 -last: 2017-04-25T17:35:00+0200 -next: 2017-04-26T00:58:00+0200 +last: 2017-04-25T17:35:00+02:00 +next: 2017-04-26T00:58:00+02:00 `) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit @@ -600,20 +1005,20 @@ case 0: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/system-info") - fmt.Fprintln(w, `{"type": "sync", "status-code": 200, "result": {"refresh": {"timer": "0:00-24:00/4", "last": "2017-04-25T17:35:00+0200", "next": "2017-04-26T00:58:00+0200", "hold": "2017-04-28T00:00:00+0200"}}}`) + fmt.Fprintln(w, `{"type": "sync", "status-code": 200, "result": {"refresh": {"timer": "0:00-24:00/4", "last": "2017-04-25T17:35:00+02:00", "next": "2017-04-26T00:58:00+02:00", "hold": "2017-04-28T00:00:00+02:00"}}}`) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) } n++ }) - rest, err := snap.Parser().ParseArgs([]string{"refresh", "--time"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--time", "--abs-time"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `timer: 0:00-24:00/4 -last: 2017-04-25T17:35:00+0200 -hold: 2017-04-28T00:00:00+0200 -next: 2017-04-26T00:58:00+0200 +last: 2017-04-25T17:35:00+02:00 +hold: 2017-04-28T00:00:00+02:00 +next: 2017-04-26T00:58:00+02:00 (but held) `) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit @@ -626,16 +1031,10 @@ c.Check(r.URL.Path, check.Equals, "/v2/system-info") fmt.Fprintln(w, `{"type": "sync", "status-code": 200, "result": {"refresh": {"last": "2017-04-25T17:35:00+0200", "next": "2017-04-26T00:58:00+0200"}}}`) }) - _, err := snap.Parser().ParseArgs([]string{"refresh", "--time"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--time"}) c.Assert(err, check.ErrorMatches, `internal error: both refresh.timer and refresh.schedule are empty`) } -func (s *SnapSuite) TestRefreshListErr(c *check.C) { - s.RedirectClientToTestServer(nil) - _, err := snap.Parser().ParseArgs([]string{"refresh", "--list", "--beta"}) - c.Check(err, check.ErrorMatches, "--list does not take .* flags") -} - func (s *SnapOpSuite) TestRefreshOne(c *check.C) { s.RedirectClientToTestServer(s.srv.handle) s.srv.checker = func(r *http.Request) { @@ -645,9 +1044,9 @@ "action": "refresh", }) } - _, err := snap.Parser().ParseArgs([]string{"refresh", "foo"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "foo"}) c.Assert(err, check.IsNil) - c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' refreshed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar refreshed`) } @@ -662,9 +1061,9 @@ }) s.srv.channel = "beta" } - _, err := snap.Parser().ParseArgs([]string{"refresh", "--beta", "foo"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--beta", "foo"}) c.Assert(err, check.IsNil) - c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(beta\) 1.0 from 'bar' refreshed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(beta\) 1.0 from Bar refreshed`) } func (s *SnapOpSuite) TestRefreshOneClassic(c *check.C) { @@ -677,7 +1076,7 @@ "classic": true, }) } - _, err := snap.Parser().ParseArgs([]string{"refresh", "--classic", "one"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--classic", "one"}) c.Assert(err, check.IsNil) } @@ -691,7 +1090,7 @@ "devmode": true, }) } - _, err := snap.Parser().ParseArgs([]string{"refresh", "--devmode", "one"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--devmode", "one"}) c.Assert(err, check.IsNil) } @@ -705,7 +1104,7 @@ "jailmode": true, }) } - _, err := snap.Parser().ParseArgs([]string{"refresh", "--jailmode", "one"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--jailmode", "one"}) c.Assert(err, check.IsNil) } @@ -719,43 +1118,63 @@ "ignore-validation": true, }) } - _, err := snap.Parser().ParseArgs([]string{"refresh", "--ignore-validation", "one"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--ignore-validation", "one"}) c.Assert(err, check.IsNil) } +func (s *SnapOpSuite) TestRefreshOneRebooting(c *check.C) { + s.RedirectClientToTestServer(s.srv.handle) + s.srv.checker = func(r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/core") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + }) + } + s.srv.rebooting = true + + restore := mockArgs("snap", "refresh", "core") + defer restore() + + err := snap.RunMain() + c.Check(err, check.IsNil) + c.Check(s.Stderr(), check.Equals, "snapd is about to reboot the system\n") + +} + func (s *SnapOpSuite) TestRefreshOneModeErr(c *check.C) { s.RedirectClientToTestServer(nil) - _, err := snap.Parser().ParseArgs([]string{"refresh", "--jailmode", "--devmode", "one"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--jailmode", "--devmode", "one"}) c.Assert(err, check.ErrorMatches, `cannot use devmode and jailmode flags together`) } func (s *SnapOpSuite) TestRefreshOneChanErr(c *check.C) { s.RedirectClientToTestServer(nil) - _, err := snap.Parser().ParseArgs([]string{"refresh", "--beta", "--channel=foo", "one"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--beta", "--channel=foo", "one"}) c.Assert(err, check.ErrorMatches, `Please specify a single channel`) } func (s *SnapOpSuite) TestRefreshAllChannel(c *check.C) { s.RedirectClientToTestServer(nil) - _, err := snap.Parser().ParseArgs([]string{"refresh", "--beta"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--beta"}) c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`) } func (s *SnapOpSuite) TestRefreshManyChannel(c *check.C) { s.RedirectClientToTestServer(nil) - _, err := snap.Parser().ParseArgs([]string{"refresh", "--beta", "one", "two"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--beta", "one", "two"}) c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`) } func (s *SnapOpSuite) TestRefreshManyIgnoreValidation(c *check.C) { s.RedirectClientToTestServer(nil) - _, err := snap.Parser().ParseArgs([]string{"refresh", "--ignore-validation", "one", "two"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--ignore-validation", "one", "two"}) c.Assert(err, check.ErrorMatches, `a single snap name must be specified when ignoring validation`) } func (s *SnapOpSuite) TestRefreshAllModeFlags(c *check.C) { s.RedirectClientToTestServer(nil) - _, err := snap.Parser().ParseArgs([]string{"refresh", "--devmode"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--devmode"}) c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`) } @@ -769,7 +1188,7 @@ "amend": true, }) } - _, err := snap.Parser().ParseArgs([]string{"refresh", "--amend", "one"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--amend", "one"}) c.Assert(err, check.IsNil) } @@ -820,7 +1239,7 @@ } } - rest, err := snap.Parser().ParseArgs(cmd) + rest, err := snap.Parser(snap.Client()).ParseArgs(cmd) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, fmt.Sprintf(`(?sm).*foo 1.0 mounted from .*%s`, tryDir)) @@ -861,19 +1280,77 @@ }) cmd := []string{"try", "/"} - _, err := snap.Parser().ParseArgs(cmd) - c.Assert(err, check.ErrorMatches, `"/" does not contain an unpacked snap. + _, err := snap.Parser(snap.Client()).ParseArgs(cmd) + c.Assert(err, testutil.EqualsWrapped, ` +"/" does not contain an unpacked snap. + +Try 'snapcraft prime' in your project directory, then 'snap try' again.`) +} + +func (s *SnapOpSuite) TestTryMissingOpt(c *check.C) { + oldArgs := os.Args + defer func() { + os.Args = oldArgs + }() + os.Args = []string{"snap", "try", "./"} + var kind string + + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, check.Equals, "POST", check.Commentf("%q", kind)) + w.WriteHeader(400) + fmt.Fprintf(w, ` +{ + "type": "error", + "result": { + "message":"error from server", + "value": "some-snap", + "kind": %q + }, + "status-code": 400 +}`, kind) + }) + + type table struct { + kind, expected string + } -Try "snapcraft prime" in your project directory, then "snap try" again.`) + tests := []table{ + {"snap-needs-classic", "published using classic confinement"}, + {"snap-needs-devmode", "only meant for development"}, + } + + for _, test := range tests { + kind = test.kind + c.Check(snap.RunMain(), testutil.ContainsWrapped, test.expected, check.Commentf("%q", kind)) + } +} + +func (s *SnapOpSuite) TestInstallConfinedAsClassic(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + w.WriteHeader(400) + fmt.Fprintf(w, `{ + "type": "error", + "result": { + "message":"error from server", + "value": "some-snap", + "kind": "snap-not-classic" + }, + "status-code": 400 +}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--classic", "some-snap"}) + c.Assert(err, check.ErrorMatches, `snap "some-snap" is not compatible with --classic`) } func (s *SnapSuite) TestInstallChannelDuplicationError(c *check.C) { - _, err := snap.Parser().ParseArgs([]string{"install", "--edge", "--beta", "some-snap"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--edge", "--beta", "some-snap"}) c.Assert(err, check.ErrorMatches, "Please specify a single channel") } func (s *SnapSuite) TestRefreshChannelDuplicationError(c *check.C) { - _, err := snap.Parser().ParseArgs([]string{"refresh", "--edge", "--beta", "some-snap"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--edge", "--beta", "some-snap"}) c.Assert(err, check.ErrorMatches, "Please specify a single channel") } @@ -888,10 +1365,10 @@ } s.RedirectClientToTestServer(s.srv.handle) - rest, err := snap.Parser().ParseArgs([]string{"install", "--edge", "foo"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--edge", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(edge\) 1.0 from 'bar' installed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(edge\) 1.0 from Bar installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) @@ -907,7 +1384,7 @@ } s.RedirectClientToTestServer(s.srv.handle) - rest, err := snap.Parser().ParseArgs([]string{"enable", "foo"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"enable", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*foo enabled`) @@ -926,7 +1403,7 @@ } s.RedirectClientToTestServer(s.srv.handle) - rest, err := snap.Parser().ParseArgs([]string{"disable", "foo"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"disable", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*foo disabled`) @@ -945,7 +1422,7 @@ } s.RedirectClientToTestServer(s.srv.handle) - rest, err := snap.Parser().ParseArgs([]string{"remove", "foo"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"remove", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*foo removed`) @@ -954,9 +1431,29 @@ c.Check(s.srv.n, check.Equals, s.srv.total) } +func (s *SnapOpSuite) TestRemoveRevision(c *check.C) { + s.srv.total = 3 + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "remove", + "revision": "17", + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"remove", "--revision=17", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(revision 17\) removed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + func (s *SnapOpSuite) TestRemoveManyRevision(c *check.C) { s.RedirectClientToTestServer(nil) - _, err := snap.Parser().ParseArgs([]string{"remove", "--revision=17", "one", "two"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"remove", "--revision=17", "one", "two"}) c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify the revision`) } @@ -990,7 +1487,7 @@ n++ }) - rest, err := snap.Parser().ParseArgs([]string{"remove", "one", "two"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"remove", "one", "two"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*one removed`) @@ -1002,13 +1499,13 @@ func (s *SnapOpSuite) TestInstallManyChannel(c *check.C) { s.RedirectClientToTestServer(nil) - _, err := snap.Parser().ParseArgs([]string{"install", "--beta", "one", "two"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--beta", "one", "two"}) c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`) } func (s *SnapOpSuite) TestInstallManyMixFileAndStore(c *check.C) { s.RedirectClientToTestServer(nil) - _, err := snap.Parser().ParseArgs([]string{"install", "store-snap", "./local.snap"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "store-snap", "./local.snap"}) c.Assert(err, check.ErrorMatches, `only one snap file can be installed at a time`) } @@ -1038,7 +1535,7 @@ case 3: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/snaps") - fmt.Fprintf(w, `{"type": "sync", "result": [{"name": "one", "status": "active", "version": "1.0", "developer": "bar", "revision":42, "channel":"stable"},{"name": "two", "status": "active", "version": "2.0", "developer": "baz", "revision":42, "channel":"edge"}]}\n`) + fmt.Fprintf(w, `{"type": "sync", "result": [{"name": "one", "status": "active", "version": "1.0", "developer": "bar", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":42, "channel":"stable"},{"name": "two", "status": "active", "version": "2.0", "developer": "baz", "publisher": {"id": "baz-id", "username": "baz", "display-name": "Baz", "validation": "unproven"}, "revision":42, "channel":"edge"}]}\n`) default: c.Fatalf("expected to get %d requests, now on %d", total, n+1) @@ -1047,17 +1544,26 @@ n++ }) - rest, err := snap.Parser().ParseArgs([]string{"install", "one", "two"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "one", "two"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) // note that (stable) is omitted - c.Check(s.Stdout(), check.Matches, `(?sm).*one 1.0 from 'bar' installed`) - c.Check(s.Stdout(), check.Matches, `(?sm).*two \(edge\) 2.0 from 'baz' installed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*one 1.0 from Bar installed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*two \(edge\) 2.0 from Baz installed`) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit c.Check(n, check.Equals, total) } +func (s *SnapOpSuite) TestInstallZeroEmpty(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install"}) + c.Assert(err, check.ErrorMatches, "cannot install zero snaps") + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"install", ""}) + c.Assert(err, check.ErrorMatches, "cannot install snap with empty name") + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"install", "", "bar"}) + c.Assert(err, check.ErrorMatches, "cannot install snap with empty name") +} + func (s *SnapOpSuite) TestNoWait(c *check.C) { s.srv.checker = func(r *http.Request) {} @@ -1069,14 +1575,26 @@ {"revert", "--no-wait", "foo"}, {"refresh", "--no-wait", "foo"}, {"refresh", "--no-wait", "foo", "bar"}, + {"refresh", "--no-wait"}, {"enable", "--no-wait", "foo"}, {"disable", "--no-wait", "foo"}, {"try", "--no-wait", "."}, + {"switch", "--no-wait", "--channel=foo", "bar"}, + // commands that use waitMixin from elsewhere + {"start", "--no-wait", "foo"}, + {"stop", "--no-wait", "foo"}, + {"restart", "--no-wait", "foo"}, + {"alias", "--no-wait", "foo", "bar"}, + {"unalias", "--no-wait", "foo"}, + {"prefer", "--no-wait", "foo"}, + {"set", "--no-wait", "foo", "bar=baz"}, + {"disconnect", "--no-wait", "foo:bar"}, + {"connect", "--no-wait", "foo:bar"}, } s.RedirectClientToTestServer(s.srv.handle) for _, cmd := range cmds { - rest, err := snap.Parser().ParseArgs(cmd) + rest, err := snap.Parser(snap.Client()).ParseArgs(cmd) c.Assert(err, check.IsNil, check.Commentf("%v", cmd)) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, "(?sm)42\n") @@ -1088,6 +1606,95 @@ } } +func (s *SnapOpSuite) TestNoWaitImmediateError(c *check.C) { + + cmds := [][]string{ + {"remove", "--no-wait", "foo"}, + {"remove", "--no-wait", "foo", "bar"}, + {"install", "--no-wait", "foo"}, + {"install", "--no-wait", "foo", "bar"}, + {"revert", "--no-wait", "foo"}, + {"refresh", "--no-wait", "foo"}, + {"refresh", "--no-wait", "foo", "bar"}, + {"refresh", "--no-wait"}, + {"enable", "--no-wait", "foo"}, + {"disable", "--no-wait", "foo"}, + {"try", "--no-wait", "."}, + {"switch", "--no-wait", "--channel=foo", "bar"}, + // commands that use waitMixin from elsewhere + {"start", "--no-wait", "foo"}, + {"stop", "--no-wait", "foo"}, + {"restart", "--no-wait", "foo"}, + {"alias", "--no-wait", "foo", "bar"}, + {"unalias", "--no-wait", "foo"}, + {"prefer", "--no-wait", "foo"}, + {"set", "--no-wait", "foo", "bar=baz"}, + {"disconnect", "--no-wait", "foo:bar"}, + {"connect", "--no-wait", "foo:bar"}, + } + + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "failure"}}`) + }) + + for _, cmd := range cmds { + _, err := snap.Parser(snap.Client()).ParseArgs(cmd) + c.Assert(err, check.ErrorMatches, "failure", check.Commentf("%v", cmd)) + } +} + +func (s *SnapOpSuite) TestWaitServerError(c *check.C) { + r := snap.MockMaxGoneTime(0) + defer r() + + cmds := [][]string{ + {"remove", "foo"}, + {"remove", "foo", "bar"}, + {"install", "foo"}, + {"install", "foo", "bar"}, + {"revert", "foo"}, + {"refresh", "foo"}, + {"refresh", "foo", "bar"}, + {"refresh"}, + {"enable", "foo"}, + {"disable", "foo"}, + {"try", "."}, + {"switch", "--channel=foo", "bar"}, + // commands that use waitMixin from elsewhere + {"start", "foo"}, + {"stop", "foo"}, + {"restart", "foo"}, + {"alias", "foo", "bar"}, + {"unalias", "foo"}, + {"prefer", "foo"}, + {"set", "foo", "bar=baz"}, + {"disconnect", "foo:bar"}, + {"connect", "foo:bar"}, + } + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + if n == 1 { + w.WriteHeader(202) + fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`) + return + } + if n == 3 { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "unexpected request"}}`) + return + } + fmt.Fprintln(w, `{"type": "error", "result": {"message": "server error"}}`) + }) + + for _, cmd := range cmds { + _, err := snap.Parser(snap.Client()).ParseArgs(cmd) + c.Assert(err, check.ErrorMatches, "server error", check.Commentf("%v", cmd)) + // reset + n = 0 + } +} + func (s *SnapOpSuite) TestSwitchHappy(c *check.C) { s.srv.total = 3 s.srv.checker = func(r *http.Request) { @@ -1099,7 +1706,7 @@ } s.RedirectClientToTestServer(s.srv.handle) - rest, err := snap.Parser().ParseArgs([]string{"switch", "--beta", "foo"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "--beta", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*"foo" switched to the "beta" channel`) @@ -1109,12 +1716,12 @@ } func (s *SnapOpSuite) TestSwitchUnhappy(c *check.C) { - _, err := snap.Parser().ParseArgs([]string{"switch"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch"}) c.Assert(err, check.ErrorMatches, "the required argument `` was not provided") } func (s *SnapOpSuite) TestSwitchAlsoUnhappy(c *check.C) { - _, err := snap.Parser().ParseArgs([]string{"switch", "foo"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "foo"}) c.Assert(err, check.ErrorMatches, `missing --channel= parameter`) } @@ -1136,6 +1743,6 @@ }) cmd := []string{"install", "hello"} - _, err := snap.Parser().ParseArgs(cmd) + _, err := snap.Parser(snap.Client()).ParseArgs(cmd) c.Assert(err, check.ErrorMatches, `unable to contact snap store`) } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_snapshot.go snapd-2.37~rc1~14.04/cmd/snap/cmd_snapshot.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_snapshot.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_snapshot.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,331 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "fmt" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/strutil" + "github.com/snapcore/snapd/strutil/quantity" +) + +func fmtSize(size int64) string { + return quantity.FormatAmount(uint64(size), -1) +} + +var ( + shortSavedHelp = i18n.G("List currently stored snapshots") + shortSaveHelp = i18n.G("Save a snapshot of the current data") + shortForgetHelp = i18n.G("Delete a snapshot") + shortCheckHelp = i18n.G("Check a snapshot") + shortRestoreHelp = i18n.G("Restore a snapshot") +) + +var longSavedHelp = i18n.G(` +The saved command displays a list of snapshots that have been created +previously with the 'save' command. +`) +var longSaveHelp = i18n.G(` +The save command creates a snapshot of the current user, system and +configuration data for the given snaps. + +By default, this command saves the data of all snaps for all users. +Alternatively, you can specify the data of which snaps to save, or +for which users, or a combination of these. + +If a snap is included in a save operation, excluding its system and +configuration data from the snapshot is not currently possible. This +restriction may be lifted in the future. +`) +var longForgetHelp = i18n.G(` +The forget command deletes a snapshot. This operation can not be +undone. + +A snapshot contains archives for the user, system and configuration +data of each snap included in the snapshot. + +By default, this command forgets all the data in a snapshot. +Alternatively, you can specify the data of which snaps to forget. +`) +var longCheckHelp = i18n.G(` +The check-snapshot command verifies the user, system and configuration +data of the snaps included in the specified snapshot. + +The check operation runs the same data integrity verification that is +performed when a snapshot is restored. + +By default, this command checks all the data in a snapshot. +Alternatively, you can specify the data of which snaps to check, or +for which users, or a combination of these. + +If a snap is included in a check-snapshot operation, excluding its +system and configuration data from the check is not currently +possible. This restriction may be lifted in the future. +`) +var longRestoreHelp = i18n.G(` +The restore command replaces the current user, system and +configuration data of included snaps, with the corresponding data from +the specified snapshot. + +By default, this command restores all the data in a snapshot. +Alternatively, you can specify the data of which snaps to restore, or +for which users, or a combination of these. + +If a snap is included in a restore operation, excluding its system and +configuration data from the restore is not currently possible. This +restriction may be lifted in the future. +`) + +type savedCmd struct { + clientMixin + durationMixin + ID snapshotID `long:"id"` + Positional struct { + Snaps []installedSnapName `positional-arg-name:""` + } `positional-args:"yes"` +} + +func (x *savedCmd) Execute([]string) error { + setID := uint64(x.ID) + snaps := installedSnapNames(x.Positional.Snaps) + list, err := x.client.SnapshotSets(setID, snaps) + if err != nil { + return err + } + if len(list) == 0 { + fmt.Fprintln(Stdout, i18n.G("No snapshots found.")) + return nil + } + w := tabWriter() + defer w.Flush() + + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + // TRANSLATORS: 'Set' as in group or bag of things + i18n.G("Set"), + "Snap", + // TRANSLATORS: 'Age' as in how old something is + i18n.G("Age"), + i18n.G("Version"), + // TRANSLATORS: 'Rev' is an abbreviation of 'Revision' + i18n.G("Rev"), + i18n.G("Size"), + // TRANSLATORS: 'Notes' as in 'Comments' + i18n.G("Notes")) + for _, sg := range list { + for _, sh := range sg.Snapshots { + note := "-" + if sh.Broken != "" { + note = "broken: " + sh.Broken + } + size := quantity.FormatAmount(uint64(sh.Size), -1) + "B" + fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\t%s\n", sg.ID, sh.Snap, x.fmtDuration(sh.Time), sh.Version, sh.Revision, size, note) + } + } + return nil +} + +type saveCmd struct { + waitMixin + durationMixin + Users string `long:"users"` + Positional struct { + Snaps []installedSnapName `positional-arg-name:""` + } `positional-args:"yes"` +} + +func (x *saveCmd) Execute([]string) error { + snaps := installedSnapNames(x.Positional.Snaps) + users := strutil.CommaSeparatedList(x.Users) + setID, changeID, err := x.client.SnapshotMany(snaps, users) + if err != nil { + return err + } + if _, err := x.wait(changeID); err != nil { + if err == noWait { + return nil + } + return err + } + + y := &savedCmd{ + clientMixin: x.clientMixin, + durationMixin: x.durationMixin, + ID: snapshotID(setID), + } + return y.Execute(nil) +} + +type forgetCmd struct { + waitMixin + Positional struct { + ID snapshotID `positional-arg-name:""` + Snaps []installedSnapName `positional-arg-name:""` + } `positional-args:"yes" required:"yes"` +} + +func (x *forgetCmd) Execute([]string) error { + setID := uint64(x.Positional.ID) + snaps := installedSnapNames(x.Positional.Snaps) + changeID, err := x.client.ForgetSnapshots(setID, snaps) + if err != nil { + return err + } + _, err = x.wait(changeID) + if err == noWait { + return nil + } + if err != nil { + return err + } + + if len(snaps) > 0 { + // TRANSLATORS: the %s is a comma-separated list of quoted snap names + fmt.Fprintf(Stdout, i18n.NG("Snapshot #%d of snap %s forgotten.\n", "Snapshot #%d of snaps %s forgotten.\n", len(snaps)), x.Positional.ID, strutil.Quoted(snaps)) + } else { + fmt.Fprintf(Stdout, i18n.G("Snapshot #%d forgotten.\n"), x.Positional.ID) + } + return nil +} + +type checkSnapshotCmd struct { + waitMixin + Users string `long:"users"` + Positional struct { + ID snapshotID `positional-arg-name:""` + Snaps []installedSnapName `positional-arg-name:""` + } `positional-args:"yes" required:"yes"` +} + +func (x *checkSnapshotCmd) Execute([]string) error { + setID := uint64(x.Positional.ID) + snaps := installedSnapNames(x.Positional.Snaps) + users := strutil.CommaSeparatedList(x.Users) + changeID, err := x.client.CheckSnapshots(setID, snaps, users) + if err != nil { + return err + } + _, err = x.wait(changeID) + if err == noWait { + return nil + } + if err != nil { + return err + } + + // TODO: also mention the home archives that were actually checked + if len(snaps) > 0 { + // TRANSLATORS: the %s is a comma-separated list of quoted snap names + fmt.Fprintf(Stdout, i18n.G("Snapshot #%d of snaps %s verified successfully.\n"), + x.Positional.ID, strutil.Quoted(snaps)) + } else { + fmt.Fprintf(Stdout, i18n.G("Snapshot #%d verified successfully.\n"), x.Positional.ID) + } + return nil +} + +type restoreCmd struct { + waitMixin + Users string `long:"users"` + Positional struct { + ID snapshotID `positional-arg-name:""` + Snaps []installedSnapName `positional-arg-name:""` + } `positional-args:"yes" required:"yes"` +} + +func (x *restoreCmd) Execute([]string) error { + setID := uint64(x.Positional.ID) + snaps := installedSnapNames(x.Positional.Snaps) + users := strutil.CommaSeparatedList(x.Users) + changeID, err := x.client.RestoreSnapshots(setID, snaps, users) + if err != nil { + return err + } + _, err = x.wait(changeID) + if err == noWait { + return nil + } + if err != nil { + return err + } + + // TODO: also mention the home archives that were actually restored + if len(snaps) > 0 { + // TRANSLATORS: the %s is a comma-separated list of quoted snap names + fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%d of snaps %s.\n"), + x.Positional.ID, strutil.Quoted(snaps)) + } else { + fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%d.\n"), x.Positional.ID) + } + return nil +} + +func init() { + addCommand("saved", + shortSavedHelp, + longSavedHelp, + func() flags.Commander { + return &savedCmd{} + }, + durationDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "id": i18n.G("Show only a specific snapshot."), + }), + nil) + + addCommand("save", + shortSaveHelp, + longSaveHelp, + func() flags.Commander { + return &saveCmd{} + }, durationDescs.also(waitDescs).also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "users": i18n.G("Snapshot data of only specific users (comma-separated) (default: all users)"), + }), nil) + + addCommand("restore", + shortRestoreHelp, + longRestoreHelp, + func() flags.Commander { + return &restoreCmd{} + }, waitDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "users": i18n.G("Restore data of only specific users (comma-separated) (default: all users)"), + }), nil) + + addCommand("forget", + shortForgetHelp, + longForgetHelp, + func() flags.Commander { + return &forgetCmd{} + }, waitDescs, nil) + + addCommand("check-snapshot", + shortCheckHelp, + longCheckHelp, + func() flags.Commander { + return &checkSnapshotCmd{} + }, waitDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "users": i18n.G("Check data of only specific users (comma-separated) (default: all users)"), + }), nil) +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_unalias.go snapd-2.37~rc1~14.04/cmd/snap/cmd_unalias.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_unalias.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_unalias.go 2019-01-16 08:36:51.000000000 +0000 @@ -26,21 +26,24 @@ ) type cmdUnalias struct { + waitMixin Positionals struct { AliasOrSnap aliasOrSnap `required:"yes"` } `positional-args:"true"` } -var shortUnaliasHelp = i18n.G("Unalias a manual alias or an entire snap") +var shortUnaliasHelp = i18n.G("Remove a manual alias, or the aliases for an entire snap") var longUnaliasHelp = i18n.G(` -The unalias command tears down a manual alias when given one or disables all aliases of a snap, removing also all manual ones, when given a snap name. +The unalias command removes a single alias if the provided argument is a manual +alias, or disables all aliases of a snap, including manual ones, if the +argument is a snap name. `) func init() { addCommand("unalias", shortUnaliasHelp, longUnaliasHelp, func() flags.Commander { return &cmdUnalias{} - }, nil, []argDesc{ - // TRANSLATORS: This needs to be wrapped in <>s. + }, waitDescs.also(nil), []argDesc{ + // TRANSLATORS: This needs to begin with < and end with > {name: i18n.G("")}, }) } @@ -50,19 +53,17 @@ return ErrExtraArgs } - cli := Client() - id, err := cli.Unalias(string(x.Positionals.AliasOrSnap)) + id, err := x.client.Unalias(string(x.Positionals.AliasOrSnap)) if err != nil { return err } - - chg, err := wait(cli, id) + chg, err := x.wait(id) if err != nil { - return err - } - if err := showAliasChanges(chg); err != nil { + if err == noWait { + return nil + } return err } - return nil + return showAliasChanges(chg) } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_unalias_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_unalias_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_unalias_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_unalias_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -30,20 +30,17 @@ func (s *SnapSuite) TestUnaliasHelp(c *C) { msg := `Usage: - snap.test [OPTIONS] unalias [] + snap.test unalias [unalias-OPTIONS] [] -The unalias command tears down a manual alias when given one or disables all -aliases of a snap, removing also all manual ones, when given a snap name. +The unalias command removes a single alias if the provided argument is a manual +alias, or disables all aliases of a snap, including manual ones, if the +argument is a snap name. -Application Options: - --version Print the version and exit - -Help Options: - -h, --help Show this help message +[unalias command options] + --no-wait Do not wait for the operation to finish but just + print the change id. ` - rest, err := Parser().ParseArgs([]string{"unalias", "--help"}) - c.Assert(err.Error(), Equals, msg) - c.Assert(rest, DeepEquals, []string{}) + s.testSubCommandHelp(c, "unalias", msg) } func (s *SnapSuite) TestUnalias(c *C) { @@ -64,7 +61,7 @@ c.Fatalf("unexpected path %q", r.URL.Path) } }) - rest, err := Parser().ParseArgs([]string{"unalias", "alias1"}) + rest, err := Parser(Client()).ParseArgs([]string{"unalias", "alias1"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Assert(s.Stdout(), Equals, ""+ diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_userd.go snapd-2.37~rc1~14.04/cmd/snap/cmd_userd.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_userd.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_userd.go 2019-01-16 08:36:51.000000000 +0000 @@ -33,10 +33,14 @@ type cmdUserd struct { userd userd.Userd + + Autostart bool `long:"autostart"` } var shortUserdHelp = i18n.G("Start the userd service") -var longUserdHelp = i18n.G("The userd command starts the snap user session service.") +var longUserdHelp = i18n.G(` +The userd command starts the snap user session service. +`) func init() { cmd := addCommand("userd", @@ -44,10 +48,10 @@ longUserdHelp, func() flags.Commander { return &cmdUserd{} - }, - nil, - []argDesc{}, - ) + }, map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "autostart": i18n.G("Autostart user applications"), + }, nil) cmd.hidden = true } @@ -56,12 +60,16 @@ return ErrExtraArgs } + if x.Autostart { + return x.runAutostart() + } + if err := x.userd.Init(); err != nil { return err } x.userd.Start() - ch := make(chan os.Signal) + ch := make(chan os.Signal, 3) signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1) select { case sig := <-ch: @@ -72,3 +80,10 @@ return x.userd.Stop() } + +func (x *cmdUserd) runAutostart() error { + if err := userd.AutostartSessionApps(); err != nil { + return fmt.Errorf("autostart failed for the following apps:\n%v", err) + } + return nil +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_userd_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_userd_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_userd_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_userd_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -21,6 +21,7 @@ import ( "os" + "strings" "syscall" "time" @@ -55,33 +56,47 @@ } func (s *userdSuite) TestUserdBadCommandline(c *C) { - _, err := snap.Parser().ParseArgs([]string{"userd", "extra-arg"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"userd", "extra-arg"}) c.Assert(err, ErrorMatches, "too many arguments for command") } -func (s *userdSuite) TestUserd(c *C) { +func (s *userdSuite) TestUserdDBus(c *C) { go func() { + myPid := os.Getpid() defer func() { - me, err := os.FindProcess(os.Getpid()) + me, err := os.FindProcess(myPid) c.Assert(err, IsNil) me.Signal(syscall.SIGUSR1) }() - needle := "io.snapcraft.Launcher" - for i := 0; i < 10; i++ { - for _, objName := range s.SessionBus.Names() { - if objName == needle { - return + names := map[string]bool{ + "io.snapcraft.Launcher": false, + "io.snapcraft.Settings": false, + } + for i := 0; i < 1000; i++ { + seenCount := 0 + for name, seen := range names { + if seen { + seenCount++ + continue + } + pid, err := testutil.DBusGetConnectionUnixProcessID(s.SessionBus, name) + c.Logf("name: %v pid: %v err: %v", name, pid, err) + if pid == myPid { + names[name] = true + seenCount++ } - time.Sleep(1 * time.Second) } - + if seenCount == len(names) { + return + } + time.Sleep(10 * time.Millisecond) } - c.Fatalf("%s does not appeared on the bus", needle) + c.Fatalf("not all names have appeared on the bus: %v", names) }() - rest, err := snap.Parser().ParseArgs([]string{"userd"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"userd"}) c.Assert(err, IsNil) c.Check(rest, DeepEquals, []string{}) - c.Check(s.Stdout(), Equals, "Exiting on user defined signal 1.\n") + c.Check(strings.ToLower(s.Stdout()), Equals, "exiting on user defined signal 1.\n") } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_version.go snapd-2.37~rc1~14.04/cmd/snap/cmd_version.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_version.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_version.go 2019-01-16 08:36:51.000000000 +0000 @@ -22,20 +22,22 @@ import ( "fmt" + "github.com/jessevdk/go-flags" + "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/cmd" "github.com/snapcore/snapd/i18n" - - "github.com/jessevdk/go-flags" ) -var shortVersionHelp = i18n.G("Shows version details") +var shortVersionHelp = i18n.G("Show version details") var longVersionHelp = i18n.G(` The version command displays the versions of the running client, server, and operating system. `) -type cmdVersion struct{} +type cmdVersion struct { + clientMixin +} func init() { addCommand("version", shortVersionHelp, longVersionHelp, func() flags.Commander { return &cmdVersion{} }, nil, nil) @@ -46,27 +48,21 @@ return ErrExtraArgs } - printVersions() + printVersions(cmd.client) return nil } -func printVersions() error { - sv, err := Client().ServerVersion() - if err != nil { - sv = &client.ServerVersion{ - Version: i18n.G("unavailable"), - Series: "-", - OSID: "-", - OSVersionID: "-", - } - } - +func printVersions(cli *client.Client) error { + sv := serverVersion(cli) w := tabWriter() fmt.Fprintf(w, "snap\t%s\n", cmd.Version) fmt.Fprintf(w, "snapd\t%s\n", sv.Version) fmt.Fprintf(w, "series\t%s\n", sv.Series) if sv.OnClassic { + if sv.OSVersionID == "" { + sv.OSVersionID = "-" + } fmt.Fprintf(w, "%s\t%s\n", sv.OSID, sv.OSVersionID) } if sv.KernelVersion != "" { @@ -74,5 +70,5 @@ } w.Flush() - return err + return nil } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_version_linux.go snapd-2.37~rc1~14.04/cmd/snap/cmd_version_linux.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_version_linux.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_version_linux.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,53 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "fmt" + "runtime" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" +) + +func serverVersion(cli *client.Client) *client.ServerVersion { + if release.OnWSL { + return &client.ServerVersion{ + Version: i18n.G("unavailable"), + Series: release.Series, + OSID: "Windows Subsystem for Linux", + OnClassic: true, + KernelVersion: fmt.Sprintf("%s (%s)", osutil.KernelVersion(), runtime.GOARCH), + } + } + sv, err := cli.ServerVersion() + + if err != nil { + sv = &client.ServerVersion{ + Version: i18n.G("unavailable"), + Series: "-", + OSID: "-", + OSVersionID: "-", + } + } + return sv +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_version_other.go snapd-2.37~rc1~14.04/cmd/snap/cmd_version_other.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_version_other.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_version_other.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,41 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +build !linux + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "fmt" + "runtime" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" +) + +func serverVersion(*client.Client) *client.ServerVersion { + return &client.ServerVersion{ + Version: i18n.G("unavailable"), + Series: release.Series, + OSID: runtime.GOOS, + OnClassic: true, + KernelVersion: fmt.Sprintf("%s (%s)", osutil.KernelVersion(), runtime.GOARCH), + } +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_version_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_version_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_version_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_version_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -37,7 +37,7 @@ restore = mockVersion("4.56") defer restore() - _, err := snap.Parser().ParseArgs([]string{"version"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"version"}) c.Assert(err, IsNil) c.Assert(s.Stdout(), Equals, "snap 4.56\nsnapd 7.89\nseries 56\nubuntu 12.34\n") c.Assert(s.Stderr(), Equals, "") @@ -52,8 +52,23 @@ restore = mockVersion("4.56") defer restore() - _, err := snap.Parser().ParseArgs([]string{"version"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"version"}) c.Assert(err, IsNil) c.Assert(s.Stdout(), Equals, "snap 4.56\nsnapd 7.89\nseries 56\n") c.Assert(s.Stderr(), Equals, "") } + +func (s *SnapSuite) TestVersionCommandOnClassicNoOsVersion(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"on-classic": true,"os-release":{"id":"arch","version-id":""},"series":"56","version":"7.89"}}`) + }) + restore := mockArgs("snap", "version") + defer restore() + restore = mockVersion("4.56") + defer restore() + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"version"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, "snap 4.56\nsnapd 7.89\nseries 56\narch -\n") + c.Assert(s.Stderr(), Equals, "") +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_wait.go snapd-2.37~rc1~14.04/cmd/snap/cmd_wait.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_wait.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_wait.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,155 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "encoding/json" + "fmt" + "math/rand" + "reflect" + "time" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" +) + +type cmdWait struct { + clientMixin + Positional struct { + Snap installedSnapName `required:"yes"` + Key string + } `positional-args:"yes"` +} + +func init() { + addCommand("wait", + "Wait for configuration", + "The wait command waits until a configration becomes true.", + func() flags.Commander { + return &cmdWait{} + }, nil, []argDesc{ + { + name: "", + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("The snap for which configuration will be checked"), + }, { + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("Key of interest within the configuration"), + }, + }) +} + +var waitConfTimeout = 500 * time.Millisecond + +func isNoOption(err error) bool { + if e, ok := err.(*client.Error); ok && e.Kind == client.ErrorKindConfigNoSuchOption { + return true + } + return false +} + +// trueishJSON takes an interface{} and returns true if the interface value +// looks "true". For strings thats if len(string) > 0 for numbers that +// they are != 0 and for maps/slices/arrays that they have elements. +// +// Note that *only* types that the json package decode with the +// "UseNumber()" options turned on are handled here. If this ever +// needs to becomes a generic "trueish" helper we need to resurrect +// the code in 306ba60edfba8d6501060c6f773235d8c994a319 (and add nil +// to it). +func trueishJSON(vi interface{}) (bool, error) { + switch v := vi.(type) { + // limited to the types that json unmarhal can produce + case nil: + return false, nil + case bool: + return v, nil + case json.Number: + if i, err := v.Int64(); err == nil { + return i != 0, nil + } + if f, err := v.Float64(); err == nil { + return f != 0.0, nil + } + case string: + return v != "", nil + } + // arrays/slices/maps + typ := reflect.TypeOf(vi) + switch typ.Kind() { + case reflect.Array, reflect.Slice, reflect.Map: + s := reflect.ValueOf(vi) + switch s.Kind() { + case reflect.Array, reflect.Slice, reflect.Map: + return s.Len() > 0, nil + } + } + + return false, fmt.Errorf("cannot test type %T for truth", vi) +} + +func (x *cmdWait) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + snapName := string(x.Positional.Snap) + confKey := x.Positional.Key + + // This is fine because not providing a confKey is unsupported so this + // won't interfere with supported uses of `snap wait`. + if snapName == "godot" && confKey == "" { + switch rand.Intn(10) { + case 0: + fmt.Fprintln(Stdout, `The tears of the world are a constant quantity. +For each one who begins to weep somewhere else another stops. +The same is true of the laugh.`) + case 1: + fmt.Fprintln(Stdout, "Nothing happens. Nobody comes, nobody goes. It's awful.") + default: + fmt.Fprintln(Stdout, `"Let's go." "We can't." "Why not?" "We're waiting for Godot."`) + } + return nil + } + if confKey == "" { + return fmt.Errorf("the required argument `` was not provided") + } + + for { + conf, err := x.client.Conf(snapName, []string{confKey}) + if err != nil && !isNoOption(err) { + return err + } + res, err := trueishJSON(conf[confKey]) + if err != nil { + return err + } + if res { + break + } + time.Sleep(waitConfTimeout) + } + + return nil +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_wait_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_wait_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_wait_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_wait_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,174 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main_test + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestCmdWaitHappy(c *C) { + restore := snap.MockWaitConfTimeout(10 * time.Millisecond) + defer restore() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/snaps/system/conf") + + fmt.Fprintln(w, fmt.Sprintf(`{"type":"sync", "status-code": 200, "result": {"seed.loaded":%v}}`, n > 1)) + n++ + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"wait", "system", "seed.loaded"}) + c.Assert(err, IsNil) + + // ensure we retried a bit but make the check not overly precise + // because this will run in super busy build hosts that where a + // 10 millisecond sleep actually takes much longer until the kernel + // hands control back to the process + c.Check(n > 2, Equals, true) +} + +func (s *SnapSuite) TestCmdWaitMissingConfKey(c *C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"wait", "snapName"}) + c.Assert(err, ErrorMatches, "the required argument `` was not provided") + + c.Check(n, Equals, 0) +} + +func (s *SnapSuite) TestTrueishJSON(c *C) { + tests := []struct { + v interface{} + b bool + errStr string + }{ + // nil + {nil, false, ""}, + // bool + {true, true, ""}, + {false, false, ""}, + // string + {"a", true, ""}, + {"", false, ""}, + // json.Number + {json.Number("1"), true, ""}, + {json.Number("-1"), true, ""}, + {json.Number("0"), false, ""}, + {json.Number("1.0"), true, ""}, + {json.Number("-1.0"), true, ""}, + {json.Number("0.0"), false, ""}, + // slices + {[]interface{}{"a"}, true, ""}, + {[]interface{}{}, false, ""}, + {[]string{"a"}, true, ""}, + {[]string{}, false, ""}, + // arrays + {[2]interface{}{"a", "b"}, true, ""}, + {[0]interface{}{}, false, ""}, + {[2]string{"a", "b"}, true, ""}, + {[0]string{}, false, ""}, + // maps + {map[string]interface{}{"a": "a"}, true, ""}, + {map[string]interface{}{}, false, ""}, + {map[interface{}]interface{}{"a": "a"}, true, ""}, + {map[interface{}]interface{}{}, false, ""}, + // invalid + {int(1), false, "cannot test type int for truth"}, + } + for _, t := range tests { + res, err := snap.TrueishJSON(t.v) + if t.errStr == "" { + c.Check(err, IsNil) + } else { + c.Check(err, ErrorMatches, t.errStr) + } + c.Check(res, Equals, t.b, Commentf("unexpected result for %v (%T), did not get expected %v", t.v, t.v, t.b)) + } +} + +func (s *SnapSuite) TestCmdWaitIntegration(c *C) { + restore := snap.MockWaitConfTimeout(2 * time.Millisecond) + defer restore() + + var tests = []struct { + v string + willWait bool + }{ + // not-waiting + {"1.0", false}, + {"-1.0", false}, + {"0.1", false}, + {"-0.1", false}, + {"1", false}, + {"-1", false}, + {`"a"`, false}, + {`["a"]`, false}, + {`{"a":"b"}`, false}, + // waiting + {"0", true}, + {"0.0", true}, + {"{}", true}, + {"[]", true}, + {`""`, true}, + {"null", true}, + } + + testValueCh := make(chan string, 2) + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + testValue := <-testValueCh + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/snaps/system/conf") + + fmt.Fprintln(w, fmt.Sprintf(`{"type":"sync", "status-code": 200, "result": {"test.value":%v}}`, testValue)) + n++ + }) + + for _, t := range tests { + n = 0 + testValueCh <- t.v + if t.willWait { + // a "trueish" value to ensure wait does not wait forever + testValueCh <- "42" + } + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"wait", "system", "test.value"}) + c.Assert(err, IsNil) + if t.willWait { + // we waited once, then got a non-wait value + c.Check(n, Equals, 2) + } else { + // no waiting happened + c.Check(n, Equals, 1) + } + } +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_warnings.go snapd-2.37~rc1~14.04/cmd/snap/cmd_warnings.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_warnings.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_warnings.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,224 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/strutil/quantity" +) + +type cmdWarnings struct { + clientMixin + timeMixin + All bool `long:"all"` + Verbose bool `long:"verbose"` +} + +type cmdOkay struct{ clientMixin } + +var shortWarningsHelp = i18n.G("List warnings") +var longWarningsHelp = i18n.G(` +The warnings command lists the warnings that have been reported to the system. + +Once warnings have been listed with 'snap warnings', 'snap okay' may be used to +silence them. A warning that's been silenced in this way will not be listed +again unless it happens again, _and_ a cooldown time has passed. + +Warnings expire automatically, and once expired they are forgotten. +`) + +var shortOkayHelp = i18n.G("Acknowledge warnings") +var longOkayHelp = i18n.G(` +The okay command acknowledges the warnings listed with 'snap warnings'. + +Once acknowledged a warning won't appear again unless it re-occurrs and +sufficient time has passed. +`) + +func init() { + addCommand("warnings", shortWarningsHelp, longWarningsHelp, func() flags.Commander { return &cmdWarnings{} }, timeDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "all": i18n.G("Show all warnings"), + // TRANSLATORS: This should not start with a lowercase letter. + "verbose": i18n.G("Show more information"), + }), nil) + addCommand("okay", shortOkayHelp, longOkayHelp, func() flags.Commander { return &cmdOkay{} }, nil, nil) +} + +func (cmd *cmdWarnings) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + now := time.Now() + + warnings, err := cmd.client.Warnings(client.WarningsOptions{All: cmd.All}) + if err != nil { + return err + } + if len(warnings) == 0 { + if t, _ := lastWarningTimestamp(); t.IsZero() { + fmt.Fprintln(Stdout, i18n.G("No warnings.")) + } else { + fmt.Fprintln(Stdout, i18n.G("No further warnings.")) + } + return nil + } + + if err := writeWarningTimestamp(now); err != nil { + return err + } + + w := tabWriter() + if cmd.Verbose { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", + i18n.G("First occurrence"), + i18n.G("Last occurrence"), + i18n.G("Expires after"), + i18n.G("Acknowledged"), + i18n.G("Repeats after"), + i18n.G("Warning")) + for _, warning := range warnings { + lastShown := "-" + if !warning.LastShown.IsZero() { + lastShown = cmd.fmtTime(warning.LastShown) + } + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", + cmd.fmtTime(warning.FirstAdded), + cmd.fmtTime(warning.LastAdded), + quantity.FormatDuration(warning.ExpireAfter.Seconds()), + lastShown, + quantity.FormatDuration(warning.RepeatAfter.Seconds()), + warning.Message) + } + } else { + fmt.Fprintf(w, "%s\t%s\n", i18n.G("Last occurrence"), i18n.G("Warning")) + for _, warning := range warnings { + fmt.Fprintf(w, "%s\t%s\n", cmd.fmtTime(warning.LastAdded), warning.Message) + } + } + w.Flush() + + return nil +} + +func (cmd *cmdOkay) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + last, err := lastWarningTimestamp() + if err != nil { + return fmt.Errorf("no client-side warning timestamp found: %v", err) + } + + return cmd.client.Okay(last) +} + +const warnFileEnvKey = "SNAPD_LAST_WARNING_TIMESTAMP_FILENAME" + +func warnFilename(homedir string) string { + if fn := os.Getenv(warnFileEnvKey); fn != "" { + return fn + } + + return filepath.Join(dirs.GlobalRootDir, homedir, ".snap", "warnings.json") +} + +type clientWarningData struct { + Timestamp time.Time `json:"timestamp"` +} + +func writeWarningTimestamp(t time.Time) error { + user, err := osutil.RealUser() + if err != nil { + return err + } + uid, gid, err := osutil.UidGid(user) + if err != nil { + return err + } + + filename := warnFilename(user.HomeDir) + if err := osutil.MkdirAllChown(filepath.Dir(filename), 0700, uid, gid); err != nil { + return err + } + + aw, err := osutil.NewAtomicFile(filename, 0600, 0, uid, gid) + if err != nil { + return err + } + // Cancel once Committed is a NOP :-) + defer aw.Cancel() + + enc := json.NewEncoder(aw) + if err := enc.Encode(clientWarningData{Timestamp: t}); err != nil { + return err + } + + return aw.Commit() +} + +func lastWarningTimestamp() (time.Time, error) { + user, err := osutil.RealUser() + if err != nil { + return time.Time{}, fmt.Errorf("cannot determine real user: %v", err) + } + f, err := os.Open(warnFilename(user.HomeDir)) + if err != nil { + return time.Time{}, fmt.Errorf("cannot open timestamp file: %v", err) + + } + dec := json.NewDecoder(f) + var d clientWarningData + if err := dec.Decode(&d); err != nil { + return time.Time{}, fmt.Errorf("cannot decode timestamp file: %v", err) + } + if dec.More() { + return time.Time{}, fmt.Errorf("spurious extra data in timestamp file") + } + return d.Timestamp, nil +} + +func maybePresentWarnings(count int, timestamp time.Time) { + if count == 0 { + return + } + + if last, _ := lastWarningTimestamp(); !timestamp.After(last) { + return + } + + fmt.Fprintf(Stderr, + i18n.NG("WARNING: There is %d new warning. See 'snap warnings'.\n", + "WARNING: There are %d new warnings. See 'snap warnings'.\n", + count), + count) +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_warnings_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_warnings_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_warnings_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_warnings_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,206 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "net/http" + "time" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +type warningSuite struct { + BaseSnapSuite +} + +var _ = check.Suite(&warningSuite{}) + +const twoWarnings = `{ + "result": [ + { + "expire-after": "672h0m0s", + "first-added": "2018-09-19T12:41:18.505007495Z", + "last-added": "2018-09-19T12:41:18.505007495Z", + "message": "hello world number one", + "repeat-after": "24h0m0s" + }, + { + "expire-after": "672h0m0s", + "first-added": "2018-09-19T12:44:19.680362867Z", + "last-added": "2018-09-19T12:44:19.680362867Z", + "message": "hello world number two", + "repeat-after": "24h0m0s" + } + ], + "status": "OK", + "status-code": 200, + "type": "sync" + }` + +func mkWarningsFakeHandler(c *check.C, body string) func(w http.ResponseWriter, r *http.Request) { + var called bool + return func(w http.ResponseWriter, r *http.Request) { + if called { + c.Fatalf("expected a single request") + } + called = true + c.Check(r.URL.Path, check.Equals, "/v2/warnings") + c.Check(r.URL.Query(), check.HasLen, 0) + + buf, err := ioutil.ReadAll(r.Body) + c.Assert(err, check.IsNil) + c.Check(string(buf), check.Equals, "") + c.Check(r.Method, check.Equals, "GET") + w.WriteHeader(200) + fmt.Fprintln(w, body) + } +} + +func (s *warningSuite) TestNoWarningsEver(c *check.C) { + s.RedirectClientToTestServer(mkWarningsFakeHandler(c, `{"type": "sync", "status-code": 200, "result": []}`)) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.HasLen, 0) + c.Check(s.Stderr(), check.Equals, "") + c.Check(s.Stdout(), check.Equals, "No warnings.\n") +} + +func (s *warningSuite) TestNoFurtherWarnings(c *check.C) { + snap.WriteWarningTimestamp(time.Now()) + + s.RedirectClientToTestServer(mkWarningsFakeHandler(c, `{"type": "sync", "status-code": 200, "result": []}`)) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.HasLen, 0) + c.Check(s.Stderr(), check.Equals, "") + c.Check(s.Stdout(), check.Equals, "No further warnings.\n") +} + +func (s *warningSuite) TestWarnings(c *check.C) { + s.RedirectClientToTestServer(mkWarningsFakeHandler(c, twoWarnings)) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.HasLen, 0) + c.Check(s.Stderr(), check.Equals, "") + c.Check(s.Stdout(), check.Equals, ` +Last occurrence Warning +2018-09-19T12:41:18Z hello world number one +2018-09-19T12:44:19Z hello world number two +`[1:]) +} + +func (s *warningSuite) TestVerboseWarnings(c *check.C) { + s.RedirectClientToTestServer(mkWarningsFakeHandler(c, twoWarnings)) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time", "--verbose"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.HasLen, 0) + c.Check(s.Stderr(), check.Equals, "") + c.Check(s.Stdout(), check.Equals, ` +First occurrence Last occurrence Expires after Acknowledged Repeats after Warning +2018-09-19T12:41:18Z 2018-09-19T12:41:18Z 28d0h - 1d00h hello world number one +2018-09-19T12:44:19Z 2018-09-19T12:44:19Z 28d0h - 1d00h hello world number two +`[1:]) +} + +func (s *warningSuite) TestOkay(c *check.C) { + t0 := time.Now() + snap.WriteWarningTimestamp(t0) + + var n int + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + if n != 1 { + c.Fatalf("expected 1 request, now on %d", n) + } + c.Check(r.URL.Path, check.Equals, "/v2/warnings") + c.Check(r.URL.Query(), check.HasLen, 0) + c.Assert(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{"action": "okay", "timestamp": t0.Format(time.RFC3339Nano)}) + c.Check(r.Method, check.Equals, "POST") + w.WriteHeader(200) + fmt.Fprintln(w, `{ + "status": "OK", + "status-code": 200, + "type": "sync" + }`) + }) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"okay"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.HasLen, 0) + c.Check(s.Stderr(), check.Equals, "") + c.Check(s.Stdout(), check.Equals, "") +} + +func (s *warningSuite) TestListWithWarnings(c *check.C) { + var called bool + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + if called { + c.Fatalf("expected a single request") + } + called = true + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + c.Check(r.URL.Query(), check.HasLen, 0) + + buf, err := ioutil.ReadAll(r.Body) + c.Assert(err, check.IsNil) + c.Check(string(buf), check.Equals, "") + c.Check(r.Method, check.Equals, "GET") + w.WriteHeader(200) + fmt.Fprintln(w, `{ + "result": [{}], + "status": "OK", + "status-code": 200, + "type": "sync", + "warning-count": 2, + "warning-timestamp": "2018-09-19T12:44:19.680362867Z" + }`) + }) + cli := snap.Client() + rest, err := snap.Parser(cli).ParseArgs([]string{"list"}) + c.Assert(err, check.IsNil) + + { + // TODO: I hope to get to refactor run() so we can + // call it from tests and not have to do this (whole + // block) by hand + + count, stamp := cli.WarningsSummary() + c.Check(count, check.Equals, 2) + c.Check(stamp, check.Equals, time.Date(2018, 9, 19, 12, 44, 19, 680362867, time.UTC)) + + snap.MaybePresentWarnings(count, stamp) + } + + c.Check(rest, check.HasLen, 0) + c.Check(s.Stdout(), check.Equals, ` +Name Version Rev Tracking Publisher Notes + unset - - disabled +`[1:]) + c.Check(s.Stderr(), check.Equals, "WARNING: There are 2 new warnings. See 'snap warnings'.\n") + +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_watch.go snapd-2.37~rc1~14.04/cmd/snap/cmd_watch.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_watch.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_watch.go 2019-01-16 08:36:51.000000000 +0000 @@ -43,12 +43,19 @@ if len(args) > 0 { return ErrExtraArgs } - cli := Client() - id, err := x.GetChangeID(cli) + id, err := x.GetChangeID() if err != nil { + if err == noChangeFoundOK { + return nil + } return err } - _, err = wait(cli, id) + + // this is the only valid use of wait without a waitMixin (ie + // without --no-wait), so we fake it here. + wmx := &waitMixin{skipAbort: true} + wmx.client = x.client + _, err = wmx.wait(id) return err } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_watch_test.go snapd-2.37~rc1~14.04/cmd/snap/cmd_watch_test.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_watch_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_watch_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -32,7 +32,7 @@ ) var fmtWatchChangeJSON = `{"type": "sync", "result": { - "id": "42", + "id": "two", "kind": "some-kind", "summary": "some summary...", "status": "Doing", @@ -43,31 +43,112 @@ func (s *SnapSuite) TestCmdWatch(c *C) { meter := &progresstest.Meter{} defer progress.MockMeter(meter)() - restore := snap.MockMaxGoneTime(time.Millisecond) - defer restore() + defer snap.MockMaxGoneTime(time.Millisecond)() + defer snap.MockPollTime(time.Millisecond)() n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ switch n { - case 0: + case 1: c.Check(r.Method, Equals, "GET") - c.Check(r.URL.Path, Equals, "/v2/changes/42") + c.Check(r.URL.Path, Equals, "/v2/changes/two") fmt.Fprintf(w, fmtWatchChangeJSON, 0, 100*1024) - case 1: + case 2: c.Check(r.Method, Equals, "GET") - c.Check(r.URL.Path, Equals, "/v2/changes/42") + c.Check(r.URL.Path, Equals, "/v2/changes/two") fmt.Fprintf(w, fmtWatchChangeJSON, 50*1024, 100*1024) - case 2: + case 3: c.Check(r.Method, Equals, "GET") - c.Check(r.URL.Path, Equals, "/v2/changes/42") - fmt.Fprintln(w, `{"type": "sync", "result": {"id": "42", "ready": true, "status": "Done"}}`) + c.Check(r.URL.Path, Equals, "/v2/changes/two") + fmt.Fprintln(w, `{"type": "sync", "result": {"id": "two", "ready": true, "status": "Done"}}`) + default: + c.Errorf("expected 3 queries, currently on %d", n) } - n++ }) - _, err := snap.Parser().ParseArgs([]string{"watch", "42"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"watch", "two"}) c.Assert(err, IsNil) + c.Assert(rest, HasLen, 0) c.Check(n, Equals, 3) + c.Check(meter.Values, DeepEquals, []float64{51200}) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestWatchLast(c *C) { + meter := &progresstest.Meter{} + defer progress.MockMeter(meter)() + defer snap.MockMaxGoneTime(time.Millisecond)() + defer snap.MockPollTime(time.Millisecond)() + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + case 1: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/changes") + fmt.Fprintln(w, mockChangesJSON) + case 2: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/changes/two") + fmt.Fprintf(w, fmtWatchChangeJSON, 0, 100*1024) + case 3: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/changes/two") + fmt.Fprintf(w, fmtWatchChangeJSON, 50*1024, 100*1024) + case 4: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/changes/two") + fmt.Fprintln(w, `{"type": "sync", "result": {"id": "two", "ready": true, "status": "Done"}}`) + default: + c.Errorf("expected 4 queries, currently on %d", n) + } + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"watch", "--last=install"}) + c.Assert(err, IsNil) + c.Assert(rest, HasLen, 0) + c.Check(n, Equals, 4) c.Check(meter.Values, DeepEquals, []float64{51200}) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestWatchLastQuestionmark(c *C) { + meter := &progresstest.Meter{} + defer progress.MockMeter(meter)() + restore := snap.MockMaxGoneTime(time.Millisecond) + defer restore() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + c.Check(r.Method, Equals, "GET") + c.Assert(r.URL.Path, Equals, "/v2/changes") + switch n { + case 1, 2: + fmt.Fprintln(w, `{"type": "sync", "result": []}`) + case 3, 4: + fmt.Fprintln(w, mockChangesJSON) + default: + c.Errorf("expected 4 calls, now on %d", n) + } + }) + for i := 0; i < 2; i++ { + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"watch", "--last=foobar?"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Matches, "") + c.Check(s.Stderr(), Equals, "") + + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"watch", "--last=foobar"}) + if i == 0 { + c.Assert(err, ErrorMatches, `no changes found`) + } else { + c.Assert(err, ErrorMatches, `no changes of type "foobar" found`) + } + } + + c.Check(n, Equals, 4) } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/cmd_whoami.go snapd-2.37~rc1~14.04/cmd/snap/cmd_whoami.go --- snapd-2.32.3.2~14.04/cmd/snap/cmd_whoami.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/cmd_whoami.go 2019-01-16 08:36:51.000000000 +0000 @@ -27,12 +27,14 @@ "github.com/jessevdk/go-flags" ) -var shortWhoAmIHelp = i18n.G("Prints the email the user is logged in with") +var shortWhoAmIHelp = i18n.G("Show the email the user is logged in with") var longWhoAmIHelp = i18n.G(` -The whoami command prints the email the user is logged in with. +The whoami command shows the email the user is logged in with. `) -type cmdWhoAmI struct{} +type cmdWhoAmI struct { + clientMixin +} func init() { addCommand("whoami", shortWhoAmIHelp, longWhoAmIHelp, func() flags.Commander { return &cmdWhoAmI{} }, nil, nil) @@ -43,7 +45,7 @@ return ErrExtraArgs } - email, err := Client().WhoAmI() + email, err := cmd.client.WhoAmI() if err != nil { return err } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/color.go snapd-2.37~rc1~14.04/cmd/snap/color.go --- snapd-2.32.3.2~14.04/cmd/snap/color.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/color.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,181 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "fmt" + "os" + "strings" + + "golang.org/x/crypto/ssh/terminal" + + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/snap" +) + +type colorMixin struct { + Color string `long:"color" default:"auto" choice:"auto" choice:"never" choice:"always"` + Unicode string `long:"unicode" default:"auto" choice:"auto" choice:"never" choice:"always"` // do we want this hidden? +} + +func (mx colorMixin) getEscapes() *escapes { + esc := colorTable(mx.Color) + if canUnicode(mx.Unicode) { + esc.dash = "–" // that's an en dash (so yaml is happy) + esc.uparrow = "↑" + esc.tick = "✓" + } else { + esc.dash = "--" // two dashes keeps yaml happy also + esc.uparrow = "^" + esc.tick = "*" + } + + return &esc +} + +func canUnicode(mode string) bool { + switch mode { + case "always": + return true + case "never": + return false + } + if !isStdoutTTY { + return false + } + var lang string + for _, k := range []string{"LC_MESSAGES", "LC_ALL", "LANG"} { + lang = os.Getenv(k) + if lang != "" { + break + } + } + if lang == "" { + return false + } + lang = strings.ToUpper(lang) + return strings.Contains(lang, "UTF-8") || strings.Contains(lang, "UTF8") +} + +var isStdoutTTY = terminal.IsTerminal(1) + +func colorTable(mode string) escapes { + switch mode { + case "always": + return color + case "never": + return noesc + } + if !isStdoutTTY { + return noesc + } + if _, ok := os.LookupEnv("NO_COLOR"); ok { + // from http://no-color.org/: + // command-line software which outputs text with ANSI color added should + // check for the presence of a NO_COLOR environment variable that, when + // present (regardless of its value), prevents the addition of ANSI color. + return mono // bold & dim is still ok + } + if term := os.Getenv("TERM"); term == "xterm-mono" || term == "linux-m" { + // these are often used to flag "I don't want to see color" more than "I can't do color" + // (if you can't *do* color, `color` and `mono` should produce the same results) + return mono + } + return color +} + +var colorDescs = mixinDescs{ + // TRANSLATORS: This should not start with a lowercase letter. + "color": i18n.G("Use a little bit of color to highlight some things."), + // TRANSLATORS: This should not start with a lowercase letter. + "unicode": i18n.G("Use a little bit of Unicode to improve legibility."), +} + +type escapes struct { + green string + end string + + tick, dash, uparrow string +} + +var ( + color = escapes{ + green: "\033[32m", + end: "\033[0m", + } + + mono = escapes{ + green: "\033[1m", + end: "\033[0m", + } + + noesc = escapes{} +) + +// fillerPublisher is used to add an no-op escape sequence to a header in a +// tabwriter table, so that things line up. +func fillerPublisher(esc *escapes) string { + return esc.green + esc.end +} + +// longPublisher returns a string that'll present the publisher of a snap to the +// terminal user: +// +// * if the publisher's username and display name match, it's just the display +// name; otherwise, it'll include the username in parentheses +// +// * if the publisher is verified, it'll include a green check mark; otherwise, +// it'll include a no-op escape sequence of the same length as the escape +// sequence used to make it green (this so that tabwriter gets things right). +func longPublisher(esc *escapes, storeAccount *snap.StoreAccount) string { + if storeAccount == nil { + return esc.dash + esc.green + esc.end + } + badge := "" + if storeAccount.Validation == "verified" { + badge = esc.tick + } + // NOTE this makes e.g. 'Potato' == 'potato', and 'Potato Team' == 'potato-team', + // but 'Potato Team' != 'potatoteam', 'Potato Inc.' != 'potato' (in fact 'Potato Inc.' != 'potato-inc') + if strings.EqualFold(strings.Replace(storeAccount.Username, "-", " ", -1), storeAccount.DisplayName) { + return storeAccount.DisplayName + esc.green + badge + esc.end + } + return fmt.Sprintf("%s (%s%s%s%s)", storeAccount.DisplayName, storeAccount.Username, esc.green, badge, esc.end) +} + +// shortPublisher returns a string that'll present the publisher of a snap to the +// terminal user: +// +// * it'll always be just the username +// +// * if the publisher is verified, it'll include a green check mark; otherwise, +// it'll include a no-op escape sequence of the same length as the escape +// sequence used to make it green (this so that tabwriter gets things right). +func shortPublisher(esc *escapes, storeAccount *snap.StoreAccount) string { + if storeAccount == nil { + return "-" + esc.green + esc.end + } + badge := "" + if storeAccount.Validation == "verified" { + badge = esc.tick + } + return storeAccount.Username + esc.green + badge + esc.end + +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/color_test.go snapd-2.37~rc1~14.04/cmd/snap/color_test.go --- snapd-2.32.3.2~14.04/cmd/snap/color_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/color_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -0,0 +1,196 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main_test + +import ( + "os" + "runtime" + // "fmt" + // "net/http" + + "gopkg.in/check.v1" + + cmdsnap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/snap" +) + +func setEnviron(env map[string]string) func() { + old := make(map[string]string, len(env)) + ok := make(map[string]bool, len(env)) + + for k, v := range env { + old[k], ok[k] = os.LookupEnv(k) + if v != "" { + os.Setenv(k, v) + } else { + os.Unsetenv(k) + } + } + + return func() { + for k := range ok { + if ok[k] { + os.Setenv(k, old[k]) + } else { + os.Unsetenv(k) + } + } + } +} + +func (s *SnapSuite) TestCanUnicode(c *check.C) { + // setenv is per thread + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + type T struct { + lang, lcAll, lcMsg string + expected bool + } + + for _, t := range []T{ + {expected: false}, // all locale unset + {lang: "C", expected: false}, + {lang: "C", lcAll: "C", expected: false}, + {lang: "C", lcAll: "C", lcMsg: "C", expected: false}, + {lang: "C.UTF-8", lcAll: "C", lcMsg: "C", expected: false}, // LC_MESSAGES wins + {lang: "C.UTF-8", lcAll: "C.UTF-8", lcMsg: "C", expected: false}, + {lang: "C.UTF-8", lcAll: "C.UTF-8", lcMsg: "C.UTF-8", expected: true}, + {lang: "C.UTF-8", lcAll: "C", lcMsg: "C.UTF-8", expected: true}, + {lang: "C", lcAll: "C", lcMsg: "C.UTF-8", expected: true}, + {lang: "C", lcAll: "C.UTF-8", expected: true}, + {lang: "C.UTF-8", expected: true}, + {lang: "C.utf8", expected: true}, // deals with a bit of rando weirdness + } { + restore := setEnviron(map[string]string{"LANG": t.lang, "LC_ALL": t.lcAll, "LC_MESSAGES": t.lcMsg}) + c.Check(cmdsnap.CanUnicode("never"), check.Equals, false) + c.Check(cmdsnap.CanUnicode("always"), check.Equals, true) + restoreIsTTY := cmdsnap.MockIsStdoutTTY(true) + c.Check(cmdsnap.CanUnicode("auto"), check.Equals, t.expected) + cmdsnap.MockIsStdoutTTY(false) + c.Check(cmdsnap.CanUnicode("auto"), check.Equals, false) + restoreIsTTY() + restore() + } +} + +func (s *SnapSuite) TestColorTable(c *check.C) { + // setenv is per thread + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + type T struct { + isTTY bool + noColor, term string + expected interface{} + desc string + } + + for _, t := range []T{ + {isTTY: false, expected: cmdsnap.NoEscColorTable, desc: "not a tty"}, + {isTTY: false, noColor: "1", expected: cmdsnap.NoEscColorTable, desc: "no tty *and* NO_COLOR set"}, + {isTTY: false, term: "linux-m", expected: cmdsnap.NoEscColorTable, desc: "no tty *and* mono term set"}, + {isTTY: true, expected: cmdsnap.ColorColorTable, desc: "is a tty"}, + {isTTY: true, noColor: "1", expected: cmdsnap.MonoColorTable, desc: "is a tty, but NO_COLOR set"}, + {isTTY: true, term: "linux-m", expected: cmdsnap.MonoColorTable, desc: "is a tty, but TERM=linux-m"}, + {isTTY: true, term: "xterm-mono", expected: cmdsnap.MonoColorTable, desc: "is a tty, but TERM=xterm-mono"}, + } { + restoreIsTTY := cmdsnap.MockIsStdoutTTY(t.isTTY) + restoreEnv := setEnviron(map[string]string{"NO_COLOR": t.noColor, "TERM": t.term}) + c.Check(cmdsnap.ColorTable("never"), check.DeepEquals, cmdsnap.NoEscColorTable, check.Commentf(t.desc)) + c.Check(cmdsnap.ColorTable("always"), check.DeepEquals, cmdsnap.ColorColorTable, check.Commentf(t.desc)) + c.Check(cmdsnap.ColorTable("auto"), check.DeepEquals, t.expected, check.Commentf(t.desc)) + restoreEnv() + restoreIsTTY() + } +} + +func (s *SnapSuite) TestPublisherEscapes(c *check.C) { + // just check never/always; for auto checks look above + type T struct { + color, unicode bool + username, display string + verified bool + short, long, fill string + } + for _, t := range []T{ + // non-verified equal under fold: + {color: false, unicode: false, username: "potato", display: "Potato", + short: "potato", long: "Potato", fill: ""}, + {color: false, unicode: true, username: "potato", display: "Potato", + short: "potato", long: "Potato", fill: ""}, + {color: true, unicode: false, username: "potato", display: "Potato", + short: "potato\x1b[32m\x1b[0m", long: "Potato\x1b[32m\x1b[0m", fill: "\x1b[32m\x1b[0m"}, + {color: true, unicode: true, username: "potato", display: "Potato", + short: "potato\x1b[32m\x1b[0m", long: "Potato\x1b[32m\x1b[0m", fill: "\x1b[32m\x1b[0m"}, + // verified equal under fold: + {color: false, unicode: false, username: "potato", display: "Potato", verified: true, + short: "potato*", long: "Potato*", fill: ""}, + {color: false, unicode: true, username: "potato", display: "Potato", verified: true, + short: "potato✓", long: "Potato✓", fill: ""}, + {color: true, unicode: false, username: "potato", display: "Potato", verified: true, + short: "potato\x1b[32m*\x1b[0m", long: "Potato\x1b[32m*\x1b[0m", fill: "\x1b[32m\x1b[0m"}, + {color: true, unicode: true, username: "potato", display: "Potato", verified: true, + short: "potato\x1b[32m✓\x1b[0m", long: "Potato\x1b[32m✓\x1b[0m", fill: "\x1b[32m\x1b[0m"}, + // non-verified, different + {color: false, unicode: false, username: "potato", display: "Carrot", + short: "potato", long: "Carrot (potato)", fill: ""}, + {color: false, unicode: true, username: "potato", display: "Carrot", + short: "potato", long: "Carrot (potato)", fill: ""}, + {color: true, unicode: false, username: "potato", display: "Carrot", + short: "potato\x1b[32m\x1b[0m", long: "Carrot (potato\x1b[32m\x1b[0m)", fill: "\x1b[32m\x1b[0m"}, + {color: true, unicode: true, username: "potato", display: "Carrot", + short: "potato\x1b[32m\x1b[0m", long: "Carrot (potato\x1b[32m\x1b[0m)", fill: "\x1b[32m\x1b[0m"}, + // verified, different + {color: false, unicode: false, username: "potato", display: "Carrot", verified: true, + short: "potato*", long: "Carrot (potato*)", fill: ""}, + {color: false, unicode: true, username: "potato", display: "Carrot", verified: true, + short: "potato✓", long: "Carrot (potato✓)", fill: ""}, + {color: true, unicode: false, username: "potato", display: "Carrot", verified: true, + short: "potato\x1b[32m*\x1b[0m", long: "Carrot (potato\x1b[32m*\x1b[0m)", fill: "\x1b[32m\x1b[0m"}, + {color: true, unicode: true, username: "potato", display: "Carrot", verified: true, + short: "potato\x1b[32m✓\x1b[0m", long: "Carrot (potato\x1b[32m✓\x1b[0m)", fill: "\x1b[32m\x1b[0m"}, + // some interesting equal-under-folds: + {color: false, unicode: false, username: "potato", display: "PoTaTo", + short: "potato", long: "PoTaTo", fill: ""}, + {color: false, unicode: false, username: "potato-team", display: "Potato Team", + short: "potato-team", long: "Potato Team", fill: ""}, + } { + pub := &snap.StoreAccount{Username: t.username, DisplayName: t.display} + if t.verified { + pub.Validation = "verified" + } + color := "never" + if t.color { + color = "always" + } + unicode := "never" + if t.unicode { + unicode = "always" + } + + mx := cmdsnap.ColorMixin(color, unicode) + esc := cmdsnap.ColorMixinGetEscapes(mx) + + c.Check(cmdsnap.ShortPublisher(esc, pub), check.Equals, t.short) + c.Check(cmdsnap.LongPublisher(esc, pub), check.Equals, t.long) + c.Check(cmdsnap.FillerPublisher(esc), check.Equals, t.fill) + } +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/complete.go snapd-2.37~rc1~14.04/cmd/snap/complete.go --- snapd-2.32.3.2~14.04/cmd/snap/complete.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/complete.go 2019-01-16 08:36:51.000000000 +0000 @@ -23,6 +23,7 @@ "bufio" "fmt" "os" + "strconv" "strings" "github.com/jessevdk/go-flags" @@ -36,7 +37,7 @@ type installedSnapName string func (s installedSnapName) Complete(match string) []flags.Completion { - snaps, err := Client().List(nil, nil) + snaps, err := mkClient().List(nil, nil) if err != nil { return nil } @@ -103,7 +104,7 @@ if len(match) < 3 { return nil } - snaps, _, err := Client().Find(&client.FindOptions{ + snaps, _, err := mkClient().Find(&client.FindOptions{ Prefix: true, Query: match, }) @@ -147,7 +148,7 @@ type changeID string func (s changeID) Complete(match string) []flags.Completion { - changes, err := Client().Changes(&client.ChangesOptions{Selector: client.ChangesAll}) + changes, err := mkClient().Changes(&client.ChangesOptions{Selector: client.ChangesAll}) if err != nil { return nil } @@ -165,7 +166,7 @@ type assertTypeName string func (n assertTypeName) Complete(match string) []flags.Completion { - cli := Client() + cli := mkClient() names, err := cli.AssertionTypes() if err != nil { return nil @@ -295,7 +296,7 @@ parts := strings.SplitN(match, ":", 2) // Ask snapd about available interfaces. - ifaces, err := Client().Connections() + ifaces, err := mkClient().Connections() if err != nil { return nil } @@ -386,7 +387,7 @@ type interfaceName string func (s interfaceName) Complete(match string) []flags.Completion { - ifaces, err := Client().Interfaces(nil) + ifaces, err := mkClient().Interfaces(nil) if err != nil { return nil } @@ -404,7 +405,7 @@ type appName string func (s appName) Complete(match string) []flags.Completion { - cli := Client() + cli := mkClient() apps, err := cli.Apps(nil, client.AppOptions{}) if err != nil { return nil @@ -428,7 +429,7 @@ type serviceName string func (s serviceName) Complete(match string) []flags.Completion { - cli := Client() + cli := mkClient() apps, err := cli.Apps(nil, client.AppOptions{Service: true}) if err != nil { return nil @@ -453,7 +454,7 @@ type aliasOrSnap string func (s aliasOrSnap) Complete(match string) []flags.Completion { - aliases, err := Client().Aliases() + aliases, err := mkClient().Aliases() if err != nil { return nil } @@ -473,3 +474,21 @@ } return ret } + +type snapshotID uint64 + +func (snapshotID) Complete(match string) []flags.Completion { + shots, err := mkClient().SnapshotSets(0, nil) + if err != nil { + return nil + } + var ret []flags.Completion + for _, sg := range shots { + sid := strconv.FormatUint(sg.ID, 10) + if strings.HasPrefix(sid, match) { + ret = append(ret, flags.Completion{Item: sid}) + } + } + + return ret +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/error.go snapd-2.37~rc1~14.04/cmd/snap/error.go --- snapd-2.32.3.2~14.04/cmd/snap/error.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/error.go 2019-01-16 08:36:51.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2017 Canonical Ltd + * Copyright (C) 2017-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -27,6 +27,7 @@ "os" "os/user" "strings" + "text/tabwriter" "golang.org/x/crypto/ssh/terminal" @@ -34,6 +35,8 @@ "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/strutil" ) var errorPrefix = i18n.G("error: %v\n") @@ -76,7 +79,8 @@ width-- var buf bytes.Buffer - doc.ToText(&buf, para, strings.Repeat(" ", indent), "", width-indent) + indentStr := strings.Repeat(" ", indent) + doc.ToText(&buf, para, indentStr, indentStr, width-indent) return strings.TrimSpace(buf.String()) } @@ -87,6 +91,10 @@ if !ok { return "", e } + // retryable errors are just passed through + if client.IsRetryable(err) { + return "", err + } // ensure the "real" error is available if we ask for it logger.Debugf("error: %s", err) @@ -97,11 +105,17 @@ usesSnapName := true var msg string switch err.Kind { - case client.ErrorKindSnapNotFound: - // FIXME: the snap store _is_ sending us a different message when a - // snap does not exist vs. when it does not exist for the current - // arch/channel/revision. Surface that here somehow! + case client.ErrorKindNotSnap: + msg = i18n.G(`%q does not contain an unpacked snap. +Try 'snapcraft prime' in your project directory, then 'snap try' again.`) + if snapName == "" || snapName == "./" { + errValStr, ok := err.Value.(string) + if ok && errValStr != "" { + snapName = errValStr + } + } + case client.ErrorKindSnapNotFound: msg = i18n.G("snap %q not found") if snapName == "" { errValStr, ok := err.Value.(string) @@ -109,21 +123,56 @@ snapName = errValStr } } + case client.ErrorKindChannelNotAvailable, + client.ErrorKindArchitectureNotAvailable: + values, ok := err.Value.(map[string]interface{}) + if ok { + candName, _ := values["snap-name"].(string) + if candName != "" { + snapName = candName + } + action, _ := values["action"].(string) + arch, _ := values["architecture"].(string) + channel, _ := values["channel"].(string) + releases, _ := values["releases"].([]interface{}) + if snapName != "" && action != "" && arch != "" && channel != "" && len(releases) != 0 { + usesSnapName = false + msg = snapRevisionNotAvailableMessage(err.Kind, snapName, action, arch, channel, releases) + break + } + } + fallthrough + case client.ErrorKindRevisionNotAvailable: + if snapName == "" { + errValStr, ok := err.Value.(string) + if ok && errValStr != "" { + snapName = errValStr + } + } + + usesSnapName = false + // TRANSLATORS: %[1]q and %[1]s refer to the same thing (a snap name). + msg = fmt.Sprintf(i18n.G(`snap %[1]q not available as specified (see 'snap info %[1]s')`), snapName) + if opts != nil { if opts.Revision != "" { - // TRANSLATORS: %%q will become a %q for the snap name; %q is whatever foo the user used for --revision=foo - msg = fmt.Sprintf(i18n.G("snap %%q not found (at least at revision %q)"), opts.Revision) + // TRANSLATORS: %[1]q and %[1]s refer to the same thing (a snap name); %s is whatever the user used for --revision= + msg = fmt.Sprintf(i18n.G(`snap %[1]q revision %s not available (see 'snap info %[1]s')`), snapName, opts.Revision) } else if opts.Channel != "" { // (note --revision overrides --channel) - // TRANSLATORS: %%q will become a %q for the snap name; %q is whatever foo the user used for --channel=foo - msg = fmt.Sprintf(i18n.G("snap %%q not found (at least in channel %q)"), opts.Channel) + // TRANSLATORS: %[1]q and %[1]s refer to the same thing (a snap name); %q is whatever foo the user used for --channel=foo + msg = fmt.Sprintf(i18n.G(`snap %[1]q not available on channel %q (see 'snap info %[1]s')`), snapName, opts.Channel) } } case client.ErrorKindSnapAlreadyInstalled: isError = false - msg = i18n.G(`snap %q is already installed, see "snap refresh --help"`) + msg = i18n.G(`snap %q is already installed, see 'snap help refresh'`) case client.ErrorKindSnapNeedsDevMode: + if opts != nil && opts.Dangerous { + msg = i18n.G("snap %q requires devmode or confinement override") + break + } msg = i18n.G(` The publisher of snap %q has indicated that they do not consider this revision to be of production quality and that it is only meant for development or testing @@ -142,12 +191,14 @@ If you understand and want to proceed repeat the command including --classic. `) + case client.ErrorKindSnapNotClassic: + msg = i18n.G(`snap %q is not compatible with --classic`) case client.ErrorKindLoginRequired: usesSnapName = false u, _ := user.Current() if u != nil && u.Username == "root" { // TRANSLATORS: %s is an error message (e.g. “cannot yadda yadda: permission denied”) - msg = fmt.Sprintf(i18n.G(`%s (see "snap login --help")`), err.Message) + msg = fmt.Sprintf(i18n.G(`%s (see 'snap help login')`), err.Message) } else { // TRANSLATORS: %s is an error message (e.g. “cannot yadda yadda: permission denied”) msg = fmt.Sprintf(i18n.G(`%s (try with sudo)`), err.Message) @@ -165,6 +216,10 @@ isError = true usesSnapName = false msg = i18n.G("unable to contact snap store") + case client.ErrorKindSystemRestart: + isError = false + usesSnapName = false + msg = i18n.G("snapd is about to reboot the system") default: usesSnapName = false msg = err.Message @@ -181,3 +236,165 @@ return msg, nil } + +func snapRevisionNotAvailableMessage(kind, snapName, action, arch, channel string, releases []interface{}) string { + // releases contains all available (arch x channel) + // as reported by the store through the daemon + req, err := snap.ParseChannel(channel, arch) + if err != nil { + // TRANSLATORS: %q is the invalid request channel, %s is the snap name + msg := fmt.Sprintf(i18n.G("requested channel %q is not valid (see 'snap info %s' for valid ones)"), channel, snapName) + return msg + } + avail := make([]*snap.Channel, 0, len(releases)) + for _, v := range releases { + rel, _ := v.(map[string]interface{}) + relCh, _ := rel["channel"].(string) + relArch, _ := rel["architecture"].(string) + if relArch == "" { + logger.Debugf("internal error: %q daemon error carries a release with invalid/empty architecture: %v", kind, v) + continue + } + a, err := snap.ParseChannel(relCh, relArch) + if err != nil { + logger.Debugf("internal error: %q daemon error carries a release with invalid/empty channel (%v): %v", kind, err, v) + continue + } + avail = append(avail, &a) + } + + matches := map[string][]*snap.Channel{} + for _, a := range avail { + m := req.Match(a) + matchRepr := m.String() + if matchRepr != "" { + matches[matchRepr] = append(matches[matchRepr], a) + } + } + + // no release is for this architecture + if kind == client.ErrorKindArchitectureNotAvailable { + // TODO: add "Get more information..." hints once snap info + // support showing multiple/all archs + + // there are matching track+risk releases for other archs + if hits := matches["track:risk"]; len(hits) != 0 { + archs := strings.Join(archsForChannels(hits), ", ") + // TRANSLATORS: %q is for the snap name, %v is the requested channel, first %s is the system architecture short name, second %s is a comma separated list of available arch short names + msg := fmt.Sprintf(i18n.G("snap %q is not available on %v for this architecture (%s) but exists on other architectures (%s)."), snapName, req, arch, archs) + return msg + } + + // not even that, generic error + archs := strings.Join(archsForChannels(avail), ", ") + // TRANSLATORS: %q is for the snap name, first %s is the system architecture short name, second %s is a comma separated list of available arch short names + msg := fmt.Sprintf(i18n.G("snap %q is not available on this architecture (%s) but exists on other architectures (%s)."), snapName, arch, archs) + return msg + } + + // a branch was requested + if req.Branch != "" { + // there are matching arch+track+risk, give main track info + if len(matches["architecture:track:risk"]) != 0 { + trackRisk := snap.Channel{Track: req.Track, Risk: req.Risk} + trackRisk = trackRisk.Clean() + + // TRANSLATORS: %q is for the snap name, first %s is the full requested channel + msg := fmt.Sprintf(i18n.G("requested a non-existing branch on %s for snap %q: %s"), trackRisk.Full(), snapName, req.Branch) + return msg + } + + msg := fmt.Sprintf(i18n.G("requested a non-existing branch for snap %q: %s"), snapName, req.Full()) + return msg + } + + // TRANSLATORS: can optionally be concatenated after a blank line at the end of other error messages, together with the "Get more information ..." hint + preRelWarn := i18n.G("Please be mindful pre-release channels may include features not completely tested or implemented.") + // TRANSLATORS: can optionally be concatenated after a blank line at the end of other error messages, together with the "Get more information ..." hint + trackWarn := i18n.G("Please be mindful that different tracks may include different features.") + // TRANSLATORS: %s is for the snap name, will be concatenated after at the end of other error messages, possibly after a blank line + moreInfoHint := fmt.Sprintf(i18n.G("Get more information with 'snap info %s'."), snapName) + + // there are matching arch+track releases => give hint and instructions + // about pre-release channels + if hits := matches["architecture:track"]; len(hits) != 0 { + // TRANSLATORS: %q is for the snap name, %v is the requested channel + msg := fmt.Sprintf(i18n.G("snap %q is not available on %v but is available to install on the following channels:\n"), snapName, req) + msg += installTable(snapName, action, hits, false) + msg += "\n" + if req.Risk == "stable" { + msg += "\n" + preRelWarn + } + msg += "\n" + moreInfoHint + return msg + } + + // there are matching arch+risk releases => give hints and instructions + // about these other tracks + if hits := matches["architecture:risk"]; len(hits) != 0 { + // TRANSLATORS: %q is for the snap name, %s is the full requested channel + msg := fmt.Sprintf(i18n.G("snap %q is not available on %s but is available to install on the following tracks:\n"), snapName, req.Full()) + msg += installTable(snapName, action, hits, true) + msg += "\n\n" + trackWarn + msg += "\n" + moreInfoHint + return msg + } + + // generic error + // TRANSLATORS: %q is for the snap name, %s is the full requested channel + msg := fmt.Sprintf(i18n.G("snap %q is not available on %s but other tracks exist.\n"), snapName, req.Full()) + msg += "\n\n" + trackWarn + msg += "\n" + moreInfoHint + return msg +} + +func installTable(snapName, action string, avail []*snap.Channel, full bool) string { + b := &bytes.Buffer{} + w := tabwriter.NewWriter(b, len("candidate")+2, 1, 2, ' ', 0) + first := true + for _, a := range avail { + if first { + first = false + } else { + fmt.Fprint(w, "\n") + } + var ch string + if full { + ch = a.Full() + } else { + ch = a.String() + } + chOption := channelOption(a) + fmt.Fprintf(w, "%s\tsnap %s %s %s", ch, action, chOption, snapName) + } + w.Flush() + tbl := b.String() + // indent to drive fill/ToText to keep the tabulations intact + lines := strings.SplitAfter(tbl, "\n") + for i := range lines { + lines[i] = " " + lines[i] + } + return strings.Join(lines, "") +} + +func channelOption(c *snap.Channel) string { + if c.Branch == "" { + if c.Track == "" { + return fmt.Sprintf("--%s", c.Risk) + } + if c.Risk == "stable" { + return fmt.Sprintf("--channel=%s", c.Track) + } + } + return fmt.Sprintf("--channel=%s", c) +} + +func archsForChannels(cs []*snap.Channel) []string { + archs := []string{} + for _, c := range cs { + if !strutil.ListContains(archs, c.Architecture) { + archs = append(archs, c.Architecture) + } + } + return archs +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/export_test.go snapd-2.37~rc1~14.04/cmd/snap/export_test.go --- snapd-2.32.3.2~14.04/cmd/snap/export_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/export_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -25,15 +25,20 @@ "github.com/jessevdk/go-flags" + "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/selinux" "github.com/snapcore/snapd/store" ) var RunMain = run var ( + Client = mkClient + + FirstNonOptionIsRun = firstNonOptionIsRun + CreateUserDataDirs = createUserDataDirs - Wait = wait ResolveApp = resolveApp IsReexeced = isReexeced MaybePrintServices = maybePrintServices @@ -42,6 +47,35 @@ AdviseCommand = adviseCommand Antialias = antialias FormatChannel = fmtChannel + PrintDescr = printDescr + TrueishJSON = trueishJSON + + CanUnicode = canUnicode + ColorTable = colorTable + MonoColorTable = mono + ColorColorTable = color + NoEscColorTable = noesc + ColorMixinGetEscapes = (colorMixin).getEscapes + FillerPublisher = fillerPublisher + LongPublisher = longPublisher + ShortPublisher = shortPublisher + + ReadRpc = readRpc + + WriteWarningTimestamp = writeWarningTimestamp + MaybePresentWarnings = maybePresentWarnings + + LongSnapDescription = longSnapDescription + SnapUsage = snapUsage + SnapHelpCategoriesIntro = snapHelpCategoriesIntro + SnapHelpAllFooter = snapHelpAllFooter + SnapHelpFooter = snapHelpFooter + HelpCategories = helpCategories + + LintArg = lintArg + LintDesc = lintDesc + + FixupArg = fixupArg ) func MockPollTime(d time.Duration) (restore func()) { @@ -130,11 +164,19 @@ return assertTypeName("").Complete(match) } -func MockIsTerminal(t bool) (restore func()) { - oldIsTerminal := isTerminal - isTerminal = func() bool { return t } +func MockIsStdoutTTY(t bool) (restore func()) { + oldIsStdoutTTY := isStdoutTTY + isStdoutTTY = t + return func() { + isStdoutTTY = oldIsStdoutTTY + } +} + +func MockIsStdinTTY(t bool) (restore func()) { + oldIsStdinTTY := isStdinTTY + isStdinTTY = t return func() { - isTerminal = oldIsTerminal + isStdinTTY = oldIsStdinTTY } } @@ -145,3 +187,57 @@ timeNow = oldTimeNow } } + +func MockTimeutilHuman(h func(time.Time) string) (restore func()) { + oldH := timeutilHuman + timeutilHuman = h + return func() { + timeutilHuman = oldH + } +} + +func MockWaitConfTimeout(d time.Duration) (restore func()) { + oldWaitConfTimeout := d + waitConfTimeout = d + return func() { + waitConfTimeout = oldWaitConfTimeout + } +} + +func Wait(cli *client.Client, id string) (*client.Change, error) { + wmx := waitMixin{} + wmx.client = cli + return wmx.wait(id) +} + +func ColorMixin(cmode, umode string) colorMixin { + return colorMixin{Color: cmode, Unicode: umode} +} + +func CmdAdviseSnap() *cmdAdviseSnap { + return &cmdAdviseSnap{} +} + +func MockSELinuxIsEnabled(isEnabled func() (bool, error)) (restore func()) { + old := selinuxIsEnabled + selinuxIsEnabled = isEnabled + return func() { + selinuxIsEnabled = old + } +} + +func MockSELinuxVerifyPathContext(verifypathcon func(string) (bool, error)) (restore func()) { + old := selinuxVerifyPathContext + selinuxVerifyPathContext = verifypathcon + return func() { + selinuxVerifyPathContext = old + } +} + +func MockSELinuxRestoreContext(restorecon func(string, selinux.RestoreMode) error) (restore func()) { + old := selinuxRestoreContext + selinuxRestoreContext = restorecon + return func() { + selinuxRestoreContext = old + } +} diff -Nru snapd-2.32.3.2~14.04/cmd/snap/interfaces_common.go snapd-2.37~rc1~14.04/cmd/snap/interfaces_common.go --- snapd-2.32.3.2~14.04/cmd/snap/interfaces_common.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/interfaces_common.go 2019-01-16 08:36:51.000000000 +0000 @@ -26,36 +26,6 @@ "github.com/snapcore/snapd/i18n" ) -// AttributePair contains a pair of key-value strings -type AttributePair struct { - // The key - Key string - // The value - Value string -} - -// UnmarshalFlag parses a string into an AttributePair -func (ap *AttributePair) UnmarshalFlag(value string) error { - parts := strings.SplitN(value, "=", 2) - if len(parts) < 2 || parts[0] == "" { - ap.Key = "" - ap.Value = "" - return fmt.Errorf(i18n.G("invalid attribute: %q (want key=value)"), value) - } - ap.Key = parts[0] - ap.Value = parts[1] - return nil -} - -// AttributePairSliceToMap converts a slice of AttributePair into a map -func AttributePairSliceToMap(attrs []AttributePair) map[string]string { - result := make(map[string]string) - for _, attr := range attrs { - result[attr.Key] = attr.Value - } - return result -} - // SnapAndName holds a snap name and a plug or slot name. type SnapAndName struct { Snap string diff -Nru snapd-2.32.3.2~14.04/cmd/snap/interfaces_common_test.go snapd-2.37~rc1~14.04/cmd/snap/interfaces_common_test.go --- snapd-2.32.3.2~14.04/cmd/snap/interfaces_common_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/interfaces_common_test.go 2019-01-16 08:36:51.000000000 +0000 @@ -25,56 +25,6 @@ . "github.com/snapcore/snapd/cmd/snap" ) -type AttributePairSuite struct{} - -var _ = Suite(&AttributePairSuite{}) - -func (s *AttributePairSuite) TestUnmarshalFlagAttributePair(c *C) { - var ap AttributePair - // Typical - err := ap.UnmarshalFlag("key=value") - c.Assert(err, IsNil) - c.Check(ap.Key, Equals, "key") - c.Check(ap.Value, Equals, "value") - // Empty key - err = ap.UnmarshalFlag("=value") - c.Assert(err, ErrorMatches, `invalid attribute: "=value" \(want key=value\)`) - c.Check(ap.Key, Equals, "") - c.Check(ap.Value, Equals, "") - // Empty value - err = ap.UnmarshalFlag("key=") - c.Assert(err, IsNil) - c.Check(ap.Key, Equals, "key") - c.Check(ap.Value, Equals, "") - // Both key and value empty - err = ap.UnmarshalFlag("=") - c.Assert(err, ErrorMatches, `invalid attribute: "=" \(want key=value\)`) - c.Check(ap.Key, Equals, "") - c.Check(ap.Value, Equals, "") - // Value containing = - err = ap.UnmarshalFlag("key=value=more") - c.Assert(err, IsNil) - c.Check(ap.Key, Equals, "key") - c.Check(ap.Value, Equals, "value=more") - // Malformed format - err = ap.UnmarshalFlag("malformed") - c.Assert(err, ErrorMatches, `invalid attribute: "malformed" \(want key=value\)`) - c.Check(ap.Key, Equals, "") - c.Check(ap.Value, Equals, "") -} - -func (s *AttributePairSuite) TestAttributePairSliceToMap(c *C) { - attrs := []AttributePair{ - {Key: "key1", Value: "value1"}, - {Key: "key2", Value: "value2"}, - } - m := AttributePairSliceToMap(attrs) - c.Check(m, DeepEquals, map[string]string{ - "key1": "value1", - "key2": "value2", - }) -} - type SnapAndNameSuite struct{} var _ = Suite(&SnapAndNameSuite{}) diff -Nru snapd-2.32.3.2~14.04/cmd/snap/last.go snapd-2.37~rc1~14.04/cmd/snap/last.go --- snapd-2.32.3.2~14.04/cmd/snap/last.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/last.go 2019-01-16 08:36:51.000000000 +0000 @@ -20,6 +20,7 @@ package main import ( + "errors" "fmt" "github.com/snapcore/snapd/client" @@ -27,24 +28,29 @@ ) type changeIDMixin struct { + clientMixin LastChangeType string `long:"last"` Positional struct { ID changeID `positional-arg-name:""` } `positional-args:"yes"` } -var changeIDMixinOptDesc = map[string]string{ - "last": i18n.G("Select last change of given type (install, refresh, remove, try, auto-refresh etc.)"), +var changeIDMixinOptDesc = mixinDescs{ + // TRANSLATORS: This should not start with a lowercase letter. + "last": i18n.G("Select last change of given type (install, refresh, remove, try, auto-refresh, etc.). A question mark at the end of the type means to do nothing (instead of returning an error) if no change of the given type is found. Note the question mark could need protecting from the shell."), } var changeIDMixinArgDesc = []argDesc{{ - // TRANSLATORS: This needs to be wrapped in <>s. + // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), - // TRANSLATORS: This should probably not start with a lowercase letter. + // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Change ID"), }} -func (l *changeIDMixin) GetChangeID(cli *client.Client) (string, error) { +// should not be user-visible, but keep it clear and polite because mistakes happen +var noChangeFoundOK = errors.New("no change found but that's ok") + +func (l *changeIDMixin) GetChangeID() (string, error) { if l.Positional.ID == "" && l.LastChangeType == "" { return "", fmt.Errorf(i18n.G("please provide change ID or type with --last=")) } @@ -57,20 +63,33 @@ return string(l.Positional.ID), nil } + cli := l.client + // note that at this point we know l.LastChangeType != "" kind := l.LastChangeType + optional := false + if l := len(kind) - 1; kind[l] == '?' { + optional = true + kind = kind[:l] + } // our internal change types use "-snap" postfix but let user skip it and use short form. if kind == "refresh" || kind == "install" || kind == "remove" || kind == "connect" || kind == "disconnect" || kind == "configure" || kind == "try" { kind += "-snap" } - changes, err := cli.Changes(&client.ChangesOptions{Selector: client.ChangesAll}) + changes, err := queryChanges(cli, &client.ChangesOptions{Selector: client.ChangesAll}) if err != nil { return "", err } if len(changes) == 0 { + if optional { + return "", noChangeFoundOK + } return "", fmt.Errorf(i18n.G("no changes found")) } chg := findLatestChangeByKind(changes, kind) if chg == nil { + if optional { + return "", noChangeFoundOK + } return "", fmt.Errorf(i18n.G("no changes of type %q found"), l.LastChangeType) } diff -Nru snapd-2.32.3.2~14.04/cmd/snap/main.go snapd-2.37~rc1~14.04/cmd/snap/main.go --- snapd-2.32.3.2~14.04/cmd/snap/main.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.37~rc1~14.04/cmd/snap/main.go 2019-01-16 08:36:51.000000000 +0000 @@ -22,8 +22,10 @@ import ( "fmt" "io" + "net/http" "os" "path/filepath" + "runtime" "strings" "unicode" "unicode/utf8" @@ -39,6 +41,7 @@ "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" ) @@ -88,6 +91,7 @@ optDescs map[string]string argDescs []argDesc alias string + extra func(*flags.Command) } // commands holds information about all non-debug commands. @@ -114,12 +118,14 @@ // addDebugCommand replaces parser.addCommand() in a way that is // compatible with re-constructing a pristine parser. It is meant for // adding debug commands. -func addDebugCommand(name, shortHelp, longHelp string, builder func() flags.Commander) *cmdInfo { +func addDebugCommand(name, shortHelp, longHelp string, builder func() flags.Commander, optDescs map[string]string, argDescs []argDesc) *cmdInfo { info := &cmdInfo{ name: name, shortHelp: shortHelp, longHelp: longHelp, builder: builder, + optDescs: optDescs, + argDescs: argDescs, } debugCommands = append(debugCommands, info) return info @@ -152,35 +158,84 @@ func lintArg(cmdName, optName, desc, origDesc string) { lintDesc(cmdName, optName, desc, origDesc) - if optName[0] != '<' || optName[len(optName)-1] != '>' { - noticef("argument %q's %q should be wrapped in <>s", cmdName, optName) + if len(optName) > 0 && optName[0] == '<' && optName[len(optName)-1] == '>' { + return + } + if len(optName) > 0 && optName[0] == '<' && strings.HasSuffix(optName, ">s") { + // see comment in fixupArg about the >s case + return } + noticef("argument %q's %q should begin with < and end with >", cmdName, optName) +} + +func fixupArg(optName string) string { + // Due to misunderstanding some localized versions of option name are + // literally "