diff --git a/cmd/snap-confine/mount-support.c b/cmd/snap-confine/mount-support.c index 7359e36a3d5..8b6b019a1b3 100644 --- a/cmd/snap-confine/mount-support.c +++ b/cmd/snap-confine/mount-support.c @@ -378,54 +378,35 @@ static void sc_bootstrap_mount_namespace(const struct sc_mount_config *config) sc_do_mount("none", dst, NULL, MS_SLAVE, NULL); } } - // The "core" base snap is special as it contains snapd and friends. - // Other base snaps do not, so whenever a base snap other than core is - // in use we need extra provisions for setting up internal tooling to - // be available. - // - // However on a core18 (and similar) system the core snap is not - // a special base anymore and we should map our own tooling in. - if (config->distro == SC_DISTRO_CORE_OTHER - || !sc_streq(config->base_snap_name, "core")) { - // when bases are used we need to bind-mount the libexecdir - // (that contains snap-exec) into /usr/lib/snapd of the - // base snap so that snap-exec is available for the snaps - // (base snaps do not ship snapd) - - // dst is always /usr/lib/snapd as this is where snapd - // assumes to find snap-exec - sc_must_snprintf(dst, sizeof dst, "%s/usr/lib/snapd", - scratch_dir); - - // bind mount the current $ROOT/usr/lib/snapd path, - // where $ROOT is either "/" or the "/snap/{core,snapd}/current" - // that we are re-execing from - char *src = NULL; - char self[PATH_MAX + 1] = { 0 }; - ssize_t nread; - nread = readlink("/proc/self/exe", self, sizeof self - 1); - if (nread < 0) { - die("cannot read /proc/self/exe"); - } - // Though we initialized self to NULs and passed one less to - // readlink, therefore guaranteeing that self is - // zero-terminated, perform an explicit assignment to make - // Coverity happy. - self[nread] = '\0'; - // this cannot happen except when the kernel is buggy - if (strstr(self, "/snap-confine") == NULL) { - die("cannot use result from readlink: %s", self); + /* XXX: Put tools exported by snapd over /usr/lib/snapd. */ + { + /* TODO: Use a fixed export and have the export manager put the + * appropriate tools inside, this will automatically handle all + * possible transitions. */ + static const char *providers[] = {"snapd", "core", NULL}; + bool tools_mounted = false; + sc_must_snprintf(dst, sizeof dst, "%s/usr/lib/snapd", scratch_dir); + for (const char **provider = providers; *provider != NULL; ++provider) { + struct stat sb; + sc_must_snprintf(src, sizeof src, "/var/lib/snapd/export/%s/tools/snap-exec", *provider); + /* Check that snap-exec exists, the tools directory may be empty. */ + if (lstat(src, &sb) != 0) { + continue; + } + /* Mount the entire tools directory as /usr/lib/snapd inside the mount namespace. */ + sc_must_snprintf(src, sizeof src, "/var/lib/snapd/export/%s/tools", *provider); + sc_do_mount(src, dst, NULL, MS_BIND | MS_RDONLY, NULL); + sc_do_mount("none", dst, NULL, MS_SLAVE, NULL); + tools_mounted = true; + break; } - src = dirname(self); - // dirname(path) might return '.' depending on path. - // /proc/self/exe should always point - // to an absolute path, but let's guarantee that. - if (src[0] != '/') { - die("cannot use the result of dirname(): %s", src); + if (!tools_mounted) { + /* XXX: We didn't find any exported tools. We could mount a + * snapshot of the host tools but since they would not be able to + * update, we should instead export the host tools with hostfs + * symlinks. */ + die("cannot find snapd tools"); } - - sc_do_mount(src, dst, NULL, MS_BIND | MS_RDONLY, NULL); - sc_do_mount("none", dst, NULL, MS_SLAVE, NULL); } // Bind mount the directory where all snaps are mounted. The location of // the this directory on the host filesystem may not match the location in diff --git a/cmd/snap-confine/snap-confine.apparmor.in b/cmd/snap-confine/snap-confine.apparmor.in index b7d5958e013..1e38eac0210 100644 --- a/cmd/snap-confine/snap-confine.apparmor.in +++ b/cmd/snap-confine/snap-confine.apparmor.in @@ -213,10 +213,9 @@ mount options=(rw bind) @LIBEXECDIR@/ -> /tmp/snap.rootfs_*/usr/lib/snapd/, mount options=(rw slave) -> /tmp/snap.rootfs_*/usr/lib/snapd/, - # allow making re-execed host snap-exec available inside base snaps - mount options=(ro bind) @SNAP_MOUNT_DIR@/core/*/usr/lib/snapd/ -> /tmp/snap.rootfs_*/usr/lib/snapd/, # allow making snapd snap tools available inside base snaps - mount options=(ro bind) @SNAP_MOUNT_DIR@/snapd/*/usr/lib/snapd/ -> /tmp/snap.rootfs_*/usr/lib/snapd/, + mount options=(rw bind) /var/lib/snapd/export/{core,snapd}/tools/ -> /tmp/snap.rootfs_*/usr/lib/snapd/, + mount options=(remount ro bind) -> /tmp/snap.rootfs_*/usr/lib/snapd/, mount options=(rw bind) /usr/bin/snapctl -> /tmp/snap.rootfs_*/usr/bin/snapctl, mount options=(rw slave) -> /tmp/snap.rootfs_*/usr/bin/snapctl, diff --git a/dirs/dirs.go b/dirs/dirs.go index fcad5b8610d..a5840e9c17b 100644 --- a/dirs/dirs.go +++ b/dirs/dirs.go @@ -33,6 +33,9 @@ var ( GlobalRootDir string SnapMountDir string + // This is like SnapMountDir but always gives value true only + // from inside the snap mount namespace. + SnapMountDirInsideNs string DistroLibExecDir string @@ -270,6 +273,7 @@ func SetRootDir(rootdir string) { } else { SnapMountDir = filepath.Join(rootdir, defaultSnapMountDir) } + SnapMountDirInsideNs = filepath.Join(rootdir, defaultSnapMountDir) SnapDataDir = filepath.Join(rootdir, "/var/snap") SnapDataHomeGlob = filepath.Join(rootdir, "/home/*/", UserHomeSnapDir) diff --git a/interfaces/apparmor/template.go b/interfaces/apparmor/template.go index 72d346deeab..546dbbf65c7 100644 --- a/interfaces/apparmor/template.go +++ b/interfaces/apparmor/template.go @@ -165,14 +165,15 @@ var templateCommon = ` /etc/libnl-3/{classid,pktloc} r, # apps that use libnl # For snappy reexec on 4.8+ kernels - /usr/lib/snapd/snap-exec m, + /{,snap/{core,snapd}/*/,var/lib/snapd/snap/{core,snapd}/*/}usr/lib/snapd/snap-exec m, + /{,snap/{core,snapd}/*/,var/lib/snapd/snap/{core,snapd}/*/}usr/lib/snapd/snap-exec ixr, # For gdb support - /usr/lib/snapd/snap-gdb-shim ixr, + /{,snap/{core,snapd}/*/,var/lib/snapd/snap/{core,snapd}/*/}usr/lib/snapd/snap-gdb-shim ixr, # For in-snap tab completion /etc/bash_completion.d/{,*} r, - /usr/lib/snapd/etelpmoc.sh ixr, # marshaller (see complete.sh for out-of-snap unmarshal) + /{,snap/{core,snapd}/*/,var/lib/snapd/snap/{core,snapd}/*/}usr/lib/snapd/etelpmoc.sh ixr, # marshaller (see complete.sh for out-of-snap unmarshal) /usr/share/bash-completion/bash_completion r, # user-provided completions (run in-snap) may use functions from here # uptime @@ -193,7 +194,7 @@ var templateCommon = ` # snapctl and its requirements /usr/bin/snapctl ixr, - /usr/lib/snapd/snapctl ixr, + /{,snap/{core,snapd}/*/,var/lib/snapd/snap/{core,snapd}/*/}usr/lib/snapd/snapctl ixr, @{PROC}/sys/net/core/somaxconn r, /run/snapd-snap.socket rw, diff --git a/overlord/devicestate/firstboot_preseed_test.go b/overlord/devicestate/firstboot_preseed_test.go index 3b045f680ef..b52552dda3c 100644 --- a/overlord/devicestate/firstboot_preseed_test.go +++ b/overlord/devicestate/firstboot_preseed_test.go @@ -73,6 +73,7 @@ func checkPreseedTaskStates(c *C, st *state.State) { "prerequisites": true, "prepare-snap": true, "link-snap": true, + "export-content": true, "mount-snap": true, "setup-profiles": true, "copy-snap-data": true, diff --git a/overlord/exportstate/exportmgr.go b/overlord/exportstate/exportmgr.go new file mode 100644 index 00000000000..d34c6ade0e6 --- /dev/null +++ b/overlord/exportstate/exportmgr.go @@ -0,0 +1,156 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 exportstate + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/snap" +) + +const defaultExportDir = "/var/lib/snapd/export" + +var exportDir = defaultExportDir + +func init() { + dirs.AddRootDirCallback(func(root string) { + exportDir = filepath.Join(root, defaultExportDir) + }) +} + +type ExportManager struct { + state *state.State + runner *state.TaskRunner +} + +// Manager returns a new ExportManager. +func Manager(s *state.State, runner *state.TaskRunner) *ExportManager { + manager := &ExportManager{state: s, runner: runner} + runner.AddHandler("export-content", manager.doExportContent, manager.undoExportContent) + runner.AddHandler("unexport-content", manager.doUnexportContent, manager.undoUnexportContent) + return manager +} + +// Ensure implements StateManager.Ensure. +func (m *ExportManager) Ensure() error { + return nil +} + +func (m *ExportManager) readInfo(task *state.Task) (*snap.Info, error) { + st := task.State() + st.Lock() + defer st.Unlock() + + snapsup, err := snapstate.TaskSnapSetup(task) + if err != nil { + return nil, err + } + return snapstate.Info(st, snapsup.InstanceName(), snapsup.Revision()) +} + +func (m *ExportManager) exportContent(task *state.Task, info *snap.Info) error { + // TODO: store each exported file into the state. + for _, export := range info.NamespaceExports { + if err := m.exportOne(dirs.SnapMountDirInsideNs, export); err != nil { + return fmt.Errorf("cannot export %q from snap %s, to mount namespace, as %q: %v", + export.PrivatePath, info.SnapName(), export.PublicPath, err) + } + } + for _, export := range info.HostExports { + if err := m.exportOne(dirs.SnapMountDir, export); err != nil { + return fmt.Errorf("cannot export %q from snap %s, to the host, as %q: %v", + export.PrivatePath, info.SnapName(), export.PublicPath, err) + } + } + return nil +} +func (m *ExportManager) unexportContent(task *state.Task, info *snap.Info) error { + // TODO: use the state to know what to unexport from the given snap name. + for _, export := range info.NamespaceExports { + if err := m.unexportOne(export); err != nil { + return fmt.Errorf("cannot unexport %q from snap %s: %v", + export.PrivatePath, info.SnapName(), err) + } + } + for _, export := range info.HostExports { + if err := m.unexportOne(export); err != nil { + return fmt.Errorf("cannot unexport %q from snap %s: %v", + export.PrivatePath, info.SnapName(), err) + } + } + return nil +} + +func (m *ExportManager) exportOne(baseDir string, export *snap.Export) error { + info := export.Snap + privateName := filepath.Join(baseDir, info.InstanceName(), info.Revision.String(), export.PrivatePath) + publicName := filepath.Join(exportDir, export.PublicPath) + if err := os.MkdirAll(filepath.Dir(publicName), 0755); err != nil { + return err + } + switch export.Method { + case snap.ExportMethodSymlink: + // Do we have an existing file? + fi, err := os.Lstat(publicName) + if err != nil && !os.IsNotExist(err) { + return err + } + // Verify existing symlink. + if fi != nil && fi.Mode()&os.ModeSymlink != 0 { + target, err := os.Readlink(publicName) + if err != nil { + return err + } + if target == privateName { + // Symlink is up-to-date. + return nil + } + } + // Remove existing file. + // XXX: This should never happen if we modeled the exported state. + if fi != nil { + if err := os.Remove(publicName); err != nil { + return err + } + } + // Export the current version. + return os.Symlink(privateName, publicName) + default: + return fmt.Errorf("unsupported export method %s", export.Method) + } +} + +func (m *ExportManager) unexportOne(export *snap.Export) error { + publicPath := filepath.Join(exportDir, export.PublicPath) + switch export.Method { + case snap.ExportMethodSymlink: + if err := os.Remove(publicPath); err != nil { + return err + } + default: + return fmt.Errorf("unsupported export method %s", export.Method) + } + return nil +} diff --git a/overlord/exportstate/exportstate.go b/overlord/exportstate/exportstate.go new file mode 100644 index 00000000000..1d5cd032124 --- /dev/null +++ b/overlord/exportstate/exportstate.go @@ -0,0 +1,22 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 exportstate implements the manager and state aspects responsible +// for exporting content from snaps to the host system. +package exportstate diff --git a/overlord/exportstate/handlers.go b/overlord/exportstate/handlers.go new file mode 100644 index 00000000000..439ad193c8f --- /dev/null +++ b/overlord/exportstate/handlers.go @@ -0,0 +1,58 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 exportstate + +import ( + "gopkg.in/tomb.v2" + + "github.com/snapcore/snapd/overlord/state" +) + +func (m *ExportManager) doExportContent(task *state.Task, tomb *tomb.Tomb) error { + info, err := m.readInfo(task) + if err != nil { + return err + } + return m.exportContent(task, info) +} + +func (m *ExportManager) undoExportContent(task *state.Task, tomb *tomb.Tomb) error { + info, err := m.readInfo(task) + if err != nil { + return err + } + return m.unexportContent(task, info) +} + +func (m *ExportManager) doUnexportContent(task *state.Task, tomb *tomb.Tomb) error { + info, err := m.readInfo(task) + if err != nil { + return err + } + return m.unexportContent(task, info) +} + +func (m *ExportManager) undoUnexportContent(task *state.Task, tomb *tomb.Tomb) error { + info, err := m.readInfo(task) + if err != nil { + return err + } + return m.exportContent(task, info) +} diff --git a/overlord/hookstate/ctlcmd/services_test.go b/overlord/hookstate/ctlcmd/services_test.go index a17131ab68d..af1f0f8cb7d 100644 --- a/overlord/hookstate/ctlcmd/services_test.go +++ b/overlord/hookstate/ctlcmd/services_test.go @@ -315,6 +315,7 @@ var ( "copy-snap-data", "setup-profiles", "link-snap", + "export-content", "auto-connect", "set-auto-aliases", "setup-aliases", @@ -336,6 +337,7 @@ var ( "copy-snap-data", "setup-profiles", "link-snap", + "export-content", "auto-connect", "set-auto-aliases", "setup-aliases", @@ -386,10 +388,10 @@ func (s *servicectlSuite) TestQueuedCommands(c *C) { for i := 1; i <= 2; i++ { laneTasks := chg.LaneTasks(i) c.Assert(taskKinds(laneTasks), DeepEquals, expectedTaskKinds) - c.Check(laneTasks[12].Summary(), Matches, `Run configure hook of .* snap if present`) - c.Check(laneTasks[14].Summary(), Equals, "stop of [test-snap.test-service]") - c.Check(laneTasks[15].Summary(), Equals, "start of [test-snap.test-service]") - c.Check(laneTasks[16].Summary(), Equals, "restart of [test-snap.test-service]") + c.Check(laneTasks[13].Summary(), Matches, `Run configure hook of .* snap if present`) + c.Check(laneTasks[15].Summary(), Equals, "stop of [test-snap.test-service]") + c.Check(laneTasks[16].Summary(), Equals, "start of [test-snap.test-service]") + c.Check(laneTasks[17].Summary(), Equals, "restart of [test-snap.test-service]") } } @@ -499,10 +501,10 @@ func (s *servicectlSuite) TestQueuedCommandsUpdateMany(c *C) { for i := 1; i <= 2; i++ { laneTasks := chg.LaneTasks(i) c.Assert(taskKinds(laneTasks), DeepEquals, expectedTaskKinds) - c.Check(laneTasks[17].Summary(), Matches, `Run configure hook of .* snap if present`) - c.Check(laneTasks[19].Summary(), Equals, "stop of [test-snap.test-service]") - c.Check(laneTasks[20].Summary(), Equals, "start of [test-snap.test-service]") - c.Check(laneTasks[21].Summary(), Equals, "restart of [test-snap.test-service]") + c.Check(laneTasks[18].Summary(), Matches, `Run configure hook of .* snap if present`) + c.Check(laneTasks[20].Summary(), Equals, "stop of [test-snap.test-service]") + c.Check(laneTasks[21].Summary(), Equals, "start of [test-snap.test-service]") + c.Check(laneTasks[22].Summary(), Equals, "restart of [test-snap.test-service]") } } @@ -537,10 +539,10 @@ func (s *servicectlSuite) TestQueuedCommandsSingleLane(c *C) { laneTasks := chg.LaneTasks(0) c.Assert(taskKinds(laneTasks), DeepEquals, append(installTaskKinds, "exec-command", "exec-command", "exec-command")) - c.Check(laneTasks[12].Summary(), Matches, `Run configure hook of .* snap if present`) - c.Check(laneTasks[14].Summary(), Equals, "stop of [test-snap.test-service]") - c.Check(laneTasks[15].Summary(), Equals, "start of [test-snap.test-service]") - c.Check(laneTasks[16].Summary(), Equals, "restart of [test-snap.test-service]") + c.Check(laneTasks[13].Summary(), Matches, `Run configure hook of .* snap if present`) + c.Check(laneTasks[15].Summary(), Equals, "stop of [test-snap.test-service]") + c.Check(laneTasks[16].Summary(), Equals, "start of [test-snap.test-service]") + c.Check(laneTasks[17].Summary(), Equals, "restart of [test-snap.test-service]") } func (s *servicectlSuite) TestTwoServices(c *C) { diff --git a/overlord/managers_test.go b/overlord/managers_test.go index ecb09579c46..d913ad549cf 100644 --- a/overlord/managers_test.go +++ b/overlord/managers_test.go @@ -3859,6 +3859,8 @@ func validateInstallTasks(c *C, tasks []*state.Task, name, revno string, flags i i++ c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Make snap "%s" (%s) available to the system`, name, revno)) i++ + c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Export content from snap "%s" (%s) to the system`, name, revno)) + i++ c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Automatically connect eligible plugs and slots of snap "%s"`, name)) i++ c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Set automatic aliases for snap "%s"`, name)) @@ -3901,6 +3903,8 @@ func validateRefreshTasks(c *C, tasks []*state.Task, name, revno string, flags i i++ c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Make snap "%s" (%s) available to the system`, name, revno)) i++ + c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Export content from snap "%s" (%s) to the system`, name, revno)) + i++ c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Automatically connect eligible plugs and slots of snap "%s"`, name)) i++ c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Set automatic aliases for snap "%s"`, name)) diff --git a/overlord/overlord.go b/overlord/overlord.go index aafef0d500d..280b63a8423 100644 --- a/overlord/overlord.go +++ b/overlord/overlord.go @@ -41,6 +41,7 @@ import ( "github.com/snapcore/snapd/overlord/configstate" "github.com/snapcore/snapd/overlord/configstate/proxyconf" "github.com/snapcore/snapd/overlord/devicestate" + "github.com/snapcore/snapd/overlord/exportstate" "github.com/snapcore/snapd/overlord/healthstate" "github.com/snapcore/snapd/overlord/hookstate" "github.com/snapcore/snapd/overlord/ifacestate" @@ -99,6 +100,7 @@ type Overlord struct { deviceMgr *devicestate.DeviceManager cmdMgr *cmdstate.CommandManager shotMgr *snapshotstate.SnapshotManager + xprtMgr *exportstate.ExportManager // proxyConf mediates the http proxy config proxyConf func(req *http.Request) (*url.URL, error) } @@ -177,6 +179,7 @@ func New(restartBehavior RestartBehavior) (*Overlord, error) { o.addManager(cmdstate.Manager(s, o.runner)) o.addManager(snapshotstate.Manager(s, o.runner)) + o.addManager(exportstate.Manager(s, o.runner)) if err := configstateInit(s, hookMgr); err != nil { return nil, err diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go index 4a74c3296bc..9e4de2bcaf0 100644 --- a/overlord/snapstate/snapstate.go +++ b/overlord/snapstate/snapstate.go @@ -247,6 +247,11 @@ func doInstall(st *state.State, snapst *SnapState, snapsup *SnapSetup, flags int addTask(linkSnap) prev = linkSnap + // export content + exportContent := st.NewTask("export-content", fmt.Sprintf(i18n.G("Export content from snap %q%s to the system"), snapsup.InstanceName(), revisionStr)) + addTask(exportContent) + prev = exportContent + // auto-connections autoConnect := st.NewTask("auto-connect", fmt.Sprintf(i18n.G("Automatically connect eligible plugs and slots of snap %q"), snapsup.InstanceName())) addTask(autoConnect) @@ -1695,16 +1700,19 @@ func Enable(st *state.State, name string) (*state.TaskSet, error) { linkSnap.Set("snap-setup-task", prepareSnap.ID()) linkSnap.WaitFor(setupProfiles) - // setup aliases + exportContent := st.NewTask("export-content", fmt.Sprintf(i18n.G("Export content from snap %q%s to the system"), snapsup.InstanceName(), snapst.Current)) + exportContent.Set("snap-setup-task", prepareSnap.ID()) + exportContent.WaitFor(linkSnap) + setupAliases := st.NewTask("setup-aliases", fmt.Sprintf(i18n.G("Setup snap %q aliases"), snapsup.InstanceName())) setupAliases.Set("snap-setup-task", prepareSnap.ID()) - setupAliases.WaitFor(linkSnap) + setupAliases.WaitFor(exportContent) startSnapServices := st.NewTask("start-snap-services", fmt.Sprintf(i18n.G("Start snap %q (%s) services"), snapsup.InstanceName(), snapst.Current)) startSnapServices.Set("snap-setup-task", prepareSnap.ID()) startSnapServices.WaitFor(setupAliases) - return state.NewTaskSet(prepareSnap, setupProfiles, linkSnap, setupAliases, startSnapServices), nil + return state.NewTaskSet(prepareSnap, setupProfiles, linkSnap, exportContent, setupAliases, startSnapServices), nil } // Disable sets a snap to the inactive state @@ -1751,15 +1759,19 @@ func Disable(st *state.State, name string) (*state.TaskSet, error) { removeAliases.Set("snap-setup-task", stopSnapServices.ID()) removeAliases.WaitFor(stopSnapServices) + unexportContent := st.NewTask("unexport-content", fmt.Sprintf(i18n.G("Make snap %q (%s) unavailable to the system"), snapsup.InstanceName(), snapst.Current)) + unexportContent.Set("snap-setup-task", stopSnapServices.ID()) + unexportContent.WaitFor(removeAliases) + unlinkSnap := st.NewTask("unlink-snap", fmt.Sprintf(i18n.G("Make snap %q (%s) unavailable to the system"), snapsup.InstanceName(), snapst.Current)) unlinkSnap.Set("snap-setup-task", stopSnapServices.ID()) - unlinkSnap.WaitFor(removeAliases) + unlinkSnap.WaitFor(unexportContent) removeProfiles := st.NewTask("remove-profiles", fmt.Sprintf(i18n.G("Remove security profiles of snap %q"), snapsup.InstanceName())) removeProfiles.Set("snap-setup-task", stopSnapServices.ID()) removeProfiles.WaitFor(unlinkSnap) - return state.NewTaskSet(stopSnapServices, removeAliases, unlinkSnap, removeProfiles), nil + return state.NewTaskSet(stopSnapServices, removeAliases, unexportContent, unlinkSnap, removeProfiles), nil } // canDisable verifies that a snap can be deactivated. @@ -1917,15 +1929,19 @@ func Remove(st *state.State, name string, revision snap.Revision, flags *RemoveF removeAliases.WaitFor(prev) // prev is not needed beyond here removeAliases.Set("snap-setup-task", stopSnapServices.ID()) + unexportContent := st.NewTask("unexport-content", fmt.Sprintf(i18n.G("Remove content exposed by snap %q"), name)) + unexportContent.WaitFor(removeAliases) + unexportContent.Set("snap-setup-task", stopSnapServices.ID()) + unlink := st.NewTask("unlink-snap", fmt.Sprintf(i18n.G("Make snap %q unavailable to the system"), name)) unlink.Set("snap-setup-task", stopSnapServices.ID()) - unlink.WaitFor(removeAliases) + unlink.WaitFor(unexportContent) removeSecurity := st.NewTask("remove-profiles", fmt.Sprintf(i18n.G("Remove security profile for snap %q (%s)"), name, revision)) removeSecurity.WaitFor(unlink) removeSecurity.Set("snap-setup-task", stopSnapServices.ID()) - tasks = append(tasks, removeAliases, unlink, removeSecurity) + tasks = append(tasks, removeAliases, unexportContent, unlink, removeSecurity) addNext(state.NewTaskSet(tasks...)) } diff --git a/overlord/snapstate/snapstate_install_test.go b/overlord/snapstate/snapstate_install_test.go index 1f481f9d27b..2e2f4c4d4ec 100644 --- a/overlord/snapstate/snapstate_install_test.go +++ b/overlord/snapstate/snapstate_install_test.go @@ -73,6 +73,7 @@ func verifyInstallTasks(c *C, opts, discards int, ts *state.TaskSet, st *state.S "copy-snap-data", "setup-profiles", "link-snap", + "export-content", ) expected = append(expected, "auto-connect", @@ -875,6 +876,11 @@ func (s *snapmgrTestSuite) TestInstallRunThrough(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/11"), }, + { + op: "export-content:Doing", + name: "some-snap", + revno: snap.R(11), + }, { op: "auto-connect:Doing", name: "some-snap", @@ -902,14 +908,14 @@ func (s *snapmgrTestSuite) TestInstallRunThrough(c *C) { c.Check(task.Summary(), Equals, `Download snap "some-snap" (11) from channel "some-channel"`) // check install-record present - mountTask := ta[len(ta)-11] + mountTask := ta[len(ta)-12] c.Check(mountTask.Kind(), Equals, "mount-snap") var installRecord backend.InstallRecord c.Assert(mountTask.Get("install-record", &installRecord), IsNil) c.Check(installRecord.TargetSnapExisted, Equals, false) // check link/start snap summary - linkTask := ta[len(ta)-8] + linkTask := ta[len(ta)-9] c.Check(linkTask.Summary(), Equals, `Make snap "some-snap" (11) available to the system`) startTask := ta[len(ta)-3] c.Check(startTask.Summary(), Equals, `Start snap "some-snap" (11) services`) @@ -1052,6 +1058,11 @@ func (s *snapmgrTestSuite) TestParallelInstanceInstallRunThrough(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/11"), }, + { + op: "export-content:Doing", + name: "some-snap_instance", + revno: snap.R(11), + }, { op: "auto-connect:Doing", name: "some-snap_instance", @@ -1079,7 +1090,7 @@ func (s *snapmgrTestSuite) TestParallelInstanceInstallRunThrough(c *C) { c.Check(task.Summary(), Equals, `Download snap "some-snap_instance" (11) from channel "some-channel"`) // check link/start snap summary - linkTask := ta[len(ta)-8] + linkTask := ta[len(ta)-9] c.Check(linkTask.Summary(), Equals, `Make snap "some-snap_instance" (11) available to the system`) startTask := ta[len(ta)-3] c.Check(startTask.Summary(), Equals, `Start snap "some-snap_instance" (11) services`) @@ -1160,7 +1171,7 @@ func (s *snapmgrTestSuite) TestInstallUndoRunThroughJustOneSnap(c *C) { s.settle(c) s.state.Lock() - mountTask := tasks[len(tasks)-11] + mountTask := tasks[len(tasks)-12] c.Assert(mountTask.Kind(), Equals, "mount-snap") var installRecord backend.InstallRecord c.Assert(mountTask.Get("install-record", &installRecord), IsNil) @@ -1239,6 +1250,11 @@ func (s *snapmgrTestSuite) TestInstallUndoRunThroughJustOneSnap(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/11"), }, + { + op: "export-content:Doing", + name: "some-snap", + revno: snap.R(11), + }, { op: "auto-connect:Doing", name: "some-snap", @@ -1385,6 +1401,11 @@ func (s *snapmgrTestSuite) TestInstallWithCohortRunThrough(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/666"), }, + { + op: "export-content:Doing", + name: "some-snap", + revno: snap.R(666), + }, { op: "auto-connect:Doing", name: "some-snap", @@ -1412,7 +1433,7 @@ func (s *snapmgrTestSuite) TestInstallWithCohortRunThrough(c *C) { c.Check(task.Summary(), Equals, `Download snap "some-snap" (666) from channel "some-channel"`) // check link/start snap summary - linkTask := ta[len(ta)-8] + linkTask := ta[len(ta)-9] c.Check(linkTask.Summary(), Equals, `Make snap "some-snap" (666) available to the system`) startTask := ta[len(ta)-3] c.Check(startTask.Summary(), Equals, `Start snap "some-snap" (666) services`) @@ -1548,6 +1569,11 @@ func (s *snapmgrTestSuite) TestInstallWithRevisionRunThrough(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/42"), }, + { + op: "export-content:Doing", + name: "some-snap", + revno: snap.R(42), + }, { op: "auto-connect:Doing", name: "some-snap", @@ -1575,7 +1601,7 @@ func (s *snapmgrTestSuite) TestInstallWithRevisionRunThrough(c *C) { c.Check(task.Summary(), Equals, `Download snap "some-snap" (42) from channel "some-channel"`) // check link/start snap summary - linkTask := ta[len(ta)-8] + linkTask := ta[len(ta)-9] c.Check(linkTask.Summary(), Equals, `Make snap "some-snap" (42) available to the system`) startTask := ta[len(ta)-3] c.Check(startTask.Summary(), Equals, `Start snap "some-snap" (42) services`) @@ -1720,6 +1746,11 @@ version: 1.0`) op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "mock/x1"), }, + { + op: "export-content:Doing", + name: "mock", + revno: snap.R("x1"), + }, { op: "auto-connect:Doing", name: "mock", @@ -1839,6 +1870,11 @@ epoch: 1* op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "mock/x3"), }, + { + op: "export-content:Doing", + name: "mock", + revno: snap.R("x3"), + }, { op: "auto-connect:Doing", name: "mock", @@ -1961,6 +1997,11 @@ epoch: 1* op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "mock/x1"), }, + { + op: "export-content:Doing", + name: "mock", + revno: snap.R("x1"), + }, { op: "auto-connect:Doing", name: "mock", @@ -2020,7 +2061,7 @@ version: 1.0`) s.state.Lock() // ensure only local install was run, i.e. first actions are pseudo-action current - c.Assert(s.fakeBackend.ops.Ops(), HasLen, 9) + c.Assert(s.fakeBackend.ops.Ops(), HasLen, 10) c.Check(s.fakeBackend.ops[0].op, Equals, "current") c.Check(s.fakeBackend.ops[0].old, Equals, "") // and setup-snap @@ -2204,6 +2245,11 @@ func (s *snapmgrTestSuite) TestInstallWithoutCoreRunThrough1(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "core/11"), }, + { + op: "export-content:Doing", + name: "core", + revno: snap.R(11), + }, { op: "auto-connect:Doing", name: "core", @@ -2263,6 +2309,11 @@ func (s *snapmgrTestSuite) TestInstallWithoutCoreRunThrough1(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/42"), }, + { + op: "export-content:Doing", + name: "some-snap", + revno: snap.R(42), + }, { op: "auto-connect:Doing", name: "some-snap", @@ -2345,8 +2396,8 @@ func (s *snapmgrTestSuite) TestInstallWithoutCoreTwoSnapsRunThrough(c *C) { if len(chg1.Tasks()) < len(chg2.Tasks()) { chg1, chg2 = chg2, chg1 } - c.Assert(taskKinds(chg1.Tasks()), HasLen, 28) - c.Assert(taskKinds(chg2.Tasks()), HasLen, 14) + c.Assert(taskKinds(chg1.Tasks()), HasLen, 30) + c.Assert(taskKinds(chg2.Tasks()), HasLen, 15) // FIXME: add helpers and do a DeepEquals here for the operations } @@ -2657,6 +2708,10 @@ func (s *snapmgrTestSuite) TestInstallDefaultProviderRunThrough(c *C) { }, { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "snap-content-slot/11"), + }, { + op: "export-content:Doing", + name: "snap-content-slot", + revno: snap.R(11), }, { op: "auto-connect:Doing", name: "snap-content-slot", @@ -2704,6 +2759,10 @@ func (s *snapmgrTestSuite) TestInstallDefaultProviderRunThrough(c *C) { }, { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "snap-content-plug/42"), + }, { + op: "export-content:Doing", + name: "snap-content-plug", + revno: snap.R(42), }, { op: "auto-connect:Doing", name: "snap-content-plug", @@ -2996,7 +3055,7 @@ func (s *snapmgrTestSuite) TestInstallUndoRunThroughUndoContextOptional(c *C) { s.settle(c) s.state.Lock() - mountTask := tasks[len(tasks)-11] + mountTask := tasks[len(tasks)-12] c.Assert(mountTask.Kind(), Equals, "mount-snap") var installRecord backend.InstallRecord c.Assert(mountTask.Get("install-record", &installRecord), Equals, state.ErrNoState) diff --git a/overlord/snapstate/snapstate_test.go b/overlord/snapstate/snapstate_test.go index 829e4edc1c8..2d0e27ba79c 100644 --- a/overlord/snapstate/snapstate_test.go +++ b/overlord/snapstate/snapstate_test.go @@ -243,6 +243,10 @@ func AddForeignTaskHandlers(runner *state.TaskRunner, tracker ForeignTaskTracker runner.AddHandler("transition-ubuntu-core", fakeHandler, nil) runner.AddHandler("transition-to-snapd-snap", fakeHandler, nil) + // Add fake handlers for tasks handled by the export manager + runner.AddHandler("export-content", fakeHandler, nil) + runner.AddHandler("unexport-content", fakeHandler, nil) + // Add handler to test full aborting of changes erroringHandler := func(task *state.Task, _ *tomb.Tomb) error { return errors.New("error out") @@ -414,6 +418,7 @@ func verifyUpdateTasks(c *C, opts, discards int, ts *state.TaskSet, st *state.St "copy-snap-data", "setup-profiles", "link-snap", + "export-content", ) if opts&maybeCore != 0 { expected = append(expected, "setup-profiles") @@ -464,6 +469,7 @@ func verifyRemoveTasks(c *C, ts *state.TaskSet) { "auto-disconnect", "save-snapshot", "remove-aliases", + "unexport-content", "unlink-snap", "remove-profiles", "clear-snap", @@ -478,6 +484,7 @@ func verifyCoreRemoveTasks(c *C, ts *state.TaskSet) { "run-hook[remove]", "auto-disconnect", "remove-aliases", + "unexport-content", "unlink-snap", "remove-profiles", "clear-snap", @@ -553,6 +560,7 @@ func (s *snapmgrTestSuite) testRevertTasksFullFlags(flags fullFlags, c *C) { "unlink-current-snap", "setup-profiles", "link-snap", + "export-content", "auto-connect", "set-auto-aliases", "setup-aliases", @@ -1240,6 +1248,7 @@ func (s *snapmgrTestSuite) TestRevertCreatesNoGCTasks(c *C) { "unlink-current-snap", "setup-profiles", "link-snap", + "export-content", "auto-connect", "set-auto-aliases", "setup-aliases", @@ -1269,6 +1278,7 @@ func (s *snapmgrTestSuite) TestEnableTasks(c *C) { "prepare-snap", "setup-profiles", "link-snap", + "export-content", "setup-aliases", "start-snap-services", }) @@ -1470,6 +1480,7 @@ func (s *snapmgrTestSuite) TestDisableTasks(c *C) { c.Assert(taskKinds(ts.Tasks()), DeepEquals, []string{ "stop-snap-services", "remove-aliases", + "unexport-content", "unlink-snap", "remove-profiles", }) @@ -1989,6 +2000,7 @@ func (s *snapmgrTestSuite) TestRemoveTasksAutoSnapshotDisabled(c *C) { "run-hook[remove]", "auto-disconnect", "remove-aliases", + "unexport-content", "unlink-snap", "remove-profiles", "clear-snap", @@ -2017,6 +2029,7 @@ func (s *snapmgrTestSuite) TestRemoveTasksAutoSnapshotDisabledByPurgeFlag(c *C) "run-hook[remove]", "auto-disconnect", "remove-aliases", + "unexport-content", "unlink-snap", "remove-profiles", "clear-snap", @@ -2705,6 +2718,7 @@ func (s *snapmgrTestSuite) TestUpdateAmendRunThrough(c *C) { "setup-profiles:Doing", "candidate", "link-snap", + "export-content:Doing", "auto-connect:Doing", "update-aliases", "cleanup-trash", @@ -2752,7 +2766,7 @@ func (s *snapmgrTestSuite) TestUpdateAmendRunThrough(c *C) { verifyStopReason(c, ts, "refresh") // check post-refresh hook - task = ts.Tasks()[14] + task = ts.Tasks()[15] c.Assert(task.Kind(), Equals, "run-hook") c.Assert(task.Summary(), Matches, `Run post-refresh hook of "some-snap" snap if present`) @@ -2913,6 +2927,11 @@ func (s *snapmgrTestSuite) TestUpdateRunThrough(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "services-snap/11"), }, + { + op: "export-content:Doing", + name: "services-snap", + revno: snap.R(11), + }, { op: "auto-connect:Doing", name: "services-snap", @@ -2978,7 +2997,7 @@ func (s *snapmgrTestSuite) TestUpdateRunThrough(c *C) { verifyStopReason(c, ts, "refresh") // check post-refresh hook - task = ts.Tasks()[14] + task = ts.Tasks()[15] c.Assert(task.Kind(), Equals, "run-hook") c.Assert(task.Summary(), Matches, `Run post-refresh hook of "services-snap" snap if present`) @@ -3139,6 +3158,11 @@ func (s *snapmgrTestSuite) TestParallelInstanceUpdateRunThrough(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "services-snap_instance/11"), }, + { + op: "export-content:Doing", + name: "services-snap_instance", + revno: snap.R(11), + }, { op: "auto-connect:Doing", name: "services-snap_instance", @@ -3204,7 +3228,7 @@ func (s *snapmgrTestSuite) TestParallelInstanceUpdateRunThrough(c *C) { verifyStopReason(c, ts, "refresh") // check post-refresh hook - task = ts.Tasks()[14] + task = ts.Tasks()[15] c.Assert(task.Kind(), Equals, "run-hook") c.Assert(task.Summary(), Matches, `Run post-refresh hook of "services-snap_instance" snap if present`) @@ -4322,6 +4346,11 @@ func (s *snapmgrTestSuite) TestUpdateTotalUndoRunThrough(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/11"), }, + { + op: "export-content:Doing", + name: "some-snap", + revno: snap.R(11), + }, { op: "auto-connect:Doing", name: "some-snap", @@ -5650,7 +5679,7 @@ func (s *snapmgrTestSuite) TestUpdateOneAutoAliasesScenarios(c *C) { } if scenario.update { first := tasks[j] - j += 19 + j += 20 c.Check(first.Kind(), Equals, "prerequisites") wait := false if expectedPruned["other-snap"]["aliasA"] { @@ -5846,6 +5875,11 @@ func (s *snapmgrTestSuite) TestRemoveRunThrough(c *C) { op: "remove-snap-aliases", name: "some-snap", }, + { + op: "unexport-content:Doing", + name: "some-snap", + revno: snap.R(7), + }, { op: "unlink-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), @@ -5983,6 +6017,11 @@ func (s *snapmgrTestSuite) TestParallelInstanceRemoveRunThrough(c *C) { op: "remove-snap-aliases", name: "some-snap_instance", }, + { + op: "unexport-content:Doing", + name: "some-snap_instance", + revno: snap.R(7), + }, { op: "unlink-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), @@ -6126,6 +6165,11 @@ func (s *snapmgrTestSuite) TestParallelInstanceRemoveRunThroughOtherInstances(c op: "remove-snap-aliases", name: "some-snap_instance", }, + { + op: "unexport-content:Doing", + name: "some-snap_instance", + revno: snap.R(7), + }, { op: "unlink-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), @@ -6229,6 +6273,11 @@ func (s *snapmgrTestSuite) TestRemoveWithManyRevisionsRunThrough(c *C) { op: "remove-snap-aliases", name: "some-snap", }, + { + op: "unexport-content:Doing", + name: "some-snap", + revno: snap.R(7), + }, { op: "unlink-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), @@ -6920,6 +6969,11 @@ func (s *snapmgrTestSuite) TestUpdateDoesGC(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/11"), }, + { + op: "export-content:Doing", + name: "some-snap", + revno: snap.R(11), + }, { op: "auto-connect:Doing", name: "some-snap", @@ -7106,6 +7160,11 @@ func (s *snapmgrTestSuite) TestRevertRunThrough(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/2"), }, + { + op: "export-content:Doing", + name: "some-snap", + revno: snap.R(2), + }, { op: "auto-connect:Doing", name: "some-snap", @@ -7215,6 +7274,11 @@ func (s *snapmgrTestSuite) TestRevertWithBaseRunThrough(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap-with-base/2"), }, + { + op: "export-content:Doing", + name: "some-snap-with-base", + revno: snap.R(2), + }, { op: "auto-connect:Doing", name: "some-snap-with-base", @@ -7301,6 +7365,11 @@ func (s *snapmgrTestSuite) TestParallelInstanceRevertRunThrough(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/2"), }, + { + op: "export-content:Doing", + name: "some-snap_instance", + revno: snap.R(2), + }, { op: "auto-connect:Doing", name: "some-snap_instance", @@ -7373,7 +7442,7 @@ func (s *snapmgrTestSuite) TestRevertWithLocalRevisionRunThrough(c *C) { s.settle(c) s.state.Lock() - c.Assert(s.fakeBackend.ops.Ops(), HasLen, 7) + c.Assert(s.fakeBackend.ops.Ops(), HasLen, 8) // verify that LocalRevision is still -7 var snapst snapstate.SnapState @@ -7439,6 +7508,11 @@ func (s *snapmgrTestSuite) TestRevertToRevisionNewVersion(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), }, + { + op: "export-content:Doing", + name: "some-snap", + revno: snap.R(7), + }, { op: "auto-connect:Doing", name: "some-snap", @@ -7528,6 +7602,11 @@ func (s *snapmgrTestSuite) TestRevertTotalUndoRunThrough(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/1"), }, + { + op: "export-content:Doing", + name: "some-snap", + revno: snap.R(1), + }, { op: "auto-connect:Doing", name: "some-snap", @@ -7743,6 +7822,11 @@ func (s *snapmgrTestSuite) TestEnableRunThrough(c *C) { name: "some-snap", revno: snap.R(7), }, + { + op: "export-content:Doing", + name: "some-snap", + revno: snap.R(7), + }, { op: "update-aliases", }, @@ -7807,6 +7891,11 @@ func (s *snapmgrTestSuite) TestDisableRunThrough(c *C) { op: "remove-snap-aliases", name: "some-snap", }, + { + op: "unexport-content:Doing", + name: "some-snap", + revno: snap.R(7), + }, { op: "unlink-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), @@ -7907,6 +7996,11 @@ func (s *snapmgrTestSuite) TestParallelInstanceEnableRunThrough(c *C) { name: "some-snap_instance", revno: snap.R(7), }, + { + op: "export-content:Doing", + name: "some-snap_instance", + revno: snap.R(7), + }, { op: "update-aliases", }, @@ -7973,6 +8067,11 @@ func (s *snapmgrTestSuite) TestParallelInstanceDisableRunThrough(c *C) { op: "remove-snap-aliases", name: "some-snap_instance", }, + { + op: "unexport-content:Doing", + name: "some-snap_instance", + revno: snap.R(7), + }, { op: "unlink-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), @@ -8231,6 +8330,7 @@ func (s *snapmgrTestSuite) testUpdateScenario(c *C, desc string, t switchScenari "setup-profiles:Doing", "candidate", "link-snap", + "export-content:Doing", "auto-connect:Doing", "update-aliases", "cleanup-trash", @@ -8390,6 +8490,7 @@ validate-snap: Done link-snap: Error INFO unlink ERROR fail +export-content: Hold auto-connect: Hold set-auto-aliases: Hold setup-aliases: Hold @@ -8406,6 +8507,7 @@ validate-snap: Done link-snap: Error INFO unlink ERROR fail +export-content: Hold auto-connect: Hold set-auto-aliases: Hold setup-aliases: Hold @@ -10202,6 +10304,11 @@ func (s *snapmgrTestSuite) TestUpdateCanDoBackwards(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), }, + { + op: "export-content:Doing", + name: "some-snap", + revno: snap.R(7), + }, { op: "auto-connect:Doing", name: "some-snap", @@ -10273,13 +10380,14 @@ func (s *snapmgrTestSuite) TestRemoveMany(c *C) { c.Assert(tts, HasLen, 2) c.Check(removed, DeepEquals, []string{"one", "two"}) - c.Assert(s.state.TaskCount(), Equals, 8*2) + c.Assert(s.state.TaskCount(), Equals, 9*2) for i, ts := range tts { c.Assert(taskKinds(ts.Tasks()), DeepEquals, []string{ "stop-snap-services", "run-hook[remove]", "auto-disconnect", "remove-aliases", + "unexport-content", "unlink-snap", "remove-profiles", "clear-snap", @@ -10808,6 +10916,11 @@ func (s *snapmgrTestSuite) TestTransitionCoreRunThrough(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "core/11"), }, + { + op: "export-content:Doing", + name: "core", + revno: snap.R(11), + }, { op: "auto-connect:Doing", name: "core", @@ -10829,6 +10942,11 @@ func (s *snapmgrTestSuite) TestTransitionCoreRunThrough(c *C) { op: "remove-snap-aliases", name: "ubuntu-core", }, + { + op: "unexport-content:Doing", + name: "ubuntu-core", + revno: snap.R(1), + }, { op: "unlink-snap", path: filepath.Join(dirs.SnapMountDir, "ubuntu-core/1"), @@ -10925,6 +11043,11 @@ func (s *snapmgrTestSuite) TestTransitionCoreRunThroughWithCore(c *C) { op: "remove-snap-aliases", name: "ubuntu-core", }, + { + op: "unexport-content:Doing", + name: "ubuntu-core", + revno: snap.R(1), + }, { op: "unlink-snap", path: filepath.Join(dirs.SnapMountDir, "ubuntu-core/1"), diff --git a/overlord/snapstate/snapstate_try_test.go b/overlord/snapstate/snapstate_try_test.go index 94f4153d64c..794b385171e 100644 --- a/overlord/snapstate/snapstate_try_test.go +++ b/overlord/snapstate/snapstate_try_test.go @@ -97,6 +97,7 @@ func (s *snapmgrTestSuite) testTrySetsTryMode(flags snapstate.Flags, c *C, extra "copy-snap-data", "setup-profiles", "link-snap", + "export-content", "auto-connect", "set-auto-aliases", "setup-aliases", diff --git a/snap/info.go b/snap/info.go index 2a4aa17349f..7e8a5c70bbe 100644 --- a/snap/info.go +++ b/snap/info.go @@ -311,6 +311,11 @@ type Info struct { // List of system users (usernames) this snap may use. The group of the same // name must also exist. SystemUsernames map[string]*SystemUsernameInfo + + // List of files to export to the host. + HostExports []*Export + // List of files to export to snap mount namespaces. + NamespaceExports []*Export } // StoreAccount holds information about a store account, for example of snap @@ -336,6 +341,20 @@ type Layout struct { Symlink string `json:"symlink,omitempty"` } +type ExportMethod string + +const ( + ExportMethodSymlink ExportMethod = "symlink" +) + +// Export describes a file or directory exported from a snap to the host system or the snap mount namespace. +type Export struct { + Snap *Info + PrivatePath string // path relative to snap mount point in the appropriate mount namespace. + PublicPath string // path relative to /var/lib/snapd/exports/ + Method ExportMethod // symlink, copy, bind mount ... +} + // String returns a simple textual representation of a layout. func (l *Layout) String() string { var buf bytes.Buffer @@ -1164,6 +1183,29 @@ func ReadInfo(name string, si *SideInfo) (*Info, error) { info.Size = st.Size() } + // Inject implicit exports to snapd and snapd snaps. + if info.SnapID == naming.WellKnownSnapID("snapd") || info.SnapID == naming.WellKnownSnapID("core") { + exportedFiles := []string{ + "snap-exec", // invoked inside the mount namespace + "snap-confine", // invoked inside the snap namespace, in special cases + "snap-update-ns", // invoked inside the mount namespace + "snapctl", // invoked inside the mount namespace + "snap-gdb-shim", // invoked inside the mount namespace + "snap-device-helper", // invoked inside the mount namespace + "etelpmoc.sh", // sourced inside the mount namespace + "info", // read inside the mount namespace by other tools + } + info.NamespaceExports = make([]*Export, 0, len(exportedFiles)) + for _, fname := range exportedFiles { + info.NamespaceExports = append(info.NamespaceExports, &Export{ + Snap: info, + PublicPath: fmt.Sprintf("%s/tools/%s", info.SnapName(), fname), + PrivatePath: fmt.Sprintf("usr/lib/snapd/%s", fname), + Method: ExportMethodSymlink, + }) + } + } + return info, nil } diff --git a/store/details_v2_test.go b/store/details_v2_test.go index f241149d2c6..ac9ba086ec6 100644 --- a/store/details_v2_test.go +++ b/store/details_v2_test.go @@ -307,6 +307,8 @@ func (s *detailsV2Suite) TestInfoFromStoreSnap(c *C) { "SideInfo.Channel", "DownloadInfo.AnonDownloadURL", // TODO: going away at some point "SystemUsernames", + "HostExports", + "NamespaceExports", } var checker func(string, reflect.Value) checker = func(pfx string, x reflect.Value) {