Compare commits

..

39 Commits

Author SHA1 Message Date
ambiamber
bce40c2db3 Change "an key-value" to "a key-value" 2023-08-08 18:05:58 +03:00
Kian-Meng Ang
78f8cad7c4 Fix typos
Found via `codespell -L poped`
2023-08-08 18:05:25 +03:00
Mohammed Tayeh
7942a540cd fix styling errors
Signed-off-by: Mohammed Tayeh <m.tayeh94@gmail.com>
2023-08-08 18:05:02 +03:00
Mohammed Tayeh
cb9cf6002f add stats command
Signed-off-by: Mohammed Tayeh <info@tayeh.me>
2023-08-08 18:05:02 +03:00
Evedel
06587c1dca rm redundant tests
Signed-off-by: Evedel <svbiriukov@gmail.com>
2023-08-02 14:19:15 +03:00
Evedel
bc9168b039 add no-normalize flag
Signed-off-by: Evedel <svbiriukov@gmail.com>
2023-08-02 14:19:15 +03:00
Sergei Biriukov
57c527c2c9 add edits from review
Signed-off-by: Sergei Biriukov <svbiriukov@gmail.com>
2023-08-02 14:19:15 +03:00
Sergei Biriukov
d1f5ac9edc convert build context path to absolute during final normalisation
Signed-off-by: Sergei Biriukov <svbiriukov@gmail.com>
2023-08-02 14:19:15 +03:00
Natanael Arndt
0164c1db56 Simplify the fix using or.
Signed-off-by: Natanael Arndt <arndtn@gmail.com>
2023-07-26 17:32:14 +03:00
Natanael Arndt
e5cdce4e7d default to an empty dict for the from service if the service is None
Signed-off-by: Natanael Arndt <arndtn@gmail.com>
2023-07-26 17:32:14 +03:00
Natanael Arndt
280f1770bf Add a test to extend using an empty service (placeholder)
Signed-off-by: Natanael Arndt <arndtn@gmail.com>
2023-07-26 17:32:14 +03:00
Muayyad alsadi
f75d12af21 broken in py 3.7 2023-07-18 13:23:55 +03:00
Hedayat Vatankhah
5454c3ad0f Add 'links' aliases to container aliases
Signed-off-by: Hedayat Vatankhah <hedayat.fwd@gmail.com>
2023-07-18 13:14:57 +03:00
Hedayat Vatankhah
901adf47d0 Use defined environment variables in the image build process
Build images with service environment variables defined so that they can be
used in the build process

Signed-off-by: Hedayat Vatankhah <hedayat.fwd@gmail.com>
2023-07-18 13:13:33 +03:00
Mahmoud Abduljawad
bf07e91163 Implement include from compose-spec
Signed-off-by: Mahmoud Abduljawad <mahmoud@masaar.com>
2023-07-18 13:05:46 +03:00
Muayyad alsadi
3890eacf57 Merge branch 'Evedel-allow-config-to-merge-strings-and-dicts-in-build' into devel 2023-05-29 11:47:41 +03:00
Sergei Biriukov
cfd24cc2e8 Merge branch 'devel' into allow-config-to-merge-strings-and-dicts-in-build
Signed-off-by: Sergei Biriukov <svbiriukov@gmail.com>
2023-05-06 18:14:44 +10:00
Sergei Biriukov
79bfad103c move logic from rec_merge to normalize_service
Signed-off-by: Sergei Biriukov <svbiriukov@gmail.com>
2023-05-06 10:42:44 +03:00
Sergei Biriukov
d1509468c3 allow empty list to be a command/entrypoint
Signed-off-by: Sergei Biriukov <svbiriukov@gmail.com>
2023-05-06 10:42:44 +03:00
Sergei Biriukov
9011e9faa1 add tests, dry up code, use shlex.split instead of str.split
Signed-off-by: Sergei Biriukov <svbiriukov@gmail.com>
2023-05-06 10:42:44 +03:00
Sergei Biriukov
517aeba330 Allow config to merge strings and lists in command and entrypoint
Signed-off-by: Sergei Biriukov <svbiriukov@gmail.com>
2023-05-06 10:42:44 +03:00
Sergei Biriukov
85d5d5dcc9 move logic from rec_merge to normalize_service
Signed-off-by: Sergei Biriukov <svbiriukov@gmail.com>
2023-05-06 15:17:54 +10:00
Cleber Rosa
1ffd24dcf9 Python version support: sync verified and advertised versions
There are differences with regards to the versions of Python that are
verified (somehow) through Pylint, and the ones that are advertised.

Given that there's no pinning of Pylint versions, it shouldn't be
possible to use it on Python versions such as 3.5 and 3.6 (latest
Pylint doesn't support those).  With that, let's cover all the
currently supported Python versions.

Signed-off-by: Cleber Rosa <crosa@redhat.com>
2023-05-06 00:28:13 +03:00
Sergei Biriukov
8c66b1cda7 add test case for when build is a complex dictionary
Signed-off-by: Sergei Biriukov <svbiriukov@gmail.com>
2023-04-30 15:37:52 +10:00
Sergei Biriukov
a0005db474 add code implementing build value merge
Signed-off-by: Sergei Biriukov <svbiriukov@gmail.com>
2023-04-29 13:52:19 +10:00
Sergei Biriukov
221cf14501 add tests for build value merge
Signed-off-by: Sergei Biriukov <svbiriukov@gmail.com>
2023-04-29 13:17:43 +10:00
Sergei Biriukov
a61945b516 fix format
Signed-off-by: Sergei Biriukov <svbiriukov@gmail.com>
2023-04-21 20:47:53 +03:00
Sergei Biriukov
6b6330c587 add build subcommand and --build arg to compose_run
Signed-off-by: Sergei Biriukov <svbiriukov@gmail.com>
2023-04-21 20:47:53 +03:00
BugFest
5d279c4948 Build-fail test example
Signed-off-by: BugFest <bugfest.dev@pm.me>
2023-04-12 22:25:33 +03:00
BugFest
5a3bdbf89b Exit code managed at PodmanCompose.run()
Signed-off-by: BugFest <bugfest.dev@pm.me>
2023-04-12 22:25:33 +03:00
BugFest
1eb166445b Linting fixes
Signed-off-by: BugFest <bugfest.dev@pm.me>
2023-04-12 22:25:33 +03:00
BugFest
82182b7bc6 Finish execution in compose_build only on command=build calls
Signed-off-by: BugFest <bugfest.dev@pm.me>
2023-04-12 22:25:33 +03:00
BugFest
3f4618866b Update project-1.env 2023-04-10 14:14:14 +03:00
BugFest
91bc6ebdb4 Keep chdir after loading env file
Signed-off-by: BugFest <bugfest.dev@pm.me>
2023-04-10 14:14:14 +03:00
BugFest
59a59c1a3a Fixes #636: env-file shall be resolved relative to the CWD
Signed-off-by: BugFest <bugfest.dev@pm.me>
2023-04-10 14:14:14 +03:00
BugFest
620f5d7473 pre-commit black config: run in check only mode
Signed-off-by: BugFest <bugfest.dev@pm.me>
2023-04-10 14:13:00 +03:00
BugFest
6f902faed0 Fix linting issues
Signed-off-by: BugFest <bugfest.dev@pm.me>
2023-04-10 14:12:32 +03:00
Muayyad Alsadi
ccdf01e9b0 Revert "Use SELinux mount flag for secrets"
This reverts commit 874192568f.
2023-04-10 12:26:53 +03:00
Muayyad Alsadi
e6b1eabe4c Revert "Use more lenient SELinux mount flag for secrets"
This reverts commit 75de39c239.
2023-04-10 12:26:53 +03:00
23 changed files with 990 additions and 67 deletions

View File

@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}

View File

@ -9,6 +9,9 @@ repos:
# https://pre-commit.com/#top_level-default_language_version
language_version: python3.10
types: [python]
args: [
"--check", # Don't apply changes automatically
]
- repo: https://github.com/pycqa/flake8
rev: 6.0.0
hooks:

View File

@ -1,7 +1,7 @@
---
- name: Manage AWX Container Images
block:
- name: Export Docker awx image if it isnt local and there isnt a registry defined
- name: Export Docker awx image if it isn't local and there isn't a registry defined
docker_image:
name: "{{ awx_image }}"
tag: "{{ awx_version }}"

View File

@ -1,4 +1,4 @@
#! /usr/bin/python3
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# https://docs.docker.com/compose/compose-file/#service-configuration-reference
@ -220,7 +220,7 @@ def fix_mount_dict(compose, mount_dict, proj_name, srv_name):
vol = (vols.get(source, None) or {}) if source else {}
name = vol.get("name", None)
mount_dict["_vol"] = vol
# handle anonymouse or implied volume
# handle anonymous or implied volume
if not source:
# missing source
vol["name"] = "_".join(
@ -591,7 +591,7 @@ def get_secret_args(compose, cnt, secret):
# docker-compose does not support external secrets outside of swarm mode.
# However accessing these via podman is trivial
# since these commands are directly translated to
# podman-create commands, albiet we can only support a 1:1 mapping
# podman-create commands, albeit we can only support a 1:1 mapping
# at the moment
if declared_secret.get("external", False) or declared_secret.get("name", None):
secret_opts += f",uid={uid}" if uid else ""
@ -618,7 +618,7 @@ def get_secret_args(compose, cnt, secret):
return ["--secret", "{}{}".format(secret_name, secret_opts)]
raise ValueError(
'ERROR: unparseable secret: "{}", service: "{}"'.format(
'ERROR: unparsable secret: "{}", service: "{}"'.format(
secret_name, cnt["_service"]
)
)
@ -758,7 +758,7 @@ def assert_cnt_nets(compose, cnt):
driver_opts = net_desc.get("driver_opts", None) or {}
for key, value in driver_opts.items():
args.extend(("--opt", f"{key}={value}"))
ipam = (net_desc.get("ipam", None) or {})
ipam = net_desc.get("ipam", None) or {}
ipam_driver = ipam.get("driver", None)
if ipam_driver:
args.extend(("--ipam-driver", ipam_driver))
@ -823,16 +823,18 @@ def get_net_args(compose, cnt):
ip = None
ip6 = None
ip_assignments = 0
if cnt.get("_aliases", None):
aliases.extend(cnt.get("_aliases", None))
if cnt_nets and is_dict(cnt_nets):
prioritized_cnt_nets = []
# cnt_nets is {net_key: net_value, ...}
for net_key, net_value in cnt_nets.items():
net_value = net_value or {}
aliases.extend(norm_as_list(net_value.get("aliases", None)))
if net_value.get("ipv4_address", None) != None:
ip_assignments = ip_assignments + 1
if net_value.get("ipv6_address", None) != None:
ip_assignments = ip_assignments + 1
if net_value.get("ipv4_address", None) is not None:
ip_assignments = ip_assignments + 1
if net_value.get("ipv6_address", None) is not None:
ip_assignments = ip_assignments + 1
if not ip:
ip = net_value.get("ipv4_address", None)
@ -862,33 +864,37 @@ def get_net_args(compose, cnt):
net_names_str = ",".join(net_names)
if ip_assignments > 1:
multipleNets = cnt.get("networks", None)
multipleNetNames = multipleNets.keys()
multiple_nets = cnt.get("networks", None)
multiple_net_names = multiple_nets.keys()
for net_ in multipleNetNames:
net_desc = nets[net_] or {}
is_ext = net_desc.get("external", None)
ext_desc = is_ext if is_dict(is_ext) else {}
default_net_name = net_ if is_ext else f"{proj_name}_{net_}"
net_name = (
ext_desc.get("name", None) or net_desc.get("name", None) or default_net_name
)
for net_ in multiple_net_names:
net_desc = nets[net_] or {}
is_ext = net_desc.get("external", None)
ext_desc = is_ext if is_dict(is_ext) else {}
default_net_name = net_ if is_ext else f"{proj_name}_{net_}"
net_name = (
ext_desc.get("name", None)
or net_desc.get("name", None)
or default_net_name
)
ipv4 = multipleNets[net_].get("ipv4_address",None)
ipv6 = multipleNets[net_].get("ipv6_address",None)
if ipv4 is not None and ipv6 is not None:
net_args.extend(["--network", f"{net_name}:ip={ipv4},ip={ipv6}"])
elif ipv4 is None and ipv6 is not None:
net_args.extend(["--network", f"{net_name}:ip={ipv6}"])
elif ipv6 is None and ipv4 is not None:
net_args.extend(["--network", f"{net_name}:ip={ipv4}"])
ipv4 = multiple_nets[net_].get("ipv4_address", None)
ipv6 = multiple_nets[net_].get("ipv6_address", None)
if ipv4 is not None and ipv6 is not None:
net_args.extend(["--network", f"{net_name}:ip={ipv4},ip={ipv6}"])
elif ipv4 is None and ipv6 is not None:
net_args.extend(["--network", f"{net_name}:ip={ipv6}"])
elif ipv6 is None and ipv4 is not None:
net_args.extend(["--network", f"{net_name}:ip={ipv4}"])
else:
if is_bridge:
net_args.extend(["--net", net_names_str, "--network-alias", ",".join(aliases)])
if ip:
net_args.append(f"--ip={ip}")
if ip6:
net_args.append(f"--ip6={ip6}")
if is_bridge:
net_args.extend(
["--net", net_names_str, "--network-alias", ",".join(aliases)]
)
if ip:
net_args.append(f"--ip={ip}")
if ip6:
net_args.append(f"--ip6={ip6}")
return net_args
@ -1024,7 +1030,7 @@ def container_to_args(compose, cnt, detached=True):
# WIP: healthchecks are still work in progress
healthcheck = cnt.get("healthcheck", None) or {}
if not is_dict(healthcheck):
raise ValueError("'healthcheck' must be an key-value mapping")
raise ValueError("'healthcheck' must be a key-value mapping")
healthcheck_disable = healthcheck.get("disable", False)
healthcheck_test = healthcheck.get("test", None)
if healthcheck_disable:
@ -1134,6 +1140,12 @@ def flat_deps(services, with_extends=False):
if not is_list(links_ls):
links_ls = [links_ls]
deps.update([(c.split(":")[0] if ":" in c else c) for c in links_ls])
for c in links_ls:
if ":" in c:
dep_name, dep_alias = c.split(":")
if not "_aliases" in services[dep_name]:
services[dep_name]["_aliases"] = set()
services[dep_name]["_aliases"].add(dep_alias)
for name, srv in services.items():
rec_deps(services, name)
@ -1233,12 +1245,13 @@ class Podman:
def normalize_service(service, sub_dir=""):
# make `build.context` relative to sub_dir
# TODO: should we make volume and secret relative too?
if "build" in service:
build = service["build"]
if is_str(build):
service["build"] = {"context": build}
if sub_dir and "build" in service:
build = service["build"]
context = build if is_str(build) else build.get("context", None)
context = context or ""
context = build.get("context", None) or ""
if context or sub_dir:
if context.startswith("./"):
context = context[2:]
@ -1247,10 +1260,11 @@ def normalize_service(service, sub_dir=""):
context = context.rstrip("/")
if not context:
context = "."
if is_str(build):
service["build"] = context
else:
service["build"]["context"] = context
service["build"]["context"] = context
for key in ("command", "entrypoint"):
if key in service:
if is_str(service[key]):
service[key] = shlex.split(service[key])
for key in ("env_file", "security_opt", "volumes"):
if key not in service:
continue
@ -1283,6 +1297,30 @@ def normalize(compose):
return compose
def normalize_service_final(service: dict, project_dir: str) -> dict:
if "build" in service:
build = service["build"]
context = build if is_str(build) else build.get("context", ".")
context = os.path.normpath(os.path.join(project_dir, context))
dockerfile = (
"Dockerfile"
if is_str(build)
else service["build"].get("dockerfile", "Dockerfile")
)
if not is_dict(service["build"]):
service["build"] = {}
service["build"]["dockerfile"] = dockerfile
service["build"]["context"] = context
return service
def normalize_final(compose: dict, project_dir: str) -> dict:
services = compose.get("services", None) or {}
for service in services.values():
normalize_service_final(service, project_dir)
return compose
def clone(value):
return value.copy() if is_list(value) or is_dict(value) else value
@ -1303,14 +1341,14 @@ def rec_merge_one(target, source):
if key not in source:
continue
value2 = source[key]
if key == "command":
if key in ("command", "entrypoint"):
target[key] = clone(value2)
continue
if not isinstance(value2, type(value)):
value_type = type(value)
value2_type = type(value2)
raise ValueError(
f"can't merge value of {key} of type {value_type} and {value2_type}"
f"can't merge value of [{key}] of type {value_type} and {value2_type}"
)
if is_list(value2):
if key == "volumes":
@ -1361,7 +1399,7 @@ def resolve_extends(services, service_names, environ):
content = content["services"]
subdirectory = os.path.dirname(filename)
content = rec_subs(content, environ)
from_service = content.get(from_service_name, {})
from_service = content.get(from_service_name, {}) or {}
normalize_service(from_service, subdirectory)
else:
from_service = services.get(from_service_name, {}).copy()
@ -1483,7 +1521,9 @@ class PodmanCompose:
if compose_required:
self._parse_compose_file()
cmd = self.commands[cmd_name]
cmd(self, args)
retcode = cmd(self, args)
if isinstance(retcode, int):
sys.exit(retcode)
def _parse_compose_file(self):
args = self.global_args
@ -1522,11 +1562,16 @@ class PodmanCompose:
dirname = os.path.realpath(os.path.dirname(filename))
dir_basename = os.path.basename(dirname)
self.dirname = dirname
# env-file is relative to the CWD
dotenv_dict = {}
if args.env_file:
dotenv_path = os.path.realpath(args.env_file)
dotenv_dict = dotenv_to_dict(dotenv_path)
# TODO: remove next line
os.chdir(dirname)
dotenv_path = os.path.join(dirname, args.env_file)
dotenv_dict = dotenv_to_dict(dotenv_path)
os.environ.update(
{
key: value
@ -1546,7 +1591,15 @@ class PodmanCompose:
}
)
compose = {}
for filename in files:
# Iterate over files primitively to allow appending to files in-loop
files_iter = iter(files)
while True:
try:
filename = next(files_iter)
except StopIteration:
break
with open(filename, "r", encoding="utf-8") as f:
content = yaml.safe_load(f)
# log(filename, json.dumps(content, indent = 2))
@ -1560,10 +1613,22 @@ class PodmanCompose:
# log(filename, json.dumps(content, indent = 2))
content = rec_subs(content, self.environ)
rec_merge(compose, content)
# If `include` is used, append included files to files
include = compose.get("include", None)
if include:
files.append(*include)
# As compose obj is updated and tested with every loop, not deleting `include`
# from it, results in it being tested again and again, original values for
# `include` be appended to `files`, and, included files be processed for ever.
# Solution is to remove 'include' key from compose obj. This doesn't break
# having `include` present and correctly processed in included files
del compose["include"]
resolved_services = self._resolve_profiles(
compose.get("services", {}), set(args.profile)
)
compose["services"] = resolved_services
if not getattr(args, "no_normalize", None):
compose = normalize_final(compose, self.dirname)
self.merged_yaml = yaml.safe_dump(compose)
merged_json_b = json.dumps(compose, separators=(",", ":")).encode("utf-8")
self.yaml_hash = hashlib.sha256(merged_json_b).hexdigest()
@ -1578,7 +1643,8 @@ class PodmanCompose:
if project_name is None:
# More strict then actually needed for simplicity: podman requires [a-zA-Z0-9][a-zA-Z0-9_.-]*
project_name = (
self.environ.get("COMPOSE_PROJECT_NAME", None) or dir_basename.lower()
self.environ.get("COMPOSE_PROJECT_NAME", None)
or dir_basename.lower()
)
project_name = norm_re.sub("", project_name)
if not project_name:
@ -2023,7 +2089,7 @@ def compose_push(compose, args):
def build_one(compose, args, cnt):
if "build" not in cnt:
return
return None
if getattr(args, "if_not_exists", None):
try:
img_id = compose.podman.output(
@ -2032,7 +2098,7 @@ def build_one(compose, args, cnt):
except subprocess.CalledProcessError:
img_id = None
if img_id:
return
return None
build_desc = cnt["build"]
if not hasattr(build_desc, "items"):
build_desc = {"context": build_desc}
@ -2069,6 +2135,10 @@ def build_one(compose, args, cnt):
build_args.append("--pull-always")
elif getattr(args, "pull", None):
build_args.append("--pull")
env = dict(cnt.get("environment", {}))
for name, value in env.items():
build_args += ["--env", f"{name}" if value is None else f"{name}={value}"]
args_list = norm_as_list(build_desc.get("args", {}))
for build_arg in args_list + args.build_arg:
build_args.extend(
@ -2084,17 +2154,31 @@ def build_one(compose, args, cnt):
@cmd_run(podman_compose, "build", "build stack images")
def compose_build(compose, 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:
container_names_by_service = compose.container_names_by_service
compose.assert_services(args.services)
for service in args.services:
cnt = compose.container_by_name[container_names_by_service[service][0]]
p = build_one(compose, args, cnt)
exit(p.returncode)
status = parse_return_code(p, status)
if status != 0:
return status
else:
for cnt in compose.containers:
p = build_one(compose, args, cnt)
exit(p.returncode)
status = parse_return_code(p, status)
if status != 0:
return status
return status
def create_pods(compose, args): # pylint: disable=unused-argument
@ -2356,6 +2440,15 @@ def compose_run(compose, args):
)
)
compose.commands["up"](compose, up_args)
build_args = argparse.Namespace(
services=[args.service],
if_not_exists=(not args.build),
build_arg=[],
**args.__dict__,
)
compose.commands["build"](compose, build_args)
# adjust one-off container options
name0 = "{}_{}_tmp{}".format(
compose.project_name, args.service, random.randrange(0, 65536)
@ -2598,6 +2691,37 @@ def compose_kill(compose, args):
compose.podman.run([], "kill", podman_args)
@cmd_run(
podman_compose,
"stats",
"Display percentage of CPU, memory, network I/O, block I/O and PIDs for services.",
)
def compose_stats(compose, args):
container_names_by_service = compose.container_names_by_service
if not args.services:
args.services = container_names_by_service.keys()
targets = []
podman_args = []
if args.interval:
podman_args.extend(["--interval", args.interval])
if args.format:
podman_args.extend(["--format", args.format])
if args.no_reset:
podman_args.append("--no-reset")
if args.no_stream:
podman_args.append("--no-stream")
for service in args.services:
targets.extend(container_names_by_service[service])
for target in targets:
podman_args.append(target)
try:
compose.podman.run([], "stats", podman_args)
except KeyboardInterrupt:
pass
###################
# command arguments parsing
###################
@ -2725,6 +2849,9 @@ def compose_down_parse(parser):
@cmd_parse(podman_compose, "run")
def compose_run_parse(parser):
parser.add_argument(
"--build", action="store_true", help="Build images before starting containers."
)
parser.add_argument(
"-d",
"--detach",
@ -2987,6 +3114,9 @@ def compose_build_parse(parser):
@cmd_parse(podman_compose, "config")
def compose_config_parse(parser):
parser.add_argument(
"--no-normalize", help="Don't normalize compose model.", action="store_true"
)
parser.add_argument(
"--services", help="Print the service names, one per line.", action="store_true"
)
@ -3042,6 +3172,35 @@ def compose_kill_parse(parser):
)
@cmd_parse(podman_compose, ["stats"])
def compose_stats_parse(parser):
parser.add_argument(
"services", metavar="services", nargs="*", default=None, help="service names"
)
parser.add_argument(
"-i",
"--interval",
type=int,
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(
"--no-reset",
help="Disable resetting the screen between intervals",
action="store_true",
)
parser.add_argument(
"--no-stream",
help="Disable streaming stats and only pull the first result",
action="store_true",
)
def main():
podman_compose.run()

View File

@ -0,0 +1,168 @@
import copy
import os
import argparse
import yaml
from podman_compose import normalize_service, PodmanCompose
test_cases_simple = [
({"test": "test"}, {"test": "test"}),
({"build": "."}, {"build": {"context": "."}}),
({"build": "./dir-1"}, {"build": {"context": "./dir-1"}}),
({"build": {"context": "./dir-1"}}, {"build": {"context": "./dir-1"}}),
(
{"build": {"dockerfile": "dockerfile-1"}},
{"build": {"dockerfile": "dockerfile-1"}},
),
(
{"build": {"context": "./dir-1", "dockerfile": "dockerfile-1"}},
{"build": {"context": "./dir-1", "dockerfile": "dockerfile-1"}},
),
]
def test_normalize_service_simple():
for test_case, expected in copy.deepcopy(test_cases_simple):
test_original = copy.deepcopy(test_case)
test_case = normalize_service(test_case)
test_result = expected == test_case
if not test_result:
print("test: ", test_original)
print("expected: ", expected)
print("actual: ", test_case)
assert test_result
test_cases_sub_dir = [
({"test": "test"}, {"test": "test"}),
({"build": "."}, {"build": {"context": "./sub_dir/."}}),
({"build": "./dir-1"}, {"build": {"context": "./sub_dir/dir-1"}}),
({"build": {"context": "./dir-1"}}, {"build": {"context": "./sub_dir/dir-1"}}),
(
{"build": {"dockerfile": "dockerfile-1"}},
{"build": {"context": "./sub_dir", "dockerfile": "dockerfile-1"}},
),
(
{"build": {"context": "./dir-1", "dockerfile": "dockerfile-1"}},
{"build": {"context": "./sub_dir/dir-1", "dockerfile": "dockerfile-1"}},
),
]
def test_normalize_service_with_sub_dir():
for test_case, expected in copy.deepcopy(test_cases_sub_dir):
test_original = copy.deepcopy(test_case)
test_case = normalize_service(test_case, sub_dir="./sub_dir")
test_result = expected == test_case
if not test_result:
print("test: ", test_original)
print("expected: ", expected)
print("actual: ", test_case)
assert test_result
test_cases_merges = [
({}, {}, {}),
({}, {"test": "test"}, {"test": "test"}),
({"test": "test"}, {}, {"test": "test"}),
({"test": "test-1"}, {"test": "test-2"}, {"test": "test-2"}),
({}, {"build": "."}, {"build": {"context": "."}}),
({"build": "."}, {}, {"build": {"context": "."}}),
({"build": "./dir-1"}, {"build": "./dir-2"}, {"build": {"context": "./dir-2"}}),
({}, {"build": {"context": "./dir-1"}}, {"build": {"context": "./dir-1"}}),
({"build": {"context": "./dir-1"}}, {}, {"build": {"context": "./dir-1"}}),
(
{"build": {"context": "./dir-1"}},
{"build": {"context": "./dir-2"}},
{"build": {"context": "./dir-2"}},
),
(
{},
{"build": {"dockerfile": "dockerfile-1"}},
{"build": {"dockerfile": "dockerfile-1"}},
),
(
{"build": {"dockerfile": "dockerfile-1"}},
{},
{"build": {"dockerfile": "dockerfile-1"}},
),
(
{"build": {"dockerfile": "./dockerfile-1"}},
{"build": {"dockerfile": "./dockerfile-2"}},
{"build": {"dockerfile": "./dockerfile-2"}},
),
(
{"build": {"dockerfile": "./dockerfile-1"}},
{"build": {"context": "./dir-2"}},
{"build": {"dockerfile": "./dockerfile-1", "context": "./dir-2"}},
),
(
{"build": {"dockerfile": "./dockerfile-1", "context": "./dir-1"}},
{"build": {"dockerfile": "./dockerfile-2", "context": "./dir-2"}},
{"build": {"dockerfile": "./dockerfile-2", "context": "./dir-2"}},
),
(
{"build": {"dockerfile": "./dockerfile-1"}},
{"build": {"dockerfile": "./dockerfile-2", "args": ["ENV1=1"]}},
{"build": {"dockerfile": "./dockerfile-2", "args": ["ENV1=1"]}},
),
(
{"build": {"dockerfile": "./dockerfile-2", "args": ["ENV1=1"]}},
{"build": {"dockerfile": "./dockerfile-1"}},
{"build": {"dockerfile": "./dockerfile-1", "args": ["ENV1=1"]}},
),
(
{"build": {"dockerfile": "./dockerfile-2", "args": ["ENV1=1"]}},
{"build": {"dockerfile": "./dockerfile-1", "args": ["ENV2=2"]}},
{"build": {"dockerfile": "./dockerfile-1", "args": ["ENV1=1", "ENV2=2"]}},
),
]
def test__parse_compose_file_when_multiple_composes() -> None:
for test_input, test_override, expected_result in copy.deepcopy(test_cases_merges):
compose_test_1 = {"services": {"test-service": test_input}}
compose_test_2 = {"services": {"test-service": test_override}}
dump_yaml(compose_test_1, "test-compose-1.yaml")
dump_yaml(compose_test_2, "test-compose-2.yaml")
podman_compose = PodmanCompose()
set_args(podman_compose, ["test-compose-1.yaml", "test-compose-2.yaml"])
podman_compose._parse_compose_file() # pylint: disable=protected-access
actual_compose = {}
if podman_compose.services:
podman_compose.services["test-service"].pop("_deps")
actual_compose = podman_compose.services["test-service"]
if actual_compose != expected_result:
print("compose: ", test_input)
print("override: ", test_override)
print("expected: ", expected_result)
print("actual: ", actual_compose)
compose_expected = expected_result
assert compose_expected == actual_compose
def set_args(podman_compose: PodmanCompose, file_names: list[str]) -> None:
podman_compose.global_args = argparse.Namespace()
podman_compose.global_args.file = file_names
podman_compose.global_args.project_name = None
podman_compose.global_args.env_file = None
podman_compose.global_args.profile = []
podman_compose.global_args.in_pod = True
podman_compose.global_args.no_normalize = True
def dump_yaml(compose: dict, name: str) -> None:
with open(name, "w", encoding="utf-8") as outfile:
yaml.safe_dump(compose, outfile, default_flow_style=False)
def test_clean_test_yamls() -> None:
test_files = ["test-compose-1.yaml", "test-compose-2.yaml"]
for file in test_files:
if os.path.exists(file):
os.remove(file)

View File

@ -0,0 +1,122 @@
import copy
import os
import argparse
import yaml
from podman_compose import normalize_service, PodmanCompose
test_keys = ["command", "entrypoint"]
test_cases_normalise_pre_merge = [
({"$$$": []}, {"$$$": []}),
({"$$$": ["sh"]}, {"$$$": ["sh"]}),
({"$$$": ["sh", "-c", "date"]}, {"$$$": ["sh", "-c", "date"]}),
({"$$$": "sh"}, {"$$$": ["sh"]}),
({"$$$": "sleep infinity"}, {"$$$": ["sleep", "infinity"]}),
(
{"$$$": "bash -c 'sleep infinity'"},
{"$$$": ["bash", "-c", "sleep infinity"]},
),
]
test_cases_merges = [
({}, {"$$$": []}, {"$$$": []}),
({"$$$": []}, {}, {"$$$": []}),
({"$$$": []}, {"$$$": "sh-2"}, {"$$$": ["sh-2"]}),
({"$$$": "sh-2"}, {"$$$": []}, {"$$$": []}),
({}, {"$$$": "sh"}, {"$$$": ["sh"]}),
({"$$$": "sh"}, {}, {"$$$": ["sh"]}),
({"$$$": "sh-1"}, {"$$$": "sh-2"}, {"$$$": ["sh-2"]}),
({"$$$": ["sh-1"]}, {"$$$": "sh-2"}, {"$$$": ["sh-2"]}),
({"$$$": "sh-1"}, {"$$$": ["sh-2"]}, {"$$$": ["sh-2"]}),
({"$$$": "sh-1"}, {"$$$": ["sh-2", "sh-3"]}, {"$$$": ["sh-2", "sh-3"]}),
({"$$$": ["sh-1"]}, {"$$$": ["sh-2", "sh-3"]}, {"$$$": ["sh-2", "sh-3"]}),
({"$$$": ["sh-1", "sh-2"]}, {"$$$": ["sh-3", "sh-4"]}, {"$$$": ["sh-3", "sh-4"]}),
({}, {"$$$": ["sh-3", "sh 4"]}, {"$$$": ["sh-3", "sh 4"]}),
({"$$$": "sleep infinity"}, {"$$$": "sh"}, {"$$$": ["sh"]}),
({"$$$": "sh"}, {"$$$": "sleep infinity"}, {"$$$": ["sleep", "infinity"]}),
(
{},
{"$$$": "bash -c 'sleep infinity'"},
{"$$$": ["bash", "-c", "sleep infinity"]},
),
]
def template_to_expression(base, override, expected, key):
base_copy = copy.deepcopy(base)
override_copy = copy.deepcopy(override)
expected_copy = copy.deepcopy(expected)
expected_copy[key] = expected_copy.pop("$$$")
if "$$$" in base:
base_copy[key] = base_copy.pop("$$$")
if "$$$" in override:
override_copy[key] = override_copy.pop("$$$")
return base_copy, override_copy, expected_copy
def test_normalize_service():
for test_input_template, expected_template in test_cases_normalise_pre_merge:
for key in test_keys:
test_input, _, expected = template_to_expression(
test_input_template, {}, expected_template, key
)
test_input = normalize_service(test_input)
test_result = expected == test_input
if not test_result:
print("base_template: ", test_input_template)
print("expected: ", expected)
print("actual: ", test_input)
assert test_result
def test__parse_compose_file_when_multiple_composes() -> None:
for base_template, override_template, expected_template in copy.deepcopy(
test_cases_merges
):
for key in test_keys:
base, override, expected = template_to_expression(
base_template, override_template, expected_template, key
)
compose_test_1 = {"services": {"test-service": base}}
compose_test_2 = {"services": {"test-service": override}}
dump_yaml(compose_test_1, "test-compose-1.yaml")
dump_yaml(compose_test_2, "test-compose-2.yaml")
podman_compose = PodmanCompose()
set_args(podman_compose, ["test-compose-1.yaml", "test-compose-2.yaml"])
podman_compose._parse_compose_file() # pylint: disable=protected-access
actual = {}
if podman_compose.services:
podman_compose.services["test-service"].pop("_deps")
actual = podman_compose.services["test-service"]
if actual != expected:
print("compose: ", base)
print("override: ", override)
print("result: ", expected)
assert expected == actual
def set_args(podman_compose: PodmanCompose, file_names: list[str]) -> None:
podman_compose.global_args = argparse.Namespace()
podman_compose.global_args.file = file_names
podman_compose.global_args.project_name = None
podman_compose.global_args.env_file = None
podman_compose.global_args.profile = []
podman_compose.global_args.in_pod = True
podman_compose.global_args.no_normalize = None
def dump_yaml(compose: dict, name: str) -> None:
with open(name, "w", encoding="utf-8") as outfile:
yaml.safe_dump(compose, outfile, default_flow_style=False)
def test_clean_test_yamls() -> None:
test_files = ["test-compose-1.yaml", "test-compose-2.yaml"]
for file in test_files:
if os.path.exists(file):
os.remove(file)

View File

@ -0,0 +1,298 @@
# pylint: disable=protected-access
import argparse
import copy
import os
import yaml
from podman_compose import (
normalize_service,
normalize,
normalize_final,
normalize_service_final,
PodmanCompose,
)
cwd = os.path.abspath(".")
test_cases_simple_normalization = [
({"image": "test-image"}, {"image": "test-image"}),
(
{"build": "."},
{
"build": {"context": cwd, "dockerfile": "Dockerfile"},
},
),
(
{"build": "../relative"},
{
"build": {
"context": os.path.normpath(os.path.join(cwd, "../relative")),
"dockerfile": "Dockerfile",
},
},
),
(
{"build": "./relative"},
{
"build": {
"context": os.path.normpath(os.path.join(cwd, "./relative")),
"dockerfile": "Dockerfile",
},
},
),
(
{"build": "/workspace/absolute"},
{
"build": {
"context": "/workspace/absolute",
"dockerfile": "Dockerfile",
},
},
),
(
{
"build": {
"dockerfile": "Dockerfile",
},
},
{
"build": {
"context": cwd,
"dockerfile": "Dockerfile",
},
},
),
(
{
"build": {
"context": ".",
},
},
{
"build": {
"context": cwd,
"dockerfile": "Dockerfile",
},
},
),
(
{
"build": {"context": "../", "dockerfile": "test-dockerfile"},
},
{
"build": {
"context": os.path.normpath(os.path.join(cwd, "../")),
"dockerfile": "test-dockerfile",
},
},
),
(
{
"build": {"context": ".", "dockerfile": "./dev/test-dockerfile"},
},
{
"build": {
"context": cwd,
"dockerfile": "./dev/test-dockerfile",
},
},
),
]
#
# [service.build] is normalised after merges
#
def test_normalize_service_final_returns_absolute_path_in_context() -> None:
project_dir = cwd
for test_input, expected_service in copy.deepcopy(test_cases_simple_normalization):
actual_service = normalize_service_final(test_input, project_dir)
assert expected_service == actual_service
def test_normalize_returns_absolute_path_in_context() -> None:
project_dir = cwd
for test_input, expected_result in copy.deepcopy(test_cases_simple_normalization):
compose_test = {"services": {"test-service": test_input}}
compose_expected = {"services": {"test-service": expected_result}}
actual_compose = normalize_final(compose_test, project_dir)
assert compose_expected == actual_compose
#
# running full parse over single compose files
#
def test__parse_compose_file_when_single_compose() -> None:
for test_input, expected_result in copy.deepcopy(test_cases_simple_normalization):
compose_test = {"services": {"test-service": test_input}}
dump_yaml(compose_test, "test-compose.yaml")
podman_compose = PodmanCompose()
set_args(podman_compose, ["test-compose.yaml"], no_normalize=None)
podman_compose._parse_compose_file()
actual_compose = {}
if podman_compose.services:
podman_compose.services["test-service"].pop("_deps")
actual_compose = podman_compose.services["test-service"]
if actual_compose != expected_result:
print("compose: ", test_input)
print("result: ", expected_result)
assert expected_result == actual_compose
test_cases_with_merges = [
(
{},
{"build": "."},
{"build": {"context": cwd, "dockerfile": "Dockerfile"}},
),
(
{"build": "."},
{},
{"build": {"context": cwd, "dockerfile": "Dockerfile"}},
),
(
{"build": "/workspace/absolute"},
{"build": "./relative"},
{
"build": {
"context": os.path.normpath(os.path.join(cwd, "./relative")),
"dockerfile": "Dockerfile",
}
},
),
(
{"build": "./relative"},
{"build": "/workspace/absolute"},
{"build": {"context": "/workspace/absolute", "dockerfile": "Dockerfile"}},
),
(
{"build": "./relative"},
{"build": "/workspace/absolute"},
{"build": {"context": "/workspace/absolute", "dockerfile": "Dockerfile"}},
),
(
{"build": {"dockerfile": "test-dockerfile"}},
{},
{"build": {"context": cwd, "dockerfile": "test-dockerfile"}},
),
(
{},
{"build": {"dockerfile": "test-dockerfile"}},
{"build": {"context": cwd, "dockerfile": "test-dockerfile"}},
),
(
{},
{"build": {"dockerfile": "test-dockerfile"}},
{"build": {"context": cwd, "dockerfile": "test-dockerfile"}},
),
(
{"build": {"dockerfile": "test-dockerfile-1"}},
{"build": {"dockerfile": "test-dockerfile-2"}},
{"build": {"context": cwd, "dockerfile": "test-dockerfile-2"}},
),
(
{"build": "/workspace/absolute"},
{"build": {"dockerfile": "test-dockerfile"}},
{"build": {"context": "/workspace/absolute", "dockerfile": "test-dockerfile"}},
),
(
{"build": {"dockerfile": "test-dockerfile"}},
{"build": "/workspace/absolute"},
{"build": {"context": "/workspace/absolute", "dockerfile": "test-dockerfile"}},
),
(
{"build": {"dockerfile": "./test-dockerfile-1"}},
{"build": {"dockerfile": "./test-dockerfile-2", "args": ["ENV1=1"]}},
{
"build": {
"context": cwd,
"dockerfile": "./test-dockerfile-2",
"args": ["ENV1=1"],
}
},
),
(
{"build": {"dockerfile": "./test-dockerfile-1", "args": ["ENV1=1"]}},
{"build": {"dockerfile": "./test-dockerfile-2"}},
{
"build": {
"context": cwd,
"dockerfile": "./test-dockerfile-2",
"args": ["ENV1=1"],
}
},
),
(
{"build": {"dockerfile": "./test-dockerfile-1", "args": ["ENV1=1"]}},
{"build": {"dockerfile": "./test-dockerfile-2", "args": ["ENV2=2"]}},
{
"build": {
"context": cwd,
"dockerfile": "./test-dockerfile-2",
"args": ["ENV1=1", "ENV2=2"],
}
},
),
]
#
# running full parse over merged
#
def test__parse_compose_file_when_multiple_composes() -> None:
for test_input, test_override, expected_result in copy.deepcopy(
test_cases_with_merges
):
compose_test_1 = {"services": {"test-service": test_input}}
compose_test_2 = {"services": {"test-service": test_override}}
dump_yaml(compose_test_1, "test-compose-1.yaml")
dump_yaml(compose_test_2, "test-compose-2.yaml")
podman_compose = PodmanCompose()
set_args(
podman_compose,
["test-compose-1.yaml", "test-compose-2.yaml"],
no_normalize=None,
)
podman_compose._parse_compose_file()
actual_compose = {}
if podman_compose.services:
podman_compose.services["test-service"].pop("_deps")
actual_compose = podman_compose.services["test-service"]
if actual_compose != expected_result:
print("compose: ", test_input)
print("override: ", test_override)
print("result: ", expected_result)
compose_expected = expected_result
assert compose_expected == actual_compose
def set_args(
podman_compose: PodmanCompose, file_names: list[str], no_normalize: bool
) -> None:
podman_compose.global_args = argparse.Namespace()
podman_compose.global_args.file = file_names
podman_compose.global_args.project_name = None
podman_compose.global_args.env_file = None
podman_compose.global_args.profile = []
podman_compose.global_args.in_pod = True
podman_compose.global_args.no_normalize = no_normalize
def dump_yaml(compose: dict, name: str) -> None:
# Path(Path.cwd()/"subdirectory").mkdir(parents=True, exist_ok=True)
with open(name, "w", encoding="utf-8") as outfile:
yaml.safe_dump(compose, outfile, default_flow_style=False)
def test_clean_test_yamls() -> None:
test_files = ["test-compose-1.yaml", "test-compose-2.yaml", "test-compose.yaml"]
for file in test_files:
if os.path.exists(file):
os.remove(file)

View File

@ -16,12 +16,11 @@ setup(
classifiers=[
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Development Status :: 3 - Alpha",

View File

@ -0,0 +1,22 @@
# Test podman-compose with build (fail scenario)
```shell
podman-compose build || echo $?
```
expected output would be something like
```
STEP 1/3: FROM busybox
STEP 2/3: RUN this_command_does_not_exist
/bin/sh: this_command_does_not_exist: not found
Error: building at STEP "RUN this_command_does_not_exist": while running runtime: exit status 127
exit code: 127
```
Expected `podman-compose` exit code:
```shell
echo $?
127
```

View File

@ -0,0 +1,3 @@
FROM busybox
RUN this_command_does_not_exist
CMD ["sh"]

View File

@ -0,0 +1,5 @@
version: "3"
services:
test:
build: ./context
image: build-fail-img

View File

@ -2,9 +2,10 @@
Defines global pytest fixtures available to all tests.
"""
import pytest
# pylint: disable=redefined-outer-name
from pathlib import Path
import os
import pytest
@pytest.fixture

View File

@ -0,0 +1,9 @@
running the following commands should always give podman-rocks-123
```
podman-compose -f project/container-compose.yaml --env-file env-files/project-1.env up
```
```
podman-compose -f $(pwd)/project/container-compose.yaml --env-file $(pwd)/env-files/project-1.env up
```

View File

@ -0,0 +1 @@
ZZVAR1=podman-rocks-123

View File

@ -0,0 +1,9 @@
services:
app:
image: busybox
command: ["/bin/busybox", "sh", "-c", "env | grep ZZ"]
tmpfs:
- /run
- /tmp
environment:
ZZVAR1: $ZZVAR1

View File

@ -0,0 +1,7 @@
services:
webapp_default:
webapp_special:
image: busybox
volumes:
- "/data"

View File

@ -0,0 +1,10 @@
version: "3"
services:
web:
image: busybox
extends:
file: common-services.yml
service: webapp_default
environment:
- DEBUG=1
cpu_shares: 5

View File

@ -0,0 +1,7 @@
version: '3.6'
services:
web:
image: busybox
command: ["/bin/busybox", "httpd", "-f", "-h", ".", "-p", "8003"]

View File

@ -0,0 +1,4 @@
version: '3.6'
include:
- docker-compose.base.yaml

View File

@ -59,3 +59,26 @@ def test_podman_compose_extends_w_file_subdir():
out, _, returncode = capture(command_check_container)
assert 0 == returncode
assert out == b""
def test_podman_compose_extends_w_empty_service():
"""
Test that podman-compose can execute podman-compose -f <file> up with extended File which
includes an empty service. (e.g. if the file is used as placeholder for more complex configurations.)
:return:
"""
main_path = Path(__file__).parent.parent
command_up = [
"python3",
str(main_path.joinpath("podman_compose.py")),
"-f",
str(
main_path.joinpath("tests", "extends_w_empty_service", "docker-compose.yml")
),
"up",
"-d",
]
_, _, returncode = capture(command_up)
assert 0 == returncode

View File

@ -3,9 +3,10 @@ test_podman_compose_config.py
Tests the podman-compose config command which is used to return defined compose services.
"""
import pytest
# pylint: disable=redefined-outer-name
import os
from test_podman_compose import capture
import pytest
@pytest.fixture
@ -23,7 +24,7 @@ def test_config_no_profiles(podman_compose_path, profile_compose_file):
"""
config_cmd = ["python3", podman_compose_path, "-f", profile_compose_file, "config"]
out, err, return_code = capture(config_cmd)
out, _, return_code = capture(config_cmd)
assert return_code == 0
string_output = out.decode("utf-8")
@ -63,7 +64,7 @@ def test_config_profiles(
config_cmd = ["python3", podman_compose_path, "-f", profile_compose_file]
config_cmd.extend(profiles)
out, err, return_code = capture(config_cmd)
out, _, return_code = capture(config_cmd)
assert return_code == 0
actual_output = out.decode("utf-8")
@ -71,7 +72,7 @@ def test_config_profiles(
assert len(expected_services) == 3
actual_services = {}
for service, expected_check in expected_services.items():
for service, _ in expected_services.items():
actual_services[service] = service in actual_output
assert expected_services == actual_services

View File

@ -0,0 +1,71 @@
from pathlib import Path
import subprocess
def capture(command):
proc = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
out, err = proc.communicate()
return out, err, proc.returncode
def test_podman_compose_include():
"""
Test that podman-compose can execute podman-compose -f <file> up with include
:return:
"""
main_path = Path(__file__).parent.parent
command_up = [
"python3",
str(main_path.joinpath("podman_compose.py")),
"-f",
str(main_path.joinpath("tests", "include", "docker-compose.yaml")),
"up",
"-d",
]
command_check_container = [
"podman",
"ps",
"-a",
"--filter",
"label=io.podman.compose.project=include",
"--format",
'"{{.Image}}"',
]
command_container_id = [
"podman",
"ps",
"-a",
"--filter",
"label=io.podman.compose.project=include",
"--format",
'"{{.ID}}"',
]
command_down = ["podman", "rm", "--force", "CONTAINER_ID"]
out, _, returncode = capture(command_up)
assert 0 == returncode
out, _, returncode = capture(command_check_container)
assert 0 == returncode
assert out == b'"docker.io/library/busybox:latest"\n'
# Get container ID to remove it
out, _, returncode = capture(command_container_id)
assert 0 == returncode
assert out != b""
container_id = out.decode().strip().replace('"', "")
command_down[3] = container_id
out, _, returncode = capture(command_down)
# cleanup test image(tags)
assert 0 == returncode
assert out != b""
# check container did not exists anymore
out, _, returncode = capture(command_check_container)
assert 0 == returncode
assert out == b""

View File

@ -3,9 +3,10 @@ test_podman_compose_up_down.py
Tests the podman compose up and down commands used to create and remove services.
"""
import pytest
# pylint: disable=redefined-outer-name
import os
from test_podman_compose import capture
import pytest
@pytest.fixture
@ -65,7 +66,7 @@ def test_up(podman_compose_path, profile_compose_file, profiles, expected_servic
]
up_cmd.extend(profiles)
out, err, return_code = capture(up_cmd)
out, _, return_code = capture(up_cmd)
assert return_code == 0
check_cmd = [
@ -75,14 +76,14 @@ def test_up(podman_compose_path, profile_compose_file, profiles, expected_servic
"--format",
'"{{.Names}}"',
]
out, err, return_code = capture(check_cmd)
out, _, return_code = capture(check_cmd)
assert return_code == 0
assert len(expected_services) == 3
actual_output = out.decode("utf-8")
actual_services = {}
for service, expected_check in expected_services.items():
for service, _ in expected_services.items():
actual_services[service] = service in actual_output
assert expected_services == actual_services