81 Commits

Author SHA1 Message Date
d38b26bb01 Release 1.2.0
Signed-off-by: Povilas Kanapickas <povilas@radix.lt>
2024-06-26 10:43:28 +03:00
22a4ad5806 Merge pull request #975 from p12tic/changelog
Add release notes for v1.2.0
2024-06-26 10:41:59 +03:00
37e2cb28d4 Add release notes for v1.2.0
Signed-off-by: Povilas Kanapickas <povilas@radix.lt>
2024-06-26 10:39:48 +03:00
0cd3902c5f Merge pull request #974 from p12tic/newsfragments
Use newsfragments pattern for the release notes
2024-06-26 10:37:54 +03:00
6ef759c6fd Use newsfragments pattern for the release notes
Signed-off-by: Povilas Kanapickas <povilas@radix.lt>
2024-06-26 10:21:03 +03:00
16cbcf4152 Merge pull request #956 from Genzer/fix/loading-.env-breaking-since-1.1.0
Load .env from Compose file's directory and cwd
2024-06-24 23:37:33 +03:00
67ce900885 Commit .env in tests/env-file-tests, bypassing root .gitignore
This commit adds a .gitignore in tests/env-file-tests to allow .env files
to be committed.

Fix: #937
Signed-off-by: Genzer <732937+Genzer@users.noreply.github.com>
2024-06-24 23:29:57 +03:00
4e9f76768c Load .env from Compose file's directory and cwd
This commit loads dotenv `.env` (exactly that name) from the following location (the later takes
precedence):

- The `.env` file in the Compose file's directory.
- The `.env` file in the current working directory (invoking podman-compose).

This preserves the behavior prior to 1.1.0 and to match with Docker Compose CLI.

Fix: https://github.com/containers/podman-compose/issues/937
Signed-off-by: Genzer <732937+Genzer@users.noreply.github.com>
2024-06-24 23:29:56 +03:00
84f7fdd7da Merge pull request #971 from mokibit/type-env-secret-support
Add support for environment variable secrets
2024-06-24 23:17:34 +03:00
405001b990 Fix comment
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2024-06-24 21:24:34 +03:00
6b1aeff55f Add unittests for type=env secret
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2024-06-24 21:24:34 +03:00
f06975b346 Update tests for type=env secret
Signed-off-by: Brett Calliss <brett@obligatory.email>
2024-06-24 20:12:44 +03:00
546cad5171 Add type=env secret support
Signed-off-by: Brett Calliss <brett@obligatory.email>
2024-06-24 20:12:44 +03:00
e07c28d127 Merge pull request #771 from wgnathanael/environment-precedence
Fix environment variable precedents
2024-06-22 20:00:08 +03:00
935029dc33 Fix environment variable precedents
Per https://docs.docker.com/compose/environment-variables/envvars-precedence/#advanced-example

Signed-off-by: nathanael.noblet <nathanael.noblet@willowglensystems.com>
2024-06-22 19:58:02 +03:00
80b2aa6ed0 Merge pull request #964 from mokibit/set-custom-in_pod-in-compose-file
Allow providing custom in_pod argument as a global compose file variable
2024-06-20 09:45:41 +03:00
360b85bf2d Allow providing custom in_pod argument as a global compose file variable
Default command line argument `in_pod` was set to True, but this breaks
the compose file for users who want to use `--userns` argument. This
commit sets default `in_pod` value to None, and later resolves whether
to create a pod by checking compose file, as new argument in compose
file x-podman is now available. Now it is convenient for users to pass
custom `in_pod` value (True or False) as a compose file argument when
command line value of `in_pod` is not provided.

Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2024-06-20 09:42:22 +03:00
650a835eca Merge pull request #966 from ArthoPacini/enhance/stdin-docker-compose-support
Add ability to input docker-compose.yaml via stdin
2024-06-19 20:06:15 +03:00
82740cc311 Add ability to input docker-compose.yaml via stdin
Signed-off-by: Artho Pacini <eu@arthopacini.com>
2024-06-18 00:05:31 +03:00
0f645e4c70 Add ability to input docker-compose.yaml via stdin
Signed-off-by: Artho Pacini <eu@arthopacini.com>
2024-06-18 00:05:31 +03:00
3b15170ccf Changed the global parser help message for file input, to reflect changes for reading from stdin
Signed-off-by: Artho Pacini <eu@arthopacini.com>
2024-06-18 00:05:31 +03:00
3359380ec6 Add ability to input docker-compose.yaml via stdin
Signed-off-by: Artho Pacini <eu@arthopacini.com>
2024-06-18 00:05:31 +03:00
14f39e5b86 Merge pull request #965 from mokibit/fix-gidmap-typo
Fix x-podman.gidmap typo
2024-06-17 10:08:17 +03:00
e799a0b0ea Fix x-podman.gidmap typo
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2024-06-14 17:31:55 +03:00
6d8d3e94fe Merge pull request #960 from mokibit/github-verbose-integration-tests
github: Add verbose option to integration tests
2024-06-08 20:15:13 +03:00
65d1fdeaa3 github: Add verbose option to integration tests
Currently it is not possible to see which test output corresponds to
which test exactly. Now before each new test its test name is printed.

Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2024-06-08 20:05:12 +03:00
d905a7c638 Merge pull request #949 from mokibit/multiline-env-file
Add support for multi-line environment files
2024-05-29 00:47:58 +03:00
2e8ed2f924 pytests: Add test for object required but path non existent
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2024-05-29 00:09:22 +03:00
040b73adab pytests: Add tests for several multi-line environment files
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2024-05-29 00:09:15 +03:00
f3e9a96c96 Fixes #908: Add support for multi-line environment files
Signed-off-by: Hedayat Vatankhah <hedayat.fwd@gmail.com>
2024-05-28 23:43:15 +03:00
04b107805a Merge pull request #954 from mokibit/fix-codespelling-update
github/workflows: Fix automatic codespelling update
2024-05-28 23:40:59 +03:00
2c5d00d3e7 github/workflows: Add codespellignore file to address false positives
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2024-05-28 20:20:33 +03:00
cac90f69b8 Merge pull request #946 from charliemirabile/selinux_tests
Missing SELinux tests
2024-05-22 10:17:05 +03:00
b513f50f30 test: add missing unit tests for selinux in verbose mount
Support for setting the selinux flags on a bind mount specified using
the verbose syntax was merged as part of #911, but at that time the PR
lacked unit tests. This commit adds the missing tests

Signed-off-by: charliemirabile <46761267+charliemirabile@users.noreply.github.com>
2024-05-21 19:43:03 -04:00
8f618b6fab Merge pull request #763 from otto-liljalaakso-nt/additional_contexts
Support additional_contexts
2024-05-21 19:49:23 +03:00
cac836b0f5 Support additional_contexts
Signed-off-by: Otto Liljalaakso <otto.liljalaakso@novatron.fi>
2024-05-21 19:44:37 +03:00
3bb305cef4 Merge pull request #945 from p12tic/split-gpu-test
test: Split test_gpu test
2024-05-21 12:25:11 +03:00
09034a0c38 test: Split test_gpu test
Signed-off-by: Povilas Kanapickas <povilas@radix.lt>
2024-05-21 12:04:32 +03:00
e668a339ce Merge pull request #943 from HernandoR/fix/multi-sub-composes
Fix the test_include for multi subcomposes
2024-05-21 11:32:17 +03:00
0065082db9 refine the test_include for multi subcomposes
Signed-off-by: Zhen Liu <lzhen.dev@outlook.com>
2024-05-21 11:28:27 +03:00
5d4de80ab7 Merge pull request #911 from charliemirabile/selinux
Add support for selinux in verbose bind mount specification
2024-05-19 11:13:45 +03:00
23ad5c3ef7 Merge pull request #920 from mokeyish/gpu
Add supoort for enabling GPU access
2024-05-19 11:12:34 +03:00
45efe461b0 Merge pull request #941 from HernandoR/patch-1
Update podman_compose.py
2024-05-19 11:10:39 +03:00
4f73f2b79e fix: add include test file, edit the assertion
Signed-off-by: Zhen Liu <lzhen.dev@outlook.com>
2024-05-18 21:23:43 +08:00
1d64f2cf8c Update podman_compose.py
fix #940

Signed-off-by: Zhen Liu <lzhen.dev@outlook.com>
2024-05-18 20:33:10 +08:00
2ce6d1a1e7 Merge pull request #933 from hedayat/fix-build-error-log
Fix logging build error message
2024-05-13 16:24:15 +03:00
4e22faefd6 Fix logging build error message
Signed-off-by: Hedayat Vatankhah <hedayat.fwd@gmail.com>
2024-05-13 15:13:40 +03:00
7a2da76ab8 Merge pull request #724 from hedayat/fix-merge-depends-on
Fixes #723: merge short & long syntax of depends_on dependencies
2024-05-08 17:57:44 +03:00
79865c2e13 Add support for enabling GPU access
Signed-off-by: YISH <mokeyish@hotmail.com>
2024-05-07 10:32:24 +08:00
33d7d35a4d Merge pull request #851 from fccagou/fix-ipam-driver-default
fix(ipam_driver): do not pass --ipam-driver option when value set to …
2024-05-06 17:31:59 +03:00
c23a8b2cbd Do not pass --ipam-driver option when value set to default
fixes #852.

Signed-off-by: fccagou <me@fccagou.fr>
2024-05-06 17:00:17 +03:00
36a3d3c207 Merge pull request #925 from GerkinDev/feat/env-file-object
Fixes #897: support `env_file` as objects
2024-05-06 16:57:25 +03:00
b202a09501 Add support for env_file as objects
Fixes: https://github.com/containers/podman-compose/issues/897

Signed-off-by: Alexandre Germain <nihilivin@gmail.com>
2024-05-06 14:13:37 +03:00
35cbc49160 Merge pull request #928 from schugabe/patch-1
add await for create_pods call
2024-05-06 14:00:29 +03:00
5c4aa40032 add await for create_pods call
fixes log output podman_compose.py:2527: RuntimeWarning: coroutine 'create_pods' was never awaited

Signed-off-by: Johannes Kasberger <schugabe@gmx.at>
2024-05-06 11:02:45 +02:00
0ee7c2630a Merge pull request #641 from DaniruKun/devel
Add instructions on install from Homebrew
2024-05-04 18:03:43 +03:00
cef1785cd5 Add instructions on install from Homebrew
Signed-off-by: Daniils Petrovs <thedanpetrov@gmail.com>
2024-05-04 17:50:06 +03:00
b4cfef12e9 Merge pull request #926 from p12tic/cleanup-tests
Cleanup tests
2024-05-04 17:32:00 +03:00
b761050b0b tests: Merge multiple compose merging tests into single test class
Signed-off-by: Povilas Kanapickas <povilas@radix.lt>
2024-05-04 17:30:09 +03:00
e1d0ea7b4e tests: Move normalize_service tests to a separate test class
Signed-off-by: Povilas Kanapickas <povilas@radix.lt>
2024-05-04 17:29:04 +03:00
1430578568 tests: Simplify command and entrypoint normalization tests
Signed-off-by: Povilas Kanapickas <povilas@radix.lt>
2024-05-04 17:28:59 +03:00
8f41cd3cdb Merge pull request #731 from g2p/patch-1
README: explain that netavark is an alternative to the dnsname plugin
2024-05-01 21:16:27 +03:00
a73dac2e39 Merge pull request #923 from winston-yallow/remove-sideeffect-from-systemd-registration
Don't create pods/container when registering systemd unit
2024-05-01 21:05:13 +03:00
d31a8b124d Don't create pods/container when registering systemd unit
Signed-off-by: Winston <44872771+winston-yallow@users.noreply.github.com>
2024-05-01 20:03:14 +02:00
5df4e786ee README: explain that netavark is an alternative to the dnsname plugin
This is a useful hint when the dnsname plugin is not packaged.

Signed-off-by: Gabriel de Perthuis <g2p.code@gmail.com>
2024-05-01 15:13:29 +02:00
27e27e9fe9 Merge pull request #918 from p12tic/fix-in-pod
Fix handling of --in-pod argument
2024-04-28 21:33:50 +03:00
70a0e2d003 Fix handling of --in-pod argument
Currently --in-pod handling is broken because the only way to set False
is by providing empty argument like "--in-pod=". As of Python 3.7 the
solution is to accept string and parse manually.

Co-authored-by: Randolph Sapp <res.sapp@gmail.com>
Signed-off-by: Povilas Kanapickas <povilas@radix.lt>
2024-04-28 21:16:34 +03:00
58641f0545 Merge pull request #716 from Tayeh/images_cmd
add `podman-compose images` command
2024-04-28 20:37:07 +03:00
eea8bac496 Add images command
Signed-off-by: Mohammed Tayeh <m.tayeh94@gmail.com>
2024-04-28 19:04:23 +03:00
09a8a3edf9 Merge pull request #917 from p12tic/x-podman-keys
Migrate x-podman dictionary on container root to x-podman.* fields
2024-04-28 18:29:33 +03:00
a6c4263738 Add tests for x-podman.uidmaps and x-podman.gidmaps
Signed-off-by: Povilas Kanapickas <povilas@radix.lt>
2024-04-28 18:25:04 +03:00
9599cc039e Migrate x-podman dictionary to x-podman.* fields in container root
Signed-off-by: Povilas Kanapickas <povilas@radix.lt>
2024-04-28 18:24:37 +03:00
0a6c057486 Merge pull request #737 from ftyghome/feat_rootfs
Support podman's external rootfs management
2024-04-28 18:04:37 +03:00
2b4ecee082 Add docs for podman specific compose file extensions
Signed-off-by: GnSight <ftyg@live.com>
2024-04-28 17:55:39 +03:00
77f2e8e5b0 Support podman's external rootfs management
Signed-off-by: GnSight <ftyg@live.com>
2024-04-28 17:55:39 +03:00
12d46ca836 Merge pull request #916 from beledouxdenis/main-run-implement-publish
implement --publish in docker-compose run
2024-04-28 17:33:56 +03:00
72a94d5185 implement --publish in docker-compose run
Signed-off-by: Denis Ledoux <dle@odoo.com>
2024-04-28 00:54:35 +02:00
3e1f7d554b add tests for selinux with verbose bind mount
based on seccomp test. Without the selinux option, visiting localhost:8080
will give a 404 error because httpd cannot access the file, but with selinux: z
the context for the file will be appropriately updated so httpd can access it

Signed-off-by: charliemirabile <46761267+charliemirabile@users.noreply.github.com>
2024-04-10 15:29:03 -04:00
d7cf0966d3 add support for selinux in verbose mount
This corresponds to specifying the `z` or `Z` option in the third
portion of a terse mount specification (i.e. src:trg:z)

Signed-off-by: charliemirabile <46761267+charliemirabile@users.noreply.github.com>
2024-04-10 15:29:03 -04:00
1f35c00694 Add unit test for depends_on normalization as a dict
Signed-off-by: Hedayat Vatankhah <hedayat.fwd@gmail.com>
2024-04-08 23:31:41 +03:30
c31b4e2816 Fixes #723: merge short & long syntax of depends_on dependencies
Signed-off-by: Hedayat Vatankhah <hedayat.fwd@gmail.com>
2023-07-08 04:10:43 +03:30
47 changed files with 1797 additions and 238 deletions

1
.codespellignore Normal file
View File

@ -0,0 +1 @@
assertIn

View File

@ -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.

View File

@ -18,3 +18,5 @@ jobs:
uses: actions/checkout@v4
- name: Codespell
uses: codespell-project/actions-codespell@v2
with:
ignore_words_file: .codespellignore

View File

@ -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
View File

@ -105,3 +105,6 @@ venv.bak/
# mypy
.mypy_cache/
.vscode

View File

@ -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
View 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

View File

@ -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
```

View 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
View 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

View File

@ -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(

View File

@ -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:

View File

@ -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)

View 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,

View File

@ -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",
],
)

View 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))

View File

@ -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",

View 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

View File

@ -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

View 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)

View 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
```

View File

@ -0,0 +1 @@
Data for dict

View File

@ -0,0 +1 @@
Data for list

View File

@ -0,0 +1,3 @@
FROM busybox
COPY --from=data data.txt /data/data.txt
CMD ["busybox", "cat", "/data/data.txt"]

View 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

View 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
View 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

View File

@ -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
```

View File

@ -1 +1,3 @@
ZZVAR1=podman-rocks-123
ZZVAR2=podman-rocks-124
ZZVAR3=podman-rocks-125

View File

@ -0,0 +1,2 @@
ZZVAR1=podman-rocks-223
ZZVAR2=podman-rocks-224

View File

@ -0,0 +1,2 @@
ZZVAR1='This value is loaded but should be overwritten'
ZZVAR2='This value is loaded from .env in project/ directory'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View File

@ -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"]

View 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

View 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"]

View File

@ -2,3 +2,4 @@ version: '3.6'
include:
- docker-compose.base.yaml
- docker-compose.extend.yaml

View 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"]

View File

@ -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

View File

@ -4,3 +4,4 @@ ls -la /run/secrets/*
ls -la /etc/custom_location
cat /run/secrets/*
cat /etc/custom_location
env | grep SECRET

View 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"

View 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)

View 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)

View File

@ -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"")