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("