diff --git a/docs/reference/definition-files/workshop-definition.rst b/docs/reference/definition-files/workshop-definition.rst index d22b0a3c5..e3514a374 100644 --- a/docs/reference/definition-files/workshop-definition.rst +++ b/docs/reference/definition-files/workshop-definition.rst @@ -267,6 +267,7 @@ Mount interface plugs can't belong to the :ref:`system SDK `. They are described by the following attributes: .. @artefact mount interface attributes +.. @artefact $SDK .. list-table:: :header-rows: 1 @@ -280,7 +281,9 @@ They are described by the following attributes: * - :samp:`workshop-target` (required) - string - A path inside the workshop - to be used as the plug's target directory. + to be used as the plug's target directory; + :file:`/project/` or :envvar:`$SDK`-based paths can be used; + :envvar:`$SDK` expands into the SDK's installation path in the workshop. * - :samp:`mode` - integer @@ -332,7 +335,7 @@ They are described by the following attributes: - string - A path inside the workshop to be used as the slot's source directory; - :file:`/project` or :envvar:`$SDK`-based paths can be used; + :file:`/project/` or :envvar:`$SDK`-based paths can be used; :envvar:`$SDK` expands into the SDK's installation path in the workshop. diff --git a/docs/reference/sdks.rst b/docs/reference/sdks.rst index 9952d47d8..1a9b662b9 100644 --- a/docs/reference/sdks.rst +++ b/docs/reference/sdks.rst @@ -239,7 +239,8 @@ Mount interface .. @artefact mount interface -A mount plug in the definition must specify the plug name, the interface, the target directory, its permissions and ownership, and optionally whether to be read-only: +A mount plug in the definition must specify the plug name, the interface, and the target directory. +The plug can specify permissions and ownership for the target, and whether it is read-only: .. code-block:: yaml :caption: sdk.yaml @@ -254,9 +255,12 @@ A mount plug in the definition must specify the plug name, the interface, the ta gid: # optional read-only: # optional +.. @artefact $SDK This mounts a directory automatically created by |ws_markup| on the host to the :samp:`workshop-target` directory. +The :envvar:`$SDK` variable can be used to refer to the SDK installation path +inside the workshop. The host directory will be created under the path designated by the :envvar:`$XDG_DATA_HOME` variable. The workshop directory will be created using the given :samp:`mode`, :samp:`uid`, and :samp:`gid`. diff --git a/internal/interfaces/builtin/mount.go b/internal/interfaces/builtin/mount.go index e44fc9cb4..7664dd097 100644 --- a/internal/interfaces/builtin/mount.go +++ b/internal/interfaces/builtin/mount.go @@ -93,16 +93,9 @@ func (iface *mountInterface) BeforePreparePlug(plug *sdk.PlugInfo) error { } } - target, ok := plug.Attrs["workshop-target"].(string) - if !ok || len(target) == 0 { - return fmt.Errorf("mount plug must contain target path") - } - - if !filepath.IsAbs(target) { - return fmt.Errorf(`mount plug "workshop-target" must be absolute: %q`, target) - } - if filepath.Clean(target) != target { - return fmt.Errorf(`mount plug "workshop-target" is not clean: %q`, target) + path, err := parseMountPath(plug.Attrs, "plug", "workshop-target", plug.Sdk.Name) + if err != nil { + return err } if _, err := parseBool(plug.Attrs, "read-only", false); err != nil { @@ -111,7 +104,7 @@ func (iface *mountInterface) BeforePreparePlug(plug *sdk.PlugInfo) error { var fallbackUid, fallbackGid int64 for _, prefix := range []string{"/home/workshop", "/project", "/run/user/1000"} { - if target == prefix || strings.HasPrefix(target, prefix+string(filepath.Separator)) { + if path == prefix || strings.HasPrefix(path, prefix+string(filepath.Separator)) { fallbackUid = workshop.Uid fallbackGid = workshop.Gid break @@ -206,20 +199,43 @@ func (iface *mountInterface) BeforePrepareSlot(slot *sdk.SlotInfo) error { return fmt.Errorf(`unknown attribute for mount interface slot: %q`, name) } } - source, exist := slot.Attrs["workshop-source"] + + _, err := parseMountPath(slot.Attrs, "slot", "workshop-source", slot.Sdk.Name) + return err +} + +func parseMountPath(attrs map[string]interface{}, kind, key string, sk string) (string, error) { + attr, exist := attrs[key] if !exist { - return fmt.Errorf("mount slot must contain source path") + return "", fmt.Errorf("mount %s must contain %q", kind, key) } - path, ok := source.(string) + template, ok := attr.(string) if !ok { - return fmt.Errorf(`mount slot "workshop-source" is not a string (found %T)`, source) + return "", fmt.Errorf(`mount %s %q is not a string (found %T)`, kind, key, attr) + } + + path, err := expandMountPath(template, sk) + if err != nil { + return "", err } + if !filepath.IsAbs(path) { + return "", fmt.Errorf(`mount %s %q must be absolute: %q`, kind, key, path) + } + if filepath.Clean(path) != path { + return "", fmt.Errorf(`mount %s %q is not clean: %q`, kind, key, path) + } + + attrs[key] = path + return path, nil +} + +func expandMountPath(template string, sk string) (string, error) { var err error - path = os.Expand(path, func(s string) string { + path := os.Expand(template, func(s string) string { switch s { case "SDK": - return sdk.SdkDir(slot.Sdk.Name) + return sdk.SdkDir(sk) case "$": // Unescape $$ -> $. return "$" @@ -228,19 +244,11 @@ func (iface *mountInterface) BeforePrepareSlot(slot *sdk.SlotInfo) error { return "" } }) - if err != nil { - return err - } - if !filepath.IsAbs(path) { - return fmt.Errorf(`mount slot "workshop-source" must be absolute: %q`, path) - } - if filepath.Clean(path) != path { - return fmt.Errorf(`mount slot "workshop-source" is not clean: %q`, path) + if err != nil { + return "", err } - - slot.Attrs["workshop-source"] = path - return nil + return path, nil } func (iface *mountInterface) setPlugAttrs(mount *workshop.Mount, plug *interfaces.ConnectedPlug) error { diff --git a/internal/interfaces/builtin/mount_test.go b/internal/interfaces/builtin/mount_test.go index bea4a286a..f179fd874 100644 --- a/internal/interfaces/builtin/mount_test.go +++ b/internal/interfaces/builtin/mount_test.go @@ -224,6 +224,47 @@ plugs: c.Check(plug.Attrs["gid"], check.Equals, int64(0)) } +func (s *mountSuite) TestSanitizePlugSDK(c *check.C) { + const mockSdkYaml = `name: mount-slot-sdk +base: ubuntu@22.04 +plugs: + mount-plug: + interface: mount + workshop-target: $SDK +` + info := sdk.MockInfo(c, mockSdkYaml, s.projectId, "ws") + plug := info.Plugs["mount-plug"] + c.Assert(interfaces.BeforePreparePlug(s.iface, plug), check.IsNil) + c.Check(plug.Attrs["workshop-target"], check.Equals, "/var/lib/workshop/sdk/mount-slot-sdk") +} + +func (s *mountSuite) TestSanitizePlugSDKSubdir(c *check.C) { + const mockSdkYaml = `name: mount-slot-sdk +base: ubuntu@22.04 +plugs: + mount-plug: + interface: mount + workshop-target: ${SDK}/lib/x86_64-linux-gnu +` + info := sdk.MockInfo(c, mockSdkYaml, s.projectId, "ws") + plug := info.Plugs["mount-plug"] + c.Assert(interfaces.BeforePreparePlug(s.iface, plug), check.IsNil) + c.Check(plug.Attrs["workshop-target"], check.Equals, "/var/lib/workshop/sdk/mount-slot-sdk/lib/x86_64-linux-gnu") +} + +func (s *mountSuite) TestSanitizePlugSDKUnclean(c *check.C) { + const mockSdkYaml = `name: mount-slot-sdk +base: ubuntu@22.04 +plugs: + mount-plug: + interface: mount + workshop-target: $SDK/ +` + info := sdk.MockInfo(c, mockSdkYaml, s.projectId, "ws") + plug := info.Plugs["mount-plug"] + c.Assert(interfaces.BeforePreparePlug(s.iface, plug), check.ErrorMatches, `mount plug "workshop-target" is not clean: "/var/lib/workshop/sdk/mount-slot-sdk/"`) +} + func (s *mountSuite) TestSanitizePlugSimpleNoTarget(c *check.C) { const mockSdkYaml = `name: mount-slot-sdk base: ubuntu@22.04 @@ -233,7 +274,7 @@ plugs: ` info := sdk.MockInfo(c, mockSdkYaml, s.projectId, "ws") plug := info.Plugs["mount-plug"] - c.Assert(interfaces.BeforePreparePlug(s.iface, plug), check.ErrorMatches, "mount plug must contain target path") + c.Assert(interfaces.BeforePreparePlug(s.iface, plug), check.ErrorMatches, `mount plug must contain "workshop-target"`) } func (s *mountSuite) TestSanitizePlugSimpleTargetRelative(c *check.C) { @@ -452,7 +493,7 @@ slots: ` info := sdk.MockInfo(c, mockSdkYaml, s.projectId, "ws") slot := info.Slots["mount-slot"] - c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), check.ErrorMatches, "mount slot must contain source path") + c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), check.ErrorMatches, `mount slot must contain "workshop-source"`) } func (s *mountSuite) TestSanitizeSlotAbsSourceFails(c *check.C) { diff --git a/internal/interfaces/builtin/tunnel.go b/internal/interfaces/builtin/tunnel.go index f9ccb6128..fa4d949ff 100644 --- a/internal/interfaces/builtin/tunnel.go +++ b/internal/interfaces/builtin/tunnel.go @@ -278,20 +278,20 @@ func (iface *tunnelInterface) MountConnectedPlug(spec *lxd_device.Specification, } switch entry.Direction { case workshop.HostToWorkshop: - if err := expandPath(&entry.Listen, spec.User); err != nil { + if err := expandProxyPath(&entry.Listen, spec.User); err != nil { return err } if err := authorizePath(entry.Listen, spec.User); err != nil { return err } - if err := expandPath(&entry.Connect, &workshop.User); err != nil { + if err := expandProxyPath(&entry.Connect, &workshop.User); err != nil { return err } case workshop.WorkshopToHost: - if err := expandPath(&entry.Listen, &workshop.User); err != nil { + if err := expandProxyPath(&entry.Listen, &workshop.User); err != nil { return err } - if err := expandPath(&entry.Connect, spec.User); err != nil { + if err := expandProxyPath(&entry.Connect, spec.User); err != nil { return err } } @@ -366,7 +366,7 @@ func checkListenPort(listen workshop.ProxyTarget, direction workshop.ProxyDirect return nil } -func expandPath(target *workshop.ProxyTarget, user *user.User) error { +func expandProxyPath(target *workshop.ProxyTarget, user *user.User) error { if target.Protocol != "unix" { return nil }