diff --git a/cmd/snap-exec/main.go b/cmd/snap-exec/main.go index cd1129a3ddb..e9f6c3b7ae9 100644 --- a/cmd/snap-exec/main.go +++ b/cmd/snap-exec/main.go @@ -33,6 +33,9 @@ import ( "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snapenv" + + // sets up the snap.NewContainerFromDir hook from snapdir + _ "github.com/snapcore/snapd/snap/snapdir" ) // for the tests @@ -81,7 +84,7 @@ 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 } @@ -93,10 +96,10 @@ func run() error { // Now actually handle the dispatching if opts.Hook != "" { - return execHook(snapApp, revision, opts.Hook) + return execHook(snapTarget, revision, opts.Hook) } - return execApp(snapApp, revision, opts.Command, extraArgs) + return execApp(snapTarget, 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 @@ -162,13 +163,17 @@ func completionHelper() (string, error) { return filepath.Join(filepath.Dir(exe), "etelpmoc.sh"), nil } -func execApp(snapApp, revision, command string, args []string) error { +func execApp(snapTarget, revision, command string, args []string) error { + if strings.ContainsRune(snapTarget, '+') { + return fmt.Errorf("snap-exec cannot run a snap component without a hook specified (use --hook)") + } + rev, err := snap.ParseRevision(revision) if err != nil { return fmt.Errorf("cannot parse revision %q: %s", revision, err) } - snapName, appName := snap.SplitSnapApp(snapApp) + snapName, appName := snap.SplitSnapApp(snapTarget) info, err := snap.ReadInfo(snapName, &snap.SideInfo{ Revision: rev, }) @@ -245,7 +250,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 +260,13 @@ 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) { + return snap.ReadCurrentComponentInfo(name, snapInfo) +} + +func execHook(snapTarget string, revision, hookName string) error { + snapName, componentName := snap.SplitSnapComponentInstanceName(snapTarget) + rev, err := snap.ParseRevision(revision) if err != nil { return err @@ -268,7 +279,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 +312,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 b5b638ca472..4720115a0cb 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 @@ -663,3 +679,64 @@ 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}) +} + +func (s *snapExecSuite) TestSnapExecComponentWithoutHookError(c *C) { + dirs.SetRootDir(c.MkDir()) + + err := snapExec.ExecApp("snapname+comp", "42", "complete", []string{"foo"}) + c.Assert(err, ErrorMatches, `snap-exec cannot run a snap component without a hook specified \(use --hook\)`) +} diff --git a/cmd/snap/cmd_run.go b/cmd/snap/cmd_run.go index 2fd2006c574..9cdd4758b92 100644 --- a/cmd/snap/cmd_run.go +++ b/cmd/snap/cmd_run.go @@ -56,6 +56,9 @@ import ( "github.com/snapcore/snapd/strutil/shlex" "github.com/snapcore/snapd/timeutil" "github.com/snapcore/snapd/x11" + + // sets up the snap.NewContainerFromDir hook from snapdir + _ "github.com/snapcore/snapd/snap/snapdir" ) var ( @@ -555,7 +558,9 @@ func (x *cmdRun) snapRunApp(snapApp string, args []string) error { return nil } - err = x.runSnapConfine(info, app.SecurityTag(), snapApp, "", closeFlockOrRetry, args) + runner := newAppRunnable(info, app) + + err = x.runSnapConfine(info, runner, closeFlockOrRetry, args) if errors.Is(err, errSnapRefreshConflict) { // Possible race condition detected, let's retry. // @@ -579,23 +584,42 @@ func (x *cmdRun) snapRunApp(snapApp string, args []string) error { } } -func (x *cmdRun) snapRunHook(snapName string) error { +func (x *cmdRun) snapRunHook(snapTarget string) error { + snapInstance, componentName := snap.SplitSnapComponentInstanceName(snapTarget) + revision, err := snap.ParseRevision(x.Revision) if err != nil { return err } - info, err := getSnapInfo(snapName, revision) + info, err := getSnapInfo(snapInstance, revision) if err != nil { return err } - hook := info.Hooks[x.HookName] + var ( + hook *snap.HookInfo + component *snap.ComponentInfo + ) + if componentName == "" { + hook = info.Hooks[x.HookName] + } else { + component, err = snap.ReadCurrentComponentInfo(componentName, info) + if err != nil { + return err + } + hook = component.Hooks[x.HookName] + } + if hook == nil { - return fmt.Errorf(i18n.G("cannot find hook %q in %q"), x.HookName, snapName) + return fmt.Errorf(i18n.G("cannot find hook %q in %q"), x.HookName, snapTarget) } - return x.runSnapConfine(info, hook.SecurityTag(), snapName, hook.Name, nil, nil) + // compoment may be nil here, meaning that this is a hook for the snap + // itself, not a component hook + runner := newHookRunnable(info, hook, component) + + return x.runSnapConfine(info, runner, nil, nil) } func (x *cmdRun) snapRunTimer(snapApp, timer string, args []string) error { @@ -776,7 +800,7 @@ func migrateXauthority(info *snap.Info) (string, error) { return targetPath, nil } -func activateXdgDocumentPortal(info *snap.Info, snapApp, hook string) error { +func activateXdgDocumentPortal(runner runnable) error { // Don't do anything for apps or hooks that don't plug the // desktop interface // @@ -787,13 +811,8 @@ func activateXdgDocumentPortal(info *snap.Info, snapApp, hook string) error { // document portal can be in use by many applications, not // just by snaps, so this is at most, pre-emptively using some // extra memory. - var plugs map[string]*snap.PlugInfo - if hook != "" { - plugs = info.Hooks[hook].Plugs - } else { - _, appName := snap.SplitSnapApp(snapApp) - plugs = info.Apps[appName].Plugs - } + plugs := runner.Plugs() + plugsDesktop := false for _, plug := range plugs { if plug.Interface == "desktop" { @@ -1107,14 +1126,115 @@ func (x *cmdRun) runCmdUnderStrace(origCmd []string, envForExec envForExecFunc) return err } -func (x *cmdRun) runSnapConfine(info *snap.Info, securityTag, snapApp, hook string, beforeExec func() error, args []string) error { +func newHookRunnable(info *snap.Info, hook *snap.HookInfo, component *snap.ComponentInfo) runnable { + return runnable{ + info: info, + component: component, + hook: hook, + } +} + +func newAppRunnable(info *snap.Info, app *snap.AppInfo) runnable { + return runnable{ + info: info, + app: app, + } +} + +// runnable bundles together the potential things that we could be running. A +// few accessor methods are provided that delegate the request to the +// appropriate field, depending on what we are running. +type runnable struct { + hook *snap.HookInfo + component *snap.ComponentInfo + app *snap.AppInfo + info *snap.Info +} + +// SecurityTag returns the security tag for the thing being run. The tag could +// come from a snap hook, a component hook, or a snap app. +func (r *runnable) SecurityTag() string { + if r.hook != nil { + return r.hook.SecurityTag() + } + return r.app.SecurityTag() +} + +// Target returns the string identifier of the thing that should be run. This +// could either be a component ref, a snap ref, or a snap ref with a specific +// app. +func (r *runnable) Target() string { + if r.component != nil { + return snap.SnapComponentName(r.info.InstanceName(), r.component.Component.ComponentName) + } + + if r.hook != nil { + return r.info.InstanceName() + } + + return fmt.Sprintf("%s.%s", r.info.InstanceName(), r.app.Name) +} + +// Plugs returns the plugs for the thing being run. The plugs could come from a +// snap hook, a component hook, or a snap app. +func (r *runnable) Plugs() map[string]*snap.PlugInfo { + if r.hook != nil { + return r.hook.Plugs + } + return r.app.Plugs +} + +// IsHook returns true if the runnable is a hook. r.Hook() will not return nil +// if this is true. +func (r *runnable) IsHook() bool { + return r.hook != nil +} + +// Hook returns the hook that is going to be run, if there is one. Will be nil +// if running an app. +func (r *runnable) Hook() *snap.HookInfo { + return r.hook +} + +// Hook returns the hook that contains the thing to be run, if there is one. +// Currently, this will only be present when running a component hook. +func (r *runnable) Component() *snap.ComponentInfo { + return r.component +} + +// App returns the app that is going to be run, if there is one. Will be nil if +// running a hook or component hook. +func (r *runnable) App() *snap.AppInfo { + return r.app +} + +// Validate checks that the runnable is in a valid state. This is used to catch +// programmer errors. +func (r *runnable) Validate() error { + if r.hook != nil && r.app != nil { + return fmt.Errorf("internal error: hook and app cannot coexist in a runnable") + } + + if r.component != nil && r.app != nil { + return fmt.Errorf("internal error: component and app cannot coexist in a runnable") + } + + return nil +} + +func (x *cmdRun) runSnapConfine(info *snap.Info, runner runnable, beforeExec func() error, args []string) error { + // check for programmer error, should never happen + if err := runner.Validate(); err != nil { + return err + } + snapConfine, err := snapdHelperPath("snap-confine") if err != nil { return err } if !osutil.FileExists(snapConfine) { - if hook != "" { - logger.Noticef("WARNING: skipping running hook %q of snap %q: missing snap-confine", hook, info.InstanceName()) + if runner.IsHook() { + logger.Noticef("WARNING: skipping running hook %q of %q: missing snap-confine", runner.Hook().Name, runner.Target()) return nil } return fmt.Errorf(i18n.G("missing snap-confine: try updating your core/snapd package")) @@ -1122,8 +1242,7 @@ func (x *cmdRun) runSnapConfine(info *snap.Info, securityTag, snapApp, hook stri logger.Debugf("executing snap-confine from %s", snapConfine) - snapName, appName := snap.SplitSnapApp(snapApp) - opts, err := getSnapDirOptions(snapName) + opts, err := getSnapDirOptions(info.InstanceName()) if err != nil { return fmt.Errorf("cannot get snap dir options: %w", err) } @@ -1137,7 +1256,7 @@ func (x *cmdRun) runSnapConfine(info *snap.Info, securityTag, snapApp, hook stri logger.Noticef("WARNING: cannot copy user Xauthority file: %s", err) } - if err := activateXdgDocumentPortal(info, snapApp, hook); err != nil { + if err := activateXdgDocumentPortal(runner); err != nil { logger.Noticef("WARNING: cannot start document portal: %s", err) } @@ -1157,7 +1276,7 @@ func (x *cmdRun) runSnapConfine(info *snap.Info, securityTag, snapApp, hook stri // kernels have no explicit base, we use the boot base modelAssertion, err := x.client.CurrentModelAssertion() if err != nil { - if hook != "" { + if runner.IsHook() { return fmt.Errorf("cannot get model assertion to setup kernel hook run: %v", err) } else { return fmt.Errorf("cannot get model assertion to setup kernel app run: %v", err) @@ -1169,6 +1288,8 @@ func (x *cmdRun) runSnapConfine(info *snap.Info, securityTag, snapApp, hook stri } } } + + securityTag := runner.SecurityTag() cmd = append(cmd, securityTag) // when under confinement, snap-exec is run from 'core' snap rootfs @@ -1197,19 +1318,20 @@ func (x *cmdRun) runSnapConfine(info *snap.Info, securityTag, snapApp, hook stri cmd = append(cmd, "--command="+x.Command) } - if hook != "" { - cmd = append(cmd, "--hook="+hook) + if runner.IsHook() { + cmd = append(cmd, "--hook="+runner.Hook().Name) } // snap-exec is POSIXly-- options must come before positionals. - cmd = append(cmd, snapApp) + cmd = append(cmd, runner.Target()) cmd = append(cmd, args...) env, err := osutil.OSEnvironment() if err != nil { return err } - snapenv.ExtendEnvForRun(env, info, opts) + + snapenv.ExtendEnvForRun(env, info, runner.Component(), opts) if len(xauthPath) > 0 { // Environment is not nil here because it comes from @@ -1271,7 +1393,8 @@ func (x *cmdRun) runSnapConfine(info *snap.Info, securityTag, snapApp, hook stri // For more information about systemd cgroups, including unit types, see: // https://www.freedesktop.org/wiki/Software/systemd/ControlGroupInterface/ needsTracking := true - if app := info.Apps[appName]; hook == "" && app != nil && app.IsService() { + + if app := runner.App(); app != nil && app.IsService() { // If we are running a service app then we do not need to use // application tracking. Services, both in the system and user scope, // do not need tracking because systemd already places them in a @@ -1295,7 +1418,7 @@ func (x *cmdRun) runSnapConfine(info *snap.Info, securityTag, snapApp, hook stri } } // Allow using the session bus for all apps but not for hooks. - allowSessionBus := hook == "" + allowSessionBus := !runner.IsHook() // Track, or confirm existing tracking from systemd. if err := cgroupConfirmSystemdAppTracking(securityTag); err != nil { if err != cgroup.ErrCannotTrackProcess { diff --git a/cmd/snap/cmd_run_test.go b/cmd/snap/cmd_run_test.go index ed8955d6abd..4254bd80e29 100644 --- a/cmd/snap/cmd_run_test.go +++ b/cmd/snap/cmd_run_test.go @@ -60,6 +60,22 @@ hooks: configure: `) +var mockYamlWithComponent = []byte(`name: snapname +version: 1.0 +components: + comp: + type: test + hooks: + install: +hooks: + configure: +`) + +var mockComponentYaml = []byte(`component: snapname+comp +type: test +version: 1.0 +`) + var mockYamlBaseNone1 = []byte(`name: snapname1 version: 1.0 base: none @@ -1169,6 +1185,73 @@ func (s *RunSuite) TestSnapRunHookIntegration(c *check.C) { c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") } +func (s *RunSuite) TestSnapRunComponentHookIntegration(c *check.C) { + const instanceKey = "" + s.testSnapRunComponentHookIntegration(c, instanceKey) +} + +func (s *RunSuite) TestSnapRunComponentHookFromInstanceIntegration(c *check.C) { + const instanceKey = "instance" + s.testSnapRunComponentHookIntegration(c, instanceKey) +} + +func (s *RunSuite) testSnapRunComponentHookIntegration(c *check.C, instanceKey string) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + var snapInfo *snap.Info + if instanceKey == "" { + snapInfo = snaptest.MockSnapCurrent(c, string(mockYamlWithComponent), &snap.SideInfo{ + Revision: snap.R(42), + }) + } else { + snapInfo = snaptest.MockSnapInstanceCurrent(c, "snapname_"+instanceKey, string(mockYamlWithComponent), &snap.SideInfo{ + Revision: snap.R(42), + }) + } + + snaptest.MockComponentCurrent(c, string(mockComponentYaml), snapInfo, snap.ComponentSideInfo{ + Revision: snap.R(21), + }) + + // 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() + + expectedTarget := "snapname+comp" + if instanceKey != "" { + expectedTarget = fmt.Sprintf("snapname_%s+comp", instanceKey) + } + + // Run a hook from the active revision + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=install", "--", expectedTarget}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + fmt.Sprintf("snap.%s.hook.install", expectedTarget), + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "--hook=install", + expectedTarget, + }) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") + + // the mount namespace should make it appear as if the instance name is not + // there from inside the snap + c.Check(execEnv, testutil.Contains, "SNAP_COMPONENT=/snap/snapname/components/mnt/comp/21") + c.Check(execEnv, testutil.Contains, "SNAP_COMPONENT_NAME=snapname+comp") + c.Check(execEnv, testutil.Contains, "SNAP_COMPONENT_VERSION=1.0") + c.Check(execEnv, testutil.Contains, "SNAP_COMPONENT_REVISION=21") +} + func (s *RunSuite) TestSnapRunHookUnsetRevisionIntegration(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() diff --git a/snap/component.go b/snap/component.go index 40f892d2325..f07ae9819eb 100644 --- a/snap/component.go +++ b/snap/component.go @@ -159,7 +159,9 @@ func (c *componentPlaceInfo) MountDescription() string { } // ComponentLinkPath returns the path for the symlink for a component for a -// given snap revision. +// given snap revision. Note that this function only uses the ContainerName +// method on the ContainerPlaceInfo. If that changes, callers of this function +// may need to change how the parameters are initialized. func ComponentLinkPath(cpi ContainerPlaceInfo, snapRev Revision) string { instanceName, compName, _ := strings.Cut(cpi.ContainerName(), "+") compBase := ComponentsBaseDir(instanceName) diff --git a/snap/info.go b/snap/info.go index fd8103bf592..e22d8539b74 100644 --- a/snap/info.go +++ b/snap/info.go @@ -1585,6 +1585,41 @@ func ReadCurrentInfo(snapName string) (*Info, error) { return ReadInfo(snapName, &SideInfo{Revision: revision}) } +// NewContainerFromDir creates a new Container from the given directory. +// Generally, the implementation of this function is set by the snapdir package. +var NewContainerFromDir func(snapName string) Container = func(snapName string) Container { + panic("internal error: snap.NewContainerFromDir function unset") +} + +// ReadCurrentComponentInfo reads the ComponentInfo for the currently linked +// revision of the given component associated with the given snap. +func ReadCurrentComponentInfo(component string, info *Info) (*ComponentInfo, error) { + // TODO: creating this here is a bit of a hack, since we aren't actually + // able to set the revision of the component. we create it so that we can + // use ComponentLinkPath, which doesn't use the revision. + cpi := MinimalComponentContainerPlaceInfo(component, Revision{}, info.InstanceName()) + link := ComponentLinkPath(cpi, info.Revision) + + linkSource, err := os.Readlink(link) + if err != nil { + return nil, fmt.Errorf("cannot find current component %q for snap %q", component, info.InstanceName()) + } + + rev := filepath.Base(linkSource) + + revision, err := ParseRevision(rev) + if err != nil { + return nil, fmt.Errorf("cannot parse current revision for component %q: %s", component, err) + } + + container := NewContainerFromDir(link) + + return ReadComponentInfoFromContainer(container, info, &ComponentSideInfo{ + Revision: revision, + Component: naming.NewComponentRef(info.SnapName(), component), + }) +} + // ReadInfoFromSnapFile reads the snap information from the given Container and // completes it with the given side-info if this is not nil. func ReadInfoFromSnapFile(snapf Container, si *SideInfo) (*Info, error) { diff --git a/snap/info_test.go b/snap/info_test.go index 0b940990822..32825da2175 100644 --- a/snap/info_test.go +++ b/snap/info_test.go @@ -35,6 +35,8 @@ import ( "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" + "github.com/snapcore/snapd/snap/snapdir" "github.com/snapcore/snapd/snap/snapfile" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/snap/squashfs" @@ -52,6 +54,7 @@ var _ = Suite(&infoSimpleSuite{}) func (s *infoSimpleSuite) SetUpTest(c *C) { dirs.SetRootDir(c.MkDir()) + snap.NewContainerFromDir = snapdir.NewContainerFromDir } func (s *infoSimpleSuite) TearDownTest(c *C) { @@ -380,6 +383,35 @@ func (s *infoSuite) TestReadCurrentInfo(c *C) { c.Assert(errors.As(err, &snap.NotFoundError{}), Equals, true) } +func (s *infoSuite) TestReadCurrentComponentInfo(c *C) { + const snapYaml = ` +name: sample +version: 1 +components: + comp: + type: test` + + const componentYaml = ` +component: sample+comp +type: test +` + + info := snaptest.MockSnapCurrent(c, snapYaml, &snap.SideInfo{ + Revision: snap.R(42), + }) + + snaptest.MockComponentCurrent(c, componentYaml, info, snap.ComponentSideInfo{ + Component: naming.NewComponentRef("sample", "comp"), + Revision: snap.R(21), + }) + + currentCompInfo, err := snap.ReadCurrentComponentInfo("comp", info) + c.Assert(err, IsNil) + + c.Assert(currentCompInfo.Revision, Equals, snap.R(21)) + c.Assert(currentCompInfo.Component, DeepEquals, naming.NewComponentRef("sample", "comp")) +} + func (s *infoSuite) TestReadCurrentInfoWithInstance(c *C) { si := &snap.SideInfo{Revision: snap.R(42)} diff --git a/snap/snapdir/snapdir.go b/snap/snapdir/snapdir.go index 8b4e5618da0..b442a363156 100644 --- a/snap/snapdir/snapdir.go +++ b/snap/snapdir/snapdir.go @@ -211,3 +211,13 @@ func (s *SnapDir) ListDir(path string) ([]string, error) { func (s *SnapDir) Unpack(src, dstDir string) error { return fmt.Errorf("unpack is not supported with snaps of type snapdir") } + +// NewContainerFromDir returns a snap.Container that is implemented by a +// SnapDir. It should be used as the implementation of snap.NewContainerFromDir. +func NewContainerFromDir(path string) snap.Container { + return New(path) +} + +func init() { + snap.NewContainerFromDir = NewContainerFromDir +} diff --git a/snap/snapenv/snapenv.go b/snap/snapenv/snapenv.go index 470d6a7f4e4..0e6a09db7e7 100644 --- a/snap/snapenv/snapenv.go +++ b/snap/snapenv/snapenv.go @@ -47,18 +47,25 @@ var userCurrent = user.Current // // It ensures all SNAP_* override any pre-existing environment // variables. -func ExtendEnvForRun(env osutil.Environment, info *snap.Info, opts *dirs.SnapDirOptions) { +func ExtendEnvForRun(env osutil.Environment, info *snap.Info, component *snap.ComponentInfo, opts *dirs.SnapDirOptions) { // Set various SNAP_ environment variables as well as some non-SNAP variables, // depending on snap confinement mode. Note that this does not include environment // set by snap-exec. - for k, v := range snapEnv(info, opts) { + for k, v := range snapEnv(info, component, opts) { env[k] = v } } -func snapEnv(info *snap.Info, opts *dirs.SnapDirOptions) osutil.Environment { +func snapEnv(info *snap.Info, component *snap.ComponentInfo, opts *dirs.SnapDirOptions) osutil.Environment { // Environment variables with basic properties of a snap. env := basicEnv(info) + + if component != nil { + for k, v := range componentEnv(info, component) { + env[k] = v + } + } + if usr, err := userCurrent(); err == nil && usr.HomeDir != "" { // Environment variables with values specific to the calling user. for k, v := range userEnv(info, usr.HomeDir, opts) { @@ -68,6 +75,26 @@ func snapEnv(info *snap.Info, opts *dirs.SnapDirOptions) osutil.Environment { return env } +func componentEnv(info *snap.Info, component *snap.ComponentInfo) osutil.Environment { + env := osutil.Environment{ + // this uses dirs.CoreSnapMountDir for the same reasons that it is used + // to set SNAP in basicEnv, see comment there for more details + "SNAP_COMPONENT": filepath.Join( + dirs.CoreSnapMountDir, + info.SnapName(), + "components", + "mnt", + component.Component.ComponentName, + component.Revision.String(), + ), + "SNAP_COMPONENT_NAME": component.FullName(), + "SNAP_COMPONENT_VERSION": component.Version, + "SNAP_COMPONENT_REVISION": component.Revision.String(), + } + + return env +} + // basicEnv returns the app-level environment variables for a snap. // Despite this being a bit snap-specific, this is in helpers.go because it's // used by so many other modules, we run into circular dependencies if it's diff --git a/snap/snapenv/snapenv_test.go b/snap/snapenv/snapenv_test.go index 951fa43fd31..d92e6c6a1bb 100644 --- a/snap/snapenv/snapenv_test.go +++ b/snap/snapenv/snapenv_test.go @@ -34,6 +34,7 @@ import ( "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/sys" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" "github.com/snapcore/snapd/testutil" ) @@ -61,6 +62,16 @@ var mockSnapInfo = &snap.Info{ Revision: snap.R(17), }, } +var mockComponentInfo = &snap.ComponentInfo{ + Component: naming.ComponentRef{ + SnapName: "foo", + ComponentName: "comp", + }, + Version: "1.0", + ComponentSideInfo: snap.ComponentSideInfo{ + Revision: snap.R(5), + }, +} var mockClassicSnapInfo = &snap.Info{ SuggestedName: "foo", Version: "1.0", @@ -174,7 +185,7 @@ func (s *HTestSuite) TestSnapRunSnapExecEnv(c *C) { os.Setenv("HOME", "") } - env := snapEnv(info, nil) + env := snapEnv(info, nil, nil) c.Assert(env, DeepEquals, osutil.Environment{ "SNAP": fmt.Sprintf("%s/snapname/42", dirs.CoreSnapMountDir), "SNAP_COMMON": "/var/snap/snapname/common", @@ -217,7 +228,7 @@ func (s *HTestSuite) TestParallelInstallSnapRunSnapExecEnv(c *C) { os.Setenv("HOME", "") } - env := snapEnv(info, nil) + env := snapEnv(info, nil, nil) c.Check(env, DeepEquals, osutil.Environment{ // Those are mapped to snap-specific directories by // mount namespace setup @@ -293,7 +304,7 @@ func (ts *HTestSuite) TestParallelInstallUserForClassicConfinement(c *C) { func (s *HTestSuite) TestExtendEnvForRunForNonClassic(c *C) { env := osutil.Environment{"TMPDIR": "/var/tmp"} - ExtendEnvForRun(env, mockSnapInfo, nil) + ExtendEnvForRun(env, mockSnapInfo, nil, nil) c.Assert(env["SNAP_NAME"], Equals, "foo") c.Assert(env["SNAP_COMMON"], Equals, "/var/snap/foo/common") @@ -305,13 +316,30 @@ func (s *HTestSuite) TestExtendEnvForRunForNonClassic(c *C) { func (s *HTestSuite) TestExtendEnvForRunForClassic(c *C) { env := osutil.Environment{"TMPDIR": "/var/tmp"} - ExtendEnvForRun(env, mockClassicSnapInfo, nil) + ExtendEnvForRun(env, mockClassicSnapInfo, nil, nil) + + c.Assert(env["SNAP_NAME"], Equals, "foo") + c.Assert(env["SNAP_COMMON"], Equals, "/var/snap/foo/common") + c.Assert(env["SNAP_DATA"], Equals, "/var/snap/foo/17") + + c.Assert(env["TMPDIR"], Equals, "/var/tmp") +} + +func (s *HTestSuite) TestExtendEnvForRunWithComponent(c *C) { + env := osutil.Environment{"TMPDIR": "/var/tmp"} + + ExtendEnvForRun(env, mockSnapInfo, mockComponentInfo, nil) c.Assert(env["SNAP_NAME"], Equals, "foo") c.Assert(env["SNAP_COMMON"], Equals, "/var/snap/foo/common") c.Assert(env["SNAP_DATA"], Equals, "/var/snap/foo/17") c.Assert(env["TMPDIR"], Equals, "/var/tmp") + + c.Assert(env["SNAP_COMPONENT"], Equals, filepath.Join(dirs.CoreSnapMountDir, "foo/components/mnt/comp/5")) + c.Assert(env["SNAP_COMPONENT_REVISION"], Equals, "5") + c.Assert(env["SNAP_COMPONENT_VERSION"], Equals, "1.0") + c.Assert(env["SNAP_COMPONENT_NAME"], Equals, "foo+comp") } func (s *HTestSuite) TestHiddenDirEnv(c *C) { @@ -333,7 +361,7 @@ func (s *HTestSuite) TestHiddenDirEnv(c *C) { {dir: dirs.HiddenSnapDataHomeDir, opts: &dirs.SnapDirOptions{HiddenSnapDataDir: true}}, {dir: dirs.HiddenSnapDataHomeDir, opts: &dirs.SnapDirOptions{HiddenSnapDataDir: true, MigratedToExposedHome: true}}} { env := osutil.Environment{} - ExtendEnvForRun(env, mockSnapInfo, t.opts) + ExtendEnvForRun(env, mockSnapInfo, nil, t.opts) c.Check(env["SNAP_USER_COMMON"], Equals, filepath.Join(testDir, t.dir, mockSnapInfo.SuggestedName, "common")) c.Check(env["SNAP_USER_DATA"], DeepEquals, filepath.Join(testDir, t.dir, mockSnapInfo.SuggestedName, mockSnapInfo.Revision.String())) diff --git a/snap/snaptest/snaptest.go b/snap/snaptest/snaptest.go index 155d47efa8e..6928322fa4b 100644 --- a/snap/snaptest/snaptest.go +++ b/snap/snaptest/snaptest.go @@ -138,6 +138,23 @@ func MockSnapCurrent(c *check.C, yamlText string, sideInfo *snap.SideInfo) *snap return si } +func MockComponentCurrent(c *check.C, yamlText string, info *snap.Info, csi snap.ComponentSideInfo) *snap.ComponentInfo { + ci := MockComponent(c, yamlText, info, csi) + + mountDir := snap.ComponentMountDir(ci.Component.ComponentName, ci.Revision, info.InstanceName()) + link := filepath.Join(snap.ComponentsBaseDir(info.InstanceName()), info.Revision.String(), ci.Component.ComponentName) + err := os.MkdirAll(filepath.Dir(link), 0755) + c.Assert(err, check.IsNil) + + linkDest, err := filepath.Rel(filepath.Dir(link), mountDir) + c.Assert(err, check.IsNil) + + err = os.Symlink(linkDest, link) + c.Assert(err, check.IsNil) + + return ci +} + // MockSnapInstanceCurrent does the same as MockSnapInstance but additionally // creates the 'current' symlink. //