mirror of
https://github.com/containers/podman-compose.git
synced 2025-07-02 05:30:17 +02:00
Compare commits
81 Commits
Author | SHA1 | Date | |
---|---|---|---|
d38b26bb01 | |||
22a4ad5806 | |||
37e2cb28d4 | |||
0cd3902c5f | |||
6ef759c6fd | |||
16cbcf4152 | |||
67ce900885 | |||
4e9f76768c | |||
84f7fdd7da | |||
405001b990 | |||
6b1aeff55f | |||
f06975b346 | |||
546cad5171 | |||
e07c28d127 | |||
935029dc33 | |||
80b2aa6ed0 | |||
360b85bf2d | |||
650a835eca | |||
82740cc311 | |||
0f645e4c70 | |||
3b15170ccf | |||
3359380ec6 | |||
14f39e5b86 | |||
e799a0b0ea | |||
6d8d3e94fe | |||
65d1fdeaa3 | |||
d905a7c638 | |||
2e8ed2f924 | |||
040b73adab | |||
f3e9a96c96 | |||
04b107805a | |||
2c5d00d3e7 | |||
cac90f69b8 | |||
b513f50f30 | |||
8f618b6fab | |||
cac836b0f5 | |||
3bb305cef4 | |||
09034a0c38 | |||
e668a339ce | |||
0065082db9 | |||
5d4de80ab7 | |||
23ad5c3ef7 | |||
45efe461b0 | |||
4f73f2b79e | |||
1d64f2cf8c | |||
2ce6d1a1e7 | |||
4e22faefd6 | |||
7a2da76ab8 | |||
79865c2e13 | |||
33d7d35a4d | |||
c23a8b2cbd | |||
36a3d3c207 | |||
b202a09501 | |||
35cbc49160 | |||
5c4aa40032 | |||
0ee7c2630a | |||
cef1785cd5 | |||
b4cfef12e9 | |||
b761050b0b | |||
e1d0ea7b4e | |||
1430578568 | |||
8f41cd3cdb | |||
a73dac2e39 | |||
d31a8b124d | |||
5df4e786ee | |||
27e27e9fe9 | |||
70a0e2d003 | |||
58641f0545 | |||
eea8bac496 | |||
09a8a3edf9 | |||
a6c4263738 | |||
9599cc039e | |||
0a6c057486 | |||
2b4ecee082 | |||
77f2e8e5b0 | |||
12d46ca836 | |||
72a94d5185 | |||
3e1f7d554b | |||
d7cf0966d3 | |||
1f35c00694 | |||
c31b4e2816 |
1
.codespellignore
Normal file
1
.codespellignore
Normal file
@ -0,0 +1 @@
|
||||
assertIn
|
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -4,4 +4,7 @@
|
||||
If this PR adds a new feature that improves compatibility with docker-compose, please add a link
|
||||
to the exact part of compose spec that the PR touches.
|
||||
|
||||
For any user-visible change please add a release note to newsfragments directory, e.g.
|
||||
newsfragments/my_feature.feature. See newsfragments/README.md for more details.
|
||||
|
||||
All changes require additional unit tests.
|
||||
|
2
.github/workflows/codespell.yml
vendored
2
.github/workflows/codespell.yml
vendored
@ -18,3 +18,5 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
- name: Codespell
|
||||
uses: codespell-project/actions-codespell@v2
|
||||
with:
|
||||
ignore_words_file: .codespellignore
|
||||
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
||||
if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi
|
||||
- name: Run tests in tests/
|
||||
run: |
|
||||
python -m unittest tests/*.py
|
||||
python -m unittest -v tests/*.py
|
||||
env:
|
||||
TESTS_DEBUG: 1
|
||||
- name: Run tests in pytests/
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -105,3 +105,6 @@ venv.bak/
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
|
||||
.vscode
|
||||
|
38
README.md
38
README.md
@ -10,7 +10,11 @@ This project focuses on:
|
||||
This project only depends on:
|
||||
|
||||
* `podman`
|
||||
* [podman dnsname plugin](https://github.com/containers/dnsname): It is usually found in the `podman-plugins` or `podman-dnsname` distro packages, those packages are not pulled by default and you need to install them. This allows containers to be able to resolve each other if they are on the same CNI network.
|
||||
* [podman dnsname plugin](https://github.com/containers/dnsname): It is usually found in
|
||||
the `podman-plugins` or `podman-dnsname` distro packages, those packages are not pulled
|
||||
by default and you need to install them. This allows containers to be able to resolve
|
||||
each other if they are on the same CNI network. This is not necessary when podman is using
|
||||
netavark as a network backend.
|
||||
* Python3
|
||||
* [PyYAML](https://pyyaml.org/)
|
||||
* [python-dotenv](https://pypi.org/project/python-dotenv/)
|
||||
@ -48,9 +52,11 @@ like `hostnet`. If you desire that behavior, pass it the standard way like `netw
|
||||
|
||||
## Installation
|
||||
|
||||
### Pip
|
||||
|
||||
Install the latest stable version from PyPI:
|
||||
|
||||
```
|
||||
```bash
|
||||
pip3 install podman-compose
|
||||
```
|
||||
|
||||
@ -58,14 +64,33 @@ pass `--user` to install inside regular user home without being root.
|
||||
|
||||
Or latest development version from GitHub:
|
||||
|
||||
```
|
||||
pip3 install https://github.com/containers/podman-compose/archive/devel.tar.gz
|
||||
```bash
|
||||
pip3 install https://github.com/containers/podman-compose/archive/main.tar.gz
|
||||
```
|
||||
|
||||
### Homebrew
|
||||
|
||||
```bash
|
||||
brew install podman-compose
|
||||
```
|
||||
|
||||
### Manual
|
||||
|
||||
```bash
|
||||
curl -o /usr/local/bin/podman-compose https://raw.githubusercontent.com/containers/podman-compose/main/podman_compose.py
|
||||
chmod +x /usr/local/bin/podman-compose
|
||||
```
|
||||
|
||||
or inside your home
|
||||
|
||||
```bash
|
||||
curl -o ~/.local/bin/podman-compose https://raw.githubusercontent.com/containers/podman-compose/main/podman_compose.py
|
||||
chmod +x ~/.local/bin/podman-compose
|
||||
```
|
||||
|
||||
or install from Fedora (starting from f31) repositories:
|
||||
|
||||
```
|
||||
```bash
|
||||
sudo dnf install podman-compose
|
||||
```
|
||||
|
||||
@ -74,10 +99,9 @@ sudo dnf install podman-compose
|
||||
We have included fully functional sample stacks inside `examples/` directory.
|
||||
You can get more examples from [awesome-compose](https://github.com/docker/awesome-compose).
|
||||
|
||||
|
||||
A quick example would be
|
||||
|
||||
```
|
||||
```bash
|
||||
cd examples/busybox
|
||||
podman-compose --help
|
||||
podman-compose up --help
|
||||
|
40
docs/Changelog-1.2.0.md
Normal file
40
docs/Changelog-1.2.0.md
Normal file
@ -0,0 +1,40 @@
|
||||
Version v1.2.0 (2024-06-26)
|
||||
===========================
|
||||
|
||||
Bug fixes
|
||||
---------
|
||||
|
||||
- Fixed handling of `--in-pod` argument. Previously it was hard to provide false value to it.
|
||||
- podman-compose no longer creates pods when registering systemd unit.
|
||||
- Fixed warning `RuntimeWarning: coroutine 'create_pods' was never awaited`
|
||||
- Fixed error when setting up IPAM network with default driver.
|
||||
- Fixed support for having list and dictionary `depends_on` sections in related compose files.
|
||||
- Fixed logging of failed build message.
|
||||
- Fixed support for multiple entries in `include` section.
|
||||
- Fixed environment variable precedence order.
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- `x-podman` dictionary in container root has been migrated to `x-podman.*` fields in container root.
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
- Added support for `--publish` in `podman-compose run`.
|
||||
- Added support for Podman external root filesystem management (`--rootfs` option).
|
||||
- Added support for `podman-compose images` command.
|
||||
- Added support for `env_file` being configured via dictionaries.
|
||||
- Added support for enabling GPU access.
|
||||
- Added support for selinux in verbose mount specification.
|
||||
- Added support for `additional_contexts` section.
|
||||
- Added support for multi-line environment files.
|
||||
- Added support for passing contents of `podman-compose.yml` via stdin.
|
||||
- Added support for specifying the value for `--in-pod` setting in `podman-compose.yml` file.
|
||||
- Added support for environmental secrets.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
- Added instructions on how to install podman-compose on Homebrew.
|
||||
- Added explanation that netavark is an alternative to dnsname plugin
|
@ -1,6 +1,30 @@
|
||||
# Podman specific extensions to the docker-compose format
|
||||
|
||||
Podman-compose supports the following extension to the docker-compose format.
|
||||
Podman-compose supports the following extension to the docker-compose format. These extensions
|
||||
are generally specified under fields with "x-podman" prefix in the compose file.
|
||||
|
||||
## Container management
|
||||
|
||||
The following extension keys are available under container configuration:
|
||||
|
||||
* `x-podman.uidmap` - Run the container in a new user namespace using the supplied UID mapping.
|
||||
|
||||
* `x-podman.gidmap` - Run the container in a new user namespace using the supplied GID mapping.
|
||||
|
||||
* `x-podman.rootfs` - Run the container without requiring any image management; the rootfs of the
|
||||
container is assumed to be managed externally.
|
||||
|
||||
For example, the following docker-compose.yml allows running a podman container with externally managed rootfs.
|
||||
```yml
|
||||
version: "3"
|
||||
services:
|
||||
my_service:
|
||||
command: ["/bin/busybox"]
|
||||
x-podman.rootfs: "/path/to/rootfs"
|
||||
```
|
||||
|
||||
For explanations of these extensions, please refer to the [Podman Documentation](https://docs.podman.io/).
|
||||
|
||||
|
||||
## Per-network MAC-addresses
|
||||
|
||||
@ -65,3 +89,23 @@ In addition, podman-compose supports the following podman-specific values for `n
|
||||
|
||||
The options to the network modes are passed to the `--network` option of the `podman create` command
|
||||
as-is.
|
||||
|
||||
|
||||
## Custom pods management
|
||||
|
||||
Podman-compose can have containers in pods. This can be controlled by extension key x-podman in_pod.
|
||||
It allows providing custom value for --in-pod and is especially relevant when --userns has to be set.
|
||||
|
||||
For example, the following docker-compose.yml allows using userns_mode by overriding the default
|
||||
value of --in-pod (unless it was specifically provided by "--in-pod=True" in command line interface).
|
||||
```yml
|
||||
version: "3"
|
||||
services:
|
||||
cont:
|
||||
image: nopush/podman-compose-test
|
||||
userns_mode: keep-id:uid=1000
|
||||
command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"]
|
||||
|
||||
x-podman:
|
||||
in_pod: false
|
||||
```
|
||||
|
11
examples/nvidia-smi/docker-compose.yaml
Normal file
11
examples/nvidia-smi/docker-compose.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
services:
|
||||
test:
|
||||
image: nvidia/cuda:12.3.1-base-ubuntu20.04
|
||||
command: nvidia-smi
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
13
newsfragments/README.txt
Normal file
13
newsfragments/README.txt
Normal file
@ -0,0 +1,13 @@
|
||||
This is the directory for news fragments used by towncrier: https://github.com/hawkowl/towncrier
|
||||
|
||||
You create a news fragment in this directory when you make a change, and the file gets removed from
|
||||
this directory when the news is published.
|
||||
|
||||
towncrier has a few standard types of news fragments, signified by the file extension. These are:
|
||||
|
||||
.feature: Signifying a new feature.
|
||||
.bugfix: Signifying a bug fix.
|
||||
.doc: Signifying a documentation improvement.
|
||||
.removal: Signifying a deprecation or removal of public API.
|
||||
.change: Signifying a change of behavior
|
||||
.misc: Miscellaneous change
|
@ -35,7 +35,7 @@ except ImportError:
|
||||
import yaml
|
||||
from dotenv import dotenv_values
|
||||
|
||||
__version__ = "1.1.0"
|
||||
__version__ = "1.2.0"
|
||||
|
||||
script = os.path.realpath(sys.argv[0])
|
||||
|
||||
@ -346,7 +346,7 @@ def norm_ulimit(inner_value):
|
||||
|
||||
|
||||
def transform(args, project_name, given_containers):
|
||||
if not args.in_pod:
|
||||
if not args.in_pod_bool:
|
||||
pod_name = None
|
||||
pods = []
|
||||
else:
|
||||
@ -431,6 +431,11 @@ def mount_desc_to_mount_args(compose, mount_desc, srv_name, cnt_name): # pylint
|
||||
tmpfs_mode = tmpfs_opts.get("mode", None)
|
||||
if tmpfs_mode:
|
||||
opts.append(f"tmpfs-mode={tmpfs_mode}")
|
||||
if mount_type == "bind":
|
||||
bind_opts = mount_desc.get("bind", {})
|
||||
selinux = bind_opts.get("selinux", None)
|
||||
if selinux is not None:
|
||||
opts.append(selinux)
|
||||
opts = ",".join(opts)
|
||||
if mount_type == "bind":
|
||||
return f"type=bind,source={source},destination={target},{opts}".rstrip(",")
|
||||
@ -496,6 +501,12 @@ def mount_desc_to_volume_args(compose, mount_desc, srv_name, cnt_name): # pylin
|
||||
read_only = mount_desc.get("read_only", None)
|
||||
if read_only is not None:
|
||||
opts.append("ro" if read_only else "rw")
|
||||
if mount_type == "bind":
|
||||
bind_opts = mount_desc.get("bind", {})
|
||||
selinux = bind_opts.get("selinux", None)
|
||||
if selinux is not None:
|
||||
opts.append(selinux)
|
||||
|
||||
args = f"{source}:{target}"
|
||||
if opts:
|
||||
args += ":" + ",".join(opts)
|
||||
@ -552,10 +563,11 @@ def get_secret_args(compose, cnt, secret, podman_is_building=False):
|
||||
dest_file = ""
|
||||
secret_opts = ""
|
||||
|
||||
target = None if is_str(secret) else secret.get("target", None)
|
||||
uid = None if is_str(secret) else secret.get("uid", None)
|
||||
gid = None if is_str(secret) else secret.get("gid", None)
|
||||
mode = None if is_str(secret) else secret.get("mode", None)
|
||||
secret_target = None if is_str(secret) else secret.get("target", None)
|
||||
secret_uid = None if is_str(secret) else secret.get("uid", None)
|
||||
secret_gid = None if is_str(secret) else secret.get("gid", None)
|
||||
secret_mode = None if is_str(secret) else secret.get("mode", None)
|
||||
secret_type = None if is_str(secret) else secret.get("type", None)
|
||||
|
||||
if source_file:
|
||||
# assemble path for source file first, because we need it for all cases
|
||||
@ -564,29 +576,29 @@ def get_secret_args(compose, cnt, secret, podman_is_building=False):
|
||||
|
||||
if podman_is_building:
|
||||
# pass file secrets to "podman build" with param --secret
|
||||
if not target:
|
||||
if not secret_target:
|
||||
secret_id = secret_name
|
||||
elif "/" in target:
|
||||
elif "/" in secret_target:
|
||||
raise ValueError(
|
||||
f'ERROR: Build secret "{secret_name}" has invalid target "{target}". '
|
||||
f'ERROR: Build secret "{secret_name}" has invalid target "{secret_target}". '
|
||||
+ "(Expected plain filename without directory as target.)"
|
||||
)
|
||||
else:
|
||||
secret_id = target
|
||||
secret_id = secret_target
|
||||
volume_ref = ["--secret", f"id={secret_id},src={source_file}"]
|
||||
else:
|
||||
# pass file secrets to "podman run" as volumes
|
||||
if not target:
|
||||
dest_file = f"/run/secrets/{secret_name}"
|
||||
elif not target.startswith("/"):
|
||||
sec = target if target else secret_name
|
||||
if not secret_target:
|
||||
dest_file = "/run/secrets/{}".format(secret_name)
|
||||
elif not secret_target.startswith("/"):
|
||||
sec = secret_target if secret_target else secret_name
|
||||
dest_file = f"/run/secrets/{sec}"
|
||||
else:
|
||||
dest_file = target
|
||||
dest_file = secret_target
|
||||
volume_ref = ["--volume", f"{source_file}:{dest_file}:ro,rprivate,rbind"]
|
||||
|
||||
if uid or gid or mode:
|
||||
sec = target if target else secret_name
|
||||
if secret_uid or secret_gid or secret_mode:
|
||||
sec = secret_target if secret_target else secret_name
|
||||
log.warning(
|
||||
"WARNING: Service %s uses secret %s with uid, gid, or mode."
|
||||
+ " These fields are not supported by this implementation of the Compose file",
|
||||
@ -602,9 +614,11 @@ def get_secret_args(compose, cnt, secret, podman_is_building=False):
|
||||
# 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 ""
|
||||
secret_opts += f",gid={gid}" if gid else ""
|
||||
secret_opts += f",mode={mode}" if mode else ""
|
||||
secret_opts += f",uid={secret_uid}" if secret_uid else ""
|
||||
secret_opts += f",gid={secret_gid}" if secret_gid else ""
|
||||
secret_opts += f",mode={secret_mode}" if secret_mode else ""
|
||||
secret_opts += f",type={secret_type}" if secret_type else ""
|
||||
secret_opts += f",target={secret_target}" if secret_target and secret_type == "env" else ""
|
||||
# The target option is only valid for type=env,
|
||||
# which in an ideal world would work
|
||||
# for type=mount as well.
|
||||
@ -617,14 +631,14 @@ def get_secret_args(compose, cnt, secret, podman_is_building=False):
|
||||
)
|
||||
if ext_name and ext_name != secret_name:
|
||||
raise ValueError(err_str.format(secret_name, ext_name))
|
||||
if target and target != secret_name:
|
||||
raise ValueError(err_str.format(target, secret_name))
|
||||
if target:
|
||||
if secret_target and secret_target != secret_name and secret_type != 'env':
|
||||
raise ValueError(err_str.format(secret_target, secret_name))
|
||||
if secret_target and secret_type != 'env':
|
||||
log.warning(
|
||||
'WARNING: Service "%s" uses target: "%s" for secret: "%s".'
|
||||
+ " That is un-supported and a no-op and is ignored.",
|
||||
cnt["_service"],
|
||||
target,
|
||||
secret_target,
|
||||
secret_name,
|
||||
)
|
||||
return ["--secret", "{}{}".format(secret_name, secret_opts)]
|
||||
@ -635,6 +649,62 @@ def get_secret_args(compose, cnt, secret, podman_is_building=False):
|
||||
|
||||
|
||||
def container_to_res_args(cnt, podman_args):
|
||||
container_to_cpu_res_args(cnt, podman_args)
|
||||
container_to_gpu_res_args(cnt, podman_args)
|
||||
|
||||
|
||||
def container_to_gpu_res_args(cnt, podman_args):
|
||||
# https://docs.docker.com/compose/gpu-support/
|
||||
# https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/cdi-support.html
|
||||
|
||||
deploy = cnt.get("deploy", None) or {}
|
||||
res = deploy.get("resources", None) or {}
|
||||
reservations = res.get("reservations", None) or {}
|
||||
devices = reservations.get("devices", [])
|
||||
gpu_on = False
|
||||
for device in devices:
|
||||
driver = device.get("driver", None)
|
||||
if driver is None:
|
||||
continue
|
||||
|
||||
capabilities = device.get("capabilities", None)
|
||||
if capabilities is None:
|
||||
continue
|
||||
|
||||
if driver != "nvidia" or "gpu" not in capabilities:
|
||||
continue
|
||||
|
||||
count = device.get("count", "all")
|
||||
device_ids = device.get("device_ids", "all")
|
||||
if device_ids != "all" and len(device_ids) > 0:
|
||||
for device_id in device_ids:
|
||||
podman_args.extend((
|
||||
"--device",
|
||||
f"nvidia.com/gpu={device_id}",
|
||||
))
|
||||
gpu_on = True
|
||||
continue
|
||||
|
||||
if count != "all":
|
||||
for device_id in range(count):
|
||||
podman_args.extend((
|
||||
"--device",
|
||||
f"nvidia.com/gpu={device_id}",
|
||||
))
|
||||
gpu_on = True
|
||||
continue
|
||||
|
||||
podman_args.extend((
|
||||
"--device",
|
||||
"nvidia.com/gpu=all",
|
||||
))
|
||||
gpu_on = True
|
||||
|
||||
if gpu_on:
|
||||
podman_args.append("--security-opt=label=disable")
|
||||
|
||||
|
||||
def container_to_cpu_res_args(cnt, podman_args):
|
||||
# v2: https://docs.docker.com/compose/compose-file/compose-file-v2/#cpu-and-other-resources
|
||||
# cpus, cpu_shares, mem_limit, mem_reservation
|
||||
cpus_limit_v2 = try_float(cnt.get("cpus", None), None)
|
||||
@ -734,7 +804,7 @@ def get_network_create_args(net_desc, proj_name, net_name):
|
||||
args.extend(("--opt", f"{key}={value}"))
|
||||
ipam = net_desc.get("ipam", None) or {}
|
||||
ipam_driver = ipam.get("driver", None)
|
||||
if ipam_driver:
|
||||
if ipam_driver and ipam_driver != "default":
|
||||
args.extend(("--ipam-driver", ipam_driver))
|
||||
ipam_config_ls = ipam.get("config", None) or []
|
||||
if net_desc.get("enable_ipv6", None):
|
||||
@ -993,11 +1063,23 @@ async def container_to_args(compose, cnt, detached=True):
|
||||
for item in norm_as_list(cnt.get("dns_search", None)):
|
||||
podman_args.extend(["--dns-search", item])
|
||||
env_file = cnt.get("env_file", [])
|
||||
if is_str(env_file):
|
||||
if is_str(env_file) or is_dict(env_file):
|
||||
env_file = [env_file]
|
||||
for i in env_file:
|
||||
i = os.path.realpath(os.path.join(dirname, i))
|
||||
podman_args.extend(["--env-file", i])
|
||||
if is_str(i):
|
||||
i = {"path": i}
|
||||
path = i["path"]
|
||||
required = i.get("required", True)
|
||||
i = os.path.realpath(os.path.join(dirname, path))
|
||||
if not os.path.exists(i):
|
||||
if not required:
|
||||
continue
|
||||
raise ValueError("Env file at {} does not exist".format(i))
|
||||
dotenv_dict = {}
|
||||
dotenv_dict = dotenv_to_dict(i)
|
||||
env = norm_as_list(dotenv_dict)
|
||||
for e in env:
|
||||
podman_args.extend(["-e", e])
|
||||
env = norm_as_list(cnt.get("environment", {}))
|
||||
for e in env:
|
||||
podman_args.extend(["-e", e])
|
||||
@ -1144,14 +1226,25 @@ async def container_to_args(compose, cnt, detached=True):
|
||||
podman_args.extend(["--healthcheck-retries", str(healthcheck["retries"])])
|
||||
|
||||
# handle podman extension
|
||||
x_podman = cnt.get("x-podman", None)
|
||||
if x_podman is not None:
|
||||
for uidmap in x_podman.get("uidmaps", []):
|
||||
podman_args.extend(["--uidmap", uidmap])
|
||||
for gidmap in x_podman.get("gidmaps", []):
|
||||
podman_args.extend(["--gidmap", gidmap])
|
||||
if 'x-podman' in cnt:
|
||||
raise ValueError(
|
||||
'Configuration under x-podman has been migrated to x-podman.uidmap and '
|
||||
'x-podman.gidmap fields'
|
||||
)
|
||||
|
||||
podman_args.append(cnt["image"]) # command, ..etc.
|
||||
rootfs_mode = False
|
||||
for uidmap in cnt.get('x-podman.uidmaps', []):
|
||||
podman_args.extend(["--uidmap", uidmap])
|
||||
for gidmap in cnt.get('x-podman.gidmaps', []):
|
||||
podman_args.extend(["--gidmap", gidmap])
|
||||
rootfs = cnt.get('x-podman.rootfs', None)
|
||||
if rootfs is not None:
|
||||
rootfs_mode = True
|
||||
podman_args.extend(["--rootfs", rootfs])
|
||||
log.warning("WARNING: x-podman.rootfs and image both specified, image field ignored")
|
||||
|
||||
if not rootfs_mode:
|
||||
podman_args.append(cnt["image"]) # command, ..etc.
|
||||
command = cnt.get("command", None)
|
||||
if command is not None:
|
||||
if is_str(command):
|
||||
@ -1378,6 +1471,12 @@ def normalize_service(service, sub_dir=""):
|
||||
if not context:
|
||||
context = "."
|
||||
service["build"]["context"] = context
|
||||
if "build" in service and "additional_contexts" in service["build"]:
|
||||
if is_dict(build["additional_contexts"]):
|
||||
new_additional_contexts = []
|
||||
for k, v in build["additional_contexts"].items():
|
||||
new_additional_contexts.append(f"{k}={v}")
|
||||
build["additional_contexts"] = new_additional_contexts
|
||||
for key in ("command", "entrypoint"):
|
||||
if key in service:
|
||||
if is_str(service[key]):
|
||||
@ -1401,6 +1500,15 @@ def normalize_service(service, sub_dir=""):
|
||||
if is_str(extends):
|
||||
extends = {"service": extends}
|
||||
service["extends"] = extends
|
||||
if "depends_on" in service:
|
||||
deps = service["depends_on"]
|
||||
if is_str(deps):
|
||||
deps = [deps]
|
||||
if is_list(deps):
|
||||
deps_dict = {}
|
||||
for d in deps:
|
||||
deps_dict[d] = {'condition': 'service_started'}
|
||||
service["depends_on"] = deps_dict
|
||||
return service
|
||||
|
||||
|
||||
@ -1636,6 +1744,18 @@ class PodmanCompose:
|
||||
if isinstance(retcode, int):
|
||||
sys.exit(retcode)
|
||||
|
||||
def resolve_in_pod(self, compose):
|
||||
if self.global_args.in_pod_bool is None:
|
||||
extension_dict = compose.get("x-podman", None)
|
||||
if extension_dict is not None:
|
||||
in_pod_value = extension_dict.get("in_pod", None)
|
||||
if in_pod_value is not None:
|
||||
self.global_args.in_pod_bool = in_pod_value
|
||||
else:
|
||||
self.global_args.in_pod_bool = True
|
||||
# otherwise use `in_pod` value provided by command line
|
||||
return self.global_args.in_pod_bool
|
||||
|
||||
def _parse_compose_file(self):
|
||||
args = self.global_args
|
||||
# cmd = args.command
|
||||
@ -1657,7 +1777,7 @@ class PodmanCompose:
|
||||
"pass files with -f"
|
||||
)
|
||||
sys.exit(-1)
|
||||
ex = map(os.path.exists, files)
|
||||
ex = map(lambda x: x == '-' or os.path.exists(x), files)
|
||||
missing = [fn0 for ex0, fn0 in zip(ex, files) if not ex0]
|
||||
if missing:
|
||||
log.fatal("missing files: %s", missing)
|
||||
@ -1678,8 +1798,14 @@ class PodmanCompose:
|
||||
# env-file is relative to the CWD
|
||||
dotenv_dict = {}
|
||||
if args.env_file:
|
||||
# Load .env from the Compose file's directory to preserve
|
||||
# behavior prior to 1.1.0 and to match with Docker Compose (v2).
|
||||
if ".env" == args.env_file:
|
||||
project_dotenv_file = os.path.realpath(os.path.join(dirname, ".env"))
|
||||
if os.path.exists(project_dotenv_file):
|
||||
dotenv_dict.update(dotenv_to_dict(project_dotenv_file))
|
||||
dotenv_path = os.path.realpath(args.env_file)
|
||||
dotenv_dict = dotenv_to_dict(dotenv_path)
|
||||
dotenv_dict.update(dotenv_to_dict(dotenv_path))
|
||||
|
||||
# TODO: remove next line
|
||||
os.chdir(dirname)
|
||||
@ -1687,8 +1813,8 @@ class PodmanCompose:
|
||||
os.environ.update({
|
||||
key: value for key, value in dotenv_dict.items() if key.startswith("PODMAN_")
|
||||
})
|
||||
self.environ = dict(os.environ)
|
||||
self.environ.update(dotenv_dict)
|
||||
self.environ = dotenv_dict
|
||||
self.environ.update(dict(os.environ))
|
||||
# see: https://docs.docker.com/compose/reference/envvars/
|
||||
# see: https://docs.docker.com/compose/env-file/
|
||||
self.environ.update({
|
||||
@ -1706,28 +1832,31 @@ class PodmanCompose:
|
||||
except StopIteration:
|
||||
break
|
||||
|
||||
with open(filename, "r", encoding="utf-8") as f:
|
||||
content = yaml.safe_load(f)
|
||||
if filename.strip().split('/')[-1] == '-':
|
||||
content = yaml.safe_load(sys.stdin)
|
||||
else:
|
||||
with open(filename, "r", encoding="utf-8") as f:
|
||||
content = yaml.safe_load(f)
|
||||
# log(filename, json.dumps(content, indent = 2))
|
||||
if not isinstance(content, dict):
|
||||
sys.stderr.write(
|
||||
"Compose file does not contain a top level object: %s\n" % filename
|
||||
)
|
||||
sys.exit(1)
|
||||
content = normalize(content)
|
||||
# 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"]
|
||||
if not isinstance(content, dict):
|
||||
sys.stderr.write(
|
||||
"Compose file does not contain a top level object: %s\n" % filename
|
||||
)
|
||||
sys.exit(1)
|
||||
content = normalize(content)
|
||||
# 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.extend(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):
|
||||
@ -1831,7 +1960,9 @@ class PodmanCompose:
|
||||
"service_name": service_name,
|
||||
**service_desc,
|
||||
}
|
||||
if "image" not in cnt:
|
||||
x_podman = service_desc.get("x-podman", None)
|
||||
rootfs_mode = x_podman is not None and x_podman.get("rootfs", None) is not None
|
||||
if "image" not in cnt and not rootfs_mode:
|
||||
cnt["image"] = f"{project_name}_{service_name}"
|
||||
labels = norm_as_list(cnt.get("labels", None))
|
||||
cnt["ports"] = norm_ports(cnt.get("ports", None))
|
||||
@ -1861,6 +1992,8 @@ class PodmanCompose:
|
||||
given_containers = list(container_by_name.values())
|
||||
given_containers.sort(key=lambda c: len(c.get("_deps", None) or []))
|
||||
# log("sorted:", [c["name"] for c in given_containers])
|
||||
|
||||
args.in_pod_bool = self.resolve_in_pod(compose)
|
||||
pods, containers = transform(args, project_name, given_containers)
|
||||
self.pods = pods
|
||||
self.containers = containers
|
||||
@ -1898,6 +2031,23 @@ class PodmanCompose:
|
||||
for cmd_parser in cmd._parse_args: # pylint: disable=protected-access
|
||||
cmd_parser(subparser)
|
||||
self.global_args = parser.parse_args()
|
||||
if self.global_args.in_pod is not None and self.global_args.in_pod.lower() not in (
|
||||
'',
|
||||
'true',
|
||||
'1',
|
||||
'false',
|
||||
'0',
|
||||
):
|
||||
raise ValueError(
|
||||
f'Invalid --in-pod value: \'{self.global_args.in_pod}\'. '
|
||||
'It must be set to either of: empty value, true, 1, false, 0'
|
||||
)
|
||||
|
||||
if self.global_args.in_pod == '' or self.global_args.in_pod is None:
|
||||
self.global_args.in_pod_bool = None
|
||||
else:
|
||||
self.global_args.in_pod_bool = self.global_args.in_pod.lower() in ('true', '1')
|
||||
|
||||
if self.global_args.version:
|
||||
self.global_args.command = "version"
|
||||
if not self.global_args.command or self.global_args.command == "help":
|
||||
@ -1914,8 +2064,8 @@ class PodmanCompose:
|
||||
"--in-pod",
|
||||
help="pod creation",
|
||||
metavar="in_pod",
|
||||
type=bool,
|
||||
default=True,
|
||||
type=str,
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pod-args",
|
||||
@ -1934,7 +2084,7 @@ class PodmanCompose:
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--file",
|
||||
help="Specify an alternate compose file (default: docker-compose.yml)",
|
||||
help="Specify an compose file (default: docker-compose.yml) or '-' to read from stdin.",
|
||||
metavar="file",
|
||||
action="append",
|
||||
default=[],
|
||||
@ -2102,8 +2252,6 @@ async def compose_systemd(compose, args):
|
||||
f.write(f"{k}={v}\n")
|
||||
log.debug("writing [%s]: done.", fn)
|
||||
log.info("\n\ncreating the pod without starting it: ...\n\n")
|
||||
process = await asyncio.create_subprocess_exec(script, ["up", "--no-start"])
|
||||
log.info("\nfinal exit code is %d", process)
|
||||
username = getpass.getuser()
|
||||
print(
|
||||
f"""
|
||||
@ -2228,6 +2376,8 @@ async def build_one(compose, args, cnt):
|
||||
build_args.extend(get_secret_args(compose, cnt, secret, podman_is_building=True))
|
||||
for tag in build_desc.get("tags", []):
|
||||
build_args.extend(["-t", tag])
|
||||
for additional_ctx in build_desc.get("additional_contexts", {}):
|
||||
build_args.extend([f"--build-context={additional_ctx}"])
|
||||
if "target" in build_desc:
|
||||
build_args.extend(["--target", build_desc["target"]])
|
||||
container_to_ulimit_build_args(cnt, build_args)
|
||||
@ -2313,7 +2463,7 @@ async def compose_up(compose: PodmanCompose, args):
|
||||
# `podman build` does not cache, so don't always build
|
||||
build_args = argparse.Namespace(if_not_exists=(not args.build), **args.__dict__)
|
||||
if await compose.commands["build"](compose, build_args) != 0:
|
||||
log("Build command failed")
|
||||
log.error("Build command failed")
|
||||
|
||||
hashes = (
|
||||
(
|
||||
@ -2525,7 +2675,7 @@ async def compose_ps(compose, args):
|
||||
"create a container similar to a service to run a one-off command",
|
||||
)
|
||||
async def compose_run(compose, args):
|
||||
create_pods(compose, args)
|
||||
await create_pods(compose, args)
|
||||
compose.assert_services(args.service)
|
||||
container_names = compose.container_names_by_service[args.service]
|
||||
container_name = container_names[0]
|
||||
@ -2587,6 +2737,10 @@ def compose_run_update_container_from_args(compose, cnt, args):
|
||||
del cnt[k]
|
||||
except KeyError:
|
||||
pass
|
||||
if args.publish:
|
||||
ports = cnt.get("ports", [])
|
||||
ports.extend(norm_ports(args.publish))
|
||||
cnt["ports"] = ports
|
||||
if args.volume:
|
||||
# TODO: handle volumes
|
||||
volumes = clone(cnt.get("volumes", None) or [])
|
||||
@ -2834,6 +2988,42 @@ async def compose_stats(compose, args):
|
||||
pass
|
||||
|
||||
|
||||
@cmd_run(podman_compose, "images", "List images used by the created containers")
|
||||
async def compose_images(compose, args):
|
||||
img_containers = [cnt for cnt in compose.containers if "image" in cnt]
|
||||
data = []
|
||||
if args.quiet is True:
|
||||
for img in img_containers:
|
||||
name = img["name"]
|
||||
output = await compose.podman.output([], "images", ["--quiet", img["image"]])
|
||||
data.append(output.decode("utf-8").split())
|
||||
else:
|
||||
data.append(["CONTAINER", "REPOSITORY", "TAG", "IMAGE ID", "SIZE", ""])
|
||||
for img in img_containers:
|
||||
name = img["name"]
|
||||
output = await compose.podman.output(
|
||||
[],
|
||||
"images",
|
||||
[
|
||||
"--format",
|
||||
"table " + name + " {{.Repository}} {{.Tag}} {{.ID}} {{.Size}}",
|
||||
"-n",
|
||||
img["image"],
|
||||
],
|
||||
)
|
||||
data.append(output.decode("utf-8").split())
|
||||
|
||||
# Determine the maximum length of each column
|
||||
column_widths = [max(map(len, column)) for column in zip(*data)]
|
||||
|
||||
# Print each row
|
||||
for row in data:
|
||||
# Format each cell using the maximum column width
|
||||
formatted_row = [cell.ljust(width) for cell, width in zip(row, column_widths)]
|
||||
formatted_row[-2:] = ["".join(formatted_row[-2:]).strip()]
|
||||
print("\t".join(formatted_row))
|
||||
|
||||
|
||||
###################
|
||||
# command arguments parsing
|
||||
###################
|
||||
@ -3268,6 +3458,11 @@ def compose_kill_parse(parser):
|
||||
)
|
||||
|
||||
|
||||
@cmd_parse(podman_compose, "images")
|
||||
def compose_images_parse(parser):
|
||||
parser.add_argument("-q", "--quiet", help="Only display images IDs", action="store_true")
|
||||
|
||||
|
||||
@cmd_parse(podman_compose, ["stats"])
|
||||
def compose_stats_parse(parser):
|
||||
parser.add_argument(
|
||||
|
@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import os
|
||||
import unittest
|
||||
|
||||
@ -9,44 +10,9 @@ import yaml
|
||||
from parameterized import parameterized
|
||||
|
||||
from podman_compose import PodmanCompose
|
||||
from podman_compose import normalize_service
|
||||
|
||||
|
||||
class TestCanMergeBuild(unittest.TestCase):
|
||||
@parameterized.expand([
|
||||
({"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_simple(self, input, expected):
|
||||
self.assertEqual(normalize_service(input), expected)
|
||||
|
||||
@parameterized.expand([
|
||||
({"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(self, input, expected):
|
||||
self.assertEqual(normalize_service(input, sub_dir="./sub_dir"), expected)
|
||||
|
||||
@parameterized.expand([
|
||||
({}, {}, {}),
|
||||
({}, {"test": "test"}, {"test": "test"}),
|
||||
@ -120,6 +86,52 @@ class TestCanMergeBuild(unittest.TestCase):
|
||||
actual_compose = podman_compose.services["test-service"]
|
||||
self.assertEqual(actual_compose, expected)
|
||||
|
||||
# $$$ is a placeholder for either command or entrypoint
|
||||
@parameterized.expand([
|
||||
({}, {"$$$": []}, {"$$$": []}),
|
||||
({"$$$": []}, {}, {"$$$": []}),
|
||||
({"$$$": []}, {"$$$": "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 test_parse_compose_file_when_multiple_composes_keys_command_entrypoint(
|
||||
self, base_template, override_template, expected_template
|
||||
):
|
||||
for key in ['command', 'entrypoint']:
|
||||
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"]
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
|
||||
def set_args(podman_compose: PodmanCompose, file_names: list[str]) -> None:
|
||||
podman_compose.global_args = argparse.Namespace()
|
||||
@ -127,7 +139,7 @@ def set_args(podman_compose: PodmanCompose, file_names: list[str]) -> None:
|
||||
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.in_pod_bool = True
|
||||
podman_compose.global_args.no_normalize = True
|
||||
|
||||
|
||||
@ -136,6 +148,19 @@ def dump_yaml(compose: dict, name: str) -> None:
|
||||
yaml.safe_dump(compose, outfile, default_flow_style=False)
|
||||
|
||||
|
||||
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_clean_test_yamls() -> None:
|
||||
test_files = ["test-compose-1.yaml", "test-compose-2.yaml"]
|
||||
for file in test_files:
|
||||
|
@ -1,115 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import yaml
|
||||
from parameterized import parameterized
|
||||
|
||||
from podman_compose import PodmanCompose
|
||||
from podman_compose import normalize_service
|
||||
|
||||
test_keys = ["command", "entrypoint"]
|
||||
|
||||
|
||||
class TestCanMergeCmdEnt(unittest.TestCase):
|
||||
@parameterized.expand([
|
||||
({"$$$": []}, {"$$$": []}),
|
||||
({"$$$": ["sh"]}, {"$$$": ["sh"]}),
|
||||
({"$$$": ["sh", "-c", "date"]}, {"$$$": ["sh", "-c", "date"]}),
|
||||
({"$$$": "sh"}, {"$$$": ["sh"]}),
|
||||
({"$$$": "sleep infinity"}, {"$$$": ["sleep", "infinity"]}),
|
||||
(
|
||||
{"$$$": "bash -c 'sleep infinity'"},
|
||||
{"$$$": ["bash", "-c", "sleep infinity"]},
|
||||
),
|
||||
])
|
||||
def test_normalize_service(self, input_template, expected_template):
|
||||
for key in test_keys:
|
||||
test_input, _, expected = template_to_expression(
|
||||
input_template, {}, expected_template, key
|
||||
)
|
||||
self.assertEqual(normalize_service(test_input), expected)
|
||||
|
||||
@parameterized.expand([
|
||||
({}, {"$$$": []}, {"$$$": []}),
|
||||
({"$$$": []}, {}, {"$$$": []}),
|
||||
({"$$$": []}, {"$$$": "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 test_parse_compose_file_when_multiple_composes(
|
||||
self, base_template, override_template, expected_template
|
||||
):
|
||||
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"]
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
|
||||
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 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)
|
@ -35,6 +35,21 @@ class TestComposeRunUpdateContainerFromArgs(unittest.TestCase):
|
||||
}
|
||||
self.assertEqual(cnt, expected_cnt)
|
||||
|
||||
def test_publish_ports(self):
|
||||
cnt = get_minimal_container()
|
||||
compose = get_minimal_compose()
|
||||
args = get_minimal_args()
|
||||
args.publish = ["1111", "2222:2222"]
|
||||
|
||||
compose_run_update_container_from_args(compose, cnt, args)
|
||||
|
||||
expected_cnt = {
|
||||
"name": "default_name",
|
||||
"ports": ["1111", "2222:2222"],
|
||||
"tty": True,
|
||||
}
|
||||
self.assertEqual(cnt, expected_cnt)
|
||||
|
||||
|
||||
def get_minimal_container():
|
||||
return {}
|
||||
@ -53,6 +68,7 @@ def get_minimal_args():
|
||||
name="default_name",
|
||||
rm=None,
|
||||
service=None,
|
||||
publish=None,
|
||||
service_ports=None,
|
||||
user=None,
|
||||
volume=None,
|
||||
|
@ -1,8 +1,11 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
import unittest
|
||||
from os import path
|
||||
from unittest import mock
|
||||
|
||||
from parameterized import parameterized
|
||||
|
||||
from podman_compose import container_to_args
|
||||
|
||||
|
||||
@ -161,3 +164,395 @@ class TestContainerToArgs(unittest.IsolatedAsyncioTestCase):
|
||||
"busybox",
|
||||
],
|
||||
)
|
||||
|
||||
async def test_uidmaps_extension_old_path(self):
|
||||
c = create_compose_mock()
|
||||
|
||||
cnt = get_minimal_container()
|
||||
cnt['x-podman'] = {'uidmaps': ['1000:1000:1']}
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
await container_to_args(c, cnt)
|
||||
|
||||
async def test_uidmaps_extension(self):
|
||||
c = create_compose_mock()
|
||||
|
||||
cnt = get_minimal_container()
|
||||
cnt['x-podman.uidmaps'] = ['1000:1000:1', '1001:1001:2']
|
||||
|
||||
args = await container_to_args(c, cnt)
|
||||
self.assertEqual(
|
||||
args,
|
||||
[
|
||||
"--name=project_name_service_name1",
|
||||
"-d",
|
||||
"--network=bridge",
|
||||
"--network-alias=service_name",
|
||||
'--uidmap',
|
||||
'1000:1000:1',
|
||||
'--uidmap',
|
||||
'1001:1001:2',
|
||||
"busybox",
|
||||
],
|
||||
)
|
||||
|
||||
async def test_gidmaps_extension(self):
|
||||
c = create_compose_mock()
|
||||
|
||||
cnt = get_minimal_container()
|
||||
cnt['x-podman.gidmaps'] = ['1000:1000:1', '1001:1001:2']
|
||||
|
||||
args = await container_to_args(c, cnt)
|
||||
self.assertEqual(
|
||||
args,
|
||||
[
|
||||
"--name=project_name_service_name1",
|
||||
"-d",
|
||||
"--network=bridge",
|
||||
"--network-alias=service_name",
|
||||
'--gidmap',
|
||||
'1000:1000:1',
|
||||
'--gidmap',
|
||||
'1001:1001:2',
|
||||
"busybox",
|
||||
],
|
||||
)
|
||||
|
||||
async def test_rootfs_extension(self):
|
||||
c = create_compose_mock()
|
||||
|
||||
cnt = get_minimal_container()
|
||||
del cnt["image"]
|
||||
cnt["x-podman.rootfs"] = "/path/to/rootfs"
|
||||
|
||||
args = await container_to_args(c, cnt)
|
||||
self.assertEqual(
|
||||
args,
|
||||
[
|
||||
"--name=project_name_service_name1",
|
||||
"-d",
|
||||
"--network=bridge",
|
||||
"--network-alias=service_name",
|
||||
"--rootfs",
|
||||
"/path/to/rootfs",
|
||||
],
|
||||
)
|
||||
|
||||
async def test_env_file_str(self):
|
||||
c = create_compose_mock()
|
||||
|
||||
cnt = get_minimal_container()
|
||||
env_file = path.realpath('tests/env-file-tests/env-files/project-1.env')
|
||||
cnt['env_file'] = env_file
|
||||
|
||||
args = await container_to_args(c, cnt)
|
||||
self.assertEqual(
|
||||
args,
|
||||
[
|
||||
"--name=project_name_service_name1",
|
||||
"-d",
|
||||
"-e",
|
||||
"ZZVAR1=podman-rocks-123",
|
||||
"-e",
|
||||
"ZZVAR2=podman-rocks-124",
|
||||
"-e",
|
||||
"ZZVAR3=podman-rocks-125",
|
||||
"--network=bridge",
|
||||
"--network-alias=service_name",
|
||||
"busybox",
|
||||
],
|
||||
)
|
||||
|
||||
async def test_env_file_str_not_exists(self):
|
||||
c = create_compose_mock()
|
||||
|
||||
cnt = get_minimal_container()
|
||||
cnt['env_file'] = 'notexists'
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
await container_to_args(c, cnt)
|
||||
|
||||
async def test_env_file_str_array_one_path(self):
|
||||
c = create_compose_mock()
|
||||
|
||||
cnt = get_minimal_container()
|
||||
env_file = path.realpath('tests/env-file-tests/env-files/project-1.env')
|
||||
cnt['env_file'] = [env_file]
|
||||
|
||||
args = await container_to_args(c, cnt)
|
||||
self.assertEqual(
|
||||
args,
|
||||
[
|
||||
"--name=project_name_service_name1",
|
||||
"-d",
|
||||
"-e",
|
||||
"ZZVAR1=podman-rocks-123",
|
||||
"-e",
|
||||
"ZZVAR2=podman-rocks-124",
|
||||
"-e",
|
||||
"ZZVAR3=podman-rocks-125",
|
||||
"--network=bridge",
|
||||
"--network-alias=service_name",
|
||||
"busybox",
|
||||
],
|
||||
)
|
||||
|
||||
async def test_env_file_str_array_two_paths(self):
|
||||
c = create_compose_mock()
|
||||
|
||||
cnt = get_minimal_container()
|
||||
env_file = path.realpath('tests/env-file-tests/env-files/project-1.env')
|
||||
env_file_2 = path.realpath('tests/env-file-tests/env-files/project-2.env')
|
||||
cnt['env_file'] = [env_file, env_file_2]
|
||||
|
||||
args = await container_to_args(c, cnt)
|
||||
self.assertEqual(
|
||||
args,
|
||||
[
|
||||
"--name=project_name_service_name1",
|
||||
"-d",
|
||||
"-e",
|
||||
"ZZVAR1=podman-rocks-123",
|
||||
"-e",
|
||||
"ZZVAR2=podman-rocks-124",
|
||||
"-e",
|
||||
"ZZVAR3=podman-rocks-125",
|
||||
"-e",
|
||||
"ZZVAR1=podman-rocks-223",
|
||||
"-e",
|
||||
"ZZVAR2=podman-rocks-224",
|
||||
"--network=bridge",
|
||||
"--network-alias=service_name",
|
||||
"busybox",
|
||||
],
|
||||
)
|
||||
|
||||
async def test_env_file_obj_required(self):
|
||||
c = create_compose_mock()
|
||||
|
||||
cnt = get_minimal_container()
|
||||
env_file = path.realpath('tests/env-file-tests/env-files/project-1.env')
|
||||
cnt['env_file'] = {'path': env_file, 'required': True}
|
||||
|
||||
args = await container_to_args(c, cnt)
|
||||
self.assertEqual(
|
||||
args,
|
||||
[
|
||||
"--name=project_name_service_name1",
|
||||
"-d",
|
||||
"-e",
|
||||
"ZZVAR1=podman-rocks-123",
|
||||
"-e",
|
||||
"ZZVAR2=podman-rocks-124",
|
||||
"-e",
|
||||
"ZZVAR3=podman-rocks-125",
|
||||
"--network=bridge",
|
||||
"--network-alias=service_name",
|
||||
"busybox",
|
||||
],
|
||||
)
|
||||
|
||||
async def test_env_file_obj_required_non_existent_path(self):
|
||||
c = create_compose_mock()
|
||||
|
||||
cnt = get_minimal_container()
|
||||
cnt['env_file'] = {'path': 'not-exists', 'required': True}
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
await container_to_args(c, cnt)
|
||||
|
||||
async def test_env_file_obj_optional(self):
|
||||
c = create_compose_mock()
|
||||
|
||||
cnt = get_minimal_container()
|
||||
cnt['env_file'] = {'path': 'not-exists', 'required': False}
|
||||
|
||||
args = await container_to_args(c, cnt)
|
||||
self.assertEqual(
|
||||
args,
|
||||
[
|
||||
"--name=project_name_service_name1",
|
||||
"-d",
|
||||
"--network=bridge",
|
||||
"--network-alias=service_name",
|
||||
"busybox",
|
||||
],
|
||||
)
|
||||
|
||||
async def test_gpu_count_all(self):
|
||||
c = create_compose_mock()
|
||||
|
||||
cnt = get_minimal_container()
|
||||
cnt["command"] = ["nvidia-smi"]
|
||||
cnt["deploy"] = {"resources": {"reservations": {"devices": [{}]}}}
|
||||
|
||||
cnt["deploy"]["resources"]["reservations"]["devices"][0] = {
|
||||
"driver": "nvidia",
|
||||
"count": "all",
|
||||
"capabilities": ["gpu"],
|
||||
}
|
||||
|
||||
args = await container_to_args(c, cnt)
|
||||
self.assertEqual(
|
||||
args,
|
||||
[
|
||||
"--name=project_name_service_name1",
|
||||
"-d",
|
||||
"--network=bridge",
|
||||
"--network-alias=service_name",
|
||||
"--device",
|
||||
"nvidia.com/gpu=all",
|
||||
"--security-opt=label=disable",
|
||||
"busybox",
|
||||
"nvidia-smi",
|
||||
],
|
||||
)
|
||||
|
||||
async def test_gpu_count_specific(self):
|
||||
c = create_compose_mock()
|
||||
|
||||
cnt = get_minimal_container()
|
||||
cnt["command"] = ["nvidia-smi"]
|
||||
cnt["deploy"] = {
|
||||
"resources": {
|
||||
"reservations": {
|
||||
"devices": [
|
||||
{
|
||||
"driver": "nvidia",
|
||||
"count": 2,
|
||||
"capabilities": ["gpu"],
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
args = await container_to_args(c, cnt)
|
||||
self.assertEqual(
|
||||
args,
|
||||
[
|
||||
"--name=project_name_service_name1",
|
||||
"-d",
|
||||
"--network=bridge",
|
||||
"--network-alias=service_name",
|
||||
"--device",
|
||||
"nvidia.com/gpu=0",
|
||||
"--device",
|
||||
"nvidia.com/gpu=1",
|
||||
"--security-opt=label=disable",
|
||||
"busybox",
|
||||
"nvidia-smi",
|
||||
],
|
||||
)
|
||||
|
||||
async def test_gpu_device_ids_all(self):
|
||||
c = create_compose_mock()
|
||||
|
||||
cnt = get_minimal_container()
|
||||
cnt["command"] = ["nvidia-smi"]
|
||||
cnt["deploy"] = {
|
||||
"resources": {
|
||||
"reservations": {
|
||||
"devices": [
|
||||
{
|
||||
"driver": "nvidia",
|
||||
"device_ids": "all",
|
||||
"capabilities": ["gpu"],
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
args = await container_to_args(c, cnt)
|
||||
self.assertEqual(
|
||||
args,
|
||||
[
|
||||
"--name=project_name_service_name1",
|
||||
"-d",
|
||||
"--network=bridge",
|
||||
"--network-alias=service_name",
|
||||
"--device",
|
||||
"nvidia.com/gpu=all",
|
||||
"--security-opt=label=disable",
|
||||
"busybox",
|
||||
"nvidia-smi",
|
||||
],
|
||||
)
|
||||
|
||||
async def test_gpu_device_ids_specific(self):
|
||||
c = create_compose_mock()
|
||||
|
||||
cnt = get_minimal_container()
|
||||
cnt["command"] = ["nvidia-smi"]
|
||||
cnt["deploy"] = {
|
||||
"resources": {
|
||||
"reservations": {
|
||||
"devices": [
|
||||
{
|
||||
"driver": "nvidia",
|
||||
"device_ids": [1, 3],
|
||||
"capabilities": ["gpu"],
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
args = await container_to_args(c, cnt)
|
||||
self.assertEqual(
|
||||
args,
|
||||
[
|
||||
"--name=project_name_service_name1",
|
||||
"-d",
|
||||
"--network=bridge",
|
||||
"--network-alias=service_name",
|
||||
"--device",
|
||||
"nvidia.com/gpu=1",
|
||||
"--device",
|
||||
"nvidia.com/gpu=3",
|
||||
"--security-opt=label=disable",
|
||||
"busybox",
|
||||
"nvidia-smi",
|
||||
],
|
||||
)
|
||||
|
||||
@parameterized.expand([
|
||||
(False, "z", ["--mount", "type=bind,source=./foo,destination=/mnt,z"]),
|
||||
(False, "Z", ["--mount", "type=bind,source=./foo,destination=/mnt,Z"]),
|
||||
(True, "z", ["-v", "./foo:/mnt:z"]),
|
||||
(True, "Z", ["-v", "./foo:/mnt:Z"]),
|
||||
])
|
||||
async def test_selinux_volume(self, prefer_volume, selinux_type, expected_additional_args):
|
||||
c = create_compose_mock()
|
||||
c.prefer_volume_over_mount = prefer_volume
|
||||
|
||||
cnt = get_minimal_container()
|
||||
|
||||
# This is supposed to happen during `_parse_compose_file`
|
||||
# but that is probably getting skipped during testing
|
||||
cnt["_service"] = cnt["service_name"]
|
||||
|
||||
cnt["volumes"] = [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "./foo",
|
||||
"target": "/mnt",
|
||||
"bind": {
|
||||
"selinux": selinux_type,
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
args = await container_to_args(c, cnt)
|
||||
self.assertEqual(
|
||||
args,
|
||||
[
|
||||
"--name=project_name_service_name1",
|
||||
"-d",
|
||||
*expected_additional_args,
|
||||
"--network=bridge",
|
||||
"--network-alias=service_name",
|
||||
"busybox",
|
||||
],
|
||||
)
|
||||
|
91
pytests/test_container_to_args_secrets.py
Normal file
91
pytests/test_container_to_args_secrets.py
Normal file
@ -0,0 +1,91 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
import unittest
|
||||
|
||||
from podman_compose import container_to_args
|
||||
|
||||
from .test_container_to_args import create_compose_mock
|
||||
from .test_container_to_args import get_minimal_container
|
||||
|
||||
|
||||
class TestContainerToArgsSecrets(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_pass_secret_as_env_variable(self):
|
||||
c = create_compose_mock()
|
||||
c.declared_secrets = {
|
||||
"my_secret": {"external": "true"} # must have external or name value
|
||||
}
|
||||
|
||||
cnt = get_minimal_container()
|
||||
cnt["secrets"] = [
|
||||
{
|
||||
"source": "my_secret",
|
||||
"target": "ENV_SECRET",
|
||||
"type": "env",
|
||||
},
|
||||
]
|
||||
|
||||
args = await container_to_args(c, cnt)
|
||||
self.assertEqual(
|
||||
args,
|
||||
[
|
||||
"--name=project_name_service_name1",
|
||||
"-d",
|
||||
"--network=bridge",
|
||||
"--network-alias=service_name",
|
||||
"--secret",
|
||||
"my_secret,type=env,target=ENV_SECRET",
|
||||
"busybox",
|
||||
],
|
||||
)
|
||||
|
||||
async def test_secret_as_env_external_true_has_no_name(self):
|
||||
c = create_compose_mock()
|
||||
c.declared_secrets = {
|
||||
"my_secret": {
|
||||
"name": "my_secret", # must have external or name value
|
||||
}
|
||||
}
|
||||
|
||||
cnt = get_minimal_container()
|
||||
cnt["_service"] = "test-service"
|
||||
cnt["secrets"] = [
|
||||
{
|
||||
"source": "my_secret",
|
||||
"target": "ENV_SECRET",
|
||||
"type": "env",
|
||||
}
|
||||
]
|
||||
|
||||
args = await container_to_args(c, cnt)
|
||||
self.assertEqual(
|
||||
args,
|
||||
[
|
||||
"--name=project_name_service_name1",
|
||||
"-d",
|
||||
"--network=bridge",
|
||||
"--network-alias=service_name",
|
||||
"--secret",
|
||||
"my_secret,type=env,target=ENV_SECRET",
|
||||
"busybox",
|
||||
],
|
||||
)
|
||||
|
||||
async def test_pass_secret_as_env_variable_no_external(self):
|
||||
c = create_compose_mock()
|
||||
c.declared_secrets = {
|
||||
"my_secret": {} # must have external or name value
|
||||
}
|
||||
|
||||
cnt = get_minimal_container()
|
||||
cnt["_service"] = "test-service"
|
||||
cnt["secrets"] = [
|
||||
{
|
||||
"source": "my_secret",
|
||||
"target": "ENV_SECRET",
|
||||
"type": "env",
|
||||
}
|
||||
]
|
||||
|
||||
with self.assertRaises(ValueError) as context:
|
||||
await container_to_args(c, cnt)
|
||||
self.assertIn('ERROR: unparsable secret: ', str(context.exception))
|
@ -77,7 +77,7 @@ class TestGetNetworkCreateArgs(unittest.TestCase):
|
||||
args = get_network_create_args(net_desc, proj_name, net_name)
|
||||
self.assertEqual(args, expected_args)
|
||||
|
||||
def test_ipam_driver(self):
|
||||
def test_ipam_driver_default(self):
|
||||
net_desc = {
|
||||
"labels": [],
|
||||
"internal": False,
|
||||
@ -96,6 +96,42 @@ class TestGetNetworkCreateArgs(unittest.TestCase):
|
||||
}
|
||||
proj_name = "test_project"
|
||||
net_name = "test_network"
|
||||
expected_args = [
|
||||
"create",
|
||||
"--label",
|
||||
f"io.podman.compose.project={proj_name}",
|
||||
"--label",
|
||||
f"com.docker.compose.project={proj_name}",
|
||||
"--subnet",
|
||||
"192.168.0.0/24",
|
||||
"--ip-range",
|
||||
"192.168.0.2/24",
|
||||
"--gateway",
|
||||
"192.168.0.1",
|
||||
net_name,
|
||||
]
|
||||
args = get_network_create_args(net_desc, proj_name, net_name)
|
||||
self.assertEqual(args, expected_args)
|
||||
|
||||
def test_ipam_driver(self):
|
||||
net_desc = {
|
||||
"labels": [],
|
||||
"internal": False,
|
||||
"driver": None,
|
||||
"driver_opts": {},
|
||||
"ipam": {
|
||||
"driver": "someipamdriver",
|
||||
"config": [
|
||||
{
|
||||
"subnet": "192.168.0.0/24",
|
||||
"ip_range": "192.168.0.2/24",
|
||||
"gateway": "192.168.0.1",
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
proj_name = "test_project"
|
||||
net_name = "test_network"
|
||||
expected_args = [
|
||||
"create",
|
||||
"--label",
|
||||
@ -103,7 +139,7 @@ class TestGetNetworkCreateArgs(unittest.TestCase):
|
||||
"--label",
|
||||
f"com.docker.compose.project={proj_name}",
|
||||
"--ipam-driver",
|
||||
"default",
|
||||
"someipamdriver",
|
||||
"--subnet",
|
||||
"192.168.0.0/24",
|
||||
"--ip-range",
|
||||
@ -122,7 +158,7 @@ class TestGetNetworkCreateArgs(unittest.TestCase):
|
||||
"driver": "bridge",
|
||||
"driver_opts": {"opt1": "value1", "opt2": "value2"},
|
||||
"ipam": {
|
||||
"driver": "default",
|
||||
"driver": "someipamdriver",
|
||||
"config": [
|
||||
{
|
||||
"subnet": "192.168.0.0/24",
|
||||
@ -153,7 +189,7 @@ class TestGetNetworkCreateArgs(unittest.TestCase):
|
||||
"--opt",
|
||||
"opt2=value2",
|
||||
"--ipam-driver",
|
||||
"default",
|
||||
"someipamdriver",
|
||||
"--ipv6",
|
||||
"--subnet",
|
||||
"192.168.0.0/24",
|
||||
|
43
pytests/test_normalize_depends_on.py
Normal file
43
pytests/test_normalize_depends_on.py
Normal file
@ -0,0 +1,43 @@
|
||||
import copy
|
||||
|
||||
from podman_compose import normalize_service
|
||||
|
||||
test_cases_simple = [
|
||||
(
|
||||
{"depends_on": "my_service"},
|
||||
{"depends_on": {"my_service": {"condition": "service_started"}}},
|
||||
),
|
||||
(
|
||||
{"depends_on": ["my_service"]},
|
||||
{"depends_on": {"my_service": {"condition": "service_started"}}},
|
||||
),
|
||||
(
|
||||
{"depends_on": ["my_service1", "my_service2"]},
|
||||
{
|
||||
"depends_on": {
|
||||
"my_service1": {"condition": "service_started"},
|
||||
"my_service2": {"condition": "service_started"},
|
||||
},
|
||||
},
|
||||
),
|
||||
(
|
||||
{"depends_on": {"my_service": {"condition": "service_started"}}},
|
||||
{"depends_on": {"my_service": {"condition": "service_started"}}},
|
||||
),
|
||||
(
|
||||
{"depends_on": {"my_service": {"condition": "service_healthy"}}},
|
||||
{"depends_on": {"my_service": {"condition": "service_healthy"}}},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
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
|
@ -254,7 +254,7 @@ def set_args(podman_compose: PodmanCompose, file_names: list[str], no_normalize:
|
||||
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.in_pod_bool = True
|
||||
podman_compose.global_args.no_normalize = no_normalize
|
||||
|
||||
|
||||
|
70
pytests/test_normalize_service.py
Normal file
70
pytests/test_normalize_service.py
Normal file
@ -0,0 +1,70 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
import unittest
|
||||
|
||||
from parameterized import parameterized
|
||||
|
||||
from podman_compose import normalize_service
|
||||
|
||||
|
||||
class TestNormalizeService(unittest.TestCase):
|
||||
@parameterized.expand([
|
||||
({"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"}},
|
||||
),
|
||||
(
|
||||
{"build": {"additional_contexts": ["ctx=../ctx", "ctx2=../ctx2"]}},
|
||||
{"build": {"additional_contexts": ["ctx=../ctx", "ctx2=../ctx2"]}},
|
||||
),
|
||||
(
|
||||
{"build": {"additional_contexts": {"ctx": "../ctx", "ctx2": "../ctx2"}}},
|
||||
{"build": {"additional_contexts": ["ctx=../ctx", "ctx2=../ctx2"]}},
|
||||
),
|
||||
])
|
||||
def test_simple(self, input, expected):
|
||||
self.assertEqual(normalize_service(input), expected)
|
||||
|
||||
@parameterized.expand([
|
||||
({"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(self, input, expected):
|
||||
self.assertEqual(normalize_service(input, sub_dir="./sub_dir"), expected)
|
||||
|
||||
@parameterized.expand([
|
||||
([], []),
|
||||
(["sh"], ["sh"]),
|
||||
(["sh", "-c", "date"], ["sh", "-c", "date"]),
|
||||
("sh", ["sh"]),
|
||||
("sleep infinity", ["sleep", "infinity"]),
|
||||
(
|
||||
"bash -c 'sleep infinity'",
|
||||
["bash", "-c", "sleep infinity"],
|
||||
),
|
||||
])
|
||||
def test_command_like(self, input, expected):
|
||||
for key in ['command', 'entrypoint']:
|
||||
input_service = {}
|
||||
input_service[key] = input
|
||||
|
||||
expected_service = {}
|
||||
expected_service[key] = expected
|
||||
self.assertEqual(normalize_service(input_service), expected_service)
|
14
tests/additional_contexts/README.md
Normal file
14
tests/additional_contexts/README.md
Normal file
@ -0,0 +1,14 @@
|
||||
# Test podman-compose with build.additional_contexts
|
||||
|
||||
```
|
||||
podman-compose build
|
||||
podman-compose up
|
||||
podman-compose down
|
||||
```
|
||||
|
||||
expected output would be
|
||||
|
||||
```
|
||||
[dict] | Data for dict
|
||||
[list] | Data for list
|
||||
```
|
1
tests/additional_contexts/data_for_dict/data.txt
Normal file
1
tests/additional_contexts/data_for_dict/data.txt
Normal file
@ -0,0 +1 @@
|
||||
Data for dict
|
1
tests/additional_contexts/data_for_list/data.txt
Normal file
1
tests/additional_contexts/data_for_list/data.txt
Normal file
@ -0,0 +1 @@
|
||||
Data for list
|
3
tests/additional_contexts/project/Dockerfile
Normal file
3
tests/additional_contexts/project/Dockerfile
Normal file
@ -0,0 +1,3 @@
|
||||
FROM busybox
|
||||
COPY --from=data data.txt /data/data.txt
|
||||
CMD ["busybox", "cat", "/data/data.txt"]
|
12
tests/additional_contexts/project/docker-compose.yml
Normal file
12
tests/additional_contexts/project/docker-compose.yml
Normal file
@ -0,0 +1,12 @@
|
||||
version: "3.7"
|
||||
services:
|
||||
dict:
|
||||
build:
|
||||
context: .
|
||||
additional_contexts:
|
||||
data: ../data_for_dict
|
||||
list:
|
||||
build:
|
||||
context: .
|
||||
additional_contexts:
|
||||
- data=../data_for_list
|
2
tests/env-file-tests/.env
Normal file
2
tests/env-file-tests/.env
Normal file
@ -0,0 +1,2 @@
|
||||
ZZVAR1='This value is overwritten by env-file-tests/.env'
|
||||
ZZVAR3='This value is loaded from env-file-tests/.env'
|
4
tests/env-file-tests/.gitignore
vendored
Normal file
4
tests/env-file-tests/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# This overrides the repository root .gitignore (ignoring all .env).
|
||||
# The .env files in this directory are important for the test cases.
|
||||
!.env
|
||||
!project/.env
|
@ -7,3 +7,31 @@ podman-compose -f project/container-compose.yaml --env-file env-files/project-1.
|
||||
```
|
||||
podman-compose -f $(pwd)/project/container-compose.yaml --env-file $(pwd)/env-files/project-1.env up
|
||||
```
|
||||
|
||||
```
|
||||
podman-compose -f $(pwd)/project/container-compose.env-file-flat.yaml up
|
||||
```
|
||||
|
||||
```
|
||||
podman-compose -f $(pwd)/project/container-compose.env-file-obj.yaml up
|
||||
```
|
||||
|
||||
```
|
||||
podman-compose -f $(pwd)/project/container-compose.env-file-obj-optional.yaml up
|
||||
```
|
||||
|
||||
based on environment variable precedent this command should give podman-rocks-321
|
||||
|
||||
```
|
||||
ZZVAR1=podman-rocks-321 podman-compose -f $(pwd)/project/container-compose.yaml --env-file $(pwd)/env-files/project-1.env up
|
||||
```
|
||||
|
||||
_The below test should print three environment variables_
|
||||
|
||||
```
|
||||
podman-compose -f $(pwd)/project/container-compose.load-.env-in-project.yaml run --rm app
|
||||
|
||||
ZZVAR1=This value is overwritten by env-file-tests/.env
|
||||
ZZVAR2=This value is loaded from .env in project/ directory
|
||||
ZZVAR3=This value is loaded from env-file-tests/.env
|
||||
```
|
||||
|
@ -1 +1,3 @@
|
||||
ZZVAR1=podman-rocks-123
|
||||
ZZVAR2=podman-rocks-124
|
||||
ZZVAR3=podman-rocks-125
|
||||
|
2
tests/env-file-tests/env-files/project-2.env
Normal file
2
tests/env-file-tests/env-files/project-2.env
Normal file
@ -0,0 +1,2 @@
|
||||
ZZVAR1=podman-rocks-223
|
||||
ZZVAR2=podman-rocks-224
|
2
tests/env-file-tests/project/.env
Normal file
2
tests/env-file-tests/project/.env
Normal file
@ -0,0 +1,2 @@
|
||||
ZZVAR1='This value is loaded but should be overwritten'
|
||||
ZZVAR2='This value is loaded from .env in project/ directory'
|
@ -0,0 +1,9 @@
|
||||
services:
|
||||
app:
|
||||
image: busybox
|
||||
command: ["/bin/busybox", "sh", "-c", "env | grep ZZ"]
|
||||
tmpfs:
|
||||
- /run
|
||||
- /tmp
|
||||
env_file:
|
||||
- ../env-files/project-1.env
|
@ -0,0 +1,11 @@
|
||||
services:
|
||||
app:
|
||||
image: busybox
|
||||
command: ["/bin/busybox", "sh", "-c", "env | grep ZZ"]
|
||||
tmpfs:
|
||||
- /run
|
||||
- /tmp
|
||||
env_file:
|
||||
- path: ../env-files/project-1.env
|
||||
- path: ../env-files/project-2.env
|
||||
required: false
|
@ -0,0 +1,9 @@
|
||||
services:
|
||||
app:
|
||||
image: busybox
|
||||
command: ["/bin/busybox", "sh", "-c", "env | grep ZZ"]
|
||||
tmpfs:
|
||||
- /run
|
||||
- /tmp
|
||||
env_file:
|
||||
- path: ../env-files/project-1.env
|
@ -0,0 +1,11 @@
|
||||
services:
|
||||
app:
|
||||
image: busybox
|
||||
command: ["/bin/busybox", "sh", "-c", "env | grep ZZ"]
|
||||
tmpfs:
|
||||
- /run
|
||||
- /tmp
|
||||
environment:
|
||||
ZZVAR1: $ZZVAR1
|
||||
ZZVAR2: $ZZVAR2
|
||||
ZZVAR3: $ZZVAR3
|
9
tests/in_pod/custom_x-podman_false/docker-compose.yml
Normal file
9
tests/in_pod/custom_x-podman_false/docker-compose.yml
Normal file
@ -0,0 +1,9 @@
|
||||
version: "3"
|
||||
services:
|
||||
cont:
|
||||
image: nopush/podman-compose-test
|
||||
userns_mode: keep-id:uid=1000
|
||||
command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"]
|
||||
|
||||
x-podman:
|
||||
in_pod: false
|
@ -0,0 +1,6 @@
|
||||
version: "3"
|
||||
services:
|
||||
cont:
|
||||
image: nopush/podman-compose-test
|
||||
userns_mode: keep-id:uid=1000
|
||||
command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"]
|
9
tests/in_pod/custom_x-podman_true/docker-compose.yml
Normal file
9
tests/in_pod/custom_x-podman_true/docker-compose.yml
Normal file
@ -0,0 +1,9 @@
|
||||
version: "3"
|
||||
services:
|
||||
cont:
|
||||
image: nopush/podman-compose-test
|
||||
userns_mode: keep-id:uid=1000
|
||||
command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"]
|
||||
|
||||
x-podman:
|
||||
in_pod: true
|
6
tests/include/docker-compose.extend.yaml
Normal file
6
tests/include/docker-compose.extend.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
version: '3.6'
|
||||
|
||||
services:
|
||||
web2:
|
||||
image: nopush/podman-compose-test
|
||||
command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", ".", "-p", "8004"]
|
@ -2,3 +2,4 @@ version: '3.6'
|
||||
|
||||
include:
|
||||
- docker-compose.base.yaml
|
||||
- docker-compose.extend.yaml
|
||||
|
15
tests/ipam_default/docker-compose.yaml
Normal file
15
tests/ipam_default/docker-compose.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
version: '3'
|
||||
|
||||
# --ipam-driver must not be pass when driver is "default"
|
||||
networks:
|
||||
ipam_test_default:
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: 172.19.0.0/24
|
||||
|
||||
services:
|
||||
testipam:
|
||||
image: busybox
|
||||
command: ["echo", "ipamtest"]
|
||||
|
@ -31,6 +31,9 @@ services:
|
||||
uid: '103'
|
||||
gid: '103'
|
||||
mode: 400
|
||||
- source: my_secret
|
||||
target: ENV_SECRET
|
||||
type: env
|
||||
|
||||
secrets:
|
||||
my_secret:
|
||||
@ -43,4 +46,3 @@ secrets:
|
||||
name: my_secret_3
|
||||
file_secret:
|
||||
file: ./my_secret
|
||||
|
||||
|
@ -4,3 +4,4 @@ ls -la /run/secrets/*
|
||||
ls -la /etc/custom_location
|
||||
cat /run/secrets/*
|
||||
cat /etc/custom_location
|
||||
env | grep SECRET
|
||||
|
14
tests/selinux/docker-compose.yml
Normal file
14
tests/selinux/docker-compose.yml
Normal file
@ -0,0 +1,14 @@
|
||||
version: "3"
|
||||
services:
|
||||
web1:
|
||||
image: busybox
|
||||
command: httpd -f -p 80 -h /var/www/html
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./docker-compose.yml
|
||||
target: /var/www/html/index.html
|
||||
bind:
|
||||
selinux: z
|
||||
ports:
|
||||
- "8080:80"
|
||||
|
44
tests/test_podman_compose_additional_contexts.py
Normal file
44
tests/test_podman_compose_additional_contexts.py
Normal file
@ -0,0 +1,44 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
|
||||
"""Test how additional contexts are passed to podman."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
from .test_podman_compose import podman_compose_path
|
||||
from .test_podman_compose import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
||||
""" "Returns the path to the compose file used for this test module"""
|
||||
return os.path.join(test_path(), "additional_contexts", "project")
|
||||
|
||||
|
||||
class TestComposeBuildAdditionalContexts(unittest.TestCase):
|
||||
def test_build_additional_context(self):
|
||||
"""podman build should receive additional contexts as --build-context
|
||||
|
||||
See additional_context/project/docker-compose.yaml for context paths
|
||||
"""
|
||||
cmd = (
|
||||
"coverage",
|
||||
"run",
|
||||
podman_compose_path(),
|
||||
"--dry-run",
|
||||
"--verbose",
|
||||
"-f",
|
||||
os.path.join(compose_yaml_path(), "docker-compose.yml"),
|
||||
"build",
|
||||
)
|
||||
p = subprocess.run(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
check=False,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(p.returncode, 0)
|
||||
self.assertIn("--build-context=data=../data_for_dict", p.stdout)
|
||||
self.assertIn("--build-context=data=../data_for_list", p.stdout)
|
442
tests/test_podman_compose_in_pod.py
Normal file
442
tests/test_podman_compose_in_pod.py
Normal file
@ -0,0 +1,442 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from .test_utils import RunSubprocessMixin
|
||||
|
||||
|
||||
def base_path():
|
||||
"""Returns the base path for the project"""
|
||||
return Path(__file__).parent.parent
|
||||
|
||||
|
||||
def test_path():
|
||||
"""Returns the path to the tests directory"""
|
||||
return os.path.join(base_path(), "tests")
|
||||
|
||||
|
||||
def podman_compose_path():
|
||||
"""Returns the path to the podman compose script"""
|
||||
return os.path.join(base_path(), "podman_compose.py")
|
||||
|
||||
|
||||
# If a compose file has userns_mode set, setting in_pod to True, results in error.
|
||||
# Default in_pod setting is True, unless compose file provides otherwise.
|
||||
# Compose file provides custom in_pod option, which can be overridden by command line in_pod option.
|
||||
# Test all combinations of command line argument in_pod and compose file argument in_pod.
|
||||
class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
|
||||
# compose file provides x-podman in_pod=false
|
||||
def test_x_podman_in_pod_false_command_line_in_pod_not_exists(self):
|
||||
"""
|
||||
Test that podman-compose will not create a pod, when x-podman in_pod=false and command line
|
||||
does not provide this option
|
||||
"""
|
||||
main_path = Path(__file__).parent.parent
|
||||
|
||||
command_up = [
|
||||
"python3",
|
||||
str(main_path.joinpath("podman_compose.py")),
|
||||
"-f",
|
||||
str(
|
||||
main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml")
|
||||
),
|
||||
"up",
|
||||
"-d",
|
||||
]
|
||||
|
||||
down_cmd = [
|
||||
"python3",
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
str(
|
||||
main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml")
|
||||
),
|
||||
"down",
|
||||
]
|
||||
|
||||
try:
|
||||
self.run_subprocess_assert_returncode(command_up)
|
||||
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode(down_cmd)
|
||||
command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_false"]
|
||||
# throws an error, can not actually find this pod because it was not created
|
||||
self.run_subprocess_assert_returncode(command_rm_pod, expected_returncode=1)
|
||||
|
||||
def test_x_podman_in_pod_false_command_line_in_pod_true(self):
|
||||
"""
|
||||
Test that podman-compose does not allow pod creating even with command line in_pod=True
|
||||
when --userns and --pod are set together: throws an error
|
||||
"""
|
||||
main_path = Path(__file__).parent.parent
|
||||
|
||||
# FIXME: creates a pod anyway, although it should not
|
||||
command_up = [
|
||||
"python3",
|
||||
str(main_path.joinpath("podman_compose.py")),
|
||||
"--in-pod=True",
|
||||
"-f",
|
||||
str(
|
||||
main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml")
|
||||
),
|
||||
"up",
|
||||
"-d",
|
||||
]
|
||||
|
||||
try:
|
||||
out, err = self.run_subprocess_assert_returncode(command_up)
|
||||
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
|
||||
|
||||
finally:
|
||||
command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_false"]
|
||||
# should throw an error of not being able to find this pod (because it should not have
|
||||
# been created) and have expected_returncode=1 (see FIXME above)
|
||||
self.run_subprocess_assert_returncode(command_rm_pod)
|
||||
|
||||
def test_x_podman_in_pod_false_command_line_in_pod_false(self):
|
||||
"""
|
||||
Test that podman-compose will not create a pod as command line sets in_pod=False
|
||||
"""
|
||||
main_path = Path(__file__).parent.parent
|
||||
|
||||
command_up = [
|
||||
"python3",
|
||||
str(main_path.joinpath("podman_compose.py")),
|
||||
"--in-pod=False",
|
||||
"-f",
|
||||
str(
|
||||
main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml")
|
||||
),
|
||||
"up",
|
||||
"-d",
|
||||
]
|
||||
|
||||
down_cmd = [
|
||||
"python3",
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
str(
|
||||
main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml")
|
||||
),
|
||||
"down",
|
||||
]
|
||||
|
||||
try:
|
||||
self.run_subprocess_assert_returncode(command_up)
|
||||
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode(down_cmd)
|
||||
command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_false"]
|
||||
# can not actually find this pod because it was not created
|
||||
self.run_subprocess_assert_returncode(command_rm_pod, 1)
|
||||
|
||||
def test_x_podman_in_pod_false_command_line_in_pod_empty_string(self):
|
||||
"""
|
||||
Test that podman-compose will not create a pod, when x-podman in_pod=false and command line
|
||||
command line in_pod=""
|
||||
"""
|
||||
main_path = Path(__file__).parent.parent
|
||||
|
||||
command_up = [
|
||||
"python3",
|
||||
str(main_path.joinpath("podman_compose.py")),
|
||||
"--in-pod=",
|
||||
"-f",
|
||||
str(
|
||||
main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml")
|
||||
),
|
||||
"up",
|
||||
"-d",
|
||||
]
|
||||
|
||||
down_cmd = [
|
||||
"python3",
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
str(
|
||||
main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml")
|
||||
),
|
||||
"down",
|
||||
]
|
||||
|
||||
try:
|
||||
self.run_subprocess_assert_returncode(command_up)
|
||||
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode(down_cmd)
|
||||
command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_false"]
|
||||
# can not actually find this pod because it was not created
|
||||
self.run_subprocess_assert_returncode(command_rm_pod, 1)
|
||||
|
||||
# compose file provides x-podman in_pod=true
|
||||
def test_x_podman_in_pod_true_command_line_in_pod_not_exists(self):
|
||||
"""
|
||||
Test that podman-compose does not allow pod creating when --userns and --pod are set
|
||||
together even when x-podman in_pod=true: throws an error
|
||||
"""
|
||||
main_path = Path(__file__).parent.parent
|
||||
|
||||
# FIXME: creates a pod anyway, although it should not
|
||||
# Container is not created, so command 'down' is not needed
|
||||
command_up = [
|
||||
"python3",
|
||||
str(main_path.joinpath("podman_compose.py")),
|
||||
"-f",
|
||||
str(
|
||||
main_path.joinpath("tests", "in_pod", "custom_x-podman_true", "docker-compose.yml")
|
||||
),
|
||||
"up",
|
||||
"-d",
|
||||
]
|
||||
|
||||
try:
|
||||
out, err = self.run_subprocess_assert_returncode(command_up)
|
||||
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
|
||||
|
||||
finally:
|
||||
command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_true"]
|
||||
# should throw an error of not being able to find this pod (it should not have been
|
||||
# created) and have expected_returncode=1 (see FIXME above)
|
||||
self.run_subprocess_assert_returncode(command_rm_pod)
|
||||
|
||||
def test_x_podman_in_pod_true_command_line_in_pod_true(self):
|
||||
"""
|
||||
Test that podman-compose does not allow pod creating when --userns and --pod are set
|
||||
together even when x-podman in_pod=true and and command line in_pod=True: throws an error
|
||||
"""
|
||||
main_path = Path(__file__).parent.parent
|
||||
|
||||
# FIXME: creates a pod anyway, although it should not
|
||||
# Container is not created, so command 'down' is not needed
|
||||
command_up = [
|
||||
"python3",
|
||||
str(main_path.joinpath("podman_compose.py")),
|
||||
"--in-pod=True",
|
||||
"-f",
|
||||
str(
|
||||
main_path.joinpath("tests", "in_pod", "custom_x-podman_true", "docker-compose.yml")
|
||||
),
|
||||
"up",
|
||||
"-d",
|
||||
]
|
||||
|
||||
try:
|
||||
out, err = self.run_subprocess_assert_returncode(command_up)
|
||||
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
|
||||
|
||||
finally:
|
||||
command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_true"]
|
||||
# should throw an error of not being able to find this pod (because it should not have
|
||||
# been created) and have expected_returncode=1 (see FIXME above)
|
||||
self.run_subprocess_assert_returncode(command_rm_pod)
|
||||
|
||||
def test_x_podman_in_pod_true_command_line_in_pod_false(self):
|
||||
"""
|
||||
Test that podman-compose will not create a pod as command line sets in_pod=False
|
||||
"""
|
||||
main_path = Path(__file__).parent.parent
|
||||
|
||||
command_up = [
|
||||
"python3",
|
||||
str(main_path.joinpath("podman_compose.py")),
|
||||
"--in-pod=False",
|
||||
"-f",
|
||||
str(
|
||||
main_path.joinpath("tests", "in_pod", "custom_x-podman_true", "docker-compose.yml")
|
||||
),
|
||||
"up",
|
||||
"-d",
|
||||
]
|
||||
|
||||
down_cmd = [
|
||||
"python3",
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
str(
|
||||
main_path.joinpath("tests", "in_pod", "custom_x-podman_true", "docker-compose.yml")
|
||||
),
|
||||
"down",
|
||||
]
|
||||
|
||||
try:
|
||||
self.run_subprocess_assert_returncode(command_up)
|
||||
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode(down_cmd)
|
||||
command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_false"]
|
||||
# can not actually find this pod because it was not created
|
||||
self.run_subprocess_assert_returncode(command_rm_pod, 1)
|
||||
|
||||
def test_x_podman_in_pod_true_command_line_in_pod_empty_string(self):
|
||||
"""
|
||||
Test that podman-compose does not allow pod creating when --userns and --pod are set
|
||||
together even when x-podman in_pod=true and command line in_pod="": throws an error
|
||||
"""
|
||||
main_path = Path(__file__).parent.parent
|
||||
|
||||
# FIXME: creates a pod anyway, although it should not
|
||||
# Container is not created, so command 'down' is not needed
|
||||
command_up = [
|
||||
"python3",
|
||||
str(main_path.joinpath("podman_compose.py")),
|
||||
"--in-pod=",
|
||||
"-f",
|
||||
str(
|
||||
main_path.joinpath("tests", "in_pod", "custom_x-podman_true", "docker-compose.yml")
|
||||
),
|
||||
"up",
|
||||
"-d",
|
||||
]
|
||||
|
||||
try:
|
||||
out, err = self.run_subprocess_assert_returncode(command_up)
|
||||
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
|
||||
|
||||
finally:
|
||||
command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_true"]
|
||||
# should throw an error of not being able to find this pod (because it should not have
|
||||
# been created) and have expected_returncode=1 (see FIXME above)
|
||||
self.run_subprocess_assert_returncode(command_rm_pod)
|
||||
|
||||
# compose file does not provide x-podman in_pod
|
||||
def test_x_podman_in_pod_not_exists_command_line_in_pod_not_exists(self):
|
||||
"""
|
||||
Test that podman-compose does not allow pod creating when --userns and --pod are set
|
||||
together: throws an error
|
||||
"""
|
||||
main_path = Path(__file__).parent.parent
|
||||
|
||||
# FIXME: creates a pod anyway, although it should not
|
||||
# Container is not created, so command 'down' is not needed
|
||||
command_up = [
|
||||
"python3",
|
||||
str(main_path.joinpath("podman_compose.py")),
|
||||
"-f",
|
||||
str(
|
||||
main_path.joinpath(
|
||||
"tests", "in_pod", "custom_x-podman_not_exists", "docker-compose.yml"
|
||||
)
|
||||
),
|
||||
"up",
|
||||
"-d",
|
||||
]
|
||||
|
||||
try:
|
||||
out, err = self.run_subprocess_assert_returncode(command_up)
|
||||
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
|
||||
|
||||
finally:
|
||||
command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_not_exists"]
|
||||
# should throw an error of not being able to find this pod (it should not have been
|
||||
# created) and have expected_returncode=1 (see FIXME above)
|
||||
self.run_subprocess_assert_returncode(command_rm_pod)
|
||||
|
||||
def test_x_podman_in_pod_not_exists_command_line_in_pod_true(self):
|
||||
"""
|
||||
Test that podman-compose does not allow pod creating when --userns and --pod are set
|
||||
together even when x-podman in_pod=true: throws an error
|
||||
"""
|
||||
main_path = Path(__file__).parent.parent
|
||||
|
||||
# FIXME: creates a pod anyway, although it should not
|
||||
# Container was not created, so command 'down' is not needed
|
||||
command_up = [
|
||||
"python3",
|
||||
str(main_path.joinpath("podman_compose.py")),
|
||||
"--in-pod=True",
|
||||
"-f",
|
||||
str(
|
||||
main_path.joinpath(
|
||||
"tests", "in_pod", "custom_x-podman_not_exists", "docker-compose.yml"
|
||||
)
|
||||
),
|
||||
"up",
|
||||
"-d",
|
||||
]
|
||||
|
||||
try:
|
||||
out, err = self.run_subprocess_assert_returncode(command_up)
|
||||
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
|
||||
|
||||
finally:
|
||||
command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_not_exists"]
|
||||
# should throw an error of not being able to find this pod (because it should not have
|
||||
# been created) and have expected_returncode=1 (see FIXME above)
|
||||
self.run_subprocess_assert_returncode(command_rm_pod)
|
||||
|
||||
def test_x_podman_in_pod_not_exists_command_line_in_pod_false(self):
|
||||
"""
|
||||
Test that podman-compose will not create a pod as command line sets in_pod=False
|
||||
"""
|
||||
main_path = Path(__file__).parent.parent
|
||||
|
||||
command_up = [
|
||||
"python3",
|
||||
str(main_path.joinpath("podman_compose.py")),
|
||||
"--in-pod=False",
|
||||
"-f",
|
||||
str(
|
||||
main_path.joinpath(
|
||||
"tests", "in_pod", "custom_x-podman_not_exists", "docker-compose.yml"
|
||||
)
|
||||
),
|
||||
"up",
|
||||
"-d",
|
||||
]
|
||||
|
||||
down_cmd = [
|
||||
"python3",
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
str(
|
||||
main_path.joinpath(
|
||||
"tests", "in_pod", "custom_x-podman_not_exists", "docker-compose.yml"
|
||||
)
|
||||
),
|
||||
"down",
|
||||
]
|
||||
|
||||
try:
|
||||
self.run_subprocess_assert_returncode(command_up)
|
||||
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode(down_cmd)
|
||||
|
||||
command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_not_exists"]
|
||||
# can not actually find this pod because it was not created
|
||||
self.run_subprocess_assert_returncode(command_rm_pod, 1)
|
||||
|
||||
def test_x_podman_in_pod_not_exists_command_line_in_pod_empty_string(self):
|
||||
"""
|
||||
Test that podman-compose does not allow pod creating when --userns and --pod are set
|
||||
together: throws an error
|
||||
"""
|
||||
main_path = Path(__file__).parent.parent
|
||||
|
||||
# FIXME: creates a pod anyway, although it should not
|
||||
# Container was not created, so command 'down' is not needed
|
||||
command_up = [
|
||||
"python3",
|
||||
str(main_path.joinpath("podman_compose.py")),
|
||||
"--in-pod=",
|
||||
"-f",
|
||||
str(
|
||||
main_path.joinpath(
|
||||
"tests", "in_pod", "custom_x-podman_not_exists", "docker-compose.yml"
|
||||
)
|
||||
),
|
||||
"up",
|
||||
"-d",
|
||||
]
|
||||
|
||||
try:
|
||||
out, err = self.run_subprocess_assert_returncode(command_up)
|
||||
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
|
||||
|
||||
finally:
|
||||
command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_not_exists"]
|
||||
# should throw an error of not being able to find this pod (because it should not have
|
||||
# been created) and have expected_returncode=1 (see FIXME above)
|
||||
self.run_subprocess_assert_returncode(command_rm_pod)
|
@ -44,16 +44,18 @@ class TestPodmanComposeInclude(unittest.TestCase, RunSubprocessMixin):
|
||||
'"{{.ID}}"',
|
||||
]
|
||||
|
||||
command_down = ["podman", "rm", "--force", "CONTAINER_ID"]
|
||||
command_down = ["podman", "rm", "--force"]
|
||||
|
||||
self.run_subprocess_assert_returncode(command_up)
|
||||
out, _ = self.run_subprocess_assert_returncode(command_check_container)
|
||||
self.assertEqual(out, b'"localhost/nopush/podman-compose-test:latest"\n')
|
||||
expected_output = b'"localhost/nopush/podman-compose-test:latest"\n' * 2
|
||||
self.assertEqual(out, expected_output)
|
||||
# Get container ID to remove it
|
||||
out, _ = self.run_subprocess_assert_returncode(command_container_id)
|
||||
self.assertNotEqual(out, b"")
|
||||
container_id = out.decode().strip().replace('"', "")
|
||||
command_down[3] = container_id
|
||||
container_ids = out.decode().strip().split("\n")
|
||||
container_ids = [container_id.replace('"', "") for container_id in container_ids]
|
||||
command_down.extend(container_ids)
|
||||
out, _ = self.run_subprocess_assert_returncode(command_down)
|
||||
# cleanup test image(tags)
|
||||
self.assertNotEqual(out, b"")
|
||||
|
Reference in New Issue
Block a user