When it comes to developing snaps, there’s a particular confusion out there that I see over and over again: build-time versus run-time. For example: “I’m building a snap, but I can’t seem to convince Snapcraft to place my config file in $SNAP_DATA.” In this post, I want to show you how to get the results you want.

First of all, we need to clear something up. Snapcraft can’t actually do this for you. Why? Because Snapcraft is responsible only for building the my-cool-thing_v1.0.snap. Once that snap is built, you can install it. Before the snap is installed/running (e.g. while you’re building the snap), $SNAP, $SNAP_DATA, etc. do not exist. They are defined by snapd when the snap is installed.

To put it another way, the snap created by Snapcraft is completely immutable and self-contained. The snap does not contain $SNAP_DATA, and has no concept of $SNAP_DATA until it is actually installed. Which means Snapcraft doesn’t have that concept either, and can’t help you.

However, as of v2.27, snapd does have the ability to help you, by way of the install hook. Generically, hooks are the way snapd tells a snap that something interesting has happened, and gives it a chance to do something about it. At its most basic, a hook is an executable contained in the snap that snapd calls when it feels like it should. In the case of the install hook, it’s letting the snap know that it’s currently being installed, and provides the opportunity for the snap to do something as part of that process, before services are fired up, and potentially bail out of the entire install process if required.

To be clear: the install hook runs only upon initial install, not a refresh or remove (there are other hooks for those situations).

Let me explain with a tutorial.

Tutorial

The problem

First of all, let’s get started by creating a brand new snap that suffers from a pretty typical problem: a service that requires a config file, but that config file needs to be in a writable place (e.g. $SNAP_DATA). Once we have that, we’ll discuss how the install hook solves that problem.

So, get into the directory that will contain our snap, and run:

$ snapcraft init
Created snap/snapcraft.yaml.
Edit the file to your liking or run `snapcraft` to get started

Now, let’s create our service. We’ll keep it simple: this service simply says “Hello, World!” over and over again, at a rate determined by a config file. First, create the service executable itself, and make sure it’s executable:

$ mkdir -p src/bin
$ touch src/bin/hellod
$ chmod a+x src/bin/hellod

Now make that file look like this:

#!/bin/sh -e

config_file="$SNAP_DATA/hello.conf"

while true; do
   # First, determine our rate by determining how long we should sleep
   sleep_time="$(awk '/^sleep_time/{print $2}' "$config_file")"

   # Now be nice and greet
   echo "Hello, World!"

   # Now sleep for the time specified in the config file
   sleep "$sleep_time"
done

Update your snap/snapcraft.yaml to include this service by adding both a part to install it as well as an app to expose it as a service. While we’re at it, let’s change the confinement type to strict instead of devmode. Make it look something like this (most of it is still the template):

name: my-snap-name # you probably want to 'snapcraft register <name>'
version: '0.1' # just for humans, typically '1.2+git' or '1.3.2'
summary: Single-line elevator pitch for your amazing snap # 79 char long summary
description: |
 This is my-snap's description. You have a paragraph or two to tell the
 most important story about your snap. Keep it under 100 words though,
 we live in tweetspace and your description wants to look good in the snap
 store. 

grade: devel # must be 'stable' to release into candidate/stable channels
confinement: strict

parts:
 my-service:
   plugin: dump
   source: src/

apps:
 hellod:
   command: hellod
   daemon: simple

Let’s go ahead and build this snap, and then install it (--dangerous because it’s not from the store):

$ snapcraft
Preparing to pull my-service 
Pulling my-service 
Preparing to build my-service 
Building my-service 
Staging my-service 
Priming my-service 
Snapping 'my-snap-name' |
Snapped my-snap-name_0.1_amd64.snap
$ sudo snap install my-snap-name_0.1_amd64.snap --dangerous
my-snap-name 0.1 installed

Now as you probably know, as soon as you install the snap, its services are fired up. Let’s take a look at the output from our service:

$ journalctl -u snap.my-snap-name.hellod.service 
-- Logs begin at Mon 2017-09-11 07:51:07 PDT, end at Mon 2017-09-11 10:26:03 PDT. --
Sep 11 10:23:18 Pandora systemd[1]: Started Service for snap application my-snap-name.hellod.
Sep 11 10:23:19 Pandora my-snap-name.hellod[8527]: awk: fatal: cannot open file `/var/snap/my-snap-name/x1/hello.conf' for reading (No such file or directory)
Sep 11 10:23:19 Pandora systemd[1]: snap.my-snap-name.hellod.service: Main process exited, code=exited, status=2/INVALIDARGUMENT
Sep 11 10:23:19 Pandora systemd[1]: snap.my-snap-name.hellod.service: Unit entered failed state.

Ah, well darn, our service is failing because it can’t find the config file we didn’t put there– how unexpected!

Sarcasm aside, while this example is contrived, this is a very common problem. Oftentimes config files need to be writable by the user, which means they simply cannot be held within the snap itself, which is by definition immutable. However, one is left with the problem of needing to get a config file in a writable area before the service that needs it is started, exactly like this example. Before snapd v2.27, the only solution was to wrap one’s service in a shell script that checked for the config file and created it if it wasn’t there. This was tedius and annoying, but no more! Let’s solve this the Right Way™ by creating an install hook. First though, let’s remove the snap we already have installed:

$ sudo snap remove my-snap-name

The solution

Let’s start by creating a new directory to hold our hooks:

$ mkdir snap/hooks

Now let’s create the install hook, and make sure it’s executable:

$ touch snap/hooks/install
$ chmod a+x snap/hooks/install

Now we’ll make that file look like this:

#!/bin/sh -e

# Create a default config file
echo "sleep_time 5" > "$SNAP_DATA/hello.conf"

Believe it or not, that’s it. Snapcraft knows this is a hook because you put it in the snap/hooks/ dir and it’s executable, and it’ll place it in the right spot in the snap. Snapd knows it’s the install hook because the file name is “install”. Note that the code for this snap is available on GitHub for reference.

NOTE: Hooks run confined, just like apps. This particular hook requires no special permission, so nothing extra was required for it to work. However, let’s say our hook required access to the network. In that case, it would need to use the network plug the same way as an app would need to. The way to specify this requirement is to add a section to your YAML that looks like this:

hooks:
 install:
   plugs: [network]

Again, that’s not required here, but keep it in mind.

Alright back to it: let’s build the snap from scratch again, and install it once more:

$ snapcraft clean
Cleaning up priming area
Cleaning up staging area
Cleaning up parts directory
$ snapcraft
Preparing to pull my-service 
Pulling my-service 
Preparing to build my-service 
Building my-service 
Staging my-service 
Priming my-service 
Snapping 'my-snap-name' |
Snapped my-snap-name_0.1_amd64.snap
$ sudo snap install my-snap-name_0.1_amd64.snap --dangerous
my-snap-name 0.1 installed

Now take a look at the service journal, and you’ll see it working:

$ journalctl -fu snap.my-snap-name.hellod.service 
-- Logs begin at Mon 2017-09-11 07:51:07 PDT. --
Sep 11 10:59:54 Pandora my-snap-name.hellod[11625]: Hello, World!
Sep 11 10:59:59 Pandora my-snap-name.hellod[11625]: Hello, World!
Sep 11 11:00:04 Pandora my-snap-name.hellod[11625]: Hello, World!

How is this working? Well, now that you provided an install hook, snapd ran it as part of the installation. You can see specifically when by taking a look at the change representing the snap installation. First, we need to determine the change ID:

$ snap changes
<snip>
1138 Done <snip> Install "my-snap-name" snap from file "my-snap-name_0.1_amd64.snap"

Now take a look at that particular change:

$ snap change 1138
Status <snip> Summary
Done           Prepare snap "/tmp/snapd-sideload-pkg-840816098" (unset)
Done           Mount snap "my-snap-name" (unset)
Done           Copy snap "my-snap-name" data
Done           Setup snap "my-snap-name" (unset) security profiles
Done           Make snap "my-snap-name" (unset) available to the system
Done           Setup snap "my-snap-name" (unset) security profiles (phase 2)
Done           Set automatic aliases for snap "my-snap-name"
Done           Setup snap "my-snap-name" aliases
Done           Run install hook of "my-snap-name" snap if present
Done           Start snap "my-snap-name" (unset) services
Done           Run configure hook of "my-snap-name" snap if present

Note in particular where the ‘Run install hook of “my-snap-name” snap if present’ is happening: right before services are started. Which gives it a chance to set things up so that services have what they require before they try to run. If the install hook runs into a problem, it can exit non-zero and cause the entire installation to abort.

After installation, one can update that config file to shorten or lengthen the sleep time as required. While that may not be particularly interesting for this snap, imagine if this was an Apache configuration file, or MySQL. Depending on the snap, it may be quite useful to expose the configuration this way, and of course can be used for other things as well (e.g. plugins, assets, generate an initial database, etc.) In a follow-up post, we’ll discuss another way to manage configurations, by way of the configure hook.