152 Commits

Author SHA1 Message Date
bd29caa797 Release 1.4.0
Signed-off-by: Povilas Kanapickas <povilas@radix.lt>
2025-05-10 15:24:32 +03:00
f0928dd399 Merge pull request #1197 from p12tic/release
Release notes for 1.4.0
2025-05-10 15:23:07 +03:00
6c9c09197a Release notes for 1.4.0
Signed-off-by: Povilas Kanapickas <povilas@radix.lt>
2025-05-10 15:03:54 +03:00
cda84f439f Merge pull request #1181 from zeyugao/main
Return non-zero exit_code on failure when doing `up -d`
2025-05-10 14:44:41 +03:00
67616bdaac Handle exit code when compose up -d
Signed-off-by: Elsa <zeyugao@outlook.com>
2025-05-10 14:38:53 +03:00
7497692b19 Merge pull request #1184 from schnell18/main
Fix service_healthy condition enforcing
2025-05-10 14:20:06 +03:00
782c44d4c3 tests: Style cleanup
Signed-off-by: Justin Zhang <schnell18@gmail.com>
2025-05-10 14:12:28 +03:00
d7762a54f0 Fix service_healthy condition enforcing
Skip dependency health check to avoid compose-up hang for podman prior
to 4.6.0, which doesn't support --condition healthy.

Signed-off-by: Justin Zhang <schnell18@gmail.com>
2025-05-10 14:12:27 +03:00
eba2ca2695 Skip running compose-down during up when there are no active containers
Signed-off-by: Povilas Kanapickas <povilas@radix.lt>
2025-05-10 14:12:18 +03:00
abe5965c9a tests: Improve reliability of network tests
The test did fail on my laptop with podman 5.4.1.

Signed-off-by: Justin Zhang <schnell18@gmail.com>
2025-05-10 14:10:22 +03:00
9e0da82726 Change compose-up to create then start container to avoid double exec
Signed-off-by: Justin Zhang <schnell18@gmail.com>
2025-05-10 13:58:23 +03:00
6acdafd5b1 Merge pull request #1190 from gtebbutt/abort-on-failure
Add `--abort-on-container-failure` option
2025-05-10 13:51:23 +03:00
8638eb9b6d tests: Test selected env variables to improve robustness
Signed-off-by: Justin Zhang <schnell18@gmail.com>
2025-05-10 13:46:26 +03:00
e1d938ffa6 Add --abort-on-container-failure
Signed-off-by: gtebbutt <5956226+gtebbutt@users.noreply.github.com>
2025-05-10 13:41:29 +03:00
d532e09d7d Merge pull request #1189 from mokibit/add-merge-reset-override
Implement `override` and `reset` analog to docker-compose
2025-05-08 01:16:15 +03:00
1dab256cdd tests/integration: Add override tag attribute test
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-04-30 23:37:05 +03:00
2a33ef5c79 tests/integration: Add override tag service test
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-04-30 23:37:05 +03:00
5ab734026c tests/integration: Add reset tag attribute test
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-04-30 23:37:05 +03:00
35dc395483 tests/integration: Add reset tag service test
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-04-30 23:37:05 +03:00
38a9263424 integration/tests: Move 'volumes_merge' tests to 'merge' directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-04-30 23:37:05 +03:00
cbe9587973 Implement override and reset analog to docker-compose
Corresponding Docker compose file documentation:
https://docs.docker.com/reference/compose-file/merge/

Signed-off-by: Sebastian Sellmeier <mail@sebastian-sellmeier.de>
Co-authored-by: Monika Kairaityte <monika@kibit.lt>
2025-04-30 23:37:05 +03:00
8bb43100b1 Merge pull request #1182 from zeyugao/pids_limit
Implement pids_limit
2025-04-21 23:50:31 +03:00
98f166d2e4 Implement pids_limit
Signed-off-by: Elsa <zeyugao@outlook.com>
2025-04-21 22:51:37 +03:00
150ab02446 Merge pull request #1187 from rgasquet/feature/add-cpuset-option
Feature: add cpuset option
2025-04-21 22:49:08 +03:00
ff58a0bff0 Add newsfragment
Signed-off-by: Romain Gasquet <romain.gasquet@neutron.fr>
2025-04-19 14:33:15 +02:00
8d899ebb65 Feature: add cpuset option
Signed-off-by: Romain Gasquet <romain.gasquet@neutron.fr>
2025-04-19 14:10:30 +02:00
342a39dcfe Merge pull request #1179 from knarfS/fix_port_cmd
Fix port command
2025-04-14 18:05:53 +03:00
d6b8476573 Merge pull request #1180 from knarfS/add_rmi_arg
Add rmi argument for down command
2025-04-14 18:04:09 +03:00
ae41ef08c3 tests/integration: Improve tests for port command
Refs #778 and #1039

Signed-off-by: Frank Stettner <frank-stettner@gmx.net>
2025-04-10 07:51:39 +02:00
da46ee3910 Fix port command for dynamic host ports
Use `podman inspect` to get the actual host ports rather echoing the
defined ports from the compose yml.

Fixes #778 and #1039

Signed-off-by: Frank Stettner <frank-stettner@gmx.net>
2025-04-08 13:57:19 +02:00
d80c31f578 tests/integration: Add tests for up and down command
Refs #387

Signed-off-by: Frank Stettner <frank-stettner@gmx.net>
2025-04-08 13:53:27 +02:00
cefa68dc75 Implement rmi argument for down command
Fixes #387

Signed-off-by: Frank Stettner <frank-stettner@gmx.net>
2025-04-08 13:53:27 +02:00
2e46ff0db2 Merge pull request #1159 from me-coder/container_scaling_update
Updates handling of scale/replicas parameter in CLI and compose file
2025-04-08 02:54:32 +03:00
fbc4c7da80 Integration tests for container scaling changes
Signed-off-by: Yashodhan Pise <technoy@gmail.com>
2025-04-08 01:42:03 +03:00
11879d3e94 Updates handling of scale/replicas through CLI & compose file
Signed-off-by: Yashodhan Pise <technoy@gmail.com>
2025-04-08 01:37:15 +03:00
27cf8da06f Addition of relevant newsfragments file
Signed-off-by: Yashodhan Pise <technoy@gmail.com>
2025-04-05 20:32:58 +05:30
10a30ba24a Merge pull request #1173 from mokibit/automate-ulimit-test
tests/integration: Automate manual `ulimit` test
2025-04-04 17:12:42 +03:00
a1be62fd31 tests/integration: Automate manual ulimit test
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-04-04 16:31:26 +03:00
15bf02a004 Merge pull request #1175 from mokibit/automate-volumes-merge-test
tests/integration: Automate manual `volumes_merge` test
2025-04-04 16:31:03 +03:00
e45b5d5063 tests/integration: Automate manual volumes_merge test
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-04-04 15:22:45 +03:00
c46ecb226b Merge pull request #1171 from mokibit/fix-git-build-url-context
Fix using git URL as build context
2025-03-31 00:26:56 +03:00
e04b8f3a60 tests/integration: Add integration test for buid git URL as context
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-03-30 21:56:55 +03:00
815450aba9 tests/unit: Add test for buid git URL as context
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-03-30 21:56:55 +03:00
92f0a8583a Fix using git URL as build context
Podman-compose actually did not work with git URL as build context.

Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-03-30 21:56:55 +03:00
5f4fc4618c Add os.path.normpath to normalize dockerfile pathname
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-03-28 22:28:30 +02:00
4d899edeb3 Merge pull request #1166 from piotr-kubiak/megre-args
Allow merging of args in both list and dict syntax
2025-03-24 13:35:31 +02:00
f9489afaf5 Allow merging of args in both list and dict syntax
Signed-off-by: Piotr Kubiak <piotr-kubiak@users.noreply.github.com>
2025-03-24 13:31:19 +02:00
7d7533772b Merge pull request #1165 from drachenfels-de/fix-project-name-interpolation
Fix `COMPOSE_PROJECT_NAME` interpolation
2025-03-20 19:26:03 +02:00
65b455f081 Fix project name evaluation order
The COMPOSE_PROJECT_NAME environment variable must override the
top-level name: attribute in the Compose file.

The precedence order is defined in the docker compose documentation
https://docs.docker.com/compose/how-tos/project-name/#set-a-project-name

Signed-off-by: Ruben Jenster <r.jenster@drachenfels.de>
2025-03-20 12:07:07 +01:00
1aa750bacf integration/tests: Test project name override with COMPOSE_PROJECT_NAME env variable
Signed-off-by: Ruben Jenster <r.jenster@drachenfels.de>
2025-03-20 12:07:07 +01:00
98b9bb9f8e Fix interpolation for COMPOSE_PROJECT_NAME
Fixes #1073

Signed-off-by: Ruben Jenster <r.jenster@drachenfels.de>
2025-03-20 12:07:07 +01:00
170411de8b test/integration: Test COMPOSE_PROJECT_NAME interpolation
Refs #1073

Signed-off-by: Ruben Jenster <r.jenster@drachenfels.de>
2025-03-20 12:07:01 +01:00
0cf1378cb5 Merge pull request #1148 from mokazemi/fix/sigint-down
Handle SIGINT when running "up" command to shutdown gracefully
2025-03-20 00:14:11 +02:00
f5a6df6dc4 added changes to release notes
Signed-off-by: Mohammad Kazemi <mokazemi@disroot.org>
2025-03-19 16:02:34 +03:30
f106ea0c01 modifications to pass pylint test
Signed-off-by: Mohammad Kazemi <mokazemi@disroot.org>
2025-03-19 15:55:55 +03:30
b748c2666c add try-except block to handle error in case of shutdown error
Signed-off-by: Mohammad Kazemi <mokazemi@disroot.org>
2025-03-19 15:55:50 +03:30
3973c476c4 catch SIGINT signal properly in 'up' function and call compose 'down' function for a graceful shutdown
Signed-off-by: Mohammad Kazemi <mokazemi@disroot.org>
2025-03-19 15:55:38 +03:30
8b1bd0123c Merge pull request #1168 from underground-software/build_exit
Properly surface errors from build commands
2025-03-19 12:50:16 +02:00
2e7d83f7f0 Properly surface errors from build commands
the commit 38b13a3 ("Use asyncio for subprocess calls") broke the way
exit codes are reported from the podman compose build command.

The tasks are awaited as they finish which means that if a later build
finishes sucessfully after a failing build, it overwrites status.

Previously the `parse_return_code` function would skip updating the status
if the new return code was zero, but in removing it, this logic was not
carried forward.

Fixes: 38b13a3 ("Use asyncio for subprocess calls")
Signed-off-by: charliemirabile <46761267+charliemirabile@users.noreply.github.com>
2025-03-18 21:49:55 -04:00
52e2912e0b Merge pull request #1157 from mokibit/automate-selinux-test
test/integration: Automate manual `selinux` test
2025-03-11 18:15:55 +02:00
8ef537e247 test/integration: Automate manual selinux test
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-03-06 13:04:53 +02:00
04fcc26a79 Merge pull request #1117 from rjeffman/no_exception_if_service_is_not_in_compose
Don't raise exception on inexistent services in 'down' command
2025-03-05 22:17:10 +02:00
d4760712b7 Don't raise exception on inexistent services in 'down' command
When running 'podman-compose down <service>', if service is not part of
the compose, a KeyError exception is raised in function 'get_excluded'.

By only allowing evaluation of services that exist in the compose
provides a cleaner and gentler exit for this case.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2025-03-05 15:49:52 -03:00
7c61f24467 Merge pull request #1158 from mokibit/automate-uidmaps-test
test/integration: Automate manual `uidmaps` test
2025-03-04 23:41:55 +02:00
202c3771a9 test/integration: Automate manual uidmaps test
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-03-04 22:28:50 +02:00
a54f0fa573 Merge pull request #1149 from AlexandreAANP/fix/windows-asyncio-loop
Fix event loop handling for Windows platform in compose_up function
2025-03-01 16:43:26 +02:00
3353697402 Merge pull request #1152 from IamTheFij/config-quiet
Add quiet flag to podman-compose config
2025-03-01 16:42:20 +02:00
ca1b59c449 Merge pull request #1153 from IamTheFij/dco-hook
Add hook to check for signoff in commit messages
2025-03-01 16:39:50 +02:00
b9f27795c0 Add hook to check for signoff in commit messages
Since this is checked on PR, it could also be checked at commit so users can avoid making commits to
the tree without expected documentation.

Signed-off-by: Ian Fijolek <ian@iamthefij.com>
2025-02-28 13:05:54 -08:00
4cd1642be0 Add quiet flag to podman-compose config
This skips printing and is useful for validating config files.

Signed-off-by: Ian Fijolek <ian@iamthefij.com>
2025-02-28 12:48:08 -08:00
fd401331e5 added release note to newsfragment directory
Signed-off-by: Alexandre Pita <alexandreanpita@gmail.com>
2025-02-27 11:45:38 +00:00
37b27fa233 Refactor event loop handling to simplify logic for Windows platforms
Signed-off-by: Alexandre Pita <alexandreanpita@gmail.com>
2025-02-26 17:28:59 +00:00
976847ef9b Merge pull request #1143 from italomaia/bug/use-ruff
Bug: replaced black with ruff on pre-commit
2025-02-26 18:21:31 +02:00
c6b3d497d6 Adds lint exclusions already ignored by the code
Added flake8 excludes to rules that are already ignored by the current
code to avoid validation issues with code that has already been
approved. Added pylint disable to line with lint offense already
accepted.

Signed-off-by: Italo Maia <italo.maia@gmail.com>
2025-02-26 17:57:34 +02:00
10ad739746 Replaces black with ruff on pre-commit-config
Current python files are already compatible with ruff, while very
incompatible with black standard therefore, this change just enforces
the reality of the codebase. Without it, pre-commit and the ci will
fight one-another with different formatting.

Signed-off-by: Italo Maia <italo.maia@gmail.com>
2025-02-26 17:57:20 +02:00
784d798dac Fix event loop handling for Windows platform in compose_up function
Signed-off-by: Alexandre Pita <alexandreanpita@gmail.com>
2025-02-26 14:38:46 +00:00
dd01d039bf Merge pull request #1140 from whym/rename-comment
Fix comment, add tests, improve coding style
2025-02-25 02:02:23 +02:00
81a0a5933e Add more logging tests
Signed-off-by: Yusuke Matsubara <whym@whym.org>
2025-02-25 01:52:45 +02:00
c289a3b827 Fix logging test coding style
Signed-off-by: Yusuke Matsubara <whym@whym.org>
2025-02-25 01:52:39 +02:00
baccce4f3f Fix comments related to logging
Signed-off-by: Yusuke Matsubara <whym@whym.org>
2025-02-25 01:38:41 +02:00
07af8488db Merge pull request #1147 from joern19/main
Allow configuration of interface_name
2025-02-24 01:26:38 +02:00
cbc5a8c8b3 Add newsfragment for interface_name option
Signed-off-by: Jörn Hirschfeld <joern@hirschfeld.tech>
2025-02-23 17:08:06 +01:00
aeaceed7ba integration test for x-podman.interface_name option
Signed-off-by: Jörn Hirschfeld <joern@hirschfeld.tech>
2025-02-23 17:08:00 +01:00
b1eb558b41 Document existence of x-podman.interface_name
Signed-off-by: Jörn Hirschfeld <joern@hirschfeld.tech>
2025-02-23 17:04:10 +01:00
1cdc9e6552 interface_name can be specified in net_config_
Signed-off-by: Jörn Hirschfeld <joern@hirschfeld.tech>
2025-02-23 17:04:10 +01:00
2f8ed2137c Merge pull request #1144 from mokibit/automate-secrets-tests
test/integration: Automate manual `secrets` test
2025-02-20 09:57:11 +02:00
838957b902 test/integration: Automate manual 'secrets' test
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-02-19 21:42:52 +02:00
15380a809d Merge pull request #1135 from rpluem-vf/keep_fds_open
Do not close file descriptors when executing podman
2025-02-18 13:26:15 +02:00
d4e5859370 Do not close file descriptors when executing podman
Do not close file descriptors when executing podman. This allows
externally created file descriptors to be passed to containers.
These file descriptors might have been created through systemd
socket activation. See also
https://github.com/containers/podman/blob/main/docs/tutorials/socket_activation.md#socket-activation-of-containers

Signed-off-by: Ruediger Pluem <ruediger.pluem@vodafone.com>
2025-02-12 11:35:51 +01:00
593d7c825e Merge pull request #1138 from mokibit/automate-seccomp-test
tests/integration: Automate manual `seccomp` test
2025-02-07 22:46:39 +02:00
bfba7ba32d tests/integration: Automate manual seccomp test
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-02-07 22:38:58 +02:00
fe9be2d98f Merge pull request #1133 from neocturne/pod-args
Implement x-podman.pod_args to override --pod-args default
2025-02-07 20:59:36 +02:00
43a2f1d01f Implement x-podman.pod_args to override --pod-args default
Allow setting an argument list as x-podman.pod_args to override the
default value `--infra=false --share=`. `--pod-args` passed on the command
line takes precedence over the value set in docker-compose.yml; the values
are not merged.

Fixes #1057.
Signed-off-by: Matthias Schiffer <mschiffer@universe-factory.net>
2025-02-07 12:11:19 +01:00
aa47a373ca Merge pull request #1132 from mokibit/describe-test_pid
tests/integration: Describe `pid` test
2025-02-06 15:29:42 +02:00
eaec19336f tests/integration: Describe pid test
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-02-06 15:16:44 +02:00
34bee28bb8 Merge pull request #1131 from mokibit/automate-test-no_services
tests/integration: Automate manual `no_services` test
2025-02-05 20:53:38 +02:00
bfea139362 tests/integration: Automate manual no_services test
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-02-05 20:47:59 +02:00
4a81bce2a5 Merge pull request #1130 from mokibit/automate-nets_test_ip
tests/integration: Automate manual `nets_test_ip` test
2025-02-05 20:44:07 +02:00
e626f15eff tests/integration: Automate manual nets_test_ip test
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-02-05 19:31:43 +02:00
974250caa5 Merge pull request #1128 from mokibit/automate-nets_test3
tests/integration: Automate manual `nets_test3` test
2025-02-03 22:59:45 +02:00
29404af723 tests/integration: Automate manual 'nets_test3' test
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-02-03 19:04:15 +02:00
d1ba2f4c7d Merge pull request #1124 from notdian/file_not_found_error
remove incorrect os.chdir, handle relative extends/includes
2025-01-31 01:48:13 +02:00
e03d675b9b Remove incorrect os.chdir call to fix folder error
Signed-off-by: notdian <dian@fishekqi.com>
2025-01-31 01:24:24 +02:00
51d180d2d0 Merge pull request #1120 from mokibit/inform-user-to-use-newer-python
Throw a readable error on too old Python
2025-01-27 21:53:24 +02:00
9c905f9012 Merge pull request #1116 from Zeglius/dockerfile_inline
Add support for dockerfile_inline
2025-01-27 21:52:05 +02:00
bdb3e4e984 Throw a readable error on too old Python
podman-compose v1.0.6 is the last to support Python3.6. When newer
podman-compose version is used with too old Python, podman-compose gives
only a confusing error. This commit gives a clear message to use
upgraded Python version.
A descriptive error can not be thrown, as line "from __future__ imports"
must occur at the beginning of the file, but older Python (older than
Python3.7) does not recognize __future__ and throws an error
immediately.
Therefore, a comment is used to inform the user to update his Python
version.

Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-27 21:49:50 +02:00
105e390f6b Add support for dockerfile_inline
Fixes #864

Signed-off-by: Zeglius <33781398+Zeglius@users.noreply.github.com>
2025-01-27 21:45:42 +02:00
d79ff01e77 Merge pull request #1113 from mokibit/categorize-integration-tests
tests/integration: Categorize integration tests
2025-01-23 02:17:08 +02:00
d9ef3d2cc6 tests/integration: Add missing __init__.py for network_scoped_aliases
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
d23ef4f481 tests/integration: Add missing __init__.py for build_labels test
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
b685bce400 tests/integration: Move test utils to one test_utils file
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
7d5bf645f6 tests/integration: Move test "vol" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
9f7ae38bac tests/integration: Move test "ports" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
3cee4e015c tests/integration: Move test "config" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
498a1994ce tests/integration: Move test "env" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
488908f809 tests/integration: Move test "env_file" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
f7bcc4221e tests/integration: Move test "up_down" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
a73df712cc tests/integration: Move test "build_ulimits" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
50dc19f5f8 tests/integration: Move test "network" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
9029dce0f6 tests/integration: Move test "nets_test2" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
a8282c77d6 tests/integration: Move test "nets_test1" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
f4b775c7e4 tests/integration: Move test "nethost" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
adf30e0475 tests/integration: Move test "multicompose" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
41675c3916 tests/integration: Move test "ipam_default" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
6caf2eae42 tests/integration: Move test "interpolation" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
3093b00326 tests/integration: Move test "include" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
1c21d655ba tests/integration: Move test "in_pod" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
18e5fd64f3 tests/integration: Move test "filesystem" to corresponding dir
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
24bdfd1e17 tests/integration: Move test "extends_w_file_subdir" to corresp. dir
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
c2d3e156c2 tests/integration: Move test "extends_w_file" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
ba95100cff tests/integration: Move test "extends_w_empty_service" to corresp. dir
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:11:46 +02:00
6022669991 tests/integration: Move test "extends" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:01:44 +02:00
e29df71d42 tests/integration: Move test "exit_from" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 02:01:44 +02:00
21b9d385b2 tests/integration: Move test "deps" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 01:48:47 +02:00
4c17ce2434 tests/integration: Move test "default_net_behavior" to corresponding dir
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 01:48:47 +02:00
09d54e9dcc tests/integration: Move test "build_secrets" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 01:48:47 +02:00
f1dd9b374e tests/integration: Move test "build_fail" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 01:48:47 +02:00
87af67fe94 tests/integration: Move test "network_scoped_aliases" to corresp. dir
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 01:48:47 +02:00
f1d663874e tests/integration: Move test "build" to corresponding directory
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 01:48:47 +02:00
69ffff33f6 tests/integration: Move test "additional_contexts" to corresp. dir
Signed-off-by: Monika Kairaityte <monika@kibit.lt>
2025-01-23 01:48:47 +02:00
f376700972 Merge pull request #1104 from rjeffman/disable_dns
Add support for disable_dns, dns and ignore on network creation
2025-01-19 19:41:30 +02:00
9be3ec985f Add network "dns" support
This patch add 'x-podman.dns' option to the 'network' configuration,
allowing users to set the DNS resolvers for a defined network.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2025-01-17 13:58:13 -03:00
6e642dca1f Add network "disable-dns" support
Podman allows to create a network disabling the DNS plugin with
'--disable-dns', but this option is not available in the compose spec.

This patch add 'x-podman.disable-dns' to the podman-compose options,
allowing the creation of a network with the DNS plugin disabled.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2025-01-17 12:14:15 -03:00
0f2c717655 Merge pull request #1110 from indrat/1105-service-env-vars
expand service environment_variables before adding to subs_dict
2025-01-16 20:13:39 +02:00
2aa042b9c7 expand service environment_variables before adding to subs_dict
Also modifies an existing integration test to expect an empty string as `docker-compose` warns that
`ZZVAR3` is not set and defaults it to an empty string per the acutal output here.

```yaml
$ docker-compose -f container-compose.load-.env-in-project.yaml config
WARN[0000] The "ZZVAR3" variable is not set. Defaulting to a blank string.
name: project
services:
  app:
    command:
      - /bin/busybox
      - sh
      - -c
      - env | grep ZZ
    environment:
      ZZVAR1: This value is loaded but should be overwritten
      ZZVAR2: This value is loaded from .env in project/ directory
      ZZVAR3: ""
...
```

Signed-off-by: indra <indra.talip@gmail.com>
2025-01-16 21:00:49 +11:00
a177603661 Merge pull request #1108 from bailsman/fix-398
Fixes #398: exclude deps on up if --no-deps
2025-01-15 23:51:37 +02:00
bc4177fbdc Exclude dependent containers on up if --no-deps.
Fixes #398.

Signed-off-by: Emanuel Rietveld <e.j.rietveld@gmail.com>
2025-01-15 22:32:28 +01:00
8206cc3ea2 Run should not add --requires if --no-deps.
Fixes #717.

Signed-off-by: Emanuel Rietveld <e.j.rietveld@gmail.com>
2025-01-15 22:29:51 +01:00
60ac5e43b3 Merge pull request #1107 from containers/dependabot/pip/virtualenv-20.26.6
build(deps): bump virtualenv from 20.25.1 to 20.26.6
2025-01-13 22:17:09 +02:00
48c6c38fcd build(deps): bump virtualenv from 20.25.1 to 20.26.6
Bumps [virtualenv](https://github.com/pypa/virtualenv) from 20.25.1 to 20.26.6.
- [Release notes](https://github.com/pypa/virtualenv/releases)
- [Changelog](https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/virtualenv/compare/20.25.1...20.26.6)

---
updated-dependencies:
- dependency-name: virtualenv
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-13 18:24:07 +00:00
84f1fbd622 Merge pull request #1101 from p12tic/post-release-fixes
RELEASING: Fix release command
2025-01-07 21:24:36 +02:00
ac5291e10b RELEASING: Fix release command
Signed-off-by: Povilas Kanapickas <povilas@radix.lt>
2025-01-07 21:18:35 +02:00
163 changed files with 3730 additions and 738 deletions

View File

@ -1,17 +1,10 @@
default_install_hook_types: [pre-commit, commit-msg]
repos:
- repo: https://github.com/psf/black
rev: 23.3.0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.6
hooks:
- id: black
# It is recommended to specify the latest version of Python
# supported by your project here, or alternatively use
# pre-commit's default_language_version, see
# https://pre-commit.com/#top_level-default_language_version
language_version: python3.10
- id: ruff
types: [python]
args: [
"--check", # Don't apply changes automatically
]
- repo: https://github.com/pycqa/flake8
rev: 6.0.0
hooks:
@ -34,3 +27,8 @@ repos:
rev: v2.2.5
hooks:
- id: codespell
- repo: https://github.com/gklein/check_signoff
rev: v1.0.5
hooks:
- id: check-signoff

View File

@ -1,7 +1,7 @@
[MESSAGES CONTROL]
# C0111 missing-docstring: missing-class-docstring, missing-function-docstring, missing-method-docstring, missing-module-docstrin
# consider-using-with: we need it for color formatter pipe
disable=too-many-lines,too-many-branches,too-many-locals,too-many-statements,too-many-arguments,too-many-instance-attributes,fixme,multiple-statements,missing-docstring,line-too-long,consider-using-f-string,consider-using-with,unnecessary-lambda-assignment
disable=too-many-lines,too-many-branches,too-many-locals,too-many-statements,too-many-arguments,too-many-instance-attributes,fixme,multiple-statements,missing-docstring,line-too-long,consider-using-f-string,consider-using-with,unnecessary-lambda-assignment,broad-exception-caught
# allow _ for ignored variables
# allow generic names like a,b,c and i,j,k,l,m,n and x,y,z
# allow k,v for key/value

View File

@ -35,7 +35,7 @@ Pull the merge commit created on the `main` branch during the step 2.
Then run:
```
./scripts/make_release.sh
./scripts/make_release.sh $VERSION
```
This will create release commit, tag and push everything.

39
docs/Changelog-1.4.0.md Normal file
View File

@ -0,0 +1,39 @@
Version 1.4.0 (2025-05-10)
==========================
Bug fixes
---------
- Fixed handling of relative includes and extends in compose files
- Fixed error when merging arguments in list and dictionary syntax
- Fixed issue where short-lived containers could execute twice when using `up` in detached mode
- Fixed `up` command hanging on Podman versions earlier than 4.6.0
- Fixed issue where `service_healthy` conditions weren't enforced during `up` command
- Fixed support for the `--scale` flag
- Fixed bug causing dependent containers to start despite `--no-deps` flag
- Fixed port command behavior for dynamic host ports
- Fixed interpolation of `COMPOSE_PROJECT_NAME` when set from top-level `name` in compose file
- Fixed project name evaluation order to match compose spec
- Fixed build context when using git URLs
- Fixed `KeyError` when `down` is called with non-existent service
- Skip `down` during `up` when no active containers exist
- Fixed non-zero exit code on failure when using `up -d`
- Fixed SIGINT handling during `up` command for graceful shutdown
- Fixed `NotImplementedError` when interrupted on Windows
Features
--------
- Added `--quiet` flag to `config` command to suppress output
- Added support for `pids_limit` and `deploy.resources.limits.pids`
- Added `--abort-on-container-failure` option
- Added `--rmi` argument to `down` command for image removal
- Added support for `x-podman.disable-dns` to disable DNS plugin on defined networks
- Added support for `x-podman.dns` to set DNS nameservers for defined networks
- Improved file descriptor handling - no longer closes externally created descriptors.
This allows descriptors created e.g. via systemd socket activation to be passed to
containers.
- Added support for `cpuset` configuration
- Added support for `reset` and `override` tags when merging compose files
- Added support for `x-podman.interface_name` to set network interface names
- Added support for `x-podman.pod_args` to override default `--pod-args`

View File

@ -27,6 +27,26 @@ services:
For explanations of these extensions, please refer to the [Podman Documentation](https://docs.podman.io/).
## Network management
The following extension keys are available under network configuration:
* `x-podman.disable-dns` - Disable the DNS plugin for the network when set to 'true'.
* `x-podman.dns` - Set nameservers for the network using supplied addresses (cannot be used with x-podman.disable-dns`).
For example, the following docker-compose.yml allows all containers on the same network to use the
specified nameservers:
```yml
version: "3"
network:
my_network:
x-podman.dns:
- "10.1.2.3"
- "10.1.2.4"
```
For explanations of these extensions, please refer to the
[Podman network create command Documentation](https://docs.podman.io/en/latest/markdown/podman-network-create.1.html).
## Per-network MAC-addresses
@ -66,7 +86,7 @@ networks:
- subnet: "192.168.1.0/24"
services:
webserver
webserver:
image: "busybox"
command: ["/bin/busybox", "httpd", "-f", "-h", "/etc", "-p", "8001"]
networks:
@ -78,6 +98,10 @@ services:
mac_address: "02:bb:bb:bb:bb:bb" # mac_address is supported
```
## Per-network interface name
Using `x-podman.interface_name` within a containers network config you can specify the interface name inside the container.
## Podman-specific network modes
Generic docker-compose supports the following values for `network-mode` for a container:
@ -154,3 +178,17 @@ services:
x-podman:
in_pod: false
```
It is also possible to override the default arguments for pod creation that are
used when --pod-args is not passed on the command line:
```yml
version: "3"
services:
cont:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"]
x-podman:
pod_args: ["--infra=false", "--share=", "--cpus=1"]
```
When not set in docker-compose.yml or on the command line, the pod args default
to `["--infra=false", "--share="]`.

View File

@ -0,0 +1,9 @@
---
version: '3'
services:
dummy:
build:
context: .
dockerfile_inline: |
FROM alpine
RUN echo "hello world"

View File

@ -7,7 +7,7 @@
# https://docs.docker.com/compose/django/
# https://docs.docker.com/compose/wordpress/
# TODO: podman pod logs --color -n -f pod_testlogs
from __future__ import annotations
from __future__ import annotations # If you see an error here, use Python 3.7 or greater
import argparse
import asyncio.exceptions
@ -15,7 +15,6 @@ import asyncio.subprocess
import getpass
import glob
import hashlib
import itertools
import json
import logging
import os
@ -25,6 +24,8 @@ import shlex
import signal
import subprocess
import sys
import tempfile
import urllib.parse
from asyncio import Task
from enum import Enum
@ -39,7 +40,7 @@ except ImportError:
import yaml
from dotenv import dotenv_values
__version__ = "1.3.0"
__version__ = "1.4.0"
script = os.path.realpath(sys.argv[0])
@ -270,7 +271,6 @@ def rec_subs(value, subs_dict):
svc_envs = {k: v for k, v in value['environment'].items() if k not in subs_dict}
# we need to add `svc_envs` to the `subs_dict` so that it can evaluate the
# service environment that reference to another service environment.
subs_dict.update(svc_envs)
svc_envs = rec_subs(svc_envs, subs_dict)
subs_dict.update(svc_envs)
@ -771,6 +771,22 @@ def container_to_cpu_res_args(cnt, podman_args):
str(mem_res).lower(),
))
# Handle pids limit from both container level and deploy section
pids_limit = cnt.get("pids_limit")
deploy_pids = limits.get("pids")
# Ensure consistency between pids_limit and deploy.resources.limits.pids
if pids_limit is not None and deploy_pids is not None:
if str(pids_limit) != str(deploy_pids):
raise ValueError(
f"Inconsistent PIDs limit: pids_limit ({pids_limit}) and "
f"deploy.resources.limits.pids ({deploy_pids}) must be the same"
)
final_pids_limit = pids_limit if pids_limit is not None else deploy_pids
if final_pids_limit is not None:
podman_args.extend(["--pids-limit", str(final_pids_limit)])
def port_dict_to_str(port_desc):
# NOTE: `mode: host|ingress` is ignored
@ -833,6 +849,13 @@ def get_network_create_args(net_desc, proj_name, net_name):
ipam_config_ls = ipam.get("config", [])
if net_desc.get("enable_ipv6"):
args.append("--ipv6")
if net_desc.get("x-podman.disable_dns"):
args.append("--disable-dns")
if net_desc.get("x-podman.dns"):
args.extend((
"--dns",
",".join(norm_as_list(net_desc.get("x-podman.dns"))),
))
if isinstance(ipam_config_ls, dict):
ipam_config_ls = [ipam_config_ls]
@ -982,6 +1005,7 @@ def get_net_args_from_networks(compose, cnt):
default_net_name = default_network_name_for_project(compose, net_, is_ext)
net_name = ext_desc.get("name") or net_desc.get("name") or default_net_name
interface_name = net_config_.get("x-podman.interface_name")
ipv4 = net_config_.get("ipv4_address")
ipv6 = net_config_.get("ipv6_address")
# Note: mac_address is supported by compose spec now, and x-podman.mac_address
@ -999,6 +1023,8 @@ def get_net_args_from_networks(compose, cnt):
mac_address = None
net_options = []
if interface_name:
net_options.append(f"interface_name={interface_name}")
if ipv4:
net_options.append(f"ip={ipv4}")
if ipv6:
@ -1020,7 +1046,7 @@ def get_net_args_from_networks(compose, cnt):
return net_args
async def container_to_args(compose, cnt, detached=True):
async def container_to_args(compose, cnt, detached=True, no_deps=False):
# TODO: double check -e , --add-host, -v, --read-only
dirname = compose.dirname
pod = cnt.get("pod", "")
@ -1035,7 +1061,7 @@ async def container_to_args(compose, cnt, detached=True):
deps = []
for dep_srv in cnt.get("_deps", []):
deps.extend(compose.container_names_by_service.get(dep_srv.name, []))
if deps:
if deps and not no_deps:
deps_csv = ",".join(deps)
podman_args.append(f"--requires={deps_csv}")
sec = norm_as_list(cnt.get("security_opt"))
@ -1179,6 +1205,10 @@ async def container_to_args(compose, cnt, detached=True):
if cnt.get("runtime"):
podman_args.extend(["--runtime", cnt["runtime"]])
cpuset = cnt.get("cpuset")
if cpuset is not None:
podman_args.extend(["--cpuset-cpus", cpuset])
# WIP: healthchecks are still work in progress
healthcheck = cnt.get("healthcheck", {})
if not isinstance(healthcheck, dict):
@ -1289,7 +1319,8 @@ class ServiceDependencyCondition(Enum):
try:
return docker_to_podman_cond[value]
except KeyError:
raise ValueError(f"Value '{value}' is not a valid condition for a service dependency") # pylint: disable=raise-missing-from
# pylint: disable-next=raise-missing-from
raise ValueError(f"Value '{value}' is not a valid condition for a service dependency")
class ServiceDependency:
@ -1376,6 +1407,57 @@ def flat_deps(services, with_extends=False):
rec_deps(services, name)
###################
# Override and reset tags
###################
class OverrideTag(yaml.YAMLObject):
yaml_dumper = yaml.Dumper
yaml_loader = yaml.SafeLoader
yaml_tag = '!override'
def __init__(self, value):
if len(value) > 0 and isinstance(value[0], tuple):
self.value = {}
# item is a tuple representing service's lower level key and value
for item in value:
# value can actually be a list, then all the elements from the list have to be
# collected
if isinstance(item[1].value, list):
self.value[item[0].value] = [item.value for item in item[1].value]
else:
self.value[item[0].value] = item[1].value
else:
self.value = [item.value for item in value]
@classmethod
def from_yaml(cls, loader, node):
return OverrideTag(node.value)
@classmethod
def to_yaml(cls, dumper, data):
return dumper.represent_scalar(cls.yaml_tag, data.value)
class ResetTag(yaml.YAMLObject):
yaml_dumper = yaml.Dumper
yaml_loader = yaml.SafeLoader
yaml_tag = '!reset'
@classmethod
def to_json(cls):
return cls.yaml_tag
@classmethod
def from_yaml(cls, loader, node):
return ResetTag()
@classmethod
def to_yaml(cls, dumper, data):
return dumper.represent_scalar(cls.yaml_tag, '')
async def wait_with_timeout(coro, timeout):
"""
Asynchronously waits for the given coroutine to complete with a timeout.
@ -1454,10 +1536,8 @@ class Podman:
chunk = await self._readchunk(reader)
parts = chunk.split(b"\n")
# Iff parts ends with '', the last part is a incomplete line;
# The rest are complete lines
for i, part in enumerate(parts):
# Iff part is last and non-empty, we leave an ongoing line to be completed later
if i < len(parts) - 1:
_formatted_print_with_nl(part.decode())
line_ongoing = False
@ -1465,7 +1545,8 @@ class Podman:
_formatted_print_without_nl(part.decode())
line_ongoing = True
if line_ongoing:
print(file=sink, end="\n") # End the unfinished line
# Make sure the last line ends with EOL
print(file=sink, end="\n")
def exec(
self,
@ -1499,7 +1580,10 @@ class Podman:
if log_formatter is not None:
p = await asyncio.create_subprocess_exec(
*cmd_ls, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
*cmd_ls,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
close_fds=False,
) # pylint: disable=consider-using-with
# This is hacky to make the tasks not get garbage collected
@ -1517,7 +1601,7 @@ class Podman:
err_t.add_done_callback(task_reference.discard)
else:
p = await asyncio.create_subprocess_exec(*cmd_ls) # pylint: disable=consider-using-with
p = await asyncio.create_subprocess_exec(*cmd_ls, close_fds=False) # pylint: disable=consider-using-with
try:
exit_code = await p.wait()
@ -1572,6 +1656,12 @@ class Podman:
def normalize_service(service, sub_dir=""):
if isinstance(service, ResetTag):
return service
if isinstance(service, OverrideTag):
service = service.value
if "build" in service:
build = service["build"]
if isinstance(build, str):
@ -1594,6 +1684,9 @@ def normalize_service(service, sub_dir=""):
for k, v in build["additional_contexts"].items():
new_additional_contexts.append(f"{k}={v}")
build["additional_contexts"] = new_additional_contexts
if "build" in service and "args" in service["build"]:
if isinstance(build["args"], dict):
build["args"] = norm_as_list(build["args"])
for key in ("command", "entrypoint"):
if key in service:
if isinstance(service[key], str):
@ -1647,6 +1740,8 @@ def normalize_service_final(service: dict, project_dir: str) -> dict:
if "build" in service:
build = service["build"]
context = build if isinstance(build, str) else build.get("context", ".")
if not is_path_git_url(context):
context = os.path.normpath(os.path.join(project_dir, context))
if not isinstance(service["build"], dict):
service["build"] = {}
@ -1670,6 +1765,8 @@ def rec_merge_one(target, source):
update target from source recursively
"""
done = set()
remove = set()
for key, value in source.items():
if key in target:
continue
@ -1679,15 +1776,37 @@ def rec_merge_one(target, source):
if key in done:
continue
if key not in source:
if isinstance(value, ResetTag):
log("INFO: Unneeded !reset found for [{key}]")
remove.add(key)
if isinstance(value, OverrideTag):
log("INFO: Unneeded !override found for [{key}] with value '{value}'")
target[key] = clone(value.value)
continue
value2 = source[key]
if isinstance(value, ResetTag) or isinstance(value2, ResetTag):
remove.add(key)
continue
if isinstance(value, OverrideTag) or isinstance(value2, OverrideTag):
target[key] = (
clone(value.value) if isinstance(value, OverrideTag) else clone(value2.value)
)
continue
if key in ("command", "entrypoint"):
target[key] = clone(value2)
continue
if not isinstance(value2, type(value)):
value_type = type(value)
value2_type = type(value2)
raise ValueError(f"can't merge value of [{key}] of type {value_type} and {value2_type}")
if is_list(value2):
if key == "volumes":
# clean duplicate mount targets
@ -1704,6 +1823,10 @@ def rec_merge_one(target, source):
rec_merge_one(value, value2)
else:
target[key] = value2
for key in remove:
del target[key]
return target
@ -1868,6 +1991,15 @@ class PodmanCompose:
# otherwise use `in_pod` value provided by command line
return self.global_args.in_pod_bool
def resolve_pod_args(self):
# Priorities:
# - Command line --pod-args
# - docker-compose.yml x-podman.pod_args
# - Default value
if self.global_args.pod_args is not None:
return shlex.split(self.global_args.pod_args)
return self.x_podman.get("pod_args", ["--infra=false", "--share="])
def _parse_compose_file(self):
args = self.global_args
# cmd = args.command
@ -1918,9 +2050,6 @@ class PodmanCompose:
dotenv_path = os.path.realpath(args.env_file)
dotenv_dict.update(dotenv_to_dict(dotenv_path))
# TODO: remove next line
os.chdir(dirname)
os.environ.update({
key: value for key, value in dotenv_dict.items() if key.startswith("PODMAN_")
})
@ -1961,12 +2090,41 @@ class PodmanCompose:
sys.exit(1)
content = normalize(content)
# log(filename, json.dumps(content, indent = 2))
# See also https://docs.docker.com/compose/how-tos/project-name/#set-a-project-name
# **project_name** is initialized to the argument of the `-p` command line flag.
if not project_name:
project_name = self.environ.get("COMPOSE_PROJECT_NAME")
if not project_name:
project_name = content.get("name")
if not project_name:
project_name = dir_basename.lower()
# More strict then actually needed for simplicity:
# podman requires [a-zA-Z0-9][a-zA-Z0-9_.-]*
project_name_normalized = norm_re.sub("", project_name)
if not project_name_normalized:
raise RuntimeError(f"Project name [{project_name}] normalized to empty")
project_name = project_name_normalized
self.project_name = project_name
self.environ.update({"COMPOSE_PROJECT_NAME": self.project_name})
content = rec_subs(content, self.environ)
if isinstance(services := content.get('services'), dict):
for service in services.values():
if not isinstance(service, OverrideTag) and not isinstance(service, ResetTag):
if 'extends' in service and (
service_file := service['extends'].get('file')
):
service['extends']['file'] = os.path.join(
os.path.dirname(filename), service_file
)
rec_merge(compose, content)
# If `include` is used, append included files to files
include = compose.get("include")
if include:
files.extend(include)
files.extend([os.path.join(os.path.dirname(filename), i) for i in 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.
@ -1986,19 +2144,6 @@ class PodmanCompose:
log.debug(" ** merged:\n%s", json.dumps(compose, indent=2))
# ver = compose.get('version')
if not project_name:
project_name = compose.get("name")
if project_name is None:
# More strict then actually needed for simplicity:
# podman requires [a-zA-Z0-9][a-zA-Z0-9_.-]*
project_name = self.environ.get("COMPOSE_PROJECT_NAME", dir_basename.lower())
project_name = norm_re.sub("", project_name)
if not project_name:
raise RuntimeError(f"Project name [{dir_basename}] normalized to empty")
self.project_name = project_name
self.environ.update({"COMPOSE_PROJECT_NAME": self.project_name})
services = compose.get("services")
if services is None:
services = {}
@ -2069,6 +2214,22 @@ class PodmanCompose:
container_names_by_service = {}
self.services = services
for service_name, service_desc in services.items():
replicas = 1
if "scale" in args and args.scale is not None:
# Check `--scale` args from CLI command
scale_args = args.scale.split('=')
if service_name == scale_args[0]:
replicas = try_int(scale_args[1], fallback=1)
elif "scale" in service_desc:
# Check `scale` value from compose yaml file
replicas = try_int(service_desc.get("scale"), fallback=1)
elif (
"deploy" in service_desc
and "replicas" in service_desc.get("deploy", {})
and "replicated" == service_desc.get("deploy", {}).get("mode", '')
):
# Check `deploy: replicas:` value from compose yaml file
# Note: All conditions are necessary to handle case
replicas = try_int(service_desc.get("deploy", {}).get("replicas"), fallback=1)
container_names_by_service[service_name] = []
@ -2122,6 +2283,7 @@ class PodmanCompose:
self.x_podman = compose.get("x-podman", {})
args.in_pod_bool = self.resolve_in_pod()
args.pod_arg_list = self.resolve_pod_args()
pods, containers = transform(args, project_name, given_containers)
self.pods = pods
self.containers = containers
@ -2200,7 +2362,7 @@ class PodmanCompose:
help="custom arguments to be passed to `podman pod`",
metavar="pod_args",
type=str,
default="--infra=false --share=",
default=None,
)
parser.add_argument(
"--env-file",
@ -2465,12 +2627,38 @@ async def compose_push(compose, args):
await compose.podman.run([], "push", [cnt["image"]])
def container_to_build_args(compose, cnt, args, path_exists):
def is_path_git_url(path):
r = urllib.parse.urlparse(path)
return r.scheme == 'git' or r.path.endswith('.git')
def container_to_build_args(compose, cnt, args, path_exists, cleanup_callbacks=None):
build_desc = cnt["build"]
if not hasattr(build_desc, "items"):
build_desc = {"context": build_desc}
ctx = build_desc.get("context", ".")
dockerfile = build_desc.get("dockerfile")
dockerfile = build_desc.get("dockerfile", "")
dockerfile_inline = build_desc.get("dockerfile_inline")
if dockerfile_inline is not None:
dockerfile_inline = str(dockerfile_inline)
# Error if both `dockerfile_inline` and `dockerfile` are set
if dockerfile and dockerfile_inline:
raise OSError("dockerfile_inline and dockerfile can't be used simultaneously")
dockerfile = tempfile.NamedTemporaryFile(delete=False, suffix=".containerfile")
dockerfile.write(dockerfile_inline.encode())
dockerfile.close()
dockerfile = dockerfile.name
def cleanup_temp_dockfile():
if os.path.exists(dockerfile):
os.remove(dockerfile)
if cleanup_callbacks is not None:
list.append(cleanup_callbacks, cleanup_temp_dockfile)
build_args = []
if not is_path_git_url(ctx):
if dockerfile:
dockerfile = os.path.join(ctx, dockerfile)
else:
@ -2486,9 +2674,16 @@ def container_to_build_args(compose, cnt, args, path_exists):
dockerfile = os.path.join(ctx, dockerfile)
if path_exists(dockerfile):
break
if not path_exists(dockerfile):
raise OSError("Dockerfile not found in " + ctx)
build_args = ["-f", dockerfile, "-t", cnt["image"]]
if path_exists(dockerfile):
# normalize dockerfile path, as the user could have provided unpredictable file formats
dockerfile = os.path.normpath(os.path.join(ctx, dockerfile))
build_args.extend(["-f", dockerfile])
else:
raise OSError(f"Dockerfile not found in {ctx}")
build_args.extend(["-t", cnt["image"]])
if "platform" in cnt:
build_args.extend(["--platform", cnt["platform"]])
for secret in build_desc.get("secrets", []):
@ -2540,8 +2735,13 @@ async def build_one(compose, args, cnt):
if img_id:
return None
build_args = container_to_build_args(compose, cnt, args, os.path.exists)
cleanup_callbacks = []
build_args = container_to_build_args(
compose, cnt, args, os.path.exists, cleanup_callbacks=cleanup_callbacks
)
status = await compose.podman.run([], "build", build_args)
for c in cleanup_callbacks:
c()
return status
@ -2563,7 +2763,7 @@ async def compose_build(compose, args):
status = 0
for t in asyncio.as_completed(tasks):
s = await t
if s is not None:
if s is not None and s != 0:
status = s
return status
@ -2582,9 +2782,7 @@ async def create_pods(compose, args): # pylint: disable=unused-argument
podman_args = [
"create",
"--name=" + pod["name"],
]
if args.pod_args:
podman_args.extend(shlex.split(args.pod_args))
] + args.pod_arg_list
# if compose.podman_version and not strverscmp_lt(compose.podman_version, "3.4.0"):
# podman_args.append("--infra-name={}_infra".format(pod["name"]))
ports = pod.get("ports", [])
@ -2600,6 +2798,8 @@ def get_excluded(compose, args):
if args.services:
excluded = set(compose.services)
for service in args.services:
# we need 'getattr' as compose_down_parse dose not configure 'no_deps'
if service in compose.services and not getattr(args, "no_deps", False):
excluded -= set(x.name for x in compose.services[service]["_deps"])
excluded.discard(service)
log.debug("** excluding: %s", excluded)
@ -2615,6 +2815,18 @@ async def check_dep_conditions(compose: PodmanCompose, deps: set) -> None:
deps_cd = []
for d in deps:
if d.condition == condition:
if (
d.condition
in (ServiceDependencyCondition.HEALTHY, ServiceDependencyCondition.UNHEALTHY)
) and strverscmp_lt(compose.podman_version, "4.6.0"):
log.warning(
"Ignored %s condition check due to podman %s doesn't support %s!",
d.name,
compose.podman_version,
condition.value,
)
continue
deps_cd.extend(compose.container_names_by_service[d.name])
if deps_cd:
@ -2658,14 +2870,24 @@ async def run_container(
return await compose.podman.run(*command, log_formatter=log_formatter)
def deps_from_container(args, cnt):
if args.no_deps:
return set()
return cnt['_deps']
@cmd_run(podman_compose, "up", "Create and start the entire stack or some of its services")
async def compose_up(compose: PodmanCompose, args):
excluded = get_excluded(compose, args)
if not args.no_build:
# `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:
build_exit_code = await compose.commands["build"](compose, build_args)
if build_exit_code != 0:
log.error("Build command failed")
if not args.dry_run:
return build_exit_code
hashes = (
(
@ -2685,26 +2907,37 @@ async def compose_up(compose: PodmanCompose, args):
.splitlines()
)
diff_hashes = [i for i in hashes if i and i != compose.yaml_hash]
if args.force_recreate or len(diff_hashes):
if (args.force_recreate and len(hashes) > 0) or len(diff_hashes):
log.info("recreating: ...")
down_args = argparse.Namespace(**dict(args.__dict__, volumes=False))
down_args = argparse.Namespace(**dict(args.__dict__, volumes=False, rmi=None))
await compose.commands["down"](compose, down_args)
log.info("recreating: done\n\n")
# args.no_recreate disables check for changes (which is not implemented)
podman_command = "run" if args.detach and not args.no_start else "create"
await create_pods(compose, args)
exit_code = 0
for cnt in compose.containers:
if cnt["_service"] in excluded:
log.debug("** skipping: %s", cnt["name"])
continue
podman_args = await container_to_args(compose, cnt, detached=args.detach)
subproc = await compose.podman.run([], podman_command, podman_args)
if podman_command == "run" and subproc is not None:
await run_container(compose, cnt["name"], cnt["_deps"], ([], "start", [cnt["name"]]))
if args.no_start or args.detach or args.dry_run:
return
podman_args = await container_to_args(compose, cnt, detached=False, no_deps=args.no_deps)
subproc_exit_code = await compose.podman.run([], "create", podman_args)
if subproc_exit_code is not None and subproc_exit_code != 0:
exit_code = subproc_exit_code
if not args.no_start and args.detach and subproc_exit_code is not None:
container_exit_code = await run_container(
compose, cnt["name"], deps_from_container(args, cnt), ([], "start", [cnt["name"]])
)
if container_exit_code is not None and container_exit_code != 0:
exit_code = container_exit_code
if args.dry_run:
return None
if args.no_start or args.detach:
return exit_code
# TODO: handle already existing
# TODO: if error creating do not enter loop
# TODO: colors if sys.stdout.isatty()
@ -2719,8 +2952,21 @@ async def compose_up(compose: PodmanCompose, args):
tasks = set()
async def handle_sigint():
log.info("Caught SIGINT or Ctrl+C, shutting down...")
try:
log.info("Shutting down gracefully, please wait...")
down_args = argparse.Namespace(**dict(args.__dict__, volumes=False, rmi=None))
await compose.commands["down"](compose, down_args)
except Exception as e:
log.error("Error during shutdown: %s", e)
finally:
for task in tasks:
task.cancel()
if sys.platform != 'win32':
loop = asyncio.get_event_loop()
loop.add_signal_handler(signal.SIGINT, lambda: [t.cancel("User exit") for t in tasks])
loop.add_signal_handler(signal.SIGINT, lambda: asyncio.create_task(handle_sigint()))
for i, cnt in enumerate(compose.containers):
# Add colored service prefix to output by piping output through sed
@ -2737,7 +2983,7 @@ async def compose_up(compose: PodmanCompose, args):
run_container(
compose,
cnt["name"],
cnt["_deps"],
deps_from_container(args, cnt),
([], "start", ["-a", cnt["name"]]),
log_formatter=log_formatter,
),
@ -2755,9 +3001,20 @@ async def compose_up(compose: PodmanCompose, args):
exit_code = 0
exiting = False
first_failed_task = None
while tasks:
done, tasks = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
if args.abort_on_container_exit:
if args.abort_on_container_failure and first_failed_task is None:
# Generally a single returned item when using asyncio.FIRST_COMPLETED, but that's not
# guaranteed. If multiple tasks finish at the exact same time the choice of which
# finished "first" is arbitrary
for t in done:
if t.result() != 0:
first_failed_task = t
if args.abort_on_container_exit or first_failed_task:
if not exiting:
# If 2 containers exit at the exact same time, the cancellation of the other ones
# cause the status to overwrite. Sleeping for 1 seems to fix this and make it match
@ -2768,6 +3025,11 @@ async def compose_up(compose: PodmanCompose, args):
t.cancel()
t: Task
exiting = True
if first_failed_task:
# Matches docker-compose behaviour, where the exit code of the task that triggered
# the cancellation is always propagated when aborting on failure
exit_code = first_failed_task.result()
else:
for t in done:
if t.get_name() == exit_code_from:
exit_code = t.result()
@ -2799,7 +3061,6 @@ async def compose_down(compose: PodmanCompose, args):
containers = list(reversed(compose.containers))
down_tasks = []
for cnt in containers:
if cnt["_service"] in excluded:
continue
@ -2820,8 +3081,10 @@ async def compose_down(compose: PodmanCompose, args):
if cnt["_service"] in excluded:
continue
await compose.podman.run([], "rm", [cnt["name"]])
orphaned_images = set()
if args.remove_orphans:
names = (
orphaned_containers = (
(
await compose.podman.output(
[],
@ -2831,13 +3094,15 @@ async def compose_down(compose: PodmanCompose, args):
f"label=io.podman.compose.project={compose.project_name}",
"-a",
"--format",
"{{ .Names }}",
"{{ .Image }} {{ .Names }}",
],
)
)
.decode("utf-8")
.splitlines()
)
orphaned_images = {item.split()[0] for item in orphaned_containers}
names = {item.split()[1] for item in orphaned_containers}
for name in names:
await compose.podman.run([], "stop", [*podman_args, name])
for name in names:
@ -2853,6 +3118,17 @@ async def compose_down(compose: PodmanCompose, args):
if volume_name in vol_names_to_keep:
continue
await compose.podman.run([], "volume", ["rm", volume_name])
if args.rmi:
images_to_remove = set()
for cnt in containers:
if cnt["_service"] in excluded:
continue
if args.rmi == "local" and not is_local(cnt):
continue
images_to_remove.add(cnt["image"])
images_to_remove.update(orphaned_images)
log.debug("images to remove: %s", images_to_remove)
await compose.podman.run([], "rmi", ["--ignore", "--force"] + list(images_to_remove))
if excluded:
return
@ -2915,7 +3191,7 @@ async def compose_run(compose, args):
compose_run_update_container_from_args(compose, cnt, args)
# run podman
podman_args = await container_to_args(compose, cnt, args.detach)
podman_args = await container_to_args(compose, cnt, args.detach, args.no_deps)
if not args.detach:
podman_args.insert(1, "-i")
if args.rm:
@ -3077,37 +3353,22 @@ async def compose_logs(compose, args):
async def compose_config(compose, args):
if args.services:
for service in compose.services:
if not args.quiet:
print(service)
return
if not args.quiet:
print(compose.merged_yaml)
@cmd_run(podman_compose, "port", "Prints the public port for a port binding.")
async def compose_port(compose, args):
# TODO - deal with pod index
compose.assert_services(args.service)
containers = compose.container_names_by_service[args.service]
container_ports = list(
itertools.chain(*(compose.container_by_name[c]["ports"] for c in containers))
)
def _published_target(port_string):
published, target = port_string.split(":")[-2:]
return int(published), int(target)
select_udp = args.protocol == "udp"
published, target = None, None
for p in container_ports:
is_udp = p[-4:] == "/udp"
if select_udp and is_udp:
published, target = _published_target(p[-4:])
if not select_udp and not is_udp:
published, target = _published_target(p)
if target == args.private_port:
print(published)
return
output = await compose.podman.output([], "inspect", [containers[args.index - 1]])
inspect_json = json.loads(output.decode("utf-8"))
private_port = str(args.private_port) + "/" + args.protocol
host_port = inspect_json[0]["NetworkSettings"]["Ports"][private_port][0]["HostPort"]
print(host_port)
@cmd_run(podman_compose, "pause", "Pause all running containers")
@ -3259,7 +3520,7 @@ def compose_up_parse(parser):
"--detach",
action="store_true",
help="Detached mode: Run container in the background, print new container name. \
Incompatible with --abort-on-container-exit.",
Incompatible with --abort-on-container-exit and --abort-on-container-failure.",
)
parser.add_argument("--no-color", action="store_true", help="Produce monochrome output.")
parser.add_argument(
@ -3300,7 +3561,14 @@ def compose_up_parse(parser):
parser.add_argument(
"--abort-on-container-exit",
action="store_true",
help="Stops all containers if any container was stopped. Incompatible with -d.",
help="Stops all containers if any container was stopped. Incompatible with -d and "
"--abort-on-container-failure.",
)
parser.add_argument(
"--abort-on-container-failure",
action="store_true",
help="Stops all containers if any container stops with a non-zero exit code. Incompatible "
"with -d and --abort-on-container-exit.",
)
parser.add_argument(
"-t",
@ -3321,12 +3589,13 @@ def compose_up_parse(parser):
action="store_true",
help="Remove containers for services not defined in the Compose file.",
)
# `--scale` argument needs to store as single value and not append,
# as multiple scale values could be confusing.
parser.add_argument(
"--scale",
metavar="SERVICE=NUM",
action="append",
help="Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if "
"present.",
help="Scale SERVICE to NUM instances. "
"Overrides the `scale` setting in the Compose file if present.",
)
parser.add_argument(
"--exit-code-from",
@ -3353,6 +3622,15 @@ def compose_down_parse(parser):
action="store_true",
help="Remove containers for services not defined in the Compose file.",
)
parser.add_argument(
"--rmi",
type=str,
nargs="?",
const="all",
choices=["local", "all"],
help="Remove images used by services. `local` remove only images that don't have a "
"custom tag. (`local` or `all`)",
)
@cmd_parse(podman_compose, "run")
@ -3613,6 +3891,12 @@ def compose_config_parse(parser):
parser.add_argument(
"--services", help="Print the service names, one per line.", action="store_true"
)
parser.add_argument(
"-q",
"--quiet",
help="Do not print config, only parse.",
action="store_true",
)
@cmd_parse(podman_compose, "port")

View File

@ -7,3 +7,5 @@ version = attr: podman_compose.__version__
[flake8]
# The GitHub editor is 127 chars wide
max-line-length=127
# These are not being followed yet
ignore=E222,E231,E272,E713,W503

View File

@ -31,4 +31,4 @@ python-dotenv==1.0.1
PyYAML==6.0.1
requests
tomlkit==0.12.4
virtualenv==20.25.1
virtualenv==20.26.6

View File

View File

@ -0,0 +1,11 @@
version: "3"
services:
sh1:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 1; exit 1"]
sh2:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 2; exit 0"]
sh3:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 3; exit 0"]

View File

@ -0,0 +1,11 @@
version: "3"
services:
sh1:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 1; exit 0"]
sh2:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 2; exit 0"]
sh3:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 3; exit 0"]

View File

@ -0,0 +1,11 @@
version: "3"
services:
sh1:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 1; exit 0"]
sh2:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 2; exit 1"]
sh3:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 3; exit 0"]

View File

@ -0,0 +1,11 @@
version: "3"
services:
sh1:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 1; exit 1"]
sh2:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 1; exit 0"]
sh3:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 2; exit 0"]

View File

@ -0,0 +1,46 @@
# SPDX-License-Identifier: GPL-2.0
import os
import unittest
from parameterized import parameterized
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path(failure_order):
return os.path.join(test_path(), "abort", f"docker-compose-fail-{failure_order}.yaml")
class TestComposeAbort(unittest.TestCase, RunSubprocessMixin):
@parameterized.expand([
("exit", "first", 0),
("failure", "first", 1),
("exit", "second", 0),
("failure", "second", 1),
("exit", "simultaneous", 0),
("failure", "simultaneous", 1),
("exit", "none", 0),
("failure", "none", 0),
])
def test_abort(self, abort_type, failure_order, expected_exit_code):
try:
self.run_subprocess_assert_returncode(
[
podman_compose_path(),
"-f",
compose_yaml_path(failure_order),
"up",
f"--abort-on-container-{abort_type}",
],
expected_exit_code,
)
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(failure_order),
"down",
])

View File

@ -0,0 +1 @@

View File

@ -7,8 +7,8 @@ import os
import subprocess
import unittest
from tests.integration.test_podman_compose import podman_compose_path
from tests.integration.test_podman_compose import test_path
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,9 @@
version: "3"
services:
test_context:
build:
context: https://github.com/mokibit/test-git-url-as-context.git
image: test-git-url-as-context
test_context_inline:
build: https://github.com/mokibit/test-git-url-as-context.git
image: test-git-url-as-context-inline

View File

@ -0,0 +1,55 @@
# SPDX-License-Identifier: GPL-2.0
import os
from parameterized import parameterized
import unittest
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
from tests.integration.test_utils import RunSubprocessMixin
def compose_yaml_path():
""" "Returns the path to the compose file used for this test module"""
base_path = os.path.join(test_path(), "build/git_url_context")
return os.path.join(base_path, "docker-compose.yml")
class TestComposeBuildGitUrlAsContext(unittest.TestCase, RunSubprocessMixin):
@parameterized.expand([
("git_url_context_test_context_1", "data_1.txt", b'test1\r\n'),
("git_url_context_test_context_1", "data_2.txt", b'test2\r\n'),
("git_url_context_test_context_inline_1", "data_1.txt", b'test1\r\n'),
("git_url_context_test_context_inline_1", "data_2.txt", b'test2\r\n'),
])
def test_build_git_url_as_context(self, container_name, file_name, output):
# test if container can access specific files from git repository when git url is used as
# a build context
try:
out, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"up",
"-d",
])
out, _ = self.run_subprocess_assert_returncode([
"podman",
"exec",
"-ti",
f"{container_name}",
"sh",
"-c",
f"cat {file_name}",
])
self.assertEqual(out, output)
finally:
out, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"down",
"-t",
"0",
])

View File

@ -5,8 +5,8 @@ import unittest
import requests
from tests.integration.test_podman_compose import podman_compose_path
from tests.integration.test_podman_compose import test_path
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
from tests.integration.test_utils import RunSubprocessMixin

View File

@ -0,0 +1 @@

View File

@ -3,9 +3,9 @@
import os
import unittest
from tests.integration.test_podman_compose import podman_compose_path
from tests.integration.test_podman_compose import test_path
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():

View File

@ -0,0 +1,3 @@
FROM busybox
RUN false

View File

@ -0,0 +1,8 @@
version: "3"
services:
bad:
build:
context: bad
good:
build:
context: good

View File

@ -0,0 +1,3 @@
FROM busybox
#ensure that this build finishes second so that it has a chance to overwrite the return code
RUN sleep 0.5

View File

@ -0,0 +1,31 @@
# SPDX-License-Identifier: GPL-2.0
import os
import unittest
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():
""" "Returns the path to the compose file used for this test module"""
base_path = os.path.join(test_path(), "build_fail_multi")
return os.path.join(base_path, "docker-compose.yml")
class TestComposeBuildFailMulti(unittest.TestCase, RunSubprocessMixin):
def test_build_fail_multi(self):
output, error = self.run_subprocess_assert_returncode(
[
podman_compose_path(),
"-f",
compose_yaml_path(),
"build",
# prevent the successful build from being cached to ensure it runs long enough
"--no-cache",
],
expected_returncode=1,
)
self.assertIn("RUN false", str(output))
self.assertIn("while running runtime: exit status 1", str(error))

View File

@ -5,9 +5,9 @@ import json
import os
import unittest
from tests.integration.test_podman_compose import podman_compose_path
from tests.integration.test_podman_compose import test_path
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
class TestBuildLabels(unittest.TestCase, RunSubprocessMixin):

View File

@ -0,0 +1 @@

View File

@ -7,8 +7,8 @@ import os
import subprocess
import unittest
from tests.integration.test_podman_compose import podman_compose_path
from tests.integration.test_podman_compose import test_path
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():

View File

@ -9,9 +9,9 @@ import unittest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from tests.integration.test_podman_compose import podman_compose_path
from tests.integration.test_podman_compose import test_path
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
expected_lines = [
"default: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFYQvN9a+toIB6jSs4zY7FMapZnHt80EKCUr/WhLwUum",

View File

@ -0,0 +1 @@

View File

@ -5,9 +5,9 @@ import unittest
from parameterized import parameterized
from tests.integration.test_podman_compose import podman_compose_path
from tests.integration.test_podman_compose import test_path
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path(scenario):

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,23 @@
version: "3.7"
services:
web:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", "/etc/", "-p", "8000"]
tmpfs:
- /run
- /tmp
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8000/hosts"]
start_period: 10s # initialization time for containers that need time to bootstrap
interval: 10s # Time between health checks
timeout: 5s # Time to wait for a response
retries: 3 # Number of consecutive failures before marking as unhealthy
sleep:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 3600"]
depends_on:
web:
condition: service_healthy
tmpfs:
- /run
- /tmp

View File

@ -0,0 +1,266 @@
# SPDX-License-Identifier: GPL-2.0
import os
import unittest
from tests.integration.test_utils import PodmanAwareRunSubprocessMixin
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import is_systemd_available
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path(suffix=""):
return os.path.join(os.path.join(test_path(), "deps"), f"docker-compose{suffix}.yaml")
class TestComposeBaseDeps(unittest.TestCase, RunSubprocessMixin):
def test_deps(self):
try:
output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"run",
"--rm",
"sleep",
"/bin/sh",
"-c",
"wget -O - http://web:8000/hosts",
])
self.assertIn(b"HTTP request sent, awaiting response... 200 OK", output)
self.assertIn(b"deps_web_1", output)
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"down",
])
def test_run_nodeps(self):
try:
output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"run",
"--rm",
"--no-deps",
"sleep",
"/bin/sh",
"-c",
"wget -O - http://web:8000/hosts || echo Failed to connect",
])
self.assertNotIn(b"HTTP request sent, awaiting response... 200 OK", output)
self.assertNotIn(b"deps_web_1", output)
self.assertIn(b"Failed to connect", output)
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"down",
])
def test_up_nodeps(self):
try:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"up",
"--no-deps",
"--detach",
"sleep",
])
output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"ps",
])
self.assertNotIn(b"deps_web_1", output)
self.assertIn(b"deps_sleep_1", output)
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"down",
])
def test_podman_compose_run(self):
"""
This will test depends_on as well
"""
run_cmd = [
"coverage",
"run",
podman_compose_path(),
"-f",
os.path.join(test_path(), "deps", "docker-compose.yaml"),
"run",
"--rm",
"sleep",
"/bin/sh",
"-c",
"wget -q -O - http://web:8000/hosts",
]
out, _ = self.run_subprocess_assert_returncode(run_cmd)
self.assertIn(b"127.0.0.1\tlocalhost", out)
# Run it again to make sure we can run it twice. I saw an issue where a second run, with
# the container left up, would fail
run_cmd = [
"coverage",
"run",
podman_compose_path(),
"-f",
os.path.join(test_path(), "deps", "docker-compose.yaml"),
"run",
"--rm",
"sleep",
"/bin/sh",
"-c",
"wget -q -O - http://web:8000/hosts",
]
out, _ = self.run_subprocess_assert_returncode(run_cmd)
self.assertIn(b"127.0.0.1\tlocalhost", out)
# This leaves a container running. Not sure it's intended, but it matches docker-compose
down_cmd = [
"coverage",
"run",
podman_compose_path(),
"-f",
os.path.join(test_path(), "deps", "docker-compose.yaml"),
"down",
]
self.run_subprocess_assert_returncode(down_cmd)
class TestComposeConditionalDeps(unittest.TestCase, RunSubprocessMixin):
def test_deps_succeeds(self):
suffix = "-conditional-succeeds"
try:
output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(suffix),
"run",
"--rm",
"sleep",
"/bin/sh",
"-c",
"wget -O - http://web:8000/hosts",
])
self.assertIn(b"HTTP request sent, awaiting response... 200 OK", output)
self.assertIn(b"deps_web_1", output)
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(suffix),
"down",
])
def test_deps_fails(self):
suffix = "-conditional-fails"
try:
output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(suffix),
"ps",
])
self.assertNotIn(b"HTTP request sent, awaiting response... 200 OK", output)
self.assertNotIn(b"deps_web_1", output)
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(suffix),
"down",
])
class TestComposeConditionalDepsHealthy(unittest.TestCase, PodmanAwareRunSubprocessMixin):
def setUp(self):
self.podman_version = self.retrieve_podman_version()
def test_up_deps_healthy(self):
suffix = "-conditional-healthy"
try:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(suffix),
"up",
"sleep",
"--detach",
])
# Since the command `podman wait --condition=healthy` is invalid prior to 4.6.0,
# we only validate healthy status for podman 4.6.0+, which won't be tested in the
# CI pipeline of the podman-compose project where podman 4.3.1 is employed.
podman_ver_major, podman_ver_minor, podman_ver_patch = self.podman_version
if podman_ver_major >= 4 and podman_ver_minor >= 6 and podman_ver_patch >= 0:
self.run_subprocess_assert_returncode([
"podman",
"wait",
"--condition=running",
"deps_web_1",
"deps_sleep_1",
])
# check both web and sleep are running
output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"ps",
"--format",
"{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.StartedAt}}",
])
# extract container id of web
decoded_out = output.decode('utf-8')
lines = decoded_out.split("\n")
web_lines = [line for line in lines if "web" in line]
self.assertTrue(web_lines)
self.assertEqual(1, len(web_lines))
web_cnt_id, web_cnt_name, web_cnt_status, web_cnt_started = web_lines[0].split("\t")
self.assertNotEqual("", web_cnt_id)
self.assertEqual("deps_web_1", web_cnt_name)
sleep_lines = [line for line in lines if "sleep" in line]
self.assertTrue(sleep_lines)
self.assertEqual(1, len(sleep_lines))
sleep_cnt_id, sleep_cnt_name, _, sleep_cnt_started = sleep_lines[0].split("\t")
self.assertNotEqual("", sleep_cnt_id)
self.assertEqual("deps_sleep_1", sleep_cnt_name)
# When test case is executed inside container like github actions, the absence of
# systemd prevents health check from working properly, resulting in failure to
# transit to healthy state. As a result, we only assert the `healthy` state where
# systemd is functioning.
if (
is_systemd_available()
and podman_ver_major >= 4
and podman_ver_minor >= 6
and podman_ver_patch >= 0
):
self.assertIn("healthy", web_cnt_status)
self.assertGreaterEqual(int(sleep_cnt_started), int(web_cnt_started))
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"down",
])

View File

@ -0,0 +1 @@

View File

@ -3,9 +3,9 @@
import os
import unittest
from tests.integration.test_podman_compose import podman_compose_path
from tests.integration.test_podman_compose import test_path
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_base_path():
@ -233,7 +233,7 @@ class TestComposeEnvFile(unittest.TestCase, RunSubprocessMixin):
[
'ZZVAR1=This value is loaded but should be overwritten\r',
'ZZVAR2=This value is loaded from .env in project/ directory\r',
'ZZVAR3=$ZZVAR3\r',
'ZZVAR3=\r',
'',
],
)

View File

@ -0,0 +1 @@

View File

@ -1,5 +1,7 @@
version: "3"
name: my-project-name
services:
env-test:
image: busybox
@ -8,3 +10,9 @@ services:
ZZVAR1: myval1
ZZVAR2: 2-$ZZVAR1
ZZVAR3: 3-$ZZVAR2
project-name-test:
image: busybox
command: sh -c "echo $$PNAME"
environment:
PNAME: ${COMPOSE_PROJECT_NAME}

View File

@ -0,0 +1,89 @@
# SPDX-License-Identifier: GPL-2.0
import os
import unittest
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():
return os.path.join(os.path.join(test_path(), "env-tests"), "container-compose.yml")
class TestComposeEnv(unittest.TestCase, RunSubprocessMixin):
"""Test that inline environment variable overrides environment variable from compose file."""
def test_env(self):
try:
output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"run",
"-l",
"monkey",
"-e",
"ZZVAR1=myval2",
"env-test",
])
self.assertIn("ZZVAR1='myval2'", str(output))
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"down",
])
"""
Tests interpolation of COMPOSE_PROJECT_NAME in the podman-compose config,
which is different from external environment variables because COMPOSE_PROJECT_NAME
is a predefined environment variable generated from the `name` value in the top-level
of the compose.yaml.
See also
- https://docs.docker.com/reference/compose-file/interpolation/
- https://docs.docker.com/reference/compose-file/version-and-name/#name-top-level-element
- https://docs.docker.com/compose/how-tos/environment-variables/envvars/
- https://github.com/compose-spec/compose-spec/blob/main/04-version-and-name.md
"""
def test_project_name(self):
try:
output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"run",
"project-name-test",
])
self.assertIn("my-project-name", str(output))
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"down",
])
def test_project_name_override(self):
try:
output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"run",
"-e",
"COMPOSE_PROJECT_NAME=project-name-override",
"project-name-test",
])
self.assertIn("project-name-override", str(output))
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"down",
])

View File

@ -0,0 +1 @@

View File

@ -3,9 +3,9 @@
import os
import unittest
from tests.integration.test_podman_compose import podman_compose_path
from tests.integration.test_podman_compose import test_path
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():
@ -52,3 +52,16 @@ class TestComposeExitFrom(unittest.TestCase, RunSubprocessMixin):
compose_yaml_path(),
"down",
])
def test_podman_compose_exit_from(self):
up_cmd = [
"coverage",
"run",
podman_compose_path(),
"-f",
os.path.join(test_path(), "exit-from", "docker-compose.yaml"),
"up",
]
self.run_subprocess_assert_returncode(up_cmd + ["--exit-code-from", "sh1"], 1)
self.run_subprocess_assert_returncode(up_cmd + ["--exit-code-from", "sh2"], 2)

View File

@ -0,0 +1 @@

View File

@ -3,9 +3,9 @@
import os
import unittest
from tests.integration.test_podman_compose import podman_compose_path
from tests.integration.test_podman_compose import test_path
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():
@ -80,18 +80,25 @@ class TestComposeExteds(unittest.TestCase, RunSubprocessMixin):
"env1",
])
lines = output.decode('utf-8').split('\n')
# HOSTNAME name is random string so is ignored in asserting
lines = sorted([line for line in lines if not line.startswith("HOSTNAME")])
# Test selected env variables to improve robustness
lines = sorted([
line
for line in lines
if line.startswith("BAR")
or line.startswith("BAZ")
or line.startswith("FOO")
or line.startswith("HOME")
or line.startswith("PATH")
or line.startswith("container")
])
self.assertEqual(
lines,
[
'',
'BAR=local',
'BAZ=local',
'FOO=original',
'HOME=/root',
'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
'TERM=xterm',
'container=podman',
],
)

View File

@ -0,0 +1 @@

View File

@ -2,10 +2,11 @@
import os
import unittest
from pathlib import Path
from tests.integration.test_podman_compose import podman_compose_path
from tests.integration.test_podman_compose import test_path
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():
@ -37,3 +38,26 @@ class TestComposeExtendsWithEmptyService(unittest.TestCase, RunSubprocessMixin):
compose_yaml_path(),
"down",
])
def test_podman_compose_extends_w_empty_service(self):
"""
Test that podman-compose can execute podman-compose -f <file> up with extended File which
includes an empty service. (e.g. if the file is used as placeholder for more complex
configurations.)
"""
main_path = Path(__file__).parent.parent.parent.parent
command_up = [
"python3",
str(main_path.joinpath("podman_compose.py")),
"-f",
str(
main_path.joinpath(
"tests", "integration", "extends_w_empty_service", "docker-compose.yml"
)
),
"up",
"-d",
]
self.run_subprocess_assert_returncode(command_up)

View File

@ -0,0 +1 @@

View File

@ -3,9 +3,9 @@
import os
import unittest
from tests.integration.test_podman_compose import podman_compose_path
from tests.integration.test_podman_compose import test_path
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():

View File

@ -0,0 +1 @@

View File

@ -5,31 +5,47 @@ import unittest
from pathlib import Path
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def base_path():
"""Returns the base path for the project"""
return Path(__file__).parent.parent.parent
def compose_yaml_path():
return os.path.join(os.path.join(test_path(), "extends_w_file_subdir"), "docker-compose.yml")
def test_path():
"""Returns the path to the tests directory"""
return os.path.join(base_path(), "tests/integration")
class TestComposeExtendsWithFileSubdir(unittest.TestCase, RunSubprocessMixin):
def test_extends_w_file_subdir(self): # when file is Dockerfile for building the image
try:
self.run_subprocess_assert_returncode(
[
podman_compose_path(),
"-f",
compose_yaml_path(),
"up",
],
)
output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"ps",
])
self.assertIn("extends_w_file_subdir_web_1", str(output))
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"down",
])
def podman_compose_path():
"""Returns the path to the podman compose script"""
return os.path.join(base_path(), "podman_compose.py")
class TestPodmanCompose(unittest.TestCase, RunSubprocessMixin):
def test_extends_w_file_subdir(self):
def test_podman_compose_extends_w_file_subdir(self):
"""
Test that podman-compose can execute podman-compose -f <file> up with extended File which
includes a build context
:return:
"""
main_path = Path(__file__).parent.parent.parent
main_path = Path(__file__).parent.parent.parent.parent
command_up = [
"coverage",
@ -86,26 +102,3 @@ class TestPodmanCompose(unittest.TestCase, RunSubprocessMixin):
# check container did not exists anymore
out, _ = self.run_subprocess_assert_returncode(command_check_container)
self.assertEqual(out, b'')
def test_extends_w_empty_service(self):
"""
Test that podman-compose can execute podman-compose -f <file> up with extended File which
includes an empty service. (e.g. if the file is used as placeholder for more complex
configurations.)
"""
main_path = Path(__file__).parent.parent.parent
command_up = [
"python3",
str(main_path.joinpath("podman_compose.py")),
"-f",
str(
main_path.joinpath(
"tests", "integration", "extends_w_empty_service", "docker-compose.yml"
)
),
"up",
"-d",
]
self.run_subprocess_assert_returncode(command_up)

View File

@ -0,0 +1 @@

View File

@ -4,9 +4,9 @@
import os
import unittest
from tests.integration.test_podman_compose import podman_compose_path
from tests.integration.test_podman_compose import test_path
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
class TestFilesystem(unittest.TestCase, RunSubprocessMixin):
@ -35,8 +35,7 @@ class TestFilesystem(unittest.TestCase, RunSubprocessMixin):
"container1",
])
# BUG: figure out why cat is called twice
self.assertEqual(out, b'data_compose_symlink\ndata_compose_symlink\n')
self.assertEqual(out, b'data_compose_symlink\n')
finally:
out, _ = self.run_subprocess_assert_returncode([

View File

@ -0,0 +1 @@

View File

@ -8,7 +8,7 @@ from tests.integration.test_utils import RunSubprocessMixin
def base_path():
"""Returns the base path for the project"""
return os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
def test_path():
@ -21,6 +21,16 @@ def podman_compose_path():
return os.path.join(base_path(), "podman_compose.py")
def is_root():
return os.geteuid() == 0
def failure_exitcode_when_rootful():
if is_root():
return 125
return 0
# 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.
@ -64,7 +74,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
]
try:
self.run_subprocess_assert_returncode(command_up)
self.run_subprocess_assert_returncode(command_up, failure_exitcode_when_rootful())
finally:
self.run_subprocess_assert_returncode(down_cmd)
@ -96,7 +106,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
]
try:
out, err = self.run_subprocess_assert_returncode(command_up)
out, err = self.run_subprocess_assert_returncode(command_up, 125)
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
finally:
@ -142,7 +152,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
]
try:
self.run_subprocess_assert_returncode(command_up)
self.run_subprocess_assert_returncode(command_up, failure_exitcode_when_rootful())
finally:
self.run_subprocess_assert_returncode(down_cmd)
@ -188,7 +198,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
]
try:
self.run_subprocess_assert_returncode(command_up)
self.run_subprocess_assert_returncode(command_up, failure_exitcode_when_rootful())
finally:
self.run_subprocess_assert_returncode(down_cmd)
@ -221,7 +231,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
]
try:
out, err = self.run_subprocess_assert_returncode(command_up)
out, err = self.run_subprocess_assert_returncode(command_up, 125)
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
finally:
@ -255,7 +265,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
]
try:
out, err = self.run_subprocess_assert_returncode(command_up)
out, err = self.run_subprocess_assert_returncode(command_up, 125)
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
finally:
@ -301,7 +311,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
]
try:
self.run_subprocess_assert_returncode(command_up)
self.run_subprocess_assert_returncode(command_up, failure_exitcode_when_rootful())
finally:
self.run_subprocess_assert_returncode(down_cmd)
@ -334,7 +344,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
]
try:
out, err = self.run_subprocess_assert_returncode(command_up)
out, err = self.run_subprocess_assert_returncode(command_up, 125)
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
finally:
@ -368,7 +378,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
]
try:
out, err = self.run_subprocess_assert_returncode(command_up)
out, err = self.run_subprocess_assert_returncode(command_up, 125)
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
finally:
@ -402,7 +412,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
]
try:
out, err = self.run_subprocess_assert_returncode(command_up)
out, err = self.run_subprocess_assert_returncode(command_up, 125)
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
finally:
@ -448,7 +458,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
]
try:
self.run_subprocess_assert_returncode(command_up)
self.run_subprocess_assert_returncode(command_up, failure_exitcode_when_rootful())
finally:
self.run_subprocess_assert_returncode(down_cmd)
@ -482,7 +492,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
]
try:
out, err = self.run_subprocess_assert_returncode(command_up)
out, err = self.run_subprocess_assert_returncode(command_up, 125)
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
finally:

View File

@ -0,0 +1 @@

View File

@ -12,7 +12,7 @@ class TestPodmanComposeInclude(unittest.TestCase, RunSubprocessMixin):
Test that podman-compose can execute podman-compose -f <file> up with include
:return:
"""
main_path = Path(__file__).parent.parent.parent
main_path = Path(__file__).parent.parent.parent.parent
command_up = [
"coverage",

View File

@ -0,0 +1 @@

View File

@ -3,9 +3,9 @@
import os
import unittest
from tests.integration.test_podman_compose import podman_compose_path
from tests.integration.test_podman_compose import test_path
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():

View File

@ -0,0 +1 @@

View File

@ -4,9 +4,9 @@ import json
import os
import unittest
from tests.integration.test_podman_compose import podman_compose_path
from tests.integration.test_podman_compose import test_path
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():

View File

@ -6,9 +6,9 @@ import unittest
from parameterized import parameterized
from tests.integration.test_podman_compose import podman_compose_path
from tests.integration.test_podman_compose import test_path
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
class TestLifetime(unittest.TestCase, RunSubprocessMixin):

View File

@ -0,0 +1,7 @@
version: "3"
services:
app:
image: busybox
command: ["/bin/busybox", "echo", "One"]
ports: !override
- "8111:81"

View File

@ -0,0 +1,7 @@
version: "3"
services:
app:
image: busybox
command: ["/bin/busybox", "echo", "Zero"]
ports:
- "8080:80"

View File

@ -0,0 +1,60 @@
# SPDX-License-Identifier: GPL-2.0
import json
import os
import unittest
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():
return os.path.join(os.path.join(test_path(), "override_tag_attribute"), "docker-compose.yaml")
class TestComposeOverrideTagAttribute(unittest.TestCase, RunSubprocessMixin):
# test if a service attribute from docker-compose.yaml file is overridden
def test_override_tag_attribute(self):
override_file = os.path.join(
os.path.join(test_path(), "override_tag_attribute"),
"docker-compose.override_attribute.yaml",
)
try:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"-f",
override_file,
"up",
])
# merge rules are still applied
output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"-f",
override_file,
"logs",
])
self.assertEqual(output, b"One\n")
# only app service attribute "ports" was overridden
output, _ = self.run_subprocess_assert_returncode([
"podman",
"inspect",
"override_tag_attribute_app_1",
])
container_info = json.loads(output.decode('utf-8'))[0]
self.assertEqual(
container_info['NetworkSettings']["Ports"],
{"81/tcp": [{"HostIp": "", "HostPort": "8111"}]},
)
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"down",
])

View File

@ -0,0 +1,7 @@
version: "3"
services:
app: !override
image: busybox
command: ["/bin/busybox", "echo", "One"]
ports:
- "8111:81"

View File

@ -0,0 +1,7 @@
version: "3"
services:
app:
image: busybox
command: ["/bin/busybox", "echo", "Zero"]
ports:
- "8080:80"

View File

@ -0,0 +1,61 @@
# SPDX-License-Identifier: GPL-2.0
import json
import os
import unittest
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():
return os.path.join(os.path.join(test_path(), "override_tag_service"), "docker-compose.yaml")
class TestComposeOverrideTagService(unittest.TestCase, RunSubprocessMixin):
# test if whole service from docker-compose.yaml file is overridden in another file
def test_override_tag_service(self):
override_file = os.path.join(
os.path.join(test_path(), "override_tag_service"),
"docker-compose.override_service.yaml",
)
try:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"-f",
override_file,
"up",
])
# Whole app service was overridden in the docker-compose.override_tag_service.yaml file.
# Command and port is overridden accordingly.
output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"-f",
override_file,
"logs",
])
self.assertEqual(output, b"One\n")
output, _ = self.run_subprocess_assert_returncode([
"podman",
"inspect",
"override_tag_service_app_1",
])
container_info = json.loads(output.decode('utf-8'))[0]
self.assertEqual(
container_info['NetworkSettings']["Ports"],
{"81/tcp": [{"HostIp": "", "HostPort": "8111"}]},
)
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"down",
])

View File

@ -0,0 +1,5 @@
version: "3"
services:
app:
image: busybox
command: !reset {}

View File

@ -0,0 +1,5 @@
version: "3"
services:
app:
image: busybox
command: ["/bin/busybox", "echo", "Zero"]

View File

@ -0,0 +1,58 @@
# SPDX-License-Identifier: GPL-2.0
import os
import unittest
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():
return os.path.join(os.path.join(test_path(), "reset_tag_attribute"), "docker-compose.yaml")
class TestComposeResetTagAttribute(unittest.TestCase, RunSubprocessMixin):
# test if the attribute of the service is correctly reset
def test_reset_tag_attribute(self):
reset_file = os.path.join(
os.path.join(test_path(), "reset_tag_attribute"), "docker-compose.reset_attribute.yaml"
)
try:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"-f",
reset_file,
"up",
])
# the service still exists, but its command attribute was reset in
# docker-compose.reset_tag_attribute.yaml file and is now empty
output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"-f",
reset_file,
"ps",
])
self.assertIn(b"reset_tag_attribute_app_1", output)
output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"-f",
reset_file,
"logs",
])
self.assertEqual(output, b"")
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"down",
])

View File

@ -0,0 +1,6 @@
version: "3"
services:
app: !reset
app2:
image: busybox
command: ["/bin/busybox", "echo", "One"]

View File

@ -0,0 +1,5 @@
version: "3"
services:
app:
image: busybox
command: ["/bin/busybox", "echo", "Zero"]

View File

@ -0,0 +1,59 @@
# SPDX-License-Identifier: GPL-2.0
import os
import unittest
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():
return os.path.join(os.path.join(test_path(), "reset_tag_service"), "docker-compose.yaml")
class TestComposeResetTagService(unittest.TestCase, RunSubprocessMixin):
# test if whole service from docker-compose.yaml file is reset
def test_reset_tag_service(self):
reset_file = os.path.join(
os.path.join(test_path(), "reset_tag_service"), "docker-compose.reset_service.yaml"
)
try:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"-f",
reset_file,
"up",
])
# app service was fully reset in docker-compose.reset_tag_service.yaml file, therefore
# does not exist. A new service was created instead.
output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"-f",
reset_file,
"ps",
])
self.assertNotIn(b"reset_tag_service_app_1", output)
self.assertIn(b"reset_tag_service_app2_1", output)
output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"-f",
reset_file,
"logs",
])
self.assertEqual(output, b"One\n")
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"down",
])

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,75 @@
# SPDX-License-Identifier: GPL-2.0
import json
import os
import unittest
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path(compose_name):
""" "Returns the path to the compose file used for this test module"""
base_path = os.path.join(test_path(), "volumes_merge/")
return os.path.join(base_path, compose_name)
class TestComposeVolumesMerge(unittest.TestCase, RunSubprocessMixin):
def test_volumes_merge(self):
# test if additional compose file overrides host path and access mode of a volume
try:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path("docker-compose.yaml"),
"-f",
compose_yaml_path("docker-compose.override.yaml"),
"up",
"-d",
])
out, _ = self.run_subprocess_assert_returncode([
"podman",
"exec",
"-ti",
"volumes_merge_web_1",
"cat",
"/var/www/html/index.html",
"/var/www/html/index2.html",
"/var/www/html/index3.html",
])
self.assertEqual(
out,
b"The file from docker-compose.override.yaml\r\n"
b"The file from docker-compose.override.yaml\r\n"
b"The file from docker-compose.override.yaml\r\n",
)
out, _ = self.run_subprocess_assert_returncode([
"podman",
"inspect",
"volumes_merge_web_1",
])
volumes_info = json.loads(out.decode('utf-8'))[0]
binds_info = volumes_info["HostConfig"]["Binds"]
binds_info.sort()
file_path = os.path.join(test_path(), "volumes_merge/override.txt")
expected = [
f'{file_path}:/var/www/html/index.html:ro,rprivate,rbind',
f'{file_path}:/var/www/html/index2.html:rw,rprivate,rbind',
f'{file_path}:/var/www/html/index3.html:rw,rprivate,rbind',
]
self.assertEqual(binds_info, expected)
finally:
out, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path("docker-compose.yaml"),
"-f",
compose_yaml_path("docker-compose.override.yaml"),
"down",
"-t",
"0",
])

View File

@ -0,0 +1 @@

View File

@ -3,9 +3,9 @@
import os
import unittest
from tests.integration.test_podman_compose import podman_compose_path
from tests.integration.test_podman_compose import test_path
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():

View File

@ -0,0 +1 @@

View File

@ -5,9 +5,9 @@ import unittest
import requests
from tests.integration.test_podman_compose import podman_compose_path
from tests.integration.test_podman_compose import test_path
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():

View File

@ -0,0 +1 @@

View File

@ -6,9 +6,9 @@ import unittest
import requests
from tests.integration.test_podman_compose import podman_compose_path
from tests.integration.test_podman_compose import test_path
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():
@ -59,9 +59,13 @@ class TestComposeNetsTest1(unittest.TestCase, RunSubprocessMixin):
)
# check if Host port is the same as provided by the service port
self.assertIsNotNone(container_info['NetworkSettings']["Ports"].get("8001/tcp", None))
self.assertGreater(len(container_info['NetworkSettings']["Ports"]["8001/tcp"]), 0)
self.assertIsNotNone(
container_info['NetworkSettings']["Ports"]["8001/tcp"][0].get("HostPort", None)
)
self.assertEqual(
container_info['NetworkSettings']["Ports"],
{"8001/tcp": [{"HostIp": "", "HostPort": "8001"}]},
container_info['NetworkSettings']["Ports"]["8001/tcp"][0]["HostPort"], "8001"
)
self.assertEqual(container_info["Config"]["Hostname"], "web1")
@ -77,9 +81,13 @@ class TestComposeNetsTest1(unittest.TestCase, RunSubprocessMixin):
list(container_info["NetworkSettings"]["Networks"].keys())[0], "nets_test1_default"
)
self.assertIsNotNone(container_info['NetworkSettings']["Ports"].get("8001/tcp", None))
self.assertGreater(len(container_info['NetworkSettings']["Ports"]["8001/tcp"]), 0)
self.assertIsNotNone(
container_info['NetworkSettings']["Ports"]["8001/tcp"][0].get("HostPort", None)
)
self.assertEqual(
container_info['NetworkSettings']["Ports"],
{"8001/tcp": [{"HostIp": "", "HostPort": "8002"}]},
container_info['NetworkSettings']["Ports"]["8001/tcp"][0]["HostPort"], "8002"
)
self.assertEqual(container_info["Config"]["Hostname"], "web2")

View File

@ -0,0 +1 @@

View File

@ -6,9 +6,9 @@ import unittest
import requests
from tests.integration.test_podman_compose import podman_compose_path
from tests.integration.test_podman_compose import test_path
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():
@ -59,9 +59,13 @@ class TestComposeNetsTest2(unittest.TestCase, RunSubprocessMixin):
)
# check if Host port is the same as prodvided by the service port
self.assertIsNotNone(container_info['NetworkSettings']["Ports"].get("8001/tcp", None))
self.assertGreater(len(container_info['NetworkSettings']["Ports"]["8001/tcp"]), 0)
self.assertIsNotNone(
container_info['NetworkSettings']["Ports"]["8001/tcp"][0].get("HostPort", None)
)
self.assertEqual(
container_info['NetworkSettings']["Ports"],
{"8001/tcp": [{"HostIp": "", "HostPort": "8001"}]},
container_info['NetworkSettings']["Ports"]["8001/tcp"][0]["HostPort"], "8001"
)
self.assertEqual(container_info["Config"]["Hostname"], "web1")
@ -78,9 +82,13 @@ class TestComposeNetsTest2(unittest.TestCase, RunSubprocessMixin):
list(container_info["NetworkSettings"]["Networks"].keys())[0], "nets_test2_mystack"
)
self.assertIsNotNone(container_info['NetworkSettings']["Ports"].get("8001/tcp", None))
self.assertGreater(len(container_info['NetworkSettings']["Ports"]["8001/tcp"]), 0)
self.assertIsNotNone(
container_info['NetworkSettings']["Ports"]["8001/tcp"][0].get("HostPort", None)
)
self.assertEqual(
container_info['NetworkSettings']["Ports"],
{"8001/tcp": [{"HostIp": "", "HostPort": "8002"}]},
container_info['NetworkSettings']["Ports"]["8001/tcp"][0]["HostPort"], "8002"
)
self.assertEqual(container_info["Config"]["Hostname"], "web2")

View File

@ -41,5 +41,5 @@ services:
aliases:
- alias21
volumes:
- ./test2.txt:/var/www/html/index.txt:ro,z
- ./test3.txt:/var/www/html/index.txt:ro,z

View File

@ -0,0 +1 @@
test3

View File

@ -0,0 +1,68 @@
# SPDX-License-Identifier: GPL-2.0
import os
import unittest
from parameterized import parameterized
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():
return os.path.join(os.path.join(test_path(), "nets_test3"), "docker-compose.yml")
class TestComposeNetsTest3(unittest.TestCase, RunSubprocessMixin):
# test if services can access the networks of other services using their respective aliases
@parameterized.expand([
("nets_test3_web2_1", "web3", b"test3", 0),
("nets_test3_web2_1", "alias11", b"test3", 0),
("nets_test3_web2_1", "alias12", b"test3", 0),
("nets_test3_web2_1", "alias21", b"test3", 0),
("nets_test3_web1_1", "web3", b"test3", 0),
("nets_test3_web1_1", "alias11", b"test3", 0),
("nets_test3_web1_1", "alias12", b"test3", 0),
# connection fails as web1 service does not know net2 and its aliases
("nets_test3_web1_1", "alias21", b"", 1),
])
def test_nets_test3(
self, container_name, nework_alias_name, expected_text, expected_returncode
):
try:
self.run_subprocess_assert_returncode(
[
podman_compose_path(),
"-f",
compose_yaml_path(),
"up",
"-d",
],
)
# check connection from different services to network aliases of web3 service
cmd = [
"podman",
"exec",
"-it",
f"{container_name}",
"/bin/busybox",
"wget",
"-O",
"-",
"-o",
"/dev/null",
f"http://{nework_alias_name}:8001/index.txt",
]
out, _, returncode = self.run_subprocess(cmd)
self.assertEqual(expected_returncode, returncode)
self.assertEqual(expected_text, out.strip())
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"down",
"-t",
"0",
])

View File

@ -0,0 +1,74 @@
# SPDX-License-Identifier: GPL-2.0
import os
import unittest
from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path
def compose_yaml_path():
return os.path.join(os.path.join(test_path(), "nets_test_ip"), "docker-compose.yml")
class TestComposeNetsTestIp(unittest.TestCase, RunSubprocessMixin):
# test if services retain custom ipv4_address and mac_address matching the subnet provided
# in networks top-level element
def test_nets_test_ip(self):
try:
self.run_subprocess_assert_returncode(
[
podman_compose_path(),
"-f",
compose_yaml_path(),
"up",
"-d",
],
)
expected_results = [
(
"web1",
b"inet 172.19.1.10/24 ",
b"link/ether 02:01:01:00:01:01 ",
b"inet 172.19.2.10/24 ",
b"link/ether 02:01:01:00:02:01 ",
b"",
),
("web2", b"", b"", b"inet 172.19.2.11/24 ", b"", b"link/ether 02:01:01:00:02:02 "),
("web3", b"", b"", b"inet 172.19.2.", b"", b""),
("web4", b"inet 172.19.1.13/24 ", b"", b"inet 172.19.2.", b"", b""),
]
for (
service_name,
shared_network_ip,
shared_network_mac_address,
internal_network_ip,
internal_network_mac_address,
mac_address,
) in expected_results:
output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"exec",
service_name,
"ip",
"a",
])
self.assertIn(shared_network_ip, output)
self.assertIn(shared_network_mac_address, output)
self.assertIn(internal_network_ip, output)
self.assertIn(internal_network_mac_address, output)
self.assertIn(mac_address, output)
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"down",
"-t",
"0",
])

View File

@ -0,0 +1 @@

Some files were not shown because too many files have changed in this diff Show More