65 Commits

Author SHA1 Message Date
1b85ebe506 version 2024-02-06 00:26:51 +03:00
9ed05a23ef add TQDM and implement pool.join 2024-02-06 00:26:51 +03:00
d475260951 add TQDM and implement pool.join 2024-02-06 00:26:51 +03:00
831caa6276 remove recursion 2024-02-06 00:23:14 +03:00
9ac33392a0 Fix issue #831
Signed-off-by: Ben Plessinger <Benjamin.Plessinger@roswellpark.org>
2024-02-06 00:20:11 +03:00
c5be5bae90 Fixup tests
Signed-off-by: Falmarri <463948+Falmarri@users.noreply.github.com>
2024-02-04 10:11:57 +03:00
c6a1c4c432 Add tests to make sure all async paths are covered
Not at 100% yet. But upped code coverage significantly and covered major
async calls.

Signed-off-by: Falmarri <463948+Falmarri@users.noreply.github.com>
2024-02-04 10:11:57 +03:00
3c9628b462 Fix a couple issues and update docs
Signed-off-by: Falmarri <463948+Falmarri@users.noreply.github.com>
2024-02-04 10:11:57 +03:00
38b13a34ea Use asyncio for subprocess calls
Removes the threads from compose_up and manages it using async. Also
uses async processing to format the log messages instead of piping
through sed. This should work on windows without having sed installed

Adds --parallel to support pull and build in parallel, same as docker
compose

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

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

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

Signed-off-by: Cleber Rosa <crosa@redhat.com>
2023-05-06 00:28:13 +03:00
8c66b1cda7 add test case for when build is a complex dictionary
Signed-off-by: Sergei Biriukov <svbiriukov@gmail.com>
2023-04-30 15:37:52 +10:00
a0005db474 add code implementing build value merge
Signed-off-by: Sergei Biriukov <svbiriukov@gmail.com>
2023-04-29 13:52:19 +10:00
221cf14501 add tests for build value merge
Signed-off-by: Sergei Biriukov <svbiriukov@gmail.com>
2023-04-29 13:17:43 +10:00
a61945b516 fix format
Signed-off-by: Sergei Biriukov <svbiriukov@gmail.com>
2023-04-21 20:47:53 +03:00
6b6330c587 add build subcommand and --build arg to compose_run
Signed-off-by: Sergei Biriukov <svbiriukov@gmail.com>
2023-04-21 20:47:53 +03:00
5d279c4948 Build-fail test example
Signed-off-by: BugFest <bugfest.dev@pm.me>
2023-04-12 22:25:33 +03:00
5a3bdbf89b Exit code managed at PodmanCompose.run()
Signed-off-by: BugFest <bugfest.dev@pm.me>
2023-04-12 22:25:33 +03:00
1eb166445b Linting fixes
Signed-off-by: BugFest <bugfest.dev@pm.me>
2023-04-12 22:25:33 +03:00
82182b7bc6 Finish execution in compose_build only on command=build calls
Signed-off-by: BugFest <bugfest.dev@pm.me>
2023-04-12 22:25:33 +03:00
3f4618866b Update project-1.env 2023-04-10 14:14:14 +03:00
91bc6ebdb4 Keep chdir after loading env file
Signed-off-by: BugFest <bugfest.dev@pm.me>
2023-04-10 14:14:14 +03:00
59a59c1a3a Fixes #636: env-file shall be resolved relative to the CWD
Signed-off-by: BugFest <bugfest.dev@pm.me>
2023-04-10 14:14:14 +03:00
620f5d7473 pre-commit black config: run in check only mode
Signed-off-by: BugFest <bugfest.dev@pm.me>
2023-04-10 14:13:00 +03:00
6f902faed0 Fix linting issues
Signed-off-by: BugFest <bugfest.dev@pm.me>
2023-04-10 14:12:32 +03:00
ccdf01e9b0 Revert "Use SELinux mount flag for secrets"
This reverts commit 874192568f.
2023-04-10 12:26:53 +03:00
e6b1eabe4c Revert "Use more lenient SELinux mount flag for secrets"
This reverts commit 75de39c239.
2023-04-10 12:26:53 +03:00
75de39c239 Use more lenient SELinux mount flag for secrets
Signed-off-by: Henry Reed <60915078+henryreed@users.noreply.github.com>
2023-04-10 12:25:53 +03:00
874192568f Use SELinux mount flag for secrets
Signed-off-by: Henry Reed <github.69ofd@simplelogin.com>
2023-04-10 12:25:53 +03:00
0b853f29f4 Ignore access mode when merging volumes short syntax
The target path inside the container is treated as a key. Ref:
https://github.com/compose-spec/compose-spec/blob/master/spec.md#merging-service-definitions

Signed-off-by: Bhavin Gandhi <bhavin7392@gmail.com>
2023-04-10 12:25:05 +03:00
847f01a6c6 Add a docker-compose test file for uidmaps/gidmaps
Add a simple docker-compose.yml test to use the x-podman extension with
uidmaps and gitmaps
2023-04-10 12:22:25 +03:00
e511e6420f FIXES #228: Add support for uidmap and gidmap
Implement an x-podman extension on the level of the individual services
to handle `--uidmap` and `--gidmap`
2023-04-10 12:22:25 +03:00
a9723ec1cf Added a way to start containers with multiple ips and nets
Signed-off-by: KuhnChris <kuhnchris@kuhnchris.eu>
2023-04-10 12:16:54 +03:00
1cb608d8a7 allow project name to be fetched from dotenv
Look for project name in `self.environ` which includes both `os.environ`
and dotenv variables so that the project name can also be defined in an
environment file.

Signed-off-by: Kuan-Yi Li <kyli@abysm.org>
2023-04-10 12:13:23 +03:00
252f1d57a5 updating black formatting for podman-compose.py
Signed-off-by: Dixon Whitmire <dixonwh@gmail.com>
2023-04-10 12:12:18 +03:00
13856d2e9c updating black formatting
Signed-off-by: Dixon Whitmire <dixonwh@gmail.com>
2023-04-10 12:12:18 +03:00
8d8df0bc28 Adding basic support for --profile argument
Signed-off-by: Dixon Whitmire <dixonwh@gmail.com>
2023-04-10 12:12:18 +03:00
bc5f0123d9 add option to start podman in existing network namespace 2023-04-10 12:11:02 +03:00
9a08f85ffd FIXES #586: preserve exit code for podman-compose build
Signed-off-by: Roman Blanco <rblanco@redhat.com>
2023-04-10 12:10:16 +03:00
8625d7a4e8 add ipam-driver support
Signed-off-by: Benedikt Braunger <bb@emlix.com>
2023-04-10 12:02:47 +03:00
016c97fd1e Fixes #663 - Fixes linting/pylint errors
Signed-off-by: BugFest <bugfest.dev@pm.me>
2023-04-10 11:53:47 +03:00
2df11674c4 Fixes #661 - Fixes linting/flake8 errors
Signed-off-by: BugFest <bugfest.dev@pm.me>
2023-04-10 11:53:47 +03:00
5eff38e743 Fixes #659: fix permissions when installing OS packages for linting/black
Signed-off-by: BugFest <bugfest.dev@pm.me>
2023-04-10 11:28:07 +03:00
7f5ce26b1b start version 1.0.7 and default with pod enabled by default 2023-04-09 14:08:54 +03:00
42 changed files with 1832 additions and 281 deletions

2
.coveragerc Normal file
View File

@ -0,0 +1,2 @@
[run]
parallel=True

View File

@ -11,8 +11,8 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Install psf/black requirements - name: Install psf/black requirements
run: | run: |
apt-get update sudo apt-get update
apt-get install -y python3 python3-venv sudo apt-get install -y python3 python3-venv
- uses: psf/black@stable - uses: psf/black@stable
with: with:
options: "--check --verbose" options: "--check --verbose"
@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: ["3.8", "3.9", "3.10"] python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}

View File

@ -21,9 +21,10 @@ jobs:
python-version: "3.10" python-version: "3.10"
- name: Install dependencies - name: Install dependencies
run: | run: |
sudo apt update && apt install podman
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi
- name: Lint with flake8 - name: Lint with flake8
run: | run: |
# stop the build if there are Python syntax errors or undefined names # stop the build if there are Python syntax errors or undefined names
@ -32,5 +33,7 @@ jobs:
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest - name: Test with pytest
run: | run: |
python -m pytest ./pytests coverage run --source podman_compose -m pytest ./pytests
python -m pytest ./tests
coverage combine
coverage report

View File

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

View File

@ -39,7 +39,15 @@ $ pre-commit install
```shell ```shell
$ pre-commit run --all-files $ pre-commit run --all-files
``` ```
6. Commit your code to your fork's branch. 6. Run code coverage
```shell
coverage run --source podman_compose -m pytest ./pytests
python -m pytest ./tests
coverage combine
coverage report
coverage html
```
7. Commit your code to your fork's branch.
- Make sure you include a `Signed-off-by` message in your commits. Read [this guide](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) to learn how to sign your commits - Make sure you include a `Signed-off-by` message in your commits. Read [this guide](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) to learn how to sign your commits
- In the commit message reference the Issue ID that your code fixes and a brief description of the changes. Example: `Fixes #516: allow empty network` - In the commit message reference the Issue ID that your code fixes and a brief description of the changes. Example: `Fixes #516: allow empty network`
7. Open a PR to `containers/podman-compose:devel` and wait for a maintainer to review your work. 7. Open a PR to `containers/podman-compose:devel` and wait for a maintainer to review your work.
@ -48,18 +56,18 @@ $ pre-commit run --all-files
To add a command you need to add a function that is decorated To add a command you need to add a function that is decorated
with `@cmd_run` passing the compose instance, command name and with `@cmd_run` passing the compose instance, command name and
description. the wrapped function should accept two arguments description. This function must be declared `async` the wrapped
the compose instance and the command-specific arguments (resulted function should accept two arguments the compose instance and
from python's `argparse` package) inside that command you can the command-specific arguments (resulted from python's `argparse`
run PodMan like this `compose.podman.run(['inspect', 'something'])` package) inside that command you can run PodMan like this
and inside that function you can access `compose.pods` `await compose.podman.run(['inspect', 'something'])`and inside
and `compose.containers` ...etc. that function you can access `compose.pods` and `compose.containers`
Here is an example ...etc. Here is an example
``` ```
@cmd_run(podman_compose, 'build', 'build images defined in the stack') @cmd_run(podman_compose, 'build', 'build images defined in the stack')
def compose_build(compose, args): async def compose_build(compose, args):
compose.podman.run(['build', 'something']) await compose.podman.run(['build', 'something'])
``` ```
## Command arguments parsing ## Command arguments parsing
@ -90,10 +98,10 @@ do something like:
``` ```
@cmd_run(podman_compose, 'up', 'up desc') @cmd_run(podman_compose, 'up', 'up desc')
def compose_up(compose, args): async def compose_up(compose, args):
compose.commands['down'](compose, args) await compose.commands['down'](compose, args)
# or # or
compose.commands['down'](argparse.Namespace(foo=123)) await compose.commands['down'](argparse.Namespace(foo=123))
``` ```

View File

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

View File

@ -7,6 +7,6 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY . . COPY . .
CMD [ "python", "-m", "App.web" ] CMD [ "python", "-m", "app.web" ]
EXPOSE 8080 EXPOSE 8080

View File

@ -1,5 +1,7 @@
# pylint: disable=import-error
# pylint: disable=unused-import
import os import os
import asyncio import asyncio # noqa: F401
import aioredis import aioredis
from aiohttp import web from aiohttp import web
@ -14,13 +16,13 @@ routes = web.RouteTableDef()
@routes.get("/") @routes.get("/")
async def hello(request): async def hello(request): # pylint: disable=unused-argument
counter = await redis.incr("mycounter") counter = await redis.incr("mycounter")
return web.Response(text=f"counter={counter}") return web.Response(text=f"counter={counter}")
@routes.get("/hello.json") @routes.get("/hello.json")
async def hello_json(request): async def hello_json(request): # pylint: disable=unused-argument
counter = await redis.incr("mycounter") counter = await redis.incr("mycounter")
data = {"counter": counter} data = {"counter": counter}
return web.json_response(data) return web.json_response(data)

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
# pylint: disable=redefined-outer-name
import pytest import pytest
from podman_compose import parse_short_mount from podman_compose import parse_short_mount

View File

@ -3,3 +3,7 @@ universal = 1
[metadata] [metadata]
version = attr: podman_compose.__version__ version = attr: podman_compose.__version__
[flake8]
# The GitHub editor is 127 chars wide
max-line-length=127

View File

@ -2,24 +2,25 @@ import os
from setuptools import setup from setuptools import setup
try: try:
readme = open(os.path.join(os.path.dirname(__file__), "README.md")).read() README = open(
except: os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf-8"
readme = "" ).read()
except: # noqa: E722 # pylint: disable=bare-except
README = ""
setup( setup(
name="podman-compose", name="podman-compose",
description="A script to run docker-compose.yml using podman", description="A script to run docker-compose.yml using podman",
long_description=readme, long_description=README,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
classifiers=[ classifiers=[
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Development Status :: 3 - Alpha", "Development Status :: 3 - Alpha",
@ -44,6 +45,7 @@ setup(
"black", "black",
"pylint", "pylint",
"pre-commit", "pre-commit",
"coverage"
] ]
} }
# test_suite='tests', # test_suite='tests',

View File

@ -3,7 +3,7 @@
# process, which may cause wedges in the gate later. # process, which may cause wedges in the gate later.
coverage coverage
pytest-cov
pytest pytest
tox tox
black black
flake8

View File

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

View File

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

View File

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

26
tests/conftest.py Normal file
View File

@ -0,0 +1,26 @@
"""conftest.py
Defines global pytest fixtures available to all tests.
"""
# pylint: disable=redefined-outer-name
from pathlib import Path
import os
import pytest
@pytest.fixture
def base_path():
"""Returns the base path for the project"""
return Path(__file__).parent.parent
@pytest.fixture
def test_path(base_path):
"""Returns the path to the tests directory"""
return os.path.join(base_path, "tests")
@pytest.fixture
def podman_compose_path(base_path):
"""Returns the path to the podman compose script"""
return os.path.join(base_path, "podman_compose.py")

View File

@ -1,4 +1,4 @@
``` ```
podman-compose run --rm sleep /bin/sh -c 'wget -O - http://localhost:8000/hosts' podman-compose run --rm sleep /bin/sh -c 'wget -O - http://web:8000/hosts'
``` ```

View File

@ -9,7 +9,8 @@ services:
sleep: sleep:
image: busybox image: busybox
command: ["/bin/busybox", "sh", "-c", "sleep 3600"] command: ["/bin/busybox", "sh", "-c", "sleep 3600"]
depends_on: "web" depends_on:
- "web"
tmpfs: tmpfs:
- /run - /run
- /tmp - /tmp

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,24 @@
version: "3"
services:
default-service:
image: busybox
command: ["/bin/busybox", "httpd", "-f", "-h", "/etc/", "-p", "8000"]
tmpfs:
- /run
- /tmp
service-1:
image: busybox
command: ["/bin/busybox", "httpd", "-f", "-h", "/etc/", "-p", "8000"]
tmpfs:
- /run
- /tmp
profiles:
- profile-1
service-2:
image: busybox
command: ["/bin/busybox", "httpd", "-f", "-h", "/etc/", "-p", "8000"]
tmpfs:
- /run
- /tmp
profiles:
- profile-2

View File

@ -1,7 +1,7 @@
version: "3" version: "3"
services: services:
redis: redis:
image: redis:alpine image: docker.io/library/redis:alpine
command: ["redis-server", "--appendonly yes", "--notify-keyspace-events", "Ex"] command: ["redis-server", "--appendonly yes", "--notify-keyspace-events", "Ex"]
volumes: volumes:
- ./data/redis:/data:z - ./data/redis:/data:z
@ -12,7 +12,7 @@ services:
- SECRET_KEY=aabbcc - SECRET_KEY=aabbcc
- ENV_IS_SET - ENV_IS_SET
web: web:
image: busybox image: docker.io/library/busybox
command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8000"] command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8000"]
working_dir: /var/www/html working_dir: /var/www/html
volumes: volumes:
@ -21,19 +21,19 @@ services:
- /run - /run
- /tmp - /tmp
web1: web1:
image: busybox image: docker.io/library/busybox
command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"] command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"]
working_dir: /var/www/html working_dir: /var/www/html
volumes: volumes:
- ./data/web:/var/www/html:ro,z - ./data/web:/var/www/html:ro,z
web2: web2:
image: busybox image: docker.io/library/busybox
command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8002"] command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8002"]
working_dir: /var/www/html working_dir: /var/www/html
volumes: volumes:
- ~/Downloads/www:/var/www/html:ro,z - ~/Downloads/www:/var/www/html:ro,z
web3: web3:
image: busybox image: docker.io/library/busybox
command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8003"] command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8003"]
working_dir: /var/www/html working_dir: /var/www/html
volumes: volumes:

View File

@ -21,7 +21,8 @@ def test_podman_compose_extends_w_file_subdir():
main_path = Path(__file__).parent.parent main_path = Path(__file__).parent.parent
command_up = [ command_up = [
"python3", "coverage",
"run",
str(main_path.joinpath("podman_compose.py")), str(main_path.joinpath("podman_compose.py")),
"-f", "-f",
str(main_path.joinpath("tests", "extends_w_file_subdir", "docker-compose.yml")), str(main_path.joinpath("tests", "extends_w_file_subdir", "docker-compose.yml")),
@ -30,12 +31,14 @@ def test_podman_compose_extends_w_file_subdir():
] ]
command_check_container = [ command_check_container = [
"podman", "coverage",
"container", "run",
str(main_path.joinpath("podman_compose.py")),
"-f",
str(main_path.joinpath("tests", "extends_w_file_subdir", "docker-compose.yml")),
"ps", "ps",
"--all",
"--format", "--format",
'"{{.Image}}"', '{{.Image}}',
] ]
command_down = [ command_down = [
@ -46,16 +49,40 @@ def test_podman_compose_extends_w_file_subdir():
"docker.io/library/busybox", "docker.io/library/busybox",
] ]
out, err, returncode = capture(command_up) out, _, returncode = capture(command_up)
assert 0 == returncode assert 0 == returncode
# check container was created and exists # check container was created and exists
out, err, returncode = capture(command_check_container) out, err, returncode = capture(command_check_container)
assert 0 == returncode assert 0 == returncode
assert out == b'"localhost/subdir_test:me"\n' assert b'localhost/subdir_test:me\n' == out
out, err, returncode = capture(command_down) out, _, returncode = capture(command_down)
# cleanup test image(tags) # cleanup test image(tags)
assert 0 == returncode assert 0 == returncode
print('ok')
# check container did not exists anymore # check container did not exists anymore
out, err, returncode = capture(command_check_container) out, _, returncode = capture(command_check_container)
assert 0 == returncode
assert b'' == out
def test_podman_compose_extends_w_empty_service():
"""
Test that podman-compose can execute podman-compose -f <file> up with extended File which
includes an empty service. (e.g. if the file is used as placeholder for more complex configurations.)
:return:
"""
main_path = Path(__file__).parent.parent
command_up = [
"python3",
str(main_path.joinpath("podman_compose.py")),
"-f",
str(
main_path.joinpath("tests", "extends_w_empty_service", "docker-compose.yml")
),
"up",
"-d",
]
_, _, returncode = capture(command_up)
assert 0 == returncode assert 0 == returncode
assert out == b""

View File

@ -0,0 +1,78 @@
"""
test_podman_compose_config.py
Tests the podman-compose config command which is used to return defined compose services.
"""
# pylint: disable=redefined-outer-name
import os
from test_podman_compose import capture
import pytest
@pytest.fixture
def profile_compose_file(test_path):
""" "Returns the path to the `profile` compose file used for this test module"""
return os.path.join(test_path, "profile", "docker-compose.yml")
def test_config_no_profiles(podman_compose_path, profile_compose_file):
"""
Tests podman-compose config command without profile enablement.
:param podman_compose_path: The fixture used to specify the path to the podman compose file.
:param profile_compose_file: The fixtued used to specify the path to the "profile" compose used in the test.
"""
config_cmd = ["coverage", "run", podman_compose_path, "-f", profile_compose_file, "config"]
out, _, return_code = capture(config_cmd)
assert return_code == 0
string_output = out.decode("utf-8")
assert "default-service" in string_output
assert "service-1" not in string_output
assert "service-2" not in string_output
@pytest.mark.parametrize(
"profiles, expected_services",
[
(
["--profile", "profile-1", "config"],
{"default-service": True, "service-1": True, "service-2": False},
),
(
["--profile", "profile-2", "config"],
{"default-service": True, "service-1": False, "service-2": True},
),
(
["--profile", "profile-1", "--profile", "profile-2", "config"],
{"default-service": True, "service-1": True, "service-2": True},
),
],
)
def test_config_profiles(
podman_compose_path, profile_compose_file, profiles, expected_services
):
"""
Tests podman-compose
:param podman_compose_path: The fixture used to specify the path to the podman compose file.
:param profile_compose_file: The fixtued used to specify the path to the "profile" compose used in the test.
:param profiles: The enabled profiles for the parameterized test.
:param expected_services: Dictionary used to model the expected "enabled" services in the profile.
Key = service name, Value = True if the service is enabled, otherwise False.
"""
config_cmd = ["coverage", "run", podman_compose_path, "-f", profile_compose_file]
config_cmd.extend(profiles)
out, _, return_code = capture(config_cmd)
assert return_code == 0
actual_output = out.decode("utf-8")
assert len(expected_services) == 3
actual_services = {}
for service, _ in expected_services.items():
actual_services[service] = service in actual_output
assert expected_services == actual_services

View File

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

View File

@ -0,0 +1,180 @@
"""
test_podman_compose_up_down.py
Tests the podman compose up and down commands used to create and remove services.
"""
# pylint: disable=redefined-outer-name
import os
import time
from test_podman_compose import capture
def test_exit_from(podman_compose_path, test_path):
up_cmd = [
"coverage",
"run",
podman_compose_path,
"-f",
os.path.join(test_path, "exit-from", "docker-compose.yaml"),
"up"
]
out, _, return_code = capture(up_cmd + ["--exit-code-from", "sh1"])
assert return_code == 1
out, _, return_code = capture(up_cmd + ["--exit-code-from", "sh2"])
assert return_code == 2
def test_run(podman_compose_path, test_path):
"""
This will test depends_on as well
"""
run_cmd = [
"coverage",
"run",
podman_compose_path,
"-f",
os.path.join(test_path, "deps", "docker-compose.yaml"),
"run",
"--rm",
"sleep",
"/bin/sh",
"-c",
"wget -q -O - http://web:8000/hosts"
]
out, _, return_code = capture(run_cmd)
assert b'127.0.0.1\tlocalhost' in out
# Run it again to make sure we can run it twice. I saw an issue where a second run, with the container left up,
# would fail
run_cmd = [
"coverage",
"run",
podman_compose_path,
"-f",
os.path.join(test_path, "deps", "docker-compose.yaml"),
"run",
"--rm",
"sleep",
"/bin/sh",
"-c",
"wget -q -O - http://web:8000/hosts"
]
out, _, return_code = capture(run_cmd)
assert b'127.0.0.1\tlocalhost' in out
assert return_code == 0
# This leaves a container running. Not sure it's intended, but it matches docker-compose
down_cmd = [
"coverage",
"run",
podman_compose_path,
"-f",
os.path.join(test_path, "deps", "docker-compose.yaml"),
"down",
]
out, _, return_code = capture(run_cmd)
assert return_code == 0
def test_up_with_ports(podman_compose_path, test_path):
up_cmd = [
"coverage",
"run",
podman_compose_path,
"-f",
os.path.join(test_path, "ports", "docker-compose.yml"),
"up",
"-d",
"--force-recreate"
]
down_cmd = [
"coverage",
"run",
podman_compose_path,
"-f",
os.path.join(test_path, "ports", "docker-compose.yml"),
"down",
"--volumes"
]
try:
out, _, return_code = capture(up_cmd)
assert return_code == 0
finally:
out, _, return_code = capture(down_cmd)
assert return_code == 0
def test_down_with_vols(podman_compose_path, test_path):
up_cmd = [
"coverage",
"run",
podman_compose_path,
"-f",
os.path.join(test_path, "vol", "docker-compose.yaml"),
"up",
"-d"
]
down_cmd = [
"coverage",
"run",
podman_compose_path,
"-f",
os.path.join(test_path, "vol", "docker-compose.yaml"),
"down",
"--volumes"
]
try:
out, _, return_code = capture(["podman", "volume", "create", "my-app-data"])
assert return_code == 0
out, _, return_code = capture(["podman", "volume", "create", "actual-name-of-volume"])
assert return_code == 0
out, _, return_code = capture(up_cmd)
assert return_code == 0
capture(["podman", "inspect", "volume", ""])
finally:
out, _, return_code = capture(down_cmd)
capture(["podman", "volume", "rm", "my-app-data"])
capture(["podman", "volume", "rm", "actual-name-of-volume"])
assert return_code == 0
def test_down_with_orphans(podman_compose_path, test_path):
container_id, _ , return_code = capture(["podman", "run", "--rm", "-d", "busybox", "/bin/busybox", "httpd", "-f", "-h", "/etc/", "-p", "8000"])
down_cmd = [
"coverage",
"run",
podman_compose_path,
"-f",
os.path.join(test_path, "ports", "docker-compose.yml"),
"down",
"--volumes",
"--remove-orphans"
]
out, _, return_code = capture(down_cmd)
assert return_code == 0
_, _, exists = capture(["podman", "container", "exists", container_id.decode("utf-8")])
assert exists == 1

View File

@ -0,0 +1,91 @@
"""
test_podman_compose_up_down.py
Tests the podman compose up and down commands used to create and remove services.
"""
# pylint: disable=redefined-outer-name
import os
from test_podman_compose import capture
import pytest
@pytest.fixture
def profile_compose_file(test_path):
""" "Returns the path to the `profile` compose file used for this test module"""
return os.path.join(test_path, "profile", "docker-compose.yml")
@pytest.fixture(autouse=True)
def teardown(podman_compose_path, profile_compose_file):
"""
Ensures that the services within the "profile compose file" are removed between each test case.
:param podman_compose_path: The path to the podman compose script.
:param profile_compose_file: The path to the compose file used for this test module.
"""
# run the test case
yield
down_cmd = [
"coverage",
"run",
podman_compose_path,
"--profile",
"profile-1",
"--profile",
"profile-2",
"-f",
profile_compose_file,
"down",
]
capture(down_cmd)
@pytest.mark.parametrize(
"profiles, expected_services",
[
(
["--profile", "profile-1", "up", "-d"],
{"default-service": True, "service-1": True, "service-2": False},
),
(
["--profile", "profile-2", "up", "-d"],
{"default-service": True, "service-1": False, "service-2": True},
),
(
["--profile", "profile-1", "--profile", "profile-2", "up", "-d"],
{"default-service": True, "service-1": True, "service-2": True},
),
],
)
def test_up(podman_compose_path, profile_compose_file, profiles, expected_services):
up_cmd = [
"coverage",
"run",
podman_compose_path,
"-f",
profile_compose_file,
]
up_cmd.extend(profiles)
out, _, return_code = capture(up_cmd)
assert return_code == 0
check_cmd = [
"podman",
"container",
"ps",
"--format",
'"{{.Names}}"',
]
out, _, return_code = capture(check_cmd)
assert return_code == 0
assert len(expected_services) == 3
actual_output = out.decode("utf-8")
actual_services = {}
for service, _ in expected_services.items():
actual_services[service] = service in actual_output
assert expected_services == actual_services

View File

@ -0,0 +1,15 @@
version: "3.7"
services:
touch:
image: busybox
command: 'touch /mnt/test'
volumes:
- ./:/mnt
user: 999:999
x-podman:
uidmaps:
- "0:1:1"
- "999:0:1"
gidmaps:
- "0:1:1"
- "999:0:1"

View File

@ -0,0 +1,7 @@
version: "3"
services:
web:
volumes:
- ./override.txt:/var/www/html/index.html:ro,z
- ./override.txt:/var/www/html/index2.html:z
- ./override.txt:/var/www/html/index3.html

View File

@ -0,0 +1,11 @@
version: "3"
services:
web:
image: busybox
command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8080"]
ports:
- 8080:8080
volumes:
- ./index.txt:/var/www/html/index.html:ro,z
- ./index.txt:/var/www/html/index2.html
- ./index.txt:/var/www/html/index3.html:ro

View File

@ -0,0 +1 @@
The file from docker-compose.yaml

View File

@ -0,0 +1 @@
The file from docker-compose.override.yaml