From 4e1002a20df553b7447c9ccf40673eb534f7ba82 Mon Sep 17 00:00:00 2001 From: Andrew Phelps Date: Thu, 29 Feb 2024 16:39:03 -0500 Subject: [PATCH] c/snap-exec: handle running component hooks in snap-exec --- cmd/snap-exec/main.go | 49 ++++++++++++++++++------ cmd/snap-exec/main_test.go | 76 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 111 insertions(+), 14 deletions(-) diff --git a/cmd/snap-exec/main.go b/cmd/snap-exec/main.go index cd1129a3ddbb..d50a67f4628e 100644 --- a/cmd/snap-exec/main.go +++ b/cmd/snap-exec/main.go @@ -32,6 +32,7 @@ import ( "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snapdir" "github.com/snapcore/snapd/snap/snapenv" ) @@ -81,11 +82,13 @@ func parseArgs(args []string) (app string, appArgs []string, err error) { } func run() error { - snapApp, extraArgs, err := parseArgs(os.Args[1:]) + snapTarget, extraArgs, err := parseArgs(os.Args[1:]) if err != nil { return err } + snapName, componentName := snap.SplitSnapComponentInstanceName(snapTarget) + // the SNAP_REVISION is set by `snap run` - we can not (easily) // find it in `snap-exec` because `snap-exec` is run inside the // confinement and (generally) can not talk to snapd @@ -93,10 +96,10 @@ func run() error { // Now actually handle the dispatching if opts.Hook != "" { - return execHook(snapApp, revision, opts.Hook) + return execHook(snapName, componentName, revision, opts.Hook) } - return execApp(snapApp, revision, opts.Command, extraArgs) + return execApp(snapName, revision, opts.Command, extraArgs) } const defaultShell = "/bin/bash" @@ -128,12 +131,10 @@ func findCommand(app *snap.AppInfo, command string) (string, error) { return cmd, nil } -func absoluteCommandChain(snapInfo *snap.Info, commandChain []string) []string { +func absoluteCommandChain(mountDir string, commandChain []string) []string { chain := make([]string, 0, len(commandChain)) - snapMountDir := snapInfo.MountDir() - for _, element := range commandChain { - chain = append(chain, filepath.Join(snapMountDir, element)) + chain = append(chain, filepath.Join(mountDir, element)) } return chain @@ -245,7 +246,7 @@ func execApp(snapApp, revision, command string, args []string) error { fullCmd = append(fullCmd, cmdArgs...) fullCmd = append(fullCmd, args...) - fullCmd = append(absoluteCommandChain(app.Snap, app.CommandChain), fullCmd...) + fullCmd = append(absoluteCommandChain(app.Snap.MountDir(), app.CommandChain), fullCmd...) logger.StartupStageTimestamp("snap-exec to app") if err := syscallExec(fullCmd[0], fullCmd, env.ForExec()); err != nil { @@ -255,7 +256,15 @@ func execApp(snapApp, revision, command string, args []string) error { return nil } -func execHook(snapName, revision, hookName string) error { +func getComponentInfo(name string, snapInfo *snap.Info) (*snap.ComponentInfo, error) { + container := func(p string) (snap.Container, error) { + return snapdir.New(p), nil + } + + return snap.ReadCurrentComponentInfo(name, snapInfo, container) +} + +func execHook(snapName, componentName, revision, hookName string) error { rev, err := snap.ParseRevision(revision) if err != nil { return err @@ -268,7 +277,23 @@ func execHook(snapName, revision, hookName string) error { return err } - hook := info.Hooks[hookName] + var ( + hook *snap.HookInfo + mountDir string + ) + + if componentName == "" { + hook = info.Hooks[hookName] + mountDir = info.MountDir() + } else { + component, err := getComponentInfo(componentName, info) + if err != nil { + return err + } + hook = component.Hooks[hookName] + mountDir = snap.ComponentMountDir(component.Component.ComponentName, component.Revision, info.InstanceName()) + } + if hook == nil { return fmt.Errorf("cannot find hook %q in %q", hookName, snapName) } @@ -285,7 +310,9 @@ func execHook(snapName, revision, hookName string) error { env.ExtendWithExpanded(eenv) } + hookPath := filepath.Join(mountDir, "meta", "hooks", hookName) + // run the hook - cmd := append(absoluteCommandChain(hook.Snap, hook.CommandChain), filepath.Join(hook.Snap.HooksDir(), hook.Name)) + cmd := append(absoluteCommandChain(mountDir, hook.CommandChain), hookPath) return syscallExec(cmd[0], cmd, env.ForExec()) } diff --git a/cmd/snap-exec/main_test.go b/cmd/snap-exec/main_test.go index b5b638ca4724..3d429f94ab89 100644 --- a/cmd/snap-exec/main_test.go +++ b/cmd/snap-exec/main_test.go @@ -76,10 +76,20 @@ apps: command-chain: [chain1, chain2] nostop: command: nostop +components: + comp: + type: test + hooks: + install: `) var mockClassicYaml = append([]byte("confinement: classic\n"), mockYaml...) +var mockComponentYaml = []byte(`component: snapname+comp +type: test +version: 1.0 +`) + var mockHookYaml = []byte(`name: snapname version: 1.0 hooks: @@ -91,6 +101,12 @@ version: 1.0 hooks: configure: command-chain: [chain1, chain2] +components: + comp: + type: test + hooks: + install: + command-chain: [chain3, chain4] `) var binaryTemplate = `#!/bin/sh @@ -287,7 +303,7 @@ func (s *snapExecSuite) TestSnapExecHookIntegration(c *C) { defer restore() // launch and verify it ran correctly - err := snapExec.ExecHook("snapname", "42", "configure") + err := snapExec.ExecHook("snapname", "", "42", "configure") c.Assert(err, IsNil) c.Check(execArgv0, Equals, fmt.Sprintf("%s/snapname/42/meta/hooks/configure", dirs.SnapMountDir)) c.Check(execArgs, DeepEquals, []string{execArgv0}) @@ -312,7 +328,7 @@ func (s *snapExecSuite) TestSnapExecHookCommandChainIntegration(c *C) { chain2_path := fmt.Sprintf("%s/snapname/42/chain2", dirs.SnapMountDir) hook_path := fmt.Sprintf("%s/snapname/42/meta/hooks/configure", dirs.SnapMountDir) - err := snapExec.ExecHook("snapname", "42", "configure") + err := snapExec.ExecHook("snapname", "", "42", "configure") c.Assert(err, IsNil) c.Check(execArgv0, Equals, chain1_path) c.Check(execArgs, DeepEquals, []string{chain1_path, chain2_path, hook_path}) @@ -324,7 +340,7 @@ func (s *snapExecSuite) TestSnapExecHookMissingHookIntegration(c *C) { Revision: snap.R("42"), }) - err := snapExec.ExecHook("snapname", "42", "missing-hook") + err := snapExec.ExecHook("snapname", "", "42", "missing-hook") c.Assert(err, NotNil) c.Assert(err, ErrorMatches, "cannot find hook \"missing-hook\" in \"snapname\"") } @@ -663,3 +679,57 @@ func (s *snapExecSuite) TestSnapExecCompleteClassicNoReexec(c *C) { c.Check(execEnv, testutil.Contains, "SNAP_DATA=/var/snap/snapname/42") c.Check(execEnv, testutil.Contains, "TMPDIR=/var/tmp99") } + +func (s *snapExecSuite) TestSnapExecComponentHookIntegration(c *C) { + dirs.SetRootDir(c.MkDir()) + snapInfo := snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R(42), + }) + snaptest.MockComponentCurrent(c, string(mockComponentYaml), snapInfo, snap.ComponentSideInfo{ + Revision: snap.R(21), + }) + + execArgv0 := "" + execArgs := []string{} + restore := snapExec.MockSyscallExec(func(argv0 string, argv []string, env []string) error { + execArgv0 = argv0 + execArgs = argv + return nil + }) + defer restore() + + hookPath := filepath.Join(dirs.SnapMountDir, "/snapname/components/mnt/comp/21/meta/hooks/install") + + err := snapExec.ExecHook("snapname", "comp", "42", "install") + c.Assert(err, IsNil) + c.Check(execArgv0, Equals, hookPath) + c.Check(execArgs, DeepEquals, []string{execArgv0}) +} + +func (s *snapExecSuite) TestSnapExecComponentHookCommandChainIntegration(c *C) { + dirs.SetRootDir(c.MkDir()) + snapInfo := snaptest.MockSnap(c, string(mockHookCommandChainYaml), &snap.SideInfo{ + Revision: snap.R(42), + }) + snaptest.MockComponentCurrent(c, string(mockComponentYaml), snapInfo, snap.ComponentSideInfo{ + Revision: snap.R(21), + }) + + execArgv0 := "" + execArgs := []string{} + restore := snapExec.MockSyscallExec(func(argv0 string, argv []string, env []string) error { + execArgv0 = argv0 + execArgs = argv + return nil + }) + defer restore() + + chain3Path := filepath.Join(dirs.SnapMountDir, "/snapname/components/mnt/comp/21/chain3") + chain4Path := filepath.Join(dirs.SnapMountDir, "/snapname/components/mnt/comp/21/chain4") + hookPath := filepath.Join(dirs.SnapMountDir, "/snapname/components/mnt/comp/21/meta/hooks/install") + + err := snapExec.ExecHook("snapname", "comp", "42", "install") + c.Assert(err, IsNil) + c.Check(execArgv0, Equals, chain3Path) + c.Check(execArgs, DeepEquals, []string{chain3Path, chain4Path, hookPath}) +}