Plugs in global position in snapcraft.yaml

Can plugs be added to the global section to affect all apps in the snap?

example:

name: someplug
...
apps:
   example:
   ...

plugs:
  wayland:
  x11:
   

Hi,

You have two options, you can either declare all plugs globally and declare no plugs in any app or hook, which would look like this:

apps:
  example:
    command: bin/sh
  example2:
    command: bin/sh2

hooks:
  configure:

plugs:
  wayland:
  x11:

or you can declare them for every app/hook like so:

apps:
  example:
    command: bin/sh
    plugs:
      wayland:
      x11:
  example2:
    command: bin/sh2
    plugs:
      wayland:
      x11:
hooks:
  configure:
    plugs:
      wayland:
      x11:

These two are equivalent for all of the hooks and apps in the snap.

But what you should not do is mix the two patterns because then the plugs declared globally become like a “pool” where they get used up by the first things that declare them and don’t apply to anything else implicitly. For example this will not work the way one expects it:

apps:
  example:
    command: bin/sh
    plugs:
      wayland:
      x11:
      opengl:
  example2:
    command: bin/sh2
hooks:
  configure:
plugs:
  wayland:
  x11:

What happens here is that the global plugs wayland and x11 will only apply to the example app, and the example2 app and the configure hook have no plugs declared at all, which is usually undesirable, but is specifically useful for interfaces that require attributes to be set.

1 Like

Thank you. This makes it clearer.

About the bug in snapcraft 3.9.x i will talk elsewhere.

@ijohnson
One more question:
If an app inside the app is not listed under apps. Would then the global plugs be applied?

Can you provide an example yaml? I don’t quite follow how you could have an app not under apps.

Sorry I had not seen this thread before reviewing https://github.com/snapcore/snapcraft/pull/2840.

While I don’t really change my position in that PR, I’m a bit perplexed as to @ijohnson’s assertion:

I have not seen documented anywhere? I assume you are correct, but I think we should consider either normalizing the behavior (global plugs apply to all apps regardless of use of per-app plugs), or have snapcraft error when we come across this scenario. Is there a rationale for the current behavior, or just a limitation of the existing implementation?

That’s exactly what I thought intuitively.

Unfortunately yes it is not documented anywhere at the moment, in fact I partially wrote the above to serve as a starting point for a doc that @degville is/will be working on to make this clear going forward.

One reason for this is that when we have a more complicated example like this (from https://bugs.launchpad.net/snapd/+bug/1824557):

plugs:
  k8s-kubelet:
    interface: kubernetes-support
    flavor: kubelet
  k8s-kubeproxy:
    interface: kubernetes-support
    flavor: kubeproxy

where these two interfaces should be treated as effectively mutually exclusive in the snap’s hooks/daemons, because they have different flavors. Specifically the “flavors” here correspond to slightly different policy that is generated that fails to compile when combined together. So in this case, we want to be able to declare a full plug definition of the kubernetes-support interface for the different apps/daemons. We also don’t want both of these things to be auto-generated for the implicit hooks, i.e. hooks that aren’t declared in the snap.yaml.

The other reason is that I think we want to be able to express “self-connecting” snaps with slots exposed by the snap and plugs connected to those slots all in the same snap. Here again there could be conflicting policy between the plug side and the plot side, so for example in the docker snap we have this:

plugs:
  docker-cli:
    interface: docker
slots:
  docker-daemon:
    interface: docker
apps:
  docker:
    ...
    plugs:
    - docker-cli
  dockerd:
    plugs:
      - docker-support
    slots:
    - docker-daemon

In this case the docker interface is the interface that allows talking to the docker socket, not permissions to operate as docker (that interface is docker-support), so the policy between the slot and the plug is not mutually exclusive but there are other examples, like network-manager, etc. that have more complicated policy in the slot and plug that could be mutually exclusive, so applying all top-level plugs and slots to all apps/hooks would be a bad idea.

I admit that this is confusing, I actually had implemented a patch in snapd to fix this and make all of the top-level plugs/slots apply to all hooks and apps, but this broke our unit tests and further investigation led me to my current understanding.

Additionally, there is some more history I’m not fully aware of here, namely @kyrofa’s PR from over 3.5 years ago: https://github.com/snapcore/snapd/pull/1240, which says this about binding plugs to hooks or apps this way:

Also note that hooks are factored into the equation when considering whether or not a global plug is unbound-- if it’s bound to either an app OR a hook it’s considered bound for both, in which case the plug will not be bound to all apps and hooks. This is up for discussion.

However I can’t find anywhere that that discussion happened or what were other reasons to do it this way.

3 Likes

Hoo boy, hey there archeologist :wink: . That was the first in a long line of PRs adding the initial hooks feature to snapd. It didn’t change the logic of how global plugs were bound to apps, just included hooks in the picture. I’m afraid I have no additional insight into why it was designed this way originally.

Assuming my memory is correct, before hooks were a thing, in the code, each plug, slot, and app was essentially a single object that tracks its connections. As a quick example, consider:

apps:
  app1:
    command: command1
    plugs: [plug1]

  app2:
    command: command2
    plugs: [plug1]

  app3:
    command: command3

plugs:
  plug2:

In this case, app1 knows that it’s using plug1. app2 also knows it’s using plug1. Likewise, plug1 knows that it’s being used by both app1 and app2. However, app3 knows it’s not using any plugs. Similarly, plug2 knows that no apps are using it. In the paragraph quoted by @ijohnson, plug2 is what I called “unbound”, i.e. it wasn’t bound to an app, therefore snapd applied it to ALL apps, 1, 2, and 3. If, instead, app3 looked like this:

  app3:
    command: command3
    plugs: [plug2]

Then plug2 would NOT be unbound (it would be bound to app3), and would thus NOT apply to app1 or app2.

This particular PR introduced the concept of a hook simply by adding hooks as one more object that was treated exactly like just another app as far as the binding logic is concerned. Basically, unbound plugs would apply to all apps AND hooks. Also, if a hook used a plug, that plug was considered bound, even when it came to apps, thereby NOT applying to all the apps (or the other hooks). If an app used a plug, it was considered bound, thereby NOT applying to all the hooks (or the other apps). To be clear, take this example:

apps:
  app1:
    command: command1
    plugs: [plug1]

  app2:
    command: command2
    plugs: [plug1]

hooks:
  hook1:

plugs:
  plug2:

plug2 is unbound, thus it applies to app1, app2, and hook1. Now take this:

apps:
  app1:
    command: command1
    plugs: [plug1]

  app2:
    command: command2
    plugs: [plug1]

hooks:
  hook1:
    plugs: [plug2]

plugs:
  plug2:

Now plug2 is bound (it’s associated with hook1), thus it will NOT be applied to app1 or app2.

Does that clarify at least the paragraph I wrote a little?

Hey, that’s sound like all case’s where answered now :wink: :slight_smile:

But it’s not very intuitive.

Maybe it should be a must, that each app or hook indicates the plug or slot that it uses.

In order to avoid duplicate definitions, groups of plugs or slots could be defined.

E. g.

apps:
   app1:
      command: 
      plugs:   [ plug1, plug_group1 ]
      slots:   [ plug3 ]

hooks:
   hook1: 
      plugs: [ group2, plug4 ]

plug-groups:
    group1: [ plug2, plug3 ]
    group2: [ plug1, plug3 ]

Then it would be possible to remove global plugs and slots.

I recommend that the top-level plugs only be used to declare the configuration of a plug, and that you must specify the plugs that an app or hook requires in the apps and hooks stanzas. If all your apps and hooks use the same interfaces then you can use yaml aliases:

apps:
  my-app:
    plugs: &allplugs
      - plug1
      - plug2
      - plug3
  another-app:
    plugs: *allplugs

hooks:
  plugs: *allplugs

– I’ve been googling to see if it is possible to superset an alias containing a list with extra items to be added, but can’t find any docs indicating that is possible :frowning:

1 Like