Recently I set out to write a snap for
jhack, a python cli tool that requires juju client access. In other words, it needs to be able to make direct juju cli calls such as
juju status --relations, and it uses
python-libjuju for some other calls.
I got a prototype working in devmode, but I wanted to release it with strict confinement to make it easier for people to trust it and install it.
Here is a small tutorial with some tips and lessons learned which should make your life easier if you are attempting to do something similar.
- A rough understanding of how snaps work and how to create one.
- Resources: snapcraft
Instead of slowly building up to the final product, I’m going to show the TLDR first:
This is a template/demo project that shows how to go about creating a snap that wraps the juju client snap and give a starting point to create similar snaps.
The README should get you started quickly.
Setting up the snap
This is not a tutorial on how to write a snap. I’ll assume you know how to write one, and instead focus on how to get it to talk to your local, snapped, juju client.
So, fast-forward to when you have a
snap/snapcraft.yaml containing your snap metadata.
The first thing you want to do is add:
stage-snaps: - juju/3.0/beta
to one of your parts.
For example, if you’re snapping a python-repo-based tool, your
parts directive could be as simple as:
parts: mypart: plugin: python source: https://github.com/YourName/repo.git source-branch: main python-packages: # any other requirements - juju==2.9.7 stage-snaps: - juju/3.0/beta # juju 3 is strictly confined
Adding the juju snap as a stage-snap is basically adding it as a dependency of your snap. It will be packaged together with it and your snap will have its own embedded juju client binary (as a snap package). We’ll call that ‘the inner juju snap’.
Defining the interfaces
First and foremost, the snap needs network access to be able to talk to your local microk8s controller, or remote clouds.
apps directive will at least include
network-bind. So unless you have them already, add the following plugs:
plugs: - network - network-bind
Secondly, the outer juju install you are trying to wrap might have some clouds, controllers, etc… already bootstrapped and active. We want our strictly confined snap to be able to see them just like your local juju does.
For that, we need access to the so-called
jujudata, a folder typically under
~/.local/share/juju which contains credentials, controller info, model info (which model is the “current” model…), etc…
A strictly-confined snap can’t just read/write what it wants, so we need to add some interfaces to it.
Crucially, read access is not enough. The juju client needs write access to
- commands like
juju switchwill change the client state by writing data to some of those files
- the juju client writes some lock files in that folder, and will not run most commands unless it manages to acquire them
The juju snap offers a
juju-client-observe interface that gives us read access, but no write.
There are two ways I considered to achieve what I needed.
Copy jujudata over to snap
Firstly, I could use the
juju-client-observe interface offered by the
juju snap to get read access to the client configuration, then add a hook to copy that directory over to some snap-controlled filesystem location (under whatever “
~” is for the snap), and prefix every juju command with
This would give the embedded juju snap awareness of the clouds, controllers etc… available to the ‘outer’ snap.
The app would have looked like:
apps: myapp: command: bin/myapp_main plugs: - network - network-bind # read-only access to .local/share/juju (JUJU_DATA) - juju-client-observe
And you’d have to add a
snap/hooks/install hook with something like:
cp -r /home/hans/.local/share/juju ~/jdata
You might need to pass the username or home root as a config option, too.
However, I was worried that the two directories would go out of sync. What happens if the inner juju unregisters a controller, and the outer juju doesn’t know about it? What happens if the inner juju switches its current model? What happens if you bootstrap a new controller?
I’m not entirely sure these worries are justified, but also, this felt like cheating.
The second option felt safer: to explicitly grant my snap write access to the user’s
jujudata. That way, two clients could share state and sidestep any desync issues.
The only way I could find to do that is via a
I copied what the
juju snap itself does and added a
dot-local-share-juju plug defined as:
dot-local-share-juju: interface: personal-files write: - $HOME/.local/share/juju
This means the inner juju snap will be able to see the same controllers, clouds, etc… as your outer one, and they will be completely in sync.
The fact that they share state also means that they might lock each other out of running certain operations in parallel.
snapcraft.yaml would then contain:
[...] parts: my-part: [...] # bulk of your part definition stage-snaps: - juju/3.0/beta apps: my-app: plugs: - network - network-bind # read-write access to .local/share/juju (JUJU_DATA) - juju-client-observe - dot-local-share-juju plugs: dot-local-share-juju: interface: personal-files write: - $HOME/.local/share/juju
And this is all that you need to wrap the juju client.
As previously announced, there is a little demo project that doubles up as a template:
If you’re snapping a python-based package, it should offer a good starting point.