diff -Nru snapd-2.32.3.2/boot/kernel_os.go snapd-2.32.9/boot/kernel_os.go --- snapd-2.32.3.2/boot/kernel_os.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/boot/kernel_os.go 2018-05-16 08:20:08.000000000 +0000 @@ -128,11 +128,22 @@ // check if we actually need to do anything, i.e. the exact same // kernel/core revision got installed again (e.g. firstboot) - m, err := bootloader.GetBootVars(goodBoot) + // and we are not in any special boot mode + m, err := bootloader.GetBootVars("snap_mode", goodBoot) if err != nil { return err } if m[goodBoot] == blobName { + // If we were in "try" mode before and now switch to + // the good core/kernel again, make sure to clean the + // snap_mode here. This also mitigates + // https://forum.snapcraft.io/t//5253 + if m["snap_mode"] != "" { + return bootloader.SetBootVars(map[string]string{ + "snap_mode": "", + nextBoot: "", + }) + } return nil } diff -Nru snapd-2.32.3.2/boot/kernel_os_test.go snapd-2.32.9/boot/kernel_os_test.go --- snapd-2.32.3.2/boot/kernel_os_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/boot/kernel_os_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -217,12 +217,16 @@ info.Revision = snap.R(40) s.bootloader.BootVars["snap_kernel"] = "krnl_40.snap" + s.bootloader.BootVars["snap_try_kernel"] = "krnl_99.snap" + s.bootloader.BootVars["snap_mode"] = "try" err := boot.SetNextBoot(info) c.Assert(err, IsNil) c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ - "snap_kernel": "krnl_40.snap", + "snap_kernel": "krnl_40.snap", + "snap_try_kernel": "", + "snap_mode": "", }) } diff -Nru snapd-2.32.3.2/client/client.go snapd-2.32.9/client/client.go --- snapd-2.32.3.2/client/client.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/client/client.go 2018-05-16 08:20:08.000000000 +0000 @@ -377,6 +377,8 @@ ErrorKindNetworkTimeout = "network-timeout" ErrorKindInterfacesUnchanged = "interfaces-unchanged" + + ErrorKindConfigNoSuchOption = "option-not-found" ) // IsTwoFactorError returns whether the given error is due to problems diff -Nru snapd-2.32.3.2/cmd/libsnap-confine-private/locking-test.c snapd-2.32.9/cmd/libsnap-confine-private/locking-test.c --- snapd-2.32.3.2/cmd/libsnap-confine-private/locking-test.c 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/cmd/libsnap-confine-private/locking-test.c 2018-05-16 08:20:08.000000000 +0000 @@ -32,6 +32,12 @@ sc_lock_dir = dir; } +// A variant of unsetenv that is compatible with GDestroyNotify +static void my_unsetenv(const char *k) +{ + unsetenv(k); +} + // Use temporary directory for locking. // // The directory is automatically reset to the real value at the end of the @@ -50,7 +56,7 @@ g_test_queue_free(lock_dir); g_assert_cmpint(setenv("SNAP_CONFINE_LOCK_DIR", lock_dir, 0), ==, 0); - g_test_queue_destroy((GDestroyNotify) unsetenv, + g_test_queue_destroy((GDestroyNotify) my_unsetenv, "SNAP_CONFINE_LOCK_DIR"); g_test_queue_destroy((GDestroyNotify) rm_rf_tmp, lock_dir); } diff -Nru snapd-2.32.3.2/cmd/libsnap-confine-private/utils-test.c snapd-2.32.9/cmd/libsnap-confine-private/utils-test.c --- snapd-2.32.3.2/cmd/libsnap-confine-private/utils-test.c 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/cmd/libsnap-confine-private/utils-test.c 2018-05-16 08:20:08.000000000 +0000 @@ -99,6 +99,22 @@ g_test_trap_assert_stderr("death message: Operation not permitted\n"); } +// A variant of rmdir that is compatible with GDestroyNotify +static void my_rmdir(const char *path) +{ + if (rmdir(path) != 0) { + die("cannot rmdir %s", path); + } +} + +// A variant of chdir that is compatible with GDestroyNotify +static void my_chdir(const char *path) +{ + if (chdir(path) != 0) { + die("cannot change dir to %s", path); + } +} + /** * Perform the rest of testing in a ephemeral directory. * @@ -114,9 +130,9 @@ g_assert_cmpint(err, ==, 0); g_test_queue_free(temp_dir); - g_test_queue_destroy((GDestroyNotify) rmdir, temp_dir); + g_test_queue_destroy((GDestroyNotify) my_rmdir, temp_dir); g_test_queue_free(orig_dir); - g_test_queue_destroy((GDestroyNotify) chdir, orig_dir); + g_test_queue_destroy((GDestroyNotify) my_chdir, orig_dir); } /** @@ -130,7 +146,7 @@ G_FILE_TEST_IS_DIR)); // Use sc_nonfatal_mkpath to create the directory and ensure that it worked // as expected. - g_test_queue_destroy((GDestroyNotify) rmdir, (char *)dirname); + g_test_queue_destroy((GDestroyNotify) my_rmdir, (char *)dirname); int err = sc_nonfatal_mkpath(dirname, 0755); g_assert_cmpint(err, ==, 0); g_assert_cmpint(errno, ==, 0); @@ -143,7 +159,7 @@ g_assert_cmpint(errno, ==, EEXIST); // Now create a sub-directory of the original directory and observe the // results. We should no longer see errno of EEXIST! - g_test_queue_destroy((GDestroyNotify) rmdir, (char *)subdirname); + g_test_queue_destroy((GDestroyNotify) my_rmdir, (char *)subdirname); err = sc_nonfatal_mkpath(subdirname, 0755); g_assert_cmpint(err, ==, 0); g_assert_cmpint(errno, ==, 0); diff -Nru snapd-2.32.3.2/cmd/snap/cmd_info.go snapd-2.32.9/cmd/snap/cmd_info.go --- snapd-2.32.3.2/cmd/snap/cmd_info.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/cmd/snap/cmd_info.go 2018-05-16 08:20:08.000000000 +0000 @@ -298,6 +298,10 @@ if i > 0 { fmt.Fprintln(w, "---") } + if snapName == "system" { + fmt.Fprintln(w, "system: You can't have it.") + continue + } if tryDirect(w, snapName, x.Verbose) { noneOK = false diff -Nru snapd-2.32.3.2/cmd/snap/cmd_run.go snapd-2.32.9/cmd/snap/cmd_run.go --- snapd-2.32.3.2/cmd/snap/cmd_run.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/cmd/snap/cmd_run.go 2018-05-16 08:20:08.000000000 +0000 @@ -233,28 +233,16 @@ return actualApp, argsOut } -func getSnapInfo(snapName string, revision snap.Revision) (*snap.Info, error) { +func getSnapInfo(snapName string, revision snap.Revision) (info *snap.Info, err error) { if revision.Unset() { - curFn := filepath.Join(dirs.SnapMountDir, snapName, "current") - realFn, err := os.Readlink(curFn) - if err != nil { - return nil, fmt.Errorf("cannot find current revision for snap %s: %s", snapName, err) - } - rev := filepath.Base(realFn) - revision, err = snap.ParseRevision(rev) - if err != nil { - return nil, fmt.Errorf("cannot read revision %s: %s", rev, err) - } + info, err = snap.ReadCurrentInfo(snapName) + } else { + info, err = snap.ReadInfo(snapName, &snap.SideInfo{ + Revision: revision, + }) } - info, err := snap.ReadInfo(snapName, &snap.SideInfo{ - Revision: revision, - }) - if err != nil { - return nil, err - } - - return info, nil + return info, err } func createOrUpdateUserDataSymlink(info *snap.Info, usr *user.User) error { diff -Nru snapd-2.32.3.2/cmd/snap/cmd_snap_op.go snapd-2.32.9/cmd/snap/cmd_snap_op.go --- snapd-2.32.3.2/cmd/snap/cmd_snap_op.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/cmd/snap/cmd_snap_op.go 2018-05-16 08:20:08.000000000 +0000 @@ -373,7 +373,7 @@ default: fmt.Fprintf(Stdout, "internal error: unknown op %q", op) } - if snap.TrackingChannel != snap.Channel { + if snap.TrackingChannel != snap.Channel && snap.Channel != "" { // TRANSLATORS: first %s is a snap name, following %s is a channel name fmt.Fprintf(Stdout, i18n.G("Snap %s is no longer tracking %s.\n"), snap.Name, snap.TrackingChannel) } diff -Nru snapd-2.32.3.2/cmd/snap/cmd_userd.go snapd-2.32.9/cmd/snap/cmd_userd.go --- snapd-2.32.3.2/cmd/snap/cmd_userd.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/cmd/snap/cmd_userd.go 2018-05-16 08:20:08.000000000 +0000 @@ -33,6 +33,8 @@ type cmdUserd struct { userd userd.Userd + + Autostart bool `long:"autostart"` } var shortUserdHelp = i18n.G("Start the userd service") @@ -44,10 +46,9 @@ longUserdHelp, func() flags.Commander { return &cmdUserd{} - }, - nil, - []argDesc{}, - ) + }, map[string]string{ + "autostart": i18n.G("Autostart user applications"), + }, nil) cmd.hidden = true } @@ -56,6 +57,10 @@ return ErrExtraArgs } + if x.Autostart { + return x.runAutostart() + } + if err := x.userd.Init(); err != nil { return err } @@ -72,3 +77,10 @@ return x.userd.Stop() } + +func (x *cmdUserd) runAutostart() error { + if err := userd.AutostartSessionApps(); err != nil { + return fmt.Errorf("autostart failed for the following apps:\n%v", err) + } + return nil +} diff -Nru snapd-2.32.3.2/cmd/snap/cmd_wait.go snapd-2.32.9/cmd/snap/cmd_wait.go --- snapd-2.32.3.2/cmd/snap/cmd_wait.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/cmd/snap/cmd_wait.go 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,134 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "encoding/json" + "time" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" +) + +type cmdWait struct { + Positional struct { + Snap installedSnapName `required:"yes"` + Key string + } `positional-args:"yes"` +} + +func init() { + addCommand("wait", + "Wait for configuration.", + "The wait command waits until a configration becomes true.", + func() flags.Commander { + return &cmdWait{} + }, nil, []argDesc{ + { + name: "", + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("The snap for which configuration will be checked"), + }, { + // TRANSLATORS: This needs to be wrapped in <>s. + name: i18n.G(""), + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("Key of interest within the configuration"), + }, + }) +} + +var waitConfTimeout = 500 * time.Millisecond + +func isNoOption(err error) bool { + if e, ok := err.(*client.Error); ok && e.Kind == client.ErrorKindConfigNoSuchOption { + return true + } + return false +} + +func trueish(vi interface{}) bool { + switch v := vi.(type) { + case bool: + if v == true { + return true + } + case int: + if v > 0 { + return true + } + case int64: + if v > 0 { + return true + } + case float32: + if v > 0 { + return true + } + case float64: + if v > 0 { + return true + } + case json.Number: + if i, err := v.Int64(); err == nil && i > 0 { + return true + } + if f, err := v.Float64(); err == nil && f != 0.0 { + return true + } + case string: + if v != "" { + return true + } + case []interface{}: + if len(v) > 0 { + return true + } + case map[string]interface{}: + if len(v) > 0 { + return true + } + } + return false +} + +func (x *cmdWait) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + snapName := string(x.Positional.Snap) + confKey := x.Positional.Key + + cli := Client() + for { + conf, err := cli.Conf(snapName, []string{confKey}) + if err != nil && !isNoOption(err) { + return err + } + if trueish(conf[confKey]) { + break + } + time.Sleep(waitConfTimeout) + } + + return nil +} diff -Nru snapd-2.32.3.2/cmd/snap/cmd_wait_test.go snapd-2.32.9/cmd/snap/cmd_wait_test.go --- snapd-2.32.3.2/cmd/snap/cmd_wait_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/cmd/snap/cmd_wait_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,59 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + "time" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestCmdWait(c *C) { + var seeded bool + + restore := snap.MockWaitConfTimeout(10 * time.Millisecond) + defer restore() + + go func() { + time.Sleep(50 * time.Millisecond) + seeded = true + }() + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/snaps/system/conf") + + fmt.Fprintln(w, fmt.Sprintf(`{"type":"sync", "status-code": 200, "result": {"seed.loaded":%v}}`, seeded)) + n++ + }) + + _, err := snap.Parser().ParseArgs([]string{"wait", "system", "seed.loaded"}) + c.Assert(err, IsNil) + + // ensure we retried a bit but make the check not overly precise + // because this will run in super busy build hosts that where a + // 10 millisecond sleep actually takes much longer until the kernel + // hands control back to the process + c.Check(n > 2, Equals, true) +} diff -Nru snapd-2.32.3.2/cmd/snap/export_test.go snapd-2.32.9/cmd/snap/export_test.go --- snapd-2.32.3.2/cmd/snap/export_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/cmd/snap/export_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -145,3 +145,11 @@ timeNow = oldTimeNow } } + +func MockWaitConfTimeout(d time.Duration) (restore func()) { + oldWaitConfTimeout := d + waitConfTimeout = d + return func() { + waitConfTimeout = oldWaitConfTimeout + } +} diff -Nru snapd-2.32.3.2/cmd/snap-confine/ns-support-test.c snapd-2.32.9/cmd/snap-confine/ns-support-test.c --- snapd-2.32.3.2/cmd/snap-confine/ns-support-test.c 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/cmd/snap-confine/ns-support-test.c 2018-05-16 08:20:08.000000000 +0000 @@ -35,6 +35,12 @@ sc_ns_dir = dir; } +// A variant of unsetenv that is compatible with GDestroyNotify +static void my_unsetenv(const char *k) +{ + unsetenv(k); +} + // Use temporary directory for namespace groups. // // The directory is automatically reset to the real value at the end of the @@ -53,7 +59,7 @@ g_test_queue_free(ns_dir); g_assert_cmpint(setenv("SNAP_CONFINE_NS_DIR", ns_dir, 0), ==, 0); - g_test_queue_destroy((GDestroyNotify) unsetenv, + g_test_queue_destroy((GDestroyNotify) my_unsetenv, "SNAP_CONFINE_NS_DIR"); g_test_queue_destroy((GDestroyNotify) rm_rf_tmp, ns_dir); } diff -Nru snapd-2.32.3.2/daemon/api.go snapd-2.32.9/daemon/api.go --- snapd-2.32.3.2/daemon/api.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/daemon/api.go 2018-05-16 08:20:08.000000000 +0000 @@ -1165,7 +1165,13 @@ var snapName string switch err { - case store.ErrSnapNotFound: + case store.ErrSnapNotFound, store.ErrRevisionNotAvailable: + // TODO: treating ErrRevisionNotAvailable the same as + // ErrSnapNotFound preserves the old error handling + // behavior of the REST API and the snap command. We + // should revisit this once the store returns more + // precise errors and makes it possible to distinguish + // the why a revision wasn't available. switch len(inst.Snaps) { case 1: return SnapNotFound(inst.Snaps[0], err) @@ -1593,7 +1599,7 @@ func getSnapConf(c *Command, r *http.Request, user *auth.UserState) Response { vars := muxVars(r) - snapName := vars["name"] + snapName := systemCoreSnapUnalias(vars["name"]) keys := splitQS(r.URL.Query().Get("keys")) @@ -1616,7 +1622,15 @@ currentConfValues = make(map[string]interface{}) break } - return BadRequest("%v", err) + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: err.Error(), + Kind: errorKindConfigNoSuchOption, + Value: err, + }, + Status: 400, + }, nil) } else { return InternalError("%v", err) } @@ -1636,7 +1650,7 @@ func setSnapConf(c *Command, r *http.Request, user *auth.UserState) Response { vars := muxVars(r) - snapName := vars["name"] + snapName := systemCoreSnapUnalias(vars["name"]) var patchValues map[string]interface{} if err := jsonutil.DecodeWithNumber(r.Body, &patchValues); err != nil { @@ -2799,3 +2813,10 @@ st.EnsureBefore(0) return AsyncResponse(nil, &Meta{Change: chg.ID()}) } + +func systemCoreSnapUnalias(name string) string { + if name == "system" { + return "core" + } + return name +} diff -Nru snapd-2.32.3.2/daemon/api_test.go snapd-2.32.9/daemon/api_test.go --- snapd-2.32.3.2/daemon/api_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/daemon/api_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -86,7 +86,8 @@ d *Daemon user *auth.UserState restoreBackends func() - refreshCandidates []*store.RefreshCandidate + currentSnaps []*store.CurrentSnap + actions []*store.SnapAction buyOptions *store.BuyOptions buyResult *store.BuyResult storeSigning *assertstest.StoreStack @@ -126,18 +127,12 @@ return s.rsnaps, s.err } -func (s *apiBaseSuite) LookupRefresh(snap *store.RefreshCandidate, user *auth.UserState) (*snap.Info, error) { - s.refreshCandidates = []*store.RefreshCandidate{snap} - s.user = user - - return s.rsnaps[0], s.err -} - -func (s *apiBaseSuite) ListRefresh(ctx context.Context, snaps []*store.RefreshCandidate, user *auth.UserState, flags *store.RefreshOptions) ([]*snap.Info, error) { +func (s *apiBaseSuite) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) { if ctx == nil { panic("context required") } - s.refreshCandidates = snaps + s.currentSnaps = currentSnaps + s.actions = actions s.user = user return s.rsnaps, s.err @@ -232,7 +227,8 @@ s.vars = nil s.user = nil s.d = nil - s.refreshCandidates = nil + s.currentSnaps = nil + s.actions = nil // Disable real security backends for all API tests s.restoreBackends = ifacestate.MockSecurityBackends(nil) @@ -1502,7 +1498,8 @@ c.Check(rsp.SuggestedCurrency, check.Equals, "EUR") c.Check(s.storeSearch, check.DeepEquals, store.Search{Query: "hi"}) - c.Check(s.refreshCandidates, check.HasLen, 0) + c.Check(s.currentSnaps, check.HasLen, 0) + c.Check(s.actions, check.HasLen, 0) } func (s *apiSuite) TestFindRefreshes(c *check.C) { @@ -1525,7 +1522,8 @@ snaps := snapList(rsp.Result) c.Assert(snaps, check.HasLen, 1) c.Assert(snaps[0]["name"], check.Equals, "store") - c.Check(s.refreshCandidates, check.HasLen, 1) + c.Check(s.currentSnaps, check.HasLen, 1) + c.Check(s.actions, check.HasLen, 1) } func (s *apiSuite) TestFindRefreshSideloaded(c *check.C) { @@ -1561,9 +1559,9 @@ rsp := searchStore(findCmd, req, nil).(*resp) snaps := snapList(rsp.Result) - c.Assert(snaps, check.HasLen, 1) - c.Assert(snaps[0]["name"], check.Equals, "store") - c.Check(s.refreshCandidates, check.HasLen, 0) + c.Assert(snaps, check.HasLen, 0) + c.Check(s.currentSnaps, check.HasLen, 0) + c.Check(s.actions, check.HasLen, 0) } func (s *apiSuite) TestFindPrivate(c *check.C) { @@ -2547,9 +2545,9 @@ return chg.Summary() } -func (s *apiSuite) runGetConf(c *check.C, keys []string, statusCode int) map[string]interface{} { - s.vars = map[string]string{"name": "test-snap"} - req, err := http.NewRequest("GET", "/v2/snaps/test-snap/conf?keys="+strings.Join(keys, ","), nil) +func (s *apiSuite) runGetConf(c *check.C, snapName string, keys []string, statusCode int) map[string]interface{} { + s.vars = map[string]string{"name": snapName} + req, err := http.NewRequest("GET", "/v2/snaps/"+snapName+"/conf?keys="+strings.Join(keys, ","), nil) c.Check(err, check.IsNil) rec := httptest.NewRecorder() snapConfCmd.GET(snapConfCmd, req, nil).ServeHTTP(rec, req) @@ -2572,16 +2570,40 @@ tr.Commit() d.overlord.State().Unlock() - result := s.runGetConf(c, []string{"test-key1"}, 200) + result := s.runGetConf(c, "test-snap", []string{"test-key1"}, 200) c.Check(result, check.DeepEquals, map[string]interface{}{"test-key1": "test-value1"}) - result = s.runGetConf(c, []string{"test-key1", "test-key2"}, 200) + result = s.runGetConf(c, "test-snap", []string{"test-key1", "test-key2"}, 200) c.Check(result, check.DeepEquals, map[string]interface{}{"test-key1": "test-value1", "test-key2": "test-value2"}) } +func (s *apiSuite) TestGetConfCoreSystemAlias(c *check.C) { + d := s.daemon(c) + + // Set a config that we'll get in a moment + d.overlord.State().Lock() + tr := config.NewTransaction(d.overlord.State()) + tr.Set("core", "test-key1", "test-value1") + tr.Commit() + d.overlord.State().Unlock() + + result := s.runGetConf(c, "core", []string{"test-key1"}, 200) + c.Check(result, check.DeepEquals, map[string]interface{}{"test-key1": "test-value1"}) + + result = s.runGetConf(c, "system", []string{"test-key1"}, 200) + c.Check(result, check.DeepEquals, map[string]interface{}{"test-key1": "test-value1"}) +} + func (s *apiSuite) TestGetConfMissingKey(c *check.C) { - result := s.runGetConf(c, []string{"test-key2"}, 400) - c.Check(result, check.DeepEquals, map[string]interface{}{"message": `snap "test-snap" has no "test-key2" configuration option`}) + result := s.runGetConf(c, "test-snap", []string{"test-key2"}, 400) + c.Check(result, check.DeepEquals, map[string]interface{}{ + "value": map[string]interface{}{ + "SnapName": "test-snap", + "Key": "test-key2", + }, + "message": `snap "test-snap" has no "test-key2" configuration option`, + "kind": "option-not-found", + }) } func (s *apiSuite) TestGetRootDocument(c *check.C) { @@ -2593,13 +2615,13 @@ tr.Commit() d.overlord.State().Unlock() - result := s.runGetConf(c, nil, 200) + result := s.runGetConf(c, "test-snap", nil, 200) c.Check(result, check.DeepEquals, map[string]interface{}{"test-key1": "test-value1", "test-key2": "test-value2"}) } func (s *apiSuite) TestGetConfBadKey(c *check.C) { // TODO: this one in particular should really be a 400 also - result := s.runGetConf(c, []string{"."}, 500) + result := s.runGetConf(c, "test-snap", []string{"."}, 500) c.Check(result, check.DeepEquals, map[string]interface{}{"message": `invalid option name: ""`}) } @@ -2651,6 +2673,57 @@ }}) } +func (s *apiSuite) TestSetConfCoreSystemAlias(c *check.C) { + d := s.daemon(c) + s.mockSnap(c, ` +name: core +version: 1 +`) + // Mock the hook runner + hookRunner := testutil.MockCommand(c, "snap", "") + defer hookRunner.Restore() + + d.overlord.Loop() + defer d.overlord.Stop() + + text, err := json.Marshal(map[string]interface{}{"key": "value"}) + c.Assert(err, check.IsNil) + + buffer := bytes.NewBuffer(text) + req, err := http.NewRequest("PUT", "/v2/snaps/system/conf", buffer) + c.Assert(err, check.IsNil) + + s.vars = map[string]string{"name": "system"} + + rec := httptest.NewRecorder() + snapConfCmd.PUT(snapConfCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 202) + + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Assert(err, check.IsNil) + id := body["change"].(string) + + st := d.overlord.State() + st.Lock() + chg := st.Change(id) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + err = chg.Err() + tr := config.NewTransaction(st) + st.Unlock() + c.Assert(err, check.IsNil) + + var value string + tr.Get("core", "key", &value) + c.Assert(value, check.Equals, "value") + +} + func (s *apiSuite) TestSetConfNumber(c *check.C) { d := s.daemon(c) s.mockSnap(c, configYaml) @@ -6471,6 +6544,7 @@ si := &snapInstruction{Action: "frobble"} errors := []error{ store.ErrSnapNotFound, + store.ErrRevisionNotAvailable, store.ErrNoUpdateAvailable, store.ErrLocalSnap, &snap.AlreadyInstalledError{Snap: "foo"}, diff -Nru snapd-2.32.3.2/daemon/response.go snapd-2.32.9/daemon/response.go --- snapd-2.32.3.2/daemon/response.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/daemon/response.go 2018-05-16 08:20:08.000000000 +0000 @@ -147,6 +147,8 @@ errorKindNetworkTimeout = errorKind("network-timeout") errorKindInterfacesUnchanged = errorKind("interfaces-unchanged") + + errorKindConfigNoSuchOption = errorKind("option-not-found") ) type errorValue interface{} diff -Nru snapd-2.32.3.2/data/desktop/Makefile snapd-2.32.9/data/desktop/Makefile --- snapd-2.32.3.2/data/desktop/Makefile 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/data/desktop/Makefile 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,38 @@ +# +# Copyright (C) 2018 Canonical Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +BINDIR = /usr/bin +SYSCONFXDGAUTOSTARTDIR = /etc/xdg/autostart + +SOURCES = snap-userd-autostart.desktop.in +DESKTOP_FILES = $(SOURCES:.in=) + +.PHONY: all +all: $(DESKTOP_FILES) + +.PHONY: install +install: $(DESKTOP_FILES) + # NOTE: old (e.g. 14.04) GNU coreutils doesn't -D with -t + install -d -m 0755 $(DESTDIR)/$(SYSCONFXDGAUTOSTARTDIR) + install -m 0644 -t $(DESTDIR)/$(SYSCONFXDGAUTOSTARTDIR) $^ + +.PHONY: clean +clean: + rm -f $(DESKTOP_FILES) + +%: %.in + cat $< | \ + sed s:@bindir@:$(BINDIR):g | \ + cat > $@ diff -Nru snapd-2.32.3.2/data/desktop/snap-userd-autostart.desktop.in snapd-2.32.9/data/desktop/snap-userd-autostart.desktop.in --- snapd-2.32.3.2/data/desktop/snap-userd-autostart.desktop.in 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/data/desktop/snap-userd-autostart.desktop.in 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,5 @@ +[Desktop Entry] +Name=Snap user application autostart helper +Comment=Helper program for launching snap applications that are configured to start automatically. +Exec=@bindir@/snap userd --autostart +Type=Application diff -Nru snapd-2.32.3.2/data/Makefile snapd-2.32.9/data/Makefile --- snapd-2.32.3.2/data/Makefile 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/data/Makefile 2018-05-16 08:20:08.000000000 +0000 @@ -2,3 +2,4 @@ $(MAKE) -C systemd $@ $(MAKE) -C dbus $@ $(MAKE) -C env $@ + $(MAKE) -C desktop $@ diff -Nru snapd-2.32.3.2/data/systemd/snapd.core-fixup.service.in snapd-2.32.9/data/systemd/snapd.core-fixup.service.in --- snapd-2.32.3.2/data/systemd/snapd.core-fixup.service.in 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/data/systemd/snapd.core-fixup.service.in 2018-05-16 08:20:08.000000000 +0000 @@ -3,7 +3,6 @@ Before=snapd.service # don't run on classic ConditionKernelCommandLine=snap_core -ConditionPathExists=!/var/lib/snapd/device/ownership-change.after Documentation=man:snap(1) [Service] diff -Nru snapd-2.32.3.2/data/systemd/snapd.core-fixup.sh snapd-2.32.9/data/systemd/snapd.core-fixup.sh --- snapd-2.32.3.2/data/systemd/snapd.core-fixup.sh 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/data/systemd/snapd.core-fixup.sh 2018-05-16 08:20:08.000000000 +0000 @@ -11,6 +11,38 @@ exit 0 fi +# Workaround https://forum.snapcraft.io/t/5253 +# +# We see sometimes corrupted uboot.env files created by fsck.vfat. +# On the fat filesystem they are indistinguishable because one +# has a fat16 name UBOOT.ENV (and not lfn (long-file-name)) but +# the other has a "uboot.env" lfn name and a FSCK0000.000 FAT16 +# name. The only known workaround is to remove all dupes and put +# one file back in place. +if [ $(ls /boot/uboot | grep ^uboot.env$ | wc -l) -gt 1 ]; then + echo "Corrupted uboot.env file detected" + # ensure we have one uboot.env to go back to + cp -a /boot/uboot/uboot.env /boot/uboot/uboot.env.save + # now delete all dupes + while ls /boot/uboot/uboot.env 2>/dev/null; do + rm -f /boot/uboot/uboot.env + done + # and move the saved one into place + mv /boot/uboot/uboot.env.save /boot/uboot/uboot.env + + # ensure we sync the fs + sync +fi + + +# The code below deals with incorrect permissions that happened on +# some buggy ubuntu-image versions. +# +# This needs to run only once so we can exit here. +if [ -f /var/lib/snapd/device/ownership-change.after ]; then + exit 0 +fi + # store important data in case we need it later if [ ! -f /var/lib/snapd/device/ownership-change.before ]; then mkdir -p /var/lib/snapd/device diff -Nru snapd-2.32.3.2/data/systemd/snapd.seeded.service.in snapd-2.32.9/data/systemd/snapd.seeded.service.in --- snapd-2.32.3.2/data/systemd/snapd.seeded.service.in 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/data/systemd/snapd.seeded.service.in 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,12 @@ +[Unit] +Description=Wait until snapd is fully seeded +After=snapd.service snapd.socket + +[Service] +Type=oneshot +ExecStart=@bindir@/snap wait system seed.loaded +RemainAfterExit=true + +[Install] +WantedBy=multi-user.target cloud-final.service + diff -Nru snapd-2.32.3.2/debian/changelog snapd-2.32.9/debian/changelog --- snapd-2.32.3.2/debian/changelog 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/debian/changelog 2018-05-16 08:20:08.000000000 +0000 @@ -1,3 +1,66 @@ +snapd (2.32.9) xenial; urgency=medium + + * New upstream release, LP: #1767833 + - tests: run all spread tests inside GCE + - tests: build spread in the autopkgtests with a more recent go + + -- Michael Vogt Wed, 16 May 2018 10:20:08 +0200 + +snapd (2.32.8) xenial; urgency=medium + + * New upstream release, LP: #1767833 + + -- Michael Vogt Fri, 11 May 2018 14:36:16 +0200 + +snapd (2.32.7) xenial; urgency=medium + + * New upstream release, LP: #1767833 + - many: add wait command and seeded target (2 + - snapd.core-fixup.sh: add workaround for corrupted uboot.env + - boot: clear "snap_mode" when needed + - cmd/libsnap: fix compile error on more restrictive gcc + - tests: cherry-pick commits to move spread to google backend + - spread.yaml: add cosmic (18.10) to autopkgtest/qemu + - userd: set up journal logging streams for autostarted apps + + -- Michael Vogt Fri, 11 May 2018 13:09:32 +0200 + +snapd (2.32.6) xenial; urgency=medium + + * New upstream release, LP: #1767833 + - snap: do not use overly short timeout in `snap + {start,stop,restart}` + - interfaces/apparmor: fix incorrect apparmor profile glob + - tests: detect kernel oops during tests and abort tests in this + case + - tests: run interfaces-boradcom-asic-control early + - tests: skip interfaces-content test on core devices + + -- Michael Vogt Sun, 29 Apr 2018 19:21:53 +0200 + +snapd (2.32.5) xenial; urgency=medium + + * New upstream release, LP: #1765090 + - many: add "stop-mode: sig{term,hup,usr[12]}{,-all}" instead of + conflating that with refresh-mode + - overlord/snapstate: poll for up to 10s if a snap is unexpectedly + not mounted in doMountSnap + - daemon: support 'system' as nickname of the core snap + + -- Michael Vogt Mon, 16 Apr 2018 11:41:48 +0200 + +snapd (2.32.4) xenial; urgency=medium + + * New upstream release, LP: #1756173 + - cmd/snap: user session application autostart + - overlord/snapstate: introduce envvars to control the channels for + bases and prereqs + - overlord/snapstate: on multi-snap refresh make sure bases and core + are finished before dependent snaps + - many: use the new install/refresh /v2/snaps/refresh store API + + -- Michael Vogt Wed, 11 Apr 2018 16:30:45 +0200 + snapd (2.32.3.2) xenial; urgency=medium * New upstream release, LP: #1756173 diff -Nru snapd-2.32.3.2/debian/tests/integrationtests snapd-2.32.9/debian/tests/integrationtests --- snapd-2.32.3.2/debian/tests/integrationtests 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/debian/tests/integrationtests 2018-05-16 08:20:08.000000000 +0000 @@ -36,11 +36,14 @@ export SPREAD_CORE_CHANNEL=stable fi +# Spread will only buid with recent go +snap install --classic go + # and now run spread against localhost # shellcheck disable=SC1091 . /etc/os-release export GOPATH=/tmp/go -go get -u github.com/snapcore/spread/cmd/spread +/snap/bin/go get -u github.com/snapcore/spread/cmd/spread /tmp/go/bin/spread -v "autopkgtest:${ID}-${VERSION_ID}-$(dpkg --print-architecture)" # store journal info for inspectsion diff -Nru snapd-2.32.3.2/dirs/dirs.go snapd-2.32.9/dirs/dirs.go --- snapd-2.32.3.2/dirs/dirs.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/dirs/dirs.go 2018-05-16 08:20:08.000000000 +0000 @@ -113,6 +113,9 @@ // are in the snap confinement environment. CoreLibExecDir = "/usr/lib/snapd" CoreSnapMountDir = "/snap" + + // Directory with snap data inside user's home + UserHomeSnapDir = "snap" ) var ( @@ -179,7 +182,7 @@ } SnapDataDir = filepath.Join(rootdir, "/var/snap") - SnapDataHomeGlob = filepath.Join(rootdir, "/home/*/snap/") + SnapDataHomeGlob = filepath.Join(rootdir, "/home/*/", UserHomeSnapDir) SnapAppArmorDir = filepath.Join(rootdir, snappyDir, "apparmor", "profiles") SnapConfineAppArmorDir = filepath.Join(rootdir, snappyDir, "apparmor", "snap-confine") AppArmorCacheDir = filepath.Join(rootdir, "/var/cache/apparmor") diff -Nru snapd-2.32.3.2/interfaces/apparmor/backend.go snapd-2.32.9/interfaces/apparmor/backend.go --- snapd-2.32.3.2/interfaces/apparmor/backend.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/interfaces/apparmor/backend.go 2018-05-16 08:20:08.000000000 +0000 @@ -259,6 +259,21 @@ return nil } +// nsProfile returns name of the apparmor profile for snap-update-ns for a given snap. +func nsProfile(snapName string) string { + return fmt.Sprintf("snap-update-ns.%s", snapName) +} + +// profileGlobs returns a list of globs that describe the apparmor profiles of +// a given snap. +// +// Currently the list is just a pair. The first glob describes profiles for all +// apps and hooks while the second profile describes the snap-update-ns profile +// for the whole snap. +func profileGlobs(snapName string) []string { + return []string{interfaces.SecurityTagGlob(snapName), nsProfile(snapName)} +} + // Setup creates and loads apparmor profiles specific to a given snap. // The snap can be in developer mode to make security violations non-fatal to // the offending application process. @@ -308,13 +323,12 @@ return fmt.Errorf("cannot obtain expected security files for snap %q: %s", snapName, err) } dir := dirs.SnapAppArmorDir - glob1 := fmt.Sprintf("snap*.%s*", snapInfo.Name()) - glob2 := fmt.Sprintf("snap-update-ns.%s", snapInfo.Name()) + globs := profileGlobs(snapInfo.Name()) cache := dirs.AppArmorCacheDir if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("cannot create directory for apparmor profiles %q: %s", dir, err) } - _, removed, errEnsure := osutil.EnsureDirStateGlobs(dir, []string{glob1, glob2}, content) + _, removed, errEnsure := osutil.EnsureDirStateGlobs(dir, globs, content) // NOTE: load all profiles instead of just the changed profiles. We're // relying on apparmor cache to make this efficient. This gives us // certainty that each call to Setup ends up with working profiles. @@ -337,10 +351,9 @@ // Remove removes and unloads apparmor profiles of a given snap. func (b *Backend) Remove(snapName string) error { dir := dirs.SnapAppArmorDir - glob1 := fmt.Sprintf("snap*.%s*", snapName) - glob2 := fmt.Sprintf("snap-update-ns.%s", snapName) + globs := profileGlobs(snapName) cache := dirs.AppArmorCacheDir - _, removed, errEnsure := osutil.EnsureDirStateGlobs(dir, []string{glob1, glob2}, nil) + _, removed, errEnsure := osutil.EnsureDirStateGlobs(dir, globs, nil) errUnload := unloadProfiles(removed, cache) if errEnsure != nil { return fmt.Errorf("cannot synchronize security files for snap %q: %s", snapName, errEnsure) @@ -397,7 +410,7 @@ }) // Ensure that the snap-update-ns profile is on disk. - profileName := fmt.Sprintf("snap-update-ns.%s", snapInfo.Name()) + profileName := nsProfile(snapInfo.Name()) content[profileName] = &osutil.FileState{ Content: []byte(policy), Mode: 0644, diff -Nru snapd-2.32.3.2/interfaces/apparmor/backend_test.go snapd-2.32.9/interfaces/apparmor/backend_test.go --- snapd-2.32.3.2/interfaces/apparmor/backend_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/interfaces/apparmor/backend_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -352,6 +352,63 @@ } } +const snapcraftPrYaml = `name: snapcraft-pr +version: 1 +apps: + snapcraft-pr: + cmd: snapcraft-pr +` + +const snapcraftYaml = `name: snapcraft +version: 1 +apps: + snapcraft: + cmd: snapcraft +` + +func (s *backendSuite) TestInstallingSnapDoesntBreakSnapsWithPrefixName(c *C) { + snapcraftProfile := filepath.Join(dirs.SnapAppArmorDir, "snap.snapcraft.snapcraft") + snapcraftPrProfile := filepath.Join(dirs.SnapAppArmorDir, "snap.snapcraft-pr.snapcraft-pr") + // Install snapcraft-pr and check that its profile was created. + s.InstallSnap(c, interfaces.ConfinementOptions{}, snapcraftPrYaml, 1) + _, err := os.Stat(snapcraftPrProfile) + c.Check(err, IsNil) + + // Install snapcraft (sans the -pr suffix) and check that its profile was created. + // Check that this didn't remove the profile of snapcraft-pr installed earlier. + s.InstallSnap(c, interfaces.ConfinementOptions{}, snapcraftYaml, 1) + _, err = os.Stat(snapcraftProfile) + c.Check(err, IsNil) + _, err = os.Stat(snapcraftPrProfile) + c.Check(err, IsNil) +} + +func (s *backendSuite) TestRemovingSnapDoesntBreakSnapsWIthPrefixName(c *C) { + snapcraftProfile := filepath.Join(dirs.SnapAppArmorDir, "snap.snapcraft.snapcraft") + snapcraftPrProfile := filepath.Join(dirs.SnapAppArmorDir, "snap.snapcraft-pr.snapcraft-pr") + + // Install snapcraft-pr and check that its profile was created. + s.InstallSnap(c, interfaces.ConfinementOptions{}, snapcraftPrYaml, 1) + _, err := os.Stat(snapcraftPrProfile) + c.Check(err, IsNil) + + // Install snapcraft (sans the -pr suffix) and check that its profile was created. + // Check that this didn't remove the profile of snapcraft-pr installed earlier. + snapInfo := s.InstallSnap(c, interfaces.ConfinementOptions{}, snapcraftYaml, 1) + _, err = os.Stat(snapcraftProfile) + c.Check(err, IsNil) + _, err = os.Stat(snapcraftPrProfile) + c.Check(err, IsNil) + + // Remove snapcraft (sans the -pr suffix) and check that its profile was removed. + // Check that this didn't remove the profile of snapcraft-pr installed earlier. + s.RemoveSnap(c, snapInfo) + _, err = os.Stat(snapcraftProfile) + c.Check(os.IsNotExist(err), Equals, true) + _, err = os.Stat(snapcraftPrProfile) + c.Check(err, IsNil) +} + func (s *backendSuite) TestRealDefaultTemplateIsNormallyUsed(c *C) { restore := release.MockAppArmorLevel(release.FullAppArmor) defer restore() @@ -1224,3 +1281,12 @@ s.RemoveSnap(c, snapInfo) } } + +func (s *backendSuite) TestProfileGlobs(c *C) { + globs := apparmor.ProfileGlobs("foo") + c.Assert(globs, DeepEquals, []string{"snap.foo.*", "snap-update-ns.foo"}) +} + +func (s *backendSuite) TestNsProfile(c *C) { + c.Assert(apparmor.NsProfile("foo"), Equals, "snap-update-ns.foo") +} diff -Nru snapd-2.32.3.2/interfaces/apparmor/export_test.go snapd-2.32.9/interfaces/apparmor/export_test.go --- snapd-2.32.3.2/interfaces/apparmor/export_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/interfaces/apparmor/export_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -27,6 +27,8 @@ var ( SnapConfineFromCoreProfile = snapConfineFromCoreProfile + ProfileGlobs = profileGlobs + NsProfile = nsProfile ) // MockIsRootWritableOverlay mocks the real implementation of osutil.IsRootWritableOverlay diff -Nru snapd-2.32.3.2/overlord/cmdstate/cmdmgr.go snapd-2.32.9/overlord/cmdstate/cmdmgr.go --- snapd-2.32.3.2/overlord/cmdstate/cmdmgr.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/cmdstate/cmdmgr.go 2018-05-16 08:20:08.000000000 +0000 @@ -61,19 +61,28 @@ m.runner.Stop() } -var execTimeout = 5 * time.Second +var defaultExecTimeout = 5 * time.Second func doExec(t *state.Task, tomb *tomb.Tomb) error { var argv []string + var tout time.Duration + st := t.State() st.Lock() - err := t.Get("argv", &argv) + err1 := t.Get("argv", &argv) + err2 := t.Get("timeout", &tout) st.Unlock() - if err != nil { - return err + if err1 != nil { + return err1 + } + if err2 != nil && err2 != state.ErrNoState { + return err2 + } + if err2 == state.ErrNoState { + tout = defaultExecTimeout } - if buf, err := osutil.RunAndWait(argv, nil, execTimeout, tomb); err != nil { + if buf, err := osutil.RunAndWait(argv, nil, tout, tomb); err != nil { st.Lock() t.Errorf("# %s\n%s", strings.Join(argv, " "), buf) st.Unlock() diff -Nru snapd-2.32.3.2/overlord/cmdstate/cmdstate.go snapd-2.32.9/overlord/cmdstate/cmdstate.go --- snapd-2.32.3.2/overlord/cmdstate/cmdstate.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/cmdstate/cmdstate.go 2018-05-16 08:20:08.000000000 +0000 @@ -22,12 +22,16 @@ package cmdstate import ( + "time" + "github.com/snapcore/snapd/overlord/state" ) -// Exec creates a task that will execute the given command. -func Exec(st *state.State, summary string, argv []string) *state.TaskSet { +// ExecWithTimeout creates a task that will execute the given command +// with the given timeout. +func ExecWithTimeout(st *state.State, summary string, argv []string, timeout time.Duration) *state.TaskSet { t := st.NewTask("exec-command", summary) t.Set("argv", argv) + t.Set("timeout", timeout) return state.NewTaskSet(t) } diff -Nru snapd-2.32.3.2/overlord/cmdstate/cmdstate_test.go snapd-2.32.9/overlord/cmdstate/cmdstate_test.go --- snapd-2.32.3.2/overlord/cmdstate/cmdstate_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/cmdstate/cmdstate_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -71,7 +71,7 @@ s.rootdir = d s.state = state.New(nil) s.manager = cmdstate.Manager(s.state) - s.restore = cmdstate.MockExecTimeout(time.Second / 10) + s.restore = cmdstate.MockDefaultExecTimeout(time.Second / 10) } func (s *cmdSuite) TearDownTest(c *check.C) { @@ -88,7 +88,7 @@ s.state.Lock() defer s.state.Unlock() argvIn := []string{"/bin/echo", "hello"} - tasks := cmdstate.Exec(s.state, "this is the summary", argvIn).Tasks() + tasks := cmdstate.ExecWithTimeout(s.state, "this is the summary", argvIn, time.Second/10).Tasks() c.Assert(tasks, check.HasLen, 1) task := tasks[0] c.Check(task.Kind(), check.Equals, "exec-command") @@ -103,7 +103,7 @@ defer s.state.Unlock() fn := filepath.Join(s.rootdir, "flag") - ts := cmdstate.Exec(s.state, "Doing the thing", []string{"touch", fn}) + ts := cmdstate.ExecWithTimeout(s.state, "Doing the thing", []string{"touch", fn}, time.Second/10) chg := s.state.NewChange("do-the-thing", "Doing the thing") chg.AddAll(ts) @@ -117,7 +117,7 @@ s.state.Lock() defer s.state.Unlock() - ts := cmdstate.Exec(s.state, "Doing the thing", []string{"sh", "-c", "echo hello; false"}) + ts := cmdstate.ExecWithTimeout(s.state, "Doing the thing", []string{"sh", "-c", "echo hello; false"}, time.Second/10) chg := s.state.NewChange("do-the-thing", "Doing the thing") chg.AddAll(ts) @@ -130,7 +130,7 @@ s.state.Lock() defer s.state.Unlock() - ts := cmdstate.Exec(s.state, "Doing the thing", []string{"sleep", "1h"}) + ts := cmdstate.ExecWithTimeout(s.state, "Doing the thing", []string{"sleep", "1h"}, time.Second/10) chg := s.state.NewChange("do-the-thing", "Doing the thing") chg.AddAll(ts) @@ -152,7 +152,7 @@ s.state.Lock() defer s.state.Unlock() - ts := cmdstate.Exec(s.state, "Doing the thing", []string{"sleep", "1h"}) + ts := cmdstate.ExecWithTimeout(s.state, "Doing the thing", []string{"sleep", "1h"}, time.Second/10) chg := s.state.NewChange("do-the-thing", "Doing the thing") chg.AddAll(ts) @@ -170,7 +170,7 @@ s.state.Lock() defer s.state.Unlock() - ts := cmdstate.Exec(s.state, "Doing the thing", []string{"sleep", "1m"}) + ts := cmdstate.ExecWithTimeout(s.state, "Doing the thing", []string{"sleep", "1m"}, time.Second/10) chg := s.state.NewChange("do-the-thing", "Doing the thing") chg.AddAll(ts) diff -Nru snapd-2.32.3.2/overlord/cmdstate/export_test.go snapd-2.32.9/overlord/cmdstate/export_test.go --- snapd-2.32.3.2/overlord/cmdstate/export_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/cmdstate/export_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -23,10 +23,10 @@ "time" ) -func MockExecTimeout(t time.Duration) func() { - ot := execTimeout - execTimeout = t +func MockDefaultExecTimeout(t time.Duration) func() { + ot := defaultExecTimeout + defaultExecTimeout = t return func() { - execTimeout = ot + defaultExecTimeout = ot } } diff -Nru snapd-2.32.3.2/overlord/configstate/configcore/corecfg.go snapd-2.32.9/overlord/configstate/configcore/corecfg.go --- snapd-2.32.3.2/overlord/configstate/configcore/corecfg.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/configstate/configcore/corecfg.go 2018-05-16 08:20:08.000000000 +0000 @@ -57,6 +57,7 @@ if err := validateRefreshSchedule(tr); err != nil { return err } + // FIXME: ensure the user cannot set "core seed.done" // capture cloud information if err := setCloudInfoWhenSeeding(tr); err != nil { diff -Nru snapd-2.32.3.2/overlord/devicestate/devicemgr.go snapd-2.32.9/overlord/devicestate/devicemgr.go --- snapd-2.32.3.2/overlord/devicestate/devicemgr.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/devicestate/devicemgr.go 2018-05-16 08:20:08.000000000 +0000 @@ -31,6 +31,7 @@ "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/overlord/assertstate" "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/hookstate" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" @@ -49,6 +50,8 @@ bootOkRan bool bootRevisionsUpdated bool + ensureSeedInConfigRan bool + lastBecomeOperationalAttempt time.Time becomeOperationalBackoff time.Duration } @@ -342,6 +345,48 @@ return nil } +func markSeededInConfig(st *state.State) error { + var seedDone bool + tr := config.NewTransaction(st) + if err := tr.Get("core", "seed.loaded", &seedDone); err != nil && !config.IsNoOption(err) { + return err + } + if !seedDone { + if err := tr.Set("core", "seed.loaded", true); err != nil { + return err + } + tr.Commit() + } + return nil +} + +func (m *DeviceManager) ensureSeedInConfig() error { + m.state.Lock() + defer m.state.Unlock() + + if !m.ensureSeedInConfigRan { + // get global seeded option + var seeded bool + if err := m.state.Get("seeded", &seeded); err != nil && err != state.ErrNoState { + return err + } + if !seeded { + // wait for ensure again, this is fine because + // doMarkSeeded will run "EnsureBefore(0)" + return nil + } + + // sync seeding with the configuration state + if err := markSeededInConfig(m.state); err != nil { + return err + } + m.ensureSeedInConfigRan = true + } + + return nil + +} + type ensureError struct { errs []error } @@ -376,6 +421,10 @@ errs = append(errs, err) } + if err := m.ensureSeedInConfig(); err != nil { + errs = append(errs, err) + } + m.runner.Ensure() if len(errs) > 0 { diff -Nru snapd-2.32.3.2/overlord/hookstate/ctlcmd/services_test.go snapd-2.32.9/overlord/hookstate/ctlcmd/services_test.go --- snapd-2.32.3.2/overlord/hookstate/ctlcmd/services_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/hookstate/ctlcmd/services_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -47,18 +47,18 @@ storetest.Store } -func (f *fakeStore) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) { - return &snap.Info{ - SideInfo: snap.SideInfo{ - RealName: spec.Name, - Revision: snap.R(2), - }, - Publisher: "foo", - Architectures: []string{"all"}, - }, nil -} +func (f *fakeStore) SnapAction(_ context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) { + if len(actions) == 1 && actions[0].Action == "install" { + return []*snap.Info{{ + SideInfo: snap.SideInfo{ + RealName: actions[0].Name, + Revision: snap.R(2), + }, + Publisher: "foo", + Architectures: []string{"all"}, + }}, nil + } -func (f *fakeStore) ListRefresh(_ context.Context, cand []*store.RefreshCandidate, user *auth.UserState, opt *store.RefreshOptions) ([]*snap.Info, error) { return []*snap.Info{{ SideInfo: snap.SideInfo{ RealName: "test-snap", diff -Nru snapd-2.32.3.2/overlord/managers_test.go snapd-2.32.9/overlord/managers_test.go --- snapd-2.32.3.2/overlord/managers_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/managers_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -360,6 +360,29 @@ "summary": "Foo", "version": "@VERSION@" }` + + snapV2 = `{ + "architectures": [ + "all" + ], + "download": { + "url": "@URL@" + }, + "type": "app", + "name": "@NAME@", + "revision": @REVISION@, + "snap-id": "@SNAPID@", + "summary": "Foo", + "description": "this is a description", + "version": "@VERSION@", + "publisher": { + "id": "devdevdev", + "name": "bar" + }, + "media": [ + {"type": "icon", "url": "@ICON@"} + ] +}` ) var fooSnapID = fakeSnapID("foo") @@ -416,7 +439,7 @@ func (ms *mgrsSuite) mockStore(c *C) *httptest.Server { var baseURL *url.URL - fillHit := func(name string) string { + fillHit := func(hitTemplate, name string) string { snapf, err := snap.Open(ms.serveSnapPath[name]) if err != nil { panic(err) @@ -425,7 +448,7 @@ if err != nil { panic(err) } - hit := strings.Replace(searchHit, "@URL@", baseURL.String()+"/api/v1/snaps/download/"+name, -1) + hit := strings.Replace(hitTemplate, "@URL@", baseURL.String()+"/api/v1/snaps/download/"+name, -1) hit = strings.Replace(hit, "@NAME@", name, -1) hit = strings.Replace(hit, "@SNAPID@", fakeSnapID(name), -1) hit = strings.Replace(hit, "@ICON@", baseURL.String()+"/icon", -1) @@ -435,13 +458,25 @@ } mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // all URLS are /api/v1/snaps/... so check the url is sane and discard - // the common prefix to simplify indexing into the comps slice. + // all URLS are /api/v1/snaps/... or /v2/snaps/... so + // check the url is sane and discard the common prefix + // to simplify indexing into the comps slice. comps := strings.Split(r.URL.Path, "/") - if len(comps) <= 4 { + if len(comps) < 2 { panic("unexpected url path: " + r.URL.Path) } - comps = comps[4:] + if comps[1] == "api" { //v1 + if len(comps) <= 4 { + panic("unexpected url path: " + r.URL.Path) + } + comps = comps[4:] + } else { // v2 + if len(comps) <= 3 { + panic("unexpected url path: " + r.URL.Path) + } + comps = comps[3:] + comps[0] = "v2:" + comps[0] + } switch comps[0] { case "assertions": @@ -465,7 +500,7 @@ return case "details": w.WriteHeader(200) - io.WriteString(w, fillHit(comps[1])) + io.WriteString(w, fillHit(searchHit, comps[1])) case "metadata": dec := json.NewDecoder(r.Body) var input struct { @@ -484,7 +519,7 @@ if snap.R(s.Revision) == snap.R(ms.serveRevision[name]) { continue } - hits = append(hits, json.RawMessage(fillHit(name))) + hits = append(hits, json.RawMessage(fillHit(searchHit, name))) } w.WriteHeader(200) output, err := json.Marshal(map[string]interface{}{ @@ -506,6 +541,50 @@ panic(err) } io.Copy(w, snapR) + case "v2:refresh": + dec := json.NewDecoder(r.Body) + var input struct { + Actions []struct { + Action string `json:"action"` + SnapID string `json:"snap-id"` + Name string `json:"name"` + } `json:"actions"` + } + if err := dec.Decode(&input); err != nil { + panic(err) + } + type resultJSON struct { + Result string `json:"result"` + SnapID string `json:"snap-id"` + Name string `json:"name"` + Snap json.RawMessage `json:"snap"` + } + var results []resultJSON + for _, a := range input.Actions { + name := ms.serveIDtoName[a.SnapID] + if a.Action == "install" { + name = a.Name + } + if ms.serveSnapPath[name] == "" { + // no match + continue + } + results = append(results, resultJSON{ + Result: a.Action, + SnapID: a.SnapID, + Name: name, + Snap: json.RawMessage(fillHit(snapV2, name)), + }) + } + w.WriteHeader(200) + output, err := json.Marshal(map[string]interface{}{ + "results": results, + }) + if err != nil { + panic(err) + } + w.Write(output) + default: panic("unexpected url path: " + r.URL.Path) } diff -Nru snapd-2.32.3.2/overlord/servicestate/servicestate.go snapd-2.32.9/overlord/servicestate/servicestate.go --- snapd-2.32.3.2/overlord/servicestate/servicestate.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/servicestate/servicestate.go 2018-05-16 08:20:08.000000000 +0000 @@ -21,6 +21,7 @@ import ( "fmt" + "time" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/overlord/cmdstate" @@ -107,5 +108,10 @@ return nil, &ServiceActionConflictError{err} } - return cmdstate.Exec(st, desc, argv), nil + // Give the systemctl a maximum time of 61 for now. + // + // Longer term we need to refactor this code and + // reuse the snapd/systemd and snapd/wrapper packages + // to control the timeout in a single place. + return cmdstate.ExecWithTimeout(st, desc, argv, 61*time.Second), nil } diff -Nru snapd-2.32.3.2/overlord/snapstate/autorefresh_test.go snapd-2.32.9/overlord/snapstate/autorefresh_test.go --- snapd-2.32.3.2/overlord/snapstate/autorefresh_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/snapstate/autorefresh_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2017 Canonical Ltd + * Copyright (C) 2017-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -54,6 +54,22 @@ } r.ops = append(r.ops, "list-refresh") return nil, r.listRefreshErr +} + +func (r *autoRefreshStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) { + if ctx == nil || !auth.IsEnsureContext(ctx) { + panic("Ensure marked context required") + } + if len(currentSnaps) != len(actions) || len(currentSnaps) == 0 { + panic("expected in test one action for each current snaps, and at least one snap") + } + for _, a := range actions { + if a.Action != "refresh" { + panic("expected refresh actions") + } + } + r.ops = append(r.ops, "list-refresh") + return nil, r.listRefreshErr } type autoRefreshTestSuite struct { diff -Nru snapd-2.32.3.2/overlord/snapstate/backend/setup.go snapd-2.32.9/overlord/snapstate/backend/setup.go --- snapd-2.32.3.2/overlord/snapstate/backend/setup.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/snapstate/backend/setup.go 2018-05-16 08:20:08.000000000 +0000 @@ -30,12 +30,12 @@ ) // SetupSnap does prepare and mount the snap for further processing. -func (b Backend) SetupSnap(snapFilePath string, sideInfo *snap.SideInfo, meter progress.Meter) (err error) { +func (b Backend) SetupSnap(snapFilePath string, sideInfo *snap.SideInfo, meter progress.Meter) (snapType snap.Type, err error) { // This assumes that the snap was already verified or --dangerous was used. s, snapf, oErr := OpenSnapFile(snapFilePath, sideInfo) if oErr != nil { - return oErr + return snapType, oErr } instdir := s.MountDir() @@ -50,25 +50,25 @@ }() if err := os.MkdirAll(instdir, 0755); err != nil { - return err + return snapType, err } if err := snapf.Install(s.MountFile(), instdir); err != nil { - return err + return snapType, err } // generate the mount unit for the squashfs if err := addMountUnit(s, meter); err != nil { - return err + return snapType, err } if s.Type == snap.TypeKernel { if err := boot.ExtractKernelAssets(s, snapf); err != nil { - return fmt.Errorf("cannot install kernel: %s", err) + return snapType, fmt.Errorf("cannot install kernel: %s", err) } } - return err + return s.Type, err } // RemoveSnapFiles removes the snap files from the disk after unmounting the snap. diff -Nru snapd-2.32.3.2/overlord/snapstate/backend/setup_test.go snapd-2.32.9/overlord/snapstate/backend/setup_test.go --- snapd-2.32.3.2/overlord/snapstate/backend/setup_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/snapstate/backend/setup_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -80,8 +80,9 @@ Revision: snap.R(14), } - err := s.be.SetupSnap(snapPath, &si, progress.Null) + snapType, err := s.be.SetupSnap(snapPath, &si, progress.Null) c.Assert(err, IsNil) + c.Check(snapType, Equals, snap.TypeApp) // after setup the snap file is in the right dir c.Assert(osutil.FileExists(filepath.Join(dirs.SnapBlobDir, "hello_14.snap")), Equals, true) @@ -131,8 +132,9 @@ Revision: snap.R(140), } - err := s.be.SetupSnap(snapPath, &si, progress.Null) + snapType, err := s.be.SetupSnap(snapPath, &si, progress.Null) c.Assert(err, IsNil) + c.Check(snapType, Equals, snap.TypeKernel) l, _ := filepath.Glob(filepath.Join(bootloader.Dir(), "*")) c.Assert(l, HasLen, 1) @@ -175,11 +177,11 @@ Revision: snap.R(140), } - err := s.be.SetupSnap(snapPath, &si, progress.Null) + _, err := s.be.SetupSnap(snapPath, &si, progress.Null) c.Assert(err, IsNil) // retry run - err = s.be.SetupSnap(snapPath, &si, progress.Null) + _, err = s.be.SetupSnap(snapPath, &si, progress.Null) c.Assert(err, IsNil) minInfo := snap.MinimalPlaceInfo("kernel", snap.R(140)) @@ -224,7 +226,7 @@ Revision: snap.R(140), } - err := s.be.SetupSnap(snapPath, &si, progress.Null) + _, err := s.be.SetupSnap(snapPath, &si, progress.Null) c.Assert(err, IsNil) minInfo := snap.MinimalPlaceInfo("kernel", snap.R(140)) @@ -264,7 +266,7 @@ }) defer r() - err := s.be.SetupSnap(snapPath, &si, progress.Null) + _, err := s.be.SetupSnap(snapPath, &si, progress.Null) c.Assert(err, ErrorMatches, "failed") // everything is gone diff -Nru snapd-2.32.3.2/overlord/snapstate/backend.go snapd-2.32.9/overlord/snapstate/backend.go --- snapd-2.32.3.2/overlord/snapstate/backend.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/snapstate/backend.go 2018-05-16 08:20:08.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2017 Canonical Ltd + * Copyright (C) 2016-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -37,9 +37,13 @@ SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) Find(search *store.Search, user *auth.UserState) ([]*snap.Info, error) LookupRefresh(*store.RefreshCandidate, *auth.UserState) (*snap.Info, error) + ListRefresh(context.Context, []*store.RefreshCandidate, *auth.UserState, *store.RefreshOptions) ([]*snap.Info, error) + SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) + Sections(ctx context.Context, user *auth.UserState) ([]string, error) WriteCatalogs(ctx context.Context, names io.Writer, adder store.SnapAdder) error + Download(context.Context, string, string, *snap.DownloadInfo, progress.Meter, *auth.UserState) error Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error) @@ -51,7 +55,7 @@ type managerBackend interface { // install releated - SetupSnap(snapFilePath string, si *snap.SideInfo, meter progress.Meter) error + SetupSnap(snapFilePath string, si *snap.SideInfo, meter progress.Meter) (snap.Type, error) CopySnapData(newSnap, oldSnap *snap.Info, meter progress.Meter) error LinkSnap(info *snap.Info) error StartServices(svcs []*snap.AppInfo, meter progress.Meter) error diff -Nru snapd-2.32.3.2/overlord/snapstate/backend_test.go snapd-2.32.9/overlord/snapstate/backend_test.go --- snapd-2.32.3.2/overlord/snapstate/backend_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/snapstate/backend_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2017 Canonical Ltd + * Copyright (C) 2016-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -47,7 +47,9 @@ revno snap.Revision sinfo snap.SideInfo stype snap.Type - cand store.RefreshCandidate + + curSnaps []store.CurrentSnap + action store.SnapAction old string @@ -93,6 +95,29 @@ macaroon string } +type byName []store.CurrentSnap + +func (bna byName) Len() int { return len(bna) } +func (bna byName) Swap(i, j int) { bna[i], bna[j] = bna[j], bna[i] } +func (bna byName) Less(i, j int) bool { + return bna[i].Name < bna[j].Name +} + +type byAction []*store.SnapAction + +func (ba byAction) Len() int { return len(ba) } +func (ba byAction) Swap(i, j int) { ba[i], ba[j] = ba[j], ba[i] } +func (ba byAction) Less(i, j int) bool { + if ba[i].Action == ba[j].Action { + if ba[i].Action == "refresh" { + return ba[i].SnapID < ba[j].SnapID + } else { + return ba[i].Name < ba[j].Name + } + } + return ba[i].Action < ba[j].Action +} + type fakeStore struct { storetest.Store @@ -114,6 +139,18 @@ func (f *fakeStore) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) { f.pokeStateLock() + info, err := f.snapInfo(spec, user) + + userID := 0 + if user != nil { + userID = user.ID + } + f.fakeBackend.ops = append(f.fakeBackend.ops, fakeOp{op: "storesvc-snap", name: spec.Name, revno: info.Revision, userID: userID}) + + return info, err +} + +func (f *fakeStore) snapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) { if spec.Revision.Unset() { spec.Revision = snap.R(11) if spec.Channel == "channel-for-7" { @@ -128,6 +165,10 @@ typ = snap.TypeOS } + if spec.Name == "snap-unknown" { + return nil, store.ErrSnapNotFound + } + info := &snap.Info{ Architectures: []string{"all"}, SideInfo: snap.SideInfo{ @@ -163,27 +204,24 @@ } } - userID := 0 - if user != nil { - userID = user.ID - } - f.fakeBackend.ops = append(f.fakeBackend.ops, fakeOp{op: "storesvc-snap", name: spec.Name, revno: spec.Revision, userID: userID}) - return info, nil } -func (f *fakeStore) LookupRefresh(cand *store.RefreshCandidate, user *auth.UserState) (*snap.Info, error) { - f.pokeStateLock() - - if cand == nil { - panic("LookupRefresh called with no candidate") - } +type refreshCand struct { + snapID string + channel string + revision snap.Revision + block []snap.Revision + ignoreValidation bool +} +func (f *fakeStore) lookupRefresh(cand refreshCand) (*snap.Info, error) { var name string - switch cand.SnapID { + typ := snap.TypeApp + switch cand.snapID { case "": - return nil, store.ErrLocalSnap + panic("store refresh APIs expect snap-ids") case "other-snap-id": return nil, store.ErrNoUpdateAvailable case "fakestore-please-error-on-refresh": @@ -194,18 +232,26 @@ name = "some-snap" case "core-snap-id": name = "core" + typ = snap.TypeOS case "snap-with-snapd-control-id": name = "snap-with-snapd-control" + case "producer-id": + name = "producer" + case "consumer-id": + name = "consumer" + case "some-base-id": + name = "some-base" + typ = snap.TypeBase default: - panic(fmt.Sprintf("ListRefresh: unknown snap-id: %s", cand.SnapID)) + panic(fmt.Sprintf("refresh: unknown snap-id: %s", cand.snapID)) } revno := snap.R(11) - if r := f.refreshRevnos[cand.SnapID]; !r.Unset() { + if r := f.refreshRevnos[cand.snapID]; !r.Unset() { revno = r } confinement := snap.StrictConfinement - switch cand.Channel { + switch cand.channel { case "channel-for-7": revno = snap.R(7) case "channel-for-classic": @@ -215,10 +261,11 @@ } info := &snap.Info{ + Type: typ, SideInfo: snap.SideInfo{ RealName: name, - Channel: cand.Channel, - SnapID: cand.SnapID, + Channel: cand.channel, + SnapID: cand.snapID, Revision: revno, }, Version: name, @@ -228,7 +275,7 @@ Confinement: confinement, Architectures: []string{"all"}, } - switch cand.Channel { + switch cand.channel { case "channel-for-layout": info.Layout = map[string]*snap.Layout{ "/usr": { @@ -237,26 +284,21 @@ Symlink: "$SNAP/usr", }, } + case "channel-for-base": + info.Base = "some-base" } var hit snap.Revision - if cand.Revision != revno { + if cand.revision != revno { hit = revno } - for _, blocked := range cand.Block { + for _, blocked := range cand.block { if blocked == revno { hit = snap.Revision{} break } } - userID := 0 - if user != nil { - userID = user.ID - } - // TODO: move this back to ListRefresh - f.fakeBackend.ops = append(f.fakeBackend.ops, fakeOp{op: "storesvc-list-refresh", cand: *cand, revno: hit, userID: userID}) - if !hit.Unset() { return info, nil } @@ -264,28 +306,136 @@ return nil, store.ErrNoUpdateAvailable } -func (f *fakeStore) ListRefresh(ctx context.Context, cands []*store.RefreshCandidate, user *auth.UserState, flags *store.RefreshOptions) ([]*snap.Info, error) { +func (f *fakeStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) { if ctx == nil { panic("context required") } f.pokeStateLock() - if len(cands) == 0 { + if len(currentSnaps) == 0 && len(actions) == 0 { return nil, nil } - if len(cands) > 3 { - panic("fake ListRefresh unexpectedly called with more than 3 candidates") + if len(actions) > 3 { + panic("fake SnapAction unexpectedly called with more than 3 actions") + } + + curByID := make(map[string]*store.CurrentSnap, len(currentSnaps)) + curSnaps := make(byName, len(currentSnaps)) + for i, cur := range currentSnaps { + if cur.Name == "" || cur.SnapID == "" || cur.Revision.Unset() { + return nil, fmt.Errorf("internal error: incomplete current snap info") + } + curByID[cur.SnapID] = cur + curSnaps[i] = *cur + } + sort.Sort(curSnaps) + + userID := 0 + if user != nil { + userID = user.ID + } + if len(curSnaps) == 0 { + curSnaps = nil } + f.fakeBackend.ops = append(f.fakeBackend.ops, fakeOp{op: "storesvc-snap-action", curSnaps: curSnaps, userID: userID}) + sorted := make(byAction, len(actions)) + copy(sorted, actions) + sort.Sort(sorted) + + refreshErrors := make(map[string]error) + installErrors := make(map[string]error) var res []*snap.Info - for _, cand := range cands { - info, err := f.LookupRefresh(cand, user) - if err == store.ErrLocalSnap || err == store.ErrNoUpdateAvailable { + for _, a := range sorted { + if a.Action != "install" && a.Action != "refresh" { + panic("not supported") + } + + if a.Action == "install" { + spec := store.SnapSpec{ + Name: a.Name, + Channel: a.Channel, + Revision: a.Revision, + } + info, err := f.snapInfo(spec, user) + if err != nil { + installErrors[a.Name] = err + continue + } + f.fakeBackend.ops = append(f.fakeBackend.ops, fakeOp{ + op: "storesvc-snap-action:action", + action: *a, + revno: info.Revision, + userID: userID, + }) + if !a.Revision.Unset() { + info.Channel = "" + } + res = append(res, info) continue } + + // refresh + + cur := curByID[a.SnapID] + channel := a.Channel + if channel == "" { + channel = cur.TrackingChannel + } + ignoreValidation := cur.IgnoreValidation + if a.Flags&store.SnapActionIgnoreValidation != 0 { + ignoreValidation = true + } else if a.Flags&store.SnapActionEnforceValidation != 0 { + ignoreValidation = false + } + cand := refreshCand{ + snapID: a.SnapID, + channel: channel, + revision: cur.Revision, + block: cur.Block, + ignoreValidation: ignoreValidation, + } + info, err := f.lookupRefresh(cand) + var hit snap.Revision + if info != nil { + if !a.Revision.Unset() { + info.Revision = a.Revision + } + hit = info.Revision + } + f.fakeBackend.ops = append(f.fakeBackend.ops, fakeOp{ + op: "storesvc-snap-action:action", + action: *a, + revno: hit, + userID: userID, + }) + if err == store.ErrNoUpdateAvailable { + refreshErrors[cur.Name] = err + continue + } + if err != nil { + return nil, err + } + if !a.Revision.Unset() { + info.Channel = "" + } res = append(res, info) } + if len(refreshErrors)+len(installErrors) > 0 || len(res) == 0 { + if len(refreshErrors) == 0 { + refreshErrors = nil + } + if len(installErrors) == 0 { + installErrors = nil + } + return res, &store.SnapActionError{ + NoResults: len(refreshErrors)+len(installErrors)+len(res) == 0, + Refresh: refreshErrors, + Install: installErrors, + } + } + return res, nil } @@ -365,7 +515,7 @@ return &snap.Info{SuggestedName: name, Architectures: []string{"all"}}, f.emptyContainer, nil } -func (f *fakeSnappyBackend) SetupSnap(snapFilePath string, si *snap.SideInfo, p progress.Meter) error { +func (f *fakeSnappyBackend) SetupSnap(snapFilePath string, si *snap.SideInfo, p progress.Meter) (snap.Type, error) { p.Notify("setup-snap") revno := snap.R(0) if si != nil { @@ -376,13 +526,23 @@ name: snapFilePath, revno: revno, }) - return nil + snapType := snap.TypeApp + switch si.RealName { + case "core": + snapType = snap.TypeOS + case "gadget": + snapType = snap.TypeGadget + } + return snapType, nil } func (f *fakeSnappyBackend) ReadInfo(name string, si *snap.SideInfo) (*snap.Info, error) { - if name == "borken" { + if name == "borken" && si.Revision == snap.R(2) { return nil, errors.New(`cannot read info for "borken" snap`) } + if name == "not-there" && si.Revision == snap.R(2) { + return nil, &snap.NotFoundError{Snap: name, Revision: si.Revision} + } // naive emulation for now, always works info := &snap.Info{ SuggestedName: name, diff -Nru snapd-2.32.3.2/overlord/snapstate/export_test.go snapd-2.32.9/overlord/snapstate/export_test.go --- snapd-2.32.3.2/overlord/snapstate/export_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/snapstate/export_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -21,6 +21,7 @@ import ( "errors" + "sort" "time" "gopkg.in/tomb.v2" @@ -83,10 +84,25 @@ m.runner.AddHandler(adhoc, do, undo) } -func MockReadInfo(mock func(name string, si *snap.SideInfo) (*snap.Info, error)) (restore func()) { - old := readInfo - readInfo = mock - return func() { readInfo = old } +func MockSnapReadInfo(mock func(name string, si *snap.SideInfo) (*snap.Info, error)) (restore func()) { + old := snapReadInfo + snapReadInfo = mock + return func() { snapReadInfo = old } +} + +func MockMountPollInterval(intv time.Duration) (restore func()) { + old := mountPollInterval + mountPollInterval = intv + return func() { mountPollInterval = old } +} + +func MockRevisionDate(mock func(info *snap.Info) time.Time) (restore func()) { + old := revisionDate + if mock == nil { + mock = revisionDateImpl + } + revisionDate = mock + return func() { revisionDate = old } } func MockOpenSnapFile(mock func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error)) (restore func()) { @@ -163,3 +179,8 @@ refreshRetryDelay = origRefreshRetryDelay } } + +func ByKindOrder(snaps ...*snap.Info) []*snap.Info { + sort.Sort(byKind(snaps)) + return snaps +} diff -Nru snapd-2.32.3.2/overlord/snapstate/handlers_discard_test.go snapd-2.32.9/overlord/snapstate/handlers_discard_test.go --- snapd-2.32.3.2/overlord/snapstate/handlers_discard_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/snapstate/handlers_discard_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -53,7 +53,7 @@ snapstate.SetSnapManagerBackend(s.snapmgr, s.fakeBackend) - s.reset = snapstate.MockReadInfo(s.fakeBackend.ReadInfo) + s.reset = snapstate.MockSnapReadInfo(s.fakeBackend.ReadInfo) } func (s *discardSnapSuite) TearDownTest(c *C) { diff -Nru snapd-2.32.3.2/overlord/snapstate/handlers_download_test.go snapd-2.32.9/overlord/snapstate/handlers_download_test.go --- snapd-2.32.3.2/overlord/snapstate/handlers_download_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/snapstate/handlers_download_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -25,6 +25,7 @@ "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/store" ) type downloadSnapSuite struct { @@ -58,7 +59,7 @@ snapstate.SetSnapManagerBackend(s.snapmgr, s.fakeBackend) - s.reset = snapstate.MockReadInfo(s.fakeBackend.ReadInfo) + s.reset = snapstate.MockSnapReadInfo(s.fakeBackend.ReadInfo) } func (s *downloadSnapSuite) TearDownTest(c *C) { @@ -90,8 +91,15 @@ // the compat code called the store "Snap" endpoint c.Assert(s.fakeBackend.ops, DeepEquals, fakeOps{ { - op: "storesvc-snap", - name: "foo", + op: "storesvc-snap-action", + }, + { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "install", + Name: "foo", + Channel: "some-channel", + }, revno: snap.R(11), }, { diff -Nru snapd-2.32.3.2/overlord/snapstate/handlers.go snapd-2.32.9/overlord/snapstate/handlers.go --- snapd-2.32.3.2/overlord/snapstate/handlers.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/snapstate/handlers.go 2018-05-16 08:20:08.000000000 +0000 @@ -37,7 +37,6 @@ "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" - "github.com/snapcore/snapd/store" ) // TaskSnapSetup returns the SnapSetup with task params hold by or referred to by the the task. @@ -114,7 +113,22 @@ */ const defaultCoreSnapName = "core" -const defaultBaseSnapsChannel = "stable" + +func defaultBaseSnapsChannel() string { + channel := os.Getenv("SNAPD_BASES_CHANNEL") + if channel == "" { + return "stable" + } + return channel +} + +func defaultPrereqSnapsChannel() string { + channel := os.Getenv("SNAPD_PREREQS_CHANNEL") + if channel == "" { + return "stable" + } + return channel +} func linkSnapInFlight(st *state.State, snapName string) (bool, error) { for _, chg := range st.Changes() { @@ -183,7 +197,7 @@ return nil } -func (m *SnapManager) installOneBaseOrRequired(st *state.State, snapName string, onInFlight error, userID int) (*state.TaskSet, error) { +func (m *SnapManager) installOneBaseOrRequired(st *state.State, snapName, channel string, onInFlight error, userID int) (*state.TaskSet, error) { // installed already? isInstalled, err := isInstalled(st, snapName) if err != nil { @@ -202,7 +216,7 @@ } // not installed, nor queued for install -> install it - ts, err := Install(st, snapName, defaultBaseSnapsChannel, snap.R(0), userID, Flags{}) + ts, err := Install(st, snapName, channel, snap.R(0), userID, Flags{}) // something might have triggered an explicit install while // the state was unlocked -> deal with that here by simply // retrying the operation. @@ -224,7 +238,7 @@ var tss []*state.TaskSet for _, prereqName := range prereq { var onInFlightErr error = nil - ts, err := m.installOneBaseOrRequired(st, prereqName, onInFlightErr, userID) + ts, err := m.installOneBaseOrRequired(st, prereqName, defaultPrereqSnapsChannel(), onInFlightErr, userID) if err != nil { return err } @@ -237,7 +251,7 @@ // for base snaps we need to wait until the change is done // (either finished or failed) onInFlightErr := &state.Retry{After: prerequisitesRetryTimeout} - tsBase, err := m.installOneBaseOrRequired(st, base, onInFlightErr, userID) + tsBase, err := m.installOneBaseOrRequired(st, base, defaultBaseSnapsChannel(), onInFlightErr, userID) if err != nil { return err } @@ -363,6 +377,12 @@ return nil } +func installInfoUnlocked(st *state.State, snapsup *SnapSetup) (*snap.Info, error) { + st.Lock() + defer st.Unlock() + return installInfo(st, snapsup.Name(), snapsup.Channel, snapsup.Revision(), snapsup.UserID) +} + func (m *SnapManager) doDownloadSnap(t *state.Task, tomb *tomb.Tomb) error { st := t.State() st.Lock() @@ -387,12 +407,7 @@ // COMPATIBILITY - this task was created from an older version // of snapd that did not store the DownloadInfo in the state // yet. - spec := store.SnapSpec{ - Name: snapsup.Name(), - Channel: snapsup.Channel, - Revision: snapsup.Revision(), - } - storeInfo, err = theStore.SnapInfo(spec, user) + storeInfo, err = installInfoUnlocked(st, snapsup) if err != nil { return err } @@ -415,6 +430,10 @@ return nil } +var ( + mountPollInterval = 1 * time.Second +) + func (m *SnapManager) doMountSnap(t *state.Task, _ *tomb.Tomb) error { t.State().Lock() snapsup, snapst, err := snapSetupAndState(t) @@ -436,18 +455,41 @@ pb := NewTaskProgressAdapterUnlocked(t) // TODO Use snapsup.Revision() to obtain the right info to mount // instead of assuming the candidate is the right one. - if err := m.backend.SetupSnap(snapsup.SnapPath, snapsup.SideInfo, pb); err != nil { + snapType, err := m.backend.SetupSnap(snapsup.SnapPath, snapsup.SideInfo, pb) + if err != nil { return err } - // set snapst type for undoMountSnap - newInfo, err := readInfo(snapsup.Name(), snapsup.SideInfo) - if err != nil { - return err + // double check that the snap is mounted + var readInfoErr error + for i := 0; i < 10; i++ { + _, readInfoErr = readInfo(snapsup.Name(), snapsup.SideInfo, errorOnBroken) + if readInfoErr == nil { + break + } + if _, ok := readInfoErr.(*snap.NotFoundError); !ok { + break + } + // snap not found, seems is not mounted yet + msg := fmt.Sprintf("expected snap %q revision %v to be mounted but is not", snapsup.Name(), snapsup.Revision()) + readInfoErr = fmt.Errorf("cannot proceed, %s", msg) + if i == 0 { + logger.Noticef(msg) + } + time.Sleep(mountPollInterval) + } + if readInfoErr != nil { + if err := m.backend.UndoSetupSnap(snapsup.placeInfo(), snapType, pb); err != nil { + t.State().Lock() + t.Errorf("cannot undo partial setup snap %q: %v", snapsup.Name(), err) + t.State().Unlock() + } + return readInfoErr } + // set snapst type for undoMountSnap t.State().Lock() - t.Set("snap-type", newInfo.Type) + t.Set("snap-type", snapType) t.State().Unlock() if snapsup.Flags.RemoveSnapPath { @@ -554,7 +596,7 @@ return err } - newInfo, err := readInfo(snapsup.Name(), snapsup.SideInfo) + newInfo, err := readInfo(snapsup.Name(), snapsup.SideInfo, 0) if err != nil { return err } @@ -576,7 +618,7 @@ return err } - newInfo, err := readInfo(snapsup.Name(), snapsup.SideInfo) + newInfo, err := readInfo(snapsup.Name(), snapsup.SideInfo, 0) if err != nil { return err } @@ -676,7 +718,7 @@ } } - newInfo, err := readInfo(snapsup.Name(), cand) + newInfo, err := readInfo(snapsup.Name(), cand, 0) if err != nil { return err } @@ -850,7 +892,7 @@ snapst.JailMode = oldJailMode snapst.Classic = oldClassic - newInfo, err := readInfo(snapsup.Name(), snapsup.SideInfo) + newInfo, err := readInfo(snapsup.Name(), snapsup.SideInfo, 0) if err != nil { return err } diff -Nru snapd-2.32.3.2/overlord/snapstate/handlers_link_test.go snapd-2.32.9/overlord/snapstate/handlers_link_test.go --- snapd-2.32.3.2/overlord/snapstate/handlers_link_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/snapstate/handlers_link_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -77,7 +77,7 @@ snapstate.SetSnapManagerBackend(s.snapmgr, s.fakeBackend) - resetReadInfo := snapstate.MockReadInfo(s.fakeBackend.ReadInfo) + resetReadInfo := snapstate.MockSnapReadInfo(s.fakeBackend.ReadInfo) s.reset = func() { resetReadInfo() dirs.SetRootDir("/") diff -Nru snapd-2.32.3.2/overlord/snapstate/handlers_mount_test.go snapd-2.32.9/overlord/snapstate/handlers_mount_test.go --- snapd-2.32.3.2/overlord/snapstate/handlers_mount_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/snapstate/handlers_mount_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -21,6 +21,7 @@ import ( "path/filepath" + "time" . "gopkg.in/check.v1" @@ -57,7 +58,7 @@ snapstate.SetSnapManagerBackend(s.snapmgr, s.fakeBackend) - reset1 := snapstate.MockReadInfo(s.fakeBackend.ReadInfo) + reset1 := snapstate.MockSnapReadInfo(s.fakeBackend.ReadInfo) s.reset = func() { reset1() dirs.SetRootDir(oldDir) @@ -153,3 +154,189 @@ }) } + +func (s *mountSnapSuite) TestDoMountSnapError(c *C) { + v1 := "name: borken\nversion: 1.0\n" + testSnap := snaptest.MakeTestSnapWithFiles(c, v1, nil) + + s.state.Lock() + defer s.state.Unlock() + si1 := &snap.SideInfo{ + RealName: "borken", + Revision: snap.R(1), + } + si2 := &snap.SideInfo{ + RealName: "borken", + Revision: snap.R(2), + } + snapstate.Set(s.state, "borken", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{si1}, + Current: si1.Revision, + SnapType: "app", + }) + + t := s.state.NewTask("mount-snap", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: si2, + SnapPath: testSnap, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + s.state.Unlock() + + for i := 0; i < 3; i++ { + s.snapmgr.Ensure() + s.snapmgr.Wait() + } + + s.state.Lock() + + c.Check(chg.Err(), ErrorMatches, `(?s).*cannot read info for "borken" snap.*`) + + c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{ + { + op: "current", + old: filepath.Join(dirs.SnapMountDir, "borken/1"), + }, + { + op: "setup-snap", + name: testSnap, + revno: snap.R(2), + }, + { + op: "undo-setup-snap", + name: filepath.Join(dirs.SnapMountDir, "borken/2"), + stype: "app", + }, + }) +} + +func (s *mountSnapSuite) TestDoMountSnapErrorNotFound(c *C) { + r := snapstate.MockMountPollInterval(10 * time.Millisecond) + defer r() + + v1 := "name: not-there\nversion: 1.0\n" + testSnap := snaptest.MakeTestSnapWithFiles(c, v1, nil) + + s.state.Lock() + defer s.state.Unlock() + si1 := &snap.SideInfo{ + RealName: "not-there", + Revision: snap.R(1), + } + si2 := &snap.SideInfo{ + RealName: "not-there", + Revision: snap.R(2), + } + snapstate.Set(s.state, "not-there", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{si1}, + Current: si1.Revision, + SnapType: "app", + }) + + t := s.state.NewTask("mount-snap", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: si2, + SnapPath: testSnap, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + s.state.Unlock() + + for i := 0; i < 3; i++ { + s.snapmgr.Ensure() + s.snapmgr.Wait() + } + + s.state.Lock() + + c.Check(chg.Err(), ErrorMatches, `(?s).*cannot proceed, expected snap "not-there" revision 2 to be mounted but is not.*`) + + c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{ + { + op: "current", + old: filepath.Join(dirs.SnapMountDir, "not-there/1"), + }, + { + op: "setup-snap", + name: testSnap, + revno: snap.R(2), + }, + { + op: "undo-setup-snap", + name: filepath.Join(dirs.SnapMountDir, "not-there/2"), + stype: "app", + }, + }) +} + +func (s *mountSnapSuite) TestDoMountNotMountedRetryRetry(c *C) { + r := snapstate.MockMountPollInterval(10 * time.Millisecond) + defer r() + n := 0 + slowMountedReadInfo := func(name string, si *snap.SideInfo) (*snap.Info, error) { + n++ + if n < 3 { + return nil, &snap.NotFoundError{Snap: "not-there", Revision: si.Revision} + } + return &snap.Info{ + SideInfo: *si, + }, nil + } + + r1 := snapstate.MockSnapReadInfo(slowMountedReadInfo) + defer r1() + + v1 := "name: not-there\nversion: 1.0\n" + testSnap := snaptest.MakeTestSnapWithFiles(c, v1, nil) + + s.state.Lock() + defer s.state.Unlock() + si1 := &snap.SideInfo{ + RealName: "not-there", + Revision: snap.R(1), + } + si2 := &snap.SideInfo{ + RealName: "not-there", + Revision: snap.R(2), + } + snapstate.Set(s.state, "not-there", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{si1}, + Current: si1.Revision, + SnapType: "app", + }) + + t := s.state.NewTask("mount-snap", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: si2, + SnapPath: testSnap, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + + s.state.Unlock() + + for i := 0; i < 3; i++ { + s.snapmgr.Ensure() + s.snapmgr.Wait() + } + + s.state.Lock() + + c.Check(chg.IsReady(), Equals, true) + c.Check(chg.Err(), IsNil) + + c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{ + { + op: "current", + old: filepath.Join(dirs.SnapMountDir, "not-there/1"), + }, + { + op: "setup-snap", + name: testSnap, + revno: snap.R(2), + }, + }) +} diff -Nru snapd-2.32.3.2/overlord/snapstate/handlers_prepare_test.go snapd-2.32.9/overlord/snapstate/handlers_prepare_test.go --- snapd-2.32.3.2/overlord/snapstate/handlers_prepare_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/snapstate/handlers_prepare_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -52,7 +52,7 @@ snapstate.SetSnapManagerBackend(s.snapmgr, s.fakeBackend) - reset1 := snapstate.MockReadInfo(s.fakeBackend.ReadInfo) + reset1 := snapstate.MockSnapReadInfo(s.fakeBackend.ReadInfo) s.reset = func() { dirs.SetRootDir("/") reset1() diff -Nru snapd-2.32.3.2/overlord/snapstate/handlers_prereq_test.go snapd-2.32.9/overlord/snapstate/handlers_prereq_test.go --- snapd-2.32.3.2/overlord/snapstate/handlers_prereq_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/snapstate/handlers_prereq_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -20,6 +20,7 @@ package snapstate_test import ( + "os" "time" . "gopkg.in/check.v1" @@ -29,6 +30,7 @@ "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/store" ) type prereqSuite struct { @@ -62,7 +64,7 @@ s.snapmgr.AddForeignTaskHandlers(s.fakeBackend) snapstate.SetSnapManagerBackend(s.snapmgr, s.fakeBackend) - s.reset = snapstate.MockReadInfo(s.fakeBackend.ReadInfo) + s.reset = snapstate.MockSnapReadInfo(s.fakeBackend.ReadInfo) } func (s *prereqSuite) TearDownTest(c *C) { @@ -123,18 +125,39 @@ defer s.state.Unlock() c.Assert(s.fakeBackend.ops, DeepEquals, fakeOps{ { - op: "storesvc-snap", - name: "prereq1", + op: "storesvc-snap-action", + }, + { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "install", + Name: "prereq1", + Channel: "stable", + }, revno: snap.R(11), }, { - op: "storesvc-snap", - name: "prereq2", + op: "storesvc-snap-action", + }, + { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "install", + Name: "prereq2", + Channel: "stable", + }, revno: snap.R(11), }, { - op: "storesvc-snap", - name: "some-base", + op: "storesvc-snap-action", + }, + { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "install", + Name: "some-base", + Channel: "stable", + }, revno: snap.R(11), }, }) @@ -236,3 +259,69 @@ c.Check(t.Status(), Equals, state.DoneStatus) } + +func (s *prereqSuite) TestDoPrereqChannelEnvvars(c *C) { + os.Setenv("SNAPD_BASES_CHANNEL", "edge") + defer os.Unsetenv("SNAPD_BASES_CHANNEL") + os.Setenv("SNAPD_PREREQS_CHANNEL", "candidate") + defer os.Unsetenv("SNAPD_PREREQS_CHANNEL") + s.state.Lock() + t := s.state.NewTask("prerequisites", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: "foo", + Revision: snap.R(33), + }, + Channel: "beta", + Base: "some-base", + Prereq: []string{"prereq1", "prereq2"}, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + s.state.Unlock() + + s.snapmgr.Ensure() + s.snapmgr.Wait() + + s.state.Lock() + defer s.state.Unlock() + c.Assert(s.fakeBackend.ops, DeepEquals, fakeOps{ + { + op: "storesvc-snap-action", + }, + { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "install", + Name: "prereq1", + Channel: "candidate", + }, + revno: snap.R(11), + }, + { + op: "storesvc-snap-action", + }, + { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "install", + Name: "prereq2", + Channel: "candidate", + }, + revno: snap.R(11), + }, + { + op: "storesvc-snap-action", + }, + { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "install", + Name: "some-base", + Channel: "edge", + }, + revno: snap.R(11), + }, + }) + c.Check(t.Status(), Equals, state.DoneStatus) +} diff -Nru snapd-2.32.3.2/overlord/snapstate/refreshhints_test.go snapd-2.32.9/overlord/snapstate/refreshhints_test.go --- snapd-2.32.3.2/overlord/snapstate/refreshhints_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/snapstate/refreshhints_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2017 Canonical Ltd + * Copyright (C) 2017-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -47,6 +47,22 @@ } r.ops = append(r.ops, "list-refresh") return nil, nil +} + +func (r *recordingStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) { + if ctx == nil || !auth.IsEnsureContext(ctx) { + panic("Ensure marked context required") + } + if len(currentSnaps) != len(actions) || len(currentSnaps) == 0 { + panic("expected in test one action for each current snaps, and at least one snap") + } + for _, a := range actions { + if a.Action != "refresh" { + panic("expected refresh actions") + } + } + r.ops = append(r.ops, "list-refresh") + return nil, nil } type refreshHintsTestSuite struct { diff -Nru snapd-2.32.3.2/overlord/snapstate/snapmgr.go snapd-2.32.9/overlord/snapstate/snapmgr.go --- snapd-2.32.3.2/overlord/snapstate/snapmgr.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/snapstate/snapmgr.go 2018-05-16 08:20:08.000000000 +0000 @@ -212,10 +212,18 @@ var ErrNoCurrent = errors.New("snap has no current revision") // Retrieval functions -var readInfo = readInfoAnyway -func readInfoAnyway(name string, si *snap.SideInfo) (*snap.Info, error) { - info, err := snap.ReadInfo(name, si) +const ( + errorOnBroken = 1 << iota +) + +var snapReadInfo = snap.ReadInfo + +func readInfo(name string, si *snap.SideInfo, flags int) (*snap.Info, error) { + info, err := snapReadInfo(name, si) + if err != nil && flags&errorOnBroken != 0 { + return nil, err + } if err != nil { logger.Noticef("cannot read snap info of snap %q at revision %s: %s", name, si.Revision, err) } @@ -234,13 +242,24 @@ return info, err } +var revisionDate = revisionDateImpl + +// revisionDate returns a good approximation of when a revision reached the system. +func revisionDateImpl(info *snap.Info) time.Time { + fi, err := os.Lstat(info.MountFile()) + if err != nil { + return time.Time{} + } + return fi.ModTime() +} + // CurrentInfo returns the information about the current active revision or the last active revision (if the snap is inactive). It returns the ErrNoCurrent error if snapst.Current is unset. func (snapst *SnapState) CurrentInfo() (*snap.Info, error) { cur := snapst.CurrentSideInfo() if cur == nil { return nil, ErrNoCurrent } - return readInfo(cur.RealName, cur) + return readInfo(cur.RealName, cur, 0) } func revisionInSequence(snapst *SnapState, needle snap.Revision) bool { diff -Nru snapd-2.32.3.2/overlord/snapstate/snapstate.go snapd-2.32.9/overlord/snapstate/snapstate.go --- snapd-2.32.3.2/overlord/snapstate/snapstate.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/snapstate/snapstate.go 2018-05-16 08:20:08.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2017 Canonical Ltd + * Copyright (C) 2016-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -24,6 +24,7 @@ "encoding/json" "fmt" "reflect" + "sort" "strings" "golang.org/x/net/context" @@ -64,6 +65,9 @@ } func doInstall(st *state.State, snapst *SnapState, snapsup *SnapSetup, flags int) (*state.TaskSet, error) { + if snapsup.Name() == "system" { + return nil, fmt.Errorf("cannot install reserved snap name 'system'") + } if snapst.IsInstalled() && !snapst.Active { return nil, fmt.Errorf("cannot update disabled snap %q", snapsup.Name()) } @@ -607,7 +611,7 @@ return nil, &snap.AlreadyInstalledError{Snap: name} } - info, err := snapInfo(st, name, channel, revision, userID) + info, err := installInfo(st, name, channel, revision, userID) if err != nil { return nil, err } @@ -637,6 +641,7 @@ func InstallMany(st *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) { installed := make([]string, 0, len(names)) tasksets := make([]*state.TaskSet, 0, len(names)) + // TODO: this could be reorged to do one single store call for _, name := range names { ts, err := Install(st, name, "", snap.R(0), userID, Flags{}) // FIXME: is this expected behavior? @@ -740,6 +745,18 @@ reportUpdated[snapName] = true } + // first core, bases, then rest + sort.Sort(byKind(updates)) + prereqs := make(map[string]*state.TaskSet) + waitPrereq := func(ts *state.TaskSet, prereqName string) { + preTs := prereqs[prereqName] + if preTs != nil { + ts.WaitAll(preTs) + } + } + + // updates is sorted by kind so this will process first core + // and bases and then other snaps for _, update := range updates { channel, flags, snapst := params(update) @@ -782,6 +799,24 @@ } ts.JoinLane(st.NewLane()) + // because of the sorting of updates we fill prereqs + // first (if branch) and only then use it to setup + // waits (else branch) + if update.Type == snap.TypeOS || update.Type == snap.TypeBase { + // prereq types come first in updates, we + // also assume bases don't have hooks, otherwise + // they would need to wait on core + prereqs[update.Name()] = ts + } else { + // prereqs were processed already, wait for + // them as necessary for the other kind of + // snaps + waitPrereq(ts, defaultCoreSnapName) + if update.Base != "" { + waitPrereq(ts, update.Base) + } + } + scheduleUpdate(update.Name(), ts) tasksets = append(tasksets, ts) } @@ -802,6 +837,22 @@ return updated, tasksets, nil } +type byKind []*snap.Info + +func (bk byKind) Len() int { return len(bk) } +func (bk byKind) Swap(i, j int) { bk[i], bk[j] = bk[j], bk[i] } + +var kindRevOrder = map[snap.Type]int{ + snap.TypeOS: 2, + snap.TypeBase: 1, +} + +func (bk byKind) Less(i, j int) bool { + iRevOrd := kindRevOrder[bk[i].Type] + jRevOrd := kindRevOrder[bk[j].Type] + return iRevOrd >= jRevOrd +} + func applyAutoAliasesDelta(st *state.State, delta map[string][]string, op string, refreshAll bool, linkTs func(snapName string, ts *state.TaskSet)) (*state.TaskSet, error) { applyTs := state.NewTaskSet() kind := "refresh-aliases" @@ -1077,11 +1128,11 @@ } if sideInfo == nil { // refresh from given revision from store - return updateToRevisionInfo(st, snapst, channel, revision, userID) + return updateToRevisionInfo(st, snapst, revision, userID) } - // refresh-to-local - return readInfo(name, sideInfo) + // refresh-to-local, this assumes the snap revision is mounted + return readInfo(name, sideInfo, errorOnBroken) } // AutoRefreshAssertions allows to hook fetching of important assertions @@ -1514,12 +1565,6 @@ return nil, fmt.Errorf("cannot transition snap %q: not installed", oldName) } - var userID int - newInfo, err := snapInfo(st, newName, oldSnapst.Channel, snap.R(0), userID) - if err != nil { - return nil, err - } - var all []*state.TaskSet // install new core (if not already installed) err = Get(st, newName, &newSnapst) @@ -1527,6 +1572,12 @@ return nil, err } if !newSnapst.IsInstalled() { + var userID int + newInfo, err := installInfo(st, newName, oldSnapst.Channel, snap.R(0), userID) + if err != nil { + return nil, err + } + // start by instaling the new snap tsInst, err := doInstall(st, &newSnapst, &SnapSetup{ Channel: oldSnapst.Channel, @@ -1595,7 +1646,7 @@ for i := len(snapst.Sequence) - 1; i >= 0; i-- { if si := snapst.Sequence[i]; si.Revision == revision { - return readInfo(name, si) + return readInfo(name, si, 0) } } diff -Nru snapd-2.32.3.2/overlord/snapstate/snapstate_test.go snapd-2.32.9/overlord/snapstate/snapstate_test.go --- snapd-2.32.3.2/overlord/snapstate/snapstate_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/snapstate/snapstate_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -78,6 +78,8 @@ var _ = Suite(&snapmgrTestSuite{}) +var fakeRevDateEpoch = time.Date(2018, 1, 0, 0, 0, 0, 0, time.UTC) + func (s *snapmgrTestSuite) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) dirs.SetRootDir(c.MkDir()) @@ -114,8 +116,16 @@ s.o.AddManager(s.snapmgr) - s.BaseTest.AddCleanup(snapstate.MockReadInfo(s.fakeBackend.ReadInfo)) + s.BaseTest.AddCleanup(snapstate.MockSnapReadInfo(s.fakeBackend.ReadInfo)) s.BaseTest.AddCleanup(snapstate.MockOpenSnapFile(s.fakeBackend.OpenSnapFile)) + revDate := func(info *snap.Info) time.Time { + if info.Revision.Local() { + panic("no local revision should reach revisionDate") + } + // for convenience a date derived from the revision + return fakeRevDateEpoch.AddDate(0, 0, info.Revision.N) + } + s.BaseTest.AddCleanup(snapstate.MockRevisionDate(revDate)) s.BaseTest.AddCleanup(func() { snapstate.SetupInstallHook = oldSetupInstallHook @@ -657,6 +667,13 @@ c.Assert(err, ErrorMatches, `cannot update disabled snap "some-snap"`) } +func (s snapmgrTestSuite) TestInstallFailsOnSystem(c *C) { + snapsup := &snapstate.SnapSetup{SideInfo: &snap.SideInfo{RealName: "system", SnapID: "some-snap-id", Revision: snap.R(1)}} + _, err := snapstate.DoInstall(s.state, nil, snapsup, 0) + c.Assert(err, NotNil) + c.Assert(err, ErrorMatches, `cannot install reserved snap name 'system'`) +} + func (s *snapmgrTestSuite) TestUpdateCreatesDiscardAfterCurrentTasks(c *C) { s.state.Lock() defer s.state.Unlock() @@ -812,6 +829,85 @@ c.Check(updates, HasLen, 0) } +func (s *snapmgrTestSuite) TestByKindOrder(c *C) { + core := &snap.Info{Type: snap.TypeOS} + base := &snap.Info{Type: snap.TypeBase} + app := &snap.Info{Type: snap.TypeApp} + + c.Check(snapstate.ByKindOrder(base, core), DeepEquals, []*snap.Info{core, base}) + c.Check(snapstate.ByKindOrder(app, core), DeepEquals, []*snap.Info{core, app}) + c.Check(snapstate.ByKindOrder(app, base), DeepEquals, []*snap.Info{base, app}) + c.Check(snapstate.ByKindOrder(app, base, core), DeepEquals, []*snap.Info{core, base, app}) + c.Check(snapstate.ByKindOrder(app, core, base), DeepEquals, []*snap.Info{core, base, app}) +} + +func (s *snapmgrTestSuite) TestUpdateManyWaitForBases(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "core", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + {RealName: "core", SnapID: "core-snap-id", Revision: snap.R(1)}, + }, + Current: snap.R(1), + SnapType: "os", + }) + + snapstate.Set(s.state, "some-base", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + {RealName: "some-base", SnapID: "some-base-id", Revision: snap.R(1)}, + }, + Current: snap.R(1), + SnapType: "base", + }) + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}, + }, + Current: snap.R(1), + SnapType: "app", + Channel: "channel-for-base", + }) + + updates, tts, err := snapstate.UpdateMany(context.TODO(), s.state, []string{"some-snap", "core", "some-base"}, 0) + c.Assert(err, IsNil) + c.Assert(tts, HasLen, 3) + c.Check(updates, HasLen, 3) + + // to make TaskSnapSetup work + chg := s.state.NewChange("refresh", "...") + for _, ts := range tts { + chg.AddAll(ts) + } + + prereqTotal := len(tts[0].Tasks()) + len(tts[1].Tasks()) + prereqs := map[string]bool{} + for i, task := range tts[2].Tasks() { + waitTasks := task.WaitTasks() + if i == 0 { + c.Check(len(waitTasks), Equals, prereqTotal) + } else if task.Kind() == "link-snap" { + c.Check(len(waitTasks), Equals, prereqTotal+1) + for _, pre := range waitTasks { + if pre.Kind() == "link-snap" { + snapsup, err := snapstate.TaskSnapSetup(pre) + c.Assert(err, IsNil) + prereqs[snapsup.Name()] = true + } + } + } + } + + c.Check(prereqs, DeepEquals, map[string]bool{ + "core": true, + "some-base": true, + }) +} + func (s *snapmgrTestSuite) TestUpdateManyValidateRefreshes(c *C) { s.state.Lock() defer s.state.Unlock() @@ -1127,7 +1223,7 @@ state *state.State } -func (s sneakyStore) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) { +func (s sneakyStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) { s.state.Lock() snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ Active: true, @@ -1137,7 +1233,7 @@ SnapType: "app", }) s.state.Unlock() - return s.fakeStore.SnapInfo(spec, user) + return s.fakeStore.SnapAction(ctx, currentSnaps, actions, user, opts) } func (s *snapmgrTestSuite) TestInstallStateConflict(c *C) { @@ -1567,6 +1663,163 @@ defer s.state.Unlock() chg := s.state.NewChange("install", "install a snap") + ts, err := snapstate.Install(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{}) + c.Assert(err, IsNil) + chg.AddAll(ts) + + s.state.Unlock() + defer s.snapmgr.Stop() + s.settle(c) + s.state.Lock() + + // ensure all our tasks ran + c.Assert(chg.Err(), IsNil) + c.Assert(chg.IsReady(), Equals, true) + c.Check(snapstate.Installing(s.state), Equals, false) + c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{{ + macaroon: s.user.StoreMacaroon, + name: "some-snap", + }}) + expected := fakeOps{ + { + op: "storesvc-snap-action", + userID: 1, + }, + { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "install", + Name: "some-snap", + Channel: "some-channel", + }, + revno: snap.R(11), + userID: 1, + }, + { + op: "storesvc-download", + name: "some-snap", + }, + { + op: "validate-snap:Doing", + name: "some-snap", + revno: snap.R(11), + }, + { + op: "current", + old: "", + }, + { + op: "open-snap-file", + name: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"), + sinfo: snap.SideInfo{ + RealName: "some-snap", + SnapID: "some-snap-id", + Channel: "some-channel", + Revision: snap.R(11), + }, + }, + { + op: "setup-snap", + name: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"), + revno: snap.R(11), + }, + { + op: "copy-data", + name: filepath.Join(dirs.SnapMountDir, "some-snap/11"), + old: "", + }, + { + op: "setup-profiles:Doing", + name: "some-snap", + revno: snap.R(11), + }, + { + op: "candidate", + sinfo: snap.SideInfo{ + RealName: "some-snap", + SnapID: "some-snap-id", + Channel: "some-channel", + Revision: snap.R(11), + }, + }, + { + op: "link-snap", + name: filepath.Join(dirs.SnapMountDir, "some-snap/11"), + }, + { + op: "auto-connect:Doing", + name: "some-snap", + revno: snap.R(11), + }, + { + op: "update-aliases", + }, + { + op: "cleanup-trash", + name: "some-snap", + revno: snap.R(11), + }, + } + // start with an easier-to-read error if this fails: + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Assert(s.fakeBackend.ops, DeepEquals, expected) + + // check progress + ta := ts.Tasks() + task := ta[1] + _, cur, total := task.Progress() + c.Assert(cur, Equals, s.fakeStore.fakeCurrentProgress) + c.Assert(total, Equals, s.fakeStore.fakeTotalProgress) + c.Check(task.Summary(), Equals, `Download snap "some-snap" (11) from channel "some-channel"`) + + // check link/start snap summary + linkTask := ta[len(ta)-7] + c.Check(linkTask.Summary(), Equals, `Make snap "some-snap" (11) available to the system`) + startTask := ta[len(ta)-2] + c.Check(startTask.Summary(), Equals, `Start snap "some-snap" (11) services`) + + // verify snap-setup in the task state + var snapsup snapstate.SnapSetup + err = task.Get("snap-setup", &snapsup) + c.Assert(err, IsNil) + c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{ + Channel: "some-channel", + UserID: s.user.ID, + SnapPath: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"), + DownloadInfo: &snap.DownloadInfo{ + DownloadURL: "https://some-server.com/some/path.snap", + }, + SideInfo: snapsup.SideInfo, + }) + c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{ + RealName: "some-snap", + Channel: "some-channel", + Revision: snap.R(11), + SnapID: "some-snap-id", + }) + + // verify snaps in the system state + var snaps map[string]*snapstate.SnapState + err = s.state.Get("snaps", &snaps) + c.Assert(err, IsNil) + + snapst := snaps["some-snap"] + c.Assert(snapst.Active, Equals, true) + c.Assert(snapst.Channel, Equals, "some-channel") + c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{ + RealName: "some-snap", + SnapID: "some-snap-id", + Channel: "some-channel", + Revision: snap.R(11), + }) + c.Assert(snapst.Required, Equals, false) +} + +func (s *snapmgrTestSuite) TestInstallWithRevisionRunThrough(c *C) { + s.state.Lock() + defer s.state.Unlock() + + chg := s.state.NewChange("install", "install a snap") ts, err := snapstate.Install(s.state, "some-snap", "some-channel", snap.R(42), s.user.ID, snapstate.Flags{}) c.Assert(err, IsNil) chg.AddAll(ts) @@ -1586,8 +1839,16 @@ }}) expected := fakeOps{ { - op: "storesvc-snap", - name: "some-snap", + op: "storesvc-snap-action", + userID: 1, + }, + { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "install", + Name: "some-snap", + Revision: snap.R(42), + }, revno: snap.R(42), userID: 1, }, @@ -1609,7 +1870,6 @@ name: filepath.Join(dirs.SnapBlobDir, "some-snap_42.snap"), sinfo: snap.SideInfo{ RealName: "some-snap", - Channel: "some-channel", SnapID: "some-snap-id", Revision: snap.R(42), }, @@ -1633,7 +1893,6 @@ op: "candidate", sinfo: snap.SideInfo{ RealName: "some-snap", - Channel: "some-channel", SnapID: "some-snap-id", Revision: snap.R(42), }, @@ -1690,7 +1949,6 @@ c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{ RealName: "some-snap", Revision: snap.R(42), - Channel: "some-channel", SnapID: "some-snap-id", }) @@ -1704,7 +1962,6 @@ c.Assert(snapst.Channel, Equals, "some-channel") c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{ RealName: "some-snap", - Channel: "some-channel", SnapID: "some-snap-id", Revision: snap.R(42), }) @@ -1732,6 +1989,13 @@ Revision: snap.R(7), SnapID: "services-snap-id", } + snaptest.MockSnap(c, `name: services-snap`, &si) + fi, err := os.Stat(snap.MountFile("services-snap", si.Revision)) + c.Assert(err, IsNil) + refreshedDate := fi.ModTime() + // look at disk + r := snapstate.MockRevisionDate(nil) + defer r() s.state.Lock() defer s.state.Unlock() @@ -1741,6 +2005,7 @@ Sequence: []*snap.SideInfo{&si}, Current: si.Revision, SnapType: "app", + Channel: "stable", }) chg := s.state.NewChange("refresh", "refresh a snap") @@ -1755,11 +2020,23 @@ expected := fakeOps{ { - op: "storesvc-list-refresh", - cand: store.RefreshCandidate{ - Channel: "some-channel", - SnapID: "services-snap-id", - Revision: snap.R(7), + op: "storesvc-snap-action", + curSnaps: []store.CurrentSnap{{ + Name: "services-snap", + SnapID: "services-snap-id", + Revision: snap.R(7), + TrackingChannel: "stable", + RefreshedDate: refreshedDate, + }}, + userID: 1, + }, + { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "refresh", + SnapID: "services-snap-id", + Channel: "some-channel", + Flags: store.SnapActionEnforceValidation, }, revno: snap.R(11), userID: 1, @@ -1940,7 +2217,7 @@ for _, op := range s.fakeBackend.ops { switch op.op { - case "storesvc-list-refresh": + case "storesvc-snap-action": c.Check(op.userID, Equals, 1) case "storesvc-download": snapName := op.name @@ -1981,7 +2258,7 @@ for _, op := range s.fakeBackend.ops { switch op.op { - case "storesvc-snap": + case "storesvc-snap-action:action": c.Check(op.userID, Equals, 1) case "storesvc-download": snapName := op.name @@ -2052,11 +2329,19 @@ userID int } seen := make(map[snapIDuserID]bool) + ir := 0 di := 0 for _, op := range s.fakeBackend.ops { switch op.op { - case "storesvc-list-refresh": - snapID := op.cand.SnapID + case "storesvc-snap-action": + ir++ + c.Check(op.curSnaps, DeepEquals, []store.CurrentSnap{ + {Name: "core", SnapID: "core-snap-id", Revision: snap.R(1), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 1)}, + {Name: "services-snap", SnapID: "services-snap-id", Revision: snap.R(2), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 2)}, + {Name: "some-snap", SnapID: "some-snap-id", Revision: snap.R(5), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 5)}, + }) + case "storesvc-snap-action:action": + snapID := op.action.SnapID seen[snapIDuserID{snapID: snapID, userID: op.userID}] = true case "storesvc-download": snapName := op.name @@ -2067,13 +2352,11 @@ di++ } } + c.Check(ir, Equals, 3) // we check all snaps with each user c.Check(seen, DeepEquals, map[snapIDuserID]bool{ - {snapID: "core-snap-id", userID: 1}: true, + {snapID: "core-snap-id", userID: 0}: true, {snapID: "some-snap-id", userID: 1}: true, - {snapID: "services-snap-id", userID: 1}: true, - {snapID: "core-snap-id", userID: 2}: true, - {snapID: "some-snap-id", userID: 2}: true, {snapID: "services-snap-id", userID: 2}: true, }) } @@ -2137,11 +2420,19 @@ userID int } seen := make(map[snapIDuserID]bool) + ir := 0 di := 0 for _, op := range s.fakeBackend.ops { switch op.op { - case "storesvc-list-refresh": - snapID := op.cand.SnapID + case "storesvc-snap-action": + ir++ + c.Check(op.curSnaps, DeepEquals, []store.CurrentSnap{ + {Name: "core", SnapID: "core-snap-id", Revision: snap.R(1), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 1)}, + {Name: "services-snap", SnapID: "services-snap-id", Revision: snap.R(2), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 2)}, + {Name: "some-snap", SnapID: "some-snap-id", Revision: snap.R(5), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 5)}, + }) + case "storesvc-snap-action:action": + snapID := op.action.SnapID seen[snapIDuserID{snapID: snapID, userID: op.userID}] = true case "storesvc-download": snapName := op.name @@ -2152,13 +2443,11 @@ di++ } } + c.Check(ir, Equals, 2) // we check all snaps with each user c.Check(seen, DeepEquals, map[snapIDuserID]bool{ - {snapID: "core-snap-id", userID: 1}: true, - {snapID: "some-snap-id", userID: 1}: true, - {snapID: "services-snap-id", userID: 1}: true, {snapID: "core-snap-id", userID: 2}: true, - {snapID: "some-snap-id", userID: 2}: true, + {snapID: "some-snap-id", userID: 1}: true, {snapID: "services-snap-id", userID: 2}: true, }) @@ -2208,11 +2497,22 @@ expected := fakeOps{ { - op: "storesvc-list-refresh", - cand: store.RefreshCandidate{ - Channel: "some-channel", - SnapID: "some-snap-id", - Revision: snap.R(7), + op: "storesvc-snap-action", + curSnaps: []store.CurrentSnap{{ + Name: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(7), + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7), + }}, + userID: 1, + }, + { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "refresh", + SnapID: "some-snap-id", + Channel: "some-channel", + Flags: store.SnapActionEnforceValidation, }, revno: snap.R(11), userID: 1, @@ -2368,11 +2668,23 @@ expected := fakeOps{ { - op: "storesvc-list-refresh", - cand: store.RefreshCandidate{ - Channel: "some-channel", - SnapID: "some-snap-id", - Revision: snap.R(7), + op: "storesvc-snap-action", + curSnaps: []store.CurrentSnap{{ + Name: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(7), + TrackingChannel: "stable", + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7), + }}, + userID: 1, + }, + { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "refresh", + SnapID: "some-snap-id", + Channel: "some-channel", + Flags: store.SnapActionEnforceValidation, }, revno: snap.R(11), userID: 1, @@ -2523,6 +2835,41 @@ c.Assert(err, Equals, store.ErrNoUpdateAvailable) } +// A noResultsStore returns no results for install/refresh requests +type noResultsStore struct { + *fakeStore +} + +func (n noResultsStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) { + return nil, &store.SnapActionError{NoResults: true} +} + +func (s *snapmgrTestSuite) TestUpdateNoStoreResults(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.ReplaceStore(s.state, noResultsStore{fakeStore: s.fakeStore}) + + // this is an atypical case in which the store didn't return + // an error nor a result, we are defensive and return + // a reasonable error + si := snap.SideInfo{ + RealName: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(7), + } + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si}, + Channel: "channel-for-7", + Current: si.Revision, + }) + + _, err := snapstate.Update(s.state, "some-snap", "channel-for-7", snap.R(0), s.user.ID, snapstate.Flags{}) + c.Assert(err, Equals, store.ErrNoUpdateAvailable) +} + func (s *snapmgrTestSuite) TestUpdateSameRevisionSwitchesChannel(c *C) { si := snap.SideInfo{ RealName: "some-snap", @@ -2601,15 +2948,28 @@ s.state.Lock() expected := fakeOps{ - // we just expect the "storesvc-list-refresh" op, we + // we just expect the "storesvc-snap-action" ops, we // don't have a fakeOp for switchChannel because it has // not a backend method, it just manipulates the state { - op: "storesvc-list-refresh", - cand: store.RefreshCandidate{ - Channel: "channel-for-7", - SnapID: "some-snap-id", - Revision: snap.R(7), + op: "storesvc-snap-action", + curSnaps: []store.CurrentSnap{{ + Name: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(7), + TrackingChannel: "other-channel", + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7), + }}, + userID: 1, + }, + + { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "refresh", + SnapID: "some-snap-id", + Channel: "channel-for-7", + Flags: store.SnapActionEnforceValidation, }, userID: 1, }, @@ -2864,13 +3224,24 @@ c.Assert(err, IsNil) c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{ - op: "storesvc-list-refresh", - revno: snap.R(11), - cand: store.RefreshCandidate{ + op: "storesvc-snap-action", + curSnaps: []store.CurrentSnap{{ + Name: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7), - Channel: "stable", - IgnoreValidation: true, + IgnoreValidation: false, + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7), + }}, + userID: 1, + }) + c.Check(s.fakeBackend.ops[1], DeepEquals, fakeOp{ + op: "storesvc-snap-action:action", + revno: snap.R(11), + action: store.SnapAction{ + Action: "refresh", + SnapID: "some-snap-id", + Channel: "stable", + Flags: store.SnapActionIgnoreValidation, }, userID: 1, }) @@ -2899,13 +3270,24 @@ c.Check(tts, HasLen, 1) c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{ - op: "storesvc-list-refresh", - revno: snap.R(12), - cand: store.RefreshCandidate{ + op: "storesvc-snap-action", + curSnaps: []store.CurrentSnap{{ + Name: "some-snap", SnapID: "some-snap-id", Revision: snap.R(11), - Channel: "stable", + TrackingChannel: "stable", IgnoreValidation: true, + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 11), + }}, + userID: 1, + }) + c.Check(s.fakeBackend.ops[1], DeepEquals, fakeOp{ + op: "storesvc-snap-action:action", + revno: snap.R(12), + action: store.SnapAction{ + Action: "refresh", + SnapID: "some-snap-id", + Flags: 0, }, userID: 1, }) @@ -2939,13 +3321,25 @@ c.Assert(err, IsNil) c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{ - op: "storesvc-list-refresh", - revno: snap.R(11), - cand: store.RefreshCandidate{ + op: "storesvc-snap-action", + curSnaps: []store.CurrentSnap{{ + Name: "some-snap", SnapID: "some-snap-id", Revision: snap.R(12), - Channel: "stable", - IgnoreValidation: false, + TrackingChannel: "stable", + IgnoreValidation: true, + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 12), + }}, + userID: 1, + }) + c.Check(s.fakeBackend.ops[1], DeepEquals, fakeOp{ + op: "storesvc-snap-action:action", + revno: snap.R(11), + action: store.SnapAction{ + Action: "refresh", + SnapID: "some-snap-id", + Channel: "stable", + Flags: store.SnapActionEnforceValidation, }, userID: 1, }) @@ -3012,7 +3406,26 @@ err = tasks[1].Get("snap-setup", &snapsup) c.Assert(err, IsNil) c.Check(snapsup.Revision(), Equals, snap.R(7)) +} +func (s *snapmgrTestSuite) TestUpdateAmendSnapNotFound(c *C) { + si := snap.SideInfo{ + RealName: "snap-unknown", + Revision: snap.R("x1"), + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "snap-unknown", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si}, + Channel: "stable", + Current: si.Revision, + }) + + _, err := snapstate.Update(s.state, "snap-unknown", "stable", snap.R(0), s.user.ID, snapstate.Flags{Amend: true}) + c.Assert(err, Equals, store.ErrSnapNotFound) } func (s *snapmgrTestSuite) TestSingleUpdateBlockedRevision(c *C) { @@ -3041,18 +3454,17 @@ _, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{}) c.Assert(err, IsNil) - c.Assert(s.fakeBackend.ops, HasLen, 1) + c.Assert(s.fakeBackend.ops, HasLen, 2) c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{ - op: "storesvc-list-refresh", - revno: snap.R(11), - cand: store.RefreshCandidate{ - SnapID: "some-snap-id", - Revision: snap.R(7), - Channel: "some-channel", - }, + op: "storesvc-snap-action", + curSnaps: []store.CurrentSnap{{ + Name: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(7), + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7), + }}, userID: 1, }) - } func (s *snapmgrTestSuite) TestMultiUpdateBlockedRevision(c *C) { @@ -3082,17 +3494,17 @@ c.Assert(err, IsNil) c.Check(updates, DeepEquals, []string{"some-snap"}) - c.Assert(s.fakeBackend.ops, HasLen, 1) + c.Assert(s.fakeBackend.ops, HasLen, 2) c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{ - op: "storesvc-list-refresh", - revno: snap.R(11), - cand: store.RefreshCandidate{ - SnapID: "some-snap-id", - Revision: snap.R(7), - }, + op: "storesvc-snap-action", + curSnaps: []store.CurrentSnap{{ + Name: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(7), + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7), + }}, userID: 1, }) - } func (s *snapmgrTestSuite) TestAllUpdateBlockedRevision(c *C) { @@ -3121,17 +3533,18 @@ c.Check(err, IsNil) c.Check(updates, HasLen, 0) - c.Assert(s.fakeBackend.ops, HasLen, 1) + c.Assert(s.fakeBackend.ops, HasLen, 2) c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{ - op: "storesvc-list-refresh", - cand: store.RefreshCandidate{ - SnapID: "some-snap-id", - Revision: snap.R(7), - Block: []snap.Revision{snap.R(11)}, - }, + op: "storesvc-snap-action", + curSnaps: []store.CurrentSnap{{ + Name: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(7), + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7), + Block: []snap.Revision{snap.R(11)}, + }}, userID: 1, }) - } var orthogonalAutoAliasesScenarios = []struct { @@ -5351,8 +5764,16 @@ expected := fakeOps{ { - op: "storesvc-snap", - name: "some-snap", + op: "storesvc-snap-action", + userID: 1, + }, + { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "install", + Name: "some-snap", + Channel: "some-channel", + }, revno: snap.R(11), userID: 1, }, @@ -7138,7 +7559,7 @@ defer r() // using MockSnap, we want to read the bits on disk - snapstate.MockReadInfo(snap.ReadInfo) + snapstate.MockSnapReadInfo(snap.ReadInfo) s.state.Lock() defer s.state.Unlock() @@ -7230,7 +7651,7 @@ makeInstalledMockCoreSnap(c) // using MockSnap, we want to read the bits on disk - snapstate.MockReadInfo(snap.ReadInfo) + snapstate.MockSnapReadInfo(snap.ReadInfo) s.state.Lock() defer s.state.Unlock() @@ -7260,7 +7681,7 @@ makeInstalledMockCoreSnap(c) // using MockSnap, we want to read the bits on disk - snapstate.MockReadInfo(snap.ReadInfo) + snapstate.MockSnapReadInfo(snap.ReadInfo) s.state.Lock() defer s.state.Unlock() @@ -7283,7 +7704,7 @@ makeInstalledMockCoreSnap(c) // using MockSnap, we want to read the bits on disk - snapstate.MockReadInfo(snap.ReadInfo) + snapstate.MockSnapReadInfo(snap.ReadInfo) s.state.Lock() defer s.state.Unlock() @@ -7402,6 +7823,7 @@ Sequence: []*snap.SideInfo{{RealName: "ubuntu-core", SnapID: "ubuntu-core-snap-id", Revision: snap.R(1)}}, Current: snap.R(1), SnapType: "os", + Channel: "beta", }) chg := s.state.NewChange("transition-ubuntu-core", "...") @@ -7426,8 +7848,18 @@ }}) expected := fakeOps{ { - op: "storesvc-snap", - name: "core", + op: "storesvc-snap-action", + curSnaps: []store.CurrentSnap{ + {Name: "ubuntu-core", SnapID: "ubuntu-core-snap-id", Revision: snap.R(1), TrackingChannel: "beta", RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 1)}, + }, + }, + { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "install", + Name: "core", + Channel: "beta", + }, revno: snap.R(11), }, { @@ -7449,6 +7881,7 @@ sinfo: snap.SideInfo{ RealName: "core", SnapID: "core-id", + Channel: "beta", Revision: snap.R(11), }, }, @@ -7472,6 +7905,7 @@ sinfo: snap.SideInfo{ RealName: "core", SnapID: "core-id", + Channel: "beta", Revision: snap.R(11), }, }, @@ -7540,6 +7974,7 @@ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) c.Assert(s.fakeBackend.ops, DeepEquals, expected) } + func (s *snapmgrTestSuite) TestTransitionCoreRunThroughWithCore(c *C) { s.state.Lock() defer s.state.Unlock() @@ -7549,12 +7984,14 @@ Sequence: []*snap.SideInfo{{RealName: "ubuntu-core", SnapID: "ubuntu-core-snap-id", Revision: snap.R(1)}}, Current: snap.R(1), SnapType: "os", + Channel: "stable", }) snapstate.Set(s.state, "core", &snapstate.SnapState{ Active: true, Sequence: []*snap.SideInfo{{RealName: "core", SnapID: "core-snap-id", Revision: snap.R(1)}}, Current: snap.R(1), SnapType: "os", + Channel: "stable", }) chg := s.state.NewChange("transition-ubuntu-core", "...") @@ -7575,11 +8012,6 @@ c.Check(s.fakeStore.downloads, HasLen, 0) expected := fakeOps{ { - op: "storesvc-snap", - name: "core", - revno: snap.R(11), - }, - { op: "transition-ubuntu-core:Doing", name: "ubuntu-core", }, @@ -7621,7 +8053,6 @@ // start with an easier-to-read error if this fails: c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) c.Assert(s.fakeBackend.ops, DeepEquals, expected) - } func (s *snapmgrTestSuite) TestTransitionCoreStartsAutomatically(c *C) { @@ -8053,16 +8484,32 @@ expected := fakeOps{ // we check the snap { - op: "storesvc-snap", - name: "some-snap", + op: "storesvc-snap-action", + userID: 1, + }, + { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "install", + Name: "some-snap", + Revision: snap.R(42), + }, revno: snap.R(42), userID: 1, }, // then we check core because its not installed already // and continue with that { - op: "storesvc-snap", - name: "core", + op: "storesvc-snap-action", + userID: 1, + }, + { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "install", + Name: "core", + Channel: "stable", + }, revno: snap.R(11), userID: 1, }, @@ -8144,7 +8591,6 @@ name: filepath.Join(dirs.SnapBlobDir, "some-snap_42.snap"), sinfo: snap.SideInfo{ RealName: "some-snap", - Channel: "some-channel", SnapID: "some-snap-id", Revision: snap.R(42), }, @@ -8168,7 +8614,6 @@ op: "candidate", sinfo: snap.SideInfo{ RealName: "some-snap", - Channel: "some-channel", SnapID: "some-snap-id", Revision: snap.R(42), }, @@ -8320,7 +8765,6 @@ // ensure we have both core and snap2 var snapst snapstate.SnapState - err = snapstate.Get(s.state, "core", &snapst) c.Assert(err, IsNil) c.Assert(snapst.Active, Equals, true) @@ -8332,14 +8776,15 @@ Revision: snap.R(11), }) - err = snapstate.Get(s.state, "snap2", &snapst) + var snapst2 snapstate.SnapState + err = snapstate.Get(s.state, "snap2", &snapst2) c.Assert(err, IsNil) - c.Assert(snapst.Active, Equals, true) - c.Assert(snapst.Sequence, HasLen, 1) - c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{ + c.Assert(snapst2.Active, Equals, true) + c.Assert(snapst2.Sequence, HasLen, 1) + c.Assert(snapst2.Sequence[0], DeepEquals, &snap.SideInfo{ RealName: "snap2", SnapID: "snap2-id", - Channel: "some-other-channel", + Channel: "", Revision: snap.R(21), }) @@ -8355,8 +8800,8 @@ chg *state.Change } -func (s behindYourBackStore) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) { - if spec.Name == "core" { +func (s behindYourBackStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) { + if len(actions) == 1 && actions[0].Action == "install" && actions[0].Name == "core" { s.state.Lock() if !s.coreInstallRequested { s.coreInstallRequested = true @@ -8390,7 +8835,7 @@ s.state.Unlock() } - return s.fakeStore.SnapInfo(spec, user) + return s.fakeStore.SnapAction(ctx, currentSnaps, actions, user, opts) } // this test the scenario that some-snap gets installed and during the @@ -8412,7 +8857,7 @@ // now install a snap that will pull in core chg := s.state.NewChange("install", "install a snap on a system without core") - ts, err := snapstate.Install(s.state, "some-snap", "some-channel", snap.R(42), s.user.ID, snapstate.Flags{}) + ts, err := snapstate.Install(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{}) c.Assert(err, IsNil) chg.AddAll(ts) @@ -8469,7 +8914,7 @@ RealName: "some-snap", SnapID: "some-snap-id", Channel: "some-channel", - Revision: snap.R(42), + Revision: snap.R(11), }) } @@ -8478,9 +8923,13 @@ state *state.State } -func (s contentStore) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) { - info, err := s.fakeStore.SnapInfo(spec, user) - switch spec.Name { +func (s contentStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) { + snaps, err := s.fakeStore.SnapAction(ctx, currentSnaps, actions, user, opts) + if len(snaps) != 1 { + panic("expected to be queried for install of only one snap at a time") + } + info := snaps[0] + switch info.Name() { case "snap-content-plug": info.Plugs = map[string]*snap.PlugInfo{ "some-plug": { @@ -8562,7 +9011,7 @@ } } - return info, err + return []*snap.Info{info}, err } func (s *snapmgrTestSuite) TestInstallDefaultProviderRunThrough(c *C) { @@ -8575,7 +9024,7 @@ ifacerepo.Replace(s.state, repo) chg := s.state.NewChange("install", "install a snap") - ts, err := snapstate.Install(s.state, "snap-content-plug", "some-channel", snap.R(42), s.user.ID, snapstate.Flags{}) + ts, err := snapstate.Install(s.state, "snap-content-plug", "stable", snap.R(42), s.user.ID, snapstate.Flags{}) c.Assert(err, IsNil) chg.AddAll(ts) @@ -8588,13 +9037,27 @@ c.Assert(chg.Err(), IsNil) c.Assert(chg.IsReady(), Equals, true) expected := fakeOps{{ - op: "storesvc-snap", - name: "snap-content-plug", + op: "storesvc-snap-action", + userID: 1, + }, { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "install", + Name: "snap-content-plug", + Revision: snap.R(42), + }, revno: snap.R(42), userID: 1, }, { - op: "storesvc-snap", - name: "snap-content-slot", + op: "storesvc-snap-action", + userID: 1, + }, { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "install", + Name: "snap-content-slot", + Channel: "stable", + }, revno: snap.R(11), userID: 1, }, { @@ -8660,7 +9123,6 @@ name: filepath.Join(dirs.SnapBlobDir, "snap-content-plug_42.snap"), sinfo: snap.SideInfo{ RealName: "snap-content-plug", - Channel: "some-channel", SnapID: "snap-content-plug-id", Revision: snap.R(42), }, @@ -8680,7 +9142,6 @@ op: "candidate", sinfo: snap.SideInfo{ RealName: "snap-content-plug", - Channel: "some-channel", SnapID: "snap-content-plug-id", Revision: snap.R(42), }, @@ -8707,7 +9168,7 @@ // do a simple c.Check(ops, DeepEquals, fakeOps{...}) c.Check(len(s.fakeBackend.ops), Equals, len(expected)) for _, op := range expected { - c.Check(s.fakeBackend.ops, testutil.DeepContains, op) + c.Assert(s.fakeBackend.ops, testutil.DeepContains, op) } } diff -Nru snapd-2.32.3.2/overlord/snapstate/storehelpers.go snapd-2.32.9/overlord/snapstate/storehelpers.go --- snapd-2.32.3.2/overlord/snapstate/storehelpers.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/overlord/snapstate/storehelpers.go 2018-05-16 08:20:08.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2017 Canonical Ltd + * Copyright (C) 2016-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -25,6 +25,7 @@ "golang.org/x/net/context" + "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" @@ -90,29 +91,39 @@ return fallbackUser, nil } -func snapNameToID(st *state.State, name string, user *auth.UserState) (string, error) { - theStore := Store(st) - st.Unlock() - info, err := theStore.SnapInfo(store.SnapSpec{Name: name}, user) - st.Lock() - return info.SnapID, err -} +func installInfo(st *state.State, name, channel string, revision snap.Revision, userID int) (*snap.Info, error) { + // TODO: support ignore-validation? + + curSnaps, err := currentSnaps(st) + if err != nil { + return nil, err + } -func snapInfo(st *state.State, name, channel string, revision snap.Revision, userID int) (*snap.Info, error) { user, err := userFromUserID(st, userID) if err != nil { return nil, err } - theStore := Store(st) - st.Unlock() // calls to the store should be done without holding the state lock - spec := store.SnapSpec{ - Name: name, - Channel: channel, + + // cannot specify both with the API + if !revision.Unset() { + channel = "" + } + + action := &store.SnapAction{ + Action: "install", + Name: name, + // the desired channel + Channel: channel, + // the desired revision Revision: revision, } - snap, err := theStore.SnapInfo(spec, user) + + theStore := Store(st) + st.Unlock() // calls to the store should be done without holding the state lock + res, err := theStore.SnapAction(context.TODO(), curSnaps, []*store.SnapAction{action}, user, nil) st.Lock() - return snap, err + + return singleActionResult(name, action.Action, res, err) } func updateInfo(st *state.State, snapst *SnapState, opts *updateInfoOpts, userID int) (*snap.Info, error) { @@ -120,26 +131,42 @@ opts = &updateInfoOpts{} } + curSnaps, err := currentSnaps(st) + if err != nil { + return nil, err + } + curInfo, user, err := preUpdateInfo(st, snapst, opts.amend, userID) if err != nil { return nil, err } - refreshCand := &store.RefreshCandidate{ + var flags store.SnapActionFlags + if opts.ignoreValidation { + flags = store.SnapActionIgnoreValidation + } else { + flags = store.SnapActionEnforceValidation + } + + action := &store.SnapAction{ + Action: "refresh", + SnapID: curInfo.SnapID, // the desired channel - Channel: opts.channel, - SnapID: curInfo.SnapID, - Revision: curInfo.Revision, - Epoch: curInfo.Epoch, - IgnoreValidation: opts.ignoreValidation, - Amend: opts.amend, + Channel: opts.channel, + Flags: flags, + } + + if curInfo.SnapID == "" { // amend + action.Action = "install" + action.Name = curInfo.Name() } theStore := Store(st) st.Unlock() // calls to the store should be done without holding the state lock - res, err := theStore.LookupRefresh(refreshCand, user) + res, err := theStore.SnapAction(context.TODO(), curSnaps, []*store.SnapAction{action}, user, nil) st.Lock() - return res, err + + return singleActionResult(curInfo.Name(), action.Action, res, err) } func preUpdateInfo(st *state.State, snapst *SnapState, amend bool, userID int) (*snap.Info, *auth.UserState, error) { @@ -157,39 +184,130 @@ if !amend { return nil, nil, store.ErrLocalSnap } + } - // in amend mode we need to move to the store rev - id, err := snapNameToID(st, curInfo.Name(), user) - if err != nil { - return nil, nil, fmt.Errorf("cannot get snap ID for %q: %v", curInfo.Name(), err) + return curInfo, user, nil +} + +func singleActionResult(name, action string, results []*snap.Info, e error) (info *snap.Info, err error) { + if len(results) > 1 { + return nil, fmt.Errorf("internal error: multiple store results for a single snap op") + } + if len(results) > 0 { + // TODO: if we also have an error log/warn about it + return results[0], nil + } + + if saErr, ok := e.(*store.SnapActionError); ok { + if len(saErr.Other) != 0 { + return nil, saErr + } + + var snapErr error + switch action { + case "refresh": + snapErr = saErr.Refresh[name] + case "install": + snapErr = saErr.Install[name] + } + if snapErr != nil { + return nil, snapErr + } + + // no result, atypical case + if saErr.NoResults { + switch action { + case "refresh": + return nil, store.ErrNoUpdateAvailable + case "install": + return nil, store.ErrSnapNotFound + } } - curInfo.SnapID = id - // set revision to "unknown" - curInfo.Revision = snap.R(0) } - return curInfo, user, nil + return nil, e } -func updateToRevisionInfo(st *state.State, snapst *SnapState, channel string, revision snap.Revision, userID int) (*snap.Info, error) { +func updateToRevisionInfo(st *state.State, snapst *SnapState, revision snap.Revision, userID int) (*snap.Info, error) { + // TODO: support ignore-validation? + + curSnaps, err := currentSnaps(st) + if err != nil { + return nil, err + } + curInfo, user, err := preUpdateInfo(st, snapst, false, userID) if err != nil { return nil, err } - theStore := Store(st) - st.Unlock() // calls to the store should be done without holding the state lock - spec := store.SnapSpec{ - Name: curInfo.Name(), - Channel: channel, + action := &store.SnapAction{ + Action: "refresh", + SnapID: curInfo.SnapID, + // the desired revision Revision: revision, } - snap, err := theStore.SnapInfo(spec, user) + + theStore := Store(st) + st.Unlock() // calls to the store should be done without holding the state lock + res, err := theStore.SnapAction(context.TODO(), curSnaps, []*store.SnapAction{action}, user, nil) st.Lock() - return snap, err + + return singleActionResult(curInfo.Name(), action.Action, res, err) +} + +func currentSnaps(st *state.State) ([]*store.CurrentSnap, error) { + snapStates, err := All(st) + if err != nil { + return nil, err + } + + curSnaps := collectCurrentSnaps(snapStates, nil) + return curSnaps, nil +} + +func collectCurrentSnaps(snapStates map[string]*SnapState, consider func(*store.CurrentSnap, *SnapState)) (curSnaps []*store.CurrentSnap) { + curSnaps = make([]*store.CurrentSnap, 0, len(snapStates)) + + for snapName, snapst := range snapStates { + if snapst.TryMode { + // try mode snaps are completely local and + // irrelevant for the operation + continue + } + + snapInfo, err := snapst.CurrentInfo() + if err != nil { + continue + } + + if snapInfo.SnapID == "" { + // the store won't be able to tell what this + // is and so cannot include it in the + // operation + continue + } + + installed := &store.CurrentSnap{ + Name: snapName, + SnapID: snapInfo.SnapID, + // the desired channel (not snapInfo.Channel!) + TrackingChannel: snapst.Channel, + Revision: snapInfo.Revision, + RefreshedDate: revisionDate(snapInfo), + IgnoreValidation: snapst.IgnoreValidation, + } + curSnaps = append(curSnaps, installed) + + if consider != nil { + consider(installed, snapst) + } + } + + return curSnaps } -func refreshCandidates(ctx context.Context, st *state.State, names []string, user *auth.UserState, flags *store.RefreshOptions) ([]*snap.Info, map[string]*SnapState, map[string]bool, error) { +func refreshCandidates(ctx context.Context, st *state.State, names []string, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, map[string]*SnapState, map[string]bool, error) { snapStates, err := All(st) if err != nil { return nil, nil, nil, err @@ -204,93 +322,69 @@ sort.Strings(names) + actionsByUserID := make(map[int][]*store.SnapAction) stateByID := make(map[string]*SnapState, len(snapStates)) - candidatesInfo := make([]*store.RefreshCandidate, 0, len(snapStates)) ignoreValidation := make(map[string]bool) - userIDs := make(map[int]bool) - for _, snapst := range snapStates { - if len(names) == 0 && (snapst.TryMode || snapst.DevMode) { - // no auto-refresh for trymode nor devmode - continue - } + fallbackID := idForUser(user) + nCands := 0 + addCand := func(installed *store.CurrentSnap, snapst *SnapState) { // FIXME: snaps that are not active are skipped for now // until we know what we want to do if !snapst.Active { - continue - } - - snapInfo, err := snapst.CurrentInfo() - if err != nil { - // log something maybe? - continue + return } - if snapInfo.SnapID == "" { - // no refresh for sideloaded - continue + if len(names) == 0 && snapst.DevMode { + // no auto-refresh for devmode + return } - if len(names) > 0 && !strutil.SortedListContains(names, snapInfo.Name()) { - continue + if len(names) > 0 && !strutil.SortedListContains(names, installed.Name) { + return } - stateByID[snapInfo.SnapID] = snapst - - // get confinement preference from the snapstate - candidateInfo := &store.RefreshCandidate{ - // the desired channel (not info.Channel!) - Channel: snapst.Channel, - SnapID: snapInfo.SnapID, - Revision: snapInfo.Revision, - Epoch: snapInfo.Epoch, - IgnoreValidation: snapst.IgnoreValidation, - } + stateByID[installed.SnapID] = snapst if len(names) == 0 { - candidateInfo.Block = snapst.Block() + installed.Block = snapst.Block() } - candidatesInfo = append(candidatesInfo, candidateInfo) - if snapst.UserID != 0 { - userIDs[snapst.UserID] = true + userID := snapst.UserID + if userID == 0 { + userID = fallbackID } + actionsByUserID[userID] = append(actionsByUserID[userID], &store.SnapAction{ + Action: "refresh", + SnapID: installed.SnapID, + }) if snapst.IgnoreValidation { - ignoreValidation[snapInfo.SnapID] = true + ignoreValidation[installed.SnapID] = true } + nCands++ } + // determine current snaps and collect candidates for refresh + curSnaps := collectCurrentSnaps(snapStates, addCand) theStore := Store(st) - // TODO: we query for all snaps for each user so that the - // store can take into account validation constraints, we can - // do better with coming APIs - updatesInfo := make(map[string]*snap.Info, len(candidatesInfo)) - fallbackUsed := false - fallbackID := idForUser(user) - if len(userIDs) == 0 { - // none of the snaps had an installed user set, just - // use the fallbackID - userIDs[fallbackID] = true - } - for userID := range userIDs { + updatesInfo := make(map[string]*snap.Info, nCands) + for userID, actions := range actionsByUserID { u, err := userFromUserIDOrFallback(st, userID, user) if err != nil { return nil, nil, nil, err } - // consider the fallback user at most once - if idForUser(u) == fallbackID { - if fallbackUsed { - continue - } - fallbackUsed = true - } st.Unlock() - updatesForUser, err := theStore.ListRefresh(ctx, candidatesInfo, u, flags) + updatesForUser, err := theStore.SnapAction(ctx, curSnaps, actions, u, opts) st.Lock() if err != nil { - return nil, nil, nil, err + saErr, ok := err.(*store.SnapActionError) + if !ok { + return nil, nil, nil, err + } + // TODO: use the warning infra here when we have it + logger.Noticef("%v", saErr) } for _, snapInfo := range updatesForUser { diff -Nru snapd-2.32.3.2/packaging/arch/PKGBUILD snapd-2.32.9/packaging/arch/PKGBUILD --- snapd-2.32.3.2/packaging/arch/PKGBUILD 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/packaging/arch/PKGBUILD 2018-05-16 08:20:08.000000000 +0000 @@ -5,7 +5,7 @@ pkgbase=snapd pkgname=snapd-git -pkgver=2.32.3.2 +pkgver=2.32.9 pkgrel=1 arch=('i686' 'x86_64') url="https://github.com/snapcore/snapd" diff -Nru snapd-2.32.3.2/packaging/fedora/snapd.spec snapd-2.32.9/packaging/fedora/snapd.spec --- snapd-2.32.3.2/packaging/fedora/snapd.spec 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/packaging/fedora/snapd.spec 2018-05-16 08:20:08.000000000 +0000 @@ -50,7 +50,7 @@ %global provider_prefix %{provider}.%{provider_tld}/%{project}/%{repo} %global import_path %{provider_prefix} -%global snappy_svcs snapd.service snapd.socket snapd.autoimport.service +%global snappy_svcs snapd.service snapd.socket snapd.autoimport.service snapd.seeded.service # Until we have a way to add more extldflags to gobuild macro... %if 0%{?fedora} >= 26 @@ -70,7 +70,7 @@ %endif Name: snapd -Version: 2.32.3.2 +Version: 2.32.9 Release: 0%{?dist} Summary: A transactional software package manager Group: System Environment/Base @@ -604,8 +604,10 @@ %{_unitdir}/snapd.socket %{_unitdir}/snapd.service %{_unitdir}/snapd.autoimport.service +%{_unitdir}/snapd.seeded.service %{_datadir}/dbus-1/services/io.snapcraft.Launcher.service %{_datadir}/polkit-1/actions/io.snapcraft.snapd.policy +%{_sysconfdir}/xdg/autostart/snap-userd-autostart.desktop %config(noreplace) %{_sysconfdir}/sysconfig/snapd %dir %{_sharedstatedir}/snapd %dir %{_sharedstatedir}/snapd/assertions @@ -710,6 +712,52 @@ %changelog +* Tue May 16 2018 Michael Vogt +- New upstream release 2.32.9 + - tests: run all spread tests inside GCE + - tests: build spread in the autopkgtests with a more recent go + +* Fri May 11 2018 Michael Vogt +- New upstream release 2.32.8 + - snapd.core-fixup.sh: add workaround for corrupted uboot.env + +* Fri May 11 2018 Michael Vogt +- New upstream release 2.32.7 + - many: add wait command and seeded target (2 + - snapd.core-fixup.sh: add workaround for corrupted uboot.env + - boot: clear "snap_mode" when needed + - cmd/libsnap: fix compile error on more restrictive gcc + - tests: cherry-pick commits to move spread to google backend + - spread.yaml: add cosmic (18.10) to autopkgtest/qemu + - userd: set up journal logging streams for autostarted apps + +* Sun Apr 29 2018 Michael Vogt +- New upstream release 2.32.6 + - snap: do not use overly short timeout in `snap + {start,stop,restart}` + - interfaces/apparmor: fix incorrect apparmor profile glob + - tests: detect kernel oops during tests and abort tests in this + case + - tests: run interfaces-boradcom-asic-control early + - tests: skip interfaces-content test on core devices + +* Mon Apr 16 2018 Michael Vogt +- New upstream release 2.32.5 + - many: add "stop-mode: sig{term,hup,usr[12]}{,-all}" instead of + conflating that with refresh-mode + - overlord/snapstate: poll for up to 10s if a snap is unexpectedly + not mounted in doMountSnap + - daemon: support 'system' as nickname of the core snap + +* Wed Apr 11 2018 Michael Vogt +- New upstream release 2.32.4 + - cmd/snap: user session application autostart + - overlord/snapstate: introduce envvars to control the channels for + bases and prereqs + - overlord/snapstate: on multi-snap refresh make sure bases and core + are finished before dependent snaps + - many: use the new install/refresh /v2/snaps/refresh store API + * Wed Apr 11 2018 Michael Vogt - New upstream release 2.32.3.2 - errtracker: make TestJournalErrorSilentError work on diff -Nru snapd-2.32.3.2/packaging/fedora-25/snapd.spec snapd-2.32.9/packaging/fedora-25/snapd.spec --- snapd-2.32.3.2/packaging/fedora-25/snapd.spec 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/packaging/fedora-25/snapd.spec 2018-05-16 08:20:08.000000000 +0000 @@ -50,7 +50,7 @@ %global provider_prefix %{provider}.%{provider_tld}/%{project}/%{repo} %global import_path %{provider_prefix} -%global snappy_svcs snapd.service snapd.socket snapd.autoimport.service +%global snappy_svcs snapd.service snapd.socket snapd.autoimport.service snapd.seeded.service # Until we have a way to add more extldflags to gobuild macro... %if 0%{?fedora} >= 26 @@ -70,7 +70,7 @@ %endif Name: snapd -Version: 2.32.3.2 +Version: 2.32.9 Release: 0%{?dist} Summary: A transactional software package manager Group: System Environment/Base @@ -604,8 +604,10 @@ %{_unitdir}/snapd.socket %{_unitdir}/snapd.service %{_unitdir}/snapd.autoimport.service +%{_unitdir}/snapd.seeded.service %{_datadir}/dbus-1/services/io.snapcraft.Launcher.service %{_datadir}/polkit-1/actions/io.snapcraft.snapd.policy +%{_sysconfdir}/xdg/autostart/snap-userd-autostart.desktop %config(noreplace) %{_sysconfdir}/sysconfig/snapd %dir %{_sharedstatedir}/snapd %dir %{_sharedstatedir}/snapd/assertions @@ -710,6 +712,52 @@ %changelog +* Tue May 16 2018 Michael Vogt +- New upstream release 2.32.9 + - tests: run all spread tests inside GCE + - tests: build spread in the autopkgtests with a more recent go + +* Fri May 11 2018 Michael Vogt +- New upstream release 2.32.8 + - snapd.core-fixup.sh: add workaround for corrupted uboot.env + +* Fri May 11 2018 Michael Vogt +- New upstream release 2.32.7 + - many: add wait command and seeded target (2 + - snapd.core-fixup.sh: add workaround for corrupted uboot.env + - boot: clear "snap_mode" when needed + - cmd/libsnap: fix compile error on more restrictive gcc + - tests: cherry-pick commits to move spread to google backend + - spread.yaml: add cosmic (18.10) to autopkgtest/qemu + - userd: set up journal logging streams for autostarted apps + +* Sun Apr 29 2018 Michael Vogt +- New upstream release 2.32.6 + - snap: do not use overly short timeout in `snap + {start,stop,restart}` + - interfaces/apparmor: fix incorrect apparmor profile glob + - tests: detect kernel oops during tests and abort tests in this + case + - tests: run interfaces-boradcom-asic-control early + - tests: skip interfaces-content test on core devices + +* Mon Apr 16 2018 Michael Vogt +- New upstream release 2.32.5 + - many: add "stop-mode: sig{term,hup,usr[12]}{,-all}" instead of + conflating that with refresh-mode + - overlord/snapstate: poll for up to 10s if a snap is unexpectedly + not mounted in doMountSnap + - daemon: support 'system' as nickname of the core snap + +* Wed Apr 11 2018 Michael Vogt +- New upstream release 2.32.4 + - cmd/snap: user session application autostart + - overlord/snapstate: introduce envvars to control the channels for + bases and prereqs + - overlord/snapstate: on multi-snap refresh make sure bases and core + are finished before dependent snaps + - many: use the new install/refresh /v2/snaps/refresh store API + * Wed Apr 11 2018 Michael Vogt - New upstream release 2.32.3.2 - errtracker: make TestJournalErrorSilentError work on diff -Nru snapd-2.32.3.2/packaging/fedora-26/snapd.spec snapd-2.32.9/packaging/fedora-26/snapd.spec --- snapd-2.32.3.2/packaging/fedora-26/snapd.spec 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/packaging/fedora-26/snapd.spec 2018-05-16 08:20:08.000000000 +0000 @@ -50,7 +50,7 @@ %global provider_prefix %{provider}.%{provider_tld}/%{project}/%{repo} %global import_path %{provider_prefix} -%global snappy_svcs snapd.service snapd.socket snapd.autoimport.service +%global snappy_svcs snapd.service snapd.socket snapd.autoimport.service snapd.seeded.service # Until we have a way to add more extldflags to gobuild macro... %if 0%{?fedora} >= 26 @@ -70,7 +70,7 @@ %endif Name: snapd -Version: 2.32.3.2 +Version: 2.32.9 Release: 0%{?dist} Summary: A transactional software package manager Group: System Environment/Base @@ -604,8 +604,10 @@ %{_unitdir}/snapd.socket %{_unitdir}/snapd.service %{_unitdir}/snapd.autoimport.service +%{_unitdir}/snapd.seeded.service %{_datadir}/dbus-1/services/io.snapcraft.Launcher.service %{_datadir}/polkit-1/actions/io.snapcraft.snapd.policy +%{_sysconfdir}/xdg/autostart/snap-userd-autostart.desktop %config(noreplace) %{_sysconfdir}/sysconfig/snapd %dir %{_sharedstatedir}/snapd %dir %{_sharedstatedir}/snapd/assertions @@ -710,6 +712,52 @@ %changelog +* Tue May 16 2018 Michael Vogt +- New upstream release 2.32.9 + - tests: run all spread tests inside GCE + - tests: build spread in the autopkgtests with a more recent go + +* Fri May 11 2018 Michael Vogt +- New upstream release 2.32.8 + - snapd.core-fixup.sh: add workaround for corrupted uboot.env + +* Fri May 11 2018 Michael Vogt +- New upstream release 2.32.7 + - many: add wait command and seeded target (2 + - snapd.core-fixup.sh: add workaround for corrupted uboot.env + - boot: clear "snap_mode" when needed + - cmd/libsnap: fix compile error on more restrictive gcc + - tests: cherry-pick commits to move spread to google backend + - spread.yaml: add cosmic (18.10) to autopkgtest/qemu + - userd: set up journal logging streams for autostarted apps + +* Sun Apr 29 2018 Michael Vogt +- New upstream release 2.32.6 + - snap: do not use overly short timeout in `snap + {start,stop,restart}` + - interfaces/apparmor: fix incorrect apparmor profile glob + - tests: detect kernel oops during tests and abort tests in this + case + - tests: run interfaces-boradcom-asic-control early + - tests: skip interfaces-content test on core devices + +* Mon Apr 16 2018 Michael Vogt +- New upstream release 2.32.5 + - many: add "stop-mode: sig{term,hup,usr[12]}{,-all}" instead of + conflating that with refresh-mode + - overlord/snapstate: poll for up to 10s if a snap is unexpectedly + not mounted in doMountSnap + - daemon: support 'system' as nickname of the core snap + +* Wed Apr 11 2018 Michael Vogt +- New upstream release 2.32.4 + - cmd/snap: user session application autostart + - overlord/snapstate: introduce envvars to control the channels for + bases and prereqs + - overlord/snapstate: on multi-snap refresh make sure bases and core + are finished before dependent snaps + - many: use the new install/refresh /v2/snaps/refresh store API + * Wed Apr 11 2018 Michael Vogt - New upstream release 2.32.3.2 - errtracker: make TestJournalErrorSilentError work on diff -Nru snapd-2.32.3.2/packaging/fedora-27/snapd.spec snapd-2.32.9/packaging/fedora-27/snapd.spec --- snapd-2.32.3.2/packaging/fedora-27/snapd.spec 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/packaging/fedora-27/snapd.spec 2018-05-16 08:20:08.000000000 +0000 @@ -50,7 +50,7 @@ %global provider_prefix %{provider}.%{provider_tld}/%{project}/%{repo} %global import_path %{provider_prefix} -%global snappy_svcs snapd.service snapd.socket snapd.autoimport.service +%global snappy_svcs snapd.service snapd.socket snapd.autoimport.service snapd.seeded.service # Until we have a way to add more extldflags to gobuild macro... %if 0%{?fedora} >= 26 @@ -70,7 +70,7 @@ %endif Name: snapd -Version: 2.32.3.2 +Version: 2.32.9 Release: 0%{?dist} Summary: A transactional software package manager Group: System Environment/Base @@ -604,8 +604,10 @@ %{_unitdir}/snapd.socket %{_unitdir}/snapd.service %{_unitdir}/snapd.autoimport.service +%{_unitdir}/snapd.seeded.service %{_datadir}/dbus-1/services/io.snapcraft.Launcher.service %{_datadir}/polkit-1/actions/io.snapcraft.snapd.policy +%{_sysconfdir}/xdg/autostart/snap-userd-autostart.desktop %config(noreplace) %{_sysconfdir}/sysconfig/snapd %dir %{_sharedstatedir}/snapd %dir %{_sharedstatedir}/snapd/assertions @@ -710,6 +712,52 @@ %changelog +* Tue May 16 2018 Michael Vogt +- New upstream release 2.32.9 + - tests: run all spread tests inside GCE + - tests: build spread in the autopkgtests with a more recent go + +* Fri May 11 2018 Michael Vogt +- New upstream release 2.32.8 + - snapd.core-fixup.sh: add workaround for corrupted uboot.env + +* Fri May 11 2018 Michael Vogt +- New upstream release 2.32.7 + - many: add wait command and seeded target (2 + - snapd.core-fixup.sh: add workaround for corrupted uboot.env + - boot: clear "snap_mode" when needed + - cmd/libsnap: fix compile error on more restrictive gcc + - tests: cherry-pick commits to move spread to google backend + - spread.yaml: add cosmic (18.10) to autopkgtest/qemu + - userd: set up journal logging streams for autostarted apps + +* Sun Apr 29 2018 Michael Vogt +- New upstream release 2.32.6 + - snap: do not use overly short timeout in `snap + {start,stop,restart}` + - interfaces/apparmor: fix incorrect apparmor profile glob + - tests: detect kernel oops during tests and abort tests in this + case + - tests: run interfaces-boradcom-asic-control early + - tests: skip interfaces-content test on core devices + +* Mon Apr 16 2018 Michael Vogt +- New upstream release 2.32.5 + - many: add "stop-mode: sig{term,hup,usr[12]}{,-all}" instead of + conflating that with refresh-mode + - overlord/snapstate: poll for up to 10s if a snap is unexpectedly + not mounted in doMountSnap + - daemon: support 'system' as nickname of the core snap + +* Wed Apr 11 2018 Michael Vogt +- New upstream release 2.32.4 + - cmd/snap: user session application autostart + - overlord/snapstate: introduce envvars to control the channels for + bases and prereqs + - overlord/snapstate: on multi-snap refresh make sure bases and core + are finished before dependent snaps + - many: use the new install/refresh /v2/snaps/refresh store API + * Wed Apr 11 2018 Michael Vogt - New upstream release 2.32.3.2 - errtracker: make TestJournalErrorSilentError work on diff -Nru snapd-2.32.3.2/packaging/fedora-rawhide/snapd.spec snapd-2.32.9/packaging/fedora-rawhide/snapd.spec --- snapd-2.32.3.2/packaging/fedora-rawhide/snapd.spec 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/packaging/fedora-rawhide/snapd.spec 2018-05-16 08:20:08.000000000 +0000 @@ -50,7 +50,7 @@ %global provider_prefix %{provider}.%{provider_tld}/%{project}/%{repo} %global import_path %{provider_prefix} -%global snappy_svcs snapd.service snapd.socket snapd.autoimport.service +%global snappy_svcs snapd.service snapd.socket snapd.autoimport.service snapd.seeded.service # Until we have a way to add more extldflags to gobuild macro... %if 0%{?fedora} >= 26 @@ -70,7 +70,7 @@ %endif Name: snapd -Version: 2.32.3.2 +Version: 2.32.9 Release: 0%{?dist} Summary: A transactional software package manager Group: System Environment/Base @@ -604,8 +604,10 @@ %{_unitdir}/snapd.socket %{_unitdir}/snapd.service %{_unitdir}/snapd.autoimport.service +%{_unitdir}/snapd.seeded.service %{_datadir}/dbus-1/services/io.snapcraft.Launcher.service %{_datadir}/polkit-1/actions/io.snapcraft.snapd.policy +%{_sysconfdir}/xdg/autostart/snap-userd-autostart.desktop %config(noreplace) %{_sysconfdir}/sysconfig/snapd %dir %{_sharedstatedir}/snapd %dir %{_sharedstatedir}/snapd/assertions @@ -710,6 +712,52 @@ %changelog +* Tue May 16 2018 Michael Vogt +- New upstream release 2.32.9 + - tests: run all spread tests inside GCE + - tests: build spread in the autopkgtests with a more recent go + +* Fri May 11 2018 Michael Vogt +- New upstream release 2.32.8 + - snapd.core-fixup.sh: add workaround for corrupted uboot.env + +* Fri May 11 2018 Michael Vogt +- New upstream release 2.32.7 + - many: add wait command and seeded target (2 + - snapd.core-fixup.sh: add workaround for corrupted uboot.env + - boot: clear "snap_mode" when needed + - cmd/libsnap: fix compile error on more restrictive gcc + - tests: cherry-pick commits to move spread to google backend + - spread.yaml: add cosmic (18.10) to autopkgtest/qemu + - userd: set up journal logging streams for autostarted apps + +* Sun Apr 29 2018 Michael Vogt +- New upstream release 2.32.6 + - snap: do not use overly short timeout in `snap + {start,stop,restart}` + - interfaces/apparmor: fix incorrect apparmor profile glob + - tests: detect kernel oops during tests and abort tests in this + case + - tests: run interfaces-boradcom-asic-control early + - tests: skip interfaces-content test on core devices + +* Mon Apr 16 2018 Michael Vogt +- New upstream release 2.32.5 + - many: add "stop-mode: sig{term,hup,usr[12]}{,-all}" instead of + conflating that with refresh-mode + - overlord/snapstate: poll for up to 10s if a snap is unexpectedly + not mounted in doMountSnap + - daemon: support 'system' as nickname of the core snap + +* Wed Apr 11 2018 Michael Vogt +- New upstream release 2.32.4 + - cmd/snap: user session application autostart + - overlord/snapstate: introduce envvars to control the channels for + bases and prereqs + - overlord/snapstate: on multi-snap refresh make sure bases and core + are finished before dependent snaps + - many: use the new install/refresh /v2/snaps/refresh store API + * Wed Apr 11 2018 Michael Vogt - New upstream release 2.32.3.2 - errtracker: make TestJournalErrorSilentError work on diff -Nru snapd-2.32.3.2/packaging/opensuse-42.1/snapd.changes snapd-2.32.9/packaging/opensuse-42.1/snapd.changes --- snapd-2.32.3.2/packaging/opensuse-42.1/snapd.changes 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/packaging/opensuse-42.1/snapd.changes 2018-05-16 08:20:08.000000000 +0000 @@ -1,4 +1,34 @@ ------------------------------------------------------------------- +Wed May 16 10:20:08 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.9 + +------------------------------------------------------------------- +Fri May 11 14:36:16 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.8 + +------------------------------------------------------------------- +Fri May 11 13:09:32 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.7 + +------------------------------------------------------------------- +Sun Apr 29 19:21:53 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.6 + +------------------------------------------------------------------- +Mon Apr 16 11:41:48 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.5 + +------------------------------------------------------------------- +Wed Apr 11 16:30:45 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.4 + +------------------------------------------------------------------- Wed Apr 11 12:40:09 UTC 2018 - mvo@fastmail.fm - Update to upstream release 2.32.3.2 diff -Nru snapd-2.32.3.2/packaging/opensuse-42.1/snapd.spec snapd-2.32.9/packaging/opensuse-42.1/snapd.spec --- snapd-2.32.3.2/packaging/opensuse-42.1/snapd.spec 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/packaging/opensuse-42.1/snapd.spec 2018-05-16 08:20:08.000000000 +0000 @@ -32,7 +32,7 @@ %define systemd_services_list snapd.socket snapd.service Name: snapd -Version: 2.32.3.2 +Version: 2.32.9 Release: 0 Summary: Tools enabling systems to work with .snap files License: GPL-3.0 @@ -293,6 +293,7 @@ %{_mandir}/man5/snap-discard-ns.5.gz %{_unitdir}/snapd.service %{_unitdir}/snapd.socket +%{_unitdir}/snapd.seeded.service /usr/bin/snap /usr/bin/snapctl /usr/sbin/rcsnapd @@ -313,6 +314,7 @@ %{_mandir}/man1/snap.1.gz /usr/share/dbus-1/services/io.snapcraft.Launcher.service /usr/share/dbus-1/services/io.snapcraft.Settings.service +%{_sysconfdir}/xdg/autostart/snap-userd-autostart.desktop %changelog diff -Nru snapd-2.32.3.2/packaging/opensuse-42.2/snapd.changes snapd-2.32.9/packaging/opensuse-42.2/snapd.changes --- snapd-2.32.3.2/packaging/opensuse-42.2/snapd.changes 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/packaging/opensuse-42.2/snapd.changes 2018-05-16 08:20:08.000000000 +0000 @@ -1,4 +1,34 @@ ------------------------------------------------------------------- +Wed May 16 10:20:08 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.9 + +------------------------------------------------------------------- +Fri May 11 14:36:16 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.8 + +------------------------------------------------------------------- +Fri May 11 13:09:32 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.7 + +------------------------------------------------------------------- +Sun Apr 29 19:21:53 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.6 + +------------------------------------------------------------------- +Mon Apr 16 11:41:48 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.5 + +------------------------------------------------------------------- +Wed Apr 11 16:30:45 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.4 + +------------------------------------------------------------------- Wed Apr 11 12:40:09 UTC 2018 - mvo@fastmail.fm - Update to upstream release 2.32.3.2 diff -Nru snapd-2.32.3.2/packaging/opensuse-42.2/snapd.spec snapd-2.32.9/packaging/opensuse-42.2/snapd.spec --- snapd-2.32.3.2/packaging/opensuse-42.2/snapd.spec 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/packaging/opensuse-42.2/snapd.spec 2018-05-16 08:20:08.000000000 +0000 @@ -32,7 +32,7 @@ %define systemd_services_list snapd.socket snapd.service Name: snapd -Version: 2.32.3.2 +Version: 2.32.9 Release: 0 Summary: Tools enabling systems to work with .snap files License: GPL-3.0 @@ -293,6 +293,7 @@ %{_mandir}/man5/snap-discard-ns.5.gz %{_unitdir}/snapd.service %{_unitdir}/snapd.socket +%{_unitdir}/snapd.seeded.service /usr/bin/snap /usr/bin/snapctl /usr/sbin/rcsnapd @@ -313,6 +314,7 @@ %{_mandir}/man1/snap.1.gz /usr/share/dbus-1/services/io.snapcraft.Launcher.service /usr/share/dbus-1/services/io.snapcraft.Settings.service +%{_sysconfdir}/xdg/autostart/snap-userd-autostart.desktop %changelog diff -Nru snapd-2.32.3.2/packaging/opensuse-42.3/permissions snapd-2.32.9/packaging/opensuse-42.3/permissions --- snapd-2.32.3.2/packaging/opensuse-42.3/permissions 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/packaging/opensuse-42.3/permissions 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1 @@ +/usr/lib/snapd/snap-confine root:root 4755 diff -Nru snapd-2.32.3.2/packaging/opensuse-42.3/permissions.easy snapd-2.32.9/packaging/opensuse-42.3/permissions.easy --- snapd-2.32.3.2/packaging/opensuse-42.3/permissions.easy 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/packaging/opensuse-42.3/permissions.easy 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1 @@ +/usr/lib/snapd/snap-confine root:root 4755 diff -Nru snapd-2.32.3.2/packaging/opensuse-42.3/permissions.paranoid snapd-2.32.9/packaging/opensuse-42.3/permissions.paranoid --- snapd-2.32.3.2/packaging/opensuse-42.3/permissions.paranoid 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/packaging/opensuse-42.3/permissions.paranoid 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1 @@ +/usr/lib/snapd/snap-confine root:root 755 diff -Nru snapd-2.32.3.2/packaging/opensuse-42.3/permissions.secure snapd-2.32.9/packaging/opensuse-42.3/permissions.secure --- snapd-2.32.3.2/packaging/opensuse-42.3/permissions.secure 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/packaging/opensuse-42.3/permissions.secure 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1 @@ +/usr/lib/snapd/snap-confine root:root 4755 diff -Nru snapd-2.32.3.2/packaging/opensuse-42.3/snapd.changes snapd-2.32.9/packaging/opensuse-42.3/snapd.changes --- snapd-2.32.3.2/packaging/opensuse-42.3/snapd.changes 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/packaging/opensuse-42.3/snapd.changes 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,228 @@ +------------------------------------------------------------------- +Wed May 16 10:20:08 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.9 + +------------------------------------------------------------------- +Fri May 11 14:36:16 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.8 + +------------------------------------------------------------------- +Fri May 11 13:09:32 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.7 + +------------------------------------------------------------------- +Sun Apr 29 19:21:53 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.6 + +------------------------------------------------------------------- +Mon Apr 16 11:41:48 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.5 + +------------------------------------------------------------------- +Wed Apr 11 16:30:45 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.4 + +------------------------------------------------------------------- +Wed Apr 11 12:40:09 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.3.2 + +------------------------------------------------------------------- +Wed Apr 11 10:34:00 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.3.1 + +------------------------------------------------------------------- +Thu Apr 05 22:35:35 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.3 + +------------------------------------------------------------------- +Sat Mar 31 21:09:29 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.2 + +------------------------------------------------------------------- +Mon Mar 26 21:03:02 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.1 + +------------------------------------------------------------------- +Sat Mar 24 08:50:11 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32 + +------------------------------------------------------------------- +Tue Feb 20 17:32:42 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.31.1 + +------------------------------------------------------------------- +Tue Feb 06 09:46:22 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.31 + +------------------------------------------------------------------- +Mon Dev 18 15:31:24 UTC 2017 - mvo@fastmail.fm + +- Update to upstream release 2.30 + +------------------------------------------------------------------- +Fri Nov 17 22:56:09 UTC 2017 - mvo@fastmail.fm + +- Update to upstream release 2.29.4 + +------------------------------------------------------------------- +Thu Nov 09 19:16:29 UTC 2017 - mvo@fastmail.fm + +- Update to upstream release 2.29.3 + +------------------------------------------------------------------- +Fri Nov 03 17:26:14:17 UTC 2017 - mvo@fastmail.fm + +- Update to upstream release 2.29.2 + +------------------------------------------------------------------- +Fri Nov 03 07:27:08:17 UTC 2017 - mvo@fastmail.fm + +- Update to upstream release 2.29.1 + +------------------------------------------------------------------- +Mon Oct 30 16:24:08 UTC 2017 - mvo@fastmail.fm + +- Update to upstream release 2.29 + +------------------------------------------------------------------- +Fri Oct 13 19:46:37 UTC 2017 - mvo@fastmail.fm + +- Update to upstream release 2.28.4 + +------------------------------------------------------------------- +Wed Oct 11 19:46:37 UTC 2017 - mvo@fastmail.fm + +- Update to upstream release 2.28.4 + +------------------------------------------------------------------- +Wed Oct 11 08:23:47 UTC 2017 - mvo@fastmail.fm + +- Update to upstream release 2.28.3 + +------------------------------------------------------------------- +Tue Oct 10 18:42:45 UTC 2017 - mvo@fastmail.fm + +- Update to upstream release 2.28.2 + +------------------------------------------------------------------- +Mon Sep 27 22:04:59 UTC 2017 - mvo@fastmail.fm + +- Update to upstream release 2.28.1 + +------------------------------------------------------------------- +Mon Sep 25 16:09:15 UTC 2017 - mvo@fastmail.fm + +- Update to upstream release 2.28 + +------------------------------------------------------------------- +Thu Sep 07 10:32:21 UTC 2017 - mvo@fastmail.fm + +- Update to upstream release 2.27.6 + +------------------------------------------------------------------- +Wed Aug 30 07:45:01 UTC 2017 - mvo@fastmail.fm + +- Update to upstream release 2.27.5 + +------------------------------------------------------------------- +Thu Aug 24 09:08:32 UTC 2017 - mvo@fastmail.fm + +- Update to upstream release 2.27.4 + +------------------------------------------------------------------- +Fri Aug 18 15:51:22 UTC 2017 - mvo@fastmail.fm + +- Update to upstream release 2.27.3 + +------------------------------------------------------------------- +Wed Aug 16 12:16:01 UTC 2017 - mvo@fastmail.fm + +- Update to upstream release 2.27.2 + +------------------------------------------------------------------- +Mon Aug 14 08:07:21 UTC 2017 - mvo@fastmail.fm + +- Update to upstream release 2.27.1 + +------------------------------------------------------------------- +Thu Aug 10 11:25:11 UTC 2017 - mvo@fastmail.fm + +- Update to upstream release 2.27 + +------------------------------------------------------------------- +Wed May 19 14:35:29 UTC 2017 - morphis@gravedo.de + +- Add bind() syscall to default seccomp policy to allow execution + of snap hooks. +- Do not share /etc/ssl with the host but use the one from the core + snap. + +------------------------------------------------------------------- +Wed May 10 12:24:44 UTC 2017 - morphis@gravedo.de + +- Update to upstream release 2.25 + +------------------------------------------------------------------- +Thu Apr 13 14:06:13 UTC 2017 - morphis@gravedo.de + +- Update to upstream release 2.24 + +------------------------------------------------------------------- +Thu Mar 30 14:14:44 UTC 2017 - morphis@gravedo.de + +- Update to upstream release 2.23.6 + +------------------------------------------------------------------- +Thu Mar 23 08:53:37 UTC 2017 - morphis@gravedo.de + +- Update to upstream release 2.23.5 +- Disable seccomp support to work around bugs in snap-confine + (see https://bugs.launchpad.net/snappy/+bug/1674193 for details) + +------------------------------------------------------------------- +Wed Mar 8 16:09:03 UTC 2017 - me@zygoon.pl + +- Fix log-out prompt to be displayed only when really necessary. +- Fix installation of /usr/lib/snapd/info (version information) +- Install bash completion for "snap" + +------------------------------------------------------------------- +Wed Mar 8 15:53:06 UTC 2017 - me@zygoon.pl + +- New upstream release. + More details are available at https://github.com/snapcore/snapd/releases/tag/2.23.1 + +------------------------------------------------------------------- +Tue Mar 7 23:00:34 UTC 2017 - me@zygoon.pl + +- Add PATH integration and post-install message asking the user to logout to + see PATH changes. + +------------------------------------------------------------------- +Tue Mar 7 00:45:12 UTC 2017 - me@zygoon.pl + +- (hacky) Disable shellcheck as it is missing on Leap 42.1 + +------------------------------------------------------------------- +Tue Mar 7 00:43:58 UTC 2017 - me@zygoon.pl + +- (hacky) fix the 32bit build + +------------------------------------------------------------------- +Mon Mar 6 18:08:04 UTC 2017 - me@zygoon.pl + +- Initial package based on fully vendorized source tarball diff -Nru snapd-2.32.3.2/packaging/opensuse-42.3/snapd-rpmlintrc snapd-2.32.9/packaging/opensuse-42.3/snapd-rpmlintrc --- snapd-2.32.3.2/packaging/opensuse-42.3/snapd-rpmlintrc 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/packaging/opensuse-42.3/snapd-rpmlintrc 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1 @@ +setBadness('permissions-unauthorized-file', 222) diff -Nru snapd-2.32.3.2/packaging/opensuse-42.3/snapd.spec snapd-2.32.9/packaging/opensuse-42.3/snapd.spec --- snapd-2.32.3.2/packaging/opensuse-42.3/snapd.spec 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/packaging/opensuse-42.3/snapd.spec 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,320 @@ +# spec file for package snapd +# +# Copyright (c) 2017 Zygmunt Krynicki +# +# All modifications and additions to the file contributed by third parties +# remain the property of their copyright owners, unless otherwise agreed +# upon. The license for this file, and modifications and additions to the +# file, is the same license as for the pristine package itself (unless the +# license for the pristine package is not an Open Source License, in which +# case the license is the MIT License). An "Open Source License" is a +# license that conforms to the Open Source Definition (Version 1.9) +# published by the Open Source Initiative. + +# Please submit bugfixes or comments via http://bugs.opensuse.org/ + +%bcond_with testkeys + +%global provider github +%global provider_tld com +%global project snapcore +%global repo snapd +%global provider_prefix %{provider}.%{provider_tld}/%{project}/%{repo} +%global import_path %{provider_prefix} + +%global with_test_keys 0 + +%if %{with testkeys} +%global with_test_keys 1 +%else +%global with_test_keys 0 +%endif + +%define systemd_services_list snapd.socket snapd.service +Name: snapd +Version: 2.32.9 +Release: 0 +Summary: Tools enabling systems to work with .snap files +License: GPL-3.0 +Group: System/Packages +Url: https://%{import_path} +Source0: https://github.com/snapcore/snapd/releases/download/%{version}/%{name}_%{version}.vendor.tar.xz +Source1: snapd-rpmlintrc +# TODO: make this enabled only on Leap 42.2+ +# BuildRequires: ShellCheck +BuildRequires: autoconf +BuildRequires: automake +BuildRequires: glib2-devel +BuildRequires: glibc-devel-static +BuildRequires: golang-packaging +BuildRequires: gpg2 +BuildRequires: indent +BuildRequires: libapparmor-devel +BuildRequires: libcap-devel +BuildRequires: libseccomp-devel +BuildRequires: libtool +BuildRequires: libudev-devel +BuildRequires: libuuid-devel +BuildRequires: make +BuildRequires: openssh +BuildRequires: pkg-config +BuildRequires: python-docutils +BuildRequires: python3-docutils +BuildRequires: squashfs +BuildRequires: timezone +BuildRequires: udev +BuildRequires: xfsprogs-devel +BuildRequires: xz + +# Make sure we are on Leap 42.2/SLE 12 SP2 or higher +%if 0%{?sle_version} >= 120200 +BuildRequires: systemd-rpm-macros +%endif + +PreReq: permissions + +Requires(post): permissions +Requires: apparmor-parser +Requires: gpg2 +Requires: openssh +Requires: squashfs + +%systemd_requires + +BuildRoot: %{_tmppath}/%{name}-%{version}-build + +# TODO strip the C executables but don't strip the go executables +# as that breaks the world in some ways. +# reenable {go_nostrip} +%{go_provides} + +%description +This package contains that snapd daemon and the snap command line tool. +Together they can be used to install, refresh (update), remove and configure +snap packages on a system. Snap packages are a novel format based on simple +principles. Bundle your dependencies, run in a predictable environment, use +moder kernel features for setting up the execution environment and security. +The same binary snap package can be installed and used on many diverse systems +such as Debian, Fedora and OpenSUSE as well as their multiple derivatives. +. +This package contains the official build, endorsed by snapd developers. It is +updated as soon as new upstream releases are made and is designed to live in +the system:snappy repository. + +%prep +%setup -q -n %{name}-%{version} + +# Set the version that is compiled into the various executables +./mkversion.sh %{version}-%{release} + +# Generate autotools build system files +cd cmd && autoreconf -i -f + +# Enable hardening; We can't use -pie here as this conflicts with +# our build of static binaries for snap-confine. Also see +# https://bugzilla.redhat.com/show_bug.cgi?id=1343892 +CFLAGS="$RPM_OPT_FLAGS -fPIC -Wl,-z,relro -Wl,-z,now" +CXXFLAGS="$RPM_OPT_FLAGS -fPIC -Wl,-z,relro -Wl,-z,now" +export CFLAGS +export CXXFLAGS + +# NOTE: until snapd and snap-confine have the improved communication mechanism +# we need to disable apparmor as snapd doesn't yet support the version of +# apparmor kernel available in SUSE and Debian. The generated apparmor profiles +# cannot be loaded into a vanilla kernel. As a temporary measure we just switch +# it all off. +%configure --disable-apparmor --libexecdir=%{_libexecdir}/snapd + +%build +# Build golang executables +%goprep %{import_path} + +%if 0%{?with_test_keys} +# The gobuild macro doesn't allow us to pass any additional parameters +# so we we have to invoke `go install` here manually. +export GOPATH=%{_builddir}/go:%{_libdir}/go/contrib +export GOBIN=%{_builddir}/go/bin +# Options used are the same as the gobuild macro does but as it +# doesn't allow us to amend new flags we have to repeat them here: +# -s: tell long running tests to shorten their build time +# -v: be verbose +# -p 4: allow parallel execution of tests +# -x: print commands +go install -s -v -p 4 -x -tags withtestkeys github.com/snapcore/snapd/cmd/snapd +%else +%gobuild cmd/snapd +%endif + +%gobuild cmd/snap +%gobuild cmd/snapctl +# build snap-exec and snap-update-ns completely static for base snaps +CGO_ENABLED=0 %gobuild cmd/snap-exec +# gobuild --ldflags '-extldflags "-static"' bin/snap-update-ns +# FIXME: ^ this doesn't work yet, it's going to be fixed with another PR. +%gobuild cmd/snap-update-ns + +# This is ok because snap-seccomp only requires static linking when it runs from the core-snap via re-exec. +sed -e "s/-Bstatic -lseccomp/-Bstatic/g" -i %{_builddir}/go/src/%{provider_prefix}/cmd/snap-seccomp/main.go +# build snap-seccomp +%gobuild cmd/snap-seccomp + +# Build C executables +make %{?_smp_mflags} -C cmd + +%check +%{gotest} %{import_path}/... +make %{?_smp_mflags} -C cmd check + +%install +# Install all the go stuff +%goinstall +# TODO: instead of removing it move this to a dedicated golang package +rm -rf %{buildroot}%{_libexecdir}64/go +rm -rf %{buildroot}%{_libexecdir}/go +find %{buildroot} +# Move snapd, snap-exec, snap-seccomp and snap-update-ns into %{_libexecdir}/snapd +install -m 755 -d %{buildroot}%{_libexecdir}/snapd +mv %{buildroot}/usr/bin/snapd %{buildroot}%{_libexecdir}/snapd/snapd +mv %{buildroot}/usr/bin/snap-exec %{buildroot}%{_libexecdir}/snapd/snap-exec +mv %{buildroot}/usr/bin/snap-update-ns %{buildroot}%{_libexecdir}/snapd/snap-update-ns +mv %{buildroot}/usr/bin/snap-seccomp %{buildroot}%{_libexecdir}/snapd/snap-seccomp +# Install profile.d-based PATH integration for /snap/bin +# and XDG_DATA_DIRS for /var/lib/snapd/desktop +make -C data/env install DESTDIR=%{buildroot} + +# Generate and install man page for snap command +install -m 755 -d %{buildroot}%{_mandir}/man1 +%{buildroot}/usr/bin/snap help --man > %{buildroot}%{_mandir}/man1/snap.1 + +# TODO: enable gosrc +# TODO: enable gofilelist + +# Install all the C executables +%{make_install} -C cmd +# Undo special permissions of the void directory +chmod 755 %{?buildroot}/var/lib/snapd/void +# Remove traces of ubuntu-core-launcher. It is a phased-out executable that is +# still partially present in the tree but should be removed in the subsequent +# release. +rm -f %{?buildroot}/usr/bin/ubuntu-core-launcher +# NOTE: we don't want to ship system-shutdown helper, it is just a helper on +# ubuntu-core systems that exclusively use snaps. It is used during the +# shutdown process and thus can be left out of the distribution package. +rm -f %{?buildroot}%{_libexecdir}/snapd/system-shutdown +# Install the directories that snapd creates by itself so that they can be a part of the package +install -d %buildroot/var/lib/snapd/{assertions,desktop/applications,device,hostfs,mount,apparmor/profiles,seccomp/bpf,snaps} + +install -d %buildroot/var/lib/snapd/{lib/gl,lib/gl32,lib/vulkan} +install -d %buildroot/var/cache/snapd +install -d %buildroot/snap/bin +# Install local permissions policy for snap-confine. This should be removed +# once snap-confine is added to the permissions package. This is done following +# the recommendations on +# https://en.opensuse.org/openSUSE:Package_security_guidelines +install -m 644 -D packaging/opensuse-42.2/permissions %buildroot/%{_sysconfdir}/permissions.d/snapd +install -m 644 -D packaging/opensuse-42.2/permissions.paranoid %buildroot/%{_sysconfdir}/permissions.d/snapd.paranoid +# Install the systemd units +make -C data install DESTDIR=%{buildroot} SYSTEMDSYSTEMUNITDIR=%{_unitdir} +for s in snapd.autoimport.service snapd.system-shutdown.service snapd.snap-repair.timer snapd.snap-repair.service snapd.core-fixup.service; do + rm -f %buildroot/%{_unitdir}/$s +done +# Remove snappy core specific scripts +rm -f %buildroot%{_libexecdir}/snapd/snapd.core-fixup.sh + +# See https://en.opensuse.org/openSUSE:Packaging_checks#suse-missing-rclink for details +install -d %{buildroot}/usr/sbin +ln -sf %{_sbindir}/service %{buildroot}/%{_sbindir}/rcsnapd +ln -sf %{_sbindir}/service %{buildroot}/%{_sbindir}/rcsnapd.refresh +# Install the "info" data file with snapd version +install -m 644 -D data/info %{buildroot}%{_libexecdir}/snapd/info +# Install bash completion for "snap" +install -m 644 -D data/completion/snap %{buildroot}/usr/share/bash-completion/completions/snap +install -m 644 -D data/completion/complete.sh %{buildroot}%{_libexecdir}/snapd +install -m 644 -D data/completion/etelpmoc.sh %{buildroot}%{_libexecdir}/snapd +# move snapd-generator +install -m 755 -d %{buildroot}/lib/systemd/system-generators/ +mv %{buildroot}%{_libexecdir}/snapd/snapd-generator %{buildroot}/lib/systemd/system-generators/ + +%verifyscript +%verify_permissions -e %{_libexecdir}/snapd/snap-confine + +%pre +%service_add_pre %{systemd_services_list} + +%post +%set_permissions %{_libexecdir}/snapd/snap-confine +%service_add_post %{systemd_services_list} +case ":$PATH:" in + *:/snap/bin:*) + ;; + *) + echo "Please reboot, logout/login or source /etc/profile to have /snap/bin added to PATH." + ;; +esac + +%preun +%service_del_preun %{systemd_services_list} +if [ $1 -eq 0 ]; then + %{_libexecdir}/snapd/snap-mgmt --purge || : +fi + +%postun +%service_del_postun %{systemd_services_list} + +%files +%defattr(-,root,root) +%config %{_sysconfdir}/permissions.d/snapd +%config %{_sysconfdir}/permissions.d/snapd.paranoid +%config %{_sysconfdir}/profile.d/snapd.sh +%dir %attr(0000,root,root) /var/lib/snapd/void +%dir /snap +%dir /snap/bin +%dir %{_libexecdir}/snapd +%dir /var/lib/snapd +%dir /var/lib/snapd/apparmor +%dir /var/lib/snapd/apparmor/profiles +%dir /var/lib/snapd/apparmor/snap-confine +%dir /var/lib/snapd/assertions +%dir /var/lib/snapd/desktop +%dir /var/lib/snapd/desktop/applications +%dir /var/lib/snapd/device +%dir /var/lib/snapd/hostfs +%dir /var/lib/snapd/mount +%dir /var/lib/snapd/seccomp +%dir /var/lib/snapd/seccomp/bpf +%dir /var/lib/snapd/snaps +%dir /var/lib/snapd/lib +%dir /var/lib/snapd/lib/gl +%dir /var/lib/snapd/lib/gl32 +%dir /var/lib/snapd/lib/vulkan +%dir /var/cache/snapd +%verify(not user group mode) %attr(06755,root,root) %{_libexecdir}/snapd/snap-confine +%{_mandir}/man1/snap-confine.1.gz +%{_mandir}/man5/snap-discard-ns.5.gz +%{_unitdir}/snapd.service +%{_unitdir}/snapd.socket +%{_unitdir}/snapd.seeded.service +/usr/bin/snap +/usr/bin/snapctl +/usr/sbin/rcsnapd +/usr/sbin/rcsnapd.refresh +%{_libexecdir}/snapd/info +%{_libexecdir}/snapd/snap-discard-ns +%{_libexecdir}/snapd/snap-update-ns +%{_libexecdir}/snapd/snap-exec +%{_libexecdir}/snapd/snap-seccomp +%{_libexecdir}/snapd/snapd +%{_libexecdir}/snapd/snap-mgmt +%{_libexecdir}/snapd/snap-gdb-shim +%{_libexecdir}/snapd/snap-device-helper +/usr/share/bash-completion/completions/snap +%{_libexecdir}/snapd/complete.sh +%{_libexecdir}/snapd/etelpmoc.sh +/lib/systemd/system-generators/snapd-generator +%{_mandir}/man1/snap.1.gz +/usr/share/dbus-1/services/io.snapcraft.Launcher.service +/usr/share/dbus-1/services/io.snapcraft.Settings.service +%{_sysconfdir}/xdg/autostart/snap-userd-autostart.desktop + +%changelog + diff -Nru snapd-2.32.3.2/packaging/ubuntu-14.04/changelog snapd-2.32.9/packaging/ubuntu-14.04/changelog --- snapd-2.32.3.2/packaging/ubuntu-14.04/changelog 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/packaging/ubuntu-14.04/changelog 2018-05-16 08:20:08.000000000 +0000 @@ -1,3 +1,67 @@ +snapd (2.32.9~14.04) trusty; urgency=medium + + * New upstream release, LP: #1767833 + - tests: run all spread tests inside GCE + - tests: build spread in the autopkgtests with a more recent go + + -- Michael Vogt Wed, 16 May 2018 10:20:08 +0200 + +snapd (2.32.8~14.04) trusty; urgency=medium + + * New upstream release, LP: #1767833 + - snapd.core-fixup.sh: add workaround for corrupted uboot.env + + -- Michael Vogt Fri, 11 May 2018 14:36:16 +0200 + +snapd (2.32.7~14.04) trusty; urgency=medium + + * New upstream release, LP: #1767833 + - many: add wait command and seeded target (2 + - snapd.core-fixup.sh: add workaround for corrupted uboot.env + - boot: clear "snap_mode" when needed + - cmd/libsnap: fix compile error on more restrictive gcc + - tests: cherry-pick commits to move spread to google backend + - spread.yaml: add cosmic (18.10) to autopkgtest/qemu + - userd: set up journal logging streams for autostarted apps + + -- Michael Vogt Fri, 11 May 2018 13:09:32 +0200 + +snapd (2.32.6~14.04) trusty; urgency=medium + + * New upstream release, LP: #1767833 + - snap: do not use overly short timeout in `snap + {start,stop,restart}` + - interfaces/apparmor: fix incorrect apparmor profile glob + - tests: detect kernel oops during tests and abort tests in this + case + - tests: run interfaces-boradcom-asic-control early + - tests: skip interfaces-content test on core devices + + -- Michael Vogt Sun, 29 Apr 2018 19:21:53 +0200 + +snapd (2.32.5~14.04) trusty; urgency=medium + + * New upstream release, LP: #1756173 + - many: add "stop-mode: sig{term,hup,usr[12]}{,-all}" instead of + conflating that with refresh-mode + - overlord/snapstate: poll for up to 10s if a snap is unexpectedly + not mounted in doMountSnap + - daemon: support 'system' as nickname of the core snap + + -- Michael Vogt Mon, 16 Apr 2018 11:41:48 +0200 + +snapd (2.32.4~14.04) trusty; urgency=medium + + * New upstream release, LP: #1756173 + - cmd/snap: user session application autostart + - overlord/snapstate: introduce envvars to control the channels for + bases and prereqs + - overlord/snapstate: on multi-snap refresh make sure bases and core + are finished before dependent snaps + - many: use the new install/refresh /v2/snaps/refresh store API + + -- Michael Vogt Wed, 11 Apr 2018 16:30:45 +0200 + snapd (2.32.3.2~14.04) trusty; urgency=medium * New upstream release, LP: #1756173 diff -Nru snapd-2.32.3.2/packaging/ubuntu-16.04/changelog snapd-2.32.9/packaging/ubuntu-16.04/changelog --- snapd-2.32.3.2/packaging/ubuntu-16.04/changelog 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/packaging/ubuntu-16.04/changelog 2018-05-16 08:20:08.000000000 +0000 @@ -1,3 +1,66 @@ +snapd (2.32.9) xenial; urgency=medium + + * New upstream release, LP: #1767833 + - tests: run all spread tests inside GCE + - tests: build spread in the autopkgtests with a more recent go + + -- Michael Vogt Wed, 16 May 2018 10:20:08 +0200 + +snapd (2.32.8) xenial; urgency=medium + + * New upstream release, LP: #1767833 + + -- Michael Vogt Fri, 11 May 2018 14:36:16 +0200 + +snapd (2.32.7) xenial; urgency=medium + + * New upstream release, LP: #1767833 + - many: add wait command and seeded target (2 + - snapd.core-fixup.sh: add workaround for corrupted uboot.env + - boot: clear "snap_mode" when needed + - cmd/libsnap: fix compile error on more restrictive gcc + - tests: cherry-pick commits to move spread to google backend + - spread.yaml: add cosmic (18.10) to autopkgtest/qemu + - userd: set up journal logging streams for autostarted apps + + -- Michael Vogt Fri, 11 May 2018 13:09:32 +0200 + +snapd (2.32.6) xenial; urgency=medium + + * New upstream release, LP: #1767833 + - snap: do not use overly short timeout in `snap + {start,stop,restart}` + - interfaces/apparmor: fix incorrect apparmor profile glob + - tests: detect kernel oops during tests and abort tests in this + case + - tests: run interfaces-boradcom-asic-control early + - tests: skip interfaces-content test on core devices + + -- Michael Vogt Sun, 29 Apr 2018 19:21:53 +0200 + +snapd (2.32.5) xenial; urgency=medium + + * New upstream release, LP: #1765090 + - many: add "stop-mode: sig{term,hup,usr[12]}{,-all}" instead of + conflating that with refresh-mode + - overlord/snapstate: poll for up to 10s if a snap is unexpectedly + not mounted in doMountSnap + - daemon: support 'system' as nickname of the core snap + + -- Michael Vogt Mon, 16 Apr 2018 11:41:48 +0200 + +snapd (2.32.4) xenial; urgency=medium + + * New upstream release, LP: #1756173 + - cmd/snap: user session application autostart + - overlord/snapstate: introduce envvars to control the channels for + bases and prereqs + - overlord/snapstate: on multi-snap refresh make sure bases and core + are finished before dependent snaps + - many: use the new install/refresh /v2/snaps/refresh store API + + -- Michael Vogt Wed, 11 Apr 2018 16:30:45 +0200 + snapd (2.32.3.2) xenial; urgency=medium * New upstream release, LP: #1756173 diff -Nru snapd-2.32.3.2/packaging/ubuntu-16.04/tests/integrationtests snapd-2.32.9/packaging/ubuntu-16.04/tests/integrationtests --- snapd-2.32.3.2/packaging/ubuntu-16.04/tests/integrationtests 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/packaging/ubuntu-16.04/tests/integrationtests 2018-05-16 08:20:08.000000000 +0000 @@ -36,11 +36,14 @@ export SPREAD_CORE_CHANNEL=stable fi +# Spread will only buid with recent go +snap install --classic go + # and now run spread against localhost # shellcheck disable=SC1091 . /etc/os-release export GOPATH=/tmp/go -go get -u github.com/snapcore/spread/cmd/spread +/snap/bin/go get -u github.com/snapcore/spread/cmd/spread /tmp/go/bin/spread -v "autopkgtest:${ID}-${VERSION_ID}-$(dpkg --print-architecture)" # store journal info for inspectsion diff -Nru snapd-2.32.3.2/packaging/ubuntu-16.10/changelog snapd-2.32.9/packaging/ubuntu-16.10/changelog --- snapd-2.32.3.2/packaging/ubuntu-16.10/changelog 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/packaging/ubuntu-16.10/changelog 2018-05-16 08:20:08.000000000 +0000 @@ -1,3 +1,66 @@ +snapd (2.32.9) xenial; urgency=medium + + * New upstream release, LP: #1767833 + - tests: run all spread tests inside GCE + - tests: build spread in the autopkgtests with a more recent go + + -- Michael Vogt Wed, 16 May 2018 10:20:08 +0200 + +snapd (2.32.8) xenial; urgency=medium + + * New upstream release, LP: #1767833 + + -- Michael Vogt Fri, 11 May 2018 14:36:16 +0200 + +snapd (2.32.7) xenial; urgency=medium + + * New upstream release, LP: #1767833 + - many: add wait command and seeded target (2 + - snapd.core-fixup.sh: add workaround for corrupted uboot.env + - boot: clear "snap_mode" when needed + - cmd/libsnap: fix compile error on more restrictive gcc + - tests: cherry-pick commits to move spread to google backend + - spread.yaml: add cosmic (18.10) to autopkgtest/qemu + - userd: set up journal logging streams for autostarted apps + + -- Michael Vogt Fri, 11 May 2018 13:09:32 +0200 + +snapd (2.32.6) xenial; urgency=medium + + * New upstream release, LP: #1767833 + - snap: do not use overly short timeout in `snap + {start,stop,restart}` + - interfaces/apparmor: fix incorrect apparmor profile glob + - tests: detect kernel oops during tests and abort tests in this + case + - tests: run interfaces-boradcom-asic-control early + - tests: skip interfaces-content test on core devices + + -- Michael Vogt Sun, 29 Apr 2018 19:21:53 +0200 + +snapd (2.32.5) xenial; urgency=medium + + * New upstream release, LP: #1765090 + - many: add "stop-mode: sig{term,hup,usr[12]}{,-all}" instead of + conflating that with refresh-mode + - overlord/snapstate: poll for up to 10s if a snap is unexpectedly + not mounted in doMountSnap + - daemon: support 'system' as nickname of the core snap + + -- Michael Vogt Mon, 16 Apr 2018 11:41:48 +0200 + +snapd (2.32.4) xenial; urgency=medium + + * New upstream release, LP: #1756173 + - cmd/snap: user session application autostart + - overlord/snapstate: introduce envvars to control the channels for + bases and prereqs + - overlord/snapstate: on multi-snap refresh make sure bases and core + are finished before dependent snaps + - many: use the new install/refresh /v2/snaps/refresh store API + + -- Michael Vogt Wed, 11 Apr 2018 16:30:45 +0200 + snapd (2.32.3.2) xenial; urgency=medium * New upstream release, LP: #1756173 diff -Nru snapd-2.32.3.2/packaging/ubuntu-16.10/tests/integrationtests snapd-2.32.9/packaging/ubuntu-16.10/tests/integrationtests --- snapd-2.32.3.2/packaging/ubuntu-16.10/tests/integrationtests 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/packaging/ubuntu-16.10/tests/integrationtests 2018-05-16 08:20:08.000000000 +0000 @@ -36,11 +36,14 @@ export SPREAD_CORE_CHANNEL=stable fi +# Spread will only buid with recent go +snap install --classic go + # and now run spread against localhost # shellcheck disable=SC1091 . /etc/os-release export GOPATH=/tmp/go -go get -u github.com/snapcore/spread/cmd/spread +/snap/bin/go get -u github.com/snapcore/spread/cmd/spread /tmp/go/bin/spread -v "autopkgtest:${ID}-${VERSION_ID}-$(dpkg --print-architecture)" # store journal info for inspectsion diff -Nru snapd-2.32.3.2/packaging/ubuntu-17.04/changelog snapd-2.32.9/packaging/ubuntu-17.04/changelog --- snapd-2.32.3.2/packaging/ubuntu-17.04/changelog 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/packaging/ubuntu-17.04/changelog 2018-05-16 08:20:08.000000000 +0000 @@ -1,3 +1,66 @@ +snapd (2.32.9) xenial; urgency=medium + + * New upstream release, LP: #1767833 + - tests: run all spread tests inside GCE + - tests: build spread in the autopkgtests with a more recent go + + -- Michael Vogt Wed, 16 May 2018 10:20:08 +0200 + +snapd (2.32.8) xenial; urgency=medium + + * New upstream release, LP: #1767833 + + -- Michael Vogt Fri, 11 May 2018 14:36:16 +0200 + +snapd (2.32.7) xenial; urgency=medium + + * New upstream release, LP: #1767833 + - many: add wait command and seeded target (2 + - snapd.core-fixup.sh: add workaround for corrupted uboot.env + - boot: clear "snap_mode" when needed + - cmd/libsnap: fix compile error on more restrictive gcc + - tests: cherry-pick commits to move spread to google backend + - spread.yaml: add cosmic (18.10) to autopkgtest/qemu + - userd: set up journal logging streams for autostarted apps + + -- Michael Vogt Fri, 11 May 2018 13:09:32 +0200 + +snapd (2.32.6) xenial; urgency=medium + + * New upstream release, LP: #1767833 + - snap: do not use overly short timeout in `snap + {start,stop,restart}` + - interfaces/apparmor: fix incorrect apparmor profile glob + - tests: detect kernel oops during tests and abort tests in this + case + - tests: run interfaces-boradcom-asic-control early + - tests: skip interfaces-content test on core devices + + -- Michael Vogt Sun, 29 Apr 2018 19:21:53 +0200 + +snapd (2.32.5) xenial; urgency=medium + + * New upstream release, LP: #1765090 + - many: add "stop-mode: sig{term,hup,usr[12]}{,-all}" instead of + conflating that with refresh-mode + - overlord/snapstate: poll for up to 10s if a snap is unexpectedly + not mounted in doMountSnap + - daemon: support 'system' as nickname of the core snap + + -- Michael Vogt Mon, 16 Apr 2018 11:41:48 +0200 + +snapd (2.32.4) xenial; urgency=medium + + * New upstream release, LP: #1756173 + - cmd/snap: user session application autostart + - overlord/snapstate: introduce envvars to control the channels for + bases and prereqs + - overlord/snapstate: on multi-snap refresh make sure bases and core + are finished before dependent snaps + - many: use the new install/refresh /v2/snaps/refresh store API + + -- Michael Vogt Wed, 11 Apr 2018 16:30:45 +0200 + snapd (2.32.3.2) xenial; urgency=medium * New upstream release, LP: #1756173 diff -Nru snapd-2.32.3.2/packaging/ubuntu-17.04/tests/integrationtests snapd-2.32.9/packaging/ubuntu-17.04/tests/integrationtests --- snapd-2.32.3.2/packaging/ubuntu-17.04/tests/integrationtests 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/packaging/ubuntu-17.04/tests/integrationtests 2018-05-16 08:20:08.000000000 +0000 @@ -36,11 +36,14 @@ export SPREAD_CORE_CHANNEL=stable fi +# Spread will only buid with recent go +snap install --classic go + # and now run spread against localhost # shellcheck disable=SC1091 . /etc/os-release export GOPATH=/tmp/go -go get -u github.com/snapcore/spread/cmd/spread +/snap/bin/go get -u github.com/snapcore/spread/cmd/spread /tmp/go/bin/spread -v "autopkgtest:${ID}-${VERSION_ID}-$(dpkg --print-architecture)" # store journal info for inspectsion diff -Nru snapd-2.32.3.2/run-checks snapd-2.32.9/run-checks --- snapd-2.32.3.2/run-checks 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/run-checks 2018-05-16 08:20:08.000000000 +0000 @@ -248,7 +248,7 @@ export PATH=$TMP_SPREAD:$PATH ( cd "$TMP_SPREAD" && curl -s -O https://niemeyer.s3.amazonaws.com/spread-amd64.tar.gz && tar xzvf spread-amd64.tar.gz ) - spread -v linode: + spread google: linode: # cleanup the debian-ubuntu-14.04 rm -rf debian-ubuntu-14.04 diff -Nru snapd-2.32.3.2/snap/info.go snapd-2.32.9/snap/info.go --- snapd-2.32.3.2/snap/info.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/snap/info.go 2018-05-16 08:20:08.000000000 +0000 @@ -304,12 +304,12 @@ // UserDataDir returns the user-specific data directory of the snap. func (s *Info) UserDataDir(home string) string { - return filepath.Join(home, "snap", s.Name(), s.Revision.String()) + return filepath.Join(home, dirs.UserHomeSnapDir, s.Name(), s.Revision.String()) } // UserCommonDataDir returns the user-specific data directory common across revision of the snap. func (s *Info) UserCommonDataDir(home string) string { - return filepath.Join(home, "snap", s.Name(), "common") + return filepath.Join(home, dirs.UserHomeSnapDir, s.Name(), "common") } // CommonDataDir returns the data directory common across revisions of the snap. @@ -537,6 +537,33 @@ Timer string } +// StopModeType is the type for the "stop-mode:" of a snap app +type StopModeType string + +// KillAll returns if the stop-mode means all processes should be killed +// when the service is stopped or just the main process. +func (st StopModeType) KillAll() bool { + return string(st) == "" || strings.HasSuffix(string(st), "-all") +} + +// KillSignal returns the signal that should be used to kill the process +// (or an empty string if no signal is needed) +func (st StopModeType) KillSignal() string { + if st.Validate() != nil || st == "" { + return "" + } + return strings.ToUpper(strings.TrimSuffix(string(st), "-all")) +} + +func (st StopModeType) Validate() error { + switch st { + case "", "sigterm", "sigterm-all", "sighup", "sighup-all", "sigusr1", "sigusr1-all", "sigusr2", "sigusr2-all": + // valid + return nil + } + return fmt.Errorf(`"stop-mode" field contains invalid value %q`, st) +} + // AppInfo provides information about a app. type AppInfo struct { Snap *Info @@ -553,6 +580,7 @@ RestartCond RestartCondition Completer string RefreshMode string + StopMode StopModeType // TODO: this should go away once we have more plumbing and can change // things vs refactor @@ -571,6 +599,8 @@ Before []string Timer *TimerInfo + + Autostart string } // ScreenshotInfo provides information about a screenshot. @@ -762,6 +792,22 @@ return info, nil } +// ReadCurrentInfo reads the snap information from the installed snap in 'current' revision +func ReadCurrentInfo(snapName string) (*Info, error) { + curFn := filepath.Join(dirs.SnapMountDir, snapName, "current") + realFn, err := os.Readlink(curFn) + if err != nil { + return nil, fmt.Errorf("cannot find current revision for snap %s: %s", snapName, err) + } + rev := filepath.Base(realFn) + revision, err := ParseRevision(rev) + if err != nil { + return nil, fmt.Errorf("cannot read revision %s: %s", rev, err) + } + + return ReadInfo(snapName, &SideInfo{Revision: revision}) +} + // ReadInfoFromSnapFile reads the snap information from the given File // and completes it with the given side-info if this is not nil. func ReadInfoFromSnapFile(snapf Container, si *SideInfo) (*Info, error) { diff -Nru snapd-2.32.3.2/snap/info_snap_yaml.go snapd-2.32.9/snap/info_snap_yaml.go --- snapd-2.32.3.2/snap/info_snap_yaml.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/snap/info_snap_yaml.go 2018-05-16 08:20:08.000000000 +0000 @@ -68,6 +68,7 @@ StopTimeout timeout.Timeout `yaml:"stop-timeout,omitempty"` Completer string `yaml:"completer,omitempty"` RefreshMode string `yaml:"refresh-mode,omitempty"` + StopMode StopModeType `yaml:"stop-mode,omitempty"` RestartCond RestartCondition `yaml:"restart-condition,omitempty"` SlotNames []string `yaml:"slots,omitempty"` @@ -83,6 +84,8 @@ Before []string `yaml:"before,omitempty"` Timer string `yaml:"timer,omitempty"` + + Autostart string `yaml:"autostart,omitempty"` } type hookYaml struct { @@ -298,8 +301,10 @@ Environment: yApp.Environment, Completer: yApp.Completer, RefreshMode: yApp.RefreshMode, + StopMode: yApp.StopMode, Before: yApp.Before, After: yApp.After, + Autostart: yApp.Autostart, } if len(y.Plugs) > 0 || len(yApp.PlugNames) > 0 { app.Plugs = make(map[string]*PlugInfo) diff -Nru snapd-2.32.3.2/snap/info_snap_yaml_test.go snapd-2.32.9/snap/info_snap_yaml_test.go --- snapd-2.32.3.2/snap/info_snap_yaml_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/snap/info_snap_yaml_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -1670,3 +1670,30 @@ app := info.Apps["foo"] c.Check(app.Timer, DeepEquals, &snap.TimerInfo{App: app, Timer: "mon,10:00-12:00"}) } + +func (s *YamlSuite) TestSnapYamlAppAutostart(c *C) { + yAutostart := []byte(`name: wat +version: 42 +apps: + foo: + command: bin/foo + autostart: foo.desktop + +`) + info, err := snap.InfoFromSnapYaml(yAutostart) + c.Assert(err, IsNil) + app := info.Apps["foo"] + c.Check(app.Autostart, Equals, "foo.desktop") + + yNoAutostart := []byte(`name: wat +version: 42 +apps: + foo: + command: bin/foo + +`) + info, err = snap.InfoFromSnapYaml(yNoAutostart) + c.Assert(err, IsNil) + app = info.Apps["foo"] + c.Check(app.Autostart, Equals, "") +} diff -Nru snapd-2.32.3.2/snap/info_test.go snapd-2.32.9/snap/info_test.go --- snapd-2.32.3.2/snap/info_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/snap/info_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -195,6 +195,23 @@ c.Check(snapInfo2, DeepEquals, snapInfo1) } +func (s *infoSuite) TestReadCurrentInfo(c *C) { + si := &snap.SideInfo{Revision: snap.R(42)} + + snapInfo1 := snaptest.MockSnapCurrent(c, sampleYaml, si) + + snapInfo2, err := snap.ReadCurrentInfo("sample") + c.Assert(err, IsNil) + + c.Check(snapInfo2.Name(), Equals, "sample") + c.Check(snapInfo2.Revision, Equals, snap.R(42)) + c.Check(snapInfo2, DeepEquals, snapInfo1) + + snapInfo3, err := snap.ReadCurrentInfo("not-sample") + c.Check(snapInfo3, IsNil) + c.Assert(err, ErrorMatches, `cannot find current revision for snap not-sample:.*`) +} + func (s *infoSuite) TestInstallDate(c *C) { si := &snap.SideInfo{Revision: snap.R(1)} info := snaptest.MockSnap(c, sampleYaml, si) @@ -910,3 +927,41 @@ c.Assert(info.ExpandSnapVariables("$SNAP_COMMON/stuff"), Equals, "/var/snap/foo/common/stuff") c.Assert(info.ExpandSnapVariables("$GARBAGE/rocks"), Equals, "/rocks") } + +func (s *infoSuite) TestStopModeTypeKillMode(c *C) { + for _, t := range []struct { + stopMode string + killall bool + }{ + {"", true}, + {"sigterm", false}, + {"sigterm-all", true}, + {"sighup", false}, + {"sighup-all", true}, + {"sigusr1", false}, + {"sigusr1-all", true}, + {"sigusr2", false}, + {"sigusr2-all", true}, + } { + c.Check(snap.StopModeType(t.stopMode).KillAll(), Equals, t.killall, Commentf("wrong KillAll for %v", t.stopMode)) + } +} + +func (s *infoSuite) TestStopModeTypeKillSignal(c *C) { + for _, t := range []struct { + stopMode string + killSig string + }{ + {"", ""}, + {"sigterm", "SIGTERM"}, + {"sigterm-all", "SIGTERM"}, + {"sighup", "SIGHUP"}, + {"sighup-all", "SIGHUP"}, + {"sigusr1", "SIGUSR1"}, + {"sigusr1-all", "SIGUSR1"}, + {"sigusr2", "SIGUSR2"}, + {"sigusr2-all", "SIGUSR2"}, + } { + c.Check(snap.StopModeType(t.stopMode).KillSignal(), Equals, t.killSig) + } +} diff -Nru snapd-2.32.3.2/snap/snaptest/snaptest.go snapd-2.32.9/snap/snaptest/snaptest.go --- snapd-2.32.3.2/snap/snaptest/snaptest.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/snap/snaptest/snaptest.go 2018-05-16 08:20:08.000000000 +0000 @@ -68,6 +68,18 @@ return snapInfo } +// MockSnapCurrent does the same as MockSnap but additionally creates the +// 'current' symlink. +// +// The caller is responsible for mocking root directory with dirs.SetRootDir() +// and for altering the overlord state if required. +func MockSnapCurrent(c *check.C, yamlText string, sideInfo *snap.SideInfo) *snap.Info { + si := MockSnap(c, yamlText, sideInfo) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + return si +} + // MockInfo parses the given snap.yaml text and returns a validated snap.Info object including the optional SideInfo. // // The result is just kept in memory, there is nothing kept on disk. If that is diff -Nru snapd-2.32.3.2/snap/snaptest/snaptest_test.go snapd-2.32.9/snap/snaptest/snaptest_test.go --- snapd-2.32.3.2/snap/snaptest/snaptest_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/snap/snaptest/snaptest_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -72,6 +72,20 @@ c.Check(snapInfo.Plugs["network"].Interface, Equals, "network") } +func (s *snapTestSuite) TestMockSnapCurrent(c *C) { + snapInfo := snaptest.MockSnapCurrent(c, sampleYaml, &snap.SideInfo{Revision: snap.R(42)}) + // Data from YAML is used + c.Check(snapInfo.Name(), Equals, "sample") + // Data from SideInfo is used + c.Check(snapInfo.Revision, Equals, snap.R(42)) + // The YAML is placed on disk + c.Check(filepath.Join(dirs.SnapMountDir, "sample", "42", "meta", "snap.yaml"), + testutil.FileEquals, sampleYaml) + link, err := os.Readlink(filepath.Join(dirs.SnapMountDir, "sample", "current")) + c.Check(err, IsNil) + c.Check(link, Equals, filepath.Join(dirs.SnapMountDir, "sample", "42")) +} + func (s *snapTestSuite) TestMockInfo(c *C) { snapInfo := snaptest.MockInfo(c, sampleYaml, &snap.SideInfo{Revision: snap.R(42)}) // Data from YAML is used diff -Nru snapd-2.32.3.2/snap/validate.go snapd-2.32.9/snap/validate.go --- snapd-2.32.3.2/snap/validate.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/snap/validate.go 2018-05-16 08:20:08.000000000 +0000 @@ -534,13 +534,20 @@ return err } - // validate refresh-mode + // validate stop-mode + if err := app.StopMode.Validate(); err != nil { + return err + } + // validate stop-mode switch app.RefreshMode { - case "", "endure", "restart", "sigterm", "sigterm-all", "sighup", "sighup-all", "sigusr1", "sigusr1-all", "sigusr2", "sigusr2-all": + case "", "endure", "restart": // valid default: return fmt.Errorf(`"refresh-mode" field contains invalid value %q`, app.RefreshMode) } + if app.StopMode != "" && app.Daemon == "" { + return fmt.Errorf(`"stop-mode" cannot be used for %q, only for services`, app.Name) + } if app.RefreshMode != "" && app.Daemon == "" { return fmt.Errorf(`"refresh-mode" cannot be used for %q, only for services`, app.Name) } diff -Nru snapd-2.32.3.2/snap/validate_test.go snapd-2.32.9/snap/validate_test.go --- snapd-2.32.3.2/snap/validate_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/snap/validate_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -415,16 +415,14 @@ } } -func (s *ValidateSuite) TestAppRefreshMode(c *C) { +func (s *ValidateSuite) TestAppStopMode(c *C) { // check services for _, t := range []struct { - refresh string - ok bool + stopMode string + ok bool }{ // good {"", true}, - {"endure", true}, - {"restart", true}, {"sigterm", true}, {"sigterm-all", true}, {"sighup", true}, @@ -437,9 +435,34 @@ {"invalid-thing", false}, } { if t.ok { - c.Check(ValidateApp(&AppInfo{Name: "foo", Daemon: "simple", RefreshMode: t.refresh}), IsNil) + c.Check(ValidateApp(&AppInfo{Name: "foo", Daemon: "simple", StopMode: StopModeType(t.stopMode)}), IsNil) + } else { + c.Check(ValidateApp(&AppInfo{Name: "foo", Daemon: "simple", StopMode: StopModeType(t.stopMode)}), ErrorMatches, fmt.Sprintf(`"stop-mode" field contains invalid value %q`, t.stopMode)) + } + } + + // non-services cannot have a stop-mode + err := ValidateApp(&AppInfo{Name: "foo", Daemon: "", StopMode: "sigterm"}) + c.Check(err, ErrorMatches, `"stop-mode" cannot be used for "foo", only for services`) +} + +func (s *ValidateSuite) TestAppRefreshMode(c *C) { + // check services + for _, t := range []struct { + refreshMode string + ok bool + }{ + // good + {"", true}, + {"endure", true}, + {"restart", true}, + // bad + {"invalid-thing", false}, + } { + if t.ok { + c.Check(ValidateApp(&AppInfo{Name: "foo", Daemon: "simple", RefreshMode: t.refreshMode}), IsNil) } else { - c.Check(ValidateApp(&AppInfo{Name: "foo", Daemon: "simple", RefreshMode: t.refresh}), ErrorMatches, fmt.Sprintf(`"refresh-mode" field contains invalid value %q`, t.refresh)) + c.Check(ValidateApp(&AppInfo{Name: "foo", Daemon: "simple", RefreshMode: t.refreshMode}), ErrorMatches, fmt.Sprintf(`"refresh-mode" field contains invalid value %q`, t.refreshMode)) } } diff -Nru snapd-2.32.3.2/spread.yaml snapd-2.32.9/spread.yaml --- snapd-2.32.3.2/spread.yaml 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/spread.yaml 2018-05-16 08:20:08.000000000 +0000 @@ -44,45 +44,50 @@ PRE_CACHE_SNAPS: core ubuntu-core test-snapd-tools backends: - linode: - key: "$(HOST: echo $SPREAD_LINODE_KEY)" - plan: 4GB - location: Fremont - halt-timeout: 2h - environment: - # Using proxy can help to accelerate testing in local conditions - # but it is unlikely anyone has a proxy that is addressable from - # Linode network. As such, don't honor host's SPREAD_HTTP_PROXY - # that was set globally above. - HTTP_PROXY: null - HTTPS_PROXY: null + google: + key: "$(HOST: echo $SPREAD_GOOGLE_KEY)" + location: computeengine/us-east1-b systems: - ubuntu-16.04-64: - kernel: GRUB 2 workers: 8 - ubuntu-16.04-32: - kernel: GRUB 2 workers: 6 - ubuntu-14.04-64: - kernel: GRUB 2 workers: 6 - ubuntu-core-16-64: - kernel: Direct Disk image: ubuntu-16.04-64 workers: 6 - debian-9-64: - kernel: GRUB 2 workers: 6 - debian-sid-64: - kernel: GRUB 2 workers: 6 manual: true + - opensuse-42.2-64: + image: opensuse-leap-42-2 + workers: 4 + manual: true + - opensuse-42.3-64: + image: opensuse-cloud/opensuse-leap-42-3-v20180116 + workers: 4 + + linode: + key: "$(HOST: echo $SPREAD_LINODE_KEY)" + plan: 4GB + location: Fremont + halt-timeout: 2h + environment: + # Using proxy can help to accelerate testing in local conditions + # but it is unlikely anyone has a proxy that is addressable from + # Linode network. As such, don't honor host's SPREAD_HTTP_PROXY + # that was set globally above. + HTTP_PROXY: null + HTTPS_PROXY: null + systems: - fedora-27-64: workers: 4 - # Fedora CI disabled because of unexplained golang bug breaking every build. manual: true - fedora-26-64: workers: 4 @@ -91,8 +96,6 @@ workers: 4 manual: true - - opensuse-42.2-64: - workers: 4 qemu: systems: - ubuntu-14.04-32: @@ -120,6 +123,12 @@ - ubuntu-18.04-32: username: ubuntu password: ubuntu + - ubuntu-18.10-64: + username: ubuntu + password: ubuntu + - ubuntu-18.10-32: + username: ubuntu + password: ubuntu - debian-sid-64: username: debian password: debian @@ -206,6 +215,25 @@ - ubuntu-18.04-arm64: username: ubuntu password: ubuntu + # Cosmic + - ubuntu-18.10-amd64: + username: ubuntu + password: ubuntu + - ubuntu-18.10-i386: + username: ubuntu + password: ubuntu + - ubuntu-18.10-ppc64el: + username: ubuntu + password: ubuntu + - ubuntu-18.10-armhf: + username: ubuntu + password: ubuntu + - ubuntu-18.10-s390x: + username: ubuntu + password: ubuntu + - ubuntu-18.10-arm64: + username: ubuntu + password: ubuntu external: type: adhoc environment: @@ -334,6 +362,7 @@ dnf install --refresh -y xdelta curl &> "$tf" || (cat "$tf"; exit 1) ;; opensuse-*) + zypper -q --gpg-auto-import-keys refresh zypper -q install -y xdelta3 curl &> "$tf" || (cat "$tf"; exit 1) ;; esac diff -Nru snapd-2.32.3.2/store/details_v2.go snapd-2.32.9/store/details_v2.go --- snapd-2.32.3.2/store/details_v2.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/store/details_v2.go 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,177 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package store + +import ( + "fmt" + "strconv" + + "github.com/snapcore/snapd/snap" +) + +// storeSnap holds the information sent as JSON by the store for a snap. +type storeSnap struct { + Architectures []string `json:"architectures"` + Base string `json:"base"` + Confinement string `json:"confinement"` + Contact string `json:"contact"` + CreatedAt string `json:"created-at"` // revision timestamp + Description string `json:"description"` + Download storeSnapDownload `json:"download"` + Epoch snap.Epoch `json:"epoch"` + License string `json:"license"` + Name string `json:"name"` + Prices map[string]string `json:"prices"` // currency->price, free: {"USD": "0"} + Private bool `json:"private"` + Publisher storeAccount `json:"publisher"` + Revision int `json:"revision"` // store revisions are ints starting at 1 + SnapID string `json:"snap-id"` + SnapYAML string `json:"snap-yaml"` // optional + Summary string `json:"summary"` + Title string `json:"title"` + Type snap.Type `json:"type"` + Version string `json:"version"` + + // TODO: not yet defined: channel map + + // media + Media []storeSnapMedia `json:"media"` +} + +type storeSnapDownload struct { + Sha3_384 string `json:"sha3-384"` + Size int64 `json:"size"` + URL string `json:"url"` + Deltas []storeSnapDelta `json:"deltas"` +} + +type storeSnapDelta struct { + Format string `json:"format"` + Sha3_384 string `json:"sha3-384"` + Size int64 `json:"size"` + Source int `json:"source"` + Target int `json:"target"` + URL string `json:"url"` +} + +type storeAccount struct { + ID string `json:"id"` + Username string `json:"username"` + DisplayName string `json:"display-name"` +} + +type storeSnapMedia struct { + Type string `json:"type"` // icon/screenshot + URL string `json:"url"` + Width int64 `json:"width"` + Height int64 `json:"height"` +} + +func infoFromStoreSnap(d *storeSnap) (*snap.Info, error) { + info := &snap.Info{} + info.RealName = d.Name + info.Revision = snap.R(d.Revision) + info.SnapID = d.SnapID + info.EditedTitle = d.Title + info.EditedSummary = d.Summary + info.EditedDescription = d.Description + info.Private = d.Private + info.Contact = d.Contact + info.Architectures = d.Architectures + info.Type = d.Type + info.Version = d.Version + info.Epoch = d.Epoch + info.Confinement = snap.ConfinementType(d.Confinement) + info.Base = d.Base + info.License = d.License + info.PublisherID = d.Publisher.ID + info.Publisher = d.Publisher.Username + info.DownloadURL = d.Download.URL + info.Size = d.Download.Size + info.Sha3_384 = d.Download.Sha3_384 + if len(d.Download.Deltas) > 0 { + deltas := make([]snap.DeltaInfo, len(d.Download.Deltas)) + for i, d := range d.Download.Deltas { + deltas[i] = snap.DeltaInfo{ + FromRevision: d.Source, + ToRevision: d.Target, + Format: d.Format, + DownloadURL: d.URL, + Size: d.Size, + Sha3_384: d.Sha3_384, + } + } + info.Deltas = deltas + } + + // fill in the plug/slot data + if rawYamlInfo, err := snap.InfoFromSnapYaml([]byte(d.SnapYAML)); err == nil { + if info.Plugs == nil { + info.Plugs = make(map[string]*snap.PlugInfo) + } + for k, v := range rawYamlInfo.Plugs { + info.Plugs[k] = v + info.Plugs[k].Snap = info + } + if info.Slots == nil { + info.Slots = make(map[string]*snap.SlotInfo) + } + for k, v := range rawYamlInfo.Slots { + info.Slots[k] = v + info.Slots[k].Snap = info + } + } + + // convert prices + if len(d.Prices) > 0 { + prices := make(map[string]float64, len(d.Prices)) + for currency, priceStr := range d.Prices { + price, err := strconv.ParseFloat(priceStr, 64) + if err != nil { + return nil, fmt.Errorf("cannot parse snap price: %v", err) + } + prices[currency] = price + } + info.Paid = true + info.Prices = prices + } + + // media + screenshots := make([]snap.ScreenshotInfo, 0, len(d.Media)) + for _, mediaObj := range d.Media { + switch mediaObj.Type { + case "icon": + if info.IconURL == "" { + info.IconURL = mediaObj.URL + } + case "screenshot": + screenshots = append(screenshots, snap.ScreenshotInfo{ + URL: mediaObj.URL, + Width: mediaObj.Width, + Height: mediaObj.Height, + }) + } + } + if len(screenshots) > 0 { + info.Screenshots = screenshots + } + + return info, nil +} diff -Nru snapd-2.32.3.2/store/details_v2_test.go snapd-2.32.9/store/details_v2_test.go --- snapd-2.32.3.2/store/details_v2_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/store/details_v2_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,305 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package store + +import ( + "reflect" + + "encoding/json" + "strings" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" +) + +type detailsV2Suite struct { + testutil.BaseTest +} + +var _ = Suite(&detailsV2Suite{}) + +const ( + coreStoreJSON = `{ + "architectures": [ + "amd64" + ], + "base": null, + "confinement": "strict", + "contact": "mailto:snappy-canonical-storeaccount@canonical.com", + "created-at": "2018-01-22T07:49:19.440720+00:00", + "description": "The core runtime environment for snapd", + "download": { + "sha3-384": "b691f6dde3d8022e4db563840f0ef82320cb824b6292ffd027dbc838535214dac31c3512c619beaf73f1aeaf35ac62d5", + "size": 85291008, + "url": "https://api.snapcraft.io/api/v1/snaps/download/99T7MUlRhtI3U0QFgl5mXXESAiSwt776_3887.snap", + "deltas": [] + }, + "epoch": { + "read": [0], + "write": [0] + }, + "license": null, + "name": "core", + "prices": {}, + "private": false, + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical" + }, + "revision": 3887, + "snap-id": "99T7MUlRhtI3U0QFgl5mXXESAiSwt776", + "summary": "snapd runtime environment", + "title": "core", + "type": "os", + "version": "16-2.30", + "media": [] +}` + + thingyStoreJSON = `{ + "architectures": [ + "amd64" + ], + "base": "base-18", + "confinement": "strict", + "contact": "https://thingy.com", + "created-at": "2018-01-26T11:38:35.536410+00:00", + "description": "Useful thingy for thinging", + "download": { + "sha3-384": "a29f8d894c92ad19bb943764eb845c6bd7300f555ee9b9dbb460599fecf712775c0f3e2117b5c56b08fcb9d78fc8ae4d", + "size": 10000021, + "url": "https://api.snapcraft.io/api/v1/snaps/download/XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV_21.snap", + "deltas": [ + { + "format": "xdelta3", + "source": 19, + "target": 21, + "url": "https://api.snapcraft.io/api/v1/snaps/download/XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV_19_21_xdelta3.delta", + "size": 9999, + "sha3-384": "29f8d894c92ad19bb943764eb845c6bd7300f555ee9b9dbb460599fecf712775c0f3e2117b5c56b08fcb9d78fc8ae4df" + } + ] + }, + "epoch": { + "read": [0,1], + "write": [1] + }, + "license": "Proprietary", + "name": "thingy", + "prices": {"USD": "9.99"}, + "private": false, + "publisher": { + "id": "ZvtzsxbsHivZLdvzrt0iqW529riGLfXJ", + "username": "thingyinc", + "display-name": "Thingy Inc." + }, + "revision": 21, + "snap-id": "XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV", + "snap-yaml": "name: test-snapd-content-plug\nversion: 1.0\napps:\n content-plug:\n command: bin/content-plug\n plugs: [shared-content-plug]\nplugs:\n shared-content-plug:\n interface: content\n target: import\n content: mylib\n default-provider: test-snapd-content-slot\nslots:\n shared-content-slot:\n interface: content\n content: mylib\n read:\n - /\n", + "summary": "useful thingy", + "title": "thingy", + "type": "app", + "version": "9.50", + "media": [ + {"type": "icon", "url": "https://dashboard.snapcraft.io/site_media/appmedia/2017/12/Thingy.png"}, + {"type": "screenshot", "url": "https://dashboard.snapcraft.io/site_media/appmedia/2018/01/Thingy_01.png"}, + {"type": "screenshot", "url": "https://dashboard.snapcraft.io/site_media/appmedia/2018/01/Thingy_02.png", "width": 600, "height": 200} + ] +}` +) + +func (s *detailsV2Suite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) +} + +func (s *detailsV2Suite) TearDownTest(c *C) { + s.BaseTest.TearDownTest(c) +} + +func (s *detailsV2Suite) TestInfoFromStoreSnapSimple(c *C) { + var snp storeSnap + err := json.Unmarshal([]byte(coreStoreJSON), &snp) + c.Assert(err, IsNil) + + info, err := infoFromStoreSnap(&snp) + c.Assert(err, IsNil) + c.Check(snap.Validate(info), IsNil) + + c.Check(info, DeepEquals, &snap.Info{ + Architectures: []string{"amd64"}, + SideInfo: snap.SideInfo{ + RealName: "core", + SnapID: "99T7MUlRhtI3U0QFgl5mXXESAiSwt776", + Revision: snap.R(3887), + Contact: "mailto:snappy-canonical-storeaccount@canonical.com", + EditedTitle: "core", + EditedSummary: "snapd runtime environment", + EditedDescription: "The core runtime environment for snapd", + Private: false, + Paid: false, + }, + Epoch: *snap.E("0"), + Type: snap.TypeOS, + Version: "16-2.30", + Confinement: snap.StrictConfinement, + PublisherID: "canonical", + Publisher: "canonical", + DownloadInfo: snap.DownloadInfo{ + DownloadURL: "https://api.snapcraft.io/api/v1/snaps/download/99T7MUlRhtI3U0QFgl5mXXESAiSwt776_3887.snap", + Sha3_384: "b691f6dde3d8022e4db563840f0ef82320cb824b6292ffd027dbc838535214dac31c3512c619beaf73f1aeaf35ac62d5", + Size: 85291008, + }, + Plugs: make(map[string]*snap.PlugInfo), + Slots: make(map[string]*snap.SlotInfo), + }) +} + +func (s *detailsV2Suite) TestInfoFromStoreSnap(c *C) { + var snp storeSnap + // base, prices, media + err := json.Unmarshal([]byte(thingyStoreJSON), &snp) + c.Assert(err, IsNil) + + info, err := infoFromStoreSnap(&snp) + c.Assert(err, IsNil) + c.Check(snap.Validate(info), IsNil) + + info2 := *info + // clear recursive bits + info2.Plugs = nil + info2.Slots = nil + c.Check(&info2, DeepEquals, &snap.Info{ + Architectures: []string{"amd64"}, + Base: "base-18", + SideInfo: snap.SideInfo{ + RealName: "thingy", + SnapID: "XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV", + Revision: snap.R(21), + Contact: "https://thingy.com", + EditedTitle: "thingy", + EditedSummary: "useful thingy", + EditedDescription: "Useful thingy for thinging", + Private: false, + Paid: true, + }, + Epoch: snap.Epoch{ + Read: []uint32{0, 1}, + Write: []uint32{1}, + }, + Type: snap.TypeApp, + Version: "9.50", + Confinement: snap.StrictConfinement, + License: "Proprietary", + PublisherID: "ZvtzsxbsHivZLdvzrt0iqW529riGLfXJ", + Publisher: "thingyinc", + DownloadInfo: snap.DownloadInfo{ + DownloadURL: "https://api.snapcraft.io/api/v1/snaps/download/XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV_21.snap", + Sha3_384: "a29f8d894c92ad19bb943764eb845c6bd7300f555ee9b9dbb460599fecf712775c0f3e2117b5c56b08fcb9d78fc8ae4d", + Size: 10000021, + Deltas: []snap.DeltaInfo{ + { + Format: "xdelta3", + FromRevision: 19, + ToRevision: 21, + DownloadURL: "https://api.snapcraft.io/api/v1/snaps/download/XYZEfjn4WJYnm0FzDKwqqRZZI77awQEV_19_21_xdelta3.delta", + Size: 9999, + Sha3_384: "29f8d894c92ad19bb943764eb845c6bd7300f555ee9b9dbb460599fecf712775c0f3e2117b5c56b08fcb9d78fc8ae4df", + }, + }, + }, + Prices: map[string]float64{ + "USD": 9.99, + }, + IconURL: "https://dashboard.snapcraft.io/site_media/appmedia/2017/12/Thingy.png", + Screenshots: []snap.ScreenshotInfo{ + {URL: "https://dashboard.snapcraft.io/site_media/appmedia/2018/01/Thingy_01.png"}, + {URL: "https://dashboard.snapcraft.io/site_media/appmedia/2018/01/Thingy_02.png", Width: 600, Height: 200}, + }, + }) + + // validate the plugs/slots + c.Assert(info.Plugs, HasLen, 1) + plug := info.Plugs["shared-content-plug"] + c.Check(plug.Name, Equals, "shared-content-plug") + c.Check(plug.Snap, Equals, info) + c.Check(plug.Apps, HasLen, 1) + c.Check(plug.Apps["content-plug"].Command, Equals, "bin/content-plug") + + c.Assert(info.Slots, HasLen, 1) + slot := info.Slots["shared-content-slot"] + c.Check(slot.Name, Equals, "shared-content-slot") + c.Check(slot.Snap, Equals, info) + c.Check(slot.Apps, HasLen, 1) + c.Check(slot.Apps["content-plug"].Command, Equals, "bin/content-plug") + + // private + err = json.Unmarshal([]byte(strings.Replace(thingyStoreJSON, `"private": false`, `"private": true`, 1)), &snp) + c.Assert(err, IsNil) + + info, err = infoFromStoreSnap(&snp) + c.Assert(err, IsNil) + c.Check(snap.Validate(info), IsNil) + + c.Check(info.Private, Equals, true) + + // check that up to few exceptions info is filled + expectedZeroFields := []string{ + "SuggestedName", + "Assumes", + "OriginalTitle", + "OriginalSummary", + "OriginalDescription", + "Environment", + "LicenseAgreement", // XXX go away? + "LicenseVersion", // XXX go away? + "Apps", + "LegacyAliases", + "Hooks", + "BadInterfaces", + "Broken", + "MustBuy", + "Channels", // TODO: support coming later + "Tracks", // TODO: support coming later + "Layout", + "SideInfo.Channel", + "DownloadInfo.AnonDownloadURL", // TODO: going away at some point + } + var checker func(string, reflect.Value) + checker = func(pfx string, x reflect.Value) { + t := x.Type() + for i := 0; i < x.NumField(); i++ { + f := t.Field(i) + v := x.Field(i) + if f.Anonymous { + checker(pfx+f.Name+".", v) + continue + } + if reflect.DeepEqual(v.Interface(), reflect.Zero(f.Type).Interface()) { + name := pfx + f.Name + c.Check(expectedZeroFields, testutil.Contains, name, Commentf("%s not set", name)) + } + } + } + x := reflect.ValueOf(info).Elem() + checker("", x) +} diff -Nru snapd-2.32.3.2/store/errors.go snapd-2.32.9/store/errors.go --- snapd-2.32.3.2/store/errors.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/store/errors.go 2018-05-16 08:20:08.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2015 Canonical Ltd + * Copyright (C) 2014-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -61,6 +61,9 @@ // ErrNoUpdateAvailable is returned when an update is attempetd for a snap that has no update available. ErrNoUpdateAvailable = errors.New("snap has no updates available") + + // ErrRevisionNotAvailable is returned when an install is attempted for a snap but the/a revision is not available (given install constraints) + ErrRevisionNotAvailable = errors.New("no snap revision given constraints") ) // DownloadError represents a download error @@ -107,3 +110,86 @@ // (empirically this checks out) return strings.Join(es, " ") } + +// SnapActionError conveys errors that were reported on otherwise overall successful snap action (install/refresh) request. +type SnapActionError struct { + // NoResults is set if the there were no results in the response + NoResults bool + // Refresh errors by snap name. + Refresh map[string]error + // Install errors by snap name. + Install map[string]error + // Other errors. + Other []error +} + +func (e SnapActionError) Error() string { + nRefresh := len(e.Refresh) + nInstall := len(e.Install) + nOther := len(e.Other) + + // single error + if nRefresh+nInstall+nOther == 1 { + if nOther == 0 { + op := "refresh" + errs := e.Refresh + if nInstall > 0 { + op = "install" + errs = e.Install + } + for name, e := range errs { + return fmt.Sprintf("cannot %s snap %q: %v", op, name, e) + } + } else { + return fmt.Sprintf("cannot refresh or install: %v", e.Other[0]) + } + } + + header := "cannot refresh:" + if nInstall > 0 { + header = "cannot install:" + } + if nOther > 0 || (nInstall > 0 && nRefresh > 0) { + header = "cannot refresh or install:" + } + es := []string{header} + + for name, e := range e.Refresh { + es = append(es, fmt.Sprintf("snap %q: %v", name, e)) + } + + for name, e := range e.Install { + es = append(es, fmt.Sprintf("snap %q: %v", name, e)) + } + + for _, e := range e.Other { + es = append(es, fmt.Sprintf("* %v", e)) + } + + if e.NoResults && len(es) == 1 { + // this is an atypical result + return "no install/refresh information results from the store" + } + return strings.Join(es, "\n") +} + +// Authorization soft-expiry errors that get handled automatically. +var ( + errUserAuthorizationNeedsRefresh = errors.New("soft-expired user authorization needs refresh") + errDeviceAuthorizationNeedsRefresh = errors.New("soft-expired device authorization needs refresh") +) + +func translateSnapActionError(action, code, message string) error { + switch code { + case "revision-not-found": + return ErrRevisionNotAvailable + case "id-not-found", "name-not-found": + return ErrSnapNotFound + case "user-authorization-needs-refresh": + return errUserAuthorizationNeedsRefresh + case "device-authorization-needs-refresh": + return errDeviceAuthorizationNeedsRefresh + default: + return fmt.Errorf("%v", message) + } +} diff -Nru snapd-2.32.3.2/store/store.go snapd-2.32.9/store/store.go --- snapd-2.32.3.2/store/store.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/store/store.go 2018-05-16 08:20:08.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2017 Canonical Ltd + * Copyright (C) 2014-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -333,6 +333,7 @@ func storeURL(api *url.URL) (*url.URL, error) { var override string var overrideName string + // XXX: time to drop FORCE_CPI support // XXX: Deprecated but present for backward-compatibility: this used // to be "Click Package Index". Remove this once people have got // used to SNAPPY_FORCE_API_URL instead. @@ -489,6 +490,8 @@ customersMeEndpPath = "api/v1/snaps/purchases/customers/me" sectionsEndpPath = "api/v1/snaps/sections" commandsEndpPath = "api/v1/snaps/names" + // v2 + snapActionEndpPath = "v2/snaps/refresh" deviceNonceEndpPath = "api/v1/snaps/auth/nonces" deviceSessionEndpPath = "api/v1/snaps/auth/sessions" @@ -693,13 +696,13 @@ } // authenticateDevice will add the store expected Macaroon X-Device-Authorization header for device -func authenticateDevice(r *http.Request, device *auth.DeviceState) { +func authenticateDevice(r *http.Request, device *auth.DeviceState, apiLevel apiLevel) { if device.SessionMacaroon != "" { - r.Header.Set("X-Device-Authorization", fmt.Sprintf(`Macaroon root="%s"`, device.SessionMacaroon)) + r.Header.Set(hdrSnapDeviceAuthorization[apiLevel], fmt.Sprintf(`Macaroon root="%s"`, device.SessionMacaroon)) } } -func (s *Store) setStoreID(r *http.Request) (customStore bool) { +func (s *Store) setStoreID(r *http.Request, apiLevel apiLevel) (customStore bool) { storeID := s.fallbackStoreID if s.authContext != nil { cand, err := s.authContext.StoreID(storeID) @@ -710,12 +713,27 @@ } } if storeID != "" { - r.Header.Set("X-Ubuntu-Store", storeID) + r.Header.Set(hdrSnapDeviceStore[apiLevel], storeID) return true } return false } +type apiLevel int + +const ( + apiV1Endps apiLevel = 0 // api/v1 endpoints + apiV2Endps apiLevel = 1 // v2 endpoints +) + +var ( + hdrSnapDeviceAuthorization = []string{"X-Device-Authorization", "Snap-Device-Authorization"} + hdrSnapDeviceStore = []string{"X-Ubuntu-Store", "Snap-Device-Store"} + hdrSnapDeviceSeries = []string{"X-Ubuntu-Series", "Snap-Device-Series"} + hdrSnapDeviceArchitecture = []string{"X-Ubuntu-Architecture", "Snap-Device-Architecture"} + hdrSnapClassic = []string{"X-Ubuntu-Classic", "Snap-Classic"} +) + type deviceAuthNeed int const ( @@ -729,6 +747,7 @@ URL *url.URL Accept string ContentType string + APILevel apiLevel ExtraHeaders map[string]string Data []byte @@ -874,18 +893,15 @@ // 4 tries: 2 tries for each in case both user // and device need refreshing var refreshNeed authRefreshNeed - refresh := false if user != nil && strings.Contains(wwwAuth, "needs_refresh=1") { // refresh user refreshNeed.user = true - refresh = true } if strings.Contains(wwwAuth, "refresh_device_session=1") { // refresh device session refreshNeed.device = true - refresh = true } - if refresh { + if refreshNeed.needed() { err := s.refreshAuth(user, refreshNeed) if err != nil { return nil, err @@ -901,6 +917,41 @@ } } +type authRefreshNeed struct { + device bool + user bool +} + +func (rn *authRefreshNeed) needed() bool { + return rn.device || rn.user +} + +func (s *Store) refreshAuth(user *auth.UserState, need authRefreshNeed) error { + if need.user { + // refresh user + err := s.refreshUser(user) + if err != nil { + return err + } + } + if need.device { + // refresh device session + if s.authContext == nil { + return fmt.Errorf("internal error: no authContext") + } + device, err := s.authContext.Device() + if err != nil { + return err + } + + err = s.refreshDeviceSession(device) + if err != nil { + return err + } + } + return nil +} + // build a new http.Request with headers for the store func (s *Store) newRequest(reqOptions *requestOptions, user *auth.UserState) (*http.Request, error) { var body io.Reader @@ -913,7 +964,7 @@ return nil, err } - customStore := s.setStoreID(req) + customStore := s.setStoreID(req, reqOptions.APILevel) if s.authContext != nil && (customStore || reqOptions.DeviceAuthNeed != deviceAuthCustomStoreOnly) { device, err := s.authContext.Device() @@ -932,7 +983,7 @@ return nil, err } } - authenticateDevice(req, device) + authenticateDevice(req, device, reqOptions.APILevel) } // only set user authentication if user logged in to the store @@ -942,16 +993,11 @@ req.Header.Set("User-Agent", httputil.UserAgent()) req.Header.Set("Accept", reqOptions.Accept) - req.Header.Set("X-Ubuntu-Architecture", s.architecture) - req.Header.Set("X-Ubuntu-Series", s.series) - req.Header.Set("X-Ubuntu-Classic", strconv.FormatBool(release.OnClassic)) - req.Header.Set("X-Ubuntu-Wire-Protocol", UbuntuCoreWireProtocol) - // still send this for now - req.Header.Set("X-Ubuntu-No-CDN", strconv.FormatBool(s.noCDN)) - // TODO: do this only for download - err = s.cdnHeader(req, reqOptions) - if err != nil { - return nil, err + req.Header.Set(hdrSnapDeviceArchitecture[reqOptions.APILevel], s.architecture) + req.Header.Set(hdrSnapDeviceSeries[reqOptions.APILevel], s.series) + req.Header.Set(hdrSnapClassic[reqOptions.APILevel], strconv.FormatBool(release.OnClassic)) + if reqOptions.APILevel == apiV1Endps { + req.Header.Set("X-Ubuntu-Wire-Protocol", UbuntuCoreWireProtocol) } if reqOptions.ContentType != "" { @@ -965,14 +1011,13 @@ return req, nil } -func (s *Store) cdnHeader(req *http.Request, reqOptions *requestOptions) error { +func (s *Store) cdnHeader() (string, error) { if s.noCDN { - req.Header.Set("Snap-CDN", "none") - return nil + return "none", nil } if s.authContext == nil { - return nil + return "", nil } // set Snap-CDN from cloud instance information @@ -985,7 +1030,7 @@ cloudInfo, err := s.authContext.CloudInfo() if err != nil { - return err + return "", err } if cloudInfo != nil { @@ -997,40 +1042,10 @@ cdnParams = append(cdnParams, fmt.Sprintf("availability-zone=%q", cloudInfo.AvailabilityZone)) } - req.Header.Set("Snap-CDN", strings.Join(cdnParams, " ")) + return strings.Join(cdnParams, " "), nil } - return nil -} - -type authRefreshNeed struct { - device bool - user bool -} - -func (s *Store) refreshAuth(user *auth.UserState, need authRefreshNeed) error { - if need.user { - // refresh user - err := s.refreshUser(user) - if err != nil { - return err - } - } - if need.device { - // refresh device session - if s.authContext == nil { - return fmt.Errorf("internal error: no authContext") - } - device, err := s.authContext.Device() - if err != nil { - return err - } - err = s.refreshDeviceSession(device) - if err != nil { - return err - } - } - return nil + return "", nil } func (s *Store) extractSuggestedCurrency(resp *http.Response) { @@ -1674,21 +1689,29 @@ return err } + cdnHeader, err := s.cdnHeader() + if err != nil { + return err + } + var finalErr error startTime := time.Now() for attempt := retry.Start(defaultRetryStrategy, nil); attempt.Next(); { reqOptions := &requestOptions{ - Method: "GET", - URL: storeURL, + Method: "GET", + URL: storeURL, + ExtraHeaders: make(map[string]string), + } + if cdnHeader != "" { + reqOptions.ExtraHeaders["Snap-CDN"] = cdnHeader } + httputil.MaybeLogRetryAttempt(reqOptions.URL.String(), attempt, startTime) h := crypto.SHA3_384.New() if resume > 0 { - reqOptions.ExtraHeaders = map[string]string{ - "Range": fmt.Sprintf("bytes=%d-", resume), - } + reqOptions.ExtraHeaders["Range"] = fmt.Sprintf("bytes=%d-", resume) // seed the sha3 with the already local file if _, err := w.Seek(0, os.SEEK_SET); err != nil { return err @@ -2143,3 +2166,296 @@ s.cacher = &nullCache{} } } + +// snap action: install/refresh + +type CurrentSnap struct { + Name string + SnapID string + Revision snap.Revision + TrackingChannel string + RefreshedDate time.Time + IgnoreValidation bool + Block []snap.Revision +} + +type currentSnapV2JSON struct { + SnapID string `json:"snap-id"` + InstanceKey string `json:"instance-key"` + Revision int `json:"revision"` + TrackingChannel string `json:"tracking-channel"` + RefreshedDate *time.Time `json:"refreshed-date,omitempty"` + IgnoreValidation bool `json:"ignore-validation,omitempty"` +} + +type SnapActionFlags int + +const ( + SnapActionIgnoreValidation SnapActionFlags = 1 << iota + SnapActionEnforceValidation +) + +type SnapAction struct { + Action string + Name string + SnapID string + Channel string + Revision snap.Revision + Flags SnapActionFlags + Epoch *snap.Epoch +} + +type snapActionJSON struct { + Action string `json:"action"` + InstanceKey string `json:"instance-key"` + Name string `json:"name,omitempty"` + SnapID string `json:"snap-id,omitempty"` + Channel string `json:"channel,omitempty"` + Revision int `json:"revision,omitempty"` + Epoch *snap.Epoch `json:"epoch,omitempty"` + IgnoreValidation *bool `json:"ignore-validation,omitempty"` +} + +type snapActionResult struct { + Result string `json:"result"` + InstanceKey string `json:"instance-key"` + SnapID string `json:"snap-id,omitempy"` + Name string `json:"name,omitempty"` + Snap storeSnap `json:"snap"` + EffectiveChannel string `json:"effective-channel,omitempty"` + Error struct { + Code string `json:"code"` + Message string `json:"message"` + } `json:"error"` +} + +type snapActionRequest struct { + Context []*currentSnapV2JSON `json:"context"` + Actions []*snapActionJSON `json:"actions"` + Fields []string `json:"fields"` +} + +type snapActionResultList struct { + Results []*snapActionResult `json:"results"` + ErrorList []struct { + Code string `json:"code"` + Message string `json:"message"` + } `json:"error-list"` +} + +var snapActionFields = getStructFields(storeSnap{}) + +// SnapAction queries the store for snap information for the given +// install/refresh actions, given the context information about +// current installed snaps in currentSnaps. If the request was overall +// successul (200) but there were reported errors it will return both +// the snap infos and an SnapActionError. +func (s *Store) SnapAction(ctx context.Context, currentSnaps []*CurrentSnap, actions []*SnapAction, user *auth.UserState, opts *RefreshOptions) ([]*snap.Info, error) { + if opts == nil { + opts = &RefreshOptions{} + } + + if len(currentSnaps) == 0 && len(actions) == 0 { + // nothing to do + return nil, &SnapActionError{NoResults: true} + } + + authRefreshes := 0 + for { + snaps, err := s.snapAction(ctx, currentSnaps, actions, user, opts) + + if saErr, ok := err.(*SnapActionError); ok && authRefreshes < 2 && len(saErr.Other) > 0 { + // do we need to try to refresh auths?, 2 tries + var refreshNeed authRefreshNeed + for _, otherErr := range saErr.Other { + switch otherErr { + case errUserAuthorizationNeedsRefresh: + refreshNeed.user = true + case errDeviceAuthorizationNeedsRefresh: + refreshNeed.device = true + } + } + if refreshNeed.needed() { + err := s.refreshAuth(user, refreshNeed) + if err != nil { + // best effort + logger.Noticef("cannot refresh soft-expired authorisation: %v", err) + } + authRefreshes++ + // TODO: we could avoid retrying here + // if refreshAuth gave no error we got + // as many non-error results from the + // store as actions anyway + continue + } + } + + return snaps, err + } +} + +func (s *Store) snapAction(ctx context.Context, currentSnaps []*CurrentSnap, actions []*SnapAction, user *auth.UserState, opts *RefreshOptions) ([]*snap.Info, error) { + + // TODO: the store already requires instance-key but doesn't + // yet support repeating in context or sending actions for the + // same snap-id, for now we keep instance-key handling internal + + curSnaps := make(map[string]*CurrentSnap, len(currentSnaps)) + curSnapJSONs := make([]*currentSnapV2JSON, len(currentSnaps)) + for i, curSnap := range currentSnaps { + if curSnap.SnapID == "" || curSnap.Name == "" || curSnap.Revision.Unset() { + return nil, fmt.Errorf("internal error: invalid current snap information") + } + curSnaps[curSnap.SnapID] = curSnap + channel := curSnap.TrackingChannel + if channel == "" { + channel = "stable" + } + var refreshedDate *time.Time + if !curSnap.RefreshedDate.IsZero() { + refreshedDate = &curSnap.RefreshedDate + } + curSnapJSONs[i] = ¤tSnapV2JSON{ + SnapID: curSnap.SnapID, + InstanceKey: curSnap.SnapID, + Revision: curSnap.Revision.N, + TrackingChannel: channel, + IgnoreValidation: curSnap.IgnoreValidation, + RefreshedDate: refreshedDate, + } + } + + installNum := 0 + installKeys := make(map[string]bool, len(actions)) + actionJSONs := make([]*snapActionJSON, len(actions)) + for i, a := range actions { + var ignoreValidation *bool + if a.Flags&SnapActionIgnoreValidation != 0 { + var t = true + ignoreValidation = &t + } else if a.Flags&SnapActionEnforceValidation != 0 { + var f = false + ignoreValidation = &f + } + + instanceKey := a.SnapID + if a.Action == "install" { + installNum++ + instanceKey = fmt.Sprintf("install-%d", installNum) + installKeys[instanceKey] = true + } + + aJSON := &snapActionJSON{ + Action: a.Action, + InstanceKey: instanceKey, + SnapID: a.SnapID, + Name: a.Name, + Channel: a.Channel, + Revision: a.Revision.N, + Epoch: a.Epoch, + IgnoreValidation: ignoreValidation, + } + actionJSONs[i] = aJSON + } + + // build input for the install/refresh endpoint + jsonData, err := json.Marshal(snapActionRequest{ + Context: curSnapJSONs, + Actions: actionJSONs, + Fields: snapActionFields, + }) + if err != nil { + return nil, err + } + + reqOptions := &requestOptions{ + Method: "POST", + URL: s.endpointURL(snapActionEndpPath, nil), + Accept: jsonContentType, + ContentType: jsonContentType, + Data: jsonData, + APILevel: apiV2Endps, + } + + if useDeltas() { + logger.Debugf("Deltas enabled. Adding header Snap-Accept-Delta-Format: %v", s.deltaFormat) + reqOptions.addHeader("Snap-Accept-Delta-Format", s.deltaFormat) + } + if opts.RefreshManaged { + reqOptions.addHeader("Snap-Refresh-Managed", "true") + } + + var results snapActionResultList + resp, err := s.retryRequestDecodeJSON(ctx, reqOptions, user, &results, nil) + if err != nil { + return nil, err + } + + if resp.StatusCode != 200 { + return nil, respToError(resp, "query the store for updates") + } + + s.extractSuggestedCurrency(resp) + + refreshErrors := make(map[string]error) + installErrors := make(map[string]error) + var otherErrors []error + + var snaps []*snap.Info + for _, res := range results.Results { + if res.Result == "error" { + if installKeys[res.InstanceKey] { + if res.Name != "" { + installErrors[res.Name] = translateSnapActionError("install", res.Error.Code, res.Error.Message) + continue + } + } else { + if cur := curSnaps[res.InstanceKey]; cur != nil { + refreshErrors[cur.Name] = translateSnapActionError("refresh", res.Error.Code, res.Error.Message) + continue + } + } + otherErrors = append(otherErrors, translateSnapActionError("-", res.Error.Code, res.Error.Message)) + continue + } + snapInfo, err := infoFromStoreSnap(&res.Snap) + if err != nil { + return nil, fmt.Errorf("unexpected invalid install/refresh API result: %v", err) + } + snapInfo.Channel = res.EffectiveChannel + if res.Result == "refresh" { + cur := curSnaps[res.SnapID] + if cur == nil { + return nil, fmt.Errorf("unexpected invalid install/refresh API result: unexpected refresh") + } + rrev := snap.R(res.Snap.Revision) + if rrev == cur.Revision || findRev(rrev, cur.Block) { + refreshErrors[cur.Name] = ErrNoUpdateAvailable + continue + } + } + snaps = append(snaps, snapInfo) + } + + for _, errObj := range results.ErrorList { + otherErrors = append(otherErrors, translateSnapActionError("-", errObj.Code, errObj.Message)) + } + + if len(refreshErrors)+len(installErrors) != 0 || len(results.Results) == 0 || len(otherErrors) != 0 { + // normalize empty maps + if len(refreshErrors) == 0 { + refreshErrors = nil + } + if len(installErrors) == 0 { + installErrors = nil + } + return snaps, &SnapActionError{ + NoResults: len(results.Results) == 0, + Refresh: refreshErrors, + Install: installErrors, + Other: otherErrors, + } + } + + return snaps, nil +} diff -Nru snapd-2.32.3.2/store/storetest/storetest.go snapd-2.32.9/store/storetest/storetest.go --- snapd-2.32.3.2/store/storetest/storetest.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/store/storetest/storetest.go 2018-05-16 08:20:08.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2017 Canonical Ltd + * Copyright (C) 2014-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -57,6 +57,10 @@ panic("Store.ListRefresh not expected") } +func (Store) SnapAction(context.Context, []*store.CurrentSnap, []*store.SnapAction, *auth.UserState, *store.RefreshOptions) ([]*snap.Info, error) { + panic("Store.SnapAction not expected") +} + func (Store) Download(context.Context, string, string, *snap.DownloadInfo, progress.Meter, *auth.UserState) error { panic("Store.Download not expected") } diff -Nru snapd-2.32.3.2/store/store_test.go snapd-2.32.9/store/store_test.go --- snapd-2.32.3.2/store/store_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/store/store_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -131,6 +131,8 @@ ordersPath = "/api/v1/snaps/purchases/orders" searchPath = "/api/v1/snaps/search" sectionsPath = "/api/v1/snaps/sections" + // v2 + snapActionPath = "/v2/snaps/refresh" ) // Build details path for a snap name. @@ -816,6 +818,7 @@ func (s *storeTestSuite) TestActualDownload(c *C) { n := 0 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Header.Get("Snap-CDN"), Equals, "") n++ io.WriteString(w, "response-data") })) @@ -832,6 +835,64 @@ c.Check(n, Equals, 1) } +func (s *storeTestSuite) TestActualDownloadNoCDN(c *C) { + os.Setenv("SNAPPY_STORE_NO_CDN", "1") + defer os.Unsetenv("SNAPPY_STORE_NO_CDN") + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Header.Get("Snap-CDN"), Equals, "none") + io.WriteString(w, "response-data") + })) + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + theStore := New(&Config{}, nil) + var buf SillyBuffer + // keep tests happy + sha3 := "" + err := download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil) + c.Assert(err, IsNil) + c.Check(buf.String(), Equals, "response-data") +} + +func (s *storeTestSuite) TestActualDownloadFullCloudInfoFromAuthContext(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Header.Get("Snap-CDN"), Equals, `cloud-name="aws" region="us-east-1" availability-zone="us-east-1c"`) + + io.WriteString(w, "response-data") + })) + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + theStore := New(&Config{}, &testAuthContext{c: c, device: s.device, cloudInfo: &auth.CloudInfo{Name: "aws", Region: "us-east-1", AvailabilityZone: "us-east-1c"}}) + + var buf SillyBuffer + // keep tests happy + sha3 := "" + err := download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil) + c.Assert(err, IsNil) + c.Check(buf.String(), Equals, "response-data") +} + +func (s *storeTestSuite) TestActualDownloadLessDetailedCloudInfoFromAuthContext(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Header.Get("Snap-CDN"), Equals, `cloud-name="openstack" availability-zone="nova"`) + + io.WriteString(w, "response-data") + })) + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + theStore := New(&Config{}, &testAuthContext{c: c, device: s.device, cloudInfo: &auth.CloudInfo{Name: "openstack", Region: "", AvailabilityZone: "nova"}}) + + var buf SillyBuffer + // keep tests happy + sha3 := "" + err := download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil) + c.Assert(err, IsNil) + c.Check(buf.String(), Equals, "response-data") +} + func (s *storeTestSuite) TestDownloadCancellation(c *C) { // the channel used by mock server to request cancellation from the test syncCh := make(chan struct{}) @@ -2303,8 +2364,6 @@ func (s *storeTestSuite) TestNonDefaults(c *C) { restore := release.MockOnClassic(true) defer restore() - os.Setenv("SNAPPY_STORE_NO_CDN", "1") - defer os.Unsetenv("SNAPPY_STORE_NO_CDN") mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assertRequest(c, r, "GET", detailsPathPattern) @@ -2318,9 +2377,6 @@ c.Check(r.Header.Get("X-Ubuntu-Series"), Equals, "21") c.Check(r.Header.Get("X-Ubuntu-Architecture"), Equals, "archXYZ") c.Check(r.Header.Get("X-Ubuntu-Classic"), Equals, "true") - // for now we have both - c.Check(r.Header.Get("X-Ubuntu-No-CDN"), Equals, "true") - c.Check(r.Header.Get("Snap-CDN"), Equals, "none") w.WriteHeader(200) io.WriteString(w, MockDetailsJSON) @@ -2380,68 +2436,6 @@ c.Check(result.Name(), Equals, "hello-world") } -func (s *storeTestSuite) TestFullCloudInfoFromAuthContext(c *C) { - mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assertRequest(c, r, "GET", detailsPathPattern) - c.Check(r.Header.Get("Snap-CDN"), Equals, `cloud-name="aws" region="us-east-1" availability-zone="us-east-1c"`) - - w.WriteHeader(200) - io.WriteString(w, MockDetailsJSON) - })) - - c.Assert(mockServer, NotNil) - defer mockServer.Close() - - mockServerURL, _ := url.Parse(mockServer.URL) - cfg := DefaultConfig() - cfg.StoreBaseURL = mockServerURL - cfg.Series = "21" - cfg.Architecture = "archXYZ" - cfg.StoreID = "fallback" - sto := New(cfg, &testAuthContext{c: c, device: s.device, cloudInfo: &auth.CloudInfo{Name: "aws", Region: "us-east-1", AvailabilityZone: "us-east-1c"}}) - - // the actual test - spec := SnapSpec{ - Name: "hello-world", - Channel: "edge", - Revision: snap.R(0), - } - result, err := sto.SnapInfo(spec, nil) - c.Assert(err, IsNil) - c.Check(result.Name(), Equals, "hello-world") -} - -func (s *storeTestSuite) TestLessDetailedCloudInfoFromAuthContext(c *C) { - mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assertRequest(c, r, "GET", detailsPathPattern) - c.Check(r.Header.Get("Snap-CDN"), Equals, `cloud-name="openstack" availability-zone="nova"`) - - w.WriteHeader(200) - io.WriteString(w, MockDetailsJSON) - })) - - c.Assert(mockServer, NotNil) - defer mockServer.Close() - - mockServerURL, _ := url.Parse(mockServer.URL) - cfg := DefaultConfig() - cfg.StoreBaseURL = mockServerURL - cfg.Series = "21" - cfg.Architecture = "archXYZ" - cfg.StoreID = "fallback" - sto := New(cfg, &testAuthContext{c: c, device: s.device, cloudInfo: &auth.CloudInfo{Name: "openstack", Region: "", AvailabilityZone: "nova"}}) - - // the actual test - spec := SnapSpec{ - Name: "hello-world", - Channel: "edge", - Revision: snap.R(0), - } - result, err := sto.SnapInfo(spec, nil) - c.Assert(err, IsNil) - c.Check(result.Name(), Equals, "hello-world") -} - func (s *storeTestSuite) TestProxyStoreFromAuthContext(c *C) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assertRequest(c, r, "GET", detailsPathPattern) @@ -5361,3 +5355,1566 @@ c.Check(obs.gets, DeepEquals, []string{fmt.Sprintf("the-snaps-sha3_384:%s", path)}) c.Check(obs.puts, DeepEquals, []string{fmt.Sprintf("the-snaps-sha3_384:%s", path)}) } + +var ( + helloRefreshedDateStr = "2018-02-27T11:00:00Z" + helloRefreshedDate time.Time +) + +func init() { + t, err := time.Parse(time.RFC3339, helloRefreshedDateStr) + if err != nil { + panic(err) + } + helloRefreshedDate = t +} + +func (s *storeTestSuite) TestSnapAction(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(c, r, "POST", snapActionPath) + // check device authorization is set, implicitly checking doRequest was used + c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) + + c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "") + + // no store ID by default + storeID := r.Header.Get("Snap-Device-Store") + c.Check(storeID, Equals, "") + + c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series) + c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.UbuntuArchitecture()) + c.Check(r.Header.Get("Snap-Classic"), Equals, "false") + + jsonReq, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + var req struct { + Context []map[string]interface{} `json:"context"` + Fields []string `json:"fields"` + Actions []map[string]interface{} `json:"actions"` + } + + err = json.Unmarshal(jsonReq, &req) + c.Assert(err, IsNil) + + c.Check(req.Fields, DeepEquals, snapActionFields) + + c.Assert(req.Context, HasLen, 1) + c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ + "snap-id": helloWorldSnapID, + "instance-key": helloWorldSnapID, + "revision": float64(1), + "tracking-channel": "beta", + "refreshed-date": helloRefreshedDateStr, + }) + c.Assert(req.Actions, HasLen, 1) + c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ + "action": "refresh", + "instance-key": helloWorldSnapID, + "snap-id": helloWorldSnapID, + }) + + io.WriteString(w, `{ + "results": [{ + "result": "refresh", + "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "snap": { + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "revision": 26, + "version": "6.1", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical" + } + } + }] +}`) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + cfg := Config{ + StoreBaseURL: mockServerURL, + } + authContext := &testAuthContext{c: c, device: s.device} + sto := New(&cfg, authContext) + + results, err := sto.SnapAction(context.TODO(), []*CurrentSnap{ + { + Name: "hello-world", + SnapID: helloWorldSnapID, + TrackingChannel: "beta", + Revision: snap.R(1), + RefreshedDate: helloRefreshedDate, + }, + }, []*SnapAction{ + { + Action: "refresh", + SnapID: helloWorldSnapID, + }, + }, nil, nil) + c.Assert(err, IsNil) + c.Assert(results, HasLen, 1) + c.Assert(results[0].Name(), Equals, "hello-world") + c.Assert(results[0].Revision, Equals, snap.R(26)) + c.Assert(results[0].Version, Equals, "6.1") + c.Assert(results[0].SnapID, Equals, helloWorldSnapID) + c.Assert(results[0].PublisherID, Equals, helloWorldDeveloperID) + c.Assert(results[0].Deltas, HasLen, 0) +} + +func (s *storeTestSuite) TestSnapActionNoResults(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(c, r, "POST", snapActionPath) + // check device authorization is set, implicitly checking doRequest was used + c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) + + jsonReq, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + var req struct { + Context []map[string]interface{} `json:"context"` + Actions []map[string]interface{} `json:"actions"` + } + + err = json.Unmarshal(jsonReq, &req) + c.Assert(err, IsNil) + + c.Assert(req.Context, HasLen, 1) + c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ + "snap-id": helloWorldSnapID, + "instance-key": helloWorldSnapID, + "revision": float64(1), + "tracking-channel": "beta", + "refreshed-date": helloRefreshedDateStr, + }) + c.Assert(req.Actions, HasLen, 0) + io.WriteString(w, `{ + "results": [] +}`) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + cfg := Config{ + StoreBaseURL: mockServerURL, + } + authContext := &testAuthContext{c: c, device: s.device} + sto := New(&cfg, authContext) + + results, err := sto.SnapAction(context.TODO(), []*CurrentSnap{ + { + Name: "hello-world", + SnapID: helloWorldSnapID, + TrackingChannel: "beta", + Revision: snap.R(1), + RefreshedDate: helloRefreshedDate, + }, + }, nil, nil, nil) + c.Check(results, HasLen, 0) + c.Check(err, DeepEquals, &SnapActionError{NoResults: true}) + + // local no-op + results, err = sto.SnapAction(context.TODO(), nil, nil, nil, nil) + c.Check(results, HasLen, 0) + c.Check(err, DeepEquals, &SnapActionError{NoResults: true}) + + c.Check(err.Error(), Equals, "no install/refresh information results from the store") +} + +func (s *storeTestSuite) TestSnapActionRefreshedDateIsOptional(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(c, r, "POST", snapActionPath) + // check device authorization is set, implicitly checking doRequest was used + c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) + + jsonReq, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + var req struct { + Context []map[string]interface{} `json:"context"` + Actions []map[string]interface{} `json:"actions"` + } + + err = json.Unmarshal(jsonReq, &req) + c.Assert(err, IsNil) + + c.Assert(req.Context, HasLen, 1) + c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ + "snap-id": helloWorldSnapID, + "instance-key": helloWorldSnapID, + + "revision": float64(1), + "tracking-channel": "beta", + }) + c.Assert(req.Actions, HasLen, 0) + io.WriteString(w, `{ + "results": [] +}`) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + cfg := Config{ + StoreBaseURL: mockServerURL, + } + authContext := &testAuthContext{c: c, device: s.device} + sto := New(&cfg, authContext) + + results, err := sto.SnapAction(context.TODO(), []*CurrentSnap{ + { + Name: "hello-world", + SnapID: helloWorldSnapID, + TrackingChannel: "beta", + Revision: snap.R(1), + }, + }, nil, nil, nil) + c.Check(results, HasLen, 0) + c.Check(err, DeepEquals, &SnapActionError{NoResults: true}) +} + +func (s *storeTestSuite) TestSnapActionSkipBlocked(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(c, r, "POST", snapActionPath) + // check device authorization is set, implicitly checking doRequest was used + c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) + + jsonReq, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + var req struct { + Context []map[string]interface{} `json:"context"` + Actions []map[string]interface{} `json:"actions"` + } + + err = json.Unmarshal(jsonReq, &req) + c.Assert(err, IsNil) + + c.Assert(req.Context, HasLen, 1) + c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ + "snap-id": helloWorldSnapID, + "instance-key": helloWorldSnapID, + "revision": float64(1), + "tracking-channel": "stable", + "refreshed-date": helloRefreshedDateStr, + }) + c.Assert(req.Actions, HasLen, 1) + c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ + "action": "refresh", + "instance-key": helloWorldSnapID, + "snap-id": helloWorldSnapID, + "channel": "stable", + }) + + io.WriteString(w, `{ + "results": [{ + "result": "refresh", + "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "snap": { + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "revision": 26, + "version": "6.1", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical" + } + } + }] +}`) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + cfg := Config{ + StoreBaseURL: mockServerURL, + } + authContext := &testAuthContext{c: c, device: s.device} + sto := New(&cfg, authContext) + + results, err := sto.SnapAction(context.TODO(), []*CurrentSnap{ + { + Name: "hello-world", + SnapID: helloWorldSnapID, + TrackingChannel: "stable", + Revision: snap.R(1), + RefreshedDate: helloRefreshedDate, + Block: []snap.Revision{snap.R(26)}, + }, + }, []*SnapAction{ + { + Action: "refresh", + SnapID: helloWorldSnapID, + Channel: "stable", + }, + }, nil, nil) + c.Assert(results, HasLen, 0) + c.Check(err, DeepEquals, &SnapActionError{ + Refresh: map[string]error{ + "hello-world": ErrNoUpdateAvailable, + }, + }) +} + +func (s *storeTestSuite) TestSnapActionSkipCurrent(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(c, r, "POST", snapActionPath) + // check device authorization is set, implicitly checking doRequest was used + c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) + + jsonReq, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + var req struct { + Context []map[string]interface{} `json:"context"` + Actions []map[string]interface{} `json:"actions"` + } + + err = json.Unmarshal(jsonReq, &req) + c.Assert(err, IsNil) + + c.Assert(req.Context, HasLen, 1) + c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ + "snap-id": helloWorldSnapID, + "instance-key": helloWorldSnapID, + "revision": float64(26), + "tracking-channel": "stable", + "refreshed-date": helloRefreshedDateStr, + }) + c.Assert(req.Actions, HasLen, 1) + c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ + "action": "refresh", + "instance-key": helloWorldSnapID, + "snap-id": helloWorldSnapID, + "channel": "stable", + }) + + io.WriteString(w, `{ + "results": [{ + "result": "refresh", + "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "snap": { + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "revision": 26, + "version": "6.1", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical" + } + } + }] +}`) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + cfg := Config{ + StoreBaseURL: mockServerURL, + } + authContext := &testAuthContext{c: c, device: s.device} + sto := New(&cfg, authContext) + + results, err := sto.SnapAction(context.TODO(), []*CurrentSnap{ + { + Name: "hello-world", + SnapID: helloWorldSnapID, + TrackingChannel: "stable", + Revision: snap.R(26), + RefreshedDate: helloRefreshedDate, + }, + }, []*SnapAction{ + { + Action: "refresh", + SnapID: helloWorldSnapID, + Channel: "stable", + }, + }, nil, nil) + c.Assert(results, HasLen, 0) + c.Check(err, DeepEquals, &SnapActionError{ + Refresh: map[string]error{ + "hello-world": ErrNoUpdateAvailable, + }, + }) +} + +func (s *storeTestSuite) TestSnapActionRetryOnEOF(c *C) { + n := 0 + var mockServer *httptest.Server + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(c, r, "POST", snapActionPath) + n++ + if n < 4 { + io.WriteString(w, "{") + mockServer.CloseClientConnections() + return + } + + var req struct { + Context []map[string]interface{} `json:"context"` + Actions []map[string]interface{} `json:"actions"` + } + + err := json.NewDecoder(r.Body).Decode(&req) + c.Assert(err, IsNil) + c.Assert(req.Context, HasLen, 1) + c.Assert(req.Actions, HasLen, 1) + io.WriteString(w, `{ + "results": [{ + "result": "refresh", + "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "snap": { + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "revision": 26, + "version": "6.1", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical" + } + } + }] +}`) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + cfg := Config{ + StoreBaseURL: mockServerURL, + } + authContext := &testAuthContext{c: c, device: s.device} + sto := New(&cfg, authContext) + + results, err := sto.SnapAction(context.TODO(), []*CurrentSnap{ + { + Name: "hello-world", + SnapID: helloWorldSnapID, + TrackingChannel: "stable", + Revision: snap.R(1), + }, + }, []*SnapAction{ + { + Action: "refresh", + SnapID: helloWorldSnapID, + Channel: "stable", + }, + }, nil, nil) + c.Assert(err, IsNil) + c.Assert(n, Equals, 4) + c.Assert(results, HasLen, 1) + c.Assert(results[0].Name(), Equals, "hello-world") +} + +func (s *storeTestSuite) TestSnapActionIgnoreValidation(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(c, r, "POST", snapActionPath) + // check device authorization is set, implicitly checking doRequest was used + c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) + + jsonReq, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + var req struct { + Context []map[string]interface{} `json:"context"` + Actions []map[string]interface{} `json:"actions"` + } + + err = json.Unmarshal(jsonReq, &req) + c.Assert(err, IsNil) + + c.Assert(req.Context, HasLen, 1) + c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ + "snap-id": helloWorldSnapID, + "instance-key": helloWorldSnapID, + "revision": float64(1), + "tracking-channel": "stable", + "refreshed-date": helloRefreshedDateStr, + "ignore-validation": true, + }) + c.Assert(req.Actions, HasLen, 1) + c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ + "action": "refresh", + "instance-key": helloWorldSnapID, + "snap-id": helloWorldSnapID, + "channel": "stable", + "ignore-validation": false, + }) + + io.WriteString(w, `{ + "results": [{ + "result": "refresh", + "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "snap": { + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "revision": 26, + "version": "6.1", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical" + } + } + }] +}`) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + cfg := Config{ + StoreBaseURL: mockServerURL, + } + authContext := &testAuthContext{c: c, device: s.device} + sto := New(&cfg, authContext) + + results, err := sto.SnapAction(context.TODO(), []*CurrentSnap{ + { + Name: "hello-world", + SnapID: helloWorldSnapID, + TrackingChannel: "stable", + Revision: snap.R(1), + RefreshedDate: helloRefreshedDate, + IgnoreValidation: true, + }, + }, []*SnapAction{ + { + Action: "refresh", + SnapID: helloWorldSnapID, + Channel: "stable", + Flags: SnapActionEnforceValidation, + }, + }, nil, nil) + c.Assert(err, IsNil) + c.Assert(results, HasLen, 1) + c.Assert(results[0].Name(), Equals, "hello-world") + c.Assert(results[0].Revision, Equals, snap.R(26)) +} + +func (s *storeTestSuite) TestInstallFallbackChannelIsStable(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(c, r, "POST", snapActionPath) + // check device authorization is set, implicitly checking doRequest was used + c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) + + jsonReq, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + var req struct { + Context []map[string]interface{} `json:"context"` + Actions []map[string]interface{} `json:"actions"` + } + + err = json.Unmarshal(jsonReq, &req) + c.Assert(err, IsNil) + + c.Assert(req.Context, HasLen, 1) + c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ + "snap-id": helloWorldSnapID, + "instance-key": helloWorldSnapID, + "revision": float64(1), + "tracking-channel": "stable", + "refreshed-date": helloRefreshedDateStr, + }) + c.Assert(req.Actions, HasLen, 1) + c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ + "action": "refresh", + "instance-key": helloWorldSnapID, + "snap-id": helloWorldSnapID, + }) + + io.WriteString(w, `{ + "results": [{ + "result": "refresh", + "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "snap": { + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "revision": 26, + "version": "6.1", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical" + } + } + }] +}`) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + cfg := Config{ + StoreBaseURL: mockServerURL, + } + authContext := &testAuthContext{c: c, device: s.device} + sto := New(&cfg, authContext) + + results, err := sto.SnapAction(context.TODO(), []*CurrentSnap{ + { + Name: "hello-world", + SnapID: helloWorldSnapID, + RefreshedDate: helloRefreshedDate, + Revision: snap.R(1), + }, + }, []*SnapAction{ + { + Action: "refresh", + SnapID: helloWorldSnapID, + }, + }, nil, nil) + c.Assert(err, IsNil) + c.Assert(results, HasLen, 1) + c.Assert(results[0].Name(), Equals, "hello-world") + c.Assert(results[0].Revision, Equals, snap.R(26)) + c.Assert(results[0].SnapID, Equals, helloWorldSnapID) +} + +func (s *storeTestSuite) TestSnapActionNonDefaultsHeaders(c *C) { + restore := release.MockOnClassic(true) + defer restore() + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(c, r, "POST", snapActionPath) + // check device authorization is set, implicitly checking doRequest was used + c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) + + storeID := r.Header.Get("Snap-Device-Store") + c.Check(storeID, Equals, "foo") + + c.Check(r.Header.Get("Snap-Device-Series"), Equals, "21") + c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, "archXYZ") + c.Check(r.Header.Get("Snap-Classic"), Equals, "true") + + jsonReq, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + var req struct { + Context []map[string]interface{} `json:"context"` + Actions []map[string]interface{} `json:"actions"` + } + + err = json.Unmarshal(jsonReq, &req) + c.Assert(err, IsNil) + + c.Assert(req.Context, HasLen, 1) + c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ + "snap-id": helloWorldSnapID, + "instance-key": helloWorldSnapID, + "revision": float64(1), + "tracking-channel": "beta", + "refreshed-date": helloRefreshedDateStr, + }) + c.Assert(req.Actions, HasLen, 1) + c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ + "action": "refresh", + "instance-key": helloWorldSnapID, + "snap-id": helloWorldSnapID, + }) + + io.WriteString(w, `{ + "results": [{ + "result": "refresh", + "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "snap": { + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "revision": 26, + "version": "6.1", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical" + } + } + }] +}`) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + cfg := DefaultConfig() + cfg.StoreBaseURL = mockServerURL + cfg.Series = "21" + cfg.Architecture = "archXYZ" + cfg.StoreID = "foo" + authContext := &testAuthContext{c: c, device: s.device} + sto := New(cfg, authContext) + + results, err := sto.SnapAction(context.TODO(), []*CurrentSnap{ + { + Name: "hello-world", + SnapID: helloWorldSnapID, + TrackingChannel: "beta", + RefreshedDate: helloRefreshedDate, + Revision: snap.R(1), + }, + }, []*SnapAction{ + { + Action: "refresh", + SnapID: helloWorldSnapID, + }, + }, nil, nil) + c.Assert(err, IsNil) + c.Assert(results, HasLen, 1) + c.Assert(results[0].Name(), Equals, "hello-world") + c.Assert(results[0].Revision, Equals, snap.R(26)) + c.Assert(results[0].Version, Equals, "6.1") + c.Assert(results[0].SnapID, Equals, helloWorldSnapID) + c.Assert(results[0].PublisherID, Equals, helloWorldDeveloperID) + c.Assert(results[0].Deltas, HasLen, 0) +} + +func (s *storeTestSuite) TestSnapActionWithDeltas(c *C) { + origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL") + defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas) + c.Assert(os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", "1"), IsNil) + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(c, r, "POST", snapActionPath) + // check device authorization is set, implicitly checking doRequest was used + c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) + + c.Check(r.Header.Get("Snap-Accept-Delta-Format"), Equals, "xdelta3") + jsonReq, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + var req struct { + Context []map[string]interface{} `json:"context"` + Actions []map[string]interface{} `json:"actions"` + } + + err = json.Unmarshal(jsonReq, &req) + c.Assert(err, IsNil) + + c.Assert(req.Context, HasLen, 1) + c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ + "snap-id": helloWorldSnapID, + "instance-key": helloWorldSnapID, + "revision": float64(1), + "tracking-channel": "beta", + "refreshed-date": helloRefreshedDateStr, + }) + c.Assert(req.Actions, HasLen, 1) + c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ + "action": "refresh", + "instance-key": helloWorldSnapID, + "snap-id": helloWorldSnapID, + }) + + io.WriteString(w, `{ + "results": [{ + "result": "refresh", + "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "snap": { + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "revision": 26, + "version": "6.1", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical" + } + } + }] +}`) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + cfg := Config{ + StoreBaseURL: mockServerURL, + } + authContext := &testAuthContext{c: c, device: s.device} + sto := New(&cfg, authContext) + + results, err := sto.SnapAction(context.TODO(), []*CurrentSnap{ + { + Name: "hello-world", + SnapID: helloWorldSnapID, + TrackingChannel: "beta", + Revision: snap.R(1), + RefreshedDate: helloRefreshedDate, + }, + }, []*SnapAction{ + { + Action: "refresh", + SnapID: helloWorldSnapID, + }, + }, nil, nil) + c.Assert(err, IsNil) + c.Assert(results, HasLen, 1) + c.Assert(results[0].Name(), Equals, "hello-world") + c.Assert(results[0].Revision, Equals, snap.R(26)) +} + +func (s *storeTestSuite) TestSnapActionOptions(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(c, r, "POST", snapActionPath) + // check device authorization is set, implicitly checking doRequest was used + c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) + + c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "true") + + jsonReq, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + var req struct { + Context []map[string]interface{} `json:"context"` + Actions []map[string]interface{} `json:"actions"` + } + + err = json.Unmarshal(jsonReq, &req) + c.Assert(err, IsNil) + + c.Assert(req.Context, HasLen, 1) + c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ + "snap-id": helloWorldSnapID, + "instance-key": helloWorldSnapID, + "revision": float64(1), + "tracking-channel": "stable", + "refreshed-date": helloRefreshedDateStr, + }) + c.Assert(req.Actions, HasLen, 1) + c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ + "action": "refresh", + "instance-key": helloWorldSnapID, + "snap-id": helloWorldSnapID, + "channel": "stable", + }) + + io.WriteString(w, `{ + "results": [{ + "result": "refresh", + "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "snap": { + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "revision": 26, + "version": "6.1", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical" + } + } + }] +}`) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + cfg := Config{ + StoreBaseURL: mockServerURL, + } + authContext := &testAuthContext{c: c, device: s.device} + sto := New(&cfg, authContext) + + results, err := sto.SnapAction(context.TODO(), []*CurrentSnap{ + { + Name: "hello-world", + SnapID: helloWorldSnapID, + TrackingChannel: "stable", + Revision: snap.R(1), + RefreshedDate: helloRefreshedDate, + }, + }, []*SnapAction{ + { + Action: "refresh", + SnapID: helloWorldSnapID, + Channel: "stable", + }, + }, nil, &RefreshOptions{RefreshManaged: true}) + c.Assert(err, IsNil) + c.Assert(results, HasLen, 1) + c.Assert(results[0].Name(), Equals, "hello-world") + c.Assert(results[0].Revision, Equals, snap.R(26)) +} + +func (s *storeTestSuite) TestSnapActionInstall(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(c, r, "POST", snapActionPath) + // check device authorization is set, implicitly checking doRequest was used + c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) + + c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "") + + // no store ID by default + storeID := r.Header.Get("Snap-Device-Store") + c.Check(storeID, Equals, "") + + c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series) + c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.UbuntuArchitecture()) + c.Check(r.Header.Get("Snap-Classic"), Equals, "false") + + jsonReq, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + var req struct { + Context []map[string]interface{} `json:"context"` + Actions []map[string]interface{} `json:"actions"` + } + + err = json.Unmarshal(jsonReq, &req) + c.Assert(err, IsNil) + + c.Assert(req.Context, HasLen, 0) + c.Assert(req.Actions, HasLen, 1) + c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ + "action": "install", + "instance-key": "install-1", + "name": "hello-world", + "channel": "beta", + }) + + io.WriteString(w, `{ + "results": [{ + "result": "install", + "instance-key": "install-1", + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "effective-channel": "candidate", + "snap": { + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "revision": 26, + "version": "6.1", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical" + } + } + }] +}`) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + cfg := Config{ + StoreBaseURL: mockServerURL, + } + authContext := &testAuthContext{c: c, device: s.device} + sto := New(&cfg, authContext) + + results, err := sto.SnapAction(context.TODO(), nil, + []*SnapAction{ + { + Action: "install", + Name: "hello-world", + Channel: "beta", + }, + }, nil, nil) + c.Assert(err, IsNil) + c.Assert(results, HasLen, 1) + c.Assert(results[0].Name(), Equals, "hello-world") + c.Assert(results[0].Revision, Equals, snap.R(26)) + c.Assert(results[0].Version, Equals, "6.1") + c.Assert(results[0].SnapID, Equals, helloWorldSnapID) + c.Assert(results[0].PublisherID, Equals, helloWorldDeveloperID) + c.Assert(results[0].Deltas, HasLen, 0) + // effective-channel + c.Assert(results[0].Channel, Equals, "candidate") +} + +func (s *storeTestSuite) TestSnapActionInstallWithRevision(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(c, r, "POST", snapActionPath) + // check device authorization is set, implicitly checking doRequest was used + c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) + + c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "") + + // no store ID by default + storeID := r.Header.Get("Snap-Device-Store") + c.Check(storeID, Equals, "") + + c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series) + c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.UbuntuArchitecture()) + c.Check(r.Header.Get("Snap-Classic"), Equals, "false") + + jsonReq, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + var req struct { + Context []map[string]interface{} `json:"context"` + Actions []map[string]interface{} `json:"actions"` + } + + err = json.Unmarshal(jsonReq, &req) + c.Assert(err, IsNil) + + c.Assert(req.Context, HasLen, 0) + c.Assert(req.Actions, HasLen, 1) + c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ + "action": "install", + "instance-key": "install-1", + "name": "hello-world", + "revision": float64(28), + }) + + io.WriteString(w, `{ + "results": [{ + "result": "install", + "instance-key": "install-1", + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "snap": { + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "revision": 28, + "version": "6.1", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical" + } + } + }] +}`) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + cfg := Config{ + StoreBaseURL: mockServerURL, + } + authContext := &testAuthContext{c: c, device: s.device} + sto := New(&cfg, authContext) + + results, err := sto.SnapAction(context.TODO(), nil, + []*SnapAction{ + { + Action: "install", + Name: "hello-world", + Revision: snap.R(28), + }, + }, nil, nil) + c.Assert(err, IsNil) + c.Assert(results, HasLen, 1) + c.Assert(results[0].Name(), Equals, "hello-world") + c.Assert(results[0].Revision, Equals, snap.R(28)) + c.Assert(results[0].Version, Equals, "6.1") + c.Assert(results[0].SnapID, Equals, helloWorldSnapID) + c.Assert(results[0].PublisherID, Equals, helloWorldDeveloperID) + c.Assert(results[0].Deltas, HasLen, 0) + // effective-channel is not set + c.Assert(results[0].Channel, Equals, "") +} + +func (s *storeTestSuite) TestSnapActionRevisionNotAvailable(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(c, r, "POST", snapActionPath) + // check device authorization is set, implicitly checking doRequest was used + c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) + + jsonReq, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + var req struct { + Context []map[string]interface{} `json:"context"` + Actions []map[string]interface{} `json:"actions"` + } + + err = json.Unmarshal(jsonReq, &req) + c.Assert(err, IsNil) + + c.Assert(req.Context, HasLen, 1) + c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ + "snap-id": helloWorldSnapID, + "instance-key": helloWorldSnapID, + "revision": float64(26), + "tracking-channel": "stable", + "refreshed-date": helloRefreshedDateStr, + }) + c.Assert(req.Actions, HasLen, 2) + c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ + "action": "refresh", + "instance-key": helloWorldSnapID, + "snap-id": helloWorldSnapID, + "channel": "stable", + }) + c.Assert(req.Actions[1], DeepEquals, map[string]interface{}{ + "action": "install", + "instance-key": "install-1", + "name": "foo", + "channel": "stable", + }) + + io.WriteString(w, `{ + "results": [{ + "result": "error", + "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "error": { + "code": "revision-not-found", + "message": "msg1" + } + }, { + "result": "error", + "instance-key": "install-1", + "snap-id": "foo-id", + "name": "foo", + "error": { + "code": "revision-not-found", + "message": "msg2" + } + }] +}`) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + cfg := Config{ + StoreBaseURL: mockServerURL, + } + authContext := &testAuthContext{c: c, device: s.device} + sto := New(&cfg, authContext) + + results, err := sto.SnapAction(context.TODO(), []*CurrentSnap{ + { + Name: "hello-world", + SnapID: helloWorldSnapID, + TrackingChannel: "stable", + Revision: snap.R(26), + RefreshedDate: helloRefreshedDate, + }, + }, []*SnapAction{ + { + Action: "refresh", + SnapID: helloWorldSnapID, + Channel: "stable", + }, { + Action: "install", + Name: "foo", + Channel: "stable", + }, + }, nil, nil) + c.Assert(results, HasLen, 0) + c.Check(err, DeepEquals, &SnapActionError{ + Refresh: map[string]error{ + "hello-world": ErrRevisionNotAvailable, + }, + Install: map[string]error{ + "foo": ErrRevisionNotAvailable, + }, + }) +} + +func (s *storeTestSuite) TestSnapActionSnapNotFound(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(c, r, "POST", snapActionPath) + // check device authorization is set, implicitly checking doRequest was used + c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) + + jsonReq, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + var req struct { + Context []map[string]interface{} `json:"context"` + Actions []map[string]interface{} `json:"actions"` + } + + err = json.Unmarshal(jsonReq, &req) + c.Assert(err, IsNil) + + c.Assert(req.Context, HasLen, 1) + c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ + "snap-id": helloWorldSnapID, + "instance-key": helloWorldSnapID, + "revision": float64(26), + "tracking-channel": "stable", + "refreshed-date": helloRefreshedDateStr, + }) + c.Assert(req.Actions, HasLen, 2) + c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ + "action": "refresh", + "instance-key": helloWorldSnapID, + "snap-id": helloWorldSnapID, + "channel": "stable", + }) + c.Assert(req.Actions[1], DeepEquals, map[string]interface{}{ + "action": "install", + "instance-key": "install-1", + "name": "foo", + "channel": "stable", + }) + + io.WriteString(w, `{ + "results": [{ + "result": "error", + "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "error": { + "code": "id-not-found", + "message": "msg1" + } + }, { + "result": "error", + "instance-key": "install-1", + "name": "foo", + "error": { + "code": "name-not-found", + "message": "msg2" + } + }] +}`) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + cfg := Config{ + StoreBaseURL: mockServerURL, + } + authContext := &testAuthContext{c: c, device: s.device} + sto := New(&cfg, authContext) + + results, err := sto.SnapAction(context.TODO(), []*CurrentSnap{ + { + Name: "hello-world", + SnapID: helloWorldSnapID, + TrackingChannel: "stable", + Revision: snap.R(26), + RefreshedDate: helloRefreshedDate, + }, + }, []*SnapAction{ + { + Action: "refresh", + SnapID: helloWorldSnapID, + Channel: "stable", + }, { + Action: "install", + Name: "foo", + Channel: "stable", + }, + }, nil, nil) + c.Assert(results, HasLen, 0) + c.Check(err, DeepEquals, &SnapActionError{ + Refresh: map[string]error{ + "hello-world": ErrSnapNotFound, + }, + Install: map[string]error{ + "foo": ErrSnapNotFound, + }, + }) +} + +func (s *storeTestSuite) TestSnapActionOtherErrors(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(c, r, "POST", snapActionPath) + // check device authorization is set, implicitly checking doRequest was used + c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) + + jsonReq, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + var req struct { + Context []map[string]interface{} `json:"context"` + Actions []map[string]interface{} `json:"actions"` + } + + err = json.Unmarshal(jsonReq, &req) + c.Assert(err, IsNil) + + c.Assert(req.Context, HasLen, 0) + c.Assert(req.Actions, HasLen, 1) + c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ + "action": "install", + "instance-key": "install-1", + "name": "foo", + "channel": "stable", + }) + + io.WriteString(w, `{ + "results": [{ + "result": "error", + "error": { + "code": "other1", + "message": "other error one" + } + }], + "error-list": [ + {"code": "global-error", "message": "global error"} + ] +}`) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + cfg := Config{ + StoreBaseURL: mockServerURL, + } + authContext := &testAuthContext{c: c, device: s.device} + sto := New(&cfg, authContext) + + results, err := sto.SnapAction(context.TODO(), nil, []*SnapAction{ + { + Action: "install", + Name: "foo", + Channel: "stable", + }, + }, nil, nil) + c.Assert(results, HasLen, 0) + c.Check(err, DeepEquals, &SnapActionError{ + Other: []error{ + fmt.Errorf("other error one"), + fmt.Errorf("global error"), + }, + }) +} + +func (s *storeTestSuite) TestSnapActionErrorError(c *C) { + e := &SnapActionError{Refresh: map[string]error{ + "foo": fmt.Errorf("sad refresh"), + }} + c.Check(e.Error(), Equals, `cannot refresh snap "foo": sad refresh`) + + e = &SnapActionError{Refresh: map[string]error{ + "foo": fmt.Errorf("sad refresh 1"), + "bar": fmt.Errorf("sad refresh 2"), + }} + errMsg := e.Error() + c.Check(strings.HasPrefix(errMsg, "cannot refresh:\n"), Equals, true) + c.Check(errMsg, testutil.Contains, "\nsnap \"foo\": sad refresh 1") + c.Check(errMsg, testutil.Contains, "\nsnap \"bar\": sad refresh 2") + + e = &SnapActionError{Install: map[string]error{ + "foo": fmt.Errorf("sad install"), + }} + c.Check(e.Error(), Equals, `cannot install snap "foo": sad install`) + + e = &SnapActionError{Install: map[string]error{ + "foo": fmt.Errorf("sad install 1"), + "bar": fmt.Errorf("sad install 2"), + }} + errMsg = e.Error() + c.Check(strings.HasPrefix(errMsg, "cannot install:\n"), Equals, true) + c.Check(errMsg, testutil.Contains, "\nsnap \"foo\": sad install 1") + c.Check(errMsg, testutil.Contains, "\nsnap \"bar\": sad install 2") + + e = &SnapActionError{Refresh: map[string]error{ + "foo": fmt.Errorf("sad refresh 1"), + }, + Install: map[string]error{ + "bar": fmt.Errorf("sad install 2"), + }} + c.Check(e.Error(), Equals, `cannot refresh or install: +snap "foo": sad refresh 1 +snap "bar": sad install 2`) + + e = &SnapActionError{ + NoResults: true, + Other: []error{fmt.Errorf("other error")}, + } + c.Check(e.Error(), Equals, `cannot refresh or install: other error`) + + e = &SnapActionError{ + Other: []error{fmt.Errorf("other error 1"), fmt.Errorf("other error 2")}, + } + c.Check(e.Error(), Equals, `cannot refresh or install: +* other error 1 +* other error 2`) + + e = &SnapActionError{ + Install: map[string]error{ + "bar": fmt.Errorf("sad install"), + }, + Other: []error{fmt.Errorf("other error 1"), fmt.Errorf("other error 2")}, + } + c.Check(e.Error(), Equals, `cannot refresh or install: +snap "bar": sad install +* other error 1 +* other error 2`) + + e = &SnapActionError{ + NoResults: true, + } + c.Check(e.Error(), Equals, "no install/refresh information results from the store") +} + +func (s *storeTestSuite) TestSnapActionRefreshesBothAuths(c *C) { + // snap action (install/refresh) has is its own custom way to + // signal macaroon refreshes that allows to do a best effort + // with the available results + + refresh, err := makeTestRefreshDischargeResponse() + c.Assert(err, IsNil) + c.Check(s.user.StoreDischarges[0], Not(Equals), refresh) + + // mock refresh response + refreshDischargeEndpointHit := false + mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, fmt.Sprintf(`{"discharge_macaroon": "%s"}`, refresh)) + refreshDischargeEndpointHit = true + })) + defer mockSSOServer.Close() + UbuntuoneRefreshDischargeAPI = mockSSOServer.URL + "/tokens/refresh" + + refreshSessionRequested := false + expiredAuth := `Macaroon root="expired-session-macaroon"` + n := 0 + // mock store response + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.UserAgent(), Equals, userAgent) + + switch r.URL.Path { + case snapActionPath: + n++ + type errObj struct { + Code string `json:"code"` + Message string `json:"message"` + } + var errors []errObj + + authorization := r.Header.Get("Authorization") + c.Check(authorization, Equals, s.expectedAuthorization(c, s.user)) + if s.user.StoreDischarges[0] != refresh { + errors = append(errors, errObj{Code: "user-authorization-needs-refresh"}) + } + + devAuthorization := r.Header.Get("Snap-Device-Authorization") + if devAuthorization == "" { + c.Fatalf("device authentication missing") + } else if devAuthorization == expiredAuth { + errors = append(errors, errObj{Code: "device-authorization-needs-refresh"}) + } else { + c.Check(devAuthorization, Equals, `Macaroon root="refreshed-session-macaroon"`) + } + + errorsJSON, err := json.Marshal(errors) + c.Assert(err, IsNil) + + io.WriteString(w, fmt.Sprintf(`{ + "results": [{ + "result": "refresh", + "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "snap": { + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "revision": 26, + "version": "6.1", + "publisher": { + "id": "canonical", + "name": "canonical", + "title": "Canonical" + } + } + }], + "error-list": %s +}`, errorsJSON)) + case authNoncesPath: + io.WriteString(w, `{"nonce": "1234567890:9876543210"}`) + case authSessionPath: + // sanity of request + jsonReq, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + var req map[string]string + err = json.Unmarshal(jsonReq, &req) + c.Assert(err, IsNil) + c.Check(strings.HasPrefix(req["device-session-request"], "type: device-session-request\n"), Equals, true) + c.Check(strings.HasPrefix(req["serial-assertion"], "type: serial\n"), Equals, true) + c.Check(strings.HasPrefix(req["model-assertion"], "type: model\n"), Equals, true) + + authorization := r.Header.Get("X-Device-Authorization") + if authorization == "" { + c.Fatalf("expecting only refresh") + } else { + c.Check(authorization, Equals, expiredAuth) + io.WriteString(w, `{"macaroon": "refreshed-session-macaroon"}`) + refreshSessionRequested = true + } + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + })) + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + + // make sure device session is expired + s.device.SessionMacaroon = "expired-session-macaroon" + authContext := &testAuthContext{c: c, device: s.device, user: s.user} + sto := New(&Config{ + StoreBaseURL: mockServerURL, + }, authContext) + + results, err := sto.SnapAction(context.TODO(), []*CurrentSnap{ + { + Name: "hello-world", + SnapID: helloWorldSnapID, + TrackingChannel: "beta", + Revision: snap.R(1), + RefreshedDate: helloRefreshedDate, + }, + }, []*SnapAction{ + { + Action: "refresh", + SnapID: helloWorldSnapID, + }, + }, s.user, nil) + c.Assert(err, IsNil) + c.Assert(results, HasLen, 1) + c.Assert(results[0].Name(), Equals, "hello-world") + c.Check(refreshDischargeEndpointHit, Equals, true) + c.Check(refreshSessionRequested, Equals, true) + c.Check(n, Equals, 2) +} diff -Nru snapd-2.32.3.2/strutil/shlex/shlex.go snapd-2.32.9/strutil/shlex/shlex.go --- snapd-2.32.3.2/strutil/shlex/shlex.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/strutil/shlex/shlex.go 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,417 @@ +/* +Copyright 2012 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Package shlex implements a simple lexer which splits input in to tokens using +shell-style rules for quoting and commenting. + +The basic use case uses the default ASCII lexer to split a string into sub-strings: + + shlex.Split("one \"two three\" four") -> []string{"one", "two three", "four"} + +To process a stream of strings: + + l := NewLexer(os.Stdin) + for ; token, err := l.Next(); err != nil { + // process token + } + +To access the raw token stream (which includes tokens for comments): + + t := NewTokenizer(os.Stdin) + for ; token, err := t.Next(); err != nil { + // process token + } + +*/ +package shlex + +import ( + "bufio" + "fmt" + "io" + "strings" +) + +// TokenType is a top-level token classification: A word, space, comment, unknown. +type TokenType int + +// runeTokenClass is the type of a UTF-8 character classification: A quote, space, escape. +type runeTokenClass int + +// the internal state used by the lexer state machine +type lexerState int + +// Token is a (type, value) pair representing a lexographical token. +type Token struct { + tokenType TokenType + value string +} + +// Equal reports whether tokens a, and b, are equal. +// Two tokens are equal if both their types and values are equal. A nil token can +// never be equal to another token. +func (a *Token) Equal(b *Token) bool { + if a == nil || b == nil { + return false + } + if a.tokenType != b.tokenType { + return false + } + return a.value == b.value +} + +// Named classes of UTF-8 runes +const ( + spaceRunes = " \t\r\n" + escapingQuoteRunes = `"` + nonEscapingQuoteRunes = "'" + escapeRunes = `\` + commentRunes = "#" +) + +// Classes of rune token +const ( + unknownRuneClass runeTokenClass = iota + spaceRuneClass + escapingQuoteRuneClass + nonEscapingQuoteRuneClass + escapeRuneClass + commentRuneClass + eofRuneClass +) + +// Classes of lexographic token +const ( + UnknownToken TokenType = iota + WordToken + SpaceToken + CommentToken +) + +// Lexer state machine states +const ( + startState lexerState = iota // no runes have been seen + inWordState // processing regular runes in a word + escapingState // we have just consumed an escape rune; the next rune is literal + escapingQuotedState // we have just consumed an escape rune within a quoted string + quotingEscapingState // we are within a quoted string that supports escaping ("...") + quotingState // we are within a string that does not support escaping ('...') + commentState // we are within a comment (everything following an unquoted or unescaped # +) + +// tokenClassifier is used for classifying rune characters. +type tokenClassifier map[rune]runeTokenClass + +func (typeMap tokenClassifier) addRuneClass(runes string, tokenType runeTokenClass) { + for _, runeChar := range runes { + typeMap[runeChar] = tokenType + } +} + +// newDefaultClassifier creates a new classifier for ASCII characters. +func newDefaultClassifier() tokenClassifier { + t := tokenClassifier{} + t.addRuneClass(spaceRunes, spaceRuneClass) + t.addRuneClass(escapingQuoteRunes, escapingQuoteRuneClass) + t.addRuneClass(nonEscapingQuoteRunes, nonEscapingQuoteRuneClass) + t.addRuneClass(escapeRunes, escapeRuneClass) + t.addRuneClass(commentRunes, commentRuneClass) + return t +} + +// ClassifyRune classifiees a rune +func (t tokenClassifier) ClassifyRune(runeVal rune) runeTokenClass { + return t[runeVal] +} + +// Lexer turns an input stream into a sequence of tokens. Whitespace and comments are skipped. +type Lexer Tokenizer + +// NewLexer creates a new lexer from an input stream. +func NewLexer(r io.Reader) *Lexer { + + return (*Lexer)(NewTokenizer(r)) +} + +// Next returns the next word, or an error. If there are no more words, +// the error will be io.EOF. +func (l *Lexer) Next() (string, error) { + for { + token, err := (*Tokenizer)(l).Next() + if err != nil { + return "", err + } + switch token.tokenType { + case WordToken: + return token.value, nil + case CommentToken: + // skip comments + default: + return "", fmt.Errorf("Unknown token type: %v", token.tokenType) + } + } +} + +// Tokenizer turns an input stream into a sequence of typed tokens +type Tokenizer struct { + input bufio.Reader + classifier tokenClassifier +} + +// NewTokenizer creates a new tokenizer from an input stream. +func NewTokenizer(r io.Reader) *Tokenizer { + input := bufio.NewReader(r) + classifier := newDefaultClassifier() + return &Tokenizer{ + input: *input, + classifier: classifier} +} + +// scanStream scans the stream for the next token using the internal state machine. +// It will panic if it encounters a rune which it does not know how to handle. +func (t *Tokenizer) scanStream() (*Token, error) { + state := startState + var tokenType TokenType + var value []rune + var nextRune rune + var nextRuneType runeTokenClass + var err error + + for { + nextRune, _, err = t.input.ReadRune() + nextRuneType = t.classifier.ClassifyRune(nextRune) + + if err == io.EOF { + nextRuneType = eofRuneClass + err = nil + } else if err != nil { + return nil, err + } + + switch state { + case startState: // no runes read yet + { + switch nextRuneType { + case eofRuneClass: + { + return nil, io.EOF + } + case spaceRuneClass: + { + } + case escapingQuoteRuneClass: + { + tokenType = WordToken + state = quotingEscapingState + } + case nonEscapingQuoteRuneClass: + { + tokenType = WordToken + state = quotingState + } + case escapeRuneClass: + { + tokenType = WordToken + state = escapingState + } + case commentRuneClass: + { + tokenType = CommentToken + state = commentState + } + default: + { + tokenType = WordToken + value = append(value, nextRune) + state = inWordState + } + } + } + case inWordState: // in a regular word + { + switch nextRuneType { + case eofRuneClass: + { + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + case spaceRuneClass: + { + t.input.UnreadRune() + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + case escapingQuoteRuneClass: + { + state = quotingEscapingState + } + case nonEscapingQuoteRuneClass: + { + state = quotingState + } + case escapeRuneClass: + { + state = escapingState + } + default: + { + value = append(value, nextRune) + } + } + } + case escapingState: // the rune after an escape character + { + switch nextRuneType { + case eofRuneClass: + { + err = fmt.Errorf("EOF found after escape character") + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + default: + { + state = inWordState + value = append(value, nextRune) + } + } + } + case escapingQuotedState: // the next rune after an escape character, in double quotes + { + switch nextRuneType { + case eofRuneClass: + { + err = fmt.Errorf("EOF found after escape character") + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + default: + { + state = quotingEscapingState + value = append(value, nextRune) + } + } + } + case quotingEscapingState: // in escaping double quotes + { + switch nextRuneType { + case eofRuneClass: + { + err = fmt.Errorf("EOF found when expecting closing quote") + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + case escapingQuoteRuneClass: + { + state = inWordState + } + case escapeRuneClass: + { + state = escapingQuotedState + } + default: + { + value = append(value, nextRune) + } + } + } + case quotingState: // in non-escaping single quotes + { + switch nextRuneType { + case eofRuneClass: + { + err = fmt.Errorf("EOF found when expecting closing quote") + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + case nonEscapingQuoteRuneClass: + { + state = inWordState + } + default: + { + value = append(value, nextRune) + } + } + } + case commentState: // in a comment + { + switch nextRuneType { + case eofRuneClass: + { + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + case spaceRuneClass: + { + if nextRune == '\n' { + state = startState + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } else { + value = append(value, nextRune) + } + } + default: + { + value = append(value, nextRune) + } + } + } + default: + { + return nil, fmt.Errorf("Unexpected state: %v", state) + } + } + } +} + +// Next returns the next token in the stream. +func (t *Tokenizer) Next() (*Token, error) { + return t.scanStream() +} + +// Split partitions a string into a slice of strings. +func Split(s string) ([]string, error) { + l := NewLexer(strings.NewReader(s)) + subStrings := make([]string, 0) + for { + word, err := l.Next() + if err != nil { + if err == io.EOF { + return subStrings, nil + } + return subStrings, err + } + subStrings = append(subStrings, word) + } +} diff -Nru snapd-2.32.3.2/strutil/shlex/shlex_test.go snapd-2.32.9/strutil/shlex/shlex_test.go --- snapd-2.32.3.2/strutil/shlex/shlex_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/strutil/shlex/shlex_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,101 @@ +/* +Copyright 2012 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package shlex + +import ( + "strings" + "testing" +) + +var ( + // one two "three four" "five \"six\"" seven#eight # nine # ten + // eleven 'twelve\' + testString = "one two \"three four\" \"five \\\"six\\\"\" seven#eight # nine # ten\n eleven 'twelve\\' thirteen=13 fourteen/14" +) + +func TestClassifier(t *testing.T) { + classifier := newDefaultClassifier() + tests := map[rune]runeTokenClass{ + ' ': spaceRuneClass, + '"': escapingQuoteRuneClass, + '\'': nonEscapingQuoteRuneClass, + '#': commentRuneClass} + for runeChar, want := range tests { + got := classifier.ClassifyRune(runeChar) + if got != want { + t.Errorf("ClassifyRune(%v) -> %v. Want: %v", runeChar, got, want) + } + } +} + +func TestTokenizer(t *testing.T) { + testInput := strings.NewReader(testString) + expectedTokens := []*Token{ + {WordToken, "one"}, + {WordToken, "two"}, + {WordToken, "three four"}, + {WordToken, "five \"six\""}, + {WordToken, "seven#eight"}, + {CommentToken, " nine # ten"}, + {WordToken, "eleven"}, + {WordToken, "twelve\\"}, + {WordToken, "thirteen=13"}, + {WordToken, "fourteen/14"}} + + tokenizer := NewTokenizer(testInput) + for i, want := range expectedTokens { + got, err := tokenizer.Next() + if err != nil { + t.Error(err) + } + if !got.Equal(want) { + t.Errorf("Tokenizer.Next()[%v] of %q -> %v. Want: %v", i, testString, got, want) + } + } +} + +func TestLexer(t *testing.T) { + testInput := strings.NewReader(testString) + expectedStrings := []string{"one", "two", "three four", "five \"six\"", "seven#eight", "eleven", "twelve\\", "thirteen=13", "fourteen/14"} + + lexer := NewLexer(testInput) + for i, want := range expectedStrings { + got, err := lexer.Next() + if err != nil { + t.Error(err) + } + if got != want { + t.Errorf("Lexer.Next()[%v] of %q -> %v. Want: %v", i, testString, got, want) + } + } +} + +func TestSplit(t *testing.T) { + want := []string{"one", "two", "three four", "five \"six\"", "seven#eight", "eleven", "twelve\\", "thirteen=13", "fourteen/14"} + got, err := Split(testString) + if err != nil { + t.Error(err) + } + if len(want) != len(got) { + t.Errorf("Split(%q) -> %v. Want: %v", testString, got, want) + } + for i := range got { + if got[i] != want[i] { + t.Errorf("Split(%q)[%v] -> %v. Want: %v", testString, i, got[i], want[i]) + } + } +} diff -Nru snapd-2.32.3.2/systemd/export_test.go snapd-2.32.9/systemd/export_test.go --- snapd-2.32.3.2/systemd/export_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/systemd/export_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -50,3 +50,11 @@ osutilStreamCommand = f return func() { osutilStreamCommand = old } } + +func MockJournalStdoutPath(path string) func() { + oldPath := journalStdoutPath + journalStdoutPath = path + return func() { + journalStdoutPath = oldPath + } +} diff -Nru snapd-2.32.3.2/systemd/journal.go snapd-2.32.9/systemd/journal.go --- snapd-2.32.3.2/systemd/journal.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/systemd/journal.go 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,72 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package systemd + +import ( + "bytes" + "fmt" + "log/syslog" + "net" + "os" +) + +var journalStdoutPath = "/run/systemd/journal/stdout" + +// NewJournalStreamFile creates log stream file descriptor to the journal. The +// semantics is identical to that of sd_journal_stream_fd(3) call. +func NewJournalStreamFile(identifier string, priority syslog.Priority, levelPrefix bool) (*os.File, error) { + conn, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: journalStdoutPath}) + if err != nil { + return nil, err + } + // does not affect *os.File created through conn.File() later on + defer conn.Close() + + if err := conn.CloseRead(); err != nil { + return nil, err + } + + // header contents taken from the original systemd code: + // https://github.com/systemd/systemd/blob/97a33b126c845327a3a19d6e66f05684823868fb/src/journal/journal-send.c#L395 + header := bytes.Buffer{} + header.WriteString(identifier) + header.WriteByte('\n') + header.WriteByte('\n') + header.WriteByte(byte('0') + byte(priority)) + header.WriteByte('\n') + var prefix int + if levelPrefix { + prefix = 1 + } + header.WriteByte(byte('0') + byte(prefix)) + header.WriteByte('\n') + header.WriteByte('0') + header.WriteByte('\n') + header.WriteByte('0') + header.WriteByte('\n') + header.WriteByte('0') + header.WriteByte('\n') + + if _, err := conn.Write(header.Bytes()); err != nil { + return nil, fmt.Errorf("failed to write header: %v", err) + } + + return conn.File() +} diff -Nru snapd-2.32.3.2/systemd/journal_test.go snapd-2.32.9/systemd/journal_test.go --- snapd-2.32.3.2/systemd/journal_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/systemd/journal_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,89 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package systemd_test + +import ( + "log/syslog" + "net" + "path" + + . "gopkg.in/check.v1" + + . "github.com/snapcore/snapd/systemd" +) + +type journalTestSuite struct{} + +var _ = Suite(&journalTestSuite{}) + +func (j *journalTestSuite) TestStreamFileErrorNoPath(c *C) { + restore := MockJournalStdoutPath(path.Join(c.MkDir(), "fake-journal")) + defer restore() + + jout, err := NewJournalStreamFile("foobar", syslog.LOG_INFO, false) + c.Assert(err, ErrorMatches, ".*no such file or directory") + c.Assert(jout, IsNil) +} + +func (j *journalTestSuite) TestStreamFileHeader(c *C) { + fakePath := path.Join(c.MkDir(), "fake-journal") + restore := MockJournalStdoutPath(fakePath) + defer restore() + + listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: fakePath}) + c.Assert(err, IsNil) + defer listener.Close() + + doneCh := make(chan struct{}, 1) + + go func() { + defer func() { close(doneCh) }() + + // see https://github.com/systemd/systemd/blob/97a33b126c845327a3a19d6e66f05684823868fb/src/journal/journal-send.c#L424 + conn, err := listener.AcceptUnix() + c.Assert(err, IsNil) + defer conn.Close() + + expectedHdrLen := len("foobar") + 1 + 1 + 2 + 2 + 2 + 2 + 2 + hdrBuf := make([]byte, expectedHdrLen) + hdrLen, err := conn.Read(hdrBuf) + c.Assert(err, IsNil) + c.Assert(hdrLen, Equals, expectedHdrLen) + c.Check(hdrBuf, DeepEquals, []byte("foobar\n\n6\n0\n0\n0\n0\n")) + + data := make([]byte, 4096) + sz, err := conn.Read(data) + c.Assert(err, IsNil) + c.Assert(sz > 0, Equals, true) + c.Check(data[0:sz], DeepEquals, []byte("hello from unit tests")) + + doneCh <- struct{}{} + }() + + jout, err := NewJournalStreamFile("foobar", syslog.LOG_INFO, false) + c.Assert(err, IsNil) + c.Assert(jout, NotNil) + + _, err = jout.WriteString("hello from unit tests") + c.Assert(err, IsNil) + defer jout.Close() + + <-doneCh +} diff -Nru snapd-2.32.3.2/tests/lib/fakestore/store/store.go snapd-2.32.9/tests/lib/fakestore/store/store.go --- snapd-2.32.3.2/tests/lib/fakestore/store/store.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/tests/lib/fakestore/store/store.go 2018-05-16 08:20:08.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -99,6 +99,8 @@ mux.HandleFunc("/api/v1/snaps/metadata", store.bulkEndpoint) mux.Handle("/download/", http.StripPrefix("/download/", http.FileServer(http.Dir(topDir)))) mux.HandleFunc("/api/v1/snaps/assertions/", store.assertionsEndpoint) + // v2 + mux.HandleFunc("/v2/snaps/refresh", store.snapActionEndpoint) return store } @@ -185,19 +187,19 @@ info, err := snap.ReadInfoFromSnapFile(snapFile, nil) if err != nil { - http.Error(w, fmt.Sprintf("can get info for: %v: %v", fn, err), 400) + http.Error(w, fmt.Sprintf("cannot get info for: %v: %v", fn, err), 400) return nil, errInfo } snapDigest, size, err := asserts.SnapFileSHA3_384(fn) if err != nil { - http.Error(w, fmt.Sprintf("can get digest for: %v: %v", fn, err), 400) + http.Error(w, fmt.Sprintf("cannot get digest for: %v: %v", fn, err), 400) return nil, errInfo } snapRev, devAcct, err := findSnapRevision(snapDigest, bs) if err != nil && !asserts.IsNotFound(err) { - http.Error(w, fmt.Sprintf("can get info for: %v: %v", fn, err), 400) + http.Error(w, fmt.Sprintf("cannot get info for: %v: %v", fn, err), 400) return nil, errInfo } @@ -407,7 +409,7 @@ name := snapIDtoName[pkg.SnapID] if name == "" { - http.Error(w, fmt.Sprintf("unknown snapid: %q", pkg.SnapID), 400) + http.Error(w, fmt.Sprintf("unknown snap-id: %q", pkg.SnapID), 400) return } @@ -439,7 +441,7 @@ // should look nice out, err := json.MarshalIndent(replyData, "", " ") if err != nil { - http.Error(w, fmt.Sprintf("can marshal: %v: %v", replyData, err), 400) + http.Error(w, fmt.Sprintf("cannot marshal: %v: %v", replyData, err), 400) return } w.Write(out) @@ -482,6 +484,153 @@ return bs, nil } +type currentSnap struct { + SnapID string `json:"snap-id"` + InstanceKey string `json:"instance-key"` +} + +type snapAction struct { + Action string `json:"action"` + InstanceKey string `json:"instance-key"` + SnapID string `json:"snap-id"` + Name string `json:"name"` +} + +type snapActionRequest struct { + Context []currentSnap `json:"context"` + Fields []string `json:"fields"` + Actions []snapAction `json:"actions"` +} + +type snapActionResult struct { + Result string `json:"result"` + InstanceKey string `json:"instance-key"` + SnapID string `json:"snap-id"` + Name string `json:"name"` + Snap detailsResultV2 `json:"snap"` +} + +type snapActionResultList struct { + Results []*snapActionResult `json:"results"` +} + +type detailsResultV2 struct { + Architectures []string `json:"architectures"` + SnapID string `json:"snap-id"` + Name string `json:"name"` + Publisher struct { + ID string `json:"id"` + Username string `json:"username"` + } `json:"publisher"` + Download struct { + URL string `json:"url"` + Sha3_384 string `json:"sha3-384"` + Size uint64 `json:"size"` + } `json:"download"` + Version string `json:"version"` + Revision int `json:"revision"` +} + +func (s *Store) snapActionEndpoint(w http.ResponseWriter, req *http.Request) { + var reqData snapActionRequest + var replyData snapActionResultList + + decoder := json.NewDecoder(req.Body) + if err := decoder.Decode(&reqData); err != nil { + http.Error(w, fmt.Sprintf("cannot decode request body: %v", err), 400) + return + } + + bs, err := s.collectAssertions() + if err != nil { + http.Error(w, fmt.Sprintf("internal error collecting assertions: %v", err), 500) + return + } + + var remoteStore string + if osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") { + remoteStore = "staging" + } else { + remoteStore = "production" + } + snapIDtoName, err := addSnapIDs(bs, someSnapIDtoName[remoteStore]) + if err != nil { + http.Error(w, fmt.Sprintf("internal error collecting snapIDs: %v", err), 500) + return + } + + snaps, err := s.collectSnaps() + if err != nil { + http.Error(w, fmt.Sprintf("internal error collecting snaps: %v", err), 500) + return + } + + actions := reqData.Actions + if len(actions) == 1 && actions[0].Action == "refresh-all" { + actions = make([]snapAction, len(reqData.Context)) + for i, s := range reqData.Context { + actions[i] = snapAction{ + Action: "refresh", + SnapID: s.SnapID, + InstanceKey: s.InstanceKey, + } + } + } + + // check if we have downloadable snap of the given SnapID or name + for _, a := range actions { + name := a.Name + snapID := a.SnapID + if a.Action == "refresh" { + name = snapIDtoName[snapID] + } + + if name == "" { + http.Error(w, fmt.Sprintf("unknown snap-id: %q", snapID), 400) + return + } + + if fn, ok := snaps[name]; ok { + essInfo, err := snapEssentialInfo(w, fn, snapID, bs) + if essInfo == nil { + if err != errInfo { + panic(err) + } + return + } + + res := &snapActionResult{ + Result: a.Action, + InstanceKey: a.InstanceKey, + SnapID: essInfo.SnapID, + Name: essInfo.Name, + Snap: detailsResultV2{ + Architectures: []string{"all"}, + SnapID: essInfo.SnapID, + Name: essInfo.Name, + Version: essInfo.Version, + Revision: essInfo.Revision, + }, + } + res.Snap.Publisher.ID = essInfo.DeveloperID + res.Snap.Publisher.Username = essInfo.DevelName + res.Snap.Download.URL = fmt.Sprintf("%s/download/%s", s.URL(), filepath.Base(fn)) + res.Snap.Download.Sha3_384 = hexify(essInfo.Digest) + res.Snap.Download.Size = essInfo.Size + replyData.Results = append(replyData.Results, res) + } + } + + // use indent because this is a development tool, output + // should look nice + out, err := json.MarshalIndent(replyData, "", " ") + if err != nil { + http.Error(w, fmt.Sprintf("cannot marshal: %v: %v", replyData, err), 400) + return + } + w.Write(out) +} + func (s *Store) retrieveAssertion(bs asserts.Backstore, assertType *asserts.AssertionType, primaryKey []string) (asserts.Assertion, error) { a, err := bs.Get(assertType, primaryKey, assertType.MaxSupportedFormat()) if asserts.IsNotFound(err) && s.assertFallback { diff -Nru snapd-2.32.3.2/tests/lib/fakestore/store/store_test.go snapd-2.32.9/tests/lib/fakestore/store/store_test.go --- snapd-2.32.3.2/tests/lib/fakestore/store/store_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/tests/lib/fakestore/store/store_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2015 Canonical Ltd + * Copyright (C) 2014-2018 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -49,12 +49,12 @@ var defaultAddr = "localhost:23321" -func getSha(fn string) string { - snapDigest, _, err := asserts.SnapFileSHA3_384(fn) +func getSha(fn string) (string, uint64) { + snapDigest, size, err := asserts.SnapFileSHA3_384(fn) if err != nil { panic(err) } - return hexify(snapDigest) + return hexify(snapDigest), size } func (s *storeTestSuite) SetUpTest(c *C) { @@ -127,6 +127,7 @@ var body map[string]interface{} c.Assert(json.NewDecoder(resp.Body).Decode(&body), IsNil) + sha3_384, _ := getSha(snapFn) c.Check(body, DeepEquals, map[string]interface{}{ "architecture": []interface{}{"all"}, "snap_id": "xidididididididididididididididid", @@ -137,7 +138,7 @@ "download_url": s.store.URL() + "/download/foo_7_all.snap", "version": "7", "revision": float64(77), - "download_sha3_384": getSha(snapFn), + "download_sha3_384": sha3_384, }) } @@ -151,6 +152,7 @@ var body map[string]interface{} c.Assert(json.NewDecoder(resp.Body).Decode(&body), IsNil) + sha3_384, _ := getSha(snapFn) c.Check(body, DeepEquals, map[string]interface{}{ "architecture": []interface{}{"all"}, "snap_id": "", @@ -161,7 +163,7 @@ "download_url": s.store.URL() + "/download/foo_1_all.snap", "version": "1", "revision": float64(424242), - "download_sha3_384": getSha(snapFn), + "download_sha3_384": sha3_384, }) } @@ -183,6 +185,7 @@ } `json:"_embedded"` } c.Assert(json.NewDecoder(resp.Body).Decode(&body), IsNil) + sha3_384, _ := getSha(snapFn) c.Check(body.Top.Cat, DeepEquals, []map[string]interface{}{{ "architecture": []interface{}{"all"}, "snap_id": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", @@ -193,7 +196,7 @@ "download_url": s.store.URL() + "/download/test-snapd-tools_1_all.snap", "version": "1", "revision": float64(424242), - "download_sha3_384": getSha(snapFn), + "download_sha3_384": sha3_384, }}) } @@ -214,6 +217,7 @@ } `json:"_embedded"` } c.Assert(json.NewDecoder(resp.Body).Decode(&body), IsNil) + sha3_384, _ := getSha(snapFn) c.Check(body.Top.Cat, DeepEquals, []map[string]interface{}{{ "architecture": []interface{}{"all"}, "snap_id": "xidididididididididididididididid", @@ -224,7 +228,7 @@ "download_url": s.store.URL() + "/download/foo_10_all.snap", "version": "10", "revision": float64(99), - "download_sha3_384": getSha(snapFn), + "download_sha3_384": sha3_384, }}) } @@ -390,3 +394,169 @@ c.Assert(err, IsNil) c.Check(respObj["status"], Equals, float64(404)) } + +func (s *storeTestSuite) TestSnapActionEndpoint(c *C) { + snapFn := s.makeTestSnap(c, "name: test-snapd-tools\nversion: 1") + + resp, err := s.StorePostJSON("/v2/snaps/refresh", []byte(`{ +"context": [{"instance-key":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","snap-id":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","tracking-channel":"stable","revision":1}], +"actions": [{"action":"refresh","instance-key":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","snap-id":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw"}] +}`)) + c.Assert(err, IsNil) + defer resp.Body.Close() + + c.Assert(resp.StatusCode, Equals, 200) + var body struct { + Results []map[string]interface{} + } + c.Assert(json.NewDecoder(resp.Body).Decode(&body), IsNil) + c.Check(body.Results, HasLen, 1) + sha3_384, size := getSha(snapFn) + c.Check(body.Results[0], DeepEquals, map[string]interface{}{ + "result": "refresh", + "instance-key": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", + "snap-id": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", + "name": "test-snapd-tools", + "snap": map[string]interface{}{ + "architectures": []interface{}{"all"}, + "snap-id": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", + "name": "test-snapd-tools", + "publisher": map[string]interface{}{ + "username": "canonical", + "id": "canonical", + }, + "download": map[string]interface{}{ + "url": s.store.URL() + "/download/test-snapd-tools_1_all.snap", + "sha3-384": sha3_384, + "size": float64(size), + }, + "version": "1", + "revision": float64(424242), + }, + }) +} + +func (s *storeTestSuite) TestSnapActionEndpointWithAssertions(c *C) { + snapFn := s.makeTestSnap(c, "name: foo\nversion: 10") + s.makeAssertions(c, snapFn, "foo", "xidididididididididididididididid", "foo-devel", "foo-devel-id", 99) + + resp, err := s.StorePostJSON("/v2/snaps/refresh", []byte(`{ +"context": [{"instance-key":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","snap-id":"xidididididididididididididididid","tracking-channel":"stable","revision":1}], +"actions": [{"action":"refresh","instance-key":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","snap-id":"xidididididididididididididididid"}] +}`)) + c.Assert(err, IsNil) + defer resp.Body.Close() + + c.Assert(resp.StatusCode, Equals, 200) + var body struct { + Results []map[string]interface{} + } + c.Assert(json.NewDecoder(resp.Body).Decode(&body), IsNil) + c.Check(body.Results, HasLen, 1) + sha3_384, size := getSha(snapFn) + c.Check(body.Results[0], DeepEquals, map[string]interface{}{ + "result": "refresh", + "instance-key": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", + "snap-id": "xidididididididididididididididid", + "name": "foo", + "snap": map[string]interface{}{ + "architectures": []interface{}{"all"}, + "snap-id": "xidididididididididididididididid", + "name": "foo", + "publisher": map[string]interface{}{ + "username": "foo-devel", + "id": "foo-devel-id", + }, + "download": map[string]interface{}{ + "url": s.store.URL() + "/download/foo_10_all.snap", + "sha3-384": sha3_384, + "size": float64(size), + }, + "version": "10", + "revision": float64(99), + }, + }) +} + +func (s *storeTestSuite) TestSnapActionEndpointRefreshAll(c *C) { + snapFn := s.makeTestSnap(c, "name: test-snapd-tools\nversion: 1") + + resp, err := s.StorePostJSON("/v2/snaps/refresh", []byte(`{ +"context": [{"instance-key":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","snap-id":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","tracking-channel":"stable","revision":1}], +"actions": [{"action":"refresh-all"}] +}`)) + c.Assert(err, IsNil) + defer resp.Body.Close() + + c.Assert(resp.StatusCode, Equals, 200) + var body struct { + Results []map[string]interface{} + } + c.Assert(json.NewDecoder(resp.Body).Decode(&body), IsNil) + c.Check(body.Results, HasLen, 1) + sha3_384, size := getSha(snapFn) + c.Check(body.Results[0], DeepEquals, map[string]interface{}{ + "result": "refresh", + "instance-key": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", + "snap-id": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", + "name": "test-snapd-tools", + "snap": map[string]interface{}{ + "architectures": []interface{}{"all"}, + "snap-id": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", + "name": "test-snapd-tools", + "publisher": map[string]interface{}{ + "username": "canonical", + "id": "canonical", + }, + "download": map[string]interface{}{ + "url": s.store.URL() + "/download/test-snapd-tools_1_all.snap", + "sha3-384": sha3_384, + "size": float64(size), + }, + "version": "1", + "revision": float64(424242), + }, + }) +} + +func (s *storeTestSuite) TestSnapActionEndpointWithAssertionsInstall(c *C) { + snapFn := s.makeTestSnap(c, "name: foo\nversion: 10") + s.makeAssertions(c, snapFn, "foo", "xidididididididididididididididid", "foo-devel", "foo-devel-id", 99) + + resp, err := s.StorePostJSON("/v2/snaps/refresh", []byte(`{ +"context": [], +"actions": [{"action":"install","instance-key":"foo","name":"foo"}] +}`)) + c.Assert(err, IsNil) + defer resp.Body.Close() + + c.Assert(resp.StatusCode, Equals, 200) + var body struct { + Results []map[string]interface{} + } + c.Assert(json.NewDecoder(resp.Body).Decode(&body), IsNil) + c.Check(body.Results, HasLen, 1) + sha3_384, size := getSha(snapFn) + c.Check(body.Results[0], DeepEquals, map[string]interface{}{ + "result": "install", + "instance-key": "foo", + "snap-id": "xidididididididididididididididid", + "name": "foo", + "snap": map[string]interface{}{ + "architectures": []interface{}{"all"}, + "snap-id": "xidididididididididididididididid", + "name": "foo", + "publisher": map[string]interface{}{ + "username": "foo-devel", + "id": "foo-devel-id", + }, + "download": map[string]interface{}{ + "url": s.store.URL() + "/download/foo_10_all.snap", + "sha3-384": sha3_384, + "size": float64(size), + }, + "version": "10", + "revision": float64(99), + }, + }) +} diff -Nru snapd-2.32.3.2/tests/lib/pkgdb.sh snapd-2.32.9/tests/lib/pkgdb.sh --- snapd-2.32.3.2/tests/lib/pkgdb.sh 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/tests/lib/pkgdb.sh 2018-05-16 08:20:08.000000000 +0000 @@ -122,7 +122,7 @@ quiet dnf -y install "$@" ;; opensuse-*) - quiet zypper install -y "$@" + quiet rpm -i "$@" ;; *) echo "ERROR: Unsupported distribution $SPREAD_SYSTEM" @@ -245,7 +245,7 @@ quiet dnf makecache ;; opensuse-*) - quiet zypper refresh + quiet zypper --gpg-auto-import-keys refresh ;; *) echo "ERROR: Unsupported distribution $SPREAD_SYSTEM" @@ -378,6 +378,7 @@ libglib2.0-dev libseccomp-dev libudev-dev + man netcat-openbsd pkg-config python3-docutils @@ -423,9 +424,14 @@ xvfb " ;; + ubuntu-17.10-64) + echo " + linux-image-extra-4.13.0-16-generic + " + ;; ubuntu-*) echo " - linux-image-extra-$(uname -r) + squashfs-tools " ;; debian-*) @@ -451,6 +457,7 @@ git golang jq + man mock redhat-lsb-core rpm-build @@ -460,12 +467,14 @@ pkg_dependencies_opensuse(){ echo " + apparmor-profiles curl expect git golang-packaging jq lsb-release + man netcat-openbsd osc uuidd diff -Nru snapd-2.32.3.2/tests/lib/prepare-restore.sh snapd-2.32.9/tests/lib/prepare-restore.sh --- snapd-2.32.3.2/tests/lib/prepare-restore.sh 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/tests/lib/prepare-restore.sh 2018-05-16 08:20:08.000000000 +0000 @@ -359,6 +359,14 @@ cat /proc/meminfo exit 1 fi + + # Check for kernel oops during the tests + if dmesg|grep "Oops: "; then + echo "A kernel oops happened during the tests, test results will be unreliable" + echo "Dmesg debug output:" + dmesg + exit 1 + fi } restore_project() { diff -Nru snapd-2.32.3.2/tests/lib/reset.sh snapd-2.32.9/tests/lib/reset.sh --- snapd-2.32.3.2/tests/lib/reset.sh 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/tests/lib/reset.sh 2018-05-16 08:20:08.000000000 +0000 @@ -5,24 +5,15 @@ # shellcheck source=tests/lib/dirs.sh . "$TESTSLIB/dirs.sh" +# shellcheck source=tests/lib/systemd.sh +. "$TESTSLIB/systemd.sh" + reset_classic() { # Reload all service units as in some situations the unit might # have changed on the disk. systemctl daemon-reload - echo "Ensure the service is active before stopping it" - retries=20 - systemctl status snapd.service snapd.socket || true - while systemctl status snapd.service snapd.socket | grep "Active: activating"; do - if [ $retries -eq 0 ]; then - echo "snapd service or socket not active" - exit 1 - fi - retries=$(( $retries - 1 )) - sleep 1 - done - - systemctl stop snapd.service snapd.socket + systemd_stop_units snapd.service snapd.socket case "$SPREAD_SYSTEM" in ubuntu-*|debian-*) diff -Nru snapd-2.32.3.2/tests/lib/snaps/test-snapd-service/bin/reload snapd-2.32.9/tests/lib/snaps/test-snapd-service/bin/reload --- snapd-2.32.3.2/tests/lib/snaps/test-snapd-service/bin/reload 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/tests/lib/snaps/test-snapd-service/bin/reload 2018-05-16 08:20:08.000000000 +0000 @@ -5,4 +5,4 @@ exit 1 fi echo "reloading main PID: $MAINPID" -kill -HUP $MAINPID +kill -HUP "$MAINPID" diff -Nru snapd-2.32.3.2/tests/lib/snaps/test-snapd-service/bin/start-refresh-mode snapd-2.32.9/tests/lib/snaps/test-snapd-service/bin/start-refresh-mode --- snapd-2.32.3.2/tests/lib/snaps/test-snapd-service/bin/start-refresh-mode 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/tests/lib/snaps/test-snapd-service/bin/start-refresh-mode 1970-01-01 00:00:00.000000000 +0000 @@ -1,11 +0,0 @@ -#!/bin/bash - -trap "echo got sigusr1" USR1 -trap "echo got sigusr2" USR2 -trap "echo got sighup" HUP - -while true; do - echo "running $1 process" - sleep 1 -done - diff -Nru snapd-2.32.3.2/tests/lib/snaps/test-snapd-service/bin/start-stop-mode snapd-2.32.9/tests/lib/snaps/test-snapd-service/bin/start-stop-mode --- snapd-2.32.3.2/tests/lib/snaps/test-snapd-service/bin/start-stop-mode 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/tests/lib/snaps/test-snapd-service/bin/start-stop-mode 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,11 @@ +#!/bin/bash + +SIG=0 +trap "echo got sigusr1; SIG=1" USR1 +trap "echo got sigusr2; SIG=1" USR2 +trap "echo got sighup; SIG=1" HUP + +while [ "$SIG" = "0" ]; do + echo "running $1 process" + sleep 1 +done diff -Nru snapd-2.32.3.2/tests/lib/snaps/test-snapd-service/bin/start-stop-mode-sigterm snapd-2.32.9/tests/lib/snaps/test-snapd-service/bin/start-stop-mode-sigterm --- snapd-2.32.3.2/tests/lib/snaps/test-snapd-service/bin/start-stop-mode-sigterm 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/tests/lib/snaps/test-snapd-service/bin/start-stop-mode-sigterm 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,8 @@ +#!/bin/sh + +set -e + +echo "start-refresh-mode-sigkill" + +echo "running a process" +sleep 3133731337 diff -Nru snapd-2.32.3.2/tests/lib/snaps/test-snapd-service/bin/stop snapd-2.32.9/tests/lib/snaps/test-snapd-service/bin/stop --- snapd-2.32.3.2/tests/lib/snaps/test-snapd-service/bin/stop 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/tests/lib/snaps/test-snapd-service/bin/stop 2018-05-16 08:20:08.000000000 +0000 @@ -1,3 +1,7 @@ #!/bin/sh echo "stop service" + +if [ -n "$1" ]; then + sleep "$1" +fi diff -Nru snapd-2.32.3.2/tests/lib/snaps/test-snapd-service/bin/stop-refresh-mode snapd-2.32.9/tests/lib/snaps/test-snapd-service/bin/stop-refresh-mode --- snapd-2.32.3.2/tests/lib/snaps/test-snapd-service/bin/stop-refresh-mode 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/tests/lib/snaps/test-snapd-service/bin/stop-refresh-mode 1970-01-01 00:00:00.000000000 +0000 @@ -1,3 +0,0 @@ -#!/bin/sh - -echo "stop $1 process" diff -Nru snapd-2.32.3.2/tests/lib/snaps/test-snapd-service/bin/stop-stop-mode snapd-2.32.9/tests/lib/snaps/test-snapd-service/bin/stop-stop-mode --- snapd-2.32.3.2/tests/lib/snaps/test-snapd-service/bin/stop-stop-mode 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/tests/lib/snaps/test-snapd-service/bin/stop-stop-mode 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "stop $1 process" diff -Nru snapd-2.32.3.2/tests/lib/snaps/test-snapd-service/meta/snap.yaml snapd-2.32.9/tests/lib/snaps/test-snapd-service/meta/snap.yaml --- snapd-2.32.3.2/tests/lib/snaps/test-snapd-service/meta/snap.yaml 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/tests/lib/snaps/test-snapd-service/meta/snap.yaml 2018-05-16 08:20:08.000000000 +0000 @@ -9,48 +9,51 @@ test-snapd-other-service: command: bin/start-other daemon: simple - test-snapd-endure-service: - command: bin/start-refresh-mode endure - stop-command: bin/stop-refresh-mode endure - daemon: simple - refresh-mode: endure - test-snapd-sigterm-service: - command: bin/start-refresh-mode sigterm - stop-command: bin/stop-refresh-mode sigterm - daemon: simple - refresh-mode: sigterm - test-snapd-sigterm-all-service: - command: bin/start-refresh-mode sigterm-all - stop-command: bin/stop-refresh-mode sigterm-all - daemon: simple - refresh-mode: sigterm test-snapd-sighup-service: - command: bin/start-refresh-mode sighup - stop-command: bin/stop-refresh-mode sighup + command: bin/start-stop-mode sighup + stop-command: bin/stop-stop-mode sighup daemon: simple - refresh-mode: sighup + stop-mode: sighup test-snapd-sighup-all-service: - command: bin/start-refresh-mode sighup-all - stop-command: bin/stop-refresh-mode sighup-all + command: bin/start-stop-mode sighup-all + stop-command: bin/stop-stop-mode sighup-all daemon: simple - refresh-mode: sighup-all + stop-mode: sighup-all test-snapd-sigusr1-service: - command: bin/start-refresh-mode sigusr1 - stop-command: bin/stop-refresh-mode sigusr1 + command: bin/start-stop-mode sigusr1 + stop-command: bin/stop-stop-mode sigusr1 daemon: simple - refresh-mode: sigusr1 + stop-mode: sigusr1 test-snapd-sigusr1-all-service: - command: bin/start-refresh-mode sigusr1-all - stop-command: bin/stop-refresh-mode sigusr1-all + command: bin/start-stop-mode sigusr1-all + stop-command: bin/stop-stop-mode sigusr1-all daemon: simple - refresh-mode: sigusr1-all + stop-mode: sigusr1-all test-snapd-sigusr2-service: - command: bin/start-refresh-mode sigusr2 - stop-command: bin/stop-refresh-mode sigusr2 + command: bin/start-stop-mode sigusr2 + stop-command: bin/stop-stop-mode sigusr2 daemon: simple - refresh-mode: sigusr2 + stop-mode: sigusr2 test-snapd-sigusr2-all-service: - command: bin/start-refresh-mode sigusr2-all - stop-command: bin/stop-refresh-mode sigusr2-all + command: bin/start-stop-mode sigusr2-all + stop-command: bin/stop-stop-mode sigusr2-all + daemon: simple + stop-mode: sigusr2-all + test-snapd-sigterm-service: + command: bin/start-stop-mode-sigterm + daemon: simple + stop-mode: sigterm + test-snapd-sigterm-all-service: + command: bin/start-stop-mode-sigterm + daemon: simple + stop-mode: sigterm-all + test-snapd-endure-service: + command: bin/start-stop-mode endure + stop-command: bin/stop-stop-mode endure + daemon: simple + refresh-mode: endure + test-snapd-service-refuses-to-stop: + command: bin/start daemon: simple - refresh-mode: sigusr2-all + stop-command: bin/stop 60 + stop-timeout: 10s diff -Nru snapd-2.32.3.2/tests/lib/snaps/test-snapd-xdg-autostart/bin/foobar snapd-2.32.9/tests/lib/snaps/test-snapd-xdg-autostart/bin/foobar --- snapd-2.32.3.2/tests/lib/snaps/test-snapd-xdg-autostart/bin/foobar 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/tests/lib/snaps/test-snapd-xdg-autostart/bin/foobar 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,26 @@ +#!/bin/sh + +dump_autostart() { + dfpath="$SNAP_USER_DATA/.config/autostart/foo.desktop" + + test -e "$dfpath" && return + + echo "generating autostart file $dfpath" + + mkdir -p $SNAP_USER_DATA/.config/autostart + cat < $dfpath +[Desktop Entry] +Name=foo autostart +Exec=/foo/bar/baz/bin/foobar --autostart a b c +EOF +} + +case "$1" in + --autostart) + echo "autostarting with args '$@'" | tee $SNAP_USER_DATA/foo-autostarted + ;; + *) + echo "regular run with args '$@'" + dump_autostart + ;; +esac diff -Nru snapd-2.32.3.2/tests/lib/snaps/test-snapd-xdg-autostart/meta/snap.yaml snapd-2.32.9/tests/lib/snaps/test-snapd-xdg-autostart/meta/snap.yaml --- snapd-2.32.3.2/tests/lib/snaps/test-snapd-xdg-autostart/meta/snap.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/tests/lib/snaps/test-snapd-xdg-autostart/meta/snap.yaml 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,6 @@ +name: test-snapd-xdg-autostart +version: 1.0 +apps: + foo: + command: bin/foobar + autostart: foo.desktop diff -Nru snapd-2.32.3.2/tests/lib/systemd.sh snapd-2.32.9/tests/lib/systemd.sh --- snapd-2.32.3.2/tests/lib/systemd.sh 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/tests/lib/systemd.sh 2018-05-16 08:20:08.000000000 +0000 @@ -36,3 +36,23 @@ echo "service $service_name did not start" exit 1 } + +systemd_stop_units() { + for unit in "$@"; do + if systemctl is-active "$unit"; then + echo "Ensure the service is active before stopping it" + retries=20 + systemctl status "$unit" || true + while systemctl status "$unit" | grep "Active: activating"; do + if [ $retries -eq 0 ]; then + echo "$unit unit not active" + exit 1 + fi + retries=$(( $retries - 1 )) + sleep 1 + done + + systemctl stop "$unit" + fi + done +} diff -Nru snapd-2.32.3.2/tests/main/completion/task.yaml snapd-2.32.9/tests/main/completion/task.yaml --- snapd-2.32.3.2/tests/main/completion/task.yaml 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/tests/main/completion/task.yaml 2018-05-16 08:20:08.000000000 +0000 @@ -9,7 +9,9 @@ NAMES: /var/cache/snapd/names prepare: | - systemctl stop snapd.service snapd.socket + . "$TESTSLIB/systemd.sh" + + systemd_stop_units snapd.service snapd.socket [ -e "$NAMES" ] && mv "$NAMES" "$NAMES.orig" cat >"$NAMES" <test.img + mount -t vfat test.img /boot/uboot + n=$(ls /boot/uboot | grep ^uboot.env$ | wc -l) + if [ "$n" != "2" ]; then + echo "Image not broken in the right way, expected two uboot.env files" + ls /boot/uboot + exit 1 + fi + + echo "Trigger cleanup" + systemctl restart snapd.core-fixup.service + + n=$(ls /boot/uboot | grep ^uboot.env$ | wc -l) + if [ "$n" != "1" ]; then + echo "Image not repaired" + ls /boot/uboot + exit 1 + fi Binary files /tmp/tmp4OuFJA/YuzmSqdfjj/snapd-2.32.3.2/tests/main/snap-core-fixup/test.img.xz and /tmp/tmp4OuFJA/pdkogafPOb/snapd-2.32.9/tests/main/snap-core-fixup/test.img.xz differ diff -Nru snapd-2.32.3.2/tests/main/snap-service/task.yaml snapd-2.32.9/tests/main/snap-service/task.yaml --- snapd-2.32.3.2/tests/main/snap-service/task.yaml 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/tests/main/snap-service/task.yaml 2018-05-16 08:20:08.000000000 +0000 @@ -17,3 +17,12 @@ while ! systemctl status snap.test-snapd-service.test-snapd-service|grep "reloading reloading reloading"; do sleep 1 done + + echo "A snap that refuses to stop is killed eventually" + snap stop test-snapd-service.test-snapd-service-refuses-to-stop + # systemd in 14.04 does not provide the "Result: timeout" information + if [[ "$SPREAD_SYSTEM" == ubuntu-14.04-* ]]; then + systemctl status snap.test-snapd-service.test-snapd-service-refuses-to-stop|MATCH "code=killed" + else + systemctl status snap.test-snapd-service.test-snapd-service-refuses-to-stop|MATCH "Result: timeout" + fi diff -Nru snapd-2.32.3.2/tests/main/snap-service-refresh-mode/task.yaml snapd-2.32.9/tests/main/snap-service-refresh-mode/task.yaml --- snapd-2.32.3.2/tests/main/snap-service-refresh-mode/task.yaml 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/tests/main/snap-service-refresh-mode/task.yaml 2018-05-16 08:20:08.000000000 +0000 @@ -1,46 +1,32 @@ summary: "Check that refresh-modes works" -kill-timeout: 3m +kill-timeout: 5m debug: | - journalctl -u snap.test-snapd-service.test-snapd-endure-service + grep -n '' *.pid || true + systemctl status snap.test-snapd-service.test-snapd-endure-service || true execute: | echo "When the service snap is installed" + . $TESTSLIB/snaps.sh install_local test-snapd-service echo "We can see it running" systemctl status snap.test-snapd-service.test-snapd-endure-service|MATCH "running" + systemctl show -p MainPID snap.test-snapd-service.test-snapd-endure-service > old-main.pid echo "When it is re-installed" install_local test-snapd-service - # note that we do not spread test sigterm{,-all} because catching this - # signal in the service means the stop will timeout with a 90s delay - refresh_modes="endure sighup sighup-all sigusr1 sigusr1-all sigusr2 sigusr2-all" - for s in $refresh_modes; do - echo "We can still see it running" - systemctl status snap.test-snapd-service.test-snapd-${s}-service|MATCH "running" - - echo "And it is not re-started" - if journalctl -u snap.test-snapd-service.test-snapd-${s}-service | grep "stop ${s}"; then - echo "reinstall did stop a service that shouldn't" - exit 1 - fi - - if [[ "$s" == sig* ]]; then - echo "checking that the right signal was sent" - sleep 1 - journalctl -u snap.test-snapd-service.test-snapd-${s}-service | MATCH "got ${s%%-all}" - fi - done + echo "We can still see it running with the same PID" + systemctl show -p MainPID snap.test-snapd-service.test-snapd-endure-service > new-main.pid - echo "But regular services are restarted" - journalctl -u snap.test-snapd-service.test-snapd-service | MATCH "stop service" + test "$(cat new-endure.pid)" = "$(cat old-endure.pid)" echo "Once the snap is removed, the service is stopped" snap remove test-snapd-service - for s in $refresh_modes; do - journalctl | MATCH "stop ${s}" - done + journalctl | MATCH "stop endure" + +restore: + rm -f *.pid || true diff -Nru snapd-2.32.3.2/tests/main/snap-service-stop-mode/task.yaml snapd-2.32.9/tests/main/snap-service-stop-mode/task.yaml --- snapd-2.32.3.2/tests/main/snap-service-stop-mode/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/tests/main/snap-service-stop-mode/task.yaml 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,54 @@ +summary: "Check that stop-modes works" + +kill-timeout: 5m + +# journald in ubuntu-14.04 not reliable +systems: [-ubuntu-14.04-*] + +debug: | + stop_modes="sighup sighup-all sigusr1 sigusr1-all sigusr2 sigusr2-all" + for s in $stop_modes; do + systemctl status snap.test-snapd-service.test-snapd-${s}-service || true + done + +execute: | + echo "When the service snap is installed" + . $TESTSLIB/snaps.sh + install_local test-snapd-service + + echo "We can see it running" + systemctl status snap.test-snapd-service.test-snapd-service|MATCH "running" + + stop_modes="sighup sighup-all sigusr1 sigusr1-all sigusr2 sigusr2-all" + for s in $stop_modes; do + systemctl show -p ActiveState snap.test-snapd-service.test-snapd-${s}-service | MATCH "ActiveState=active" + done + + echo "When it is re-installed" + install_local test-snapd-service + + # note that sigterm{,-all} is tested separately + for s in $stop_modes; do + echo "We can see it is running" + systemctl show -p ActiveState snap.test-snapd-service.test-snapd-${s}-service | MATCH "ActiveState=active" + + echo "and it got the right signal" + echo "checking that the right signal was sent" + sleep 1 + journalctl -u snap.test-snapd-service.test-snapd-${s}-service | MATCH "got ${s%%-all}" + done + + echo "Regular services are restarted normally" + journalctl -u snap.test-snapd-service.test-snapd-service | MATCH "stop service" + + echo "Once the snap is removed, all services are stopped" + snap remove test-snapd-service + for s in $stop_modes; do + journalctl | MATCH "stop ${s}" + done + +restore: | + rm -f *.pid || true + # remove to ensure all services are stopped + snap remove test-snapd-service || true + killall sleep || true diff -Nru snapd-2.32.3.2/tests/main/snap-service-stop-mode-sigkill/task.yaml snapd-2.32.9/tests/main/snap-service-stop-mode-sigkill/task.yaml --- snapd-2.32.3.2/tests/main/snap-service-stop-mode-sigkill/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/tests/main/snap-service-stop-mode-sigkill/task.yaml 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,37 @@ +summary: "Check that refresh-modes sigkill works" + +kill-timeout: 5m + +restore: | + # remove to ensure all services are stopped + snap remove test-snapd-service || true + killall sleep || true + +debug: | + ps afx + +execute: | + echo "Ensure no stray sleep processes are around" + killall sleep || true + + echo "When the service snap is installed" + . $TESTSLIB/snaps.sh + install_local test-snapd-service + + refresh_modes="sigterm sigterm-all" + for s in $refresh_modes; do + systemctl show -p ActiveState snap.test-snapd-service.test-snapd-${s}-service | MATCH "ActiveState=active" + done + + echo "we expect two sleep processes (children) from the two sigterm services" + n=$(ps afx | grep [3]133731337 | grep -v grep | wc -l) + [ "$n" = "2" ] + + echo "When it is re-installed one process uses sigterm, the other sigterm-all" + install_local test-snapd-service + + echo "After reinstall the sigterm-all service and all children got killed" + echo "but the sigterm service only got a kill for the main process " + echo "and one sleep is still alive" + n=$(ps afx | grep [3]133731337 | wc -l) + [ "$n" = "3" ] diff -Nru snapd-2.32.3.2/tests/main/snap-userd-desktop-app-autostart/task.yaml snapd-2.32.9/tests/main/snap-userd-desktop-app-autostart/task.yaml --- snapd-2.32.3.2/tests/main/snap-userd-desktop-app-autostart/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/tests/main/snap-userd-desktop-app-autostart/task.yaml 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,42 @@ +summary: Check that snap userd can autostart user session applications + +execute: | + echo "When the snap is installed" + . $TESTSLIB/snaps.sh + install_local test-snapd-xdg-autostart + + # run the app directly, it will dump a *.desktop file + snap run test-snapd-xdg-autostart.foo + + echo "And snap application autostart file exists" + test -e ~/snap/test-snapd-xdg-autostart/current/.config/autostart/foo.desktop + + echo "Applications can be automatically started by snap userd --autostart" + + test ! -e ~/snap/test-xdg-snap-autostart/current/foo-autostarted + if [[ "$SPREAD_SYSTEM" != ubuntu-14.04-* ]]; then + # 14.04 journalctl does not have cursor support + cursor=$(journalctl --show-cursor -n 0 |grep cursor | cut -f3 -d' ') + fi + snap userd --autostart > autostart.log 2>&1 + + # when app is autostarted it dumps a file at $SNAP_USER_DATA/foo-autostarted, + # applications are forked by userd, but userd does not wait for them + for i in `seq 20`; do + test -e ~/snap/test-snapd-xdg-autostart/current/foo-autostarted && break + sleep 1 + done + test -e ~/snap/test-snapd-xdg-autostart/current/foo-autostarted + + if [[ "$SPREAD_SYSTEM" != ubuntu-14.04-* ]]; then + journalctl --flush + test "$(journalctl -t foo.desktop --after-cursor=$cursor | wc -l)" -eq 1 + else + # 14.04 journalctl does not have cursor, neither --identifier support, + # nor --flush + test "$(journalctl | grep foo.desktop | wc -l)" -eq 1 + fi + +restore: | + rm -f ~/snap/test-snapd-xdg-autostart/current/foo-autostarted + rm -f ~/snap/test-snapd-xdg-autostart/current/.config/autostart/foo.desktop diff -Nru snapd-2.32.3.2/tests/main/snap-wait/task.yaml snapd-2.32.9/tests/main/snap-wait/task.yaml --- snapd-2.32.3.2/tests/main/snap-wait/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/tests/main/snap-wait/task.yaml 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,26 @@ +summary: Check that `snap wait` works + +kill-timeout: 2m + +prepare: | + . "$TESTSLIB/snaps.sh" + install_local basic-hooks + +execute: | + echo "Ensure snap wait for seeding works" + snap wait system seed.loaded + + echo "Ensure snap wait for arbitrary stuff works" + # set to a false value + snap set basic-hooks foo=0 + # keep track + start=$(date +%s) + # ensure we wait 3s before the false value becomes true + ((sleep 3; snap set basic-hooks foo=1)&) + snap wait basic-hooks foo + end=$(date +%s) + # ensure we waited + if [ $((end-start)) -lt 2 ]; then + echo "snap wait returned too early" + exit 1 + fi diff -Nru snapd-2.32.3.2/tests/main/system-core-alias/task.yaml snapd-2.32.9/tests/main/system-core-alias/task.yaml --- snapd-2.32.3.2/tests/main/system-core-alias/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/tests/main/system-core-alias/task.yaml 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,19 @@ +summary: Verify that 'system' can be used as an alias for 'core' + +execute: | + echo "When a configuration is set for the core snap" + snap set core foo.bar=true + snap get core foo.bar | MATCH "true" + + echo "It can be retrieved using system alias" + snap get system foo.bar | MATCH "true" + + echo "When a configuration is set using the system alias" + snap set system foo.bar=baz + snap get system foo.bar | MATCH "baz" + + echo "It is also visible through the core snap" + snap get core foo.bar | MATCH "baz" +debug: | + snap get core -d || true + snap get system -d || true diff -Nru snapd-2.32.3.2/.travis.yml snapd-2.32.9/.travis.yml --- snapd-2.32.3.2/.travis.yml 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/.travis.yml 2018-05-16 08:20:08.000000000 +0000 @@ -10,6 +10,8 @@ - secure: "LjqfvJ2xz/7cxt1Cywaw5l8gaj5jOhUsf502UeaH+rOnj+9tCdWTtyP8U4nOjjQwiJ0xuygba+FgdnXEyxV+THeXHOF69SRF/1N8JIc3i9G6JK/CqDfFTRMqiRaCf5u7KuOrYZ0ssYNBXyZ8X4Ahls3uFu2DgEuAim1J6wOVSgIoUkduLVrbsn6uB9G5Uuc+C4NMA3TH21IJ6ct35t3T+/EjvoGUHcKtoOsPXdBZvz96xw5mKGIBaLpZdy5WxmhPUsz3MIlZgvi4DR3YIa/9u+QoGNU05f8upJRhwdwkuu9vJwqekXNXDJi/ZGlpkkAPx0feJbyhtz68551Pn1TtmA3TS5JtuMeMZWxCL9SudA7/C3oBRNGnKI3LwvP20pPjdlEYMOCq/oHlxoJylGVdpynZXTtaFS+s4Qhnr+WuNcG3zFa9bJvXPyy1vxPKcjI2DojneTrCTW/L6zg7tBIVQGzTxmC7QWsbTvOQzu+YICyeeS3g+iJ+QyP6+/oTyER3a3vmZCtXqsBJTznesS0SL5AkK+8moBGct96S6kT55XCDVgThWV0OGH6l4LwVSOjPioNzXNhVLZ8GKkXrMZXKSaWAeYptzWl4Gfz0Y4nFCu3aqIOyie7janPPgeEL0E2ZjndIs+ZigtN1LCol+GJN7fXzUFy8Fichqhhwvb3YLyE=" # SPREAD_STORE_PASSWORD - secure: "Le4CMhklfadi4aBQIEaEMbsFIB608GOvSHjVUxkDxkkUAVwl/Ov4Dni5d0Tn4e/xcxPkcm+pPg64dn0Jxzwx6XfWlxhWC10vYh+/GjpZW1znahtb/Gf9CNZOJJEy5LSeI7/uJ3LYcFd0FU0EJSerNeQJc5d8jmJH8UnuqObHOk29YD//XILiLRa1XALEimwXeQyGQePBmDTxPQQv1VLFjgfaJa5Xy55Us7AKTML2V7lhaeKCSEIp3x9liLAtnKlJhyXaXO/e4b3ZJTgXwYh+vENK1E2pxalpjBNPaJNkvtbsjFtYNXJoXca+hBVs5Sq1PCBhkEGxqFUsD8VLQd+MEXp4MYOF5fBhxIa3qOSjtuR+WmZ9G6fEysEBV6Y3F3D6HYWTpNkcHNXJCwdtOM+n92zNEBDIrufwzTPpyJXpoxZCCXrk3HHRdyDktvJYLrHdn1bM19mgYguesMZHTC5xMD6ifwdRoylmApjImXOvVxf2HdQiNvNLDqvaHgmYwNfl0+KbaVz+O2EDPCRnT5wOCpSeSUet47EPITdjr5OnTwLpOVaY+iSvn90EUB/8+ZU01TRYgc+6VNPHokLVjuiQJSrE4yTx/c2MnY9eRaOosVXngYfoS/L3XwDwZiQoeLZs04bScvxzGQIGCJ+CBzNPENtZ4AUh55Yl/vVNReZJeaY=" + # SPREAD_GOOGLE_KEY + - secure: "dIA2HrartowFL2Gl5jXiVMd9hIJyIeummYwxeBL9MzO48E/BIJyIGHudEOo8oCnZ5a0yb8TqYgND2FCgJU1V5I2LyxH6T9kizHjtmIGgeM4qlEGKRlptb2v7DFkaHeW4Mpp4gLk8hYIeWyq9OR+SlK6f0Jj049LLKfQoX6GzTPug5+MMEQOJs55OJ6f6gvCv2o3oj6WFybaohMCO4GbNYQSPLwheyTSkT0efnW9QqTN0w62pDMqscVURO90/CUeZyCcXw2uOBegwPNTBoo/+4+nZsfSNeupV8wX4vVYL0ZFL6IO3mViDoZBD4SGTNF/9x8Lc1WeKm9HlELzy5krdLqsvdV/fQSWhBzwkdykKVA3Aae5dAMIGRt7e5bJaUg+/HdtOgA5jr+qey/c/BN11MyaSOMNPNGjRuv9NAcEjxoN2JkiDXfpA3lE9kjd7TBTexGe4RJGJLJjT9s8XxdKufBfruC/yhVGdVkRoc2tsAJPZ72Ds9qH0FH28zNFAgAitCLDfInjhPMPvZJhb3Bqx5P/0DE5zUbduE9kYK0iiZRJ4AaytQy+R4nJCXE42mWv5cxoE84opVqO9cBu1TPCC8gTRQFWpJt1rP+DvwjaFiswvptG8obxNpHmkhcItPGmRVN9P9Yjd9nHvegS83tsbrd2KOyMmCk3/1KWhLufisHE=" git: quiet: true diff -Nru snapd-2.32.3.2/userd/autostart.go snapd-2.32.9/userd/autostart.go --- snapd-2.32.3.2/userd/autostart.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/userd/autostart.go 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,225 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package userd + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "log/syslog" + "os" + "os/exec" + "os/user" + "path/filepath" + "sort" + "strings" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/strutil/shlex" + "github.com/snapcore/snapd/systemd" +) + +// expandDesktopFields processes the input string and expands any % +// patterns. '%%' expands to '%', all other patterns expand to empty strings. +func expandDesktopFields(in string) string { + raw := []rune(in) + out := make([]rune, 0, len(raw)) + + var hasKey bool + for _, r := range raw { + if hasKey { + hasKey = false + // only allow %% -> % expansion, drop other keys + if r == '%' { + out = append(out, r) + } + continue + } else if r == '%' { + hasKey = true + continue + } + out = append(out, r) + } + return string(out) +} + +// Note: consider wrappers/desktop.go if we start parsing more than Exec line +func findExec(desktopFileContent []byte) (string, error) { + scanner := bufio.NewScanner(bytes.NewBuffer(desktopFileContent)) + execCmd := "" + for scanner.Scan() { + bline := scanner.Bytes() + + if !bytes.HasPrefix(bline, []byte("Exec=")) { + continue + } + + full := string(bline[len("Exec="):]) + execCmd = expandDesktopFields(full) + break + } + if err := scanner.Err(); err != nil { + return "", err + } + + execCmd = strings.TrimSpace(execCmd) + if execCmd == "" { + return "", fmt.Errorf("Exec not found or invalid") + } + return execCmd, nil +} + +func autostartCmd(snapName, desktopFilePath string) (*exec.Cmd, error) { + desktopFile := filepath.Base(desktopFilePath) + + info, err := snap.ReadCurrentInfo(snapName) + if err != nil { + return nil, err + } + + var app *snap.AppInfo + for _, candidate := range info.Apps { + if candidate.Autostart == desktopFile { + app = candidate + break + } + } + if app == nil { + return nil, fmt.Errorf("cannot match desktop file with snap %s applications", snapName) + } + + content, err := ioutil.ReadFile(desktopFilePath) + if err != nil { + return nil, err + } + + // NOTE: Ignore all fields and just look for Exec=..., this also means + // that fields with meaning such as TryExec, X-GNOME-Autostart and so on + // are ignored + + command, err := findExec(content) + if err != nil { + return nil, fmt.Errorf("cannot determine startup command for application %s in snap %s: %v", app.Name, snapName, err) + } + logger.Debugf("exec line: %v", command) + + split, err := shlex.Split(command) + if err != nil { + return nil, fmt.Errorf("invalid application startup command: %v", err) + } + + // NOTE: Ignore the actual argv[0] in Exec=.. line and replace it with a + // command of the snap application. Any arguments passed in the Exec=.. + // line to the original command are preserved. + cmd := exec.Command(app.WrapperPath(), split[1:]...) + return cmd, nil +} + +// failedAutostartError keeps track of errors that occurred when starting an +// application for a specific desktop file, desktop file name is as a key +type failedAutostartError map[string]error + +func (f failedAutostartError) Error() string { + var out bytes.Buffer + + dfiles := make([]string, 0, len(f)) + for desktopFile := range f { + dfiles = append(dfiles, desktopFile) + } + sort.Strings(dfiles) + for _, desktopFile := range dfiles { + fmt.Fprintf(&out, "- %q: %v\n", desktopFile, f[desktopFile]) + } + return out.String() +} + +func makeStdStreams(identifier string) (stdout *os.File, stderr *os.File) { + var err error + + stdout, err = systemd.NewJournalStreamFile(identifier, syslog.LOG_INFO, false) + if err != nil { + logger.Noticef("failed to set up stdout journal stream for %q: %v", identifier, err) + stdout = os.Stdout + } + + stderr, err = systemd.NewJournalStreamFile(identifier, syslog.LOG_WARNING, false) + if err != nil { + logger.Noticef("failed to set up stderr journal stream for %q: %v", identifier, err) + stderr = os.Stderr + } + + return stdout, stderr +} + +var userCurrent = user.Current + +// AutostartSessionApps starts applications which have placed their desktop +// files in $SNAP_USER_DATA/.config/autostart +// +// NOTE: By the spec, the actual path is $SNAP_USER_DATA/${XDG_CONFIG_DIR}/autostart +func AutostartSessionApps() error { + usr, err := userCurrent() + if err != nil { + return err + } + + usrSnapDir := filepath.Join(usr.HomeDir, dirs.UserHomeSnapDir) + + glob := filepath.Join(usrSnapDir, "*/current/.config/autostart/*.desktop") + matches, err := filepath.Glob(glob) + if err != nil { + return err + } + + failedApps := make(failedAutostartError) + for _, desktopFilePath := range matches { + desktopFile := filepath.Base(desktopFilePath) + logger.Debugf("autostart desktop file %v", desktopFile) + + // /home/foo/snap/some-snap/current/.config/autostart/some-app.desktop -> + // some-snap/current/.config/autostart/some-app.desktop + noHomePrefix := strings.TrimPrefix(desktopFilePath, usrSnapDir+"/") + // some-snap/current/.config/autostart/some-app.desktop -> some-snap + snapName := noHomePrefix[0:strings.IndexByte(noHomePrefix, '/')] + + logger.Debugf("snap name: %q", snapName) + + cmd, err := autostartCmd(snapName, desktopFilePath) + if err != nil { + failedApps[desktopFile] = err + continue + } + + // similarly to gnome-session, use the desktop file name as + // identifier, see: + // https://github.com/GNOME/gnome-session/blob/099c19099de8e351f6cc0f2110ad27648780a0fe/gnome-session/gsm-autostart-app.c#L948 + cmd.Stdout, cmd.Stderr = makeStdStreams(desktopFile) + if err := cmd.Start(); err != nil { + failedApps[desktopFile] = fmt.Errorf("cannot autostart %q: %v", desktopFile, err) + } + } + if len(failedApps) > 0 { + return failedApps + } + return nil +} diff -Nru snapd-2.32.3.2/userd/autostart_test.go snapd-2.32.9/userd/autostart_test.go --- snapd-2.32.3.2/userd/autostart_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.32.9/userd/autostart_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -0,0 +1,244 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package userd_test + +import ( + "io/ioutil" + "os" + "os/user" + "path" + "path/filepath" + "strings" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" + "github.com/snapcore/snapd/userd" +) + +type autostartSuite struct { + dir string + autostartDir string + userDir string + userCurrentRestore func() +} + +var _ = Suite(&autostartSuite{}) + +func (s *autostartSuite) SetUpTest(c *C) { + s.dir = c.MkDir() + dirs.SetRootDir(s.dir) + snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}) + + s.userDir = path.Join(s.dir, "home") + s.autostartDir = path.Join(s.userDir, ".config", "autostart") + s.userCurrentRestore = userd.MockUserCurrent(func() (*user.User, error) { + return &user.User{HomeDir: s.userDir}, nil + }) + + err := os.MkdirAll(s.autostartDir, 0755) + c.Assert(err, IsNil) +} + +func (s *autostartSuite) TearDownTest(c *C) { + s.dir = c.MkDir() + dirs.SetRootDir("/") + if s.userCurrentRestore != nil { + s.userCurrentRestore() + } +} + +func (s *autostartSuite) TestFindExec(c *C) { + allGood := `[Desktop Entry] +Exec=foo --bar +` + allGoodWithFlags := `[Desktop Entry] +Exec=foo --bar "%%p" %U %D +%s %% +` + noExec := `[Desktop Entry] +Type=Application +` + emptyExec := `[Desktop Entry] +Exec= +` + onlySpacesExec := `[Desktop Entry] +Exec= +` + for i, tc := range []struct { + in string + out string + err string + }{{ + in: allGood, + out: "foo --bar", + }, { + in: noExec, + err: "Exec not found or invalid", + }, { + in: emptyExec, + err: "Exec not found or invalid", + }, { + in: onlySpacesExec, + err: "Exec not found or invalid", + }, { + in: allGoodWithFlags, + out: `foo --bar "%p" + %`, + }} { + c.Logf("tc %d", i) + + cmd, err := userd.FindExec([]byte(tc.in)) + if tc.err != "" { + c.Check(cmd, Equals, "") + c.Check(err, ErrorMatches, tc.err) + } else { + c.Check(err, IsNil) + c.Check(cmd, Equals, tc.out) + } + } +} + +var mockYaml = `name: snapname +version: 1.0 +apps: + foo: + command: run-app + autostart: foo-stable.desktop +` + +func (s *autostartSuite) TestTryAutostartAppValid(c *C) { + si := snaptest.MockSnapCurrent(c, mockYaml, &snap.SideInfo{Revision: snap.R("x2")}) + + appWrapperPath := si.Apps["foo"].WrapperPath() + err := os.MkdirAll(filepath.Dir(appWrapperPath), 0755) + c.Assert(err, IsNil) + + appCmd := testutil.MockCommand(c, appWrapperPath, "") + defer appCmd.Restore() + + fooDesktopFile := filepath.Join(s.autostartDir, "foo-stable.desktop") + writeFile(c, fooDesktopFile, + []byte(`[Desktop Entry] +Exec=this-is-ignored -a -b --foo="a b c" -z "dev" +`)) + + cmd, err := userd.AutostartCmd("snapname", fooDesktopFile) + c.Assert(err, IsNil) + c.Assert(cmd.Path, Equals, appWrapperPath) + + err = cmd.Start() + c.Assert(err, IsNil) + cmd.Wait() + + c.Assert(appCmd.Calls(), DeepEquals, + [][]string{ + { + filepath.Base(appWrapperPath), + "-a", + "-b", + "--foo=a b c", + "-z", + "dev", + }, + }) +} + +func (s *autostartSuite) TestTryAutostartAppNoMatchingApp(c *C) { + snaptest.MockSnapCurrent(c, mockYaml, &snap.SideInfo{Revision: snap.R("x2")}) + + fooDesktopFile := filepath.Join(s.autostartDir, "foo-no-match.desktop") + writeFile(c, fooDesktopFile, + []byte(`[Desktop Entry] +Exec=this-is-ignored -a -b --foo="a b c" -z "dev" +`)) + + cmd, err := userd.AutostartCmd("snapname", fooDesktopFile) + c.Assert(cmd, IsNil) + c.Assert(err, ErrorMatches, `cannot match desktop file with snap snapname applications`) +} + +func (s *autostartSuite) TestTryAutostartAppNoSnap(c *C) { + fooDesktopFile := filepath.Join(s.autostartDir, "foo-stable.desktop") + writeFile(c, fooDesktopFile, + []byte(`[Desktop Entry] +Exec=this-is-ignored -a -b --foo="a b c" -z "dev" +`)) + + cmd, err := userd.AutostartCmd("snapname", fooDesktopFile) + c.Assert(cmd, IsNil) + c.Assert(err, ErrorMatches, `cannot find current revision for snap snapname.*`) +} + +func (s *autostartSuite) TestTryAutostartAppBadExec(c *C) { + snaptest.MockSnapCurrent(c, mockYaml, &snap.SideInfo{Revision: snap.R("x2")}) + + fooDesktopFile := filepath.Join(s.autostartDir, "foo-stable.desktop") + writeFile(c, fooDesktopFile, + []byte(`[Desktop Entry] +Foo=bar +`)) + + cmd, err := userd.AutostartCmd("snapname", fooDesktopFile) + c.Assert(cmd, IsNil) + c.Assert(err, ErrorMatches, `cannot determine startup command for application foo in snap snapname: Exec not found or invalid`) +} + +func writeFile(c *C, path string, content []byte) { + err := os.MkdirAll(filepath.Dir(path), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(path, content, 0644) + c.Assert(err, IsNil) +} + +func (s *autostartSuite) TestTryAutostartMany(c *C) { + var mockYamlTemplate = `name: {snap} +version: 1.0 +apps: + foo: + command: run-app + autostart: foo-stable.desktop +` + + snaptest.MockSnapCurrent(c, strings.Replace(mockYamlTemplate, "{snap}", "a-foo", -1), + &snap.SideInfo{Revision: snap.R("x2")}) + snaptest.MockSnapCurrent(c, strings.Replace(mockYamlTemplate, "{snap}", "b-foo", -1), + &snap.SideInfo{Revision: snap.R("x2")}) + writeFile(c, filepath.Join(s.userDir, "snap/a-foo/current/.config/autostart/foo-stable.desktop"), + []byte(`[Desktop Entry] +Foo=bar +`)) + writeFile(c, filepath.Join(s.userDir, "snap/b-foo/current/.config/autostart/no-match.desktop"), + []byte(`[Desktop Entry] +Exec=no-snap +`)) + writeFile(c, filepath.Join(s.userDir, "snap/c-foo/current/.config/autostart/no-snap.desktop"), + []byte(`[Desktop Entry] +Exec=no-snap +`)) + + err := userd.AutostartSessionApps() + c.Assert(err, NotNil) + c.Check(err, ErrorMatches, `- "foo-stable.desktop": cannot determine startup command for application foo in snap a-foo: Exec not found or invalid +- "no-match.desktop": cannot match desktop file with snap b-foo applications +- "no-snap.desktop": cannot find current revision for snap c-foo: readlink.*no such file or directory +`) +} diff -Nru snapd-2.32.3.2/userd/export_test.go snapd-2.32.9/userd/export_test.go --- snapd-2.32.3.2/userd/export_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/userd/export_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -20,11 +20,16 @@ package userd import ( + "os/user" + "github.com/godbus/dbus" ) var ( SnapFromPid = snapFromPid + + FindExec = findExec + AutostartCmd = autostartCmd ) func MockSnapFromSender(f func(*dbus.Conn, dbus.Sender) (string, error)) func() { @@ -34,3 +39,11 @@ snapFromSender = origSnapFromSender } } + +func MockUserCurrent(f func() (*user.User, error)) func() { + origUserCurrent := userCurrent + userCurrent = f + return func() { + userCurrent = origUserCurrent + } +} diff -Nru snapd-2.32.3.2/wrappers/services_gen_test.go snapd-2.32.9/wrappers/services_gen_test.go --- snapd-2.32.3.2/wrappers/services_gen_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/wrappers/services_gen_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -333,3 +333,44 @@ c.Logf("service: \n%v\n", string(generatedWrapper)) c.Assert(string(generatedWrapper), Equals, expectedService) } + +func (s *servicesWrapperGenSuite) TestKillModeSig(c *C) { + for _, rm := range []string{"sigterm", "sighup", "sigusr1", "sigusr2"} { + service := &snap.AppInfo{ + Snap: &snap.Info{ + SuggestedName: "snap", + Version: "0.3.4", + SideInfo: snap.SideInfo{Revision: snap.R(44)}, + }, + Name: "app", + Command: "bin/foo start", + Daemon: "simple", + StopMode: snap.StopModeType(rm), + } + + generatedWrapper, err := wrappers.GenerateSnapServiceFile(service) + c.Assert(err, IsNil) + + c.Check(string(generatedWrapper), Equals, fmt.Sprintf(`[Unit] +# Auto-generated, DO NOT EDIT +Description=Service for snap application snap.app +Requires=%s-snap-44.mount +Wants=network-online.target +After=%s-snap-44.mount network-online.target +X-Snappy=yes + +[Service] +ExecStart=/usr/bin/snap run snap.app +SyslogIdentifier=snap.app +Restart=on-failure +WorkingDirectory=/var/snap/snap/44 +TimeoutStopSec=30 +Type=simple +KillMode=process +KillSignal=%s + +[Install] +WantedBy=multi-user.target +`, mountUnitPrefix, mountUnitPrefix, strings.ToUpper(rm))) + } +} diff -Nru snapd-2.32.3.2/wrappers/services.go snapd-2.32.9/wrappers/services.go --- snapd-2.32.3.2/wrappers/services.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/wrappers/services.go 2018-05-16 08:20:08.000000000 +0000 @@ -233,42 +233,27 @@ // Skip stop on refresh when refresh mode is set to something // other than "restart" (or "" which is the same) if reason == snap.StopReasonRefresh { - logger.Debugf(" %s refresh-mode: %v", app.Name, app.RefreshMode) + logger.Debugf(" %s refresh-mode: %v", app.Name, app.StopMode) switch app.RefreshMode { case "endure": // skip this service continue - case "sigterm": - sysd.Kill(app.ServiceName(), "TERM", "main") - continue - case "sigterm-all": - sysd.Kill(app.ServiceName(), "TERM", "all") - continue - case "sighup": - sysd.Kill(app.ServiceName(), "HUP", "main") - continue - case "sighup-all": - sysd.Kill(app.ServiceName(), "HUP", "all") - continue - case "sigusr1": - sysd.Kill(app.ServiceName(), "USR1", "main") - continue - case "sigusr1-all": - sysd.Kill(app.ServiceName(), "USR1", "all") - continue - case "sigusr2": - sysd.Kill(app.ServiceName(), "USR2", "main") - continue - case "sigusr2-all": - sysd.Kill(app.ServiceName(), "USR2", "all") - continue - case "", "restart": - // do nothing here, the default below to stop } } if err := stopService(sysd, app, inter); err != nil { return err } + + // ensure the service is really stopped on remove regardless + // of stop-mode + if reason == snap.StopReasonRemove && !app.StopMode.KillAll() { + // FIXME: make this smarter and avoid the killWait + // delay if not needed (i.e. if all processes + // have died) + sysd.Kill(app.ServiceName(), "TERM", "all") + time.Sleep(killWait) + sysd.Kill(app.ServiceName(), "KILL", "") + } } return nil @@ -367,6 +352,12 @@ {{- if .App.BusName}} BusName={{.App.BusName}} {{- end}} +{{- if .KillMode}} +KillMode={{.KillMode}} +{{- end}} +{{- if .KillSignal}} +KillSignal={{.KillSignal}} +{{- end}} {{- if not .App.Sockets}} [Install] @@ -391,6 +382,10 @@ remain = "yes" } } + var killMode string + if !appInfo.StopMode.KillAll() { + killMode = "process" + } wrapperData := struct { App *snap.AppInfo @@ -401,6 +396,8 @@ PrerequisiteTarget string MountUnit string Remain string + KillMode string + KillSignal string Before []string After []string @@ -415,8 +412,11 @@ PrerequisiteTarget: systemd.PrerequisiteTarget, MountUnit: filepath.Base(systemd.MountUnitPath(appInfo.Snap.MountDir())), Remain: remain, - Before: genServiceNames(appInfo.Snap, appInfo.Before), - After: genServiceNames(appInfo.Snap, appInfo.After), + KillMode: killMode, + KillSignal: appInfo.StopMode.KillSignal(), + + Before: genServiceNames(appInfo.Snap, appInfo.Before), + After: genServiceNames(appInfo.Snap, appInfo.After), // systemd runs as PID 1 so %h will not work. Home: "/root", diff -Nru snapd-2.32.3.2/wrappers/services_test.go snapd-2.32.9/wrappers/services_test.go --- snapd-2.32.3.2/wrappers/services_test.go 2018-04-11 10:40:09.000000000 +0000 +++ snapd-2.32.9/wrappers/services_test.go 2018-05-16 08:20:08.000000000 +0000 @@ -652,6 +652,9 @@ }) defer r() + r = wrappers.MockKillWait(1 * time.Millisecond) + defer r() + survivorFile := filepath.Join(s.tempdir, "/etc/systemd/system/snap.survive-snap.srv.service") for _, t := range []struct { mode string @@ -672,7 +675,7 @@ apps: srv: command: bin/survivor - refresh-mode: %s + stop-mode: %s daemon: simple `, t.mode) info := snaptest.MockSnap(c, surviveYaml, &snap.SideInfo{Revision: snap.R(1)}) @@ -689,16 +692,29 @@ err = wrappers.StopServices(info.Services(), snap.StopReasonRefresh, progress.Null) c.Assert(err, IsNil) c.Check(sysdLog, DeepEquals, [][]string{ - {"kill", filepath.Base(survivorFile), "-s", t.expectedSig, "--kill-who=" + t.expectedWho}, + {"stop", filepath.Base(survivorFile)}, + {"show", "--property=ActiveState", "snap.survive-snap.srv.service"}, }, Commentf("failure in %s", t.mode)) sysdLog = nil err = wrappers.StopServices(info.Services(), snap.StopReasonRemove, progress.Null) c.Assert(err, IsNil) - c.Check(sysdLog, DeepEquals, [][]string{ - {"stop", filepath.Base(survivorFile)}, - {"show", "--property=ActiveState", "snap.survive-snap.srv.service"}, - }) + switch t.expectedWho { + case "all": + c.Check(sysdLog, DeepEquals, [][]string{ + {"stop", filepath.Base(survivorFile)}, + {"show", "--property=ActiveState", "snap.survive-snap.srv.service"}, + }) + case "main": + c.Check(sysdLog, DeepEquals, [][]string{ + {"stop", filepath.Base(survivorFile)}, + {"show", "--property=ActiveState", "snap.survive-snap.srv.service"}, + {"kill", filepath.Base(survivorFile), "-s", "TERM", "--kill-who=all"}, + {"kill", filepath.Base(survivorFile), "-s", "KILL", "--kill-who=all"}, + }) + default: + panic("not reached") + } } }