You’ve heard it a million times: snaps bundle their dependencies. People seem to understand and accept the technical aspects of this, but today I want to talk about a more philosophical aspect. If you’re used to more traditional packaging, then you’re used to each project being standalone, e.g. Apache is its own package, with its own configuration; PHP is its own package, with its own configuration; and so on. As you begin creating a snap, perhaps one that bundles Apache, it’s easy to automatically gravitate toward wanting to make those config files available to your users as well. That’s fine, and doing something like that is covered in an earlier post about the
install hook, but I want to challenge your thinking.
Think about what you’re creating. Continuing with the Apache example, is it just an Apache snap? Meant to host any web application? If so, then yeah, it probably needs to be super flexible and exposing the full Apache config to the user may be a reasonable thing to do. However, oftentimes you’re bundling Apache together with other things to create a specific product, perhaps a web application. If so, I suggest you stop thinking of your snap as a collection of parts (Apache, PHP, MySQL), and think of it as quite simply your product. What experience do you want your user to have when interacting with it? Does your user even need to care that you use Apache? What if you want to use NGINX later? Do you want to force your user to edit the full Apache config when the only thing they want to do is change the port? Perhaps it would be better to have a slick way for the user to ask your product to simply listen on a different port. Snapd actually provides a standardized configuration interface that interacts with your snap by way of the
As a reminder, hooks are simply executables that are shipped in the snap which snapd calls at predefined times. When is the
configure hook called? In a few different situations:
- When the snap is first installed, after services are started
- When the snap is refreshed (updated)
- Whenever snap set is called to alter the snap’s configuration
(Honestly, I think the first two situations was a mistake, but I digress.)
Bear with me, now. I want to show you what I mean with a quick tutorial.
In an earlier post about the install hook, we created a very simple snap containing a service that said “Hello, World!” at a rate determined by a configuration file. I’d like to build on that example here, by no longer using a configuration file at all, and using snapd to manage the configuration for us.
Let’s start with that snap. If you don’t happen to have it around, you don’t need to read the other post– just grab it off of GitHub.
Our first step toward letting snapd manage our configuration is to stop using a configuration file. That file is created in the
install hook, so let’s start there. Right now it looks like this:
#!/bin/sh -e # Create a default config file echo "sleep_time 5" > "$SNAP_DATA/hello.conf"
Instead of creating that config file, we can ask snapd to record that configuration for us by using the
snapctl command (see snapctl -h to learn more about it). Change the install hook to look like this:
#!/bin/sh -e # Initialize the sleep time to a default snapctl set sleep-time=5
snapctl in this way saves the value
5 into the
sleep-time variable of this particular snap’s configuration (e.g. another snap could use
snapctl set sleep-time=2 and this one would still be
Now let’s change our service to use
snapctl instead of the config file as well:
#!/bin/sh -e while true; do # First, determine our rate by determining how long we should sleep sleep_time="$(snapctl get sleep-time)" # Now be nice and greet echo "Hello, World!" # Now sleep for the time requested sleep "$sleep_time" done
At this point we could build and install this snap, and it would behave exactly the same way as in the previous post (printing its greeting every 5 seconds), but we wouldn’t be using a configuration file anymore. Of course, that also means that the user has no way to change this behavior, and here I was bragging about this “standardized configuration interface” blah blah, what about that, you ask?
Well, in the same way that
get can be used from within the snap,
set can be used from outside the snap (by the user). However,
snap set can only be used once we implement a configure hook, so let’s do that.
First, create the hook and make it executable:
$ touch snap/hooks/configure $ chmod a+x snap/hooks/configure
Now make that file look like this:
#!/bin/sh -e # Obtain sleep-time value sleep_time="$(snapctl get sleep-time)" # Validate it if ! expr "$sleep_time" : '^[0-9]*$' > /dev/null; then echo "\"$sleep_time\" is not a valid sleep time" >&2 exit 1 fi
So here’s how this works. When the user calls
snap set with key-value pairs, snapd calls the
configure hook. If the
configure hook exits successfully, snapd applies (saves) the configuration. If the
configure hook exits non-zero, snapd considers the configuration bad and doesn’t save it. So all we need to do in the
configure hook is ensure that the sleep time is valid, and if not, exit non-zero.
An important point before we move on: if the
configure hook exits non-zero during an install or a refresh, that operation is rolled back (e.g. if a refresh, the snap is rolled back to the previously-working snap). (How does the hook know that it’s running during one of those times? It doesn’t, which is one of the reasons I think this design was a mistake and you should use the
post-refresh hooks for that stuff, but I digress again.)
Alright, now that we have that out of the way, let’s build and install this snap (remember,
--dangerous because this isn’t 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 check the journal:
$ journalctl -fu snap.my-snap-name.hellod.service -- Logs begin at Mon 2017-09-11 07:51:07 PDT. -- Sep 11 13:35:40 Pandora my-snap-name.hellod: Hello, World! Sep 11 13:35:45 Pandora my-snap-name.hellod: Hello, World!
As expected, we see a greeting every 5 seconds. We can verify that configuration setting using
$ snap get my-snap-name sleep-time 5
There we see the value that we set in the
install hook. We can change it using
$ snap set my-snap-name sleep-time=1 $ snap get my-snap-name sleep-time 1
You can see that the new value applied. Let’s check the journal:
$ journalctl -fu snap.my-snap-name.hellod.service -- Logs begin at Mon 2017-09-11 07:51:07 PDT. -- Sep 11 13:40:17 Pandora my-snap-name.hellod: Hello, World! Sep 11 13:40:18 Pandora my-snap-name.hellod: Hello, World!
There we go, a greeting once a second. Now let’s see what happens if we try to set an invalid value:
$ snap set my-snap-name sleep-time=foo error: cannot perform the following tasks: - Run configure hook of "my-snap-name" snap (run hook "configure": "foo" is not a valid sleep time) $ snap get my-snap-name sleep-time 1
As you can see, the invalid value was caught by our
configure script, and it was not applied.
I hope you can see how this might apply to your snap, and how giving your product a consistent interface for configuration might lead to a better overall user experience than requiring them to edit configuration files. It’s also easier for you to validate!