Skip to content

Commit 5a3c924

Browse files
authored
facts+operations/docker: add docker plugin support
1 parent bd02b67 commit 5a3c924

File tree

13 files changed

+391
-38
lines changed

13 files changed

+391
-38
lines changed

pyinfra/facts/docker.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,28 @@ def command(self) -> str:
7676
return "docker network inspect `docker network ls -q`"
7777

7878

79+
class DockerVolumes(DockerFactBase):
80+
"""
81+
Returns ``docker inspect`` output for all Docker volumes.
82+
"""
83+
84+
@override
85+
def command(self) -> str:
86+
return "docker volume inspect `docker volume ls -q`"
87+
88+
89+
class DockerPlugins(DockerFactBase):
90+
"""
91+
Returns ``docker plugin inspect`` output for all Docker plugins.
92+
"""
93+
94+
@override
95+
def command(self) -> str:
96+
return """
97+
ids=$(docker plugin ls -q) && [ -n "$ids" ] && docker plugin inspect $ids || echo "[]"
98+
""".strip()
99+
100+
79101
# Single Docker objects
80102
#
81103

@@ -113,19 +135,17 @@ class DockerNetwork(DockerSingleMixin):
113135
docker_type = "network"
114136

115137

116-
class DockerVolumes(DockerFactBase):
138+
class DockerVolume(DockerSingleMixin):
117139
"""
118-
Returns ``docker inspect`` output for all Docker volumes.
140+
Returns ``docker inspect`` output for a single Docker container.
119141
"""
120142

121-
@override
122-
def command(self) -> str:
123-
return "docker volume inspect `docker volume ls -q`"
143+
docker_type = "volume"
124144

125145

126-
class DockerVolume(DockerSingleMixin):
146+
class DockerPlugin(DockerSingleMixin):
127147
"""
128-
Returns ``docker inspect`` output for a single Docker container.
148+
Returns ``docker plugin inspect`` output for a single Docker plugin.
129149
"""
130150

131-
docker_type = "volume"
151+
docker_type = "plugin"

pyinfra/operations/docker.py

Lines changed: 130 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,27 @@
44
as inventory directly.
55
"""
66

7+
from __future__ import annotations
8+
79
from pyinfra import host
810
from pyinfra.api import operation
9-
from pyinfra.facts.docker import DockerContainer, DockerNetwork, DockerVolume
11+
from pyinfra.facts.docker import DockerContainer, DockerNetwork, DockerPlugin, DockerVolume
1012

1113
from .util.docker import ContainerSpec, handle_docker
1214

1315

1416
@operation()
1517
def container(
16-
container,
17-
image="",
18-
ports=None,
19-
networks=None,
20-
volumes=None,
21-
env_vars=None,
22-
pull_always=False,
23-
present=True,
24-
force=False,
25-
start=True,
18+
container: str,
19+
image: str = "",
20+
ports: list[str] | None = None,
21+
networks: list[str] | None = None,
22+
volumes: list[str] | None = None,
23+
env_vars: list[str] | None = None,
24+
pull_always: bool = False,
25+
present: bool = True,
26+
force: bool = False,
27+
start: bool = True,
2628
):
2729
"""
2830
Manage Docker containers
@@ -168,7 +170,7 @@ def image(image, present=True):
168170

169171

170172
@operation()
171-
def volume(volume, driver="", labels=None, present=True):
173+
def volume(volume: str, driver: str = "", labels: list[str] | None = None, present: bool = True):
172174
"""
173175
Manage Docker volumes
174176
@@ -220,20 +222,20 @@ def volume(volume, driver="", labels=None, present=True):
220222

221223
@operation()
222224
def network(
223-
network,
224-
driver="",
225-
gateway="",
226-
ip_range="",
227-
ipam_driver="",
228-
subnet="",
229-
scope="",
230-
aux_addresses=None,
231-
opts=None,
232-
ipam_opts=None,
233-
labels=None,
234-
ingress=False,
235-
attachable=False,
236-
present=True,
225+
network: str,
226+
driver: str = "",
227+
gateway: str = "",
228+
ip_range: str = "",
229+
ipam_driver: str = "",
230+
subnet: str = "",
231+
scope: str = "",
232+
aux_addresses: dict[str, str] | None = None,
233+
opts: list[str] | None = None,
234+
ipam_opts: list[str] | None = None,
235+
labels: list[str] | None = None,
236+
ingress: bool = False,
237+
attachable: bool = False,
238+
present: bool = True,
237239
):
238240
"""
239241
Manage docker networks
@@ -245,6 +247,7 @@ def network(
245247
+ ipam_driver: IP Address Management Driver
246248
+ subnet: Subnet in CIDR format that represents a network segment
247249
+ scope: Control the network's scope
250+
+ aux_addresses: named aux addresses for the network
248251
+ opts: Set driver specific options
249252
+ ipam_opts: Set IPAM driver specific options
250253
+ labels: Label list to attach in the network
@@ -303,9 +306,9 @@ def network(
303306

304307
@operation(is_idempotent=False)
305308
def prune(
306-
all=False,
307-
volumes=False,
308-
filter="",
309+
all: bool = False,
310+
volumes: bool = False,
311+
filter: str = "",
309312
):
310313
"""
311314
Execute a docker system prune.
@@ -344,3 +347,101 @@ def prune(
344347
volumes=volumes,
345348
filter=filter,
346349
)
350+
351+
352+
@operation()
353+
def plugin(
354+
plugin: str,
355+
alias: str | None = None,
356+
present: bool = True,
357+
enabled: bool = True,
358+
plugin_options: dict[str, str] | None = None,
359+
):
360+
"""
361+
Manage Docker plugins
362+
363+
+ plugin: Plugin name
364+
+ alias: Alias for the plugin (optional)
365+
+ present: Whether the plugin should be installed
366+
+ enabled: Whether the plugin should be enabled
367+
+ plugin_options: Options to pass to the plugin
368+
369+
**Examples:**
370+
371+
.. code:: python
372+
373+
# Install and enable a Docker plugin
374+
docker.plugin(
375+
name="Install and enable a Docker plugin",
376+
plugin="username/my-awesome-plugin:latest",
377+
alias="my-plugin",
378+
present=True,
379+
enabled=True,
380+
plugin_options={"option1": "value1", "option2": "value2"},
381+
)
382+
"""
383+
plugin_name = alias if alias else plugin
384+
existent_plugin = host.get_fact(DockerPlugin, object_id=plugin_name)
385+
if existent_plugin:
386+
existent_plugin = existent_plugin[0]
387+
388+
if present:
389+
if existent_plugin:
390+
plugin_options_different = (
391+
plugin_options and existent_plugin["Settings"]["Env"] != plugin_options
392+
)
393+
if plugin_options_different:
394+
# Update options on existing plugin
395+
if existent_plugin["Enabled"]:
396+
yield handle_docker(
397+
resource="plugin",
398+
command="disable",
399+
plugin=plugin_name,
400+
)
401+
yield handle_docker(
402+
resource="plugin",
403+
command="set",
404+
plugin=plugin_name,
405+
enabled=enabled,
406+
existent_options=existent_plugin["Settings"]["Env"],
407+
required_options=plugin_options,
408+
)
409+
if enabled:
410+
yield handle_docker(
411+
resource="plugin",
412+
command="enable",
413+
plugin=plugin_name,
414+
)
415+
else:
416+
# Options are the same, check if enabled state is different
417+
if existent_plugin["Enabled"] == enabled:
418+
host.noop(
419+
f"Plugin '{plugin_name}' is already installed with the same options "
420+
f"and {'enabled' if enabled else 'disabled'}."
421+
)
422+
return
423+
else:
424+
command = "enable" if enabled else "disable"
425+
yield handle_docker(
426+
resource="plugin",
427+
command=command,
428+
plugin=plugin_name,
429+
)
430+
else:
431+
yield handle_docker(
432+
resource="plugin",
433+
command="install",
434+
plugin=plugin,
435+
alias=alias,
436+
enabled=enabled,
437+
plugin_options=plugin_options,
438+
)
439+
else:
440+
if not existent_plugin:
441+
host.noop(f"Plugin '{plugin_name}' is not installed.")
442+
return
443+
yield handle_docker(
444+
resource="plugin",
445+
command="remove",
446+
plugin=plugin_name,
447+
)

pyinfra/operations/util/docker.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,46 @@ def _remove_network(**kwargs):
165165
return "docker network rm {0}".format(kwargs["network"])
166166

167167

168-
def handle_docker(resource, command, **kwargs):
168+
def _install_plugin(**kwargs):
169+
command = ["docker plugin install {0} --grant-all-permissions".format(kwargs["plugin"])]
170+
171+
plugin_options = kwargs["plugin_options"] if kwargs["plugin_options"] else {}
172+
173+
if kwargs["alias"]:
174+
command.append("--alias {0}".format(kwargs["alias"]))
175+
176+
if not kwargs["enabled"]:
177+
command.append("--disable")
178+
179+
for option, value in plugin_options.items():
180+
command.append("{0}={1}".format(option, value))
181+
182+
return " ".join(command)
183+
184+
185+
def _remove_plugin(**kwargs):
186+
return "docker plugin rm -f {0}".format(kwargs["plugin"])
187+
188+
189+
def _enable_plugin(**kwargs):
190+
return "docker plugin enable {0}".format(kwargs["plugin"])
191+
192+
193+
def _disable_plugin(**kwargs):
194+
return "docker plugin disable {0}".format(kwargs["plugin"])
195+
196+
197+
def _set_plugin_options(**kwargs):
198+
command = ["docker plugin set {0}".format(kwargs["plugin"])]
199+
existent_options = kwargs.get("existing_options", {})
200+
required_options = kwargs.get("required_options", {})
201+
options_to_set = existent_options | required_options
202+
for option, value in options_to_set.items():
203+
command.append("{0}={1}".format(option, value))
204+
return " ".join(command)
205+
206+
207+
def handle_docker(resource: str, command: str, **kwargs):
169208
container_commands = {
170209
"create": _create_container,
171210
"remove": _remove_container,
@@ -192,12 +231,21 @@ def handle_docker(resource, command, **kwargs):
192231
"prune": _prune_command,
193232
}
194233

234+
plugin_commands = {
235+
"install": _install_plugin,
236+
"remove": _remove_plugin,
237+
"enable": _enable_plugin,
238+
"disable": _disable_plugin,
239+
"set": _set_plugin_options,
240+
}
241+
195242
docker_commands = {
196243
"container": container_commands,
197244
"image": image_commands,
198245
"volume": volume_commands,
199246
"network": network_commands,
200247
"system": system_commands,
248+
"plugin": plugin_commands,
201249
}
202250

203251
return docker_commands[resource][command](**kwargs)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"arg": "myid",
3+
"command": "docker plugin inspect myid 2>&- || true",
4+
"requires_command": "docker",
5+
"output": [
6+
"{\"hello\": \"world\"}"
7+
],
8+
"fact": {
9+
"hello": "world"
10+
}
11+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"command": "ids=$(docker plugin ls -q) && [ -n \"$ids\" ] && docker plugin inspect $ids || echo \"[]\"",
3+
"requires_command": "docker",
4+
"output": ["{\"hello\": \"world\"}"],
5+
"fact": {
6+
"hello": "world"
7+
}
8+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"kwargs": {
3+
"plugin": "my-plugin",
4+
"enabled": false
5+
},
6+
"facts": {
7+
"docker.DockerPlugin": {
8+
"object_id=my-plugin": [
9+
{
10+
"Name": "my-plugin:latest",
11+
"Enabled": true,
12+
"Id": "1234567890abcdef"
13+
}
14+
]
15+
}
16+
},
17+
"commands": [
18+
"docker plugin disable my-plugin"
19+
]
20+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"kwargs": {
3+
"plugin": "my-plugin"
4+
},
5+
"facts": {
6+
"docker.DockerPlugin": {
7+
"object_id=my-plugin": [
8+
{
9+
"Name": "my-plugin:latest",
10+
"Enabled": false,
11+
"Id": "1234567890abcdef"
12+
}
13+
]
14+
}
15+
},
16+
"commands": [
17+
"docker plugin enable my-plugin"
18+
]
19+
}

0 commit comments

Comments
 (0)