Kyle Fazzari
on 3 October 2017
This article originally appeared on Kyle’s blog
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 configure hook.
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
Bear with me. I want to show you what I mean with a quick tutorial.
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. Normally I’d suggest that you use snapctl -h to learn about it, but that’s broken, so you’ll just have to trust me. Change the install hook to look like this:
#!/bin/sh -e # Initialize the sleep time to a default snapctl set sleep-time=5
Using 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 5).
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 snapctl set/get can be used from within the snap, snap get/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).
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[18026]: Hello, World! Sep 11 13:35:45 Pandora my-snap-name.hellod[18026]: Hello, World!
As expected, we see a greeting every 5 seconds. We can verify that configuration setting using snap get:
$ 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:
$ 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[18026]: Hello, World! Sep 11 13:40:18 Pandora my-snap-name.hellod[18026]: 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.
Conclusion
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!