Skip to content

Commit b3adbb5

Browse files
authored
feat(operations.docker): add compose operation (#1693)
1 parent 0db589d commit b3adbb5

9 files changed

Lines changed: 190 additions & 0 deletions

src/pyinfra/operations/docker.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,3 +668,120 @@ def logout(server: str | None = None):
668668
command_bits.append(QuoteString(server))
669669

670670
yield StringCommand(*command_bits)
671+
672+
673+
@operation(is_idempotent=False)
674+
def compose(
675+
project_directory: str,
676+
files: str | list[str] | None = None,
677+
project_name: str | None = None,
678+
present: bool = True,
679+
pull: str | None = None,
680+
build: bool = False,
681+
force_recreate: bool = False,
682+
remove_orphans: bool = True,
683+
remove_volumes: bool = False,
684+
compose_command: str = "docker compose",
685+
):
686+
"""
687+
Deploy a Docker Compose stack on the target.
688+
689+
+ project_directory: project directory on the target (maps to
690+
``--project-directory``). Compose discovers ``compose.yaml`` /
691+
``compose.yml`` / ``docker-compose.yaml`` / ``docker-compose.yml`` inside
692+
this directory by default.
693+
+ files: optional path or list of paths to specific compose file(s) on the
694+
target (maps to ``-f``); use this to override the default discovery or to
695+
layer overrides.
696+
+ project_name: compose project name (maps to ``--project-name``; defaults
697+
to compose's own default — typically the basename of ``project_directory``)
698+
+ present: ``True`` runs ``up -d``, ``False`` runs ``down``
699+
+ pull: policy for ``up -d --pull`` (``None``, ``"always"``, ``"missing"``, ``"never"``)
700+
+ build: pass ``--build`` on ``up``
701+
+ force_recreate: pass ``--force-recreate`` on ``up``
702+
+ remove_orphans: pass ``--remove-orphans`` on ``up`` / ``down``
703+
+ remove_volumes: pass ``-v`` on ``down`` (only honored when ``present=False``)
704+
+ compose_command: compose binary to invoke; use ``"docker-compose"`` for v1
705+
706+
This operation is not idempotent from pyinfra's perspective: it always shells out
707+
to compose. Docker itself skips services whose definition has not changed, so
708+
re-runs are safe and cheap.
709+
710+
``_env`` and ``_chdir`` are the standard pyinfra global operation kwargs and
711+
work here without any special handling, which is useful for compose variable
712+
interpolation.
713+
714+
**Examples:**
715+
716+
.. code:: python
717+
718+
from pyinfra.operations import docker, files
719+
720+
# Upload the compose file then bring the stack up using compose's
721+
# default discovery (looks for compose.yaml / docker-compose.yml in
722+
# the project directory)
723+
files.put(
724+
name="Upload compose file",
725+
src="files/docker-compose.yml",
726+
dest="/srv/app/docker-compose.yml",
727+
)
728+
docker.compose(
729+
name="Deploy app stack",
730+
project_directory="/srv/app",
731+
project_name="app",
732+
_env={"DIR_STORAGE": "/srv/app/data"},
733+
)
734+
735+
# Layer a base compose file with an override
736+
docker.compose(
737+
name="Deploy app stack with override",
738+
project_directory="/srv/app",
739+
files=["docker-compose.yml", "docker-compose.prod.yml"],
740+
)
741+
742+
# Tear the stack down, including named volumes
743+
docker.compose(
744+
name="Remove app stack",
745+
project_directory="/srv/app",
746+
project_name="app",
747+
present=False,
748+
remove_volumes=True,
749+
)
750+
"""
751+
if not project_directory:
752+
raise OperationError("docker.compose requires a project_directory")
753+
754+
if pull is not None and pull not in ("always", "missing", "never"):
755+
raise OperationError(
756+
'docker.compose pull must be one of None, "always", "missing", "never"',
757+
)
758+
759+
file_list: list[str] = (
760+
[files] if isinstance(files, str) else list(files) if files is not None else []
761+
)
762+
763+
command_bits: list[str | QuoteString] = [compose_command]
764+
command_bits.extend(["--project-directory", QuoteString(project_directory)])
765+
if project_name:
766+
command_bits.extend(["--project-name", QuoteString(project_name)])
767+
for compose_file in file_list:
768+
command_bits.extend(["-f", QuoteString(compose_file)])
769+
770+
if present:
771+
command_bits.append("up -d")
772+
if pull:
773+
command_bits.extend(["--pull", pull])
774+
if build:
775+
command_bits.append("--build")
776+
if force_recreate:
777+
command_bits.append("--force-recreate")
778+
if remove_orphans:
779+
command_bits.append("--remove-orphans")
780+
else:
781+
command_bits.append("down")
782+
if remove_volumes:
783+
command_bits.append("-v")
784+
if remove_orphans:
785+
command_bits.append("--remove-orphans")
786+
787+
yield StringCommand(*command_bits)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"args": ["/srv/app"],
3+
"kwargs": {
4+
"present": false
5+
},
6+
"commands": [
7+
"docker compose --project-directory /srv/app down --remove-orphans"
8+
]
9+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"args": ["/srv/app"],
3+
"kwargs": {
4+
"present": false,
5+
"remove_volumes": true
6+
},
7+
"commands": [
8+
"docker compose --project-directory /srv/app down -v --remove-orphans"
9+
]
10+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"args": ["/srv/app"],
3+
"commands": [
4+
"docker compose --project-directory /srv/app up -d --remove-orphans"
5+
]
6+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"args": ["/srv/app"],
3+
"kwargs": {
4+
"build": true,
5+
"force_recreate": true,
6+
"remove_orphans": true
7+
},
8+
"commands": [
9+
"docker compose --project-directory /srv/app up -d --build --force-recreate --remove-orphans"
10+
]
11+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"args": ["/srv/app"],
3+
"kwargs": {
4+
"files": ["docker-compose.yml", "docker-compose.prod.yml"]
5+
},
6+
"commands": [
7+
"docker compose --project-directory /srv/app -f docker-compose.yml -f docker-compose.prod.yml up -d --remove-orphans"
8+
]
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"args": ["/srv/app"],
3+
"kwargs": {
4+
"files": "docker-compose.yml"
5+
},
6+
"commands": [
7+
"docker compose --project-directory /srv/app -f docker-compose.yml up -d --remove-orphans"
8+
]
9+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"args": ["/srv/app"],
3+
"kwargs": {
4+
"project_name": "app",
5+
"pull": "always"
6+
},
7+
"commands": [
8+
"docker compose --project-directory /srv/app --project-name app up -d --pull always --remove-orphans"
9+
]
10+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"args": ["/srv/app"],
3+
"kwargs": {
4+
"compose_command": "docker-compose"
5+
},
6+
"commands": [
7+
"docker-compose --project-directory /srv/app up -d --remove-orphans"
8+
]
9+
}

0 commit comments

Comments
 (0)