Skip to content

Commit

Permalink
Merge pull request #1508 from kyrofa/feature/1586465/snap-exec_hook
Browse files Browse the repository at this point in the history
cmd/snap,cmd/snap-exec: support running hooks via snap-exec.
  • Loading branch information
kyrofa authored Jul 14, 2016
2 parents 1e773d4 + 2a17eb1 commit eb7958f
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 19 deletions.
49 changes: 45 additions & 4 deletions cmd/snap-exec/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"syscall"

"github.com/jessevdk/go-flags"
Expand All @@ -36,6 +37,7 @@ var syscallExec = syscall.Exec
// commandline args
var opts struct {
Command string `long:"command" description:"use a different command like {stop,post-stop} from the app"`
Hook string `long:"hook" description:"hook to run" hidden:"yes"`
}

func main() {
Expand All @@ -55,11 +57,19 @@ func parseArgs(args []string) (app string, appArgs []string, err error) {
return "", nil, fmt.Errorf("need the application to run as argument")
}

// Catch some invalid parameter combinations, provide helpful errors
if opts.Hook != "" && opts.Command != "" {
return "", nil, fmt.Errorf("cannot use --hook and --command together")
}
if opts.Hook != "" && len(rest) > 0 {
return "", nil, fmt.Errorf("too many arguments for hook %q: %s", opts.Hook, strings.Join(rest, " "))
}

return rest[0], rest[1:], nil
}

func run() error {
snapApp, args, err := parseArgs(os.Args[1:])
snapApp, extraArgs, err := parseArgs(os.Args[1:])
if err != nil {
return err
}
Expand All @@ -69,7 +79,12 @@ func run() error {
// confinement and (generally) can not talk to snapd
revision := os.Getenv("SNAP_REVISION")

return snapExec(snapApp, revision, opts.Command, args)
// Now actually handle the dispatching
if opts.Hook != "" {
return snapExecHook(snapApp, revision, opts.Hook)
}

return snapExecApp(snapApp, revision, opts.Command, extraArgs)
}

func findCommand(app *snap.AppInfo, command string) (string, error) {
Expand All @@ -93,7 +108,7 @@ func findCommand(app *snap.AppInfo, command string) (string, error) {
return cmd, nil
}

func snapExec(snapApp, revision, command string, args []string) error {
func snapExecApp(snapApp, revision, command string, args []string) error {
rev, err := snap.ParseRevision(revision)
if err != nil {
return fmt.Errorf("cannot parse revision %q: %s", revision, err)
Expand All @@ -117,7 +132,7 @@ func snapExec(snapApp, revision, command string, args []string) error {
return err
}

// build the evnironment from the yamle
// build the environment from the yaml
env := append(os.Environ(), app.Env()...)

// run the command
Expand All @@ -130,3 +145,29 @@ func snapExec(snapApp, revision, command string, args []string) error {
// this is never reached except in tests
return nil
}

func snapExecHook(snapName, revision, hookName string) error {
rev, err := snap.ParseRevision(revision)
if err != nil {
return err
}

info, err := snap.ReadInfo(snapName, &snap.SideInfo{
Revision: rev,
})
if err != nil {
return err
}

hook := info.Hooks[hookName]
if hook == nil {
return fmt.Errorf("cannot find hook %q in %q", hookName, snapName)
}

// build the environment
env := append(os.Environ(), hook.Env()...)

// run the hook
hookPath := filepath.Join(hook.Snap.HooksDir(), hook.Name)
return syscallExec(hookPath, []string{hookPath}, env)
}
55 changes: 53 additions & 2 deletions cmd/snap-exec/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ var _ = Suite(&snapExecSuite{})
func (s *snapExecSuite) SetUpTest(c *C) {
// clean previous parse runs
opts.Command = ""
opts.Hook = ""
}

func (s *snapExecSuite) TearDown(c *C) {
Expand All @@ -67,6 +68,24 @@ apps:
command: nostop
`)

var mockHookYaml = []byte(`name: snapname
version: 1.0
hooks:
apply-config:
`)

func (s *snapExecSuite) TestInvalidCombinedParameters(c *C) {
invalidParameters := []string{"--hook=hook-name", "--command=command-name", "snap-name"}
_, _, err := parseArgs(invalidParameters)
c.Check(err, ErrorMatches, ".*cannot use --hook and --command together.*")
}

func (s *snapExecSuite) TestInvalidExtraParameters(c *C) {
invalidParameters := []string{"--hook=hook-name", "snap-name", "foo", "bar"}
_, _, err := parseArgs(invalidParameters)
c.Check(err, ErrorMatches, ".*too many arguments for hook \"hook-name\": snap-name foo bar.*")
}

func (s *snapExecSuite) TestFindCommand(c *C) {
info, err := snap.InfoFromSnapYaml(mockYaml)
c.Assert(err, IsNil)
Expand Down Expand Up @@ -101,7 +120,7 @@ func (s *snapExecSuite) TestFindCommandNoCommand(c *C) {
c.Check(err, ErrorMatches, `no "stop" command found for "nostop"`)
}

func (s *snapExecSuite) TestSnapLaunchIntegration(c *C) {
func (s *snapExecSuite) TestSnapExecAppIntegration(c *C) {
dirs.SetRootDir(c.MkDir())
snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{
Revision: snap.R("42"),
Expand All @@ -118,13 +137,45 @@ func (s *snapExecSuite) TestSnapLaunchIntegration(c *C) {
}

// launch and verify its run the right way
err := snapExec("snapname.app", "42", "stop", []string{"arg1", "arg2"})
err := snapExecApp("snapname.app", "42", "stop", []string{"arg1", "arg2"})
c.Assert(err, IsNil)
c.Check(execArgv0, Equals, fmt.Sprintf("%s/snapname/42/stop-app", dirs.SnapSnapsDir))
c.Check(execArgs, DeepEquals, []string{execArgv0, "arg1", "arg2"})
c.Check(execEnv, testutil.Contains, "LD_LIBRARY_PATH=/some/path\n")
}

func (s *snapExecSuite) TestSnapExecHookIntegration(c *C) {
dirs.SetRootDir(c.MkDir())
snaptest.MockSnap(c, string(mockHookYaml), &snap.SideInfo{
Revision: snap.R("42"),
})

execArgv0 := ""
execArgs := []string{}
syscallExec = func(argv0 string, argv []string, env []string) error {
execArgv0 = argv0
execArgs = argv
return nil
}

// launch and verify it ran correctly
err := snapExecHook("snapname", "42", "apply-config")
c.Assert(err, IsNil)
c.Check(execArgv0, Equals, fmt.Sprintf("%s/snapname/42/meta/hooks/apply-config", dirs.SnapSnapsDir))
c.Check(execArgs, DeepEquals, []string{execArgv0})
}

func (s *snapExecSuite) TestSnapExecHookMissingHookIntegration(c *C) {
dirs.SetRootDir(c.MkDir())
snaptest.MockSnap(c, string(mockHookYaml), &snap.SideInfo{
Revision: snap.R("42"),
})

err := snapExecHook("snapname", "42", "missing-hook")
c.Assert(err, NotNil)
c.Assert(err, ErrorMatches, "cannot find hook \"missing-hook\" in \"snapname\"")
}

func (s *snapExecSuite) TestSnapExecIgnoresUnknownArgs(c *C) {
snapApp, rest, err := parseArgs([]string{"--command=shell", "snapname.app", "--arg1", "arg2"})
c.Assert(err, IsNil)
Expand Down
19 changes: 12 additions & 7 deletions cmd/snap/cmd_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ func snapRunApp(snapApp, command string, args []string) error {
return fmt.Errorf("cannot find app %q in %q", appName, snapName)
}

return runSnapConfine(info, app.SecurityTag(), snapApp, command, args)
return runSnapConfine(info, app.SecurityTag(), snapApp, command, "", args)
}

func snapRunHook(snapName, hookName, revision string) error {
Expand All @@ -169,16 +169,17 @@ func snapRunHook(snapName, hookName, revision string) error {
}

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 fmt.Errorf("cannot find hook %q in %q", hookName, snapName)
return nil
}

hookBinary := filepath.Join(info.HooksDir(), hook.Name)

return runSnapConfine(info, hook.SecurityTag(), hookBinary, "", nil)
return runSnapConfine(info, hook.SecurityTag(), snapName, "", hook.Name, nil)
}

func runSnapConfine(info *snap.Info, securityTag, binary, command string, args []string) error {
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)
}
Expand All @@ -188,13 +189,17 @@ func runSnapConfine(info *snap.Info, securityTag, binary, command string, args [
securityTag,
securityTag,
"/usr/lib/snapd/snap-exec",
binary,
snapApp,
}

if command != "" {
cmd = append(cmd, "--command="+command)
}

if hook != "" {
cmd = append(cmd, "--hook="+hook)
}

cmd = append(cmd, args...)

env := append(os.Environ(), snapExecEnv(info)...)
Expand Down
52 changes: 46 additions & 6 deletions cmd/snap/cmd_run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,44 @@ func (s *SnapSuite) TestSnapRunAppIntegration(c *check.C) {
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",
"snapname.app", "--command=my-command",
"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)
Expand Down Expand Up @@ -178,7 +216,7 @@ func (s *SnapSuite) TestSnapRunHookIntegration(c *check.C) {
"snap.snapname.hook.apply-config",
"snap.snapname.hook.apply-config",
"/usr/lib/snapd/snap-exec",
filepath.Join(dirs.GlobalRootDir, "/snap/snapname/42/meta/hooks/apply-config")})
"snapname", "--hook=apply-config"})
c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42")
}

Expand Down Expand Up @@ -219,7 +257,7 @@ func (s *SnapSuite) TestSnapRunHookSpecificRevisionIntegration(c *check.C) {
"snap.snapname.hook.apply-config",
"snap.snapname.hook.apply-config",
"/usr/lib/snapd/snap-exec",
filepath.Join(dirs.GlobalRootDir, "/snap/snapname/41/meta/hooks/apply-config")})
"snapname", "--hook=apply-config"})
c.Check(execEnv, testutil.Contains, "SNAP_REVISION=41")
}

Expand Down Expand Up @@ -254,11 +292,12 @@ func (s *SnapSuite) TestSnapRunHookInvalidRevisionIntegration(c *check.C) {
c.Check(err, check.ErrorMatches, "invalid snap revision: \"invalid\"")
}

func (s *SnapSuite) TestSnapRunMissingHookIntegration(c *check.C) {
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),
})
Expand All @@ -267,15 +306,16 @@ func (s *SnapSuite) TestSnapRunMissingHookIntegration(c *check.C) {
s.mockServer(c)

// redirect exec
called := false
restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
called = true
return nil
})
defer restorer()

// Run a hook from the active revision
err := snaprun.SnapRunHook("snapname", "missing-hook", "")
c.Assert(err, check.NotNil)
c.Check(err, check.ErrorMatches, "cannot find hook \"missing-hook\" in \"snapname\"")
c.Assert(err, check.IsNil)
c.Check(called, check.Equals, false)
}

func (s *SnapSuite) mockServer(c *check.C) {
Expand Down
10 changes: 10 additions & 0 deletions snap/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,16 @@ func (hook *HookInfo) SecurityTag() string {
return HookSecurityTag(hook.Snap.Name(), hook.Name)
}

// Env returns the hook-specific environment overrides
func (hook *HookInfo) Env() []string {
env := []string{}
hookEnv := copyEnv(hook.Snap.Environment)
for k, v := range hookEnv {
env = append(env, fmt.Sprintf("%s=%s\n", k, v))
}
return env
}

func infoFromSnapYamlWithSideInfo(meta []byte, si *SideInfo) (*Info, error) {
info, err := InfoFromSnapYaml(meta)
if err != nil {
Expand Down

0 comments on commit eb7958f

Please sign in to comment.