From e360626dbceea226f73f029935e7b4b8c3ba4c9f Mon Sep 17 00:00:00 2001 From: Scott Minor Date: Fri, 17 Jan 2025 10:49:29 -0500 Subject: [PATCH 1/3] `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/3] `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 From 8ef9a234af83a2a8c42e0d96ae8660aafe772126 Mon Sep 17 00:00:00 2001 From: Scott Minor Date: Mon, 20 Jan 2025 11:33:57 -0500 Subject: [PATCH 3/3] `docker.container`: Recreate container when args change This PR allows the `docker.container` operation to tear down and recreate the container when operation arguments change, instead of reporting `No change` and doing nothing. This is intended to reduce the possibility for human error/need for manual intervention when changing args to `docker.container` operations. Since it is not possible to extract all operation args from e.g. `docker inspect` output, this PR takes a similar approach to Docker Compose to tackle this issue - it serializes the operation args in a deterministic way, hashes the serialized bytes, and stores this as a label on the container. If the hash differs from a currently-running container, the container is recreated. Tested: Added additional tests for behavior when args are changing/static in different scenarios --- pyinfra/operations/docker.py | 38 +- pyinfra/operations/util/docker.py | 51 +- .../add_and_start_no_existent_container.json | 17 +- .../add_existent_container_changed_ports.json | 249 ++++++++++ .../add_existent_container_force.json | 249 ++++++++++ ...=> add_existent_container_no_changes.json} | 12 +- .../add_no_existent_container.json | 4 +- .../docker.container/remove_container.json | 460 +++++++++--------- .../docker.container/start_container.json | 6 +- ...er.json => stop_container_no_changes.json} | 7 +- .../stop_container_with_changes.json | 266 ++++++++++ 11 files changed, 1081 insertions(+), 278 deletions(-) create mode 100644 tests/operations/docker.container/add_existent_container_changed_ports.json create mode 100644 tests/operations/docker.container/add_existent_container_force.json rename tests/operations/docker.container/{add_existent_container.json => add_existent_container_no_changes.json} (97%) rename tests/operations/docker.container/{stop_container.json => stop_container_no_changes.json} (98%) create mode 100644 tests/operations/docker.container/stop_container_with_changes.json diff --git a/pyinfra/operations/docker.py b/pyinfra/operations/docker.py index db781aabc..d968ac563 100644 --- a/pyinfra/operations/docker.py +++ b/pyinfra/operations/docker.py @@ -4,11 +4,13 @@ as inventory directly. """ +from typing import Any, Dict + from pyinfra import host from pyinfra.api import operation from pyinfra.facts.docker import DockerContainer, DockerNetwork, DockerVolume -from .util.docker import ContainerSpec, handle_docker +from .util.docker import CONTAINER_CONFIG_HASH_LABEL, ContainerSpec, handle_docker @operation() @@ -75,28 +77,36 @@ def container( want_spec = ContainerSpec( image, args or list(), - ports or list(), - networks or list(), + set(ports) if ports else set(), + set(networks) if networks else set(), volumes or list(), - env_vars or list(), + set(env_vars) if env_vars else set(), pull_always, ) - existent_container = host.get_fact(DockerContainer, object_id=container) - container_spec_changes = want_spec.diff_from_inspect(existent_container) + existent_container: Dict[str, Any] = next( + iter(host.get_fact(DockerContainer, object_id=container)), {} + ) - is_running = ( - (existent_container[0]["State"]["Status"] == "running") - if existent_container and existent_container[0] - else False + old_hash = ( + existent_container.get("Config", {}) + .get("Labels", {}) + .get(CONTAINER_CONFIG_HASH_LABEL, None) ) - recreating = existent_container and (force or container_spec_changes) + + container_spec_changed = old_hash != want_spec.config_hash() + + is_running = existent_container.get("State", {}).get("Status", "") == "running" + recreating = existent_container and (force or container_spec_changed) 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 + do_create = not removing and ((present and not existent_container) or recreating) + do_start = present and start and (recreating or not is_running) + do_stop = not start and not removing and is_running and not recreating + + if not (do_remove or do_create or do_start or do_stop): + host.noop("container configuration is already correct") if do_remove: yield handle_docker( diff --git a/pyinfra/operations/util/docker.py b/pyinfra/operations/util/docker.py index 3c18aad2e..eda003316 100644 --- a/pyinfra/operations/util/docker.py +++ b/pyinfra/operations/util/docker.py @@ -1,31 +1,49 @@ import dataclasses -from typing import Any, Dict, List +import hashlib +import json +from typing import Any, List, Set from pyinfra.api import OperationError +CONTAINER_CONFIG_HASH_LABEL = "com.github.pyinfra.config-hash" + + +def _json_repr(obj: Any): + try: + return dataclasses.asdict(obj) + except TypeError: + pass + + if isinstance(obj, set): + return sorted(obj) + + # If there are other alternative types to try (e.g. dates) then do so here + + raise TypeError(f"object {type(obj).__name__} not serializable") + @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) + ports: Set[str] = dataclasses.field(default_factory=set) + networks: Set[str] = dataclasses.field(default_factory=set) volumes: List[str] = dataclasses.field(default_factory=list) - env_vars: List[str] = dataclasses.field(default_factory=list) + env_vars: Set[str] = dataclasses.field(default_factory=set) pull_always: bool = False def container_create_args(self): - args = [] - for network in self.networks: + args = [f"--label '{CONTAINER_CONFIG_HASH_LABEL}={self.config_hash()}'"] + for network in sorted(self.networks): args.append("--network {0}".format(network)) - for port in self.ports: + for port in sorted(self.ports): args.append("-p {0}".format(port)) for volume in self.volumes: args.append("-v {0}".format(volume)) - for env_var in self.env_vars: + for env_var in sorted(self.env_vars): args.append("-e {0}".format(env_var)) if self.pull_always: @@ -36,13 +54,16 @@ def container_create_args(self): return args - 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 config_hash(self) -> str: + serialized = json.dumps( + self, + default=_json_repr, + ensure_ascii=False, + sort_keys=True, + indent=None, + separators=(",", ":"), + ).encode("utf-8") + return hashlib.sha256(serialized).hexdigest() def _create_container(**kwargs): 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 a2c8d7008..307ba16f9 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 @@ -7,8 +7,21 @@ "-g", "'daemon off;'" ], + "networks": [ + "foo", + "bar" + ], + "volumes": [ + "/host/a:/container/a", + "/host/b:/container/b" + ], "ports": [ - "80:80" + "80:80", + "8081:8081" + ], + "env_vars": [ + "ENV_A=foo", + "ENV_B=bar" ], "present": true, "start": true @@ -19,7 +32,7 @@ } }, "commands": [ - "docker container create --name nginx -p 80:80 nginx:alpine nginx-debug -g 'daemon off;'", + "docker container create --name nginx --label 'com.github.pyinfra.config-hash=72d37ab8f5ea3db48272d045bc10e211bbd628f2b4bab5c54d948b073313c9a3' --network bar --network foo -p 8081:8081 -p 80:80 -v /host/a:/container/a -v /host/b:/container/b -e ENV_A=foo -e ENV_B=bar nginx:alpine nginx-debug -g 'daemon off;'", "docker container start nginx" ] } \ No newline at end of file diff --git a/tests/operations/docker.container/add_existent_container_changed_ports.json b/tests/operations/docker.container/add_existent_container_changed_ports.json new file mode 100644 index 000000000..4c90673a9 --- /dev/null +++ b/tests/operations/docker.container/add_existent_container_changed_ports.json @@ -0,0 +1,249 @@ +{ + "kwargs": { + "container": "nginx", + "image": "nginx:alpine", + "ports": [ + "80:8080" + ], + "present": true, + "force": false + }, + "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 ", + "com.github.pyinfra.config-hash": "9be1c70576c6c4762c54bfce5fbd903638f9a68807453ce0549ad2e48b9a31f9" + }, + "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 --label 'com.github.pyinfra.config-hash=5351e6b8b50baa96591b7df3e87758fbbea39c56e29722ab17c4988a23e2803d' -p 80:8080 nginx:alpine", + "docker container start nginx" + ] +} \ No newline at end of file diff --git a/tests/operations/docker.container/add_existent_container_force.json b/tests/operations/docker.container/add_existent_container_force.json new file mode 100644 index 000000000..17684545e --- /dev/null +++ b/tests/operations/docker.container/add_existent_container_force.json @@ -0,0 +1,249 @@ +{ + "kwargs": { + "container": "nginx", + "image": "nginx:alpine", + "ports": [ + "80:80" + ], + "present": true, + "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 ", + "com.github.pyinfra.config-hash": "927b8c9ed243674a1bc23d0d88d867973cb71f0363e3029187ccaab7c7e79f19" + }, + "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 --label 'com.github.pyinfra.config-hash=927b8c9ed243674a1bc23d0d88d867973cb71f0363e3029187ccaab7c7e79f19' -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_no_changes.json similarity index 97% rename from tests/operations/docker.container/add_existent_container.json rename to tests/operations/docker.container/add_existent_container_no_changes.json index 1be334df3..32664a46b 100644 --- a/tests/operations/docker.container/add_existent_container.json +++ b/tests/operations/docker.container/add_existent_container_no_changes.json @@ -6,7 +6,7 @@ "80:80" ], "present": true, - "force": true + "force": false }, "facts": { "docker.DockerContainer": { @@ -188,7 +188,8 @@ ], "OnBuild": "None", "Labels": { - "maintainer": "NGINX Docker Maintainers " + "maintainer": "NGINX Docker Maintainers ", + "com.github.pyinfra.config-hash": "927b8c9ed243674a1bc23d0d88d867973cb71f0363e3029187ccaab7c7e79f19" }, "StopSignal": "SIGQUIT" }, @@ -240,9 +241,6 @@ ] } }, - "commands": [ - "docker container rm -f nginx", - "docker container create --name nginx -p 80:80 nginx:alpine", - "docker container start nginx" - ] + "commands": [], + "noop_description": "container configuration is already correct" } \ No newline at end of file diff --git a/tests/operations/docker.container/add_no_existent_container.json b/tests/operations/docker.container/add_no_existent_container.json index fdaa8ffba..8f87bb4d1 100644 --- a/tests/operations/docker.container/add_no_existent_container.json +++ b/tests/operations/docker.container/add_no_existent_container.json @@ -10,10 +10,10 @@ }, "facts": { "docker.DockerContainer": { - "object_id=nginx": [] + "object_id=nginx": [] } }, "commands": [ - "docker container create --name nginx -p 80:80 nginx:alpine" + "docker container create --name nginx --label 'com.github.pyinfra.config-hash=927b8c9ed243674a1bc23d0d88d867973cb71f0363e3029187ccaab7c7e79f19' -p 80:80 nginx:alpine" ] } \ No newline at end of file diff --git a/tests/operations/docker.container/remove_container.json b/tests/operations/docker.container/remove_container.json index 6f753fe83..0a8894ce2 100644 --- a/tests/operations/docker.container/remove_container.json +++ b/tests/operations/docker.container/remove_container.json @@ -8,239 +8,237 @@ "present": false }, "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 ", + "com.github.pyinfra.config-hash": "9be1c70576c6c4762c54bfce5fbd903638f9a68807453ce0549ad2e48b9a31f9" + }, + "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" diff --git a/tests/operations/docker.container/start_container.json b/tests/operations/docker.container/start_container.json index f16a01634..4b28087cc 100644 --- a/tests/operations/docker.container/start_container.json +++ b/tests/operations/docker.container/start_container.json @@ -9,8 +9,7 @@ }, "facts": { "docker.DockerContainer": { - - "object_id=nginx" :[ + "object_id=nginx": [ { "Id": "9bb5a79e7c4da9ad38e7183aa7c943ee41dcbbab70e4832fe29b2788ab88d4b4", "Created": "2024-05-26T22: 01: 24.10525839Z", @@ -188,7 +187,8 @@ ], "OnBuild": "None", "Labels": { - "maintainer": "NGINX Docker Maintainers " + "maintainer": "NGINX Docker Maintainers ", + "com.github.pyinfra.config-hash": "927b8c9ed243674a1bc23d0d88d867973cb71f0363e3029187ccaab7c7e79f19" }, "StopSignal": "SIGQUIT" }, diff --git a/tests/operations/docker.container/stop_container.json b/tests/operations/docker.container/stop_container_no_changes.json similarity index 98% rename from tests/operations/docker.container/stop_container.json rename to tests/operations/docker.container/stop_container_no_changes.json index af9c57b87..273679f5f 100644 --- a/tests/operations/docker.container/stop_container.json +++ b/tests/operations/docker.container/stop_container_no_changes.json @@ -9,8 +9,7 @@ "start": false }, "facts": { - "docker.DockerContainer": - { + "docker.DockerContainer": { "object_id=nginx": [ { "Id": "9bb5a79e7c4da9ad38e7183aa7c943ee41dcbbab70e4832fe29b2788ab88d4b4", @@ -189,7 +188,8 @@ ], "OnBuild": "None", "Labels": { - "maintainer": "NGINX Docker Maintainers " + "maintainer": "NGINX Docker Maintainers ", + "com.github.pyinfra.config-hash": "927b8c9ed243674a1bc23d0d88d867973cb71f0363e3029187ccaab7c7e79f19" }, "StopSignal": "SIGQUIT" }, @@ -240,7 +240,6 @@ } ] } - }, "commands": [ "docker container stop nginx" diff --git a/tests/operations/docker.container/stop_container_with_changes.json b/tests/operations/docker.container/stop_container_with_changes.json new file mode 100644 index 000000000..9c674b409 --- /dev/null +++ b/tests/operations/docker.container/stop_container_with_changes.json @@ -0,0 +1,266 @@ +{ + "kwargs": { + "container": "nginx", + "image": "nginx:alpine", + "args": [ + "nginx-debug", + "-g", + "'daemon off;'" + ], + "networks": [ + "foo", + "bar" + ], + "volumes": [ + "/host/a:/container/a", + "/host/b:/container/b" + ], + "ports": [ + "80:80", + "8081:8081" + ], + "env_vars": [ + "ENV_A=foo", + "ENV_B=bar" + ], + "present": true, + "start": false + }, + "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 ", + "com.github.pyinfra.config-hash": "927b8c9ed243674a1bc23d0d88d867973cb71f0363e3029187ccaab7c7e79f19" + }, + "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 --label 'com.github.pyinfra.config-hash=72d37ab8f5ea3db48272d045bc10e211bbd628f2b4bab5c54d948b073313c9a3' --network bar --network foo -p 8081:8081 -p 80:80 -v /host/a:/container/a -v /host/b:/container/b -e ENV_A=foo -e ENV_B=bar nginx:alpine nginx-debug -g 'daemon off;'" + ] +} \ No newline at end of file