9 Commits

Author SHA1 Message Date
1b85ebe506 version 2024-02-06 00:26:51 +03:00
9ed05a23ef add TQDM and implement pool.join 2024-02-06 00:26:51 +03:00
d475260951 add TQDM and implement pool.join 2024-02-06 00:26:51 +03:00
831caa6276 remove recursion 2024-02-06 00:23:14 +03:00
9ac33392a0 Fix issue #831
Signed-off-by: Ben Plessinger <Benjamin.Plessinger@roswellpark.org>
2024-02-06 00:20:11 +03:00
c5be5bae90 Fixup tests
Signed-off-by: Falmarri <463948+Falmarri@users.noreply.github.com>
2024-02-04 10:11:57 +03:00
c6a1c4c432 Add tests to make sure all async paths are covered
Not at 100% yet. But upped code coverage significantly and covered major
async calls.

Signed-off-by: Falmarri <463948+Falmarri@users.noreply.github.com>
2024-02-04 10:11:57 +03:00
3c9628b462 Fix a couple issues and update docs
Signed-off-by: Falmarri <463948+Falmarri@users.noreply.github.com>
2024-02-04 10:11:57 +03:00
38b13a34ea Use asyncio for subprocess calls
Removes the threads from compose_up and manages it using async. Also
uses async processing to format the log messages instead of piping
through sed. This should work on windows without having sed installed

Adds --parallel to support pull and build in parallel, same as docker
compose

Signed-off-by: Falmarri <463948+Falmarri@users.noreply.github.com>
2024-02-04 10:11:57 +03:00
14 changed files with 515 additions and 250 deletions

2
.coveragerc Normal file
View File

@ -0,0 +1,2 @@
[run]
parallel=True

View File

@ -21,9 +21,10 @@ jobs:
python-version: "3.10" python-version: "3.10"
- name: Install dependencies - name: Install dependencies
run: | run: |
sudo apt update && apt install podman
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi
- name: Lint with flake8 - name: Lint with flake8
run: | run: |
# stop the build if there are Python syntax errors or undefined names # stop the build if there are Python syntax errors or undefined names
@ -32,5 +33,7 @@ jobs:
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest - name: Test with pytest
run: | run: |
python -m pytest ./pytests coverage run --source podman_compose -m pytest ./pytests
python -m pytest ./tests
coverage combine
coverage report

View File

@ -39,7 +39,15 @@ $ pre-commit install
```shell ```shell
$ pre-commit run --all-files $ pre-commit run --all-files
``` ```
6. Commit your code to your fork's branch. 6. Run code coverage
```shell
coverage run --source podman_compose -m pytest ./pytests
python -m pytest ./tests
coverage combine
coverage report
coverage html
```
7. Commit your code to your fork's branch.
- Make sure you include a `Signed-off-by` message in your commits. Read [this guide](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) to learn how to sign your commits - Make sure you include a `Signed-off-by` message in your commits. Read [this guide](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) to learn how to sign your commits
- In the commit message reference the Issue ID that your code fixes and a brief description of the changes. Example: `Fixes #516: allow empty network` - In the commit message reference the Issue ID that your code fixes and a brief description of the changes. Example: `Fixes #516: allow empty network`
7. Open a PR to `containers/podman-compose:devel` and wait for a maintainer to review your work. 7. Open a PR to `containers/podman-compose:devel` and wait for a maintainer to review your work.
@ -48,18 +56,18 @@ $ pre-commit run --all-files
To add a command you need to add a function that is decorated To add a command you need to add a function that is decorated
with `@cmd_run` passing the compose instance, command name and with `@cmd_run` passing the compose instance, command name and
description. the wrapped function should accept two arguments description. This function must be declared `async` the wrapped
the compose instance and the command-specific arguments (resulted function should accept two arguments the compose instance and
from python's `argparse` package) inside that command you can the command-specific arguments (resulted from python's `argparse`
run PodMan like this `compose.podman.run(['inspect', 'something'])` package) inside that command you can run PodMan like this
and inside that function you can access `compose.pods` `await compose.podman.run(['inspect', 'something'])`and inside
and `compose.containers` ...etc. that function you can access `compose.pods` and `compose.containers`
Here is an example ...etc. Here is an example
``` ```
@cmd_run(podman_compose, 'build', 'build images defined in the stack') @cmd_run(podman_compose, 'build', 'build images defined in the stack')
def compose_build(compose, args): async def compose_build(compose, args):
compose.podman.run(['build', 'something']) await compose.podman.run(['build', 'something'])
``` ```
## Command arguments parsing ## Command arguments parsing
@ -90,10 +98,10 @@ do something like:
``` ```
@cmd_run(podman_compose, 'up', 'up desc') @cmd_run(podman_compose, 'up', 'up desc')
def compose_up(compose, args): async def compose_up(compose, args):
compose.commands['down'](compose, args) await compose.commands['down'](compose, args)
# or # or
compose.commands['down'](argparse.Namespace(foo=123)) await compose.commands['down'](argparse.Namespace(foo=123))
``` ```

View File

@ -1,31 +1,29 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# https://docs.docker.com/compose/compose-file/#service-configuration-reference # https://docs.docker.com/compose/compose-file/#service-configuration-reference
# https://docs.docker.com/samples/ # https://docs.docker.com/samples/
# https://docs.docker.com/compose/gettingstarted/ # https://docs.docker.com/compose/gettingstarted/
# https://docs.docker.com/compose/django/ # https://docs.docker.com/compose/django/
# https://docs.docker.com/compose/wordpress/ # https://docs.docker.com/compose/wordpress/
# TODO: podman pod logs --color -n -f pod_testlogs # TODO: podman pod logs --color -n -f pod_testlogs
import sys import sys
import os import os
import getpass import getpass
import argparse import argparse
import itertools import itertools
import subprocess import subprocess
import time
import re import re
import hashlib import hashlib
import random import random
import json import json
import glob import glob
import asyncio.subprocess
import signal
from threading import Thread from multiprocessing import cpu_count
import shlex import shlex
from asyncio import Task
try: try:
from shlex import quote as cmd_quote from shlex import quote as cmd_quote
@ -35,10 +33,16 @@ except ImportError:
# import fnmatch # import fnmatch
# fnmatch.fnmatchcase(env, "*_HOST") # fnmatch.fnmatchcase(env, "*_HOST")
from tqdm.asyncio import tqdm
# TODO: we need a do-nothing fallback for tqdm
#except ImportError:
# tqdm = asyncio
import yaml import yaml
from dotenv import dotenv_values from dotenv import dotenv_values
__version__ = "1.0.7" __version__ = "1.0.8"
script = os.path.realpath(sys.argv[0]) script = os.path.realpath(sys.argv[0])
@ -87,7 +91,13 @@ def try_float(i, fallback=None):
def log(*msgs, sep=" ", end="\n"): def log(*msgs, sep=" ", end="\n"):
try:
current_task = asyncio.current_task()
except RuntimeError:
current_task = None
line = (sep.join([str(msg) for msg in msgs])) + end line = (sep.join([str(msg) for msg in msgs])) + end
if current_task and not current_task.get_name().startswith("Task"):
line = f"[{current_task.get_name()}] " + line
sys.stderr.write(line) sys.stderr.write(line)
sys.stderr.flush() sys.stderr.flush()
@ -371,7 +381,7 @@ def transform(args, project_name, given_containers):
return pods, containers return pods, containers
def assert_volume(compose, mount_dict): async def assert_volume(compose, mount_dict):
""" """
inspect volume to get directory inspect volume to get directory
create volume if needed create volume if needed
@ -398,7 +408,7 @@ def assert_volume(compose, mount_dict):
# TODO: might move to using "volume list" # TODO: might move to using "volume list"
# podman volume list --format '{{.Name}}\t{{.MountPoint}}' -f 'label=io.podman.compose.project=HERE' # podman volume list --format '{{.Name}}\t{{.MountPoint}}' -f 'label=io.podman.compose.project=HERE'
try: try:
_ = compose.podman.output([], "volume", ["inspect", vol_name]).decode("utf-8") _ = (await compose.podman.output([], "volume", ["inspect", vol_name])).decode("utf-8")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
if is_ext: if is_ext:
raise RuntimeError(f"External volume [{vol_name}] does not exists") from e raise RuntimeError(f"External volume [{vol_name}] does not exists") from e
@ -419,8 +429,8 @@ def assert_volume(compose, mount_dict):
for opt, value in driver_opts.items(): for opt, value in driver_opts.items():
args.extend(["--opt", f"{opt}={value}"]) args.extend(["--opt", f"{opt}={value}"])
args.append(vol_name) args.append(vol_name)
compose.podman.output([], "volume", args) await compose.podman.output([], "volume", args)
_ = compose.podman.output([], "volume", ["inspect", vol_name]).decode("utf-8") _ = (await compose.podman.output([], "volume", ["inspect", vol_name])).decode("utf-8")
def mount_desc_to_mount_args( def mount_desc_to_mount_args(
@ -522,12 +532,12 @@ def get_mnt_dict(compose, cnt, volume):
return fix_mount_dict(compose, volume, proj_name, srv_name) return fix_mount_dict(compose, volume, proj_name, srv_name)
def get_mount_args(compose, cnt, volume): async def get_mount_args(compose, cnt, volume):
volume = get_mnt_dict(compose, cnt, volume) volume = get_mnt_dict(compose, cnt, volume)
# proj_name = compose.project_name # proj_name = compose.project_name
srv_name = cnt["_service"] srv_name = cnt["_service"]
mount_type = volume["type"] mount_type = volume["type"]
assert_volume(compose, volume) await assert_volume(compose, volume)
if compose.prefer_volume_over_mount: if compose.prefer_volume_over_mount:
if mount_type == "tmpfs": if mount_type == "tmpfs":
# TODO: --tmpfs /tmp:rw,size=787448k,mode=1777 # TODO: --tmpfs /tmp:rw,size=787448k,mode=1777
@ -710,7 +720,7 @@ def norm_ports(ports_in):
return ports_out return ports_out
def assert_cnt_nets(compose, cnt): async def assert_cnt_nets(compose, cnt):
""" """
create missing networks create missing networks
""" """
@ -733,7 +743,7 @@ def assert_cnt_nets(compose, cnt):
ext_desc.get("name", None) or net_desc.get("name", None) or default_net_name ext_desc.get("name", None) or net_desc.get("name", None) or default_net_name
) )
try: try:
compose.podman.output([], "network", ["exists", net_name]) await compose.podman.output([], "network", ["exists", net_name])
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
if is_ext: if is_ext:
raise RuntimeError( raise RuntimeError(
@ -776,8 +786,8 @@ def assert_cnt_nets(compose, cnt):
if gateway: if gateway:
args.extend(("--gateway", gateway)) args.extend(("--gateway", gateway))
args.append(net_name) args.append(net_name)
compose.podman.output([], "network", args) await compose.podman.output([], "network", args)
compose.podman.output([], "network", ["exists", net_name]) await compose.podman.output([], "network", ["exists", net_name])
def get_net_args(compose, cnt): def get_net_args(compose, cnt):
@ -898,7 +908,7 @@ def get_net_args(compose, cnt):
return net_args return net_args
def container_to_args(compose, cnt, detached=True): async def container_to_args(compose, cnt, detached=True):
# TODO: double check -e , --add-host, -v, --read-only # TODO: double check -e , --add-host, -v, --read-only
dirname = compose.dirname dirname = compose.dirname
pod = cnt.get("pod", None) or "" pod = cnt.get("pod", None) or ""
@ -955,9 +965,9 @@ def container_to_args(compose, cnt, detached=True):
for i in tmpfs_ls: for i in tmpfs_ls:
podman_args.extend(["--tmpfs", i]) podman_args.extend(["--tmpfs", i])
for volume in cnt.get("volumes", []): for volume in cnt.get("volumes", []):
podman_args.extend(get_mount_args(compose, cnt, volume)) podman_args.extend(await get_mount_args(compose, cnt, volume))
assert_cnt_nets(compose, cnt) await assert_cnt_nets(compose, cnt)
podman_args.extend(get_net_args(compose, cnt)) podman_args.extend(get_net_args(compose, cnt))
logging = cnt.get("logging", None) logging = cnt.get("logging", None)
@ -1161,12 +1171,23 @@ class Podman:
self.podman_path = podman_path self.podman_path = podman_path
self.dry_run = dry_run self.dry_run = dry_run
def output(self, podman_args, cmd="", cmd_args=None): async def output(self, podman_args, cmd="", cmd_args=None):
# NOTE: do we need pool.output? like pool.run
cmd_args = cmd_args or [] cmd_args = cmd_args or []
xargs = self.compose.get_podman_args(cmd) if cmd else [] xargs = self.compose.get_podman_args(cmd) if cmd else []
cmd_ls = [self.podman_path, *podman_args, cmd] + xargs + cmd_args cmd_ls = [self.podman_path, *podman_args, cmd] + xargs + cmd_args
log(cmd_ls) log(cmd_ls)
return subprocess.check_output(cmd_ls) p = await asyncio.subprocess.create_subprocess_exec(
*cmd_ls,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout_data, stderr_data = await p.communicate()
if p.returncode == 0:
return stdout_data
else:
raise subprocess.CalledProcessError(p.returncode, " ".join(cmd_ls), stderr_data)
def exec( def exec(
self, self,
@ -1180,55 +1201,59 @@ class Podman:
log(" ".join([str(i) for i in cmd_ls])) log(" ".join([str(i) for i in cmd_ls]))
os.execlp(self.podman_path, *cmd_ls) os.execlp(self.podman_path, *cmd_ls)
def run( async def run(
self, self,
podman_args, podman_args,
cmd="", cmd="",
cmd_args=None, cmd_args=None,
wait=True, log_formatter=None) -> int:
sleep=1, cmd_args = list(map(str, cmd_args or []))
obj=None, xargs = self.compose.get_podman_args(cmd) if cmd else []
log_formatter=None, cmd_ls = [self.podman_path, *podman_args, cmd] + xargs + cmd_args
): log(" ".join([str(i) for i in cmd_ls]))
if obj is not None: if self.dry_run:
obj.exit_code = None return None
cmd_args = list(map(str, cmd_args or []))
xargs = self.compose.get_podman_args(cmd) if cmd else []
cmd_ls = [self.podman_path, *podman_args, cmd] + xargs + cmd_args
log(" ".join([str(i) for i in cmd_ls]))
if self.dry_run:
return None
# subprocess.Popen(
# args, bufsize = 0, executable = None, stdin = None, stdout = None, stderr = None, preexec_fn = None,
# close_fds = False, shell = False, cwd = None, env = None, universal_newlines = False, startupinfo = None,
# creationflags = 0
# )
if log_formatter is not None:
# Pipe podman process output through log_formatter (which can add colored prefix)
p = subprocess.Popen(
cmd_ls, stdout=subprocess.PIPE
) # pylint: disable=consider-using-with
_ = subprocess.Popen(
log_formatter, stdin=p.stdout
) # pylint: disable=consider-using-with
p.stdout.close() # Allow p_process to receive a SIGPIPE if logging process exits.
else:
p = subprocess.Popen(cmd_ls) # pylint: disable=consider-using-with
if wait: if log_formatter is not None:
exit_code = p.wait()
log("exit code:", exit_code)
if obj is not None:
obj.exit_code = exit_code
if sleep: async def format_out(stdout):
time.sleep(sleep) while True:
return p l = await stdout.readline()
if l:
print(log_formatter, l.decode('utf-8'), end='')
if stdout.at_eof():
break
def volume_ls(self, proj=None): p = await asyncio.subprocess.create_subprocess_exec(
*cmd_ls, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
) # pylint: disable=consider-using-with
out_t = asyncio.create_task(format_out(p.stdout))
err_t = asyncio.create_task(format_out(p.stderr))
else:
p = await asyncio.subprocess.create_subprocess_exec(*cmd_ls) # pylint: disable=consider-using-with
try:
exit_code = await p.wait()
except asyncio.CancelledError as e:
log(f"Sending termination signal")
p.terminate()
try:
async with asyncio.timeout(10):
exit_code = await p.wait()
except TimeoutError:
log(f"container did not shut down after 10 seconds, killing")
p.kill()
exit_code = await p.wait()
log(f"exit code: {exit_code}")
return exit_code
async def volume_ls(self, proj=None):
if not proj: if not proj:
proj = self.compose.project_name proj = self.compose.project_name
output = self.output( output = (await self.output(
[], [],
"volume", "volume",
[ [
@ -1239,10 +1264,38 @@ class Podman:
"--format", "--format",
"{{.Name}}", "{{.Name}}",
], ],
).decode("utf-8") )).decode("utf-8")
volumes = output.splitlines() volumes = output.splitlines()
return volumes return volumes
class Pool:
def __init__(self, podman: Podman, parallel):
self.podman: Podman = podman
self.semaphore = asyncio.Semaphore(parallel) if isinstance(parallel, int) else parallel
self.tasks = []
def create_task(self, coro, *, name=None, context=None):
return self.tasks.append(asyncio.create_task(coro, name=name, context=context))
def run(self, *args, name=None, **kwargs):
return self.create_task(self._run_one(*args, **kwargs), name=name)
async def _run_one(self, *args, **kwargs) -> int:
async with self.semaphore:
return await self.podman.run(*args, **kwargs)
async def join(self, *, desc="joining enqueued tasks") -> int:
if not self.tasks: return 0
ls = await tqdm.gather(*self.tasks, desc=desc)
failed = [ i for i in ls if i != 0]
del self.tasks[:]
count = len(failed)
if count>1:
log(f"** EE ** got multiple failures: [{count}] failures")
if failed:
log(f"** EE ** retcode: [{failed[0]}]")
return failed[0]
return 0
def normalize_service(service, sub_dir=""): def normalize_service(service, sub_dir=""):
if "build" in service: if "build" in service:
@ -1439,6 +1492,7 @@ COMPOSE_DEFAULT_LS = [
class PodmanCompose: class PodmanCompose:
def __init__(self): def __init__(self):
self.podman = None self.podman = None
self.pool = None
self.podman_version = None self.podman_version = None
self.environ = {} self.environ = {}
self.exit_code = None self.exit_code = None
@ -1487,7 +1541,7 @@ class PodmanCompose:
xargs.extend(shlex.split(args)) xargs.extend(shlex.split(args))
return xargs return xargs
def run(self): async def run(self):
log("podman-compose version: " + __version__) log("podman-compose version: " + __version__)
args = self._parse_args() args = self._parse_args()
podman_path = args.podman_path podman_path = args.podman_path
@ -1500,12 +1554,14 @@ class PodmanCompose:
log(f"Binary {podman_path} has not been found.") log(f"Binary {podman_path} has not been found.")
sys.exit(1) sys.exit(1)
self.podman = Podman(self, podman_path, args.dry_run) self.podman = Podman(self, podman_path, args.dry_run)
self.pool = Pool(self.podman, args.parallel)
if not args.dry_run: if not args.dry_run:
# just to make sure podman is running # just to make sure podman is running
try: try:
self.podman_version = ( self.podman_version = (
self.podman.output(["--version"], "", []).decode("utf-8").strip() (await self.podman.output(["--version"], "", [])).decode("utf-8").strip()
or "" or ""
) )
self.podman_version = (self.podman_version.split() or [""])[-1] self.podman_version = (self.podman_version.split() or [""])[-1]
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
@ -1521,7 +1577,8 @@ class PodmanCompose:
if compose_required: if compose_required:
self._parse_compose_file() self._parse_compose_file()
cmd = self.commands[cmd_name] cmd = self.commands[cmd_name]
retcode = cmd(self, args) retcode = await cmd(self, args)
print("retcode", retcode)
if isinstance(retcode, int): if isinstance(retcode, int):
sys.exit(retcode) sys.exit(retcode)
@ -1900,6 +1957,11 @@ class PodmanCompose:
help="No action; perform a simulation of commands", help="No action; perform a simulation of commands",
action="store_true", action="store_true",
) )
parser.add_argument(
"--parallel",
type=int,
default=os.environ.get("COMPOSE_PARALLEL_LIMIT", 2*cpu_count())
)
podman_compose = PodmanCompose() podman_compose = PodmanCompose()
@ -1919,6 +1981,8 @@ class cmd_run: # pylint: disable=invalid-name,too-few-public-methods
def wrapped(*args, **kw): def wrapped(*args, **kw):
return func(*args, **kw) return func(*args, **kw)
if not asyncio.iscoroutinefunction(func):
raise Exception("Command must be async")
wrapped._compose = self.compose wrapped._compose = self.compose
# Trim extra indentation at start of multiline docstrings. # Trim extra indentation at start of multiline docstrings.
wrapped.desc = self.cmd_desc or re.sub(r"^\s+", "", func.__doc__) wrapped.desc = self.cmd_desc or re.sub(r"^\s+", "", func.__doc__)
@ -1947,7 +2011,7 @@ class cmd_parse: # pylint: disable=invalid-name,too-few-public-methods
@cmd_run(podman_compose, "version", "show version") @cmd_run(podman_compose, "version", "show version")
def compose_version(compose, args): async def compose_version(compose, args):
if getattr(args, "short", False): if getattr(args, "short", False):
print(__version__) print(__version__)
return return
@ -1956,7 +2020,7 @@ def compose_version(compose, args):
print(json.dumps(res)) print(json.dumps(res))
return return
print("podman-compose version", __version__) print("podman-compose version", __version__)
compose.podman.run(["--version"], "", [], sleep=0) await compose.podman.run(["--version"], "", [])
def is_local(container: dict) -> bool: def is_local(container: dict) -> bool:
@ -1972,15 +2036,15 @@ def is_local(container: dict) -> bool:
@cmd_run(podman_compose, "wait", "wait running containers to stop") @cmd_run(podman_compose, "wait", "wait running containers to stop")
def compose_wait(compose, args): # pylint: disable=unused-argument async def compose_wait(compose, args): # pylint: disable=unused-argument
containers = [cnt["name"] for cnt in compose.containers] containers = [cnt["name"] for cnt in compose.containers]
cmd_args = ["--"] cmd_args = ["--"]
cmd_args.extend(containers) cmd_args.extend(containers)
compose.podman.exec([], "wait", cmd_args) await compose.podman.exec([], "wait", cmd_args)
@cmd_run(podman_compose, "systemd") @cmd_run(podman_compose, "systemd")
def compose_systemd(compose, args): async def compose_systemd(compose, args):
""" """
create systemd unit file and register its compose stacks create systemd unit file and register its compose stacks
@ -2000,8 +2064,8 @@ def compose_systemd(compose, args):
f.write(f"{k}={v}\n") f.write(f"{k}={v}\n")
print(f"writing [{fn}]: done.") print(f"writing [{fn}]: done.")
print("\n\ncreating the pod without starting it: ...\n\n") print("\n\ncreating the pod without starting it: ...\n\n")
process = subprocess.run([script, "up", "--no-start"], check=False) process = await asyncio.subprocess.create_subprocess_exec(script, ["up", "--no-start"])
print("\nfinal exit code is ", process.returncode) print("\nfinal exit code is ", process)
username = getpass.getuser() username = getpass.getuser()
print( print(
f""" f"""
@ -2063,7 +2127,7 @@ while in your project type `podman-compose systemd -a register`
@cmd_run(podman_compose, "pull", "pull stack images") @cmd_run(podman_compose, "pull", "pull stack images")
def compose_pull(compose, args): async def compose_pull(compose: PodmanCompose, args):
img_containers = [cnt for cnt in compose.containers if "image" in cnt] img_containers = [cnt for cnt in compose.containers if "image" in cnt]
if args.services: if args.services:
services = set(args.services) services = set(args.services)
@ -2072,27 +2136,33 @@ def compose_pull(compose, args):
if not args.force_local: if not args.force_local:
local_images = {cnt["image"] for cnt in img_containers if is_local(cnt)} local_images = {cnt["image"] for cnt in img_containers if is_local(cnt)}
images -= local_images images -= local_images
for image in images:
compose.podman.run([], "pull", [image], sleep=0) images_process = tqdm(images, desc="pulling images")
for image in images_process:
# images_process.set_postfix_str(image)
compose.pool.run([], "pull", [image])
# uncomment to see how progress work
# await asyncio.sleep(1)
return await compose.pool.join(desc="waiting pull")
@cmd_run(podman_compose, "push", "push stack images") @cmd_run(podman_compose, "push", "push stack images")
def compose_push(compose, args): async def compose_push(compose, args):
services = set(args.services) services = set(args.services)
for cnt in compose.containers: for cnt in compose.containers:
if "build" not in cnt: if "build" not in cnt:
continue continue
if services and cnt["_service"] not in services: if services and cnt["_service"] not in services:
continue continue
compose.podman.run([], "push", [cnt["image"]], sleep=0) await compose.podman.run([], "push", [cnt["image"]])
def build_one(compose, args, cnt): async def build_one(compose, args, cnt):
if "build" not in cnt: if "build" not in cnt:
return None return None
if getattr(args, "if_not_exists", None): if getattr(args, "if_not_exists", None):
try: try:
img_id = compose.podman.output( img_id = await compose.podman.output(
[], "inspect", ["-t", "image", "-f", "{{.Id}}", cnt["image"]] [], "inspect", ["-t", "image", "-f", "{{.Id}}", cnt["image"]]
) )
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
@ -2148,40 +2218,28 @@ def build_one(compose, args, cnt):
) )
) )
build_args.append(ctx) build_args.append(ctx)
status = compose.podman.run([], "build", build_args, sleep=0) status = await compose.podman.run([], "build", build_args)
return status return status
@cmd_run(podman_compose, "build", "build stack images") @cmd_run(podman_compose, "build", "build stack images")
def compose_build(compose, args): async def compose_build(compose: PodmanCompose, args):
# keeps the status of the last service/container built
status = 0
def parse_return_code(obj, current_status):
if obj and obj.returncode != 0:
return obj.returncode
return current_status
if args.services: if args.services:
container_names_by_service = compose.container_names_by_service container_names_by_service = compose.container_names_by_service
compose.assert_services(args.services) compose.assert_services(args.services)
for service in args.services: for service in tqdm(args.services, desc="building"):
cnt = compose.container_by_name[container_names_by_service[service][0]] cnt = compose.container_by_name[container_names_by_service[service][0]]
p = build_one(compose, args, cnt) compose.pool.create_task(build_one(compose, args, cnt))
status = parse_return_code(p, status)
if status != 0:
return status
else: else:
for cnt in compose.containers: for cnt in tqdm(compose.containers, desc="building"):
p = build_one(compose, args, cnt) compose.pool.create_task(build_one(compose, args, cnt))
status = parse_return_code(p, status)
if status != 0:
return status
return status return await compose.pool.join("waiting build")
def create_pods(compose, args): # pylint: disable=unused-argument async def create_pods(compose, args): # pylint: disable=unused-argument
for pod in compose.pods: for pod in compose.pods:
podman_args = [ podman_args = [
"create", "create",
@ -2196,7 +2254,7 @@ def create_pods(compose, args): # pylint: disable=unused-argument
ports = [ports] ports = [ports]
for i in ports: for i in ports:
podman_args.extend(["-p", str(i)]) podman_args.extend(["-p", str(i)])
compose.podman.run([], "pod", podman_args) await compose.podman.run([], "pod", podman_args)
def get_excluded(compose, args): def get_excluded(compose, args):
@ -2213,16 +2271,17 @@ def get_excluded(compose, args):
@cmd_run( @cmd_run(
podman_compose, "up", "Create and start the entire stack or some of its services" podman_compose, "up", "Create and start the entire stack or some of its services"
) )
def compose_up(compose, args): async def compose_up(compose: PodmanCompose, args):
proj_name = compose.project_name proj_name = compose.project_name
excluded = get_excluded(compose, args) excluded = get_excluded(compose, args)
if not args.no_build: if not args.no_build:
# `podman build` does not cache, so don't always build # `podman build` does not cache, so don't always build
build_args = argparse.Namespace(if_not_exists=(not args.build), **args.__dict__) build_args = argparse.Namespace(if_not_exists=(not args.build), **args.__dict__)
compose.commands["build"](compose, build_args) if await compose.commands["build"](compose, build_args) != 0:
log("Build command failed")
hashes = ( hashes = (
compose.podman.output( (await compose.podman.output(
[], [],
"ps", "ps",
[ [
@ -2232,7 +2291,7 @@ def compose_up(compose, args):
"--format", "--format",
'{{ index .Labels "io.podman.compose.config-hash"}}', '{{ index .Labels "io.podman.compose.config-hash"}}',
], ],
) ))
.decode("utf-8") .decode("utf-8")
.splitlines() .splitlines()
) )
@ -2240,21 +2299,21 @@ def compose_up(compose, args):
if args.force_recreate or len(diff_hashes): if args.force_recreate or len(diff_hashes):
log("recreating: ...") log("recreating: ...")
down_args = argparse.Namespace(**dict(args.__dict__, volumes=False)) down_args = argparse.Namespace(**dict(args.__dict__, volumes=False))
compose.commands["down"](compose, down_args) await compose.commands["down"](compose, down_args)
log("recreating: done\n\n") log("recreating: done\n\n")
# args.no_recreate disables check for changes (which is not implemented) # args.no_recreate disables check for changes (which is not implemented)
podman_command = "run" if args.detach and not args.no_start else "create" podman_command = "run" if args.detach and not args.no_start else "create"
create_pods(compose, args) await create_pods(compose, args)
for cnt in compose.containers: for cnt in compose.containers:
if cnt["_service"] in excluded: if cnt["_service"] in excluded:
log("** skipping: ", cnt["name"]) log("** skipping: ", cnt["name"])
continue continue
podman_args = container_to_args(compose, cnt, detached=args.detach) podman_args = await container_to_args(compose, cnt, detached=args.detach)
subproc = compose.podman.run([], podman_command, podman_args) subproc = await compose.podman.run([], podman_command, podman_args)
if podman_command == "run" and subproc and subproc.returncode: if podman_command == "run" and subproc is not None:
compose.podman.run([], "start", [cnt["name"]]) await compose.podman.run([], "start", [cnt["name"]])
if args.no_start or args.detach or args.dry_run: if args.no_start or args.detach or args.dry_run:
return return
# TODO: handle already existing # TODO: handle already existing
@ -2264,54 +2323,54 @@ def compose_up(compose, args):
if exit_code_from: if exit_code_from:
args.abort_on_container_exit = True args.abort_on_container_exit = True
threads = []
max_service_length = 0 max_service_length = 0
for cnt in compose.containers: for cnt in compose.containers:
curr_length = len(cnt["_service"]) curr_length = len(cnt["_service"])
max_service_length = ( max_service_length = (
curr_length if curr_length > max_service_length else max_service_length curr_length if curr_length > max_service_length else max_service_length
) )
has_sed = os.path.isfile("/bin/sed")
tasks = set()
loop = asyncio.get_event_loop()
loop.add_signal_handler(signal.SIGINT, lambda: [t.cancel("User exit") for t in tasks])
for i, cnt in enumerate(compose.containers): for i, cnt in enumerate(compose.containers):
# Add colored service prefix to output by piping output through sed # Add colored service prefix to output by piping output through sed
color_idx = i % len(compose.console_colors) color_idx = i % len(compose.console_colors)
color = compose.console_colors[color_idx] color = compose.console_colors[color_idx]
space_suffix = " " * (max_service_length - len(cnt["_service"]) + 1) space_suffix = " " * (max_service_length - len(cnt["_service"]) + 1)
log_formatter = "s/^/{}[{}]{}|\x1B[0m\\ /;".format( log_formatter = "{}[{}]{}|\x1B[0m".format(
color, cnt["_service"], space_suffix color, cnt["_service"], space_suffix
) )
log_formatter = ["sed", "-e", log_formatter] if has_sed else None
if cnt["_service"] in excluded: if cnt["_service"] in excluded:
log("** skipping: ", cnt["name"]) log("** skipping: ", cnt["name"])
continue continue
# TODO: remove sleep from podman.run
obj = compose if exit_code_from == cnt["_service"] else None
thread = Thread(
target=compose.podman.run,
args=[[], "start", ["-a", cnt["name"]]],
kwargs={"obj": obj, "log_formatter": log_formatter},
daemon=True,
name=cnt["name"],
)
thread.start()
threads.append(thread)
time.sleep(1)
while threads: tasks.add(
to_remove = [] asyncio.create_task(
for thread in threads: compose.podman.run([], "start", ["-a", cnt["name"]], log_formatter=log_formatter),
thread.join(timeout=1.0) name=cnt["_service"]
if not thread.is_alive(): )
to_remove.append(thread) )
if args.abort_on_container_exit:
time.sleep(1) exit_code = 0
exit_code = ( exiting = False
compose.exit_code if compose.exit_code is not None else -1 while tasks:
) done, tasks = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
sys.exit(exit_code) if args.abort_on_container_exit:
for thread in to_remove: if not exiting:
threads.remove(thread) # If 2 containers exit at the exact same time, the cancellation of the other ones cause the status
# to overwrite. Sleeping for 1 seems to fix this and make it match docker-compose
await asyncio.sleep(1)
[_.cancel() for _ in tasks if not _.cancelling() and not _.cancelled()]
t: Task
exiting = True
for t in done:
if t.get_name() == exit_code_from:
exit_code = t.result()
return exit_code
def get_volume_names(compose, cnt): def get_volume_names(compose, cnt):
@ -2332,13 +2391,13 @@ def get_volume_names(compose, cnt):
@cmd_run(podman_compose, "down", "tear down entire stack") @cmd_run(podman_compose, "down", "tear down entire stack")
def compose_down(compose, args): async def compose_down(compose: PodmanCompose, args):
excluded = get_excluded(compose, args) excluded = get_excluded(compose, args)
podman_args = [] podman_args = []
timeout_global = getattr(args, "timeout", None) timeout_global = getattr(args, "timeout", None)
containers = list(reversed(compose.containers)) containers = list(reversed(compose.containers))
for cnt in containers: for cnt in tqdm(containers, "stopping ..."):
if cnt["_service"] in excluded: if cnt["_service"] in excluded:
continue continue
podman_stop_args = [*podman_args] podman_stop_args = [*podman_args]
@ -2348,14 +2407,16 @@ def compose_down(compose, args):
timeout = str_to_seconds(timeout_str) timeout = str_to_seconds(timeout_str)
if timeout is not None: if timeout is not None:
podman_stop_args.extend(["-t", str(timeout)]) podman_stop_args.extend(["-t", str(timeout)])
compose.podman.run([], "stop", [*podman_stop_args, cnt["name"]], sleep=0) compose.pool.run([], "stop", [*podman_stop_args, cnt["name"]], name=cnt["name"])
await compose.pool.join(desc="waiting to be stopped")
for cnt in containers: for cnt in containers:
if cnt["_service"] in excluded: if cnt["_service"] in excluded:
continue continue
compose.podman.run([], "rm", [cnt["name"]], sleep=0) await compose.podman.run([], "rm", [cnt["name"]])
if args.remove_orphans: if args.remove_orphans:
names = ( names = (
compose.podman.output( (await compose.podman.output(
[], [],
"ps", "ps",
[ [
@ -2365,14 +2426,14 @@ def compose_down(compose, args):
"--format", "--format",
"{{ .Names }}", "{{ .Names }}",
], ],
) ))
.decode("utf-8") .decode("utf-8")
.splitlines() .splitlines()
) )
for name in names: for name in names:
compose.podman.run([], "stop", [*podman_args, name], sleep=0) compose.podman.run([], "stop", [*podman_args, name])
for name in names: for name in names:
compose.podman.run([], "rm", [name], sleep=0) await compose.podman.run([], "rm", [name])
if args.volumes: if args.volumes:
vol_names_to_keep = set() vol_names_to_keep = set()
for cnt in containers: for cnt in containers:
@ -2380,36 +2441,31 @@ def compose_down(compose, args):
continue continue
vol_names_to_keep.update(get_volume_names(compose, cnt)) vol_names_to_keep.update(get_volume_names(compose, cnt))
log("keep", vol_names_to_keep) log("keep", vol_names_to_keep)
for volume_name in compose.podman.volume_ls(): for volume_name in await compose.podman.volume_ls():
if volume_name in vol_names_to_keep: if volume_name in vol_names_to_keep:
continue continue
compose.podman.run([], "volume", ["rm", volume_name]) await compose.podman.run([], "volume", ["rm", volume_name])
if excluded: if excluded:
return return
for pod in compose.pods: for pod in compose.pods:
compose.podman.run([], "pod", ["rm", pod["name"]], sleep=0) await compose.podman.run([], "pod", ["rm", pod["name"]])
@cmd_run(podman_compose, "ps", "show status of containers") @cmd_run(podman_compose, "ps", "show status of containers")
def compose_ps(compose, args): async def compose_ps(compose, args):
proj_name = compose.project_name proj_name = compose.project_name
ps_args = ["-a", "--filter", f"label=io.podman.compose.project={proj_name}"]
if args.quiet is True: if args.quiet is True:
compose.podman.run( ps_args.extend(["--format", "{{.ID}}"])
[], elif args.format:
"ps", ps_args.extend(["--format", args.format])
[
"-a", await compose.podman.run(
"--format", [],
"{{.ID}}", "ps",
"--filter", ps_args,
f"label=io.podman.compose.project={proj_name}", )
],
)
else:
compose.podman.run(
[], "ps", ["-a", "--filter", f"label=io.podman.compose.project={proj_name}"]
)
@cmd_run( @cmd_run(
@ -2417,7 +2473,7 @@ def compose_ps(compose, args):
"run", "run",
"create a container similar to a service to run a one-off command", "create a container similar to a service to run a one-off command",
) )
def compose_run(compose, args): async def compose_run(compose, args):
create_pods(compose, args) create_pods(compose, args)
compose.assert_services(args.service) compose.assert_services(args.service)
container_names = compose.container_names_by_service[args.service] container_names = compose.container_names_by_service[args.service]
@ -2437,17 +2493,19 @@ def compose_run(compose, args):
no_start=False, no_start=False,
no_cache=False, no_cache=False,
build_arg=[], build_arg=[],
parallel=1,
remove_orphans=True
) )
) )
compose.commands["up"](compose, up_args) await compose.commands["up"](compose, up_args)
build_args = argparse.Namespace( build_args = argparse.Namespace(
services=[args.service], services=[args.service],
if_not_exists=(not args.build), if_not_exists=(not args.build),
build_arg=[], build_arg=[],
**args.__dict__, **args.__dict__
) )
compose.commands["build"](compose, build_args) await compose.commands["build"](compose, build_args)
# adjust one-off container options # adjust one-off container options
name0 = "{}_{}_tmp{}".format( name0 = "{}_{}_tmp{}".format(
@ -2483,17 +2541,17 @@ def compose_run(compose, args):
if args.rm and "restart" in cnt: if args.rm and "restart" in cnt:
del cnt["restart"] del cnt["restart"]
# run podman # run podman
podman_args = container_to_args(compose, cnt, args.detach) podman_args = await container_to_args(compose, cnt, args.detach)
if not args.detach: if not args.detach:
podman_args.insert(1, "-i") podman_args.insert(1, "-i")
if args.rm: if args.rm:
podman_args.insert(1, "--rm") podman_args.insert(1, "--rm")
p = compose.podman.run([], "run", podman_args, sleep=0) p = await compose.podman.run([], "run", podman_args)
sys.exit(p.returncode) sys.exit(p)
@cmd_run(podman_compose, "exec", "execute a command in a running container") @cmd_run(podman_compose, "exec", "execute a command in a running container")
def compose_exec(compose, args): async def compose_exec(compose, args):
compose.assert_services(args.service) compose.assert_services(args.service)
container_names = compose.container_names_by_service[args.service] container_names = compose.container_names_by_service[args.service]
container_name = container_names[args.index - 1] container_name = container_names[args.index - 1]
@ -2518,11 +2576,11 @@ def compose_exec(compose, args):
podman_args += [container_name] podman_args += [container_name]
if args.cnt_command is not None and len(args.cnt_command) > 0: if args.cnt_command is not None and len(args.cnt_command) > 0:
podman_args += args.cnt_command podman_args += args.cnt_command
p = compose.podman.run([], "exec", podman_args, sleep=0) p = await compose.podman.run([], "exec", podman_args)
sys.exit(p.returncode) sys.exit(p)
def transfer_service_status(compose, args, action): async def transfer_service_status(compose: PodmanCompose, args, action):
# TODO: handle dependencies, handle creations # TODO: handle dependencies, handle creations
container_names_by_service = compose.container_names_by_service container_names_by_service = compose.container_names_by_service
if not args.services: if not args.services:
@ -2537,7 +2595,7 @@ def transfer_service_status(compose, args, action):
targets = list(reversed(targets)) targets = list(reversed(targets))
podman_args = [] podman_args = []
timeout_global = getattr(args, "timeout", None) timeout_global = getattr(args, "timeout", None)
for target in targets: for target in tqdm(targets):
if action != "start": if action != "start":
timeout = timeout_global timeout = timeout_global
if timeout is None: if timeout is None:
@ -2548,26 +2606,27 @@ def transfer_service_status(compose, args, action):
timeout = str_to_seconds(timeout_str) timeout = str_to_seconds(timeout_str)
if timeout is not None: if timeout is not None:
podman_args.extend(["-t", str(timeout)]) podman_args.extend(["-t", str(timeout)])
compose.podman.run([], action, podman_args + [target], sleep=0) compose.pool.run([], action, podman_args + [target], name=target)
await compose.pool.join()
@cmd_run(podman_compose, "start", "start specific services") @cmd_run(podman_compose, "start", "start specific services")
def compose_start(compose, args): async def compose_start(compose, args):
transfer_service_status(compose, args, "start") await transfer_service_status(compose, args, "start")
@cmd_run(podman_compose, "stop", "stop specific services") @cmd_run(podman_compose, "stop", "stop specific services")
def compose_stop(compose, args): async def compose_stop(compose, args):
transfer_service_status(compose, args, "stop") await transfer_service_status(compose, args, "stop")
@cmd_run(podman_compose, "restart", "restart specific services") @cmd_run(podman_compose, "restart", "restart specific services")
def compose_restart(compose, args): async def compose_restart(compose, args):
transfer_service_status(compose, args, "restart") await transfer_service_status(compose, args, "restart")
@cmd_run(podman_compose, "logs", "show logs from services") @cmd_run(podman_compose, "logs", "show logs from services")
def compose_logs(compose, args): async def compose_logs(compose, args):
container_names_by_service = compose.container_names_by_service container_names_by_service = compose.container_names_by_service
if not args.services and not args.latest: if not args.services and not args.latest:
args.services = container_names_by_service.keys() args.services = container_names_by_service.keys()
@ -2594,11 +2653,11 @@ def compose_logs(compose, args):
podman_args.extend(["--until", args.until]) podman_args.extend(["--until", args.until])
for target in targets: for target in targets:
podman_args.append(target) podman_args.append(target)
compose.podman.run([], "logs", podman_args) await compose.podman.run([], "logs", podman_args)
@cmd_run(podman_compose, "config", "displays the compose file") @cmd_run(podman_compose, "config", "displays the compose file")
def compose_config(compose, args): async def compose_config(compose, args):
if args.services: if args.services:
for service in compose.services: for service in compose.services:
print(service) print(service)
@ -2607,7 +2666,7 @@ def compose_config(compose, args):
@cmd_run(podman_compose, "port", "Prints the public port for a port binding.") @cmd_run(podman_compose, "port", "Prints the public port for a port binding.")
def compose_port(compose, args): async def compose_port(compose, args):
# TODO - deal with pod index # TODO - deal with pod index
compose.assert_services(args.service) compose.assert_services(args.service)
containers = compose.container_names_by_service[args.service] containers = compose.container_names_by_service[args.service]
@ -2635,31 +2694,31 @@ def compose_port(compose, args):
@cmd_run(podman_compose, "pause", "Pause all running containers") @cmd_run(podman_compose, "pause", "Pause all running containers")
def compose_pause(compose, args): async def compose_pause(compose, args):
container_names_by_service = compose.container_names_by_service container_names_by_service = compose.container_names_by_service
if not args.services: if not args.services:
args.services = container_names_by_service.keys() args.services = container_names_by_service.keys()
targets = [] targets = []
for service in args.services: for service in args.services:
targets.extend(container_names_by_service[service]) targets.extend(container_names_by_service[service])
compose.podman.run([], "pause", targets) await compose.podman.run([], "pause", targets)
@cmd_run(podman_compose, "unpause", "Unpause all running containers") @cmd_run(podman_compose, "unpause", "Unpause all running containers")
def compose_unpause(compose, args): async def compose_unpause(compose, args):
container_names_by_service = compose.container_names_by_service container_names_by_service = compose.container_names_by_service
if not args.services: if not args.services:
args.services = container_names_by_service.keys() args.services = container_names_by_service.keys()
targets = [] targets = []
for service in args.services: for service in args.services:
targets.extend(container_names_by_service[service]) targets.extend(container_names_by_service[service])
compose.podman.run([], "unpause", targets) await compose.podman.run([], "unpause", targets)
@cmd_run( @cmd_run(
podman_compose, "kill", "Kill one or more running containers with a specific signal" podman_compose, "kill", "Kill one or more running containers with a specific signal"
) )
def compose_kill(compose, args): async def compose_kill(compose, args):
# to ensure that the user did not execute the command by mistake # to ensure that the user did not execute the command by mistake
if not args.services and not args.all: if not args.services and not args.all:
print( print(
@ -2680,15 +2739,14 @@ def compose_kill(compose, args):
targets.extend(container_names_by_service[service]) targets.extend(container_names_by_service[service])
for target in targets: for target in targets:
podman_args.append(target) podman_args.append(target)
compose.podman.run([], "kill", podman_args) await compose.podman.run([], "kill", podman_args)
elif args.services:
if args.services:
targets = [] targets = []
for service in args.services: for service in args.services:
targets.extend(container_names_by_service[service]) targets.extend(container_names_by_service[service])
for target in targets: for target in targets:
podman_args.append(target) podman_args.append(target)
compose.podman.run([], "kill", podman_args) await compose.podman.run([], "kill", podman_args)
@cmd_run( @cmd_run(
@ -2696,7 +2754,7 @@ def compose_kill(compose, args):
"stats", "stats",
"Display percentage of CPU, memory, network I/O, block I/O and PIDs for services.", "Display percentage of CPU, memory, network I/O, block I/O and PIDs for services.",
) )
def compose_stats(compose, args): async def compose_stats(compose, args):
container_names_by_service = compose.container_names_by_service container_names_by_service = compose.container_names_by_service
if not args.services: if not args.services:
args.services = container_names_by_service.keys() args.services = container_names_by_service.keys()
@ -2717,7 +2775,7 @@ def compose_stats(compose, args):
podman_args.append(target) podman_args.append(target)
try: try:
compose.podman.run([], "stats", podman_args) await compose.podman.run([], "stats", podman_args)
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
@ -3183,12 +3241,6 @@ def compose_stats_parse(parser):
type=int, type=int,
help="Time in seconds between stats reports (default 5)", help="Time in seconds between stats reports (default 5)",
) )
parser.add_argument(
"-f",
"--format",
type=str,
help="Pretty-print container statistics to JSON or using a Go template",
)
parser.add_argument( parser.add_argument(
"--no-reset", "--no-reset",
help="Disable resetting the screen between intervals", help="Disable resetting the screen between intervals",
@ -3201,9 +3253,20 @@ def compose_stats_parse(parser):
) )
def main(): @cmd_parse(podman_compose, ["ps", "stats"])
podman_compose.run() def compose_format_parse(parser):
parser.add_argument(
"-f",
"--format",
type=str,
help="Pretty-print container statistics to JSON or using a Go template",
)
async def async_main():
await podman_compose.run()
def main():
asyncio.run(async_main())
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -45,6 +45,7 @@ setup(
"black", "black",
"pylint", "pylint",
"pre-commit", "pre-commit",
"coverage"
] ]
} }
# test_suite='tests', # test_suite='tests',

View File

@ -3,7 +3,7 @@
# process, which may cause wedges in the gate later. # process, which may cause wedges in the gate later.
coverage coverage
pytest-cov
pytest pytest
tox tox
black black
flake8

View File

@ -1,4 +1,4 @@
``` ```
podman-compose run --rm sleep /bin/sh -c 'wget -O - http://localhost:8000/hosts' podman-compose run --rm sleep /bin/sh -c 'wget -O - http://web:8000/hosts'
``` ```

View File

@ -9,7 +9,8 @@ services:
sleep: sleep:
image: busybox image: busybox
command: ["/bin/busybox", "sh", "-c", "sleep 3600"] command: ["/bin/busybox", "sh", "-c", "sleep 3600"]
depends_on: "web" depends_on:
- "web"
tmpfs: tmpfs:
- /run - /run
- /tmp - /tmp

View File

@ -1,7 +1,7 @@
version: "3" version: "3"
services: services:
redis: redis:
image: redis:alpine image: docker.io/library/redis:alpine
command: ["redis-server", "--appendonly yes", "--notify-keyspace-events", "Ex"] command: ["redis-server", "--appendonly yes", "--notify-keyspace-events", "Ex"]
volumes: volumes:
- ./data/redis:/data:z - ./data/redis:/data:z
@ -12,7 +12,7 @@ services:
- SECRET_KEY=aabbcc - SECRET_KEY=aabbcc
- ENV_IS_SET - ENV_IS_SET
web: web:
image: busybox image: docker.io/library/busybox
command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8000"] command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8000"]
working_dir: /var/www/html working_dir: /var/www/html
volumes: volumes:
@ -21,19 +21,19 @@ services:
- /run - /run
- /tmp - /tmp
web1: web1:
image: busybox image: docker.io/library/busybox
command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"] command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"]
working_dir: /var/www/html working_dir: /var/www/html
volumes: volumes:
- ./data/web:/var/www/html:ro,z - ./data/web:/var/www/html:ro,z
web2: web2:
image: busybox image: docker.io/library/busybox
command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8002"] command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8002"]
working_dir: /var/www/html working_dir: /var/www/html
volumes: volumes:
- ~/Downloads/www:/var/www/html:ro,z - ~/Downloads/www:/var/www/html:ro,z
web3: web3:
image: busybox image: docker.io/library/busybox
command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8003"] command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8003"]
working_dir: /var/www/html working_dir: /var/www/html
volumes: volumes:

View File

@ -21,7 +21,8 @@ def test_podman_compose_extends_w_file_subdir():
main_path = Path(__file__).parent.parent main_path = Path(__file__).parent.parent
command_up = [ command_up = [
"python3", "coverage",
"run",
str(main_path.joinpath("podman_compose.py")), str(main_path.joinpath("podman_compose.py")),
"-f", "-f",
str(main_path.joinpath("tests", "extends_w_file_subdir", "docker-compose.yml")), str(main_path.joinpath("tests", "extends_w_file_subdir", "docker-compose.yml")),
@ -30,12 +31,14 @@ def test_podman_compose_extends_w_file_subdir():
] ]
command_check_container = [ command_check_container = [
"podman", "coverage",
"container", "run",
str(main_path.joinpath("podman_compose.py")),
"-f",
str(main_path.joinpath("tests", "extends_w_file_subdir", "docker-compose.yml")),
"ps", "ps",
"--all",
"--format", "--format",
'"{{.Image}}"', '{{.Image}}',
] ]
command_down = [ command_down = [
@ -49,16 +52,17 @@ def test_podman_compose_extends_w_file_subdir():
out, _, returncode = capture(command_up) out, _, returncode = capture(command_up)
assert 0 == returncode assert 0 == returncode
# check container was created and exists # check container was created and exists
out, _, returncode = capture(command_check_container) out, err, returncode = capture(command_check_container)
assert 0 == returncode assert 0 == returncode
assert out == b'"localhost/subdir_test:me"\n' assert b'localhost/subdir_test:me\n' == out
out, _, returncode = capture(command_down) out, _, returncode = capture(command_down)
# cleanup test image(tags) # cleanup test image(tags)
assert 0 == returncode assert 0 == returncode
print('ok')
# check container did not exists anymore # check container did not exists anymore
out, _, returncode = capture(command_check_container) out, _, returncode = capture(command_check_container)
assert 0 == returncode assert 0 == returncode
assert out == b"" assert b'' == out
def test_podman_compose_extends_w_empty_service(): def test_podman_compose_extends_w_empty_service():

View File

@ -22,7 +22,7 @@ def test_config_no_profiles(podman_compose_path, profile_compose_file):
:param podman_compose_path: The fixture used to specify the path to the podman compose file. :param podman_compose_path: The fixture used to specify the path to the podman compose file.
:param profile_compose_file: The fixtued used to specify the path to the "profile" compose used in the test. :param profile_compose_file: The fixtued used to specify the path to the "profile" compose used in the test.
""" """
config_cmd = ["python3", podman_compose_path, "-f", profile_compose_file, "config"] config_cmd = ["coverage", "run", podman_compose_path, "-f", profile_compose_file, "config"]
out, _, return_code = capture(config_cmd) out, _, return_code = capture(config_cmd)
assert return_code == 0 assert return_code == 0
@ -61,7 +61,7 @@ def test_config_profiles(
:param expected_services: Dictionary used to model the expected "enabled" services in the profile. :param expected_services: Dictionary used to model the expected "enabled" services in the profile.
Key = service name, Value = True if the service is enabled, otherwise False. Key = service name, Value = True if the service is enabled, otherwise False.
""" """
config_cmd = ["python3", podman_compose_path, "-f", profile_compose_file] config_cmd = ["coverage", "run", podman_compose_path, "-f", profile_compose_file]
config_cmd.extend(profiles) config_cmd.extend(profiles)
out, _, return_code = capture(config_cmd) out, _, return_code = capture(config_cmd)

View File

@ -20,7 +20,8 @@ def test_podman_compose_include():
main_path = Path(__file__).parent.parent main_path = Path(__file__).parent.parent
command_up = [ command_up = [
"python3", "coverage",
"run",
str(main_path.joinpath("podman_compose.py")), str(main_path.joinpath("podman_compose.py")),
"-f", "-f",
str(main_path.joinpath("tests", "include", "docker-compose.yaml")), str(main_path.joinpath("tests", "include", "docker-compose.yaml")),

View File

@ -0,0 +1,180 @@
"""
test_podman_compose_up_down.py
Tests the podman compose up and down commands used to create and remove services.
"""
# pylint: disable=redefined-outer-name
import os
import time
from test_podman_compose import capture
def test_exit_from(podman_compose_path, test_path):
up_cmd = [
"coverage",
"run",
podman_compose_path,
"-f",
os.path.join(test_path, "exit-from", "docker-compose.yaml"),
"up"
]
out, _, return_code = capture(up_cmd + ["--exit-code-from", "sh1"])
assert return_code == 1
out, _, return_code = capture(up_cmd + ["--exit-code-from", "sh2"])
assert return_code == 2
def test_run(podman_compose_path, test_path):
"""
This will test depends_on as well
"""
run_cmd = [
"coverage",
"run",
podman_compose_path,
"-f",
os.path.join(test_path, "deps", "docker-compose.yaml"),
"run",
"--rm",
"sleep",
"/bin/sh",
"-c",
"wget -q -O - http://web:8000/hosts"
]
out, _, return_code = capture(run_cmd)
assert b'127.0.0.1\tlocalhost' in out
# Run it again to make sure we can run it twice. I saw an issue where a second run, with the container left up,
# would fail
run_cmd = [
"coverage",
"run",
podman_compose_path,
"-f",
os.path.join(test_path, "deps", "docker-compose.yaml"),
"run",
"--rm",
"sleep",
"/bin/sh",
"-c",
"wget -q -O - http://web:8000/hosts"
]
out, _, return_code = capture(run_cmd)
assert b'127.0.0.1\tlocalhost' in out
assert return_code == 0
# This leaves a container running. Not sure it's intended, but it matches docker-compose
down_cmd = [
"coverage",
"run",
podman_compose_path,
"-f",
os.path.join(test_path, "deps", "docker-compose.yaml"),
"down",
]
out, _, return_code = capture(run_cmd)
assert return_code == 0
def test_up_with_ports(podman_compose_path, test_path):
up_cmd = [
"coverage",
"run",
podman_compose_path,
"-f",
os.path.join(test_path, "ports", "docker-compose.yml"),
"up",
"-d",
"--force-recreate"
]
down_cmd = [
"coverage",
"run",
podman_compose_path,
"-f",
os.path.join(test_path, "ports", "docker-compose.yml"),
"down",
"--volumes"
]
try:
out, _, return_code = capture(up_cmd)
assert return_code == 0
finally:
out, _, return_code = capture(down_cmd)
assert return_code == 0
def test_down_with_vols(podman_compose_path, test_path):
up_cmd = [
"coverage",
"run",
podman_compose_path,
"-f",
os.path.join(test_path, "vol", "docker-compose.yaml"),
"up",
"-d"
]
down_cmd = [
"coverage",
"run",
podman_compose_path,
"-f",
os.path.join(test_path, "vol", "docker-compose.yaml"),
"down",
"--volumes"
]
try:
out, _, return_code = capture(["podman", "volume", "create", "my-app-data"])
assert return_code == 0
out, _, return_code = capture(["podman", "volume", "create", "actual-name-of-volume"])
assert return_code == 0
out, _, return_code = capture(up_cmd)
assert return_code == 0
capture(["podman", "inspect", "volume", ""])
finally:
out, _, return_code = capture(down_cmd)
capture(["podman", "volume", "rm", "my-app-data"])
capture(["podman", "volume", "rm", "actual-name-of-volume"])
assert return_code == 0
def test_down_with_orphans(podman_compose_path, test_path):
container_id, _ , return_code = capture(["podman", "run", "--rm", "-d", "busybox", "/bin/busybox", "httpd", "-f", "-h", "/etc/", "-p", "8000"])
down_cmd = [
"coverage",
"run",
podman_compose_path,
"-f",
os.path.join(test_path, "ports", "docker-compose.yml"),
"down",
"--volumes",
"--remove-orphans"
]
out, _, return_code = capture(down_cmd)
assert return_code == 0
_, _, exists = capture(["podman", "container", "exists", container_id.decode("utf-8")])
assert exists == 1

View File

@ -27,7 +27,8 @@ def teardown(podman_compose_path, profile_compose_file):
yield yield
down_cmd = [ down_cmd = [
"python3", "coverage",
"run",
podman_compose_path, podman_compose_path,
"--profile", "--profile",
"profile-1", "profile-1",
@ -59,7 +60,8 @@ def teardown(podman_compose_path, profile_compose_file):
) )
def test_up(podman_compose_path, profile_compose_file, profiles, expected_services): def test_up(podman_compose_path, profile_compose_file, profiles, expected_services):
up_cmd = [ up_cmd = [
"python3", "coverage",
"run",
podman_compose_path, podman_compose_path,
"-f", "-f",
profile_compose_file, profile_compose_file,