The content interface

The content interface allows sharing code and data from a slot on a producer snap to one or more plugs on consumer snaps. It’s often used to share common themes and icons from a producer snap, for example, to many snapped applications.

Sharing happens at the filesystem level, which means anything that can be expressed as a file can be shared. This includes executables, libraries and data files, but also sockets.

Example

The Yaru MATE Icons snap is a good producer snap example, letting other applications access the wonderful MATE icon theme. But there are many other producer snaps too, including several for GTK Common Themes and KDE Frameworks for better application integration with the desktop.

Interface documentation:

See Interface management and Supported interfaces for further details on how interfaces are used.


Developer details

Auto-connect: no, unless connecting to snaps from the same publisher.

Attributes:

  • source (slot): allows multiple directories to be exposed separately rather than grouped together
  • read (slot): read-only paths to be exposed to a consuming snap
  • write (slot): read and write paths to be exposed to a consuming snap
  • target (plug): path in consuming snap to find producer snap’s files
  • default-provider (plug): name of preferred producer snap (<SNAP>)
  • content (slot and plug): an arbitrary identifier for content type. Defaults to either local slot name or local plug name for slot/plug definitions respectively.
  • interface (slot and plug): snapd interface name (must be interface: content)

See below for more details on the following:

Sharing content

By default, when multiple directories are shared from a producer snap, or when multiple slots are connected to a single plug, the shared content is merged under the target path of the consuming path’s plug definition. This behaviour can be modified with the source attribute.

Read, write and target should start with either $SNAP, $SNAP_DATA or $SNAP_COMMON to refer to the designated directory. See Environment variables for details on where these point to on the filesystem.

The content identifier specified by the consuming snap (plug) must match the content attribute of the producer snap (slot).

At a very basic level, the content interface enables one directory, file or socket to appear in a place where another snap can access it.

Each example below involve two snaps: the first provides some content (using a content slot) while the second consumes that content (using a content plug).

In all of the cases we see a small set of attributes defined on the particular interface:

  • the producer declares which path can be read, using either the read attribute for read-only, or the write attribute for both read and write permissions
  • the consumer uses the target attribute to define where the content should become available at runtime.
  • both the producer and consumer use an arbitrary content attribute to describe the content. This attribute must match on both sides for the connection to happen.

Using source

The source attribute presents one or more sub-directories, shared from a slot to a plug, beneath the plug’s target path. Adding the source attribute ensures that sub-directories, shared from one or more producer snaps, are presented separately to the consumer snap beneath its target path.

When multiple slots are connected to the same plug and they share directories with the same name, those directories are given unique names with the following pattern: <directory>, <directory>-2, <directory>-3, <directory>-x. The names of shared directories with unique names are retained, as defined by the slot.

With the following example, directories from the producer snap are shared in corresponding directories beneath the consumer snap’s target path:

producer/snapcraft.yaml:

slots:
  _slot_name_:
    interface: content
    content: my-binaries
    source:
      read: 
        - $SNAP/bin

consumer/snapcraft.yaml:

plugs:
  _plug_name_:
    interface: content
    content: my-binaries
    target: $SNAP/usr/local/bin

Using the above configuration, the consumer snap could implement a part to run an executable from the following path:

$SNAP/usr/local/bin/<executable-name>

When more than one slot is connected to the same plug, the plug directory for the new connection will be incremented:

$SNAP/usr/local/bin-2/<executable-name>

Directory names are preserved after a reboot.

Read-only content sharing

Read-only content sharing is ideal for executables and files related to global graphical themes and images.

Sharing an executable

When the following two interfaces are connected, the consumer snap can invoke executables from $SNAP/usr/local/bin:

producer/snapcraft.yaml:

slots:
  _slot_name_:
    interface: content
    content: my-binaries
    read: 
      - $SNAP/bin

consumer/snapcraft.yaml:

plugs:
  _plug_name_:
    interface: content
    content: my-binaries
    target: $SNAP/usr/local/bin

The directory can be added to PATH in the wrapper script, if desired, and the directory can also be inspected by any applications that wish to check if the extra executables are available (they can then fail gracefully).

Sharing a C-level library

A consumer snap can link to libraries shared by a producer snap:

producer/snapcraft.yaml:

slots:
  old-libraries:
    interface: content
    content: lib0-1604
    read: 
      - $SNAP/lib

consumer/snapcraft.yaml:

plugs:
  old-libraries:
    interface: content
    content: lib0-1604
    target: $SNAP/extra-libs

After connecting the interface, the consumer snap can link to libraries from $SNAP/extra-libs. The directory can be added to LD_LIBRARY_PATH in the wrapper script if desired.

The value of the content attribute can be anything, but it is good practice to follow the form nameAPI-BUILDENV to remind slot consumers of the API level and build tools used. This naming convention is also required when sharing content between snap publishers.

In the above example:

  • 0 indicates API level 0
  • 1604 denotes Ubuntu 16.04 LTS toolchain and libraries were used within the build environment

API and BUILDENV can be anything that is meaningful to the provider and consumers. For example, the GNOME content snap uses gnome-3-26-1604 to denote the full GNOME 3.26 platform libraries and supporting files built on Ubuntu 16.04 LTS.

Content identifier obligations

The content identifier attribute identifies a mostly-immutable compatibility contract (API/ABI or similar) between the snap providing the corresponding content and the snaps consuming it.

The providing snap must preserve backward compatibility for the content provided under a given identifier.

Equally, updates to consuming snaps cannot strongly depend on changes of the identified content from updates to the providing snaps.

There is no support in snapd to synchronise updates between consuming and providing snaps. Compatibility breaking changes need to happen under a different content identifier.

Default provider

The optional default-provider attribute can be used to set to the name of a snap offering a corresponding content slot:

consumer/snapcraft.yaml

plugs:
  lib0-1604:
    interface: content
    content: lib0-1604
    target: $SNAP/extra-libs
    default-provider: lib01604

If the system does not contain a snap providing a matching slot, installing a consumer snap with a default-provider will trigger the automatic installation of the named provider snap (from snapd 2.32). If the named snap is already installed, the absence of a matching slot will instead trigger an update of the named provider snap (from snapd 2.53). The plug and slot will be auto-connected assuming the auto-connection mechanism is configured properly for this.

For example, a snap consuming the GNOME content snap for GNOME 3.26 can set default-provider to gnome-3-26-1604.

Sharing writable data

Sharing writable data can be used to share data files, and UNIX sockets, between a group of snaps. This allows for the creation of a simple form of IPC between them.

Sharing writable files (from snapd 2.19.1):

producer/snapcraft.yaml:

slots:
  _slot_name_:
    interface: content
    content: writable-data
    write: 
      - $SNAP_DATA

consumer/snapcraft.yaml:

plugs:
  _plug_name_:
    interface: content
    content: writable-data
    target: $SNAP_DATA

Sharing UNIX sockets (from snapd 2.19.1):

producer/snapcraft.yaml:

slots:
  _slot_name_:
    interface: content
    content: socket-directory
    write: 
      - $SNAP_DATA

consumer/snapcraft.yaml:

plugs:
  _plug_name_:
    interface: content
    content: socket-directory
    target: $SNAP_DATA

When the two interfaces are connected the consumer snap can see the socket in $SNAP_DATA.

Technical details

The content interface is implemented via an interplay between two systems: AppArmor and bind mounts.

By default, the AppArmor sandbox allows writes to $SNAP_DATA and reads from $SNAP (see Environment variables for details).

The content interface takes advantage of this feature to map data from other locations to either $SNAP or $SNAP_DATA.

A bind mount is then created to link $SNAP in one snap (e.g. from /snap/my-snap/1234/content) to an empty directory in the other snap (e.g., to /snap/my-other-snap/4321/incoming-content). Locations need to be selected carefully because a bind mount could potentially permit mounts over important locations, such as $SNAP_DATA, preventing access to a snap’s own writable space.

The same can be done for particular files, if desired, but it requires a pair of interfaces for each file and is more cumbersome.

Code examples

The previously mentioned Yaru MATE Icons snap is a good example of how this interface can be used to share media with other snaps. Its snapcraft.yaml can be found here: https://github.com/ubuntu-mate/icon-theme-yaru-mate-snap/blob/main/snap/snapcraft.yaml

The source code for this interface is in the snapd repository: https://github.com/snapcore/snapd/blob/master/interfaces/builtin/content.go

6 Likes

Content moved out of the wiki.

The enhancements implemented in Improvements in the content interface are not documented here yet (cc @zyga-snapd).

2 Likes

Thank you for sharing this! One note: on recent builds this does not appear to work unless

interface: content

is also specified alongside the content: xxx directive. Without this attempting to publish a snap throws errors such as

Error: interface '_slot_name_' not found in base declaration
Error: unknown interface '_slot_name_'

(in this case I was building against a core18 base; maybe there’s been a change since 16)

As far as I know that should have never worked, a plug has an interface type of _slot_name_ unless it specifies "interface". So the examples here all need fixes. cc @degville @zyga-snapd

1 Like

I fixed the examples and added the ‘interface’ attribute at the top. @degville, please feel free to adjust as necessary.

The attribute description for default-provider has now been corrected but has been for long:

In practice snapd has always ignored and still does the :<SLOT> part. The connection between the snap and its default provider will depend only and entirely on the general auto connection mechanism. The reason for doing this in the actual implementation is avoiding tight coupling between snaps, as was explained also in this old comment.

1 Like

I have a few questions about socket-directories using the content interface.

I have one snap that provides a socket-directory slot https://paste.ubuntu.com/p/N7wchfdYhM/.

The socket that the munge process creates here https://paste.ubuntu.com/p/FwvQ7JQNZw/ in SNAP_DATA.

I create another snap to test with munger https://paste.ubuntu.com/p/GtxZkyy9Dq/ which provides the plug.

I install both of the snaps and connect the content interface plug of one to the content-interface slot of the other https://paste.ubuntu.com/p/qhrQQGKFq3/.

Once both the snaps are connected, I can see the socket in the munge snap that creates it, but I can’t see the socket in SNAP_DATA of the snap that has the consuming plug https://paste.ubuntu.com/p/6tJPVs7x9s/

My question is, should I be able to ls SNAP_DATA and see the socket in both of the snaps?

Thanks!

EDIT: It seems that even though I can’t see the socket in the consuming snap, the process can see it. Given this, I feel that my socket-directory slot and plug are working as expected, its just seems difficult to verify this from the user perspective.

I’m experiencing an odd glitch when trying to set up a content slot on a newly built snap, according to the above instructions (which have worked for me previously). Issue is described here: Can't connect to content slot, since "content" directive somehow gets dropped from snapcraft.yaml.

Haven’t had any responses to that original post, and this has turned into a bit of a blocking issue, so I figured I’d drop a note here in case there are any ideas.

@jamesbeedy I know you posed your question a while ago and mostly self-answered it, but I figured I’d add my $0.02. Indeed, if you are just browsing your filesystem you will not see the content of mounted content plugs. So for example

$ ls /var/snap/my-snap/current/slot-content

will return nothing, even if there’s something mounted there. To look at the contents, you can drop into the snap’s shell environment by doing

sudo snap run --shell my-snap.my-command

Then you should be able to see the mounted contents

# ls $SNAP_DATA/slot-content
socket

I’m not 100% sure if this applies for all types of content, but it does seem to be the case for socket files at least.

1 Like

should be “provides” right? Just a simple error.

Corrected, thanks! :slight_smile:

I wonder if we should list common content snaps here…

1 Like

I think it could be useful

1 Like

Hi @mapero, I recommend creating a new topic for your question. This discussion is maily for talking about the docs. Also, creating a new topic, your issue will have far more visibility :wink:

1 Like

Edit: Apparently I had edit permissions, so this has now been fixed.

The example under Using source has a slight error in it:

slots:
  _slot_name_:
    interface: content
    content: executables
-     source:
+    source:
       read: 
         - $SNAP/bin
1 Like

Great spot, thanks for the comment and updating the doc!

Should that say “should”, or should it say “must”? What other directories are eligible for sharing with a content interface, apart from those three?

Hi! I’ve tried sharing a socket between to snaps, but couldn’t get it to work the same way that this documentation does. Here’s what I did:

Provider-Snap:

slots:
  tailscale-socket:
    interface: content
    content: tailscale-socket
    write: 
      - $SNAP_DATA

Consumer Snap:

plugs:
  tailscale-socket:
    interface: content
    content: tailscale-socket
    target: $SNAP_DATA

The provider snap creates the socket at $SNAP_DATA, as expected. But I couldn’t get it to show up on the other snap:

root@auth-test:~# snap run --shell derper
root@auth-test:/root# ls $SNAP_DATA
var
root@auth-test:/root# 

It only worked, after moving it into a separate directory on both sides:

slots:
  tailscale-socket:
    interface: content
    content: tailscale-socket
    write: 
      - $SNAP_DATA/socket
plugs:
  tailscale-socket:
    interface: content
    content: tailscale-socket
    target: $SNAP_DATA/socket
root@auth-test:~# snap run --shell derper
root@auth-test:/root# ls $SNAP_DATA/socket/
tailscaled.sock
root@auth-test:/root# 

Is sharing the socket from $SNAP_DATA directly not possible? The example in the docs says so. Or did I miss something else?

There is a bind mount going on between the two snaps, if you would actually allow one snap to mount its whole $SNAP_DATA over the $SNAP_DATA of the other snap, it would lose its whole own writable space and might result in weird, broken or unexpected behavior …

We should surely somehow note this in the doc though…

1 Like