Snap layouts

zyga

#1

Layouts are a developer-centric feature that simplify the creation of snap applications.

Up until snapd 2.36 an experimental feature-flag must be enabled before you can install a layout-dependent snap. Since snapd 2.36 the feature is enabled by default:

$ sudo snap set core experimental.layouts=true

Attempting to install a layout-dependent snap when the feature is disabled will generate the following error:

error: cannot install snap file: experimental feature disabled - test it by setting                                                                 
       'experimental.layouts' to true

Building snaps with layouts

Layouts enable snap developers to modify the execution environment of their snap. They simplify the process of using pre-compiled binaries and libraries that expect to find files and directories outside of locations referenced by $SNAP or $SNAP_DATA.

With layouts, you can take elements from $SNAP, $SNAP_DATA, $SNAP_COMMON and expose them in locations such as /usr, /var and /etc.

Layouts are supported by snapcraft 3.0 when the snap developer declares a base snap (e.g. base: core18). In prior versions, layout options within snapcraft.yaml must be made within the scope of a top-level passthrough field.

As a simple example, let’s consider a snap that includes software that:

  • stores all data in /var/lib/foo
  • has a configuration file in /etc/foo.conf
  • uses read-only data in /usr/share/foo

A layout that allows such software to be used without snap-specific modifications can be defined as follows:

layout:
  /var/lib/foo:
    bind: $SNAP_DATA/var/lib/foo
  /usr/share/foo:
    bind: $SNAP/usr/share/foo
  /etc/foo.conf:
    bind-file: $SNAP_DATA/etc/foo.conf

None of the above filesystem modifications are visible to any other snaps, or from the wider user session. They’re only visible within the per-snap mount namespace.

Layout reference

The syntax for defining a layout is:

layout:
  <target-path>: <declaration>
  <target-path>: <declaration>
  ..

Layouts are defined as a key-value map, mapping from a <target-path> to a layout declaration. Each declaration may be one of the following:

  • bind: <source-path>: bind-mount a directory
  • bind-file: <source-path>: bind-mount a file
  • symlink: <source-path>: create a symbolic link
  • type: tmpfs: mount a private temporary in-memory filesystem

<source-path> must refer to either $SNAP, $SNAP_DATA or $SNAP_USER_DATA.

<target-path> can include nearly any path except for /proc, /sys, /dev, /run, /boot, /lost+found, /media, /var/lib/snapd and /var/snap. Some other limitations apply (see the limitations section below).

If <source-path> and <target-path> don’t already exist, they will be automatically created by snapd. This includes the creation of new empty files, but doesn’t include the creation of symbolic link targets because snapd doesn’t know what kind of objects they may eventually point to. In the example above, $SNAP_DATA/etc/foo.conf is created before any snap application code is executed.

Creating new files and directories in read-only spaces

Layouts can create new directories and files even in read-only locations such as /usr/share. For example, the following declaration will create /usr/share/foo, visible only to executing snap applications (it’s assumed that /usr/share/foo does not exist in the base snap declared by the application developer).

layout:
  /usr/share/foo:
    bind: $SNAP/usr/share/foo

To accomplish the above, snapd uses a temporary filesystem (tmpfs) mounted on /usr/share and populated with a set of empty files and directories. These are then used for bind mounts as well as symlinks to reconstruct the original /usr/share. This allows snapd to make /usr/share writable, and consequently, allows snapd to create /usr/share/foo and configure it as desired.

Current limitations

The following limitations apply as of snapd 2.36

New entries in / (root)

Layouts cannot currently create new top-level files or directories. For example, the following layout declaration will not work:

layout:
  /foo: # Unsupported, cannot create new top-level directories.
     bind: $SNAP/foo

Incompatible existing file, directory or symbolic link

Layouts cannot replace an existing but incompatible filesystem object. This means, for example, that files cannot replace directories or symbolic links, files cannot replace a directory, and existing symbolic links cannot be redirected to a new target. You can, however, replace a directory with another directory.


Snaps and OpenCL
Qt app with no-system-libraries and without
The system-files interface
Access to /var/run for confined snap
GSequencer needs manual review confinement: next-generation
Layouts: re-mapping snap directories
The snap format
Unprivileged access using IOCTL tcgets
#2

Works perfectly, thanks again! Using it to map tmp/log directories within my snapped web app (i. e. read-only, but the framework expects them at the app root dir) to writable bind-mounts at $SNAP_DATA. Just have to make sure that those directories do not exist in source.


#3

@zyga I think I have identified an edge case issue with layouts. Mounting directories and files works fine, but if I declare two target paths for bind-file where the second path’s last component (i. e. filename) contains all of the first path’s plus a suffix, snap installation will fail with:

error: cannot perform the following tasks:
- Setup snap "my-snap" (unset) security profiles (cannot setup mount for snap "my-snap": cannot update mount namespace of snap "my-snap": cannot update preserved namespace of snap "my-snap": cannot update snap namespace: no such file or directory)
- Setup snap "my-snap" (unset) security profiles (cannot update mount namespace of snap "my-snap": cannot update preserved namespace of snap "my-snap": cannot update snap namespace: no such file or directory)

My layout definition:

$SNAP/api/Gemfile:
    bind-file: $SNAP_DATA/Gemfile
$SNAP/api/Gemfile.lock:
    bind-file: $SNAP_DATA/Gemfile.lock

As you can see, the second target path just adds .lock to the filename. If I rename the first mount “Gemfile” to “Gemfile_B”, everything works fine – but this breaks my Rails application. I can configure an alternative name for Gemfile, but Rails expects a $filename.lock file for whatever name I choose. To rule out that layouts has a problem with suffix-less files, I tried a layout with Gemfile.gems and Gemfile.gems.lock to no avail.

I also tested it with bind folders (attempting to mount $SNAP/folder alongside $SNAP/folder.suffix) just to make sure it’s not specific to bind-file.

Am I “holding it wrong” ™ or is this actually an issue with the layouts feature?


#4

I wrote a small test case based on your report and I’m running it now against master. The test passed. I will now revert a bug fix I landed to that area last week to ensure that it was the culprit.

EDIT: Some more debugging I cannot reproduce the problem just yet. Can you share more details about your setup:

  1. Were the files $SNAP/api/Gemfile and $SNAP/api/Gemfile.lock present in the snap?
  2. Did this test on core or classic system? (snap version output is best)
  3. Were you experimenting with this snap and it was already installed (and perhaps invoked at least once) or was this a clean install?

#5

Thanks for the quick feedback, here are the requested details:

  1. $SNAP/api/Gemfile and $SNAP/api/Gemfile.lock were present during snap creation, but the override-build script of the Rails part renames them to Gemfile_installed Gemfile.lock_installed. I verified that they are not present in the final snap.
  2. Both:

On classic Ubuntu Desktop (snap built with LXD container)

snap   2.35.2
snapd  2.35.2
series 16
ubuntu 18.04
kernel 4.150-36-generic

on Ubuntu Core device:

snap    2.35.2
snapd   2.35.2
series  16
kernel  4.4.0-1098-raspi2
  1. Clean install – or rather attempting to install, since it aborts the installation because of said error. I made sure that no previous versions were installed during the installation attempt.

#6

I’m looking at reproducing this with the new details right now.

EDIT: I failed reproducing this. Here is what I tried. I used snapd 2.35.4 from Ubuntu Cosmic:

snap    2.35.4+18.10
snapd   2.35.4+18.10
series  16
ubuntu  18.10
kernel  4.18.0-8-generic

There I created an application with the following definition:

name: app
version: 1
apps:
    app:
        command: bin/app
layout:
    $SNAP/api/tmp:
        bind: $SNAP_DATA/rails/tmp
    $SNAP/api/log:
        bind: $SNAP_DATA/rails/log
    $SNAP/api/vendor:
        bind: $SNAP_DATA/rails/vendor
    $SNAP/api/Gemfile:
        bind-file: $SNAP_DATA/rails/Gemfile
    $SNAP/api/Gemfile.lock:
        bind-file: $SNAP_DATA/rails/Gemfile.lock

I made a dummy bin/app shell script that just prints ok. Apart from bin/app and meta/snap.yaml there were no other files present in the snap.

Running the application I get ok and a working layout. I can then run snap run --shell app to explore the environment.

zyga@fyke:$ snap run --shell app
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

zyga@fyke:$ cd $SNAP
zyga@fyke:/snap/app/x1$ find
.
./api
./api/vendor
./api/tmp
./api/log
./api/Gemfile
./api/Gemfile.lock
./meta
./meta/snap.yaml
./bin
./bin/app

Looking at $SNAP_DATA, I see:

zyga@fyke:/snap/app/x1$ cd $SNAP_DATA
zyga@fyke:/var/snap/app/x1$ find
.
./rails
./rails/Gemfile.lock
./rails/tmp
./rails/Gemfile
./rails/vendor
./rails/log

Lastly I can inspect the mount namespace but this requires more privileges so I leave the confined shell (exit) and inspect the mount namespace using the nsenter command.

zyga@fyke:/var/snap/app/x1$ exit
exit

zyga@fyke:$ sudo nsenter -m/run/snapd/ns/app.mnt
root@fyke:/# cat /proc/self/mountinfo | tail -n 6
2130 2257 0:56 / /snap/app/x1/api rw,relatime - tmpfs tmpfs rw,mode=775,uid=1000,gid=1000
2067 2130 8:1 /var/snap/app/x1/rails/Gemfile.lock /snap/app/x1/api/Gemfile.lock rw,relatime master:1 - ext4 /dev/sda1 rw,errors=remount-ro
2131 2130 8:1 /var/snap/app/x1/rails/Gemfile /snap/app/x1/api/Gemfile rw,relatime master:1 - ext4 /dev/sda1 rw,errors=remount-ro
2132 2130 8:1 /var/snap/app/x1/rails/log /snap/app/x1/api/log rw,relatime master:1 - ext4 /dev/sda1 rw,errors=remount-ro
2133 2130 8:1 /var/snap/app/x1/rails/tmp /snap/app/x1/api/tmp rw,relatime master:1 - ext4 /dev/sda1 rw,errors=remount-ro
2134 2130 8:1 /var/snap/app/x1/rails/vendor /snap/app/x1/api/vendor rw,relatime master:1 - ext4 /dev/sda1 rw,errors=remount-ro

As you can see /snap/app/x1/api is a tmpfs populated with the objects from $SNAP_DATA (in this case /var/snap/app/x1/).

Can you tell me what I am missing in this test?


#7

(Very belated answer, but fortunately I left this browser tab open :smile: )

Thank you for this extensive test! The only difference is that in my snap, $SNAP/api is an existing directory created during snap creation – and that my snap has an install hook attempting to do stuff in the directories that I specified in layouts.

Short version: The bug sat in front of my computer :smile: I misread the docs about nested paths in bind-mounts and added mkdir commands to my install hook at some point. I previously used the $SNAP_DATA/rails_{$folder} version, but then switched to $SNAP_DATA/rails/$folder – and added an mkdir $SNAP_DATA/rails/$folder, which then tried to overwrite the bind-mount.

Takeaway: Maybe the error message could be clearer, but not a fault of layouts.


#8

Woot, that’s great to know. The up side is that 2.36 ships with layouts enabled and that nested directory limitation lifted now. Please try edge again and see if you can simplify your snapcraft.yaml now. I also heard that snapcraft will support layouts directly (CC @kyrofa to confirm please) so that the passthrough section can go away.


#9

Yessir, but only in edge right now, it’ll be in the next stable release (but only if using bases).


#10

@zyga, @degville this should probably be mentioned and linked to from The snap format


#11

I’ve just added this to The snap format document. Thanks!