forked from extern/podman-compose
Compare commits
9 Commits
devel
...
devel-asyn
Author | SHA1 | Date | |
---|---|---|---|
1b85ebe506 | |||
9ed05a23ef | |||
d475260951 | |||
831caa6276 | |||
9ac33392a0 | |||
c5be5bae90 | |||
c6a1c4c432 | |||
3c9628b462 | |||
38b13a34ea |
2
.coveragerc
Normal file
2
.coveragerc
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[run]
|
||||||
|
parallel=True
|
9
.github/workflows/pytest.yml
vendored
9
.github/workflows/pytest.yml
vendored
@ -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
|
||||||
|
@ -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))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
1
setup.py
1
setup.py
@ -45,6 +45,7 @@ setup(
|
|||||||
"black",
|
"black",
|
||||||
"pylint",
|
"pylint",
|
||||||
"pre-commit",
|
"pre-commit",
|
||||||
|
"coverage"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
# test_suite='tests',
|
# test_suite='tests',
|
||||||
|
@ -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
|
@ -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'
|
||||||
```
|
```
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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():
|
||||||
|
@ -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)
|
||||||
|
@ -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")),
|
||||||
|
180
tests/test_podman_compose_tests.py
Normal file
180
tests/test_podman_compose_tests.py
Normal 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
|
||||||
|
|
@ -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,
|
||||||
|
Reference in New Issue
Block a user