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.
Prerequisites
- A rough understanding of how snaps work and how to create one.
- Resources: snapcraft
-
snap
andsnapcraft
install.
jujuwrapper
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.
So your apps
directive will at least include network
and 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 jujudata
because
- commands like
juju switch
will 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 JUJU_DATA="~/path/to/data"
.
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.
Shared jujudata
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 personal-files
interface:
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.
Summing up
The final 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: strict-juju-wrapper-template
.
If youâre snapping a python-based package, it should offer a good starting point.