From e360626dbceea226f73f029935e7b4b8c3ba4c9f Mon Sep 17 00:00:00 2001 From: Scott Minor Date: Fri, 17 Jan 2025 10:49:29 -0500 Subject: [PATCH 1/2] `docker.container`: Refactor to support container recreation This change refactors the way the `docker.container` operation manages containers, in preparation for work to make recreation more intelligent. This change is (mostly) a pure refactor; future changes will diff the current container against the operation parameters to determine if a container needs to be recreated. This will fix an issue where changing any of the operation arguments does not result in actual container changes upon execution. In this refactor, container parameters are moved to a dedicated class, which centralizes the `docker container` command-line argument generation logic and the future diffing logic. The diff function is roughed in, though it currently reports "no diff" (to match the operation's current behavior). Since conditional recreation complicates the operation's logic on which commands to execute, the decisions are boosted into boolean variables to increase readability. As a side benefit, supporting additional docker container parameters should be more straightforward due to the centralization in said dedicated class (I'm planning on adding support for container args, uid, and other Docker params currently not supported). The only behavioral change is that creating and starting a container is no longer done in one exec (joined by `;`) but rather two separate docker commands. This sidesteps questions about whether `;` is the correct joiner (as opposed to `&&`) and reduces the amount of `kwargs` fishing in the implementation. Tested: * `scripts/dev-test.sh` and `scripts/dev-test-e2e.sh` both pass, save for warnings also present prior to this change --- pyinfra/operations/docker.py | 88 ++-- pyinfra/operations/util/docker.py | 66 ++- .../add_and_start_no_existent_container.json | 5 +- .../add_existent_container.json | 462 +++++++++--------- 4 files changed, 323 insertions(+), 298 deletions(-) diff --git a/pyinfra/operations/docker.py b/pyinfra/operations/docker.py index 915791b68..d36ef6698 100644 --- a/pyinfra/operations/docker.py +++ b/pyinfra/operations/docker.py @@ -8,7 +8,7 @@ from pyinfra.api import operation from pyinfra.facts.docker import DockerContainer, DockerNetwork, DockerVolume -from .util.docker import handle_docker +from .util.docker import ContainerSpec, handle_docker @operation() @@ -70,56 +70,60 @@ def container( ) """ + want_spec = ContainerSpec( + image, + ports or list(), + networks or list(), + volumes or list(), + env_vars or list(), + pull_always, + ) existent_container = host.get_fact(DockerContainer, object_id=container) - if force: - if existent_container: - yield handle_docker( - resource="container", - command="remove", - container=container, - ) + container_spec_changes = want_spec.diff_from_inspect(existent_container) - if present: - if not existent_container or force: - yield handle_docker( - resource="container", - command="create", - container=container, - image=image, - ports=ports, - networks=networks, - volumes=volumes, - env_vars=env_vars, - pull_always=pull_always, - present=present, - force=force, - start=start, - ) - - if existent_container and start: - if existent_container[0]["State"]["Status"] != "running": - yield handle_docker( - resource="container", - command="start", - container=container, - ) - - if existent_container and not start: - if existent_container[0]["State"]["Status"] == "running": - yield handle_docker( - resource="container", - command="stop", - container=container, - ) - - if existent_container and not present: + is_running = ( + (existent_container[0]["State"]["Status"] == "running") + if existent_container and existent_container[0] + else False + ) + recreating = existent_container and (force or container_spec_changes) + removing = existent_container and not present + + do_remove = recreating or removing + do_create = (present and not existent_container) or recreating + do_start = start and (recreating or not is_running) + do_stop = not start and not removing and is_running + + if do_remove: yield handle_docker( resource="container", command="remove", container=container, ) + if do_create: + yield handle_docker( + resource="container", + command="create", + container=container, + spec=want_spec, + ) + + if do_start: + yield handle_docker( + resource="container", + command="start", + container=container, + ) + + if do_stop: + yield handle_docker( + resource="container", + command="stop", + container=container, + ) + @operation(is_idempotent=False) def image(image, present=True): diff --git a/pyinfra/operations/util/docker.py b/pyinfra/operations/util/docker.py index fc8015e22..98ff714a1 100644 --- a/pyinfra/operations/util/docker.py +++ b/pyinfra/operations/util/docker.py @@ -1,38 +1,60 @@ +import dataclasses +from typing import Any, Dict, List + from pyinfra.api import OperationError -def _create_container(**kwargs): - command = [] +@dataclasses.dataclass +class ContainerSpec: + image: str = "" + ports: List[str] = dataclasses.field(default_factory=list) + networks: List[str] = dataclasses.field(default_factory=list) + volumes: List[str] = dataclasses.field(default_factory=list) + env_vars: List[str] = dataclasses.field(default_factory=list) + pull_always: bool = False + + def container_create_args(self): + args = [] + for network in self.networks: + args.append("--network {0}".format(network)) - networks = kwargs["networks"] if kwargs["networks"] else [] - ports = kwargs["ports"] if kwargs["ports"] else [] - volumes = kwargs["volumes"] if kwargs["volumes"] else [] - env_vars = kwargs["env_vars"] if kwargs["env_vars"] else [] + for port in self.ports: + args.append("-p {0}".format(port)) - if kwargs["image"] == "": - raise OperationError("missing 1 required argument: 'image'") + for volume in self.volumes: + args.append("-v {0}".format(volume)) - command.append("docker container create --name {0}".format(kwargs["container"])) + for env_var in self.env_vars: + args.append("-e {0}".format(env_var)) - for network in networks: - command.append("--network {0}".format(network)) + if self.pull_always: + args.append("--pull always") - for port in ports: - command.append("-p {0}".format(port)) + args.append(self.image) - for volume in volumes: - command.append("-v {0}".format(volume)) + return args - for env_var in env_vars: - command.append("-e {0}".format(env_var)) + def diff_from_inspect(self, inspect_dict: Dict[str, Any]) -> List[str]: + # TODO(@minor-fixes): Diff output of "docker inspect" against this spec + # to determine if the container needs to be recreated. Currently, this + # function will never recreate when attributes change, which is + # consistent with prior behavior. + del inspect_dict + return [] + + +def _create_container(**kwargs): + if "spec" not in kwargs: + raise OperationError("missing 1 required argument: 'spec'") - if kwargs["pull_always"]: - command.append("--pull always") + spec = kwargs["spec"] - command.append(kwargs["image"]) + if not spec.image: + raise OperationError("Docker image not specified") - if kwargs["start"]: - command.append("; {0}".format(_start_container(container=kwargs["container"]))) + command = [ + "docker container create --name {0}".format(kwargs["container"]) + ] + spec.container_create_args() return " ".join(command) diff --git a/tests/operations/docker.container/add_and_start_no_existent_container.json b/tests/operations/docker.container/add_and_start_no_existent_container.json index e16b515e2..fbd630257 100644 --- a/tests/operations/docker.container/add_and_start_no_existent_container.json +++ b/tests/operations/docker.container/add_and_start_no_existent_container.json @@ -10,10 +10,11 @@ }, "facts": { "docker.DockerContainer": { - "object_id=nginx": [] + "object_id=nginx": [] } }, "commands": [ - "docker container create --name nginx -p 80:80 nginx:alpine ; docker container start nginx" + "docker container create --name nginx -p 80:80 nginx:alpine", + "docker container start nginx" ] } \ No newline at end of file diff --git a/tests/operations/docker.container/add_existent_container.json b/tests/operations/docker.container/add_existent_container.json index 4473baebc..1be334df3 100644 --- a/tests/operations/docker.container/add_existent_container.json +++ b/tests/operations/docker.container/add_existent_container.json @@ -9,242 +9,240 @@ "force": true }, "facts": { - "docker.DockerContainer": - { - "object_id=nginx": [ - - { - "Id": "9bb5a79e7c4da9ad38e7183aa7c943ee41dcbbab70e4832fe29b2788ab88d4b4", - "Created": "2024-05-26T22: 01: 24.10525839Z", - "Path": "/docker-entrypoint.sh", - "Args": [ - "nginx", - "-g", - "daemon off;" - ], - "State": { - "Status": "running", - "Running": "True", - "Paused": "False", - "Restarting": "False", - "OOMKilled": "False", - "Dead": "False", - "Pid": 8407, - "ExitCode": 0, - "Error": "", - "StartedAt": "2024-05-26T22: 01: 24.502384646Z", - "FinishedAt": "0001-01-01T00: 00: 00Z" - }, - "Image": "sha256:e784f4560448b14a66f55c26e1b4dad2c2877cc73d001b7cd0b18e24a700a070", - "ResolvConfPath": "/var/lib/docker/containers/9bb5a79e7c4da9ad38e7183aa7c943ee41dcbbab70e4832fe29b2788ab88d4b4/resolv.conf", - "HostnamePath": "/var/lib/docker/containers/9bb5a79e7c4da9ad38e7183aa7c943ee41dcbbab70e4832fe29b2788ab88d4b4/hostname", - "HostsPath": "/var/lib/docker/containers/9bb5a79e7c4da9ad38e7183aa7c943ee41dcbbab70e4832fe29b2788ab88d4b4/hosts", - "LogPath": "/var/lib/docker/containers/9bb5a79e7c4da9ad38e7183aa7c943ee41dcbbab70e4832fe29b2788ab88d4b4/9bb5a79e7c4da9ad38e7183aa7c943ee41dcbbab70e4832fe29b2788ab88d4b4-json.log", - "Name": "/nginx", - "RestartCount": 0, - "Driver": "overlay2", - "Platform": "linux", - "MountLabel": "", - "ProcessLabel": "", - "AppArmorProfile": "", - "ExecIDs": "None", - "HostConfig": { - "Binds": "None", - "ContainerIDFile": "", - "LogConfig": { - "Type": "json-file", - "Config": {} - }, - "NetworkMode": "bridge", - "PortBindings": { - "80/tcp": [ - { - "HostIp": "", - "HostPort": "80" - } - ] - }, - "RestartPolicy": { - "Name": "no", - "MaximumRetryCount": 0 - }, - "AutoRemove": "False", - "VolumeDriver": "", - "VolumesFrom": "None", - "ConsoleSize": [ - 0, - 0 - ], - "CapAdd": "None", - "CapDrop": "None", - "CgroupnsMode": "private", - "Dns": [], - "DnsOptions": [], - "DnsSearch": [], - "ExtraHosts": "None", - "GroupAdd": "None", - "IpcMode": "private", - "Cgroup": "", - "Links": "None", - "OomScoreAdj": 0, - "PidMode": "", - "Privileged": "False", - "PublishAllPorts": "False", - "ReadonlyRootfs": "False", - "SecurityOpt": "None", - "UTSMode": "", - "UsernsMode": "", - "ShmSize": 67108864, - "Runtime": "runc", - "Isolation": "", - "CpuShares": 0, - "Memory": 0, - "NanoCpus": 0, - "CgroupParent": "", - "BlkioWeight": 0, - "BlkioWeightDevice": [], - "BlkioDeviceReadBps": [], - "BlkioDeviceWriteBps": [], - "BlkioDeviceReadIOps": [], - "BlkioDeviceWriteIOps": [], - "CpuPeriod": 0, - "CpuQuota": 0, - "CpuRealtimePeriod": 0, - "CpuRealtimeRuntime": 0, - "CpusetCpus": "", - "CpusetMems": "", - "Devices": [], - "DeviceCgroupRules": "None", - "DeviceRequests": "None", - "MemoryReservation": 0, - "MemorySwap": 0, - "MemorySwappiness": "None", - "OomKillDisable": "None", - "PidsLimit": "None", - "Ulimits": [], - "CpuCount": 0, - "CpuPercent": 0, - "IOMaximumIOps": 0, - "IOMaximumBandwidth": 0, - "MaskedPaths": [ - "/proc/asound", - "/proc/acpi", - "/proc/kcore", - "/proc/keys", - "/proc/latency_stats", - "/proc/timer_list", - "/proc/timer_stats", - "/proc/sched_debug", - "/proc/scsi", - "/sys/firmware", - "/sys/devices/virtual/powercap" - ], - "ReadonlyPaths": [ - "/proc/bus", - "/proc/fs", - "/proc/irq", - "/proc/sys", - "/proc/sysrq-trigger" - ] - }, - "GraphDriver": { - "Data": { - "LowerDir": "/var/lib/docker/overlay2/ee7c8aaf7d9d84d989ea529b2cb1b4aea63c378ef0527ce8562f6d4d7530e1ca-init/diff:/var/lib/docker/overlay2/21b7dde68322832abacbc35a1fdfe6a75bacb0ef4b55d5341b29c0be312d457e/diff:/var/lib/docker/overlay2/7b978a86e25dd74d51c81795b8af92c4ae3c0cd8d5d34602e70dfe3be604aebe/diff:/var/lib/docker/overlay2/483058275ebfbe7dbc3e5bb15f2d7c45ec3f61a935b2a098d82f16cf8ba5231b/diff:/var/lib/docker/overlay2/87438f28df76ef0175c7ccd134f23f70e83fd85870f693735d6757bf6cb88f98/diff:/var/lib/docker/overlay2/ac61c96de3b3fd3644979b81c98d1071e6a063bfd2dcfb9c73cae30e064efdb8/diff:/var/lib/docker/overlay2/d744f3a16ab1a2cfd7889959279e6cc693199e5c1099d48f3c0794f62b045cc7/diff:/var/lib/docker/overlay2/572ed1139aed78c5873bc6ca8f8172b10b9876062f9a385c653e17244d88e421/diff", - "MergedDir": "/var/lib/docker/overlay2/ee7c8aaf7d9d84d989ea529b2cb1b4aea63c378ef0527ce8562f6d4d7530e1ca/merged", - "UpperDir": "/var/lib/docker/overlay2/ee7c8aaf7d9d84d989ea529b2cb1b4aea63c378ef0527ce8562f6d4d7530e1ca/diff", - "WorkDir": "/var/lib/docker/overlay2/ee7c8aaf7d9d84d989ea529b2cb1b4aea63c378ef0527ce8562f6d4d7530e1ca/work" - }, - "Name": "overlay2" - }, - "Mounts": [], - "Config": { - "Hostname": "9bb5a79e7c4d", - "Domainname": "", - "User": "", - "AttachStdin": "False", - "AttachStdout": "True", - "AttachStderr": "True", - "ExposedPorts": { - "80/tcp": {} - }, - "Tty": "False", - "OpenStdin": "False", - "StdinOnce": "False", - "Env": [ - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", - "NGINX_VERSION=1.25.5", - "NJS_VERSION=0.8.4", - "NJS_RELEASE=3~bookworm", - "PKG_RELEASE=1~bookworm" - ], - "Cmd": [ - "nginx", - "-g", - "daemon off;" - ], - "Image": "nginx", - "Volumes": "None", - "WorkingDir": "", - "Entrypoint": [ - "/docker-entrypoint.sh" - ], - "OnBuild": "None", - "Labels": { - "maintainer": "NGINX Docker Maintainers " - }, - "StopSignal": "SIGQUIT" - }, - "NetworkSettings": { - "Bridge": "", - "SandboxID": "30102172778c8c2268fa443d0c29ac898beaa07c6ca3d73a93751a21e0812f14", - "SandboxKey": "/var/run/docker/netns/30102172778c", - "Ports": { - "80/tcp": [ - { - "HostIp": "0.0.0.0", - "HostPort": "80" - } - ] - }, - "HairpinMode": "False", - "LinkLocalIPv6Address": "", - "LinkLocalIPv6PrefixLen": 0, - "SecondaryIPAddresses": "None", - "SecondaryIPv6Addresses": "None", - "EndpointID": "2e0e98fd4346d9ad61d78294bd97e7c27f512a88c402115163a0df73bf83cad4", - "Gateway": "172.17.0.1", - "GlobalIPv6Address": "", - "GlobalIPv6PrefixLen": 0, - "IPAddress": "172.17.0.2", - "IPPrefixLen": 16, - "IPv6Gateway": "", - "MacAddress": "02: 42:ac: 11: 00: 02", - "Networks": { - "bridge": { - "IPAMConfig": "None", - "Links": "None", - "Aliases": "None", - "MacAddress": "02: 42:ac: 11: 00: 02", - "NetworkID": "9602c89bf5d2a3b55665598ec99ec803cd54e0df0c2a25a511d59ecc2dc047b5", - "EndpointID": "2e0e98fd4346d9ad61d78294bd97e7c27f512a88c402115163a0df73bf83cad4", - "Gateway": "172.17.0.1", - "IPAddress": "172.17.0.2", - "IPPrefixLen": 16, - "IPv6Gateway": "", - "GlobalIPv6Address": "", - "GlobalIPv6PrefixLen": 0, - "DriverOpts": "None", - "DNSNames": "None" - } - } + "docker.DockerContainer": { + "object_id=nginx": [ + { + "Id": "9bb5a79e7c4da9ad38e7183aa7c943ee41dcbbab70e4832fe29b2788ab88d4b4", + "Created": "2024-05-26T22: 01: 24.10525839Z", + "Path": "/docker-entrypoint.sh", + "Args": [ + "nginx", + "-g", + "daemon off;" + ], + "State": { + "Status": "running", + "Running": "True", + "Paused": "False", + "Restarting": "False", + "OOMKilled": "False", + "Dead": "False", + "Pid": 8407, + "ExitCode": 0, + "Error": "", + "StartedAt": "2024-05-26T22: 01: 24.502384646Z", + "FinishedAt": "0001-01-01T00: 00: 00Z" + }, + "Image": "sha256:e784f4560448b14a66f55c26e1b4dad2c2877cc73d001b7cd0b18e24a700a070", + "ResolvConfPath": "/var/lib/docker/containers/9bb5a79e7c4da9ad38e7183aa7c943ee41dcbbab70e4832fe29b2788ab88d4b4/resolv.conf", + "HostnamePath": "/var/lib/docker/containers/9bb5a79e7c4da9ad38e7183aa7c943ee41dcbbab70e4832fe29b2788ab88d4b4/hostname", + "HostsPath": "/var/lib/docker/containers/9bb5a79e7c4da9ad38e7183aa7c943ee41dcbbab70e4832fe29b2788ab88d4b4/hosts", + "LogPath": "/var/lib/docker/containers/9bb5a79e7c4da9ad38e7183aa7c943ee41dcbbab70e4832fe29b2788ab88d4b4/9bb5a79e7c4da9ad38e7183aa7c943ee41dcbbab70e4832fe29b2788ab88d4b4-json.log", + "Name": "/nginx", + "RestartCount": 0, + "Driver": "overlay2", + "Platform": "linux", + "MountLabel": "", + "ProcessLabel": "", + "AppArmorProfile": "", + "ExecIDs": "None", + "HostConfig": { + "Binds": "None", + "ContainerIDFile": "", + "LogConfig": { + "Type": "json-file", + "Config": {} + }, + "NetworkMode": "bridge", + "PortBindings": { + "80/tcp": [ + { + "HostIp": "", + "HostPort": "80" } - + ] + }, + "RestartPolicy": { + "Name": "no", + "MaximumRetryCount": 0 + }, + "AutoRemove": "False", + "VolumeDriver": "", + "VolumesFrom": "None", + "ConsoleSize": [ + 0, + 0 + ], + "CapAdd": "None", + "CapDrop": "None", + "CgroupnsMode": "private", + "Dns": [], + "DnsOptions": [], + "DnsSearch": [], + "ExtraHosts": "None", + "GroupAdd": "None", + "IpcMode": "private", + "Cgroup": "", + "Links": "None", + "OomScoreAdj": 0, + "PidMode": "", + "Privileged": "False", + "PublishAllPorts": "False", + "ReadonlyRootfs": "False", + "SecurityOpt": "None", + "UTSMode": "", + "UsernsMode": "", + "ShmSize": 67108864, + "Runtime": "runc", + "Isolation": "", + "CpuShares": 0, + "Memory": 0, + "NanoCpus": 0, + "CgroupParent": "", + "BlkioWeight": 0, + "BlkioWeightDevice": [], + "BlkioDeviceReadBps": [], + "BlkioDeviceWriteBps": [], + "BlkioDeviceReadIOps": [], + "BlkioDeviceWriteIOps": [], + "CpuPeriod": 0, + "CpuQuota": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "CpusetCpus": "", + "CpusetMems": "", + "Devices": [], + "DeviceCgroupRules": "None", + "DeviceRequests": "None", + "MemoryReservation": 0, + "MemorySwap": 0, + "MemorySwappiness": "None", + "OomKillDisable": "None", + "PidsLimit": "None", + "Ulimits": [], + "CpuCount": 0, + "CpuPercent": 0, + "IOMaximumIOps": 0, + "IOMaximumBandwidth": 0, + "MaskedPaths": [ + "/proc/asound", + "/proc/acpi", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + "/sys/firmware", + "/sys/devices/virtual/powercap" + ], + "ReadonlyPaths": [ + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ] + }, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/ee7c8aaf7d9d84d989ea529b2cb1b4aea63c378ef0527ce8562f6d4d7530e1ca-init/diff:/var/lib/docker/overlay2/21b7dde68322832abacbc35a1fdfe6a75bacb0ef4b55d5341b29c0be312d457e/diff:/var/lib/docker/overlay2/7b978a86e25dd74d51c81795b8af92c4ae3c0cd8d5d34602e70dfe3be604aebe/diff:/var/lib/docker/overlay2/483058275ebfbe7dbc3e5bb15f2d7c45ec3f61a935b2a098d82f16cf8ba5231b/diff:/var/lib/docker/overlay2/87438f28df76ef0175c7ccd134f23f70e83fd85870f693735d6757bf6cb88f98/diff:/var/lib/docker/overlay2/ac61c96de3b3fd3644979b81c98d1071e6a063bfd2dcfb9c73cae30e064efdb8/diff:/var/lib/docker/overlay2/d744f3a16ab1a2cfd7889959279e6cc693199e5c1099d48f3c0794f62b045cc7/diff:/var/lib/docker/overlay2/572ed1139aed78c5873bc6ca8f8172b10b9876062f9a385c653e17244d88e421/diff", + "MergedDir": "/var/lib/docker/overlay2/ee7c8aaf7d9d84d989ea529b2cb1b4aea63c378ef0527ce8562f6d4d7530e1ca/merged", + "UpperDir": "/var/lib/docker/overlay2/ee7c8aaf7d9d84d989ea529b2cb1b4aea63c378ef0527ce8562f6d4d7530e1ca/diff", + "WorkDir": "/var/lib/docker/overlay2/ee7c8aaf7d9d84d989ea529b2cb1b4aea63c378ef0527ce8562f6d4d7530e1ca/work" + }, + "Name": "overlay2" + }, + "Mounts": [], + "Config": { + "Hostname": "9bb5a79e7c4d", + "Domainname": "", + "User": "", + "AttachStdin": "False", + "AttachStdout": "True", + "AttachStderr": "True", + "ExposedPorts": { + "80/tcp": {} + }, + "Tty": "False", + "OpenStdin": "False", + "StdinOnce": "False", + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "NGINX_VERSION=1.25.5", + "NJS_VERSION=0.8.4", + "NJS_RELEASE=3~bookworm", + "PKG_RELEASE=1~bookworm" + ], + "Cmd": [ + "nginx", + "-g", + "daemon off;" + ], + "Image": "nginx", + "Volumes": "None", + "WorkingDir": "", + "Entrypoint": [ + "/docker-entrypoint.sh" + ], + "OnBuild": "None", + "Labels": { + "maintainer": "NGINX Docker Maintainers " + }, + "StopSignal": "SIGQUIT" + }, + "NetworkSettings": { + "Bridge": "", + "SandboxID": "30102172778c8c2268fa443d0c29ac898beaa07c6ca3d73a93751a21e0812f14", + "SandboxKey": "/var/run/docker/netns/30102172778c", + "Ports": { + "80/tcp": [ + { + "HostIp": "0.0.0.0", + "HostPort": "80" + } + ] + }, + "HairpinMode": "False", + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "SecondaryIPAddresses": "None", + "SecondaryIPv6Addresses": "None", + "EndpointID": "2e0e98fd4346d9ad61d78294bd97e7c27f512a88c402115163a0df73bf83cad4", + "Gateway": "172.17.0.1", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "MacAddress": "02: 42:ac: 11: 00: 02", + "Networks": { + "bridge": { + "IPAMConfig": "None", + "Links": "None", + "Aliases": "None", + "MacAddress": "02: 42:ac: 11: 00: 02", + "NetworkID": "9602c89bf5d2a3b55665598ec99ec803cd54e0df0c2a25a511d59ecc2dc047b5", + "EndpointID": "2e0e98fd4346d9ad61d78294bd97e7c27f512a88c402115163a0df73bf83cad4", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "DriverOpts": "None", + "DNSNames": "None" + } } - ] - } + } + } + ] + } }, "commands": [ "docker container rm -f nginx", - "docker container create --name nginx -p 80:80 nginx:alpine ; docker container start nginx" + "docker container create --name nginx -p 80:80 nginx:alpine", + "docker container start nginx" ] } \ No newline at end of file From 3988181ea4207a00e4150060451fcf9e6920e038 Mon Sep 17 00:00:00 2001 From: Scott Minor Date: Fri, 17 Jan 2025 11:51:40 -0500 Subject: [PATCH 2/2] `docker.container`: Support container args This change adds an `args` parameter to the `docker.container` operation that passes said supplied args to the container at creation time. This allows the operation to support container images that have an entrypoint expecting to receive additional arguments, without needing to build+push a custom image that embeds said arguments. --- pyinfra/operations/docker.py | 3 +++ pyinfra/operations/util/docker.py | 2 ++ .../add_and_start_no_existent_container.json | 7 ++++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pyinfra/operations/docker.py b/pyinfra/operations/docker.py index d36ef6698..db781aabc 100644 --- a/pyinfra/operations/docker.py +++ b/pyinfra/operations/docker.py @@ -15,6 +15,7 @@ def container( container, image="", + args=None, ports=None, networks=None, volumes=None, @@ -28,6 +29,7 @@ def container( Manage Docker containers + container: name to identify the container + + args: list of command-line args to supply to the image + image: container image and tag ex: nginx:alpine + networks: network list to attach on container + ports: port list to expose @@ -72,6 +74,7 @@ def container( want_spec = ContainerSpec( image, + args or list(), ports or list(), networks or list(), volumes or list(), diff --git a/pyinfra/operations/util/docker.py b/pyinfra/operations/util/docker.py index 98ff714a1..3c18aad2e 100644 --- a/pyinfra/operations/util/docker.py +++ b/pyinfra/operations/util/docker.py @@ -7,6 +7,7 @@ @dataclasses.dataclass class ContainerSpec: image: str = "" + args: List[str] = dataclasses.field(default_factory=list) ports: List[str] = dataclasses.field(default_factory=list) networks: List[str] = dataclasses.field(default_factory=list) volumes: List[str] = dataclasses.field(default_factory=list) @@ -31,6 +32,7 @@ def container_create_args(self): args.append("--pull always") args.append(self.image) + args.extend(self.args) return args diff --git a/tests/operations/docker.container/add_and_start_no_existent_container.json b/tests/operations/docker.container/add_and_start_no_existent_container.json index fbd630257..a2c8d7008 100644 --- a/tests/operations/docker.container/add_and_start_no_existent_container.json +++ b/tests/operations/docker.container/add_and_start_no_existent_container.json @@ -2,6 +2,11 @@ "kwargs": { "container": "nginx", "image": "nginx:alpine", + "args": [ + "nginx-debug", + "-g", + "'daemon off;'" + ], "ports": [ "80:80" ], @@ -14,7 +19,7 @@ } }, "commands": [ - "docker container create --name nginx -p 80:80 nginx:alpine", + "docker container create --name nginx -p 80:80 nginx:alpine nginx-debug -g 'daemon off;'", "docker container start nginx" ] } \ No newline at end of file