diff --git a/cmd/snap/cmd_run.go b/cmd/snap/cmd_run.go index 717bada98fc..1ea99365753 100644 --- a/cmd/snap/cmd_run.go +++ b/cmd/snap/cmd_run.go @@ -125,6 +125,10 @@ func createUserDataDirs(info *snap.Info) error { return fmt.Errorf(i18n.G("cannot get the current user: %v"), err) } + snapDir := filepath.Join(usr.HomeDir, dirs.UserHomeSnapDir) + if err := os.MkdirAll(snapDir, 0700); err != nil { + return fmt.Errorf(i18n.G("cannot create snap home dir: %w"), err) + } // see snapenv.User userData := info.UserDataDir(usr.HomeDir) commonUserData := info.UserCommonDataDir(usr.HomeDir) diff --git a/cmd/snap/cmd_run.go.orig b/cmd/snap/cmd_run.go.orig new file mode 100644 index 00000000000..717bada98fc --- /dev/null +++ b/cmd/snap/cmd_run.go.orig @@ -0,0 +1,202 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "fmt" + "os" + "os/user" + "strings" + "syscall" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snapenv" +) + +var ( + syscallExec = syscall.Exec + userCurrent = user.Current +) + +type cmdRun struct { + Command string `long:"command" hidden:"yes"` + Hook string `long:"hook" hidden:"yes"` + Revision string `short:"r" default:"unset" hidden:"yes"` + Shell bool `long:"shell" ` +} + +func init() { + addCommand("run", + i18n.G("Run the given snap command"), + i18n.G("Run the given snap command with the right confinement and environment"), + func() flags.Commander { + return &cmdRun{} + }, map[string]string{ + "command": i18n.G("Alternative command to run"), + "hook": i18n.G("Hook to run"), + "r": i18n.G("Use a specific snap revision when running hook"), + "shell": i18n.G("Run a shell instead of the command (useful for debugging)"), + }, nil) +} + +func (x *cmdRun) Execute(args []string) error { + if len(args) == 0 { + return fmt.Errorf(i18n.G("need the application to run as argument")) + } + snapApp := args[0] + args = args[1:] + + // Catch some invalid parameter combinations, provide helpful errors + if x.Hook != "" && x.Command != "" { + return fmt.Errorf(i18n.G("cannot use --hook and --command together")) + } + if x.Revision != "unset" && x.Revision != "" && x.Hook == "" { + return fmt.Errorf(i18n.G("-r can only be used with --hook")) + } + if x.Hook != "" && len(args) > 0 { + // TRANSLATORS: %q is the hook name; %s a space-separated list of extra arguments + return fmt.Errorf(i18n.G("too many arguments for hook %q: %s"), x.Hook, strings.Join(args, " ")) + } + + // Now actually handle the dispatching + if x.Hook != "" { + return snapRunHook(snapApp, x.Revision, x.Hook) + } + + // pass shell as a special command to snap-exec + if x.Shell { + x.Command = "shell" + } + + return snapRunApp(snapApp, x.Command, args) +} + +func getSnapInfo(snapName string, revision snap.Revision) (*snap.Info, error) { + if revision.Unset() { + // User didn't supply a revision, so we need to get it via the snapd API + // here because once we're inside the confinement it may be unavailable. + snaps, err := Client().List([]string{snapName}) + if err != nil { + return nil, err + } + if len(snaps) == 0 { + return nil, fmt.Errorf("cannot find snap %q", snapName) + } + if len(snaps) > 1 { + return nil, fmt.Errorf(i18n.G("multiple snaps for %q: %d"), snapName, len(snaps)) + } + revision = snaps[0].Revision + } + + info, err := snap.ReadInfo(snapName, &snap.SideInfo{ + Revision: revision, + }) + if err != nil { + return nil, err + } + + return info, nil +} + +func createUserDataDirs(info *snap.Info) error { + usr, err := userCurrent() + if err != nil { + return fmt.Errorf(i18n.G("cannot get the current user: %v"), err) + } + + // see snapenv.User + userData := info.UserDataDir(usr.HomeDir) + commonUserData := info.UserCommonDataDir(usr.HomeDir) + for _, d := range []string{userData, commonUserData} { + if err := os.MkdirAll(d, 0755); err != nil { + // TRANSLATORS: %q is the directory whose creation failed, %v the error message + return fmt.Errorf(i18n.G("cannot create %q: %v"), d, err) + } + } + return nil +} + +func snapRunApp(snapApp, command string, args []string) error { + snapName, appName := snap.SplitSnapApp(snapApp) + info, err := getSnapInfo(snapName, snap.R(0)) + if err != nil { + return err + } + + app := info.Apps[appName] + if app == nil { + return fmt.Errorf(i18n.G("cannot find app %q in %q"), appName, snapName) + } + + return runSnapConfine(info, app.SecurityTag(), snapApp, command, "", args) +} + +func snapRunHook(snapName, snapRevision, hookName string) error { + revision, err := snap.ParseRevision(snapRevision) + if err != nil { + return err + } + + info, err := getSnapInfo(snapName, revision) + if err != nil { + return err + } + + hook := info.Hooks[hookName] + + // Make sure this hook is valid for this snap. If not, don't run it. This + // isn't an error, e.g. it will happen if a snap doesn't ship a system hook. + if hook == nil { + return nil + } + + return runSnapConfine(info, hook.SecurityTag(), snapName, "", hook.Name, nil) +} + +func runSnapConfine(info *snap.Info, securityTag, snapApp, command, hook string, args []string) error { + if err := createUserDataDirs(info); err != nil { + logger.Noticef("WARNING: cannot create user data directory: %s", err) + } + + cmd := []string{ + "/usr/bin/ubuntu-core-launcher", + securityTag, + securityTag, + "/usr/lib/snapd/snap-exec", + } + + if command != "" { + cmd = append(cmd, "--command="+command) + } + + if hook != "" { + cmd = append(cmd, "--hook="+hook) + } + + // snap-exec is POSIXly-- options must come before positionals. + cmd = append(cmd, snapApp) + cmd = append(cmd, args...) + + return syscallExec(cmd[0], cmd, snapenv.ExecEnv(info)) +} diff --git a/cmd/snap/cmd_run_test.go b/cmd/snap/cmd_run_test.go index 1d799e9de1e..a74b894a254 100644 --- a/cmd/snap/cmd_run_test.go +++ b/cmd/snap/cmd_run_test.go @@ -393,3 +393,20 @@ func (s *SnapSuite) TestSnapRunSaneEnvironmentHandling(c *check.C) { c.Check(execEnv, check.Not(testutil.Contains), "SNAP_ARCH=PDP-7") c.Check(execEnv, testutil.Contains, "SNAP_THE_WORLD=YES") } + +func (s *RunSuite) TestCreateSnapDirPermissions(c *check.C) { + usr, err := user.Current() + c.Assert(err, check.IsNil) + + usr.HomeDir = s.fakeHome + snaprun.MockUserCurrent(func() (*user.User, error) { + return usr, nil + }) + + info := &snap.Info{SuggestedName: "some-snap"} + c.Assert(snaprun.CreateUserDataDirs(info), check.IsNil) + + fi, err := os.Stat(filepath.Join(s.fakeHome, dirs.UserHomeSnapDir)) + c.Assert(err, check.IsNil) + c.Assert(fi.Mode()&os.ModePerm, check.Equals, os.FileMode(0700)) +} diff --git a/cmd/snap/cmd_run_test.go.orig b/cmd/snap/cmd_run_test.go.orig new file mode 100644 index 00000000000..1d799e9de1e --- /dev/null +++ b/cmd/snap/cmd_run_test.go.orig @@ -0,0 +1,395 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + "os" + "os/user" + "path/filepath" + + "gopkg.in/check.v1" + + snaprun "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" +) + +var mockYaml = []byte(`name: snapname +version: 1.0 +apps: + app: + command: run-app +hooks: + apply-config: +`) + +func (s *SnapSuite) TestInvalidParameters(c *check.C) { + invalidParameters := []string{"run", "--hook=apply-config", "--command=command-name", "snap-name"} + _, err := snaprun.Parser().ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*cannot use --hook and --command together.*") + + invalidParameters = []string{"run", "-r=1", "--command=command-name", "snap-name"} + _, err = snaprun.Parser().ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*-r can only be used with --hook.*") + + invalidParameters = []string{"run", "-r=1", "snap-name"} + _, err = snaprun.Parser().ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*-r can only be used with --hook.*") + + invalidParameters = []string{"run", "--hook=apply-config", "foo", "bar", "snap-name"} + _, err = snaprun.Parser().ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*too many arguments for hook \"apply-config\": bar.*") +} + +func (s *SnapSuite) TestSnapRunAppIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // and mock the server + s.mockServer(c) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // and run it! + rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(execArg0, check.Equals, "/usr/bin/ubuntu-core-launcher") + c.Check(execArgs, check.DeepEquals, []string{ + "/usr/bin/ubuntu-core-launcher", + "snap.snapname.app", + "snap.snapname.app", + "/usr/lib/snapd/snap-exec", + "snapname.app", + "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") +} + +func (s *SnapSuite) TestSnapRunAppWithCommandIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // and mock the server + s.mockServer(c) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // and run it! + err := snaprun.SnapRunApp("snapname.app", "my-command", []string{"arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, "/usr/bin/ubuntu-core-launcher") + c.Check(execArgs, check.DeepEquals, []string{ + "/usr/bin/ubuntu-core-launcher", + "snap.snapname.app", + "snap.snapname.app", + "/usr/lib/snapd/snap-exec", + "--command=my-command", "snapname.app", + "arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") +} + +func (s *SnapSuite) TestSnapRunCreateDataDirs(c *check.C) { + info, err := snap.InfoFromSnapYaml(mockYaml) + c.Assert(err, check.IsNil) + info.SideInfo.Revision = snap.R(42) + + fakeHome := c.MkDir() + restorer := snaprun.MockUserCurrent(func() (*user.User, error) { + return &user.User{HomeDir: fakeHome}, nil + }) + defer restorer() + + err = snaprun.CreateUserDataDirs(info) + c.Assert(err, check.IsNil) + c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname/42")), check.Equals, true) + c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname/common")), check.Equals, true) +} + +func (s *SnapSuite) TestSnapRunHookIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // and mock the server + s.mockServer(c) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // Run a hook from the active revision + _, err := snaprun.Parser().ParseArgs([]string{"run", "--hook=apply-config", "snapname"}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, "/usr/bin/ubuntu-core-launcher") + c.Check(execArgs, check.DeepEquals, []string{ + "/usr/bin/ubuntu-core-launcher", + "snap.snapname.hook.apply-config", + "snap.snapname.hook.apply-config", + "/usr/lib/snapd/snap-exec", + "--hook=apply-config", "snapname"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") +} + +func (s *SnapSuite) TestSnapRunHookUnsetRevisionIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // and mock the server + s.mockServer(c) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // Specifically pass "unset" which would use the active version. + _, err := snaprun.Parser().ParseArgs([]string{"run", "--hook=apply-config", "-r=unset", "snapname"}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, "/usr/bin/ubuntu-core-launcher") + c.Check(execArgs, check.DeepEquals, []string{ + "/usr/bin/ubuntu-core-launcher", + "snap.snapname.hook.apply-config", + "snap.snapname.hook.apply-config", + "/usr/lib/snapd/snap-exec", + "--hook=apply-config", "snapname"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") +} + +func (s *SnapSuite) TestSnapRunHookSpecificRevisionIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + // Create both revisions 41 and 42 + snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R(41), + }) + snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // and mock the server + s.mockServer(c) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // Run a hook on revision 41 + _, err := snaprun.Parser().ParseArgs([]string{"run", "--hook=apply-config", "-r=41", "snapname"}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, "/usr/bin/ubuntu-core-launcher") + c.Check(execArgs, check.DeepEquals, []string{ + "/usr/bin/ubuntu-core-launcher", + "snap.snapname.hook.apply-config", + "snap.snapname.hook.apply-config", + "/usr/lib/snapd/snap-exec", + "--hook=apply-config", "snapname"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=41") +} + +func (s *SnapSuite) TestSnapRunHookMissingRevisionIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + // Only create revision 42 + snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // and mock the server + s.mockServer(c) + + // redirect exec + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + return nil + }) + defer restorer() + + // Attempt to run a hook on revision 41, which doesn't exist + _, err := snaprun.Parser().ParseArgs([]string{"run", "--hook=apply-config", "-r=41", "snapname"}) + c.Assert(err, check.NotNil) + c.Check(err, check.ErrorMatches, "cannot find .*") +} + +func (s *SnapSuite) TestSnapRunHookInvalidRevisionIntegration(c *check.C) { + _, err := snaprun.Parser().ParseArgs([]string{"run", "--hook=apply-config", "-r=invalid", "snapname"}) + c.Assert(err, check.NotNil) + c.Check(err, check.ErrorMatches, "invalid snap revision: \"invalid\"") +} + +func (s *SnapSuite) TestSnapRunHookMissingHookIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + // Only create revision 42 + snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // and mock the server + s.mockServer(c) + + // redirect exec + called := false + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + called = true + return nil + }) + defer restorer() + + err := snaprun.SnapRunHook("snapname", "unset", "missing-hook") + c.Assert(err, check.IsNil) + c.Check(called, check.Equals, false) +} + +func (s *SnapSuite) mockServer(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "snapname", "status": "active", "version": "1.0", "developer": "someone", "revision":42}]}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) +} + +func (s *SnapSuite) TestSnapRunErorsForUnknownRunArg(c *check.C) { + _, err := snaprun.Parser().ParseArgs([]string{"run", "--unknown", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.ErrorMatches, "unknown flag `unknown'") +} + +func (s *SnapSuite) TestSnapRunErorsForMissingApp(c *check.C) { + _, err := snaprun.Parser().ParseArgs([]string{"run", "--command=shell"}) + c.Assert(err, check.ErrorMatches, "need the application to run as argument") +} + +func (s *SnapSuite) TestSnapRunSaneEnvironmentHandling(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // and mock the server + s.mockServer(c) + + // redirect exec + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execEnv = envv + return nil + }) + defer restorer() + + // set a SNAP{,_*} variable in the environment + os.Setenv("SNAP_NAME", "something-else") + os.Setenv("SNAP_ARCH", "PDP-7") + defer os.Unsetenv("SNAP_NAME") + defer os.Unsetenv("SNAP_ARCH") + // but unreleated stuff is ok + os.Setenv("SNAP_THE_WORLD", "YES") + defer os.Unsetenv("SNAP_THE_WORLD") + + // and ensure those SNAP_ vars get overriden + rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") + c.Check(execEnv, check.Not(testutil.Contains), "SNAP_NAME=something-else") + c.Check(execEnv, check.Not(testutil.Contains), "SNAP_ARCH=PDP-7") + c.Check(execEnv, testutil.Contains, "SNAP_THE_WORLD=YES") +}