Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions docs/reference/definition-files/workshop-definition.rst
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ Mount interface plugs can't belong to the :ref:`system SDK <ref_system_sdk>`.
They are described by the following attributes:

.. @artefact mount interface attributes
.. @artefact $SDK

.. list-table::
:header-rows: 1
Expand All @@ -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
Expand Down Expand Up @@ -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.


Expand Down
6 changes: 5 additions & 1 deletion docs/reference/sdks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -254,9 +255,12 @@ A mount plug in the definition must specify the plug name, the interface, the ta
gid: <GROUP ID> # optional
read-only: <true | false> # 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`.
Expand Down
64 changes: 36 additions & 28 deletions internal/interfaces/builtin/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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 "$"
Expand All @@ -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 {
Expand Down
45 changes: 43 additions & 2 deletions internal/interfaces/builtin/mount_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
10 changes: 5 additions & 5 deletions internal/interfaces/builtin/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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
}
Expand Down
Loading