From fe2993097309e2093e5fa8b7cf279b8b88862584 Mon Sep 17 00:00:00 2001 From: axel7083 <42176370+axel7083@users.noreply.github.com> Date: Tue, 9 Sep 2025 16:08:43 +0200 Subject: [PATCH 1/4] feat(podman): kube play --- core/testcontainers/podman/__init__.py | 9 ++ core/testcontainers/podman/kube_play.py | 88 +++++++++++++++++++ .../podman_fixtures/kube_play/basic/play.yaml | 10 +++ core/tests/test_podman_kube_play.py | 14 +++ 4 files changed, 121 insertions(+) create mode 100644 core/testcontainers/podman/__init__.py create mode 100644 core/testcontainers/podman/kube_play.py create mode 100644 core/tests/podman_fixtures/kube_play/basic/play.yaml create mode 100644 core/tests/test_podman_kube_play.py diff --git a/core/testcontainers/podman/__init__.py b/core/testcontainers/podman/__init__.py new file mode 100644 index 00000000..c9bb16c0 --- /dev/null +++ b/core/testcontainers/podman/__init__.py @@ -0,0 +1,9 @@ +# flake8: noqa: F401 +from testcontainers.podman.kube_play import ( + KubePlay +) + +__all__ = [ + "KubePlay", +] + diff --git a/core/testcontainers/podman/kube_play.py b/core/testcontainers/podman/kube_play.py new file mode 100644 index 00000000..130a3846 --- /dev/null +++ b/core/testcontainers/podman/kube_play.py @@ -0,0 +1,88 @@ +from dataclasses import dataclass +from logging import getLogger +from os import PathLike, path +from subprocess import CompletedProcess, CalledProcessError, run as subprocess_run +from types import TracebackType +from typing import Optional, Union +import re + +logger = getLogger(__name__) + +def parse_kube_play_stdout(stdout: str) -> dict[str, list[str]]: + pattern = re.compile(r"(^.+?):\n((?:[a-f0-9]+\n?)*)", re.MULTILINE) + result = {} + for key, values in pattern.findall(stdout): + # split values by newline, remove empties + value_list = [v for v in values.strip().splitlines() if v] + result[key] = value_list + return result + +@dataclass +class KubePlay: + kube_play_file: Union[str, PathLike[str]] + context: Optional[str] = None + podman_command_path: str = "podman" + build: bool = False + keep_volumes: bool = False + replace: bool = False + + + _pods: Optional[list[str]] = None + + def __enter__(self) -> "KubePlay": + self.start() + return self + + def __exit__( + self, exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType] + ) -> None: + self.stop(force=not self.keep_volumes) + + def start(self) -> None: + start_cmd = [self.podman_command_path, "kube", "play", self.kube_play_file] + + # build means modifying the up command + if self.build: + start_cmd.append("--build") + + if self.replace: + start_cmd.append("--replace") + + result = self._run_command(cmd=start_cmd) + if result.returncode != 0: + raise RuntimeError(f"Podman command failed with exit code {result.returncode}") + + info = parse_kube_play_stdout(result.stdout.decode("utf-8")) + self._pods = info['Pod'] + + + def stop(self, force = False) -> None: + down_cmd = [self.podman_command_path, "kube", "down", self.kube_play_file] + + if force: + # Tear down the volumes linked to the PersistentVolumeClaims as part of --down + down_cmd.append("--force") + + self._run_command(cmd=down_cmd) + self._pods = None + + def get_pods(self) -> list[str]: + return self._pods if self._pods is not None else [] + + def _run_command( + self, + cmd: Union[str, list[str]], + ) -> CompletedProcess[bytes]: + context = self.context if self.context else path.dirname(self.kube_play_file) + try: + return subprocess_run( + cmd, + capture_output=True, + check=True, + cwd=context, + ) + except CalledProcessError as e: + logger.error(f"Command '{e.cmd}' failed with exit code {e.returncode}") + logger.error(f"STDOUT:\n{e.stdout.decode(errors='ignore')}") + logger.error(f"STDERR:\n{e.stderr.decode(errors='ignore')}") + raise e from e \ No newline at end of file diff --git a/core/tests/podman_fixtures/kube_play/basic/play.yaml b/core/tests/podman_fixtures/kube_play/basic/play.yaml new file mode 100644 index 00000000..13dba09b --- /dev/null +++ b/core/tests/podman_fixtures/kube_play/basic/play.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx +spec: + containers: + - name: nginx + image: nginx + ports: + - containerPort: 80 \ No newline at end of file diff --git a/core/tests/test_podman_kube_play.py b/core/tests/test_podman_kube_play.py new file mode 100644 index 00000000..eef0f93d --- /dev/null +++ b/core/tests/test_podman_kube_play.py @@ -0,0 +1,14 @@ +from pathlib import Path + +from testcontainers.podman import KubePlay + +FIXTURES = Path(__file__).parent.joinpath("podman_fixtures", "kube_play") + +def test_podman_kube_play(): + basic = KubePlay( + kube_play_file=FIXTURES.joinpath("basic", "play.yaml"), + replace=True, + ) + + with basic: + assert len(basic.get_pods()) == 1 From d6cdbf37627a5bdf8a5031c05ed3e9ef8b49ca84 Mon Sep 17 00:00:00 2001 From: axel7083 <42176370+axel7083@users.noreply.github.com> Date: Tue, 9 Sep 2025 16:13:46 +0200 Subject: [PATCH 2/4] fix: remove replace in tests --- core/tests/test_podman_kube_play.py | 1 - 1 file changed, 1 deletion(-) diff --git a/core/tests/test_podman_kube_play.py b/core/tests/test_podman_kube_play.py index eef0f93d..a0476d68 100644 --- a/core/tests/test_podman_kube_play.py +++ b/core/tests/test_podman_kube_play.py @@ -7,7 +7,6 @@ def test_podman_kube_play(): basic = KubePlay( kube_play_file=FIXTURES.joinpath("basic", "play.yaml"), - replace=True, ) with basic: From d4f49d1a4663bb406532bb0b5aadfe228e9dfa4f Mon Sep 17 00:00:00 2001 From: axel7083 <42176370+axel7083@users.noreply.github.com> Date: Tue, 9 Sep 2025 16:24:48 +0200 Subject: [PATCH 3/4] chore: bump runners to ubuntu-24.04 --- .github/workflows/ci-community.yml | 4 ++-- .github/workflows/ci-core.yml | 4 ++-- .github/workflows/ci-lint.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/pr-lint.yml | 2 +- .github/workflows/release-please.yml | 4 ++-- .readthedocs.yml | 2 +- core/testcontainers/podman/kube_play.py | 4 ++-- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci-community.yml b/.github/workflows/ci-community.yml index 58faa42a..f986952b 100644 --- a/.github/workflows/ci-community.yml +++ b/.github/workflows/ci-community.yml @@ -14,7 +14,7 @@ on: jobs: track-modules: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout contents uses: actions/checkout@v4 @@ -38,7 +38,7 @@ jobs: outputs: changed_modules: ${{ steps.compute-changes.outputs.computed_modules }} test: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 needs: [track-modules] if: ${{ needs.track-modules.outputs.changed_modules != '[]' }} strategy: diff --git a/.github/workflows/ci-core.yml b/.github/workflows/ci-core.yml index 34aabc73..2518fc2d 100644 --- a/.github/workflows/ci-core.yml +++ b/.github/workflows/ci-core.yml @@ -10,7 +10,7 @@ on: jobs: run-tests-and-coverage: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: @@ -43,7 +43,7 @@ jobs: coverage-compile: needs: "run-tests-and-coverage" - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Set up Python diff --git a/.github/workflows/ci-lint.yml b/.github/workflows/ci-lint.yml index 3d9d1701..ab95297b 100644 --- a/.github/workflows/ci-lint.yml +++ b/.github/workflows/ci-lint.yml @@ -10,7 +10,7 @@ on: jobs: python: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Setup Env diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e27c89ad..f23131ba 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,7 +8,7 @@ on: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Set up Python diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 84d80590..9201fa40 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -13,7 +13,7 @@ permissions: jobs: validate: name: validate-pull-request-title - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: validate pull request title uses: kontrolplane/pull-request-title-validator@ab2b54babb5337246f4b55cf8e0a1ecb0575e46d #v1 diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 9176c674..45eee8b6 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -6,7 +6,7 @@ on: jobs: release: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 outputs: release_created: ${{ steps.track-release.outputs.release_created }} steps: @@ -16,7 +16,7 @@ jobs: manifest-file: .github/.release-please-manifest.json config-file: .github/release-please-config.json publish: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 environment: release permissions: id-token: write diff --git a/.readthedocs.yml b/.readthedocs.yml index 43b3dc8c..7405b410 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -7,7 +7,7 @@ sphinx: configuration: conf.py build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: python: "3.10" diff --git a/core/testcontainers/podman/kube_play.py b/core/testcontainers/podman/kube_play.py index 130a3846..87438c26 100644 --- a/core/testcontainers/podman/kube_play.py +++ b/core/testcontainers/podman/kube_play.py @@ -39,7 +39,7 @@ def __exit__( self.stop(force=not self.keep_volumes) def start(self) -> None: - start_cmd = [self.podman_command_path, "kube", "play", self.kube_play_file] + start_cmd = [self.podman_command_path, "kube", "play", str(self.kube_play_file)] # build means modifying the up command if self.build: @@ -56,7 +56,7 @@ def start(self) -> None: self._pods = info['Pod'] - def stop(self, force = False) -> None: + def stop(self, force: bool = False) -> None: down_cmd = [self.podman_command_path, "kube", "down", self.kube_play_file] if force: From d3e09a3138b9344c55b4081588239688f0bc048a Mon Sep 17 00:00:00 2001 From: axel7083 <42176370+axel7083@users.noreply.github.com> Date: Tue, 9 Sep 2025 16:31:56 +0200 Subject: [PATCH 4/4] fix: linter --- core/testcontainers/podman/kube_play.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/testcontainers/podman/kube_play.py b/core/testcontainers/podman/kube_play.py index 87438c26..b7d31c11 100644 --- a/core/testcontainers/podman/kube_play.py +++ b/core/testcontainers/podman/kube_play.py @@ -57,7 +57,7 @@ def start(self) -> None: def stop(self, force: bool = False) -> None: - down_cmd = [self.podman_command_path, "kube", "down", self.kube_play_file] + down_cmd = [self.podman_command_path, "kube", "down", str(self.kube_play_file)] if force: # Tear down the volumes linked to the PersistentVolumeClaims as part of --down