Kyle Fazzari
on 3 October 2017
This article originally appeared on Kyle’s blog
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.
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! We never expected that!
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
As mentioned in the docs, there are two ways to create hooks using snapcraft. If we were writing hooks in, say, Python, we’d want to use the second method so Snapcraft defines an environment for us. However, in this tutorial we’re going to be writing pretty simple shell scripts for our hooks, so we’ll use the first method, which is a little easier, and create 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’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.