diff -Nru snapd-2.39.2ubuntu0.2/asserts/assertstest/assertstest.go snapd-2.40/asserts/assertstest/assertstest.go --- snapd-2.39.2ubuntu0.2/asserts/assertstest/assertstest.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/asserts/assertstest/assertstest.go 2019-07-12 08:40:08.000000000 +0000 @@ -444,3 +444,167 @@ } } } + +// FakeAssertionWithBody builds a fake assertion with the given body +// and layered headers. A fake assertion cannot be verified or added +// to a database or properly encoded. It can still be useful for unit +// tests but shouldn't be used in integration tests. +func FakeAssertionWithBody(body []byte, headerLayers ...map[string]interface{}) asserts.Assertion { + headers := map[string]interface{}{ + "sign-key-sha3-384": "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", + } + for _, h := range headerLayers { + for k, v := range h { + headers[k] = v + } + } + + _, hasTimestamp := headers["timestamp"] + _, hasSince := headers["since"] + if !(hasTimestamp || hasSince) { + headers["timestamp"] = time.Now().Format(time.RFC3339) + } + + a, err := asserts.Assemble(headers, body, nil, []byte("AXNpZw==")) + if err != nil { + panic(fmt.Sprintf("cannot build fake assertion: %v", err)) + } + return a +} + +// FakeAssertion builds a fake assertion with given layered headers +// and an empty body. A fake assertion cannot be verified or added to +// a database or properly encoded. It can still be useful for unit +// tests but shouldn't be used in integration tests. +func FakeAssertion(headerLayers ...map[string]interface{}) asserts.Assertion { + return FakeAssertionWithBody(nil, headerLayers...) +} + +type accuDB interface { + Add(asserts.Assertion) error +} + +// AddMany conveniently adds the given assertions to the db. +// It is idempotent but otherwise panics on error. +func AddMany(db accuDB, assertions ...asserts.Assertion) { + for _, a := range assertions { + err := db.Add(a) + if _, ok := err.(*asserts.RevisionError); !ok { + if err != nil { + panic(fmt.Sprintf("cannot add test assertions: %v", err)) + } + } + } +} + +// SigningAccounts manages a set of brand or user accounts, +// with their keys that can sign models etc. +type SigningAccounts struct { + store *StoreStack + + signing map[string]*SigningDB + + accts map[string]*asserts.Account + acctKeys map[string]*asserts.AccountKey +} + +// NewSigningAccounts creates a new SigningAccounts instance. +func NewSigningAccounts(store *StoreStack) *SigningAccounts { + return &SigningAccounts{ + store: store, + signing: make(map[string]*SigningDB), + accts: make(map[string]*asserts.Account), + acctKeys: make(map[string]*asserts.AccountKey), + } +} + +func (sa *SigningAccounts) Register(accountID string, brandPrivKey asserts.PrivateKey, extra map[string]interface{}) *SigningDB { + brandSigning := NewSigningDB(accountID, brandPrivKey) + sa.signing[accountID] = brandSigning + + acctHeaders := map[string]interface{}{ + "account-id": accountID, + } + for k, v := range extra { + acctHeaders[k] = v + } + + brandAcct := NewAccount(sa.store, accountID, acctHeaders, "") + sa.accts[accountID] = brandAcct + + brandPubKey, err := brandSigning.PublicKey("") + if err != nil { + panic(err) + } + brandAcctKey := NewAccountKey(sa.store, brandAcct, nil, brandPubKey, "") + sa.acctKeys[accountID] = brandAcctKey + + return brandSigning +} + +func (sa *SigningAccounts) Account(accountID string) *asserts.Account { + if acct := sa.accts[accountID]; acct != nil { + return acct + } + panic(fmt.Sprintf("unknown test account-id: %s", accountID)) +} + +func (sa *SigningAccounts) AccountKey(accountID string) *asserts.AccountKey { + if acctKey := sa.acctKeys[accountID]; acctKey != nil { + return acctKey + } + panic(fmt.Sprintf("unknown test account-id: %s", accountID)) +} + +func (sa *SigningAccounts) PublicKey(accountID string) asserts.PublicKey { + pubKey, err := sa.Signing(accountID).PublicKey("") + if err != nil { + panic(err) + } + return pubKey +} + +func (sa *SigningAccounts) Signing(accountID string) *SigningDB { + // convenience + if accountID == sa.store.RootSigning.AuthorityID { + return sa.store.RootSigning + } + if signer := sa.signing[accountID]; signer != nil { + return signer + } + panic(fmt.Sprintf("unknown test account-id: %s", accountID)) +} + +// Model creates a new model for accountID. accountID can also be the account-id of the underlying store stack. +func (sa *SigningAccounts) Model(accountID, model string, extras ...map[string]interface{}) *asserts.Model { + headers := map[string]interface{}{ + "series": "16", + "brand-id": accountID, + "model": model, + "timestamp": time.Now().Format(time.RFC3339), + } + for _, extra := range extras { + for k, v := range extra { + headers[k] = v + } + } + + signer := sa.Signing(accountID) + + modelAs, err := signer.Sign(asserts.ModelType, headers, nil, "") + if err != nil { + panic(err) + } + return modelAs.(*asserts.Model) +} + +// AccountsAndKeys returns the account and account-key for each given +// accountID in that order. +func (sa *SigningAccounts) AccountsAndKeys(accountIDs ...string) []asserts.Assertion { + res := make([]asserts.Assertion, 0, 2*len(accountIDs)) + for _, accountID := range accountIDs { + res = append(res, sa.Account(accountID)) + res = append(res, sa.AccountKey(accountID)) + } + return res +} diff -Nru snapd-2.39.2ubuntu0.2/asserts/assertstest/assertstest_test.go snapd-2.40/asserts/assertstest/assertstest_test.go --- snapd-2.39.2ubuntu0.2/asserts/assertstest/assertstest_test.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/asserts/assertstest/assertstest_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -161,3 +161,65 @@ err = db.Add(store.GenericKey) c.Assert(err, IsNil) } + +func (s *helperSuite) TestSigningAccounts(c *C) { + brandKey, _ := assertstest.GenerateKey(752) + + store := assertstest.NewStoreStack("super", nil) + + sa := assertstest.NewSigningAccounts(store) + sa.Register("my-brand", brandKey, map[string]interface{}{ + "validation": "verified", + }) + + acct := sa.Account("my-brand") + c.Check(acct.Username(), Equals, "my-brand") + c.Check(acct.Validation(), Equals, "verified") + + c.Check(sa.AccountKey("my-brand").PublicKeyID(), Equals, brandKey.PublicKey().ID()) + + c.Check(sa.PublicKey("my-brand").ID(), Equals, brandKey.PublicKey().ID()) + + model := sa.Model("my-brand", "my-model", map[string]interface{}{ + "classic": "true", + }) + c.Check(model.BrandID(), Equals, "my-brand") + c.Check(model.Model(), Equals, "my-model") + c.Check(model.Classic(), Equals, true) + + // can also sign models for store account-id + model = sa.Model("super", "pc", map[string]interface{}{ + "classic": "true", + }) + c.Check(model.BrandID(), Equals, "super") + c.Check(model.Model(), Equals, "pc") +} + +func (s *helperSuite) TestSigningAccountsAccountsAndKeysPlusAddMany(c *C) { + brandKey, _ := assertstest.GenerateKey(752) + + store := assertstest.NewStoreStack("super", nil) + + sa := assertstest.NewSigningAccounts(store) + sa.Register("my-brand", brandKey, map[string]interface{}{ + "validation": "verified", + }) + + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: store.Trusted, + }) + c.Assert(err, IsNil) + err = db.Add(store.StoreAccountKey("")) + c.Assert(err, IsNil) + + assertstest.AddMany(db, sa.AccountsAndKeys("my-brand")...) + as, err := db.FindMany(asserts.AccountKeyType, map[string]string{ + "account-id": "my-brand", + }) + c.Check(err, IsNil) + c.Check(as, HasLen, 1) + + // idempotent + assertstest.AddMany(db, sa.AccountsAndKeys("my-brand")...) +} diff -Nru snapd-2.39.2ubuntu0.2/boot/boottest/mockbootloader.go snapd-2.40/boot/boottest/mockbootloader.go --- snapd-2.39.2ubuntu0.2/boot/boottest/mockbootloader.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/boot/boottest/mockbootloader.go 2019-07-12 08:40:08.000000000 +0000 @@ -20,6 +20,7 @@ package boottest import ( + "github.com/snapcore/snapd/snap" "path/filepath" ) @@ -32,6 +33,9 @@ name string bootdir string + + ExtractKernelAssetsCalls []*snap.Info + RemoveKernelAssetsCalls []snap.PlaceInfo } func NewMockBootloader(name, bootdir string) *MockBootloader { @@ -70,3 +74,13 @@ func (b *MockBootloader) ConfigFile() string { return filepath.Join(b.bootdir, "mockboot/mockboot.cfg") } + +func (b *MockBootloader) ExtractKernelAssets(s *snap.Info, snapf snap.Container) error { + b.ExtractKernelAssetsCalls = append(b.ExtractKernelAssetsCalls, s) + return nil +} + +func (b *MockBootloader) RemoveKernelAssets(s snap.PlaceInfo) error { + b.RemoveKernelAssetsCalls = append(b.RemoveKernelAssetsCalls, s) + return nil +} diff -Nru snapd-2.39.2ubuntu0.2/boot/kernel_os.go snapd-2.40/boot/kernel_os.go --- snapd-2.39.2ubuntu0.2/boot/kernel_os.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/boot/kernel_os.go 2019-07-12 08:40:08.000000000 +0000 @@ -21,7 +21,6 @@ import ( "fmt" - "os" "path/filepath" "github.com/snapcore/snapd/bootloader" @@ -33,69 +32,30 @@ // RemoveKernelAssets removes the unpacked kernel/initrd for the given // kernel snap. func RemoveKernelAssets(s snap.PlaceInfo) error { - loader, err := bootloader.Find() + bootloader, err := bootloader.Find() if err != nil { return fmt.Errorf("no not remove kernel assets: %s", err) } - // remove the kernel blob - blobName := filepath.Base(s.MountFile()) - dstDir := filepath.Join(loader.Dir(), blobName) - if err := os.RemoveAll(dstDir); err != nil { - return err - } - - return nil + // ask bootloader to remove the kernel assets if needed + return bootloader.RemoveKernelAssets(s) } // 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. func ExtractKernelAssets(s *snap.Info, snapf snap.Container) error { - if s.Type != snap.TypeKernel { - return fmt.Errorf("cannot extract kernel assets from snap type %q", s.Type) + if s.GetType() != snap.TypeKernel { + return fmt.Errorf("cannot extract kernel assets from snap type %q", s.GetType()) } - loader, err := bootloader.Find() + bootloader, err := bootloader.Find() if err != nil { return fmt.Errorf("cannot extract kernel assets: %s", err) } - // XXX: should we use "kernel.yaml" for this? - var forceKernelExtraction bool - if _, err := snapf.ReadFile("meta/force-kernel-extraction"); err == nil { - forceKernelExtraction = true - } - - if !forceKernelExtraction && loader.Name() == "grub" { - return nil - } - - // now do the kernel specific bits - blobName := filepath.Base(s.MountFile()) - dstDir := filepath.Join(loader.Dir(), blobName) - if err := os.MkdirAll(dstDir, 0755); err != nil { - return err - } - dir, err := os.Open(dstDir) - if err != nil { - return err - } - defer dir.Close() - - for _, src := range []string{"kernel.img", "initrd.img"} { - if err := snapf.Unpack(src, dstDir); err != nil { - return err - } - if err := dir.Sync(); err != nil { - return err - } - } - if err := snapf.Unpack("dtbs/*", dstDir); err != nil { - return err - } - - return dir.Sync() + // ask bootloader to extract the kernel assets if needed + return bootloader.ExtractKernelAssets(s, snapf) } // SetNextBoot will schedule the given OS or base or kernel snap to be @@ -106,8 +66,8 @@ return fmt.Errorf("cannot set next boot on classic systems") } - 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) + if s.GetType() != snap.TypeOS && s.GetType() != snap.TypeKernel && s.GetType() != snap.TypeBase { + return fmt.Errorf("cannot set next boot to snap %q with type %q", s.SnapName(), s.GetType()) } bootloader, err := bootloader.Find() @@ -116,7 +76,7 @@ } var nextBoot, goodBoot string - switch s.Type { + switch s.GetType() { case snap.TypeOS, snap.TypeBase: nextBoot = "snap_try_core" goodBoot = "snap_core" @@ -156,7 +116,7 @@ // 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 { + if s.GetType() != snap.TypeKernel && s.GetType() != snap.TypeOS && s.GetType() != snap.TypeBase { return false } @@ -167,7 +127,7 @@ } var nextBoot, goodBoot string - switch s.Type { + switch s.GetType() { case snap.TypeKernel: nextBoot = "snap_try_kernel" goodBoot = "snap_kernel" diff -Nru snapd-2.39.2ubuntu0.2/boot/kernel_os_test.go snapd-2.40/boot/kernel_os_test.go --- snapd-2.39.2ubuntu0.2/boot/kernel_os_test.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/boot/kernel_os_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -20,6 +20,7 @@ package boot_test import ( + "io/ioutil" "path/filepath" "testing" @@ -38,27 +39,6 @@ func TestBoot(t *testing.T) { TestingT(t) } -type kernelOSSuite struct { - testutil.BaseTest - bootloader *boottest.MockBootloader -} - -var _ = Suite(&kernelOSSuite{}) - -func (s *kernelOSSuite) SetUpTest(c *C) { - s.BaseTest.SetUpTest(c) - s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) - dirs.SetRootDir(c.MkDir()) - s.bootloader = boottest.NewMockBootloader("mock", c.MkDir()) - bootloader.Force(s.bootloader) -} - -func (s *kernelOSSuite) TearDownTest(c *C) { - s.BaseTest.TearDownTest(c) - dirs.SetRootDir("") - bootloader.Force(nil) -} - const packageKernel = ` name: ubuntu-kernel version: 4.0-1 @@ -66,123 +46,43 @@ vendor: Someone ` -func (s *kernelOSSuite) TestExtractKernelAssetsAndRemove(c *C) { - files := [][]string{ - {"kernel.img", "I'm a kernel"}, - {"initrd.img", "...and I'm an initrd"}, - {"dtbs/foo.dtb", "g'day, I'm foo.dtb"}, - {"dtbs/bar.dtb", "hello, I'm bar.dtb"}, - // must be last - {"meta/kernel.yaml", "version: 4.2"}, - } - - si := &snap.SideInfo{ - RealName: "ubuntu-kernel", - Revision: snap.R(42), - } - fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) - snapf, err := snap.Open(fn) - c.Assert(err, IsNil) - - info, err := snap.ReadInfoFromSnapFile(snapf, si) - c.Assert(err, IsNil) - - err = boot.ExtractKernelAssets(info, snapf) - c.Assert(err, IsNil) - - // this is where the kernel/initrd is unpacked - bootdir := s.bootloader.Dir() - - kernelAssetsDir := filepath.Join(bootdir, "ubuntu-kernel_42.snap") - - for _, def := range files { - if def[0] == "meta/kernel.yaml" { - break - } - - fullFn := filepath.Join(kernelAssetsDir, def[0]) - c.Check(fullFn, testutil.FileEquals, def[1]) - } - - // remove - err = boot.RemoveKernelAssets(info) - c.Assert(err, IsNil) - - c.Check(osutil.FileExists(kernelAssetsDir), Equals, false) +// baseKernelOSSuite is used to setup the common test environment +type baseKernelOSSuite struct { + testutil.BaseTest } -func (s *kernelOSSuite) TestExtractKernelAssetsNoUnpacksKernelForGrub(c *C) { - // pretend to be a grub system - mockGrub := boottest.NewMockBootloader("grub", c.MkDir()) - bootloader.Force(mockGrub) - - files := [][]string{ - {"kernel.img", "I'm a kernel"}, - {"initrd.img", "...and I'm an initrd"}, - {"meta/kernel.yaml", "version: 4.2"}, - } - si := &snap.SideInfo{ - RealName: "ubuntu-kernel", - Revision: snap.R(42), - } - fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) - snapf, err := snap.Open(fn) - c.Assert(err, IsNil) - - info, err := snap.ReadInfoFromSnapFile(snapf, si) - c.Assert(err, IsNil) - - err = boot.ExtractKernelAssets(info, snapf) - c.Assert(err, IsNil) +func (s *baseKernelOSSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) - // kernel is *not* here - kernimg := filepath.Join(mockGrub.Dir(), "ubuntu-kernel_42.snap", "kernel.img") - c.Assert(osutil.FileExists(kernimg), Equals, false) + dirs.SetRootDir(c.MkDir()) + s.AddCleanup(func() { dirs.SetRootDir("") }) + restore := snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}) + s.AddCleanup(restore) + restore = release.MockOnClassic(false) + s.AddCleanup(restore) } -func (s *kernelOSSuite) TestExtractKernelForceWorks(c *C) { - // pretend to be a grub system - mockGrub := boottest.NewMockBootloader("grub", c.MkDir()) - bootloader.Force(mockGrub) - - files := [][]string{ - {"kernel.img", "I'm a kernel"}, - {"initrd.img", "...and I'm an initrd"}, - {"meta/force-kernel-extraction", ""}, - {"meta/kernel.yaml", "version: 4.2"}, - } - si := &snap.SideInfo{ - RealName: "ubuntu-kernel", - Revision: snap.R(42), - } - fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) - snapf, err := snap.Open(fn) - c.Assert(err, IsNil) +// kernelOSSuite tests the abstract bootloader behaviour including +// bootenv setting, error handling etc +type kernelOSSuite struct { + baseKernelOSSuite - info, err := snap.ReadInfoFromSnapFile(snapf, si) - c.Assert(err, IsNil) + loader *boottest.MockBootloader +} - err = boot.ExtractKernelAssets(info, snapf) - c.Assert(err, IsNil) +var _ = Suite(&kernelOSSuite{}) - // kernel is extracted - kernimg := filepath.Join(mockGrub.Dir(), "ubuntu-kernel_42.snap", "kernel.img") - c.Assert(osutil.FileExists(kernimg), Equals, true) - // initrd - initrdimg := filepath.Join(mockGrub.Dir(), "ubuntu-kernel_42.snap", "initrd.img") - c.Assert(osutil.FileExists(initrdimg), Equals, true) +func (s *kernelOSSuite) SetUpTest(c *C) { + s.baseKernelOSSuite.SetUpTest(c) - // ensure that removal of assets also works - err = boot.RemoveKernelAssets(info) - c.Assert(err, IsNil) - exists, _, err := osutil.DirExists(filepath.Dir(kernimg)) - c.Assert(err, IsNil) - c.Check(exists, Equals, false) + s.loader = boottest.NewMockBootloader("mock", c.MkDir()) + bootloader.Force(s.loader) + s.AddCleanup(func() { bootloader.Force(nil) }) } func (s *kernelOSSuite) TestExtractKernelAssetsError(c *C) { info := &snap.Info{} - info.Type = snap.TypeApp + info.SnapType = snap.TypeApp err := boot.ExtractKernelAssets(info, nil) c.Assert(err, ErrorMatches, `cannot extract kernel assets from snap type "app"`) @@ -198,22 +98,21 @@ err := boot.SetNextBoot(snapInfo) c.Assert(err, ErrorMatches, "cannot set next boot on classic systems") - c.Assert(s.bootloader.BootVars, HasLen, 0) + c.Assert(s.loader.BootVars, HasLen, 0) } func (s *kernelOSSuite) TestSetNextBootForCore(c *C) { - restore := release.MockOnClassic(false) - defer restore() - info := &snap.Info{} - info.Type = snap.TypeOS + info.SnapType = snap.TypeOS info.RealName = "core" info.Revision = snap.R(100) err := boot.SetNextBoot(info) c.Assert(err, IsNil) - c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ + v, err := s.loader.GetBootVars("snap_try_core", "snap_mode") + c.Assert(err, IsNil) + c.Assert(v, DeepEquals, map[string]string{ "snap_try_core": "core_100.snap", "snap_mode": "try", }) @@ -222,18 +121,17 @@ } func (s *kernelOSSuite) TestSetNextBootWithBaseForCore(c *C) { - restore := release.MockOnClassic(false) - defer restore() - info := &snap.Info{} - info.Type = snap.TypeBase + info.SnapType = 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{ + v, err := s.loader.GetBootVars("snap_try_core", "snap_mode") + c.Assert(err, IsNil) + c.Assert(v, DeepEquals, map[string]string{ "snap_try_core": "core18_1818.snap", "snap_mode": "try", }) @@ -242,67 +140,70 @@ } func (s *kernelOSSuite) TestSetNextBootForKernel(c *C) { - restore := release.MockOnClassic(false) - defer restore() - info := &snap.Info{} - info.Type = snap.TypeKernel + info.SnapType = snap.TypeKernel info.RealName = "krnl" info.Revision = snap.R(42) err := boot.SetNextBoot(info) c.Assert(err, IsNil) - c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ + v, err := s.loader.GetBootVars("snap_try_kernel", "snap_mode") + c.Assert(err, IsNil) + c.Assert(v, DeepEquals, map[string]string{ "snap_try_kernel": "krnl_42.snap", "snap_mode": "try", }) - s.bootloader.BootVars["snap_kernel"] = "krnl_40.snap" - s.bootloader.BootVars["snap_try_kernel"] = "krnl_42.snap" + bootVars := map[string]string{ + "snap_kernel": "krnl_40.snap", + "snap_try_kernel": "krnl_42.snap"} + s.loader.SetBootVars(bootVars) c.Check(boot.ChangeRequiresReboot(info), Equals, true) // simulate good boot - s.bootloader.BootVars["snap_kernel"] = "krnl_42.snap" + bootVars = map[string]string{"snap_kernel": "krnl_42.snap"} + s.loader.SetBootVars(bootVars) c.Check(boot.ChangeRequiresReboot(info), Equals, false) } func (s *kernelOSSuite) TestSetNextBootForKernelForTheSameKernel(c *C) { - restore := release.MockOnClassic(false) - defer restore() - info := &snap.Info{} - info.Type = snap.TypeKernel + info.SnapType = snap.TypeKernel info.RealName = "krnl" info.Revision = snap.R(40) - s.bootloader.BootVars["snap_kernel"] = "krnl_40.snap" + bootVars := map[string]string{"snap_kernel": "krnl_40.snap"} + s.loader.SetBootVars(bootVars) err := boot.SetNextBoot(info) c.Assert(err, IsNil) - c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ + v, err := s.loader.GetBootVars("snap_kernel") + c.Assert(err, IsNil) + c.Assert(v, DeepEquals, map[string]string{ "snap_kernel": "krnl_40.snap", }) } func (s *kernelOSSuite) TestSetNextBootForKernelForTheSameKernelTryMode(c *C) { - restore := release.MockOnClassic(false) - defer restore() - info := &snap.Info{} - info.Type = snap.TypeKernel + info.SnapType = 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" + bootVars := map[string]string{ + "snap_kernel": "krnl_40.snap", + "snap_try_kernel": "krnl_99.snap", + "snap_mode": "try"} + s.loader.SetBootVars(bootVars) err := boot.SetNextBoot(info) c.Assert(err, IsNil) - c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ + v, err := s.loader.GetBootVars("snap_kernel", "snap_try_kernel", "snap_mode") + c.Assert(err, IsNil) + c.Assert(v, DeepEquals, map[string]string{ "snap_kernel": "krnl_40.snap", "snap_try_kernel": "", "snap_mode": "", @@ -330,7 +231,192 @@ {"snap_kernel", "kernel_111.snap", "kernel", snap.R(1), false}, {"snap_try_kernel", "kernel_111.snap", "kernel", snap.R(1), false}, } { - s.bootloader.BootVars[t.bootVarKey] = t.bootVarValue + s.loader.BootVars[t.bootVarKey] = t.bootVarValue c.Assert(boot.InUse(t.snapName, t.snapRev), Equals, t.inUse, Commentf("unexpected result: %s %s %v", t.snapName, t.snapRev, t.inUse)) } } + +// ubootKernelOSSuite tests the uboot specific code in the bootloader handling +type ubootKernelOSSuite struct { + baseKernelOSSuite +} + +var _ = Suite(&ubootKernelOSSuite{}) + +func (s *ubootKernelOSSuite) forceUbootBootloader(c *C) bootloader.Bootloader { + mockGadgetDir := c.MkDir() + err := ioutil.WriteFile(filepath.Join(mockGadgetDir, "uboot.conf"), nil, 0644) + c.Assert(err, IsNil) + err = bootloader.InstallBootConfig(mockGadgetDir) + c.Assert(err, IsNil) + + loader, err := bootloader.Find() + c.Assert(err, IsNil) + c.Check(loader, NotNil) + bootloader.Force(loader) + s.AddCleanup(func() { bootloader.Force(nil) }) + + fn := filepath.Join(dirs.GlobalRootDir, "/boot/uboot/uboot.env") + c.Assert(osutil.FileExists(fn), Equals, true) + return loader +} + +func (s *ubootKernelOSSuite) TestExtractKernelAssetsAndRemoveOnUboot(c *C) { + loader := s.forceUbootBootloader(c) + c.Assert(loader, NotNil) + + files := [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"dtbs/foo.dtb", "g'day, I'm foo.dtb"}, + {"dtbs/bar.dtb", "hello, I'm bar.dtb"}, + // must be last + {"meta/kernel.yaml", "version: 4.2"}, + } + + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(42), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snap.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + err = boot.ExtractKernelAssets(info, snapf) + c.Assert(err, IsNil) + + // this is where the kernel/initrd is unpacked + bootdir := loader.Dir() + kernelAssetsDir := filepath.Join(bootdir, "ubuntu-kernel_42.snap") + for _, def := range files { + if def[0] == "meta/kernel.yaml" { + break + } + + fullFn := filepath.Join(kernelAssetsDir, def[0]) + c.Check(fullFn, testutil.FileEquals, def[1]) + } + + // it's idempotent + err = boot.ExtractKernelAssets(info, snapf) + c.Assert(err, IsNil) + + // remove + err = boot.RemoveKernelAssets(info) + c.Assert(err, IsNil) + c.Check(osutil.FileExists(kernelAssetsDir), Equals, false) + + // it's idempotent + err = boot.RemoveKernelAssets(info) + c.Assert(err, IsNil) +} + +// grubKernelOSSuite tests the GRUB specific code in the bootloader handling +type grubKernelOSSuite struct { + baseKernelOSSuite +} + +var _ = Suite(&grubKernelOSSuite{}) + +func (s *grubKernelOSSuite) forceGrubBootloader(c *C) bootloader.Bootloader { + // make mock grub bootenv dir + mockGadgetDir := c.MkDir() + err := ioutil.WriteFile(filepath.Join(mockGadgetDir, "grub.conf"), nil, 0644) + c.Assert(err, IsNil) + err = bootloader.InstallBootConfig(mockGadgetDir) + c.Assert(err, IsNil) + + loader, err := bootloader.Find() + c.Assert(err, IsNil) + c.Check(loader, NotNil) + loader.SetBootVars(map[string]string{ + "snap_kernel": "kernel_41.snap", + "snap_core": "core_21.snap", + }) + bootloader.Force(loader) + s.AddCleanup(func() { bootloader.Force(nil) }) + + fn := filepath.Join(dirs.GlobalRootDir, "/boot/grub/grub.cfg") + c.Assert(osutil.FileExists(fn), Equals, true) + return loader +} + +func (s *grubKernelOSSuite) TestExtractKernelAssetsNoUnpacksKernelForGrub(c *C) { + loader := s.forceGrubBootloader(c) + + files := [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"meta/kernel.yaml", "version: 4.2"}, + } + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(42), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snap.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + err = boot.ExtractKernelAssets(info, snapf) + c.Assert(err, IsNil) + + // kernel is *not* here + kernimg := filepath.Join(loader.Dir(), "ubuntu-kernel_42.snap", "kernel.img") + c.Assert(osutil.FileExists(kernimg), Equals, false) + + // it's idempotent + err = boot.ExtractKernelAssets(info, snapf) + c.Assert(err, IsNil) +} + +func (s *grubKernelOSSuite) TestExtractKernelForceWorks(c *C) { + loader := s.forceGrubBootloader(c) + + files := [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"meta/force-kernel-extraction", ""}, + {"meta/kernel.yaml", "version: 4.2"}, + } + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(42), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snap.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + err = boot.ExtractKernelAssets(info, snapf) + c.Assert(err, IsNil) + + // kernel is extracted + kernimg := filepath.Join(loader.Dir(), "ubuntu-kernel_42.snap", "kernel.img") + c.Assert(osutil.FileExists(kernimg), Equals, true) + // initrd + initrdimg := filepath.Join(loader.Dir(), "ubuntu-kernel_42.snap", "initrd.img") + c.Assert(osutil.FileExists(initrdimg), Equals, true) + + // it's idempotent + err = boot.ExtractKernelAssets(info, snapf) + c.Assert(err, IsNil) + + // ensure that removal of assets also works + err = boot.RemoveKernelAssets(info) + c.Assert(err, IsNil) + exists, _, err := osutil.DirExists(filepath.Dir(kernimg)) + c.Assert(err, IsNil) + c.Check(exists, Equals, false) + + // it's idempotent + err = boot.RemoveKernelAssets(info) + c.Assert(err, IsNil) +} diff -Nru snapd-2.39.2ubuntu0.2/bootloader/androidboot.go snapd-2.40/bootloader/androidboot.go --- snapd-2.39.2ubuntu0.2/bootloader/androidboot.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/bootloader/androidboot.go 2019-07-12 08:40:08.000000000 +0000 @@ -26,6 +26,7 @@ "github.com/snapcore/snapd/bootloader/androidbootenv" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" ) type androidboot struct{} @@ -75,3 +76,12 @@ } return env.Save() } + +func (a *androidboot) ExtractKernelAssets(s *snap.Info, snapf snap.Container) error { + return nil + +} + +func (a *androidboot) RemoveKernelAssets(s snap.PlaceInfo) error { + return nil +} diff -Nru snapd-2.39.2ubuntu0.2/bootloader/androidboot_test.go snapd-2.40/bootloader/androidboot_test.go --- snapd-2.39.2ubuntu0.2/bootloader/androidboot_test.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/bootloader/androidboot_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -20,28 +20,30 @@ package bootloader_test import ( + "path/filepath" + . "gopkg.in/check.v1" "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" ) type androidBootTestSuite struct { + baseBootenvTestSuite } var _ = Suite(&androidBootTestSuite{}) -func (g *androidBootTestSuite) SetUpTest(c *C) { - dirs.SetRootDir(c.MkDir()) +func (s *androidBootTestSuite) SetUpTest(c *C) { + s.baseBootenvTestSuite.SetUpTest(c) // the file needs to exist for androidboot object to be created bootloader.MockAndroidBootFile(c, 0644) } -func (g *androidBootTestSuite) TearDownTest(c *C) { - dirs.SetRootDir("") -} - func (s *androidBootTestSuite) TestNewAndroidbootNoAndroidbootReturnsNil(c *C) { dirs.GlobalRootDir = "/something/not/there" a := bootloader.NewAndroidBoot() @@ -63,3 +65,32 @@ c.Check(v, HasLen, 1) c.Check(v["snap_mode"], Equals, "try") } + +func (s *androidBootTestSuite) TestExtractKernelAssetsNoUnpacksKernel(c *C) { + a := bootloader.NewAndroidBoot() + + c.Assert(a, NotNil) + + files := [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"meta/kernel.yaml", "version: 4.2"}, + } + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(42), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snap.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + err = a.ExtractKernelAssets(info, snapf) + c.Assert(err, IsNil) + + // kernel is *not* here + kernimg := filepath.Join(a.Dir(), "ubuntu-kernel_42.snap", "kernel.img") + c.Assert(osutil.FileExists(kernimg), Equals, false) +} diff -Nru snapd-2.39.2ubuntu0.2/bootloader/bootloader.go snapd-2.40/bootloader/bootloader.go --- snapd-2.39.2ubuntu0.2/bootloader/bootloader.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/bootloader/bootloader.go 2019-07-12 08:40:08.000000000 +0000 @@ -26,6 +26,7 @@ "path/filepath" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" ) const ( @@ -62,6 +63,12 @@ // ConfigFile returns the name of the config file ConfigFile() string + + // ExtractKernelAssets extracts kernel assets from the given kernel snap + ExtractKernelAssets(s *snap.Info, snapf snap.Container) error + + // RemoveKernelAssets removes the assets for the given kernel snap. + RemoveKernelAssets(s snap.PlaceInfo) error } // InstallBootConfig installs the bootloader config from the gadget @@ -164,3 +171,42 @@ return bootloader.SetBootVars(m) } + +func extractKernelAssetsToBootDir(bootDir string, s *snap.Info, snapf snap.Container) error { + // now do the kernel specific bits + blobName := filepath.Base(s.MountFile()) + dstDir := filepath.Join(bootDir, blobName) + if err := os.MkdirAll(dstDir, 0755); err != nil { + return err + } + dir, err := os.Open(dstDir) + if err != nil { + return err + } + defer dir.Close() + + for _, src := range []string{"kernel.img", "initrd.img"} { + if err := snapf.Unpack(src, dstDir); err != nil { + return err + } + if err := dir.Sync(); err != nil { + return err + } + } + if err := snapf.Unpack("dtbs/*", dstDir); err != nil { + return err + } + + return dir.Sync() +} + +func removeKernelAssetsFromBootDir(bootDir string, s snap.PlaceInfo) error { + // remove the kernel blob + blobName := filepath.Base(s.MountFile()) + dstDir := filepath.Join(bootDir, blobName) + if err := os.RemoveAll(dstDir); err != nil { + return err + } + + return nil +} diff -Nru snapd-2.39.2ubuntu0.2/bootloader/bootloader_test.go snapd-2.40/bootloader/bootloader_test.go --- snapd-2.39.2ubuntu0.2/bootloader/bootloader_test.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/bootloader/bootloader_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -17,86 +17,72 @@ * */ -package bootloader +package bootloader_test import ( "io/ioutil" - "os" "path/filepath" "testing" . "gopkg.in/check.v1" + "github.com/snapcore/snapd/boot/boottest" + "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" ) // Hook up check.v1 into the "go test" runner func Test(t *testing.T) { TestingT(t) } -// partition specific testsuite -type PartitionTestSuite struct { +const packageKernel = ` +name: ubuntu-kernel +version: 4.0-1 +type: kernel +vendor: Someone +` + +type baseBootenvTestSuite struct { + testutil.BaseTest } -var _ = Suite(&PartitionTestSuite{}) - -type mockBootloader struct { - bootVars map[string]string +func (s *baseBootenvTestSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + s.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) + dirs.SetRootDir(c.MkDir()) + s.AddCleanup(func() { dirs.SetRootDir("") }) } -func newMockBootloader() *mockBootloader { - return &mockBootloader{ - bootVars: make(map[string]string), - } -} -func (b *mockBootloader) Name() string { - return "mocky" -} -func (b *mockBootloader) Dir() string { - return "/boot/mocky" -} -func (b *mockBootloader) GetBootVars(names ...string) (map[string]string, error) { - out := map[string]string{} - for _, name := range names { - out[name] = b.bootVars[name] - } +type bootenvTestSuite struct { + baseBootenvTestSuite - return out, nil -} -func (b *mockBootloader) SetBootVars(values map[string]string) error { - for k, v := range values { - b.bootVars[k] = v - } - return nil -} -func (b *mockBootloader) ConfigFile() string { - return "/boot/mocky/mocky.env" + b *boottest.MockBootloader } -func (s *PartitionTestSuite) SetUpTest(c *C) { - dirs.SetRootDir(c.MkDir()) - err := os.MkdirAll((&grub{}).Dir(), 0755) - c.Assert(err, IsNil) - err = os.MkdirAll((&uboot{}).Dir(), 0755) - c.Assert(err, IsNil) +var _ = Suite(&bootenvTestSuite{}) + +func (s *bootenvTestSuite) SetUpTest(c *C) { + s.baseBootenvTestSuite.SetUpTest(c) + + s.b = boottest.NewMockBootloader("mocky", c.MkDir()) } -func (s *PartitionTestSuite) TestForceBootloader(c *C) { - b := newMockBootloader() - Force(b) - defer Force(nil) +func (s *bootenvTestSuite) TestForceBootloader(c *C) { + bootloader.Force(s.b) + defer bootloader.Force(nil) - got, err := Find() + got, err := bootloader.Find() c.Assert(err, IsNil) - c.Check(got, Equals, b) + c.Check(got, Equals, s.b) } -func (s *PartitionTestSuite) TestMarkBootSuccessfulAllSnap(c *C) { - b := newMockBootloader() - b.bootVars["snap_mode"] = "trying" - b.bootVars["snap_try_core"] = "os1" - b.bootVars["snap_try_kernel"] = "k1" - err := MarkBootSuccessful(b) +func (s *bootenvTestSuite) TestMarkBootSuccessfulAllSnap(c *C) { + s.b.BootVars["snap_mode"] = "trying" + s.b.BootVars["snap_try_core"] = "os1" + s.b.BootVars["snap_try_kernel"] = "k1" + err := bootloader.MarkBootSuccessful(s.b) c.Assert(err, IsNil) expected := map[string]string{ @@ -108,24 +94,23 @@ "snap_kernel": "k1", "snap_core": "os1", } - c.Assert(b.bootVars, DeepEquals, expected) + c.Assert(s.b.BootVars, DeepEquals, expected) // do it again, verify its still valid - err = MarkBootSuccessful(b) + err = bootloader.MarkBootSuccessful(s.b) c.Assert(err, IsNil) - c.Assert(b.bootVars, DeepEquals, expected) + c.Assert(s.b.BootVars, DeepEquals, expected) } -func (s *PartitionTestSuite) TestMarkBootSuccessfulKKernelUpdate(c *C) { - b := newMockBootloader() - b.bootVars["snap_mode"] = "trying" - b.bootVars["snap_core"] = "os1" - b.bootVars["snap_kernel"] = "k1" - b.bootVars["snap_try_core"] = "" - b.bootVars["snap_try_kernel"] = "k2" - err := MarkBootSuccessful(b) +func (s *bootenvTestSuite) TestMarkBootSuccessfulKKernelUpdate(c *C) { + s.b.BootVars["snap_mode"] = "trying" + s.b.BootVars["snap_core"] = "os1" + s.b.BootVars["snap_kernel"] = "k1" + s.b.BootVars["snap_try_core"] = "" + s.b.BootVars["snap_try_kernel"] = "k2" + err := bootloader.MarkBootSuccessful(s.b) c.Assert(err, IsNil) - c.Assert(b.bootVars, DeepEquals, map[string]string{ + c.Assert(s.b.BootVars, DeepEquals, map[string]string{ // cleared "snap_mode": "", "snap_try_kernel": "", @@ -137,12 +122,12 @@ }) } -func (s *PartitionTestSuite) TestInstallBootloaderConfigNoConfig(c *C) { - err := InstallBootConfig(c.MkDir()) +func (s *bootenvTestSuite) TestInstallBootloaderConfigNoConfig(c *C) { + err := bootloader.InstallBootConfig(c.MkDir()) c.Assert(err, ErrorMatches, `cannot find boot config in.*`) } -func (s *PartitionTestSuite) TestInstallBootloaderConfig(c *C) { +func (s *bootenvTestSuite) TestInstallBootloaderConfig(c *C) { for _, t := range []struct{ gadgetFile, systemFile string }{ {"grub.conf", "/boot/grub/grub.cfg"}, {"uboot.conf", "/boot/uboot/uboot.env"}, @@ -151,7 +136,7 @@ mockGadgetDir := c.MkDir() err := ioutil.WriteFile(filepath.Join(mockGadgetDir, t.gadgetFile), nil, 0644) c.Assert(err, IsNil) - err = InstallBootConfig(mockGadgetDir) + err = bootloader.InstallBootConfig(mockGadgetDir) c.Assert(err, IsNil) fn := filepath.Join(dirs.GlobalRootDir, t.systemFile) c.Assert(osutil.FileExists(fn), Equals, true) diff -Nru snapd-2.39.2ubuntu0.2/bootloader/export_test.go snapd-2.40/bootloader/export_test.go --- snapd-2.39.2ubuntu0.2/bootloader/export_test.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/bootloader/export_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -24,6 +24,8 @@ "os" . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/bootloader/ubootenv" ) // creates a new Androidboot bootloader object @@ -38,3 +40,31 @@ err = ioutil.WriteFile(f.ConfigFile(), nil, mode) c.Assert(err, IsNil) } + +func NewUboot() Bootloader { + return newUboot() +} + +func MockUbootFiles(c *C) { + u := &uboot{} + err := os.MkdirAll(u.Dir(), 0755) + c.Assert(err, IsNil) + + // ensure that we have a valid uboot.env too + env, err := ubootenv.Create(u.envFile(), 4096) + c.Assert(err, IsNil) + err = env.Save() + c.Assert(err, IsNil) +} + +func NewGrub() Bootloader { + return newGrub() +} + +func MockGrubFiles(c *C) { + g := &grub{} + err := os.MkdirAll(g.Dir(), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(g.ConfigFile(), nil, 0644) + c.Assert(err, IsNil) +} diff -Nru snapd-2.39.2ubuntu0.2/bootloader/grub.go snapd-2.40/bootloader/grub.go --- snapd-2.39.2ubuntu0.2/bootloader/grub.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/bootloader/grub.go 2019-07-12 08:40:08.000000000 +0000 @@ -26,6 +26,7 @@ "github.com/snapcore/snapd/bootloader/grubenv" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" ) type grub struct{} @@ -81,3 +82,15 @@ } return env.Save() } + +func (g *grub) ExtractKernelAssets(s *snap.Info, snapf snap.Container) error { + // XXX: should we use "kernel.yaml" for this? + if _, err := snapf.ReadFile("meta/force-kernel-extraction"); err == nil { + return extractKernelAssetsToBootDir(g.Dir(), s, snapf) + } + return nil +} + +func (g *grub) RemoveKernelAssets(s snap.PlaceInfo) error { + return removeKernelAssetsFromBootDir(g.Dir(), s) +} diff -Nru snapd-2.39.2ubuntu0.2/bootloader/grub_test.go snapd-2.40/bootloader/grub_test.go --- snapd-2.39.2ubuntu0.2/bootloader/grub_test.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/bootloader/grub_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -17,21 +17,34 @@ * */ -package bootloader +package bootloader_test import ( "fmt" - "io/ioutil" "os/exec" "path/filepath" "github.com/mvo5/goconfigparser" . "gopkg.in/check.v1" + "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" ) +type grubTestSuite struct { + baseBootenvTestSuite +} + +var _ = Suite(&grubTestSuite{}) + +func (s *grubTestSuite) SetUpTest(c *C) { + s.baseBootenvTestSuite.SetUpTest(c) + bootloader.MockGrubFiles(c) +} + // grubEditenvCmd finds the right grub{,2}-editenv command func grubEditenvCmd() string { for _, exe := range []string{"grub2-editenv", "grub-editenv"} { @@ -71,51 +84,48 @@ return v } -func (s *PartitionTestSuite) makeFakeGrubEnv(c *C) { - g := &grub{} - err := ioutil.WriteFile(g.ConfigFile(), nil, 0644) - c.Assert(err, IsNil) +func (s *grubTestSuite) makeFakeGrubEnv(c *C) { grubEditenvSet(c, "k", "v") } -func (s *PartitionTestSuite) TestNewGrubNoGrubReturnsNil(c *C) { +func (s *grubTestSuite) TestNewGrubNoGrubReturnsNil(c *C) { dirs.GlobalRootDir = "/something/not/there" - g := newGrub() + g := bootloader.NewGrub() c.Assert(g, IsNil) } -func (s *PartitionTestSuite) TestNewGrub(c *C) { +func (s *grubTestSuite) TestNewGrub(c *C) { s.makeFakeGrubEnv(c) - g := newGrub() + g := bootloader.NewGrub() c.Assert(g, NotNil) - c.Assert(g, FitsTypeOf, &grub{}) + c.Assert(g.Name(), Equals, "grub") } -func (s *PartitionTestSuite) TestGetBootloaderWithGrub(c *C) { +func (s *grubTestSuite) TestGetBootloaderWithGrub(c *C) { s.makeFakeGrubEnv(c) - bootloader, err := Find() + bootloader, err := bootloader.Find() c.Assert(err, IsNil) - c.Assert(bootloader, FitsTypeOf, &grub{}) + c.Assert(bootloader.Name(), Equals, "grub") } -func (s *PartitionTestSuite) TestGetBootVer(c *C) { +func (s *grubTestSuite) TestGetBootVer(c *C) { s.makeFakeGrubEnv(c) - grubEditenvSet(c, bootmodeVar, "regular") + grubEditenvSet(c, "snap_mode", "regular") - g := newGrub() - v, err := g.GetBootVars(bootmodeVar) + g := bootloader.NewGrub() + v, err := g.GetBootVars("snap_mode") c.Assert(err, IsNil) c.Check(v, HasLen, 1) - c.Check(v[bootmodeVar], Equals, "regular") + c.Check(v["snap_mode"], Equals, "regular") } -func (s *PartitionTestSuite) TestSetBootVer(c *C) { +func (s *grubTestSuite) TestSetBootVer(c *C) { s.makeFakeGrubEnv(c) - g := newGrub() + g := bootloader.NewGrub() err := g.SetBootVars(map[string]string{ "k1": "v1", "k2": "v2", @@ -125,3 +135,73 @@ c.Check(grubEditenvGet(c, "k1"), Equals, "v1") c.Check(grubEditenvGet(c, "k2"), Equals, "v2") } + +func (s *grubTestSuite) TestExtractKernelAssetsNoUnpacksKernelForGrub(c *C) { + s.makeFakeGrubEnv(c) + + g := bootloader.NewGrub() + + files := [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"meta/kernel.yaml", "version: 4.2"}, + } + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(42), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snap.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + err = g.ExtractKernelAssets(info, snapf) + c.Assert(err, IsNil) + + // kernel is *not* here + kernimg := filepath.Join(g.Dir(), "ubuntu-kernel_42.snap", "kernel.img") + c.Assert(osutil.FileExists(kernimg), Equals, false) +} + +func (s *grubTestSuite) TestExtractKernelForceWorks(c *C) { + s.makeFakeGrubEnv(c) + + g := bootloader.NewGrub() + c.Assert(g, NotNil) + + files := [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"meta/force-kernel-extraction", ""}, + {"meta/kernel.yaml", "version: 4.2"}, + } + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(42), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snap.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + err = g.ExtractKernelAssets(info, snapf) + c.Assert(err, IsNil) + + // kernel is extracted + kernimg := filepath.Join(g.Dir(), "ubuntu-kernel_42.snap", "kernel.img") + c.Assert(osutil.FileExists(kernimg), Equals, true) + // initrd + initrdimg := filepath.Join(g.Dir(), "ubuntu-kernel_42.snap", "initrd.img") + c.Assert(osutil.FileExists(initrdimg), Equals, true) + + // ensure that removal of assets also works + err = g.RemoveKernelAssets(info) + c.Assert(err, IsNil) + exists, _, err := osutil.DirExists(filepath.Dir(kernimg)) + c.Assert(err, IsNil) + c.Check(exists, Equals, false) +} diff -Nru snapd-2.39.2ubuntu0.2/bootloader/uboot.go snapd-2.40/bootloader/uboot.go --- snapd-2.39.2ubuntu0.2/bootloader/uboot.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/bootloader/uboot.go 2019-07-12 08:40:08.000000000 +0000 @@ -25,6 +25,7 @@ "github.com/snapcore/snapd/bootloader/ubootenv" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" ) type uboot struct{} @@ -92,3 +93,11 @@ return out, nil } + +func (u *uboot) ExtractKernelAssets(s *snap.Info, snapf snap.Container) error { + return extractKernelAssetsToBootDir(u.Dir(), s, snapf) +} + +func (u *uboot) RemoveKernelAssets(s snap.PlaceInfo) error { + return removeKernelAssetsFromBootDir(u.Dir(), s) +} diff -Nru snapd-2.39.2ubuntu0.2/bootloader/uboot_test.go snapd-2.40/bootloader/uboot_test.go --- snapd-2.39.2ubuntu0.2/bootloader/uboot_test.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/bootloader/uboot_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -17,44 +17,44 @@ * */ -package bootloader +package bootloader_test import ( "os" + "path/filepath" "time" . "gopkg.in/check.v1" + "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/bootloader/ubootenv" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" ) -func (s *PartitionTestSuite) makeFakeUbootEnv(c *C) { - u := &uboot{} - - // ensure that we have a valid uboot.env too - env, err := ubootenv.Create(u.envFile(), 4096) - c.Assert(err, IsNil) - err = env.Save() - c.Assert(err, IsNil) +type ubootTestSuite struct { + baseBootenvTestSuite } -func (s *PartitionTestSuite) TestNewUbootNoUbootReturnsNil(c *C) { - u := newUboot() +var _ = Suite(&ubootTestSuite{}) + +func (s *ubootTestSuite) TestNewUbootNoUbootReturnsNil(c *C) { + u := bootloader.NewUboot() c.Assert(u, IsNil) } -func (s *PartitionTestSuite) TestNewUboot(c *C) { - s.makeFakeUbootEnv(c) - - u := newUboot() +func (s *ubootTestSuite) TestNewUboot(c *C) { + bootloader.MockUbootFiles(c) + u := bootloader.NewUboot() c.Assert(u, NotNil) - c.Assert(u, FitsTypeOf, &uboot{}) + c.Assert(u.Name(), Equals, "uboot") } -func (s *PartitionTestSuite) TestUbootGetEnvVar(c *C) { - s.makeFakeUbootEnv(c) - - u := newUboot() +func (s *ubootTestSuite) TestUbootGetEnvVar(c *C) { + bootloader.MockUbootFiles(c) + u := bootloader.NewUboot() c.Assert(u, NotNil) err := u.SetBootVars(map[string]string{ "snap_mode": "", @@ -70,18 +70,20 @@ }) } -func (s *PartitionTestSuite) TestGetBootloaderWithUboot(c *C) { - s.makeFakeUbootEnv(c) +func (s *ubootTestSuite) TestGetBootloaderWithUboot(c *C) { + bootloader.MockUbootFiles(c) - bootloader, err := Find() + bootloader, err := bootloader.Find() c.Assert(err, IsNil) - c.Assert(bootloader, FitsTypeOf, &uboot{}) + c.Assert(bootloader.Name(), Equals, "uboot") } -func (s *PartitionTestSuite) TestUbootSetEnvNoUselessWrites(c *C) { - s.makeFakeUbootEnv(c) +func (s *ubootTestSuite) TestUbootSetEnvNoUselessWrites(c *C) { + bootloader.MockUbootFiles(c) + u := bootloader.NewUboot() + c.Assert(u, NotNil) - envFile := (&uboot{}).envFile() + envFile := u.ConfigFile() env, err := ubootenv.Create(envFile, 4096) c.Assert(err, IsNil) env.Set("snap_ab", "b") @@ -93,9 +95,6 @@ c.Assert(err, IsNil) time.Sleep(100 * time.Millisecond) - u := newUboot() - c.Assert(u, NotNil) - // note that we set to the same var as above err = u.SetBootVars(map[string]string{"snap_ab": "b"}) c.Assert(err, IsNil) @@ -109,10 +108,10 @@ c.Assert(st.ModTime(), Equals, st2.ModTime()) } -func (s *PartitionTestSuite) TestUbootSetBootVarFwEnv(c *C) { - s.makeFakeUbootEnv(c) +func (s *ubootTestSuite) TestUbootSetBootVarFwEnv(c *C) { + bootloader.MockUbootFiles(c) + u := bootloader.NewUboot() - u := newUboot() err := u.SetBootVars(map[string]string{"key": "value"}) c.Assert(err, IsNil) @@ -121,10 +120,10 @@ c.Assert(content, DeepEquals, map[string]string{"key": "value"}) } -func (s *PartitionTestSuite) TestUbootGetBootVarFwEnv(c *C) { - s.makeFakeUbootEnv(c) +func (s *ubootTestSuite) TestUbootGetBootVarFwEnv(c *C) { + bootloader.MockUbootFiles(c) + u := bootloader.NewUboot() - u := newUboot() err := u.SetBootVars(map[string]string{"key2": "value2"}) c.Assert(err, IsNil) @@ -132,3 +131,50 @@ c.Assert(err, IsNil) c.Assert(content, DeepEquals, map[string]string{"key2": "value2"}) } + +func (s *ubootTestSuite) TestExtractKernelAssetsAndRemove(c *C) { + bootloader.MockUbootFiles(c) + u := bootloader.NewUboot() + + files := [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"dtbs/foo.dtb", "g'day, I'm foo.dtb"}, + {"dtbs/bar.dtb", "hello, I'm bar.dtb"}, + // must be last + {"meta/kernel.yaml", "version: 4.2"}, + } + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(42), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snap.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + err = u.ExtractKernelAssets(info, snapf) + c.Assert(err, IsNil) + + // this is where the kernel/initrd is unpacked + bootdir := u.Dir() + + kernelAssetsDir := filepath.Join(bootdir, "ubuntu-kernel_42.snap") + + for _, def := range files { + if def[0] == "meta/kernel.yaml" { + break + } + + fullFn := filepath.Join(kernelAssetsDir, def[0]) + c.Check(fullFn, testutil.FileEquals, def[1]) + } + + // remove + err = u.RemoveKernelAssets(info) + c.Assert(err, IsNil) + + c.Check(osutil.FileExists(kernelAssetsDir), Equals, false) +} diff -Nru snapd-2.39.2ubuntu0.2/check-pr-title.py snapd-2.40/check-pr-title.py --- snapd-2.39.2ubuntu0.2/check-pr-title.py 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.40/check-pr-title.py 2019-07-12 08:40:08.000000000 +0000 @@ -0,0 +1,75 @@ +#!/usr/bin/python3 + +import argparse +import base64 +import json +import os +import re +import sys +import urllib.request + +from html.parser import HTMLParser + +class InvalidPRTitle(Exception): + def __init__(self, invalid_title): + self.invalid_title = invalid_title + + +class GithubTitleParser(HTMLParser): + def __init__(self): + HTMLParser.__init__(self) + self._cur_tag = "" + self.title = "" + def handle_starttag(self, tag, attributes): + self._cur_tag = tag + def handle_endtag(self, tag): + self._cur_tag = "" + def handle_data(self, data): + if self._cur_tag == "title": + self.title = data + + +def check_pr_title(pr_number: int): + # ideally we would use the github API - however we can't because: + # a) its rate limiting and travis IPs hit the API a lot so we regularly + # get errors + # b) using a API token is tricky because travis will not allow the secure + # vars for forks + # so instead we just scrape the html title which is unlikely to change + # radically + parser = GithubTitleParser() + with urllib.request.urlopen('https://github.com/snapcore/snapd/pull/{}'.format(pr_number)) as f: + parser.feed(f.read().decode("utf-8")) + # the title has the format: + # "Added api endpoint for downloading snaps by glower · Pull Request #6958 · snapcore/snapd · GitHub" + # so we rsplit() once to get the title (rsplit to not get confused by + # possible "by" words in the real title) + title = parser.title.rsplit(" by ", maxsplit=1)[0] + print(title) + # cover most common cases: + # package: foo + # package, otherpackage/subpackage: this is a title + # tests/regression/lp-12341234: foo + # [RFC] foo: bar + if not re.match(r'[a-zA-Z0-9_\-/,. \[\]]+: .*', title): + raise InvalidPRTitle(title) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + 'pr_number', metavar='PR number', help='the github PR number to check') + args = parser.parse_args() + try: + check_pr_title(args.pr_number) + except InvalidPRTitle as e: + print("Invalid PR title: \"{}\"\n".format(e.invalid_title)) + print("Please provide a title in the following format:") + print("module: short description") + print("E.g.:") + print("daemon: fix frobinator bug") + sys.exit(1) + + +if __name__ == "__main__": + main() diff -Nru snapd-2.39.2ubuntu0.2/client/client.go snapd-2.40/client/client.go --- snapd-2.39.2ubuntu0.2/client/client.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/client/client.go 2019-07-12 08:40:08.000000000 +0000 @@ -70,6 +70,9 @@ // DisableKeepAlive indicates whether the connections should not be kept // alive for later reuse DisableKeepAlive bool + + // User-Agent to sent to the snapd daemon + UserAgent string } // A Client knows how to talk to the snappy daemon. @@ -84,6 +87,8 @@ warningCount int warningTimestamp time.Time + + userAgent string } // New returns a new instance of Client @@ -103,6 +108,7 @@ doer: &http.Client{Transport: transport}, disableAuth: config.DisableAuth, interactive: config.Interactive, + userAgent: config.UserAgent, } } @@ -115,6 +121,7 @@ doer: &http.Client{Transport: &http.Transport{DisableKeepAlives: config.DisableKeepAlive}}, disableAuth: config.DisableAuth, interactive: config.Interactive, + userAgent: config.UserAgent, } } @@ -194,6 +201,9 @@ if err != nil { return nil, RequestError{err} } + if client.userAgent != "" { + req.Header.Set("User-Agent", client.userAgent) + } for key, value := range headers { req.Header.Set(key, value) diff -Nru snapd-2.39.2ubuntu0.2/client/client_test.go snapd-2.40/client/client_test.go --- snapd-2.39.2ubuntu0.2/client/client_test.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/client/client_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -458,6 +458,16 @@ }) } +func (cs *clientSuite) TestUserAgent(c *C) { + cli := client.New(&client.Config{UserAgent: "some-agent/9.87"}) + cli.SetDoer(cs) + + var v string + _ = cli.Do("GET", "/", nil, nil, &v) + c.Assert(cs.req, NotNil) + c.Check(cs.req.Header.Get("User-Agent"), Equals, "some-agent/9.87") +} + var createUsersTests = []struct { options []*client.CreateUserOptions bodies []string diff -Nru snapd-2.39.2ubuntu0.2/client/cohort.go snapd-2.40/client/cohort.go --- snapd-2.39.2ubuntu0.2/client/cohort.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.40/client/cohort.go 2019-07-12 08:40:08.000000000 +0000 @@ -0,0 +1,47 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2019 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" + "fmt" +) + +type CohortAction struct { + Action string `json:"action"` + Snaps []string `json:"snaps"` +} + +func (client *Client) CreateCohorts(snaps []string) (map[string]string, error) { + data, err := json.Marshal(&CohortAction{Action: "create", Snaps: snaps}) + if err != nil { + return nil, fmt.Errorf("cannot request cohorts: %v", err) + } + + var cohorts map[string]string + + if _, err := client.doSync("POST", "/v2/cohorts", nil, nil, bytes.NewReader(data), &cohorts); err != nil { + return nil, fmt.Errorf("cannot create cohorts: %v", err) + } + + return cohorts, nil + +} diff -Nru snapd-2.39.2ubuntu0.2/client/cohort_test.go snapd-2.40/client/cohort_test.go --- snapd-2.39.2ubuntu0.2/client/cohort_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.40/client/cohort_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -0,0 +1,67 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2019 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" + "io/ioutil" + + "gopkg.in/check.v1" +) + +func (cs *clientSuite) TestClientCreateCohortsEndpoint(c *check.C) { + cs.cli.CreateCohorts([]string{"foo", "bar"}) + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/cohorts") + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + var jsonBody map[string]interface{} + err = json.Unmarshal(body, &jsonBody) + c.Assert(err, check.IsNil) + c.Check(jsonBody, check.DeepEquals, map[string]interface{}{ + "action": "create", + "snaps": []interface{}{"foo", "bar"}, + }) +} + +func (cs *clientSuite) TestClientCreateCohorts(c *check.C) { + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": {"foo": "xyzzy", "bar": "what-what"} + }` + cohorts, err := cs.cli.CreateCohorts([]string{"foo", "bar"}) + c.Assert(err, check.IsNil) + c.Check(cohorts, check.DeepEquals, map[string]string{ + "foo": "xyzzy", + "bar": "what-what", + }) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + var jsonBody map[string]interface{} + err = json.Unmarshal(body, &jsonBody) + c.Assert(err, check.IsNil) + c.Check(jsonBody, check.DeepEquals, map[string]interface{}{ + "action": "create", + "snaps": []interface{}{"foo", "bar"}, + }) +} diff -Nru snapd-2.39.2ubuntu0.2/client/packages.go snapd-2.40/client/packages.go --- snapd-2.39.2ubuntu0.2/client/packages.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/client/packages.go 2019-07-12 08:40:08.000000000 +0000 @@ -63,6 +63,7 @@ License string `json:"license,omitempty"` CommonIDs []string `json:"common-ids,omitempty"` MountedFrom string `json:"mounted-from,omitempty"` + CohortKey string `json:"cohort-key,omitempty"` Prices map[string]float64 `json:"prices,omitempty"` Screenshots []snap.ScreenshotInfo `json:"screenshots,omitempty"` diff -Nru snapd-2.39.2ubuntu0.2/client/packages_test.go snapd-2.40/client/packages_test.go --- snapd-2.39.2ubuntu0.2/client/packages_test.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/client/packages_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -204,6 +204,7 @@ func (cs *clientSuite) TestClientSnap(c *check.C) { // example data obtained via // printf "GET /v2/find?name=test-snapd-tools HTTP/1.0\r\n\r\n" | nc -U -q 1 /run/snapd.socket|grep '{'|python3 -m json.tool + // XXX: update / sync with what daemon is actually putting out cs.rsp = `{ "type": "sync", "result": { @@ -242,6 +243,7 @@ {"type": "screenshot", "url":"http://example.com/shot1.png", "width":640, "height":480}, {"type": "screenshot", "url":"http://example.com/shot2.png"} ], + "cohort-key": "some-long-cohort-key", "common-ids": ["org.funky.snap"] } }` @@ -285,6 +287,7 @@ {Type: "screenshot", URL: "http://example.com/shot2.png"}, }, CommonIDs: []string{"org.funky.snap"}, + CohortKey: "some-long-cohort-key", }) } diff -Nru snapd-2.39.2ubuntu0.2/client/snap_op.go snapd-2.40/client/snap_op.go --- snapd-2.39.2ubuntu0.2/client/snap_op.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/client/snap_op.go 2019-07-12 08:40:08.000000000 +0000 @@ -30,15 +30,18 @@ ) type SnapOptions struct { - Amend bool `json:"amend,omitempty"` Channel string `json:"channel,omitempty"` Revision string `json:"revision,omitempty"` + CohortKey string `json:"cohort-key,omitempty"` + LeaveCohort bool `json:"leave-cohort,omitempty"` DevMode bool `json:"devmode,omitempty"` JailMode bool `json:"jailmode,omitempty"` Classic bool `json:"classic,omitempty"` Dangerous bool `json:"dangerous,omitempty"` IgnoreValidation bool `json:"ignore-validation,omitempty"` Unaliased bool `json:"unaliased,omitempty"` + Purge bool `json:"purge,omitempty"` + Amend bool `json:"amend,omitempty"` Users []string `json:"users,omitempty"` } diff -Nru snapd-2.39.2ubuntu0.2/client/snap_op_test.go snapd-2.40/client/snap_op_test.go --- snapd-2.39.2ubuntu0.2/client/snap_op_test.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/client/snap_op_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -400,3 +400,26 @@ _, err := cs.cli.Try(snapdir, &client.SnapOptions{Dangerous: true}) c.Assert(err, check.Equals, client.ErrDangerousNotApplicable) } + +func (cs *clientSuite) TestSnapOptionsSerialises(c *check.C) { + tests := map[string]client.SnapOptions{ + "{}": {}, + `{"channel":"edge"}`: {Channel: "edge"}, + `{"revision":"42"}`: {Revision: "42"}, + `{"cohort-key":"what"}`: {CohortKey: "what"}, + `{"leave-cohort":true}`: {LeaveCohort: true}, + `{"devmode":true}`: {DevMode: true}, + `{"jailmode":true}`: {JailMode: true}, + `{"classic":true}`: {Classic: true}, + `{"dangerous":true}`: {Dangerous: true}, + `{"ignore-validation":true}`: {IgnoreValidation: true}, + `{"unaliased":true}`: {Unaliased: true}, + `{"purge":true}`: {Purge: true}, + `{"amend":true}`: {Amend: true}, + } + for expected, opts := range tests { + buf, err := json.Marshal(&opts) + c.Assert(err, check.IsNil, check.Commentf("%s", expected)) + c.Check(string(buf), check.Equals, expected) + } +} diff -Nru snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/error.c snapd-2.40/cmd/libsnap-confine-private/error.c --- snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/error.c 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/libsnap-confine-private/error.c 2019-07-12 08:40:08.000000000 +0000 @@ -112,13 +112,14 @@ } } -void sc_error_forward(sc_error ** recipient, sc_error * error) +int sc_error_forward(sc_error ** recipient, sc_error * error) { if (recipient != NULL) { *recipient = error; } else { sc_die_on_error(error); } + return error != NULL ? -1 : 0; } bool sc_error_match(sc_error * error, const char *domain, int code) diff -Nru snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/error.h snapd-2.40/cmd/libsnap-confine-private/error.h --- snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/error.h 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/libsnap-confine-private/error.h 2019-07-12 08:40:08.000000000 +0000 @@ -61,6 +61,21 @@ #define SC_ERRNO_DOMAIN "errno" /** + * Error domain for errors in the libsnap-confine-private library. + **/ +#define SC_LIBSNAP_ERROR "libsnap-confine-private" + +/** sc_libsnap_error represents distinct error codes used by libsnap-confine-private library. */ +typedef enum sc_libsnap_error { + /** SC_UNSPECIFIED_ERROR indicates an error not worthy of a distinct code. */ + SC_UNSPECIFIED_ERROR = 0, + /** SC_API_MISUSE indicates that public API was called incorrectly. */ + SC_API_MISUSE, + /** SC_BUG indicates that private API was called incorrectly. */ + SC_BUG, +} sc_libsnap_error; + +/** * Initialize a new error object. * * The domain is a cookie-like string that allows the caller to distinguish @@ -86,7 +101,6 @@ format(printf, 2, 3) SC_APPEND_RETURNS_NONNULL)) sc_error *sc_error_init_from_errno(int errno_copy, const char *msgfmt, ...); - /** * Get the error domain out of an error object. * @@ -153,10 +167,14 @@ * sc_die_on_error() is called as a safety measure. * * Change of ownership takes place and the error is now stored in the recipient. + * + * The return value -1 if error is non-NULL and 0 otherwise. The return value + * makes it convenient to `return sc_error_forward(err_out, err);` as the last + * line of a function. **/ // NOTE: There's no nonnull(1) attribute as the recipient *can* be NULL. With // the attribute in place GCC optimizes some things out and tests fail. -void sc_error_forward(sc_error ** recipient, sc_error * error); +int sc_error_forward(sc_error ** recipient, sc_error * error); /** * Check if a given error matches the specified domain and code. diff -Nru snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/error-test.c snapd-2.40/cmd/libsnap-confine-private/error-test.c --- snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/error-test.c 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/libsnap-confine-private/error-test.c 2019-07-12 08:40:08.000000000 +0000 @@ -161,8 +161,10 @@ // Check that forwarding NULL does exactly that. struct sc_error *recipient = (void *)0xDEADBEEF; struct sc_error *err = NULL; - sc_error_forward(&recipient, err); + int rc; + rc = sc_error_forward(&recipient, err); g_assert_null(recipient); + g_assert_cmpint(rc, ==, 0); } static void test_sc_error_forward__something_somewhere(void) @@ -170,10 +172,12 @@ // Check that forwarding a real error works OK. struct sc_error *recipient = NULL; struct sc_error *err = sc_error_init("domain", 42, "just testing"); + int rc; g_test_queue_destroy((GDestroyNotify) sc_error_free, err); g_assert_nonnull(err); - sc_error_forward(&recipient, err); + rc = sc_error_forward(&recipient, err); g_assert_nonnull(recipient); + g_assert_cmpint(rc, ==, -1); } static void test_sc_error_forward__something_nowhere(void) diff -Nru snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/infofile.c snapd-2.40/cmd/libsnap-confine-private/infofile.c --- snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/infofile.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.40/cmd/libsnap-confine-private/infofile.c 2019-07-12 08:40:08.000000000 +0000 @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2019 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 "infofile.h" + +#include +#include +#include +#include + +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/error.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/utils.h" + +int sc_infofile_get_key(FILE *stream, const char *key, char **value, sc_error **err_out) { + sc_error *err = NULL; + size_t line_size = 0; + char *line_buf SC_CLEANUP(sc_cleanup_string) = NULL; + + if (stream == NULL) { + err = sc_error_init(SC_LIBSNAP_ERROR, SC_API_MISUSE, "stream cannot be NULL"); + goto out; + } + if (key == NULL) { + err = sc_error_init(SC_LIBSNAP_ERROR, SC_API_MISUSE, "key cannot be NULL"); + goto out; + } + if (value == NULL) { + err = sc_error_init(SC_LIBSNAP_ERROR, SC_API_MISUSE, "value cannot be NULL"); + goto out; + } + + /* Store NULL in case we don't find the key. + * This makes the value always well-defined. */ + *value = NULL; + + /* This loop advances through subsequent lines. */ + for (int lineno = 1;; ++lineno) { + errno = 0; + ssize_t nread = getline(&line_buf, &line_size, stream); + if (nread < 0 && errno != 0) { + err = sc_error_init_from_errno(errno, "cannot read beyond line %d", lineno); + goto out; + } + if (nread <= 0) { + break; /* There is nothing more to read. */ + } + /* NOTE: beyond this line the buffer is never empty (ie, nread > 0). */ + + /* Guard against malformed input that may contain NUL bytes that + * would confuse the code below. */ + if (memchr(line_buf, '\0', nread) != NULL) { + err = sc_error_init(SC_LIBSNAP_ERROR, 0, "line %d contains NUL byte", lineno); + goto out; + } + /* Guard against non-strictly formatted input that doesn't contain + * trailing newline. */ + if (line_buf[nread - 1] != '\n') { + err = sc_error_init(SC_LIBSNAP_ERROR, 0, "line %d does not end with a newline", lineno); + goto out; + } + /* Replace the trailing newline character with the NUL byte. */ + line_buf[nread - 1] = '\0'; + /* Guard against malformed input that does not contain '=' byte */ + char *eq_ptr = memchr(line_buf, '=', nread); + if (eq_ptr == NULL) { + err = sc_error_init(SC_LIBSNAP_ERROR, 0, "line %d is not a key=value assignment", lineno); + goto out; + } + /* Guard against malformed input with empty key. */ + if (eq_ptr == line_buf) { + err = sc_error_init(SC_LIBSNAP_ERROR, 0, "line %d contains empty key", lineno); + goto out; + } + /* Replace the first '=' with string terminator byte. */ + *eq_ptr = '\0'; + + /* If the key matches the one we are looking for, store it and stop scanning. */ + const char *scanned_key = line_buf; + const char *scanned_value = eq_ptr + 1; + if (sc_streq(scanned_key, key)) { + *value = sc_strdup(scanned_value); + break; + } + } + +out: + return sc_error_forward(err_out, err); +} diff -Nru snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/infofile.h snapd-2.40/cmd/libsnap-confine-private/infofile.h --- snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/infofile.h 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.40/cmd/libsnap-confine-private/infofile.h 2019-07-12 08:40:08.000000000 +0000 @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2019 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_INFOFILE_H +#define SNAP_CONFINE_INFOFILE_H + +#include + +#include "../libsnap-confine-private/error.h" + +/** + * sc_infofile_get_key extracts a single value of a key=value pair from a given + * stream. + * + * On success the return value is zero and err_out, if not NULL, is deferences + * and set to NULL. On failure the return value is -1 is and detailed error + * information is stored by dereferencing err_out. If an error occurs and + * err_out is NULL then the program dies, printing the error message. + **/ +int sc_infofile_get_key(FILE *stream, const char *key, char **value, sc_error **err_out); + +#endif diff -Nru snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/infofile-test.c snapd-2.40/cmd/libsnap-confine-private/infofile-test.c --- snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/infofile-test.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.40/cmd/libsnap-confine-private/infofile-test.c 2019-07-12 08:40:08.000000000 +0000 @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2019 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 "infofile.h" +#include "infofile.c" + +#include +#include + +static void test_infofile_get_key(void) { + int rc; + sc_error *err; + + char text[] = + "key=value\n" + "other-key=other-value\n" + "dup-key=value-one\n" + "dup-key=value-two\n"; + FILE *stream = fmemopen(text, sizeof text - 1, "r"); + g_assert_nonnull(stream); + + char *value; + + /* Caller must provide the stream to scan. */ + rc = sc_infofile_get_key(NULL, "key", &value, &err); + g_assert_cmpint(rc, ==, -1); + g_assert_nonnull(err); + g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_ERROR); + g_assert_cmpint(sc_error_code(err), ==, SC_API_MISUSE); + g_assert_cmpstr(sc_error_msg(err), ==, "stream cannot be NULL"); + sc_error_free(err); + + /* Caller must provide the key to look for. */ + rc = sc_infofile_get_key(stream, NULL, &value, &err); + g_assert_cmpint(rc, ==, -1); + g_assert_nonnull(err); + g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_ERROR); + g_assert_cmpint(sc_error_code(err), ==, SC_API_MISUSE); + g_assert_cmpstr(sc_error_msg(err), ==, "key cannot be NULL"); + sc_error_free(err); + + /* Caller must provide storage for the value. */ + rc = sc_infofile_get_key(stream, "key", NULL, &err); + g_assert_cmpint(rc, ==, -1); + g_assert_nonnull(err); + g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_ERROR); + g_assert_cmpint(sc_error_code(err), ==, SC_API_MISUSE); + g_assert_cmpstr(sc_error_msg(err), ==, "value cannot be NULL"); + sc_error_free(err); + + /* Keys that are not found get NULL values. */ + value = (void *)0xfefefefe; + rewind(stream); + rc = sc_infofile_get_key(stream, "missing-key", &value, &err); + g_assert_cmpint(rc, ==, 0); + g_assert_null(err); + g_assert_null(value); + + /* Keys that are found get strdup-duplicated values. */ + value = NULL; + rewind(stream); + rc = sc_infofile_get_key(stream, "key", &value, &err); + g_assert_cmpint(rc, ==, 0); + g_assert_null(err); + g_assert_nonnull(value); + g_assert_cmpstr(value, ==, "value"); + free(value); + + /* When duplicate keys are present the first value is extracted. */ + char *dup_value; + rewind(stream); + rc = sc_infofile_get_key(stream, "dup-key", &dup_value, &err); + g_assert_cmpint(rc, ==, 0); + g_assert_null(err); + g_assert_nonnull(dup_value); + g_assert_cmpstr(dup_value, ==, "value-one"); + free(dup_value); + + fclose(stream); + + /* Key without a value. */ + char *tricky_value; + char tricky1[] = "key\n"; + stream = fmemopen(tricky1, sizeof tricky1 - 1, "r"); + g_assert_nonnull(stream); + rc = sc_infofile_get_key(stream, "key", &tricky_value, &err); + g_assert_cmpint(rc, ==, -1); + g_assert_nonnull(err); + g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_ERROR); + g_assert_cmpint(sc_error_code(err), ==, 0); + g_assert_cmpstr(sc_error_msg(err), ==, "line 1 is not a key=value assignment"); + g_assert_null(tricky_value); + sc_error_free(err); + fclose(stream); + + /* Key-value pair with embedded NUL byte. */ + char tricky2[] = "key=value\0garbage\n"; + stream = fmemopen(tricky2, sizeof tricky2 - 1, "r"); + g_assert_nonnull(stream); + rc = sc_infofile_get_key(stream, "key", &tricky_value, &err); + g_assert_cmpint(rc, ==, -1); + g_assert_nonnull(err); + g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_ERROR); + g_assert_cmpint(sc_error_code(err), ==, 0); + g_assert_cmpstr(sc_error_msg(err), ==, "line 1 contains NUL byte"); + g_assert_null(tricky_value); + sc_error_free(err); + fclose(stream); + + /* Key with empty value but without trailing newline. */ + char tricky3[] = "key="; + stream = fmemopen(tricky3, sizeof tricky3 - 1, "r"); + g_assert_nonnull(stream); + rc = sc_infofile_get_key(stream, "key", &tricky_value, &err); + g_assert_cmpint(rc, ==, -1); + g_assert_nonnull(err); + g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_ERROR); + g_assert_cmpint(sc_error_code(err), ==, 0); + g_assert_cmpstr(sc_error_msg(err), ==, "line 1 does not end with a newline"); + g_assert_null(tricky_value); + sc_error_free(err); + fclose(stream); + + /* Key with empty value with a trailing newline (which is also valid). */ + char tricky4[] = "key=\n"; + stream = fmemopen(tricky4, sizeof tricky4 - 1, "r"); + g_assert_nonnull(stream); + rc = sc_infofile_get_key(stream, "key", &tricky_value, &err); + g_assert_cmpint(rc, ==, 0); + g_assert_null(err); + g_assert_cmpstr(tricky_value, ==, ""); + sc_error_free(err); + fclose(stream); + free(tricky_value); + + /* The equals character alone (key is empty) */ + char tricky5[] = "=\n"; + stream = fmemopen(tricky5, sizeof tricky5 - 1, "r"); + g_assert_nonnull(stream); + rc = sc_infofile_get_key(stream, "key", &tricky_value, &err); + g_assert_cmpint(rc, ==, -1); + g_assert_nonnull(err); + g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_ERROR); + g_assert_cmpint(sc_error_code(err), ==, 0); + g_assert_cmpstr(sc_error_msg(err), ==, "line 1 contains empty key"); + g_assert_null(tricky_value); + sc_error_free(err); + fclose(stream); +} + +static void __attribute__((constructor)) init(void) { g_test_add_func("/infofile/get_key", test_infofile_get_key); } diff -Nru snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/mountinfo.c snapd-2.40/cmd/libsnap-confine-private/mountinfo.c --- snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/mountinfo.c 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/libsnap-confine-private/mountinfo.c 2019-07-12 08:40:08.000000000 +0000 @@ -148,12 +148,12 @@ } static char *parse_next_string_field(sc_mountinfo_entry * entry, - const char *line, size_t * offset) + const char *line, size_t *offset) { const char *input = &line[*offset]; char *output = &entry->line_buf[*offset]; - size_t input_idx = 0; // reading index - size_t output_idx = 0; // writing index + size_t input_idx = 0; // reading index + size_t output_idx = 0; // writing index // Scan characters until we run out of memory to scan or we find a // space. The kernel uses simple octal escape sequences for the @@ -166,7 +166,7 @@ // return NULL. This is an indication of end-of-input // to the caller. if (output_idx == 0) { - return NULL; + return NULL; } // The scanned line is NUL terminated. This ensures that the // terminator is copied to the output buffer. diff -Nru snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/snap.c snapd-2.40/cmd/libsnap-confine-private/snap.c --- snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/snap.c 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/libsnap-confine-private/snap.c 2019-07-12 08:40:08.000000000 +0000 @@ -98,8 +98,7 @@ return 0; } -void sc_instance_name_validate(const char *instance_name, - sc_error **errorp) +void sc_instance_name_validate(const char *instance_name, sc_error ** errorp) { // NOTE: This function should be synchronized with the two other // implementations: validate_instance_name and snap.ValidateInstanceName. @@ -142,8 +141,7 @@ sc_error_forward(errorp, err); } -void sc_instance_key_validate(const char *instance_key, - sc_error **errorp) +void sc_instance_key_validate(const char *instance_key, sc_error ** errorp) { // NOTE: see snap.ValidateInstanceName for reference of a valid instance key // format @@ -186,7 +184,7 @@ sc_error_forward(errorp, err); } -void sc_snap_name_validate(const char *snap_name, sc_error **errorp) +void sc_snap_name_validate(const char *snap_name, sc_error ** errorp) { // NOTE: This function should be synchronized with the two other // implementations: validate_snap_name and snap.ValidateName. diff -Nru snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/string-utils.c snapd-2.40/cmd/libsnap-confine-private/string-utils.c --- snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/string-utils.c 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/libsnap-confine-private/string-utils.c 2019-07-12 08:40:08.000000000 +0000 @@ -56,6 +56,22 @@ return strncmp(str - xlen + slen, suffix, xlen) == 0; } +bool sc_startswith(const char *str, const char *prefix) +{ + if (!str || !prefix) { + return false; + } + + size_t xlen = strlen(prefix); + size_t slen = strlen(str); + + if (slen < xlen) { + return false; + } + + return strncmp(str, prefix, xlen) == 0; +} + char *sc_strdup(const char *str) { size_t len; diff -Nru snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/string-utils.h snapd-2.40/cmd/libsnap-confine-private/string-utils.h --- snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/string-utils.h 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/libsnap-confine-private/string-utils.h 2019-07-12 08:40:08.000000000 +0000 @@ -32,6 +32,11 @@ bool sc_endswith(const char *str, const char *suffix); /** + * Check if a string has a given prefix. + **/ +bool sc_startswith(const char *str, const char *prefix); + +/** * Allocate and return a copy of a string. **/ char *sc_strdup(const char *str); diff -Nru snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/string-utils-test.c snapd-2.40/cmd/libsnap-confine-private/string-utils-test.c --- snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/string-utils-test.c 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/libsnap-confine-private/string-utils-test.c 2019-07-12 08:40:08.000000000 +0000 @@ -53,6 +53,27 @@ g_assert_false(sc_endswith("ba", "bar")); } +static void test_sc_startswith(void) +{ + // NULL doesn't start with anything, nothing starts with NULL + g_assert_false(sc_startswith("", NULL)); + g_assert_false(sc_startswith(NULL, "")); + g_assert_false(sc_startswith(NULL, NULL)); + // Empty string starts with an empty string + g_assert_true(sc_startswith("", "")); + // Starts-with (matches) + g_assert_true(sc_startswith("foobar", "foo")); + g_assert_true(sc_startswith("foobar", "fo")); + g_assert_true(sc_startswith("foobar", "f")); + g_assert_true(sc_startswith("foobar", "")); + g_assert_true(sc_startswith("bar", "bar")); + // Starts-with (non-matches) + g_assert_false(sc_startswith("foobar", "quux")); + g_assert_false(sc_startswith("", "bar")); + g_assert_false(sc_startswith("b", "bar")); + g_assert_false(sc_startswith("ba", "bar")); +} + static void test_sc_must_snprintf(void) { char buf[5] = { 0 }; @@ -794,6 +815,7 @@ { g_test_add_func("/string-utils/sc_streq", test_sc_streq); g_test_add_func("/string-utils/sc_endswith", test_sc_endswith); + g_test_add_func("/string-utils/sc_startswith", test_sc_startswith); g_test_add_func("/string-utils/sc_must_snprintf", test_sc_must_snprintf); g_test_add_func("/string-utils/sc_must_snprintf/fail", diff -Nru snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/utils.c snapd-2.40/cmd/libsnap-confine-private/utils.c --- snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/utils.c 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/libsnap-confine-private/utils.c 2019-07-12 08:40:08.000000000 +0000 @@ -42,16 +42,6 @@ exit(1); } -bool error(const char *msg, ...) -{ - va_list va; - va_start(va, msg); - vfprintf(stderr, msg, va); - va_end(va); - - return false; -} - struct sc_bool_name { const char *text; bool value; diff -Nru snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/utils.h snapd-2.40/cmd/libsnap-confine-private/utils.h --- snapd-2.39.2ubuntu0.2/cmd/libsnap-confine-private/utils.h 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/libsnap-confine-private/utils.h 2019-07-12 08:40:08.000000000 +0000 @@ -25,9 +25,6 @@ void die(const char *fmt, ...); __attribute__((format(printf, 1, 2))) -bool error(const char *fmt, ...); - -__attribute__((format(printf, 1, 2))) void debug(const char *fmt, ...); /** diff -Nru snapd-2.39.2ubuntu0.2/cmd/Makefile.am snapd-2.40/cmd/Makefile.am --- snapd-2.39.2ubuntu0.2/cmd/Makefile.am 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/Makefile.am 2019-07-12 08:40:08.000000000 +0000 @@ -59,12 +59,15 @@ libsnap-confine-private/cgroup-pids-support.h \ libsnap-confine-private/cgroup-support.c \ libsnap-confine-private/cgroup-support.h \ + libsnap-confine-private/infofile-test.c \ + libsnap-confine-private/infofile.c \ + libsnap-confine-private/infofile.h \ snap-confine/seccomp-support-ext.c \ snap-confine/seccomp-support-ext.h \ - snap-confine/snap-confine-invocation.c \ - snap-confine/snap-confine-invocation.h \ snap-confine/selinux-support.c \ snap-confine/selinux-support.h \ + snap-confine/snap-confine-invocation.c \ + snap-confine/snap-confine-invocation.h \ snap-discard-ns/snap-discard-ns.c # NOTE: clang-format is using project-wide .clang-format file. @@ -119,6 +122,7 @@ libsnap-confine-private/fault-injection.h \ libsnap-confine-private/feature.c \ libsnap-confine-private/feature.h \ + libsnap-confine-private/infofile.c \ libsnap-confine-private/locking.c \ libsnap-confine-private/locking.h \ libsnap-confine-private/mount-opt.c \ @@ -151,6 +155,7 @@ libsnap-confine-private/error-test.c \ libsnap-confine-private/fault-injection-test.c \ libsnap-confine-private/feature-test.c \ + libsnap-confine-private/infofile-test.c \ libsnap-confine-private/locking-test.c \ libsnap-confine-private/mount-opt-test.c \ libsnap-confine-private/mountinfo-test.c \ diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/cmd_advise.go snapd-2.40/cmd/snap/cmd_advise.go --- snapd-2.39.2ubuntu0.2/cmd/snap/cmd_advise.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/snap/cmd_advise.go 2019-07-12 08:40:08.000000000 +0000 @@ -26,6 +26,7 @@ "io" "net" "os" + "sort" "strconv" "github.com/jessevdk/go-flags" @@ -47,6 +48,9 @@ // FromApt tells advise that it got started from an apt hook // and needs to communicate over a socket FromApt bool `long:"from-apt"` + + // DumpDb dumps the whole advise database + DumpDb bool `long:"dump-db"` } var shortAdviseSnapHelp = i18n.G("Advise on available snaps") @@ -64,6 +68,8 @@ // TRANSLATORS: This should not start with a lowercase letter. "command": i18n.G("Advise on snaps that provide the given command"), // TRANSLATORS: This should not start with a lowercase letter. + "dump-db": i18n.G("Dump advise database for use by command-not-found."), + // TRANSLATORS: This should not start with a lowercase letter. "from-apt": i18n.G("Run as an apt hook"), // TRANSLATORS: This should not start with a lowercase letter. "format": i18n.G("Use the given output format"), @@ -213,11 +219,55 @@ return nil } +type Snap struct { + Snap string + Version string + Command string +} + +func dumpDbHook() error { + commands, err := advisor.DumpCommands() + if err != nil { + return err + } + + commands_processed := make([]string, 0) + var b []Snap + + var sortedCmds []string + for cmd := range commands { + sortedCmds = append(sortedCmds, cmd) + } + sort.Strings(sortedCmds) + + for _, key := range sortedCmds { + value := commands[key] + err := json.Unmarshal([]byte(value), &b) + if err != nil { + return err + } + for i := range b { + var s = fmt.Sprintf("%s %s %s\n", key, b[i].Snap, b[i].Version) + commands_processed = append(commands_processed, s) + } + } + + for _, value := range commands_processed { + fmt.Fprint(Stdout, value) + } + + return nil +} + func (x *cmdAdviseSnap) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } + if x.DumpDb { + return dumpDbHook() + } + if x.FromApt { return adviseViaAptHook() } diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/cmd_advise_test.go snapd-2.40/cmd/snap/cmd_advise_test.go --- snapd-2.39.2ubuntu0.2/cmd/snap/cmd_advise_test.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/snap/cmd_advise_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -33,6 +33,7 @@ "github.com/snapcore/snapd/advisor" snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/dirs" ) type sillyFinder struct{} @@ -98,6 +99,23 @@ c.Assert(s.Stderr(), Equals, "") } +func (s *SnapSuite) TestAdviseCommandDumpDb(c *C) { + dirs.SetRootDir(c.MkDir()) + c.Assert(os.MkdirAll(dirs.SnapCacheDir, 0755), IsNil) + defer dirs.SetRootDir("") + + db, err := advisor.Create() + c.Assert(err, IsNil) + c.Assert(db.AddSnap("foo", "1.0", "foo summary", []string{"foo", "bar"}), IsNil) + c.Assert(db.Commit(), IsNil) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"advise-snap", "--dump-db"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stderr(), Equals, "") + c.Assert(s.Stdout(), Matches, `bar foo 1.0\nfoo foo 1.0\n`) +} + func (s *SnapSuite) TestAdviseCommandMisspellText(c *C) { restore := advisor.ReplaceCommandsFinder(mkSillyFinder) defer restore() diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/cmd_create_cohort.go snapd-2.40/cmd/snap/cmd_create_cohort.go --- snapd-2.39.2ubuntu0.2/cmd/snap/cmd_create_cohort.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.40/cmd/snap/cmd_create_cohort.go 2019-07-12 08:40:08.000000000 +0000 @@ -0,0 +1,85 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2019 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 ( + "github.com/jessevdk/go-flags" + "gopkg.in/yaml.v2" + + "github.com/snapcore/snapd/i18n" +) + +var shortCreateCohortHelp = i18n.G("Create cohort keys for a series of snaps") +var longCreateCohortHelp = i18n.G(` +The create-cohort command creates a set of cohort keys for a given set of snaps. + +A cohort is a view or snapshot of a snap's "channel map" at a given point in +time that fixes the set of revisions for the snap given other constraints +(e.g. channel or architecture). The cohort is then identified by an opaque +per-snap key that works across systems. Installations or refreshes of the snap +using a given cohort key would use a fixed revision for up to 90 days, after +which a new set of revisions would be fixed under that same cohort key and a +new 90 days window started. +`) + +type cmdCreateCohort struct { + clientMixin + Positional struct { + Snaps []anySnapName `positional-arg-name:"" required:"1"` + } `positional-args:"yes" required:"yes"` +} + +func init() { + addCommand("create-cohort", shortCreateCohortHelp, longCreateCohortHelp, func() flags.Commander { return &cmdCreateCohort{} }, nil, nil) +} + +// output should be YAML, so we use these two as helpers to get that done easy +type cohortInnerYAML struct { + CohortKey string `yaml:"cohort-key"` +} +type cohortOutYAML struct { + Cohorts map[string]cohortInnerYAML `yaml:"cohorts"` +} + +func (x *cmdCreateCohort) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + snaps := make([]string, len(x.Positional.Snaps)) + for i, s := range x.Positional.Snaps { + snaps[i] = string(s) + } + + cohorts, err := x.client.CreateCohorts(snaps) + if len(cohorts) == 0 || err != nil { + return err + } + + var out cohortOutYAML + out.Cohorts = make(map[string]cohortInnerYAML, len(cohorts)) + for k, v := range cohorts { + out.Cohorts[k] = cohortInnerYAML{v} + } + + enc := yaml.NewEncoder(Stdout) + defer enc.Close() + return enc.Encode(out) +} diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/cmd_create_cohort_test.go snapd-2.40/cmd/snap/cmd_create_cohort_test.go --- snapd-2.39.2ubuntu0.2/cmd/snap/cmd_create_cohort_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.40/cmd/snap/cmd_create_cohort_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -0,0 +1,87 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2019 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" + "gopkg.in/yaml.v2" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestCreateCohort(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + fmt.Fprintln(w, `{ +"type": "sync", +"status-code": 200, +"status": "OK", +"result": {"foo": "what", "bar": "this"}}`) + + }) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-cohort", "foo", "bar"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.HasLen, 0) + + var v map[string]map[string]map[string]string + c.Assert(yaml.Unmarshal(s.stdout.Bytes(), &v), check.IsNil) + c.Check(v, check.DeepEquals, map[string]map[string]map[string]string{ + "cohorts": { + "foo": {"cohort-key": "what"}, + "bar": {"cohort-key": "this"}, + }, + }) + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestCreateCohortNoSnaps(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + panic("shouldn't be called") + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-cohort"}) + c.Check(err, check.ErrorMatches, "the required argument .* was not provided") +} + +func (s *SnapSuite) TestCreateCohortNotFound(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + fmt.Fprintln(w, `{"type": "error", "result": {"message": "snap not found", "kind": "snap-not-found"}, "status-code": 404}`) + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-cohort", "foo", "bar"}) + c.Check(err, check.ErrorMatches, "cannot create cohorts: snap not found") + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestCreateCohortError(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + fmt.Fprintln(w, `{"type": "error", "result": {"message": "something went wrong"}}`) + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-cohort", "foo", "bar"}) + c.Check(err, check.ErrorMatches, "cannot create cohorts: something went wrong") + c.Check(n, check.Equals, 1) +} diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/cmd_create_user.go snapd-2.40/cmd/snap/cmd_create_user.go --- snapd-2.39.2ubuntu0.2/cmd/snap/cmd_create_user.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/snap/cmd_create_user.go 2019-07-12 08:40:08.000000000 +0000 @@ -23,10 +23,10 @@ "encoding/json" "fmt" + "github.com/jessevdk/go-flags" + "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" - - "github.com/jessevdk/go-flags" ) var shortCreateUserHelp = i18n.G("Create a local system user") diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/cmd_debug_timings.go snapd-2.40/cmd/snap/cmd_debug_timings.go --- snapd-2.39.2ubuntu0.2/cmd/snap/cmd_debug_timings.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/snap/cmd_debug_timings.go 2019-07-12 08:40:08.000000000 +0000 @@ -32,7 +32,10 @@ type cmdChangeTimings struct { changeIDMixin - Verbose bool `long:"verbose"` + EnsureTag string `long:"ensure" choice:"auto-refresh" choice:"become-operational" choice:"refresh-catalogs" choice:"refresh-hints" choice:"seed"` + All bool `long:"all"` + StartupTag string `long:"startup" choice:"load-state" choice:"ifacemgr"` + Verbose bool `long:"verbose"` } func init() { @@ -42,6 +45,9 @@ func() flags.Commander { return &cmdChangeTimings{} }, changeIDMixinOptDesc.also(map[string]string{ + "ensure": i18n.G("Show timings for a change related to the given Ensure activity (one of: auto-refresh, become-operational, refresh-catalogs, refresh-hints, seed)"), + "all": i18n.G("Show timings for all executions of the given Ensure or startup activity, not just the latest"), + "startup": i18n.G("Show timings for the startup of given subsystem (one of: load-state, ifacemgr)"), // TRANSLATORS: This should not start with a lowercase letter. "verbose": i18n.G("Show more information"), }), changeIDMixinArgDesc) @@ -58,7 +64,21 @@ return fmt.Sprintf("%dms", dur/time.Millisecond) } -func printTiming(w io.Writer, t *Timing, verbose, doing bool) { +func printTiming(w io.Writer, verbose bool, nestLevel int, id, status, doingTimeStr, undoingTimeStr, label, summary string) { + // don't display id for nesting>1, instead show nesting indicator + if nestLevel > 0 { + id = strings.Repeat(" ", nestLevel) + "^" + } + // Duration formats to 17m14.342s or 2.038s or 970ms, so with + // 11 chars we can go up to 59m59.999s + if verbose { + fmt.Fprintf(w, "%s\t%s\t%11s\t%11s\t%s\t%s\n", id, status, doingTimeStr, undoingTimeStr, label, strings.Repeat(" ", 2*nestLevel)+summary) + } else { + fmt.Fprintf(w, "%s\t%s\t%11s\t%11s\t%s\n", id, status, doingTimeStr, undoingTimeStr, strings.Repeat(" ", 2*nestLevel)+summary) + } +} + +func printTaskTiming(w io.Writer, t *Timing, verbose, doing bool) { var doingTimeStr, undoingTimeStr string if doing { doingTimeStr = formatDuration(t.Duration) @@ -69,72 +89,152 @@ undoingTimeStr = formatDuration(t.Duration) } } - if verbose { - fmt.Fprintf(w, "%s\t \t%11s\t%11s\t%s\t%s\n", strings.Repeat(" ", t.Level+1)+"^", doingTimeStr, undoingTimeStr, t.Label, strings.Repeat(" ", 2*(t.Level+1))+t.Summary) - } else { - fmt.Fprintf(w, "%s\t \t%11s\t%11s\t%s\n", strings.Repeat(" ", t.Level+1)+"^", doingTimeStr, undoingTimeStr, strings.Repeat(" ", 2*(t.Level+1))+t.Summary) - } + printTiming(w, verbose, t.Level+1, "", "", doingTimeStr, undoingTimeStr, t.Label, t.Summary) } -func (x *cmdChangeTimings) Execute(args []string) error { - if len(args) > 0 { - return ErrExtraArgs - } - chgid, err := x.GetChangeID() +func (x *cmdChangeTimings) printChangeTimings(w io.Writer, timing *timingsData) error { + chgid := timing.ChangeID + chg, err := x.client.Change(chgid) if err != nil { return err } - // gather debug timings first - var timings map[string]struct { + for _, t := range chg.Tasks { + doingTime := formatDuration(timing.ChangeTimings[t.ID].DoingTime) + if timing.ChangeTimings[t.ID].DoingTime == 0 { + doingTime = "-" + } + undoingTime := formatDuration(timing.ChangeTimings[t.ID].UndoingTime) + if timing.ChangeTimings[t.ID].UndoingTime == 0 { + undoingTime = "-" + } + + printTiming(w, x.Verbose, 0, t.ID, t.Status, doingTime, undoingTime, t.Kind, t.Summary) + for _, nested := range timing.ChangeTimings[t.ID].DoingTimings { + showDoing := true + printTaskTiming(w, &nested, x.Verbose, showDoing) + } + for _, nested := range timing.ChangeTimings[t.ID].UndoingTimings { + showDoing := false + printTaskTiming(w, &nested, x.Verbose, showDoing) + } + } + + return nil +} + +func (x *cmdChangeTimings) printEnsureTimings(w io.Writer, timings []*timingsData) error { + for _, td := range timings { + printTiming(w, x.Verbose, 0, x.EnsureTag, "", "-", "-", "", "") + for _, t := range td.EnsureTimings { + printTiming(w, x.Verbose, t.Level+1, "", "", formatDuration(t.Duration), "-", t.Label, t.Summary) + } + + // change is optional for ensure timings + if td.ChangeID != "" { + x.printChangeTimings(w, td) + } + } + return nil +} + +func (x *cmdChangeTimings) printStartupTimings(w io.Writer, timings []*timingsData) error { + for _, td := range timings { + printTiming(w, x.Verbose, 0, x.StartupTag, "", "-", "-", "", "") + for _, t := range td.StartupTimings { + printTiming(w, x.Verbose, t.Level+1, "", "", formatDuration(t.Duration), "-", t.Label, t.Summary) + } + } + return nil +} + +type timingsData struct { + ChangeID string `json:"change-id"` + EnsureTimings []Timing `json:"ensure-timings,omitempty"` + StartupTimings []Timing `json:"startup-timings,omitempty"` + ChangeTimings map[string]struct { DoingTime time.Duration `json:"doing-time,omitempty"` UndoingTime time.Duration `json:"undoing-time,omitempty"` DoingTimings []Timing `json:"doing-timings,omitempty"` UndoingTimings []Timing `json:"undoing-timings,omitempty"` + } `json:"change-timings,omitempty"` +} + +func (x *cmdChangeTimings) checkConflictingFlags() error { + var i int + for _, opt := range []string{string(x.Positional.ID), x.StartupTag, x.EnsureTag} { + if opt != "" { + i++ + if i > 1 { + return fmt.Errorf("cannot use change id, 'startup' or 'ensure' together") + } + } + } + + if x.All && (x.Positional.ID != "" || x.LastChangeType != "") { + return fmt.Errorf("cannot use 'all' with change id or 'last'") } + return nil +} - if err := x.client.DebugGet("change-timings", &timings, map[string]string{"change-id": chgid}); err != nil { +func (x *cmdChangeTimings) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + if err := x.checkConflictingFlags(); err != nil { return err } - // now combine with the other data about the change - chg, err := x.client.Change(chgid) - if err != nil { + var chgid string + var err error + + if x.EnsureTag == "" && x.StartupTag == "" { + if x.Positional.ID == "" && x.LastChangeType == "" { + // GetChangeID() below checks for empty change ID / --last, check them early here to provide more helpful error message + return fmt.Errorf("please provide change ID or type with --last=, or query for --ensure= or --startup=") + } + + // GetChangeID takes care of --last=... if change ID was not specified by the user + chgid, err = x.GetChangeID() + if err != nil { + return err + } + } + + // gather debug timings first + var timings []*timingsData + var allEnsures string + if x.All { + allEnsures = "true" + } else { + allEnsures = "false" + } + if err := x.client.DebugGet("change-timings", &timings, map[string]string{"change-id": chgid, "ensure": x.EnsureTag, "all": allEnsures, "startup": x.StartupTag}); err != nil { return err } + w := tabWriter() if x.Verbose { fmt.Fprintf(w, "ID\tStatus\t%11s\t%11s\tLabel\tSummary\n", "Doing", "Undoing") } else { fmt.Fprintf(w, "ID\tStatus\t%11s\t%11s\tSummary\n", "Doing", "Undoing") } - for _, t := range chg.Tasks { - doingTime := formatDuration(timings[t.ID].DoingTime) - if timings[t.ID].DoingTime == 0 { - doingTime = "-" - } - undoingTime := formatDuration(timings[t.ID].UndoingTime) - if timings[t.ID].UndoingTime == 0 { - undoingTime = "-" - } - summary := t.Summary - // Duration formats to 17m14.342s or 2.038s or 970ms, so with - // 11 chars we can go up to 59m59.999s - if x.Verbose { - fmt.Fprintf(w, "%s\t%s\t%11s\t%11s\t%s\t%s\n", t.ID, t.Status, doingTime, undoingTime, t.Kind, summary) - } else { - fmt.Fprintf(w, "%s\t%s\t%11s\t%11s\t%s\n", t.ID, t.Status, doingTime, undoingTime, summary) - } - for _, nested := range timings[t.ID].DoingTimings { - showDoing := true - printTiming(w, &nested, x.Verbose, showDoing) - } - for _, nested := range timings[t.ID].UndoingTimings { - showDoing := false - printTiming(w, &nested, x.Verbose, showDoing) - } + // If a specific change was requested, we expect exactly one timingsData element. + // If "ensure" activity was requested, we may get multiple elements (for multiple executions of the ensure) + if chgid != "" && len(timings) > 0 { + x.printChangeTimings(w, timings[0]) } + + if x.EnsureTag != "" { + x.printEnsureTimings(w, timings) + } + + if x.StartupTag != "" { + x.printStartupTimings(w, timings) + } + w.Flush() fmt.Fprintln(Stdout) diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/cmd_debug_timings_test.go snapd-2.40/cmd/snap/cmd_debug_timings_test.go --- snapd-2.39.2ubuntu0.2/cmd/snap/cmd_debug_timings_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.40/cmd/snap/cmd_debug_timings_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -0,0 +1,238 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2019 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" + "strings" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/cmd/snap" +) + +type timingsCmdArgs struct { + args, stdout, stderr, error string +} + +var timingsTests = []timingsCmdArgs{{ + args: "debug timings", + error: "please provide change ID or type with --last=, or query for --ensure= or --startup=", +}, { + args: "debug timings --ensure=seed 9", + error: "cannot use change id, 'startup' or 'ensure' together", +}, { + args: "debug timings --ensure=seed --startup=ifacemgr", + error: "cannot use change id, 'startup' or 'ensure' together", +}, { + args: "debug timings --last=install --all", + error: "cannot use 'all' with change id or 'last'", +}, { + args: "debug timings --last=remove", + error: `no changes of type "remove" found`, +}, { + args: "debug timings --startup=load-state 9", + error: "cannot use change id, 'startup' or 'ensure' together", +}, { + args: "debug timings --all 9", + error: "cannot use 'all' with change id or 'last'", +}, { + args: "debug timings --last=install", + stdout: "ID Status Doing Undoing Summary\n" + + "40 Doing 910ms - task bar summary\n" + + " ^ 1ms - foo summary\n" + + " ^ 1ms - bar summary\n\n", +}, { + args: "debug timings 1", + stdout: "ID Status Doing Undoing Summary\n" + + "40 Doing 910ms - task bar summary\n" + + " ^ 1ms - foo summary\n" + + " ^ 1ms - bar summary\n\n", +}, { + args: "debug timings 1 --verbose", + stdout: "ID Status Doing Undoing Label Summary\n" + + "40 Doing 910ms - bar task bar summary\n" + + " ^ 1ms - foo foo summary\n" + + " ^ 1ms - bar bar summary\n\n", +}, { + args: "debug timings --ensure=seed", + stdout: "ID Status Doing Undoing Summary\n" + + "seed - - \n" + + " ^ 8ms - baz summary\n" + + " ^ 8ms - booze summary\n" + + "40 Doing 910ms - task bar summary\n" + + " ^ 1ms - foo summary\n" + + " ^ 1ms - bar summary\n\n", +}, { + args: "debug timings --ensure=seed --all", + stdout: "ID Status Doing Undoing Summary\n" + + "seed - - \n" + + " ^ 8ms - bar summary 1\n" + + " ^ 8ms - bar summary 2\n" + + "40 Doing 910ms - task bar summary\n" + + " ^ 1ms - foo summary\n" + + " ^ 1ms - bar summary\n\n", +}, { + args: "debug timings --ensure=seed --all --verbose", + stdout: "ID Status Doing Undoing Label Summary\n" + + "seed - - \n" + + " ^ 8ms - abc bar summary 1\n" + + " ^ 8ms - abc bar summary 2\n" + + "40 Doing 910ms - bar task bar summary\n" + + " ^ 1ms - foo foo summary\n" + + " ^ 1ms - bar bar summary\n\n", +}, { + args: "debug timings --startup=ifacemgr", + stdout: "ID Status Doing Undoing Summary\n" + + "ifacemgr - - \n" + + " ^ 8ms - baz summary\n" + + " ^ 8ms - booze summary\n\n", +}, { + args: "debug timings --startup=ifacemgr --all", + stdout: "ID Status Doing Undoing Summary\n" + + "ifacemgr - - \n" + + " ^ 8ms - baz summary\n" + + " ^ 9ms - baz summary\n\n", +}} + +func (s *SnapSuite) TestGetDebugTimings(c *C) { + s.mockCmdTimingsAPI(c) + + restore := main.MockIsStdinTTY(true) + defer restore() + + for _, test := range timingsTests { + s.stdout.Truncate(0) + s.stderr.Truncate(0) + + c.Logf("Test: %s", test.args) + + _, err := main.Parser(main.Client()).ParseArgs(strings.Fields(test.args)) + if test.error != "" { + c.Check(err, ErrorMatches, test.error) + } else { + c.Check(err, IsNil) + c.Check(s.Stderr(), Equals, test.stderr) + c.Check(s.Stdout(), Equals, test.stdout) + } + } +} + +func (s *SnapSuite) mockCmdTimingsAPI(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Assert(r.Method, Equals, "GET") + + if r.URL.Path == "/v2/debug" { + q := r.URL.Query() + aspect := q.Get("aspect") + c.Assert(aspect, Equals, "change-timings") + + changeID := q.Get("change-id") + ensure := q.Get("ensure") + startup := q.Get("startup") + all := q.Get("all") + + switch { + case changeID == "1": + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[ + {"change-id":"1", "change-timings":{ + "40":{"doing-time":910000000, + "doing-timings":[ + {"label":"foo", "summary": "foo summary", "duration": 1000001}, + {"level":1, "label":"bar", "summary": "bar summary", "duration": 1000002} + ]}}}]}`) + case ensure == "seed" && all == "false": + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[ + {"change-id":"1", + "ensure-timings": [ + {"label":"baz", "summary": "baz summary", "duration": 8000001}, + {"level":1, "label":"booze", "summary": "booze summary", "duration": 8000002} + ], + "change-timings":{ + "40":{"doing-time":910000000, + "doing-timings":[ + {"label":"foo", "summary": "foo summary", "duration": 1000001}, + {"level":1, "label":"bar", "summary": "bar summary", "duration": 1000002} + ]}}}]}`) + case ensure == "seed" && all == "true": + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[ + {"change-id":"1", + "ensure-timings": [ + {"label":"abc", "summary": "bar summary 1", "duration": 8000001}, + {"label":"abc", "summary": "bar summary 2", "duration": 8000002} + ], + "change-timings":{ + "40":{"doing-time":910000000, + "doing-timings":[ + {"label":"foo", "summary": "foo summary", "duration": 1000001}, + {"level":1, "label":"bar", "summary": "bar summary", "duration": 1000002} + ]}}}]}`) + case startup == "ifacemgr" && all == "false": + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[ + {"startup-timings": [ + {"label":"baz", "summary": "baz summary", "duration": 8000001}, + {"level":1, "label":"booze", "summary": "booze summary", "duration": 8000002} + ]}]}`) + case startup == "ifacemgr" && all == "true": + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[ + {"startup-timings": [ + {"label":"baz", "summary": "baz summary", "duration": 8000001}, + {"label":"baz", "summary": "baz summary", "duration": 9000001} + ]}]}`) + default: + c.Errorf("unexpected request: %s, %s, %s", changeID, ensure, all) + } + return + } + + // request for all changes on --last=... + if r.URL.Path == "/v2/changes" { + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[{ + "id": "1", + "kind": "install-snap", + "summary": "a", + "status": "Doing", + "ready": false, + "spawn-time": "2016-04-21T01:02:03Z", + "ready-time": "2016-04-21T01:02:04Z", + "tasks": [{"id":"99", "kind": "bar", "summary": ".", "status": "Doing", "progress": {"done": 0, "total": 1}, "spawn-time": "2016-04-21T01:02:03Z", "ready-time": "2016-04-21T01:02:04Z"}] + }]}`) + return + } + + // request for specific change + if r.URL.Path == "/v2/changes/1" { + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{ + "id": "1", + "kind": "foo", + "summary": "a", + "status": "Doing", + "ready": false, + "spawn-time": "2016-04-21T01:02:03Z", + "ready-time": "2016-04-21T01:02:04Z", + "tasks": [{"id":"40", "kind": "bar", "summary": "task bar summary", "status": "Doing", "progress": {"done": 0, "total": 1}, "spawn-time": "2016-04-21T01:02:03Z", "ready-time": "2016-04-21T01:02:04Z"}] + }}`) + return + } + + c.Errorf("unexpected path %q", r.URL.Path) + }) +} diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/cmd_download.go snapd-2.40/cmd/snap/cmd_download.go --- snapd-2.39.2ubuntu0.2/cmd/snap/cmd_download.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/snap/cmd_download.go 2019-07-12 08:40:08.000000000 +0000 @@ -36,8 +36,11 @@ type cmdDownload struct { channelMixin - Revision string `long:"revision"` + Revision string `long:"revision"` + Basename string `long:"basename"` + TargetDir string `long:"target-directory"` + CohortKey string `long:"cohort"` Positional struct { Snap remoteSnapName } `positional-args:"true" required:"true"` @@ -53,7 +56,14 @@ addCommand("download", shortDownloadHelp, longDownloadHelp, func() flags.Commander { return &cmdDownload{} }, channelDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. "revision": i18n.G("Download the given revision of a snap, to which you must have developer access"), + // TRANSLATORS: This should not start with a lowercase letter. + "cohort": i18n.G("Download from the given cohort"), + // TRANSLATORS: This should not start with a lowercase letter. + "basename": i18n.G("Use this basename for the snap and assertion files (defaults to _)"), + // TRANSLATORS: This should not start with a lowercase letter. + "target-directory": i18n.G("Download to this directory (defaults to the current directory)"), }), []argDesc{{ name: "", // TRANSLATORS: This should not start with a lowercase letter. @@ -88,6 +98,9 @@ } func (x *cmdDownload) Execute(args []string) error { + if strings.ContainsRune(x.Basename, filepath.Separator) { + return fmt.Errorf(i18n.G("cannot specify a path in basename (use --target-dir for that)")) + } if err := x.setChannelFromCommandline(); err != nil { return err } @@ -103,6 +116,9 @@ if x.Channel != "" { return fmt.Errorf(i18n.G("cannot specify both channel and revision")) } + if x.CohortKey != "" { + return fmt.Errorf(i18n.G("cannot specify both cohort and revision")) + } var err error revision, err = snap.ParseRevision(x.Revision) if err != nil { @@ -119,10 +135,13 @@ fmt.Fprintf(Stdout, i18n.G("Fetching snap %q\n"), snapName) dlOpts := image.DownloadOptions{ - TargetDir: "", // cwd + TargetDir: x.TargetDir, + Basename: x.Basename, Channel: x.Channel, + CohortKey: x.CohortKey, + Revision: revision, } - snapPath, snapInfo, err := tsto.DownloadSnap(snapName, revision, &dlOpts) + snapPath, snapInfo, err := tsto.DownloadSnap(snapName, dlOpts) if err != nil { return err } diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/cmd_download_test.go snapd-2.40/cmd/snap/cmd_download_test.go --- snapd-2.39.2ubuntu0.2/cmd/snap/cmd_download_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.40/cmd/snap/cmd_download_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -0,0 +1,61 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2019 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" +) + +// these only cover errors that happen before hitting the network, +// because we're not (yet!) mocking the tooling store + +func (s *SnapSuite) TestDownloadBadBasename(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{ + "download", "--basename=/foo", "a-snap", + }) + + c.Check(err, check.ErrorMatches, "cannot specify a path in basename .use --target-dir for that.") +} + +func (s *SnapSuite) TestDownloadBadChannelCombo(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{ + "download", "--beta", "--channel=foo", "a-snap", + }) + + c.Check(err, check.ErrorMatches, "Please specify a single channel") +} + +func (s *SnapSuite) TestDownloadCohortAndRevision(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{ + "download", "--cohort=what", "--revision=1234", "a-snap", + }) + + c.Check(err, check.ErrorMatches, "cannot specify both cohort and revision") +} + +func (s *SnapSuite) TestDownloadChannelAndRevision(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{ + "download", "--beta", "--revision=1234", "a-snap", + }) + + c.Check(err, check.ErrorMatches, "cannot specify both channel and revision") +} diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/cmd_help.go snapd-2.40/cmd/snap/cmd_help.go --- snapd-2.39.2ubuntu0.2/cmd/snap/cmd_help.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/snap/cmd_help.go 2019-07-12 08:40:08.000000000 +0000 @@ -48,8 +48,18 @@ // 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 + // this means *either* a bare 'snap --help', + // *or* 'snap --help command' + // + // If we return nil in the first case go-flags + // will throw up an ErrCommandRequired on its + // own, but in the second case it'll go on to + // run the command, which is very unexpected. + // + // So we force the ErrCommandRequired here. + + // toplevel --help gets handled via ErrCommandRequired + return &flags.Error{Type: flags.ErrCommandRequired} } // not toplevel, so ask for regular help return &flags.Error{Type: flags.ErrHelp} @@ -206,7 +216,7 @@ }, { Label: i18n.G("Other"), Description: i18n.G("miscellanea"), - Commands: []string{"version", "warnings", "okay", "ack", "known"}, + Commands: []string{"version", "warnings", "okay", "ack", "known", "create-cohort"}, }, { Label: i18n.G("Development"), Description: i18n.G("developer-oriented features"), diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/cmd_help_test.go snapd-2.40/cmd/snap/cmd_help_test.go --- snapd-2.39.2ubuntu0.2/cmd/snap/cmd_help_test.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/snap/cmd_help_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -41,6 +41,7 @@ {"snap", "help"}, {"snap", "--help"}, {"snap", "-h"}, + {"snap", "--help", "install"}, } { s.ResetStdStreams() @@ -194,13 +195,4 @@ err := snap.RunMain() c.Assert(err, check.ErrorMatches, `unknown command "brotato", see 'snap help debug'.`) -} - -func (s *SnapSuite) TestWorseSub(c *check.C) { - origArgs := os.Args - defer func() { os.Args = origArgs }() - os.Args = []string{"snap", "-h", "debug", "brotato"} - - err := snap.RunMain() - c.Assert(err, check.ErrorMatches, `unknown command "brotato", see 'snap help debug'.`) } diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/cmd_info.go snapd-2.40/cmd/snap/cmd_info.go --- snapd-2.39.2ubuntu0.2/cmd/snap/cmd_info.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/snap/cmd_info.go 2019-07-12 08:40:08.000000000 +0000 @@ -35,9 +35,11 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/cmd" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/squashfs" "github.com/snapcore/snapd/strutil" ) @@ -74,106 +76,31 @@ }), nil) } -func norm(path string) string { - path = filepath.Clean(path) - if osutil.IsDirectory(path) { - path = path + "/" - } - - return path -} - -func maybePrintPrice(w io.Writer, snap *client.Snap, resInfo *client.ResultInfo) { - if resInfo == nil { - return - } - price, currency, err := getPrice(snap.Prices, resInfo.SuggestedCurrency) - if err != nil { - return - } - fmt.Fprintf(w, "price:\t%s\n", formatPrice(price, currency)) -} - -func maybePrintType(w io.Writer, t string) { - // XXX: using literals here until we reshuffle snap & client properly - // (and os->core rename happens, etc) - switch t { - case "", "app", "application": - return - case "os": - t = "core" - } - - fmt.Fprintf(w, "type:\t%s\n", t) -} - -func maybePrintID(w io.Writer, snap *client.Snap) { - if snap.ID != "" { - fmt.Fprintf(w, "snap-id:\t%s\n", snap.ID) - } -} - -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, termWidth int) bool { - path = norm(path) - +func clientSnapFromPath(path string) (*client.Snap, error) { snapf, err := snap.Open(path) if err != nil { - return false - } - - var sha3_384 string - if verbose && !osutil.IsDirectory(path) { - var err error - sha3_384, _, err = asserts.SnapFileSHA3_384(path) - if err != nil { - return false - } + return nil, err } - info, err := snap.ReadInfoFromSnapFile(snapf, nil) if err != nil { - return false + return nil, err } - fmt.Fprintf(w, "path:\t%q\n", path) - fmt.Fprintf(w, "name:\t%s\n", info.InstanceName()) - printSummary(w, info.Summary(), termWidth) - - var notes *Notes - if verbose { - fmt.Fprintln(w, "notes:\t") - fmt.Fprintf(w, " confinement:\t%s\n", info.Confinement) - if info.Broken == "" { - fmt.Fprintln(w, " broken:\tfalse") - } else { - fmt.Fprintf(w, " broken:\ttrue (%s)\n", info.Broken) - } - } else { - notes = NotesFromInfo(info) - } - 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) + direct, err := cmd.ClientSnapFromSnapInfo(info) + if err != nil { + return nil, err } - return true + return direct, nil } -func coalesce(snaps ...*client.Snap) *client.Snap { - for _, s := range snaps { - if s != nil { - return s - } +func norm(path string) string { + path = filepath.Clean(path) + if osutil.IsDirectory(path) { + path = path + "/" } - return nil + + return path } // runesTrimRightSpace returns text, with any trailing whitespace dropped. @@ -271,21 +198,6 @@ return err } -func printSummary(w io.Writer, raw string, termWidth int) error { - // simplest way of checking to see if it needs quoting is to try - raw = strings.TrimSpace(raw) - type T struct { - S string - } - if len(raw) == 0 { - raw = `""` - } else if err := yaml.UnmarshalStrict([]byte("s: "+raw), &T{}); err != nil { - raw = strconv.Quote(raw) - } - - return wrapFlow(w, []rune(raw), "summary:\t", termWidth) -} - // printDescr formats a given string (typically a snap description) // in a user friendly way. // @@ -305,37 +217,240 @@ return err } -func maybePrintCommands(w io.Writer, snapName string, allApps []client.AppInfo, n int) { - if len(allApps) == 0 { +type writeflusher interface { + io.Writer + Flush() error +} + +type infoWriter struct { + // fields that are set every iteration + theSnap *client.Snap + diskSnap *client.Snap + localSnap *client.Snap + remoteSnap *client.Snap + resInfo *client.ResultInfo + path string + // fields that don't change and so can be set once + writeflusher + esc *escapes + termWidth int + fmtTime func(time.Time) string + absTime bool + verbose bool +} + +func (iw *infoWriter) setupDiskSnap(path string, diskSnap *client.Snap) { + iw.localSnap, iw.remoteSnap, iw.resInfo = nil, nil, nil + iw.path = path + iw.diskSnap = diskSnap + iw.theSnap = diskSnap +} + +func (iw *infoWriter) setupSnap(localSnap, remoteSnap *client.Snap, resInfo *client.ResultInfo) { + iw.path, iw.diskSnap = "", nil + iw.localSnap = localSnap + iw.remoteSnap = remoteSnap + iw.resInfo = resInfo + if localSnap != nil { + iw.theSnap = localSnap + } else { + iw.theSnap = remoteSnap + } +} + +func (iw *infoWriter) maybePrintPrice() { + if iw.resInfo == nil { + return + } + price, currency, err := getPrice(iw.remoteSnap.Prices, iw.resInfo.SuggestedCurrency) + if err != nil { return } + fmt.Fprintf(iw, "price:\t%s\n", formatPrice(price, currency)) +} - commands := make([]string, 0, len(allApps)) - for _, app := range allApps { +func (iw *infoWriter) maybePrintType() { + // XXX: using literals here until we reshuffle snap & client properly + // (and os->core rename happens, etc) + t := iw.theSnap.Type + switch t { + case "", "app", "application": + return + case "os": + t = "core" + } + + fmt.Fprintf(iw, "type:\t%s\n", t) +} + +func (iw *infoWriter) maybePrintID() { + if iw.theSnap.ID != "" { + fmt.Fprintf(iw, "snap-id:\t%s\n", iw.theSnap.ID) + } +} + +func (iw *infoWriter) maybePrintTrackingChannel() { + if iw.localSnap == nil { + return + } + if iw.localSnap.TrackingChannel == "" { + return + } + fmt.Fprintf(iw, "tracking:\t%s\n", iw.localSnap.TrackingChannel) +} + +func (iw *infoWriter) maybePrintInstallDate() { + if iw.localSnap == nil { + return + } + if iw.localSnap.InstallDate.IsZero() { + return + } + fmt.Fprintf(iw, "refresh-date:\t%s\n", iw.fmtTime(iw.localSnap.InstallDate)) +} + +func (iw *infoWriter) maybePrintChinfo() { + if iw.diskSnap != nil { + return + } + chInfos := channelInfos{ + chantpl: "%s%s:\t%s %s%*s %*s %s\n", + releasedfmt: "2006-01-02", + esc: iw.esc, + } + if iw.absTime { + chInfos.releasedfmt = time.RFC3339 + } + if iw.remoteSnap != nil && iw.remoteSnap.Channels != nil && iw.remoteSnap.Tracks != nil { + iw.Flush() + chInfos.chantpl = "%s%s:\t%s\t%s\t%*s\t%*s\t%s\n" + chInfos.addFromRemote(iw.remoteSnap) + } + if iw.localSnap != nil { + chInfos.addFromLocal(iw.localSnap) + } + chInfos.dump(iw) +} + +func (iw *infoWriter) maybePrintBase() { + if iw.verbose && iw.theSnap.Base != "" { + fmt.Fprintf(iw, "base:\t%s\n", iw.theSnap.Base) + } +} + +func (iw *infoWriter) maybePrintPath() { + if iw.path != "" { + fmt.Fprintf(iw, "path:\t%q\n", iw.path) + } +} + +func (iw *infoWriter) printName() { + fmt.Fprintf(iw, "name:\t%s\n", iw.theSnap.Name) +} + +func (iw *infoWriter) printSummary() { + // simplest way of checking to see if it needs quoting is to try + raw := strings.TrimSpace(iw.theSnap.Summary) + type T struct { + S string + } + if len(raw) == 0 { + raw = `""` + } else if err := yaml.UnmarshalStrict([]byte("s: "+raw), &T{}); err != nil { + raw = strconv.Quote(raw) + } + + wrapFlow(iw, []rune(raw), "summary:\t", iw.termWidth) +} + +func (iw *infoWriter) maybePrintPublisher() { + if iw.diskSnap != nil { + // snaps read from disk won't have a publisher + return + } + fmt.Fprintf(iw, "publisher:\t%s\n", longPublisher(iw.esc, iw.theSnap.Publisher)) +} + +func (iw *infoWriter) maybePrintStandaloneVersion() { + if iw.diskSnap == nil { + // snaps not read from disk will have version information shown elsewhere + return + } + version := iw.diskSnap.Version + if version == "" { + version = iw.esc.dash + } + // NotesFromRemote might be better called NotesFromNotInstalled but that's nasty + fmt.Fprintf(iw, "version:\t%s %s\n", version, NotesFromRemote(iw.diskSnap, nil)) +} + +func (iw *infoWriter) maybePrintBuildDate() { + if iw.diskSnap == nil { + return + } + if osutil.IsDirectory(iw.path) { + return + } + buildDate := squashfs.BuildDate(iw.path) + if buildDate.IsZero() { + return + } + fmt.Fprintf(iw, "build-date:\t%s\n", iw.fmtTime(buildDate)) +} + +func (iw *infoWriter) maybePrintContact() error { + contact := strings.TrimPrefix(iw.theSnap.Contact, "mailto:") + if contact == "" { + return nil + } + _, err := fmt.Fprintf(iw, "contact:\t%s\n", contact) + return err +} + +func (iw *infoWriter) printLicense() { + license := iw.theSnap.License + if license == "" { + license = "unset" + } + fmt.Fprintf(iw, "license:\t%s\n", license) +} + +func (iw *infoWriter) printDescr() { + fmt.Fprintln(iw, "description: |") + printDescr(iw, iw.theSnap.Description, iw.termWidth) +} + +func (iw *infoWriter) maybePrintCommands() { + if len(iw.theSnap.Apps) == 0 { + return + } + + commands := make([]string, 0, len(iw.theSnap.Apps)) + for _, app := range iw.theSnap.Apps { if app.IsService() { continue } - cmdStr := snap.JoinSnapApp(snapName, app.Name) + cmdStr := snap.JoinSnapApp(iw.theSnap.Name, app.Name) commands = append(commands, cmdStr) } if len(commands) == 0 { return } - fmt.Fprintf(w, "commands:\n") + fmt.Fprintf(iw, "commands:\n") for _, cmd := range commands { - fmt.Fprintf(w, " - %s\n", cmd) + fmt.Fprintf(iw, " - %s\n", cmd) } } -func maybePrintServices(w io.Writer, snapName string, allApps []client.AppInfo, n int) { - if len(allApps) == 0 { +func (iw *infoWriter) maybePrintServices() { + if len(iw.theSnap.Apps) == 0 { return } - services := make([]string, 0, len(allApps)) - for _, app := range allApps { + services := make([]string, 0, len(iw.theSnap.Apps)) + for _, app := range iw.theSnap.Apps { if !app.IsService() { continue } @@ -351,16 +466,78 @@ } else { enabled = "disabled" } - services = append(services, fmt.Sprintf(" %s:\t%s, %s, %s", snap.JoinSnapApp(snapName, app.Name), app.Daemon, enabled, active)) + services = append(services, fmt.Sprintf(" %s:\t%s, %s, %s", snap.JoinSnapApp(iw.theSnap.Name, app.Name), app.Daemon, enabled, active)) } if len(services) == 0 { return } - fmt.Fprintf(w, "services:\n") + fmt.Fprintf(iw, "services:\n") for _, svc := range services { - fmt.Fprintln(w, svc) + fmt.Fprintln(iw, svc) + } +} + +func (iw *infoWriter) maybePrintNotes() { + if !iw.verbose { + return + } + fmt.Fprintln(iw, "notes:\t") + fmt.Fprintf(iw, " private:\t%t\n", iw.theSnap.Private) + fmt.Fprintf(iw, " confinement:\t%s\n", iw.theSnap.Confinement) + if iw.localSnap == nil { + return + } + jailMode := iw.localSnap.Confinement == client.DevModeConfinement && !iw.localSnap.DevMode + fmt.Fprintf(iw, " devmode:\t%t\n", iw.localSnap.DevMode) + fmt.Fprintf(iw, " jailmode:\t%t\n", jailMode) + fmt.Fprintf(iw, " trymode:\t%t\n", iw.localSnap.TryMode) + fmt.Fprintf(iw, " enabled:\t%t\n", iw.localSnap.Status == client.StatusActive) + if iw.localSnap.Broken == "" { + fmt.Fprintf(iw, " broken:\t%t\n", false) + } else { + fmt.Fprintf(iw, " broken:\t%t (%s)\n", true, iw.localSnap.Broken) + } + + fmt.Fprintf(iw, " ignore-validation:\t%t\n", iw.localSnap.IgnoreValidation) + return +} + +func (iw *infoWriter) maybePrintCohortKey() { + if !iw.verbose { + return + } + if iw.localSnap == nil { + return + } + coh := iw.localSnap.CohortKey + if coh == "" { + return } + if isStdoutTTY { + // 15 is 1 + the length of "refresh-date: " + coh = strutil.ElliptLeft(iw.localSnap.CohortKey, iw.termWidth-15) + } + fmt.Fprintf(iw, "cohort:\t%s\n", coh) +} + +func (iw *infoWriter) maybePrintSum() { + if !iw.verbose { + return + } + if iw.diskSnap == nil { + // TODO: expose the sha via /v2/snaps and /v2/find + return + } + if osutil.IsDirectory(iw.path) { + // no sha3_384 of a directory :-) + return + } + sha3_384, _, _ := asserts.SnapFileSHA3_384(iw.path) + if sha3_384 == "" { + return + } + fmt.Fprintf(iw, "sha3-384:\t%s\n", sha3_384) } var channelRisks = []string{"stable", "candidate", "beta", "edge"} @@ -457,10 +634,18 @@ esc := x.getEscapes() w := tabwriter.NewWriter(Stdout, 2, 2, 1, ' ', 0) + iw := &infoWriter{ + writeflusher: w, + esc: esc, + termWidth: termWidth, + verbose: x.Verbose, + fmtTime: x.fmtTime, + absTime: x.AbsTime, + } noneOK := true for i, snapName := range x.Positional.Snaps { - snapName := string(snapName) + snapName := norm(string(snapName)) if i > 0 { fmt.Fprintln(w, "---") } @@ -469,17 +654,18 @@ continue } - if tryDirect(w, snapName, x.Verbose, termWidth) { - noneOK = false - continue + if diskSnap, err := clientSnapFromPath(snapName); err == nil { + iw.setupDiskSnap(snapName, diskSnap) + } else { + remoteSnap, resInfo, _ := x.client.FindOne(snapName) + localSnap, _, _ := x.client.Snap(snapName) + iw.setupSnap(localSnap, remoteSnap, resInfo) } - remote, resInfo, _ := x.client.FindOne(snapName) - local, _, _ := x.client.Snap(snapName) - - both := coalesce(local, remote) + // note diskSnap == nil, or localSnap == nil and remoteSnap == nil - if both == nil { + if iw.theSnap == nil { if len(x.Positional.Snaps) == 1 { + w.Flush() return fmt.Errorf("no snap found for %q", snapName) } @@ -488,76 +674,29 @@ } noneOK = false - fmt.Fprintf(w, "name:\t%s\n", both.Name) - printSummary(w, both.Summary, termWidth) - 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 = "unset" - } - fmt.Fprintf(w, "license:\t%s\n", license) - maybePrintPrice(w, remote, resInfo) - fmt.Fprintln(w, "description: |") - printDescr(w, both.Description, termWidth) - maybePrintCommands(w, snapName, both.Apps, termWidth) - maybePrintServices(w, snapName, both.Apps, termWidth) - - if x.Verbose { - fmt.Fprintln(w, "notes:\t") - fmt.Fprintf(w, " private:\t%t\n", both.Private) - fmt.Fprintf(w, " confinement:\t%s\n", both.Confinement) - } - - if local != nil { - if x.Verbose { - jailMode := local.Confinement == client.DevModeConfinement && !local.DevMode - fmt.Fprintf(w, " devmode:\t%t\n", local.DevMode) - fmt.Fprintf(w, " jailmode:\t%t\n", jailMode) - fmt.Fprintf(w, " trymode:\t%t\n", local.TryMode) - fmt.Fprintf(w, " enabled:\t%t\n", local.Status == client.StatusActive) - if local.Broken == "" { - fmt.Fprintf(w, " broken:\t%t\n", false) - } else { - fmt.Fprintf(w, " broken:\t%t (%s)\n", true, local.Broken) - } - - fmt.Fprintf(w, " ignore-validation:\t%t\n", local.IgnoreValidation) - } - } + iw.maybePrintPath() + iw.printName() + iw.printSummary() + iw.maybePrintPublisher() + iw.maybePrintStandaloneVersion() + iw.maybePrintBuildDate() + iw.maybePrintContact() + iw.printLicense() + iw.maybePrintPrice() + iw.printDescr() + iw.maybePrintCommands() + iw.maybePrintServices() + iw.maybePrintNotes() // 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) - if local != nil { - 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)) - } - } - - chInfos := channelInfos{ - chantpl: "%s%s:\t%s %s%*s %*s %s\n", - releasedfmt: "2006-01-02", - esc: esc, - } - if x.AbsTime { - chInfos.releasedfmt = time.RFC3339 - } - if remote != nil && remote.Channels != nil && remote.Tracks != nil { - w.Flush() - chInfos.chantpl = "%s%s:\t%s\t%s\t%*s\t%*s\t%s\n" - chInfos.addFromRemote(remote) - } - if local != nil { - chInfos.addFromLocal(local) - } - chInfos.dump(w) + iw.Flush() + iw.maybePrintType() + iw.maybePrintBase() + iw.maybePrintSum() + iw.maybePrintID() + iw.maybePrintCohortKey() + iw.maybePrintTrackingChannel() + iw.maybePrintInstallDate() + iw.maybePrintChinfo() } w.Flush() diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/cmd_info_test.go snapd-2.40/cmd/snap/cmd_info_test.go --- snapd-2.39.2ubuntu0.2/cmd/snap/cmd_info_test.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/snap/cmd_info_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -22,13 +22,18 @@ import ( "bytes" "fmt" + "io/ioutil" "net/http" + "path/filepath" "time" "gopkg.in/check.v1" "github.com/snapcore/snapd/client" snap "github.com/snapcore/snapd/cmd/snap" + snaplib "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/snap/squashfs" ) var cmdAppInfos = []client.AppInfo{{Name: "app1"}, {Name: "app2"}} @@ -55,10 +60,17 @@ var _ = check.Suite(&infoSuite{}) +type flushBuffer struct{ bytes.Buffer } + +func (*flushBuffer) Flush() error { return nil } + func (s *infoSuite) TestMaybePrintServices(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) for _, infos := range [][]client.AppInfo{svcAppInfos, mixedAppInfos} { - var buf bytes.Buffer - snap.MaybePrintServices(&buf, "foo", infos, -1) + buf.Reset() + snap.SetupDiskSnap(iw, "", &client.Snap{Name: "foo", Apps: infos}) + snap.MaybePrintServices(iw) c.Check(buf.String(), check.Equals, `services: foo.svc1: simple, disabled, active @@ -68,18 +80,23 @@ } func (s *infoSuite) TestMaybePrintServicesNoServices(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) for _, infos := range [][]client.AppInfo{cmdAppInfos, nil} { - var buf bytes.Buffer - snap.MaybePrintServices(&buf, "foo", infos, -1) - + buf.Reset() + snap.SetupDiskSnap(iw, "", &client.Snap{Name: "foo", Apps: infos}) + snap.MaybePrintServices(iw) c.Check(buf.String(), check.Equals, "") } } func (s *infoSuite) TestMaybePrintCommands(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) for _, infos := range [][]client.AppInfo{cmdAppInfos, mixedAppInfos} { - var buf bytes.Buffer - snap.MaybePrintCommands(&buf, "foo", infos, -1) + buf.Reset() + snap.SetupDiskSnap(iw, "", &client.Snap{Name: "foo", Apps: infos}) + snap.MaybePrintCommands(iw) c.Check(buf.String(), check.Equals, `commands: - foo.app1 @@ -89,14 +106,302 @@ } func (s *infoSuite) TestMaybePrintCommandsNoCommands(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) for _, infos := range [][]client.AppInfo{svcAppInfos, nil} { - var buf bytes.Buffer - snap.MaybePrintCommands(&buf, "foo", infos, -1) + buf.Reset() + snap.SetupDiskSnap(iw, "", &client.Snap{Name: "foo", Apps: infos}) + snap.MaybePrintCommands(iw) c.Check(buf.String(), check.Equals, "") } } +func (infoSuite) TestPrintType(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) + for from, to := range map[string]string{ + "": "", + "app": "", + "application": "", + "gadget": "type:\tgadget\n", + "core": "type:\tcore\n", + "os": "type:\tcore\n", + } { + buf.Reset() + snap.SetupDiskSnap(iw, "", &client.Snap{Type: from}) + snap.MaybePrintType(iw) + c.Check(buf.String(), check.Equals, to, check.Commentf("%q", from)) + } +} + +func (infoSuite) TestPrintSummary(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) + for from, to := range map[string]string{ + "": `""`, // empty results in quoted empty + "foo": "foo", // plain text results in unquoted + "two words": "two words", // ...even when multi-word + "{": `"{"`, // but yaml-breaking is quoted + "very long text": "very long\n text", // too-long text gets split (TODO: split with tabbed indent to preserve alignment) + } { + buf.Reset() + snap.SetupDiskSnap(iw, "", &client.Snap{Summary: from}) + snap.PrintSummary(iw) + c.Check(buf.String(), check.Equals, "summary:\t"+to+"\n", check.Commentf("%q", from)) + } +} + +func (s *infoSuite) TestMaybePrintPublisher(c *check.C) { + acct := &snaplib.StoreAccount{ + Validation: "verified", + Username: "team-potato", + DisplayName: "Team Potato", + } + + type T struct { + diskSnap, localSnap *client.Snap + expected string + } + + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) + for i, t := range []T{ + {&client.Snap{}, nil, ""}, // nothing output for on-disk snap + {nil, &client.Snap{}, "publisher:\t--\n"}, // from-snapd snap with no publisher is explicit + {nil, &client.Snap{Publisher: acct}, "publisher:\tTeam Potato*\n"}, + } { + buf.Reset() + if t.diskSnap == nil { + snap.SetupSnap(iw, t.localSnap, nil, nil) + } else { + snap.SetupDiskSnap(iw, "", t.diskSnap) + } + snap.MaybePrintPublisher(iw) + c.Check(buf.String(), check.Equals, t.expected, check.Commentf("%d", i)) + } +} + +func (s *infoSuite) TestMaybePrintNotes(c *check.C) { + type T struct { + localSnap, diskSnap *client.Snap + expected string + } + + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) + for i, t := range []T{ + { + nil, + &client.Snap{Private: true, Confinement: "devmode"}, + "notes:\t\n" + + " private:\ttrue\n" + + " confinement:\tdevmode\n", + }, { + &client.Snap{Private: true, Confinement: "devmode"}, + nil, + "notes:\t\n" + + " private:\ttrue\n" + + " confinement:\tdevmode\n" + + " devmode:\tfalse\n" + + " jailmode:\ttrue\n" + + " trymode:\tfalse\n" + + " enabled:\tfalse\n" + + " broken:\tfalse\n" + + " ignore-validation:\tfalse\n", + }, { + &client.Snap{Private: true, Confinement: "devmode", Broken: "ouch"}, + nil, + "notes:\t\n" + + " private:\ttrue\n" + + " confinement:\tdevmode\n" + + " devmode:\tfalse\n" + + " jailmode:\ttrue\n" + + " trymode:\tfalse\n" + + " enabled:\tfalse\n" + + " broken:\ttrue (ouch)\n" + + " ignore-validation:\tfalse\n", + }, + } { + buf.Reset() + snap.SetVerbose(iw, false) + if t.diskSnap == nil { + snap.SetupSnap(iw, t.localSnap, nil, nil) + } else { + snap.SetupDiskSnap(iw, "", t.diskSnap) + } + snap.MaybePrintNotes(iw) + c.Check(buf.String(), check.Equals, "", check.Commentf("%d/false", i)) + + buf.Reset() + snap.SetVerbose(iw, true) + snap.MaybePrintNotes(iw) + c.Check(buf.String(), check.Equals, t.expected, check.Commentf("%d/true", i)) + } +} + +func (s *infoSuite) TestMaybePrintStandaloneVersion(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) + + // no disk snap -> no version + snap.MaybePrintStandaloneVersion(iw) + c.Check(buf.String(), check.Equals, "") + + for version, expected := range map[string]string{ + "": "--", + "4.2": "4.2", + } { + buf.Reset() + snap.SetupDiskSnap(iw, "", &client.Snap{Version: version}) + snap.MaybePrintStandaloneVersion(iw) + c.Check(buf.String(), check.Equals, "version:\t"+expected+" -\n", check.Commentf("%q", version)) + + buf.Reset() + snap.SetupDiskSnap(iw, "", &client.Snap{Version: version, Confinement: "devmode"}) + snap.MaybePrintStandaloneVersion(iw) + c.Check(buf.String(), check.Equals, "version:\t"+expected+" devmode\n", check.Commentf("%q", version)) + } +} + +func (s *infoSuite) TestMaybePrintBuildDate(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) + // some prep + dir := c.MkDir() + arbfile := filepath.Join(dir, "arb") + c.Assert(ioutil.WriteFile(arbfile, nil, 0600), check.IsNil) + filename := filepath.Join(c.MkDir(), "foo.snap") + diskSnap := squashfs.New(filename) + c.Assert(diskSnap.Build(dir, "app"), check.IsNil) + buildDate := diskSnap.BuildDate().Format(time.Kitchen) + + // no disk snap -> no build date + snap.MaybePrintBuildDate(iw) + c.Check(buf.String(), check.Equals, "") + + // path is directory -> no build date + buf.Reset() + snap.SetupDiskSnap(iw, dir, &client.Snap{}) + snap.MaybePrintBuildDate(iw) + c.Check(buf.String(), check.Equals, "") + + // not actually a snap -> no build date + buf.Reset() + snap.SetupDiskSnap(iw, arbfile, &client.Snap{}) + snap.MaybePrintBuildDate(iw) + c.Check(buf.String(), check.Equals, "") + + // disk snap -> get build date + buf.Reset() + snap.SetupDiskSnap(iw, filename, &client.Snap{}) + snap.MaybePrintBuildDate(iw) + c.Check(buf.String(), check.Equals, "build-date:\t"+buildDate+"\n") +} + +func (s *infoSuite) TestMaybePrintSum(c *check.C) { + var buf flushBuffer + // some prep + dir := c.MkDir() + filename := filepath.Join(c.MkDir(), "foo.snap") + diskSnap := squashfs.New(filename) + c.Assert(diskSnap.Build(dir, "app"), check.IsNil) + iw := snap.NewInfoWriter(&buf) + snap.SetVerbose(iw, true) + + // no disk snap -> no checksum + snap.MaybePrintSum(iw) + c.Check(buf.String(), check.Equals, "") + + // path is directory -> no checksum + buf.Reset() + snap.SetupDiskSnap(iw, dir, &client.Snap{}) + snap.MaybePrintSum(iw) + c.Check(buf.String(), check.Equals, "") + + // disk snap and verbose -> get checksum + buf.Reset() + snap.SetupDiskSnap(iw, filename, &client.Snap{}) + snap.MaybePrintSum(iw) + c.Check(buf.String(), check.Matches, "sha3-384:\t\\S+\n") + + // disk snap but not verbose -> no checksum + buf.Reset() + snap.SetVerbose(iw, false) + snap.MaybePrintSum(iw) + c.Check(buf.String(), check.Equals, "") +} + +func (s *infoSuite) TestMaybePrintContact(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) + + for contact, expected := range map[string]string{ + "": "", + "mailto:joe@example.com": "contact:\tjoe@example.com\n", + "foo": "contact:\tfoo\n", + } { + buf.Reset() + snap.SetupDiskSnap(iw, "", &client.Snap{Contact: contact}) + snap.MaybePrintContact(iw) + c.Check(buf.String(), check.Equals, expected, check.Commentf("%q", contact)) + } +} + +func (s *infoSuite) TestMaybePrintBase(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) + dSnap := &client.Snap{} + snap.SetupDiskSnap(iw, "", dSnap) + + // no verbose -> no base + snap.SetVerbose(iw, false) + snap.MaybePrintBase(iw) + c.Check(buf.String(), check.Equals, "") + buf.Reset() + + // no base -> no base :) + snap.SetVerbose(iw, true) + snap.MaybePrintBase(iw) + c.Check(buf.String(), check.Equals, "") + buf.Reset() + + // base + verbose -> base + dSnap.Base = "xyzzy" + snap.MaybePrintBase(iw) + c.Check(buf.String(), check.Equals, "base:\txyzzy\n") + buf.Reset() +} + +func (s *infoSuite) TestMaybePrintPath(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) + dSnap := &client.Snap{} + + // no path -> no path + snap.SetupDiskSnap(iw, "", dSnap) + snap.MaybePrintPath(iw) + c.Check(buf.String(), check.Equals, "") + buf.Reset() + + // path -> path (quoted!) + snap.SetupDiskSnap(iw, "xyzzy", dSnap) + snap.MaybePrintPath(iw) + c.Check(buf.String(), check.Equals, "path:\t\"xyzzy\"\n") + buf.Reset() +} + +func (s *infoSuite) TestClientSnapFromPath(c *check.C) { + // minimal sanity check + fn := snaptest.MakeTestSnapWithFiles(c, ` +name: some-snap +version: 9 +`, nil) + dSnap, err := snap.ClientSnapFromPath(fn) + c.Assert(err, check.IsNil) + c.Check(dSnap.Version, check.Equals, "9") +} + func (s *infoSuite) TestInfoPricedNarrowTerminal(c *check.C) { defer snap.MockTermSize(func() (int, int) { return 44, 25 })() @@ -390,6 +695,28 @@ c.Check(s.Stderr(), check.Equals, "") } +func (s *infoSuite) TestInfoNotFound(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n % 2 { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/x") + } + w.WriteHeader(404) + fmt.Fprintln(w, `{"type":"error","status-code":404,"status":"Not Found","result":{"message":"No.","kind":"snap-not-found","value":"x"}}`) + + n++ + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "--verbose", "/x"}) + c.Check(err, check.ErrorMatches, `no snap found for "/x"`) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + func (s *infoSuite) TestInfoWithLocalNoLicense(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { @@ -590,6 +917,45 @@ } } +func (infoSuite) TestMaybePrintCohortKey(c *check.C) { + type T struct { + snap *client.Snap + verbose bool + expected string + } + + tests := []T{ + {snap: nil, verbose: false, expected: ""}, + {snap: nil, verbose: true, expected: ""}, + {snap: &client.Snap{}, verbose: false, expected: ""}, + {snap: &client.Snap{}, verbose: true, expected: ""}, + {snap: &client.Snap{CohortKey: "some-cohort-key"}, verbose: false, expected: ""}, + {snap: &client.Snap{CohortKey: "some-cohort-key"}, verbose: true, expected: "cohort:\t…-key\n"}, + } + + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) + defer snap.MockIsStdoutTTY(true)() + + for i, t := range tests { + buf.Reset() + snap.SetupSnap(iw, t.snap, nil, nil) + snap.SetVerbose(iw, t.verbose) + snap.MaybePrintCohortKey(iw) + c.Check(buf.String(), check.Equals, t.expected, check.Commentf("tty:true/%d", i)) + } + // now the same but without a tty -> the last test should no longer ellipt + tests[len(tests)-1].expected = "cohort:\tsome-cohort-key\n" + snap.MockIsStdoutTTY(false) + for i, t := range tests { + buf.Reset() + snap.SetupSnap(iw, t.snap, nil, nil) + snap.SetVerbose(iw, t.verbose) + snap.MaybePrintCohortKey(iw) + c.Check(buf.String(), check.Equals, t.expected, check.Commentf("tty:false/%d", i)) + } +} + func (infoSuite) TestWrapCornerCase(c *check.C) { // this particular corner case isn't currently reachable from // printDescr nor printSummary, but best to have it covered diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/cmd_run.go snapd-2.40/cmd/snap/cmd_run.go --- snapd-2.39.2ubuntu0.2/cmd/snap/cmd_run.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/snap/cmd_run.go 2019-07-12 08:40:08.000000000 +0000 @@ -897,6 +897,11 @@ if info.NeedsClassic() { cmd = append(cmd, "--classic") } + + // this should never happen since we validate snaps with "base: none" and do not allow hooks/apps + if info.Base == "none" { + return fmt.Errorf(`cannot run hooks / applications with base "none"`) + } if info.Base != "" { cmd = append(cmd, "--base", info.Base) } diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/cmd_run_test.go snapd-2.40/cmd/snap/cmd_run_test.go --- snapd-2.39.2ubuntu0.2/cmd/snap/cmd_run_test.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/snap/cmd_run_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -50,15 +50,30 @@ configure: `) +var mockYamlBaseNone1 = []byte(`name: snapname1 +version: 1.0 +base: none +apps: + app: + command: run-app +`) + +var mockYamlBaseNone2 = []byte(`name: snapname2 +version: 1.0 +base: none +hooks: + configure: +`) + type RunSuite struct { fakeHome string - SnapSuite + BaseSnapSuite } var _ = check.Suite(&RunSuite{}) func (s *RunSuite) SetUpTest(c *check.C) { - s.SnapSuite.SetUpTest(c) + s.BaseSnapSuite.SetUpTest(c) s.fakeHome = c.MkDir() u, err := user.Current() @@ -94,6 +109,24 @@ c.Check(err, check.ErrorMatches, ".*too many arguments for hook \"configure\": bar.*") } +func (s *RunSuite) TestRunCmdWithBaseNone(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYamlBaseNone1), &snap.SideInfo{ + Revision: snap.R("1"), + }) + snaptest.MockSnapCurrent(c, string(mockYamlBaseNone2), &snap.SideInfo{ + Revision: snap.R("1"), + }) + + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname1.app", "--arg1", "arg2"}) + c.Assert(err, check.ErrorMatches, `cannot run hooks / applications with base \"none\"`) + + _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "--", "snapname2"}) + c.Assert(err, check.ErrorMatches, `cannot run hooks / applications with base \"none\"`) +} + func (s *RunSuite) TestSnapRunWhenMissingConfine(c *check.C) { _, r := logger.MockLogger() defer r() diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/cmd_snap_op.go snapd-2.40/cmd/snap/cmd_snap_op.go --- snapd-2.39.2ubuntu0.2/cmd/snap/cmd_snap_op.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/snap/cmd_snap_op.go 2019-07-12 08:40:08.000000000 +0000 @@ -36,6 +36,7 @@ "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/strutil" ) var ( @@ -113,6 +114,7 @@ waitMixin Revision string `long:"revision"` + Purge bool `long:"purge"` Positional struct { Snaps []installedSnapName `positional-arg-name:"" required:"1"` } `positional-args:"yes" required:"yes"` @@ -184,7 +186,7 @@ } func (x *cmdRemove) Execute([]string) error { - opts := &client.SnapOptions{Revision: x.Revision} + opts := &client.SnapOptions{Revision: x.Revision, Purge: x.Purge} if len(x.Positional.Snaps) == 1 { return x.removeOne(opts) } @@ -412,6 +414,7 @@ Name string `long:"name"` + Cohort string `long:"cohort"` Positional struct { Snaps []remoteSnapName `positional-arg-name:""` } `positional-args:"yes" required:"yes"` @@ -457,6 +460,7 @@ } } + // TODO: mention details of the install (e.g. like switch does) return showDone(x.client, []string{snapName}, "install", opts, x.getEscapes()) } @@ -531,6 +535,7 @@ Revision: x.Revision, Dangerous: dangerous, Unaliased: x.Unaliased, + CohortKey: x.Cohort, } x.setModes(opts) @@ -567,6 +572,8 @@ Amend bool `long:"amend"` Revision string `long:"revision"` + Cohort string `long:"cohort"` + LeaveCohort bool `long:"leave-cohort"` List bool `long:"list"` Time bool `long:"time"` IgnoreValidation bool `long:"ignore-validation"` @@ -621,6 +628,8 @@ return err } + // TODO: this doesn't really tell about all the things you + // could set while refreshing (something switch does) return showDone(x.client, []string{name}, "refresh", opts, x.getEscapes()) } @@ -734,6 +743,8 @@ Channel: x.Channel, IgnoreValidation: x.IgnoreValidation, Revision: x.Revision, + CohortKey: x.Cohort, + LeaveCohort: x.LeaveCohort, } x.setModes(opts) return x.refreshOne(names[0], opts) @@ -952,6 +963,9 @@ waitMixin channelMixin + Cohort string `long:"cohort"` + LeaveCohort bool `long:"leave-cohort"` + Positional struct { Snap installedSnapName `positional-arg-name:"" required:"1"` } `positional-args:"yes" required:"yes"` @@ -961,14 +975,43 @@ if err := x.setChannelFromCommandline(); err != nil { return err } - if x.Channel == "" { - return fmt.Errorf("missing --channel= parameter") - } name := string(x.Positional.Snap) channel := string(x.Channel) + + var msg string + // some duplication between this and the two other switch-summarisers... + // in this one, we have three boolean things to check, meaning 2³=8 possibilities + // of which 3 are errors (which is why we look at this before running it) + switchCohort := x.Cohort != "" + switchChannel := x.Channel != "" + switch { + case switchCohort && x.LeaveCohort: + // this one counts as two (no channel filter) + return fmt.Errorf(i18n.G("cannot specify both --cohort and --leave-cohort")) + case switchCohort && !x.LeaveCohort && !switchChannel: + // TRANSLATORS: the first %q will be the (quoted) snap name, the second an ellipted cohort string + msg = fmt.Sprintf(i18n.G("%q switched to the %q cohort\n"), name, strutil.ElliptLeft(x.Cohort, 10)) + case switchCohort && !x.LeaveCohort && switchChannel: + // TRANSLATORS: the first %q will be the (quoted) snap name, the second a channel, the third an ellipted cohort string + msg = fmt.Sprintf(i18n.G("%q switched to the %q channel and the %q cohort\n"), name, channel, strutil.ElliptLeft(x.Cohort, 10)) + case !switchCohort && !x.LeaveCohort && switchChannel: + // TRANSLATORS: the first %q will be the (quoted) snap name, the second a channel + msg = fmt.Sprintf(i18n.G("%q switched to the %q channel\n"), name, channel) + case !switchCohort && x.LeaveCohort && switchChannel: + // TRANSLATORS: the first %q will be the (quoted) snap name, the second a channel + msg = fmt.Sprintf(i18n.G("%q left the cohort, and switched to the %q channel"), name, channel) + case !switchCohort && x.LeaveCohort && !switchChannel: + // TRANSLATORS: %q will be the (quoted) snap name + msg = fmt.Sprintf(i18n.G("%q left the cohort"), name) + case !switchCohort && !x.LeaveCohort && !switchChannel: + return fmt.Errorf(i18n.G("nothing to switch; specify --channel (and/or one of --cohort/--leave-cohort)")) + } // and that's the 8 \o/ + opts := &client.SnapOptions{ - Channel: channel, + Channel: channel, + CohortKey: x.Cohort, + LeaveCohort: x.LeaveCohort, } changeID, err := x.client.Switch(name, opts) if err != nil { @@ -982,7 +1025,7 @@ return err } - fmt.Fprintf(Stdout, i18n.G("%q switched to the %q channel\n"), name, channel) + fmt.Fprintln(Stdout, msg) return nil } @@ -991,6 +1034,8 @@ waitDescs.also(map[string]string{ // TRANSLATORS: This should not start with a lowercase letter. "revision": i18n.G("Remove only the given revision"), + // TRANSLATORS: This should not start with a lowercase letter. + "purge": i18n.G("Remove the snap without saving a snapshot of its data"), }), nil) addCommand("install", shortInstallHelp, longInstallHelp, func() flags.Commander { return &cmdInstall{} }, colorDescs.also(waitDescs).also(channelDescs).also(modeDescs).also(map[string]string{ @@ -1004,6 +1049,8 @@ "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"), + // TRANSLATORS: This should not start with a lowercase letter. + "cohort": i18n.G("Install the snap in the given cohort"), }), nil) addCommand("refresh", shortRefreshHelp, longRefreshHelp, func() flags.Commander { return &cmdRefresh{} }, colorDescs.also(waitDescs).also(channelDescs).also(modeDescs).also(timeDescs).also(map[string]string{ @@ -1017,6 +1064,10 @@ "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"), + // TRANSLATORS: This should not start with a lowercase letter. + "cohort": i18n.G("Refresh the snap into the given cohort"), + // TRANSLATORS: This should not start with a lowercase letter. + "leave-cohort": i18n.G("Refresh the snap out of its cohort"), }), 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) @@ -1025,5 +1076,10 @@ // 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{} }, waitDescs.also(channelDescs), nil) + addCommand("switch", shortSwitchHelp, longSwitchHelp, func() flags.Commander { return &cmdSwitch{} }, waitDescs.also(channelDescs).also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "cohort": i18n.G("Switch the snap into the given cohort"), + // TRANSLATORS: This should not start with a lowercase letter. + "leave-cohort": i18n.G("Switch the snap out of its cohort"), + }), nil) } diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/cmd_snap_op_test.go snapd-2.40/cmd/snap/cmd_snap_op_test.go --- snapd-2.39.2ubuntu0.2/cmd/snap/cmd_snap_op_test.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/snap/cmd_snap_op_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -191,14 +191,15 @@ 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": "install", - "channel": "candidate", + "action": "install", + "channel": "candidate", + "cohort-key": "what", }) s.srv.channel = "candidate" } s.RedirectClientToTestServer(s.srv.handle) - rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "candidate", "foo"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "candidate", "--cohort", "what", "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`) @@ -1178,6 +1179,36 @@ c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(beta\) 1.0 from Bar refreshed`) } +func (s *SnapOpSuite) TestRefreshOneSwitchCohort(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/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + "cohort-key": "what", + }) + } + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--cohort=what", "foo"}) + c.Assert(err, check.IsNil) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar refreshed`) +} + +func (s *SnapOpSuite) TestRefreshOneLeaveCohort(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/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + "leave-cohort": true, + }) + } + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--leave-cohort", "foo"}) + c.Assert(err, check.IsNil) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar refreshed`) +} + func (s *SnapOpSuite) TestRefreshOneWithPinnedTrack(c *check.C) { s.RedirectClientToTestServer(s.srv.handle) s.srv.checker = func(r *http.Request) { @@ -1560,6 +1591,26 @@ c.Check(s.srv.n, check.Equals, s.srv.total) } +func (s *SnapOpSuite) TestRemoveWithPurge(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", + "purge": true, + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"remove", "--purge", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 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) TestRemoveRevision(c *check.C) { s.srv.total = 3 s.srv.checker = func(r *http.Request) { @@ -1844,6 +1895,88 @@ c.Check(s.srv.n, check.Equals, s.srv.total) } +func (s *SnapOpSuite) TestSwitchHappyCohort(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": "switch", + "cohort-key": "what", + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "--cohort=what", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*"foo" switched to the "what" cohort`) + 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) TestSwitchHappyLeaveCohort(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": "switch", + "leave-cohort": true, + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "--leave-cohort", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*"foo" left the cohort`) + 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) TestSwitchHappyChannelAndCohort(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": "switch", + "cohort-key": "what", + "channel": "edge", + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "--cohort=what", "--edge", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*"foo" switched to the "edge" channel and the "what" cohort`) + 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) TestSwitchHappyChannelAndLeaveCohort(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": "switch", + "leave-cohort": true, + "channel": "edge", + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "--leave-cohort", "--edge", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*"foo" left the cohort, and switched to the "edge" channel`) + 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) TestSwitchUnhappy(c *check.C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch"}) c.Assert(err, check.ErrorMatches, "the required argument `` was not provided") @@ -1851,7 +1984,12 @@ func (s *SnapOpSuite) TestSwitchAlsoUnhappy(c *check.C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "foo"}) - c.Assert(err, check.ErrorMatches, `missing --channel= parameter`) + c.Assert(err, check.ErrorMatches, `nothing to switch.*`) +} + +func (s *SnapOpSuite) TestSwitchMoreUnhappy(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "foo", "--cohort=what", "--leave-cohort"}) + c.Assert(err, check.ErrorMatches, `cannot specify both --cohort and --leave-cohort`) } func (s *SnapOpSuite) TestSnapOpNetworkTimeoutError(c *check.C) { diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/cmd_snapshot_test.go snapd-2.40/cmd/snap/cmd_snapshot_test.go --- snapd-2.39.2ubuntu0.2/cmd/snap/cmd_snapshot_test.go 2019-06-27 09:58:22.000000000 +0000 +++ snapd-2.40/cmd/snap/cmd_snapshot_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -23,6 +23,7 @@ "fmt" "net/http" "strings" + "time" . "gopkg.in/check.v1" @@ -93,11 +94,13 @@ switch r.URL.Path { case "/v2/snapshots": if r.Method == "GET" { + // simulate a 1-month old snapshot + snapshotTime := time.Now().AddDate(0, -1, 0).Format(time.RFC3339) if r.URL.Query().Get("set") == "3" { - fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[{"id":3,"snapshots":[{"set":3,"time":"2019-03-18T16:15:20.48905909Z","snap":"htop","revision":"1168","snap-id":"Z","auto":true,"epoch":{"read":[0],"write":[0]},"summary":"","version":"2","sha3-384":{"archive.tgz":""},"size":1}]}]}`) + fmt.Fprintf(w, `{"type":"sync","status-code":200,"status":"OK","result":[{"id":3,"snapshots":[{"set":3,"time":%q,"snap":"htop","revision":"1168","snap-id":"Z","auto":true,"epoch":{"read":[0],"write":[0]},"summary":"","version":"2","sha3-384":{"archive.tgz":""},"size":1}]}]}`, snapshotTime) return } - fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[{"id":1,"snapshots":[{"set":1,"time":"2019-03-18T16:15:20.48905909Z","snap":"htop","revision":"1168","snap-id":"Z","epoch":{"read":[0],"write":[0]},"summary":"","version":"2","sha3-384":{"archive.tgz":""},"size":1}]}]}`) + fmt.Fprintf(w, `{"type":"sync","status-code":200,"status":"OK","result":[{"id":1,"snapshots":[{"set":1,"time":%q,"snap":"htop","revision":"1168","snap-id":"Z","epoch":{"read":[0],"write":[0]},"summary":"","version":"2","sha3-384":{"archive.tgz":""},"size":1}]}]}`, snapshotTime) } else { fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "9"}`) } diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/cmd_warnings.go snapd-2.40/cmd/snap/cmd_warnings.go --- snapd-2.39.2ubuntu0.2/cmd/snap/cmd_warnings.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/snap/cmd_warnings.go 2019-07-12 08:40:08.000000000 +0000 @@ -138,7 +138,7 @@ last, err := lastWarningTimestamp() if err != nil { - return fmt.Errorf("no client-side warning timestamp found: %v", err) + return err } return cmd.client.Okay(last) @@ -193,8 +193,12 @@ if err != nil { return time.Time{}, fmt.Errorf("cannot determine real user: %v", err) } + f, err := os.Open(warnFilename(user.HomeDir)) if err != nil { + if os.IsNotExist(err) { + return time.Time{}, fmt.Errorf("you must have looked at the warnings before acknowledging them. Try 'snap warnings'.") + } return time.Time{}, fmt.Errorf("cannot open timestamp file: %v", err) } diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/cmd_warnings_test.go snapd-2.40/cmd/snap/cmd_warnings_test.go --- snapd-2.39.2ubuntu0.2/cmd/snap/cmd_warnings_test.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/snap/cmd_warnings_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -172,6 +172,13 @@ c.Check(s.Stdout(), check.Equals, "") } +func (s *warningSuite) TestOkayBeforeWarnings(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"okay"}) + c.Assert(err, check.ErrorMatches, "you must have looked at the warnings before acknowledging them. Try 'snap warnings'.") + 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) { diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/export_test.go snapd-2.40/cmd/snap/export_test.go --- snapd-2.39.2ubuntu0.2/cmd/snap/export_test.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/snap/export_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -41,8 +41,6 @@ CreateUserDataDirs = createUserDataDirs ResolveApp = resolveApp SnapdHelperPath = snapdHelperPath - MaybePrintServices = maybePrintServices - MaybePrintCommands = maybePrintCommands SortByPath = sortByPath AdviseCommand = adviseCommand Antialias = antialias @@ -81,6 +79,38 @@ InterfacesDeprecationNotice = interfacesDeprecationNotice ) +func NewInfoWriter(w writeflusher) *infoWriter { + return &infoWriter{ + writeflusher: w, + termWidth: 20, + esc: &escapes{dash: "--", tick: "*"}, + fmtTime: func(t time.Time) string { return t.Format(time.Kitchen) }, + } +} + +func SetVerbose(iw *infoWriter, verbose bool) { + iw.verbose = verbose +} + +var ( + ClientSnapFromPath = clientSnapFromPath + SetupDiskSnap = (*infoWriter).setupDiskSnap + SetupSnap = (*infoWriter).setupSnap + MaybePrintServices = (*infoWriter).maybePrintServices + MaybePrintCommands = (*infoWriter).maybePrintCommands + MaybePrintType = (*infoWriter).maybePrintType + PrintSummary = (*infoWriter).printSummary + MaybePrintPublisher = (*infoWriter).maybePrintPublisher + MaybePrintNotes = (*infoWriter).maybePrintNotes + MaybePrintStandaloneVersion = (*infoWriter).maybePrintStandaloneVersion + MaybePrintBuildDate = (*infoWriter).maybePrintBuildDate + MaybePrintContact = (*infoWriter).maybePrintContact + MaybePrintBase = (*infoWriter).maybePrintBase + MaybePrintPath = (*infoWriter).maybePrintPath + MaybePrintSum = (*infoWriter).maybePrintSum + MaybePrintCohortKey = (*infoWriter).maybePrintCohortKey +) + func MockPollTime(d time.Duration) (restore func()) { d0 := pollTime pollTime = d diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/main.go snapd-2.40/cmd/snap/main.go --- snapd-2.39.2ubuntu0.2/cmd/snap/main.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/snap/main.go 2019-07-12 08:40:08.000000000 +0000 @@ -351,7 +351,12 @@ // Client returns a new client using ClientConfig as configuration. // commands should (in general) not use this, and instead use clientMixin. func mkClient() *client.Client { - cli := client.New(&ClientConfig) + cfg := &ClientConfig + // Set client user-agent when talking to the snapd daemon to the + // same value as when talking to the store. + cfg.UserAgent = httputil.UserAgent() + + cli := client.New(cfg) goos := runtime.GOOS if release.OnWSL { goos = "Windows Subsystem for Linux" diff -Nru snapd-2.39.2ubuntu0.2/cmd/snap/main_test.go snapd-2.40/cmd/snap/main_test.go --- snapd-2.39.2ubuntu0.2/cmd/snap/main_test.go 2019-06-05 06:41:21.000000000 +0000 +++ snapd-2.40/cmd/snap/main_test.go 2019-07-12 08:40:08.000000000 +0000 @@ -408,3 +408,18 @@ // Trailing ">s" is fixed to just >. c.Check(snap.FixupArg("