From 54f59cffc00bf43a00e58159950fdd4c1bc69a85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20=C4=8Cerm=C3=A1k?= Date: Tue, 27 Aug 2024 10:24:00 +0200 Subject: [PATCH] WIP: refactor tests using MultiStageBuild to use MultiStageContainer & container_image --- tests/files/main.go | 23 ++++ tests/test_all.py | 103 +++++++---------- tests/test_multistage.py | 234 +++++++++++++-------------------------- tests/test_spack.py | 121 ++++++++++---------- tox.ini | 2 +- 5 files changed, 195 insertions(+), 288 deletions(-) create mode 100644 tests/files/main.go diff --git a/tests/files/main.go b/tests/files/main.go new file mode 100644 index 00000000..1b29ec6a --- /dev/null +++ b/tests/files/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" + "io" + "net/http" +) + +func main() { + resp, err := http.Get("https://suse.com/") + if err != nil { + panic(err) + } + + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + panic(err) + } + + fmt.Println(string(body)) +} diff --git a/tests/test_all.py b/tests/test_all.py index 18b26924..422a0560 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -10,15 +10,13 @@ import packaging.version import pytest -from _pytest.config import Config from pytest_container import Container from pytest_container import DerivedContainer -from pytest_container import MultiStageBuild from pytest_container import container_and_marks_from_pytest_param -from pytest_container import get_extra_build_args -from pytest_container import get_extra_run_args from pytest_container.container import BindMount from pytest_container.container import ContainerData +from pytest_container.container import ContainerImageData +from pytest_container.container import MultiStageContainer from bci_tester.data import ALLOWED_BCI_REPO_OS_VERSIONS from bci_tester.data import ALL_CONTAINERS @@ -39,33 +37,6 @@ CONTAINER_IMAGES = ALL_CONTAINERS -#: go file to perform a GET request to suse.com and that panics if the request -#: fails -FETCH_SUSE_DOT_COM = """package main - -import "net/http" - -func main() { - _, err := http.Get("https://suse.com/") - if err != nil { - panic(err) - } -} -""" - -MULTISTAGE_DOCKERFILE = """FROM $builder as builder -WORKDIR /src -COPY main.go . -RUN CGO_ENABLED=0 GOOS=linux go build main.go - -FROM $runner -ENTRYPOINT [] -WORKDIR /fetcher/ -COPY --from=builder /src/main . -CMD ["/fetcher/main"] -""" - - def test_os_release(auto_container): """ :file:`/etc/os-release` is present and the values of ``OS_VERSION`` and @@ -421,10 +392,44 @@ def test_no_compat_packages(container): ).is_installed -@pytest.mark.parametrize("runner", ALL_CONTAINERS) +MULTISTAGE_DOCKERFILE = """FROM $builder as builder +WORKDIR /src +COPY tests/files/main.go . +RUN CGO_ENABLED=0 GOOS=linux go build main.go + +FROM $runner +ENTRYPOINT [] +WORKDIR /fetcher/ +COPY --from=builder /src/main . +CMD ["/fetcher/main"] +""" + + +CERTIFICATE_FETCHER_CONTAINERS = [] +for param in ALL_CONTAINERS: + ctr, marks = container_and_marks_from_pytest_param(param) + assert isinstance(ctr, (DerivedContainer, Container)) + + CERTIFICATE_FETCHER_CONTAINERS.append( + pytest.param( + MultiStageContainer( + containerfile=MULTISTAGE_DOCKERFILE, + containers={ + "builder": "registry.suse.com/bci/golang:latest", + "runner": ctr, + }, + ), + marks=marks or [], + ) + ) + + +@pytest.mark.parametrize( + "container_image", CERTIFICATE_FETCHER_CONTAINERS, indirect=True +) def test_certificates_are_present( - host, tmp_path, container_runtime, runner: Container, pytestconfig: Config -): + container_image: ContainerImageData, host +) -> None: """This is a multistage container build, verifying that the certificates are correctly set up in the containers. @@ -435,34 +440,8 @@ def test_certificates_are_present( If the certificates are incorrectly set up, then the GET request will fail. """ - multi_stage_build = MultiStageBuild( - containers={ - "builder": "registry.suse.com/bci/golang:latest", - "runner": runner, - }, - containerfile_template=MULTISTAGE_DOCKERFILE, - ) - multi_stage_build.prepare_build( - tmp_path, container_runtime, pytestconfig.rootpath - ) - - with open(tmp_path / "main.go", "w", encoding="utf-8") as main_go: - main_go.write(FETCH_SUSE_DOT_COM) - - # FIXME: ugly duplication of pytest_container internals :-/ - # see: https://github.com/dcermak/pytest_container/issues/149 - iidfile = tmp_path / "iid" - host.run_expect( - [0], - f"{' '.join(container_runtime.build_command + get_extra_build_args(pytestconfig))} " - f"--iidfile={iidfile} {tmp_path}", - ) - img_id = container_runtime.get_image_id_from_iidfile(iidfile) - - host.run_expect( - [0], - f"{container_runtime.runner_binary} run --rm {' '.join(get_extra_run_args(pytestconfig))} {img_id}", - ) + out = host.check_output(container_image.run_command) + assert "html" in out @pytest.mark.skipif( diff --git a/tests/test_multistage.py b/tests/test_multistage.py index 5d0e76ab..d06a7075 100644 --- a/tests/test_multistage.py +++ b/tests/test_multistage.py @@ -1,15 +1,13 @@ """Integration tests via multistage container builds.""" +from typing import Optional + import pytest -from _pytest.config import Config -from pytest_container import GitRepositoryBuild -from pytest_container import MultiStageBuild -from pytest_container import get_extra_build_args -from pytest_container import get_extra_run_args +from pytest_container import container_and_marks_from_pytest_param +from pytest_container.container import ContainerImageData +from pytest_container.container import MultiStageContainer from pytest_container.runtime import LOCALHOST -from bci_tester.data import DOTNET_ASPNET_8_0_CONTAINER -from bci_tester.data import DOTNET_SDK_8_0_CONTAINER from bci_tester.data import GOLANG_CONTAINERS from bci_tester.data import OPENJDK_11_CONTAINER from bci_tester.data import OPENJDK_21_CONTAINER @@ -17,15 +15,24 @@ from bci_tester.data import OPENJDK_DEVEL_21_CONTAINER from bci_tester.data import OS_VERSION + +def _clone_cmd(repo_url: str, tag: Optional[str] = None) -> str: + res = "git clone --depth 1 " + if tag: + res += f"--branch {tag} " + return res + repo_url + + #: maven version that is being build in the multistage test build -MAVEN_VERSION = "3.9.6" +MAVEN_VERSION = "3.9.9" #: Dockerfile template to build `amidst #: `_ -AMIDST_DOCKERFILE = """FROM $builder as builder +AMIDST_DOCKERFILE = f"""FROM $builder as builder +RUN {_clone_cmd('https://github.com/toolbox4minecraft/amidst', 'v4.7')} /amidst WORKDIR /amidst -COPY ./amidst . RUN mvn package -DskipTests=True + FROM $runner WORKDIR /amidst/ COPY --from=builder /amidst/target . @@ -35,10 +42,11 @@ #: template of a Dockerfile to build maven in the OpenJDK devel container and #: copy it into the OpenJDK base container MAVEN_BUILD_DOCKERFILE = f"""FROM $builder as builder +RUN {_clone_cmd('https://github.com/apache/maven', 'maven-' + MAVEN_VERSION)} /maven WORKDIR /maven -COPY ./maven . RUN mvn package && zypper -n in unzip && \ unzip /maven/apache-maven/target/apache-maven-{MAVEN_VERSION}-bin.zip + FROM $runner WORKDIR /maven/ COPY --from=builder /maven/apache-maven-{MAVEN_VERSION}/ . @@ -47,9 +55,9 @@ #: Dockerfile to build pdftk in the openjdk devel container and transfer it into #: the openjdk container. -PDFTK_BUILD_DOCKERFILE = """FROM $builder as builder +PDFTK_BUILD_DOCKERFILE = f"""FROM $builder as builder +RUN {_clone_cmd('https://gitlab.com/pdftk-java/pdftk.git', 'v3.3.3')} /pdftk WORKDIR /pdftk -COPY ./pdftk . RUN zypper -n in apache-ant apache-ivy && ant test-resolve && ant compile && ant jar FROM $runner @@ -61,10 +69,10 @@ #: dockerfile template to build k3sup in the go #: container and transfer it into a scratch container -K3SUP_DOCKERFILE = """FROM $builder as builder +K3SUP_DOCKERFILE = f"""FROM $builder as builder +RUN {_clone_cmd('https://github.com/alexellis/k3sup', '0.13.7')} /k3sup WORKDIR /k3sup -COPY ./k3sup . -RUN zypper -n in make && echo > ./hack/hashgen.sh && make all +RUN echo > ./hack/hashgen.sh && make all FROM $runner WORKDIR /k3sup @@ -72,43 +80,6 @@ CMD ["/k3sup/k3sup"] """ -#: modified version of upstream's `Dockerfile -#: `_: -#: -#: - the ``entrypoint.sh`` is custom, so that the application terminates -#: - the docker build is not run from the repos top level dir, but one directory -#: "above" -DOTNET_K8S_SAMPLE_DOCKERFILE = r"""FROM $builder AS build -WORKDIR /src - -COPY ./adventureworks-k8s-sample/AdventureWorks.sln AdventureWorks.sln -COPY ./adventureworks-k8s-sample/AdventureWorks.App/*.csproj ./AdventureWorks.App/ -COPY ./adventureworks-k8s-sample/BlazorLeaflet/*.csproj ./BlazorLeaflet/ -RUN dotnet restore AdventureWorks.sln - -COPY ./adventureworks-k8s-sample/AdventureWorks.App/. ./AdventureWorks.App/ -COPY ./adventureworks-k8s-sample/BlazorLeaflet/. ./BlazorLeaflet/ - -RUN dotnet publish -c release -o /app --no-restore - -FROM $runner -WORKDIR /app -COPY --from=build /app ./ -RUN zypper -n in curl -RUN echo $$'#!/bin/bash -e \n\ -dotnet AdventureWorks.App.dll & \n\ -c=0 \n\ -until curl localhost:5000 >/dev/null ; do \n\ - ((c++)) && ((c==20)) && ( \n\ - curl localhost:5000|grep Adventure \n\ - exit 1 \n\ - ) \n\ - sleep 1 \n\ -done \n\ -pkill dotnet \n\ -' > entrypoint.sh && chmod +x entrypoint.sh -ENTRYPOINT ["/app/entrypoint.sh"] -""" OPENJDK_DEVEL_CONTAINER = OPENJDK_DEVEL_11_CONTAINER if OS_VERSION in ("15.6",): @@ -118,118 +89,79 @@ if OS_VERSION in ("15.6",): OPENJDK_CONTAINER = OPENJDK_21_CONTAINER +_jdk_devel_ctr, _jdk_devel_marks = container_and_marks_from_pytest_param( + OPENJDK_DEVEL_CONTAINER +) +_jdk_ctr, _jdk_marks = container_and_marks_from_pytest_param(OPENJDK_CONTAINER) + @pytest.mark.parametrize( - "host_git_clone,multi_stage_build,retval,cmd_stdout", + "container_image, retval, expected_stdout", [ - ( - GitRepositoryBuild( - repository_url="https://github.com/toolbox4minecraft/amidst", - repository_tag="v4.7", - ), - MultiStageBuild( + pytest.param( + MultiStageContainer( containers={ - "builder": OPENJDK_DEVEL_CONTAINER, - "runner": OPENJDK_CONTAINER, + "builder": _jdk_devel_ctr, + "runner": _jdk_ctr, }, - containerfile_template=AMIDST_DOCKERFILE, + containerfile=AMIDST_DOCKERFILE, ), 0, "[info] Amidst v4.7", + marks=_jdk_marks + _jdk_devel_marks, ), - ( - GitRepositoryBuild( - repository_url="https://github.com/apache/maven", - repository_tag=f"maven-{MAVEN_VERSION}", - ), - MultiStageBuild( - containers={ - "builder": OPENJDK_DEVEL_CONTAINER, - "runner": OPENJDK_DEVEL_CONTAINER, - }, - containerfile_template=MAVEN_BUILD_DOCKERFILE, - ), - 1, - "[ERROR] No goals have been specified for this build.", - ), - ( - GitRepositoryBuild( - repository_tag="v3.3.3", - repository_url="https://gitlab.com/pdftk-java/pdftk.git", - ), - MultiStageBuild( - containers={ - "builder": OPENJDK_DEVEL_CONTAINER, - "runner": OPENJDK_CONTAINER, - }, - containerfile_template=PDFTK_BUILD_DOCKERFILE, + pytest.param( + MultiStageContainer( + containers={"builder": _jdk_devel_ctr, "runner": _jdk_ctr}, + containerfile=PDFTK_BUILD_DOCKERFILE, ), 0, """SYNOPSIS - pdftk - """, + pdftk """, + marks=_jdk_marks + _jdk_devel_marks, ), pytest.param( - GitRepositoryBuild( - repository_tag="0.12.4", - repository_url="https://github.com/alexellis/k3sup", - ), - MultiStageBuild( + MultiStageContainer( containers={ - "builder": GOLANG_CONTAINERS[-1], - "runner": "scratch", + "builder": _jdk_devel_ctr, + "runner": _jdk_devel_ctr, }, - containerfile_template=K3SUP_DOCKERFILE, - ), - 0, - 'Use "k3sup [command] --help" for more information about a command.', - marks=pytest.mark.xfail( - condition=LOCALHOST.system_info.arch != "x86_64", - reason="Currently broken on arch != x86_64, see https://github.com/alexellis/k3sup/pull/345", + containerfile=MAVEN_BUILD_DOCKERFILE, ), + 1, + "No goals have been specified for this build.", + marks=_jdk_devel_marks, ), + ] + + [ pytest.param( - GitRepositoryBuild( - repository_url="https://github.com/phillipsj/adventureworks-k8s-sample.git" - ), - MultiStageBuild( + MultiStageContainer( containers={ - "builder": DOTNET_SDK_8_0_CONTAINER, - "runner": DOTNET_ASPNET_8_0_CONTAINER, + "builder": container_and_marks_from_pytest_param(param)[0], + "runner": "scratch", }, - containerfile_template=DOTNET_K8S_SAMPLE_DOCKERFILE, + containerfile=K3SUP_DOCKERFILE, ), 0, - """Microsoft.Hosting.Lifetime[0]\n Now listening on: http://localhost:5000 -\x1b[40m\x1b[32minfo\x1b[39m\x1b[22m\x1b[49m: Microsoft.Hosting.Lifetime[0] - Application started. Press Ctrl+C to shut down. -\x1b[40m\x1b[32minfo\x1b[39m\x1b[22m\x1b[49m: Microsoft.Hosting.Lifetime[0] - Hosting environment: Production -\x1b[40m\x1b[32minfo\x1b[39m\x1b[22m\x1b[49m: Microsoft.Hosting.Lifetime[0] - Content root path: /app -""", + 'Use "k3sup [command] --help" for more information about a command.', marks=[ - pytest.mark.skipif( - LOCALHOST.system_info.arch != "x86_64", - reason="The dotnet containers are only available for x86_64", - ), - pytest.mark.skip( - reason="This relies on .Net 5, which reached Eol, see: https://github.com/phillipsj/adventureworks-k8s-sample/issues/4" - ), - ], - ), + pytest.mark.xfail( + condition=LOCALHOST.system_info.arch != "x86_64", + reason="Currently broken on arch != x86_64, see https://github.com/alexellis/k3sup/pull/345", + ) + ] + + param.marks, + ) + for param in GOLANG_CONTAINERS ], - indirect=["host_git_clone"], + indirect=["container_image"], ) -def test_dockerfile_build( - host, - container_runtime, - host_git_clone, - multi_stage_build: MultiStageBuild, +def test_multistage_build( + container_image: ContainerImageData, retval: int, - cmd_stdout: str, - pytestconfig: Config, -): + expected_stdout: str, + host, +) -> None: """Integration test of multistage container builds. We fetch a project (optionally checking out a specific tag), run a two stage build using a dockerfile template where we substitute the ``$runner`` and ``$builder`` @@ -259,7 +191,7 @@ def test_dockerfile_build( - OpenJDK devel - OpenJDK - ``1`` - - ``[ERROR] No goals have been specified for this build.`` + - ``No goals have been specified for this build.`` * - ``_ at ``v3.3.1`` - :py:const:`PDFTK_BUILD_DOCKERFILE` @@ -275,26 +207,8 @@ def test_dockerfile_build( - ``0`` - ``Use "k3sup [command] --help" for more information about a command.`` - * - ``_ - - :py:const:`DOTNET_K8S_SAMPLE_DOCKERFILE` - - .Net 5.0 - - ASP.Net 5.0 - - ``0`` - - `omitted` """ - tmp_path, _ = host_git_clone - - img_id = multi_stage_build.build( - tmp_path, - pytestconfig, - container_runtime, - extra_build_args=get_extra_build_args(pytestconfig), - ) - assert ( - cmd_stdout - in host.run_expect( - [retval], - f"{container_runtime.runner_binary} run --rm {' '.join(get_extra_run_args(pytestconfig))}{img_id}", - ).stdout - ) + assert expected_stdout in host.run_expect( + [retval], container_image.run_command + ).stdout.replace("\r", "") diff --git a/tests/test_spack.py b/tests/test_spack.py index b42a2a8a..09fc5038 100644 --- a/tests/test_spack.py +++ b/tests/test_spack.py @@ -1,95 +1,86 @@ """Tests for Spack application build container images.""" -from textwrap import dedent - import pytest from _pytest.config import Config -from pytest_container import MultiStageBuild -from pytest_container.container import BindMount +from pytest_container.container import ContainerImageData +from pytest_container.container import ContainerLauncher from pytest_container.container import DerivedContainer +from pytest_container.container import EntrypointSelection from pytest_container.container import ImageFormat +from pytest_container.container import MultiStageContainer from pytest_container.container import container_and_marks_from_pytest_param -from pytest_container.helpers import get_extra_build_args -from pytest_container.helpers import get_extra_run_args +from pytest_container.runtime import OciRuntimeBase from bci_tester.data import BASE_CONTAINER from bci_tester.data import SPACK_CONTAINERS -from bci_tester.runtime_choice import PODMAN_SELECTED + +BASE_CTR, _ = container_and_marks_from_pytest_param(BASE_CONTAINER) +assert isinstance(BASE_CTR, DerivedContainer) + +SPACK_IMAGES_WITH_YAML_CONFIG = [] + +for param in SPACK_CONTAINERS: + spac_ctr, spac_marks = container_and_marks_from_pytest_param(param) + assert isinstance(spac_ctr, DerivedContainer) + + SPACK_IMAGES_WITH_YAML_CONFIG.append( + pytest.param( + DerivedContainer( + base=spac_ctr, + containerfile=rf"""SHELL ["/bin/bash", "-c"] +RUN echo $'spack: \n\ + specs: \n\ + - zsh \n\ + container: \n\ + format: docker \n\ + images: \n\ + build: "{spac_ctr.baseurl}" \n\ + final: "{BASE_CTR.baseurl}" \n\ +' > /root/spack.yaml +""", + ), + marks=spac_marks or [], + id=param.id, + ) + ) @pytest.mark.parametrize( - "container", - SPACK_CONTAINERS, - indirect=True, + "container_image", SPACK_IMAGES_WITH_YAML_CONFIG, indirect=True ) def test_spack( - container, + container_image: ContainerImageData, host, - container_runtime, - tmp_path, + container_runtime: OciRuntimeBase, pytestconfig: Config, ) -> None: - """ - Test if Spack Container can build a zsh container. + """Test if Spack Container can build a zsh container. - This function creates a `spack.yaml` input for spack, mounts it into the container, - runs the container with the 'containerize' argument which provides a `Containerfile` - multi-stage build description to build a zsh container. - - For the final stage, the base container of the spack container is being used. + This function uses the spack container image with a :file:`spack.yaml` + embedded in the image to run :command:`spack containerize` which outputs a + `Containerfile` multi-stage build description to build a zsh container. The test is building this description as a multi-stage container, and finally tests whether the zsh in the resulting container can be successfully launched. - """ - # Create spack.yaml file in temporary directory - with open(tmp_path / "spack.yaml", "w", encoding="utf-8") as spack_yaml: - spack_yaml.write( - dedent( - f""" - spack: - specs: - - zsh - - container: - format: docker - images: - build: "{container.image_url_or_id}" - final: "{DerivedContainer.get_base(container_and_marks_from_pytest_param(BASE_CONTAINER)[0]).url}" - """ - ) - ) - # mount spack.yaml into container (/root) - mount_arg = BindMount( - host_path=tmp_path / "spack.yaml", - container_path="/root/spack.yaml", - ).cli_arg - # run container with argument: 'containerize', save output to variable 'containerfile' + """ containerfile = host.check_output( - f"{container_runtime.runner_binary} run --rm {mount_arg} " - f"{' '.join(get_extra_run_args(pytestconfig))} " - f"{container.image_url_or_id} containerize", + f"{container_image.run_command} containerize" ) - multi_stage_build = MultiStageBuild( - containers={ - "builder": container.container, - "runner": BASE_CONTAINER, - }, - containerfile_template=containerfile.replace("$", "$$"), + ctr = MultiStageContainer( + containerfile=containerfile.replace("$", "$$"), + image_format=ImageFormat.DOCKER, + entry_point=EntrypointSelection.IMAGE, ) - build_args = get_extra_run_args(pytestconfig) - if PODMAN_SELECTED: - build_args += ["--format", str(ImageFormat.DOCKER)] + with ContainerLauncher.from_pytestconfig( + container=ctr, + container_runtime=container_runtime, + pytestconfig=pytestconfig, + ) as launcher: + launcher.prepare_container() + ci = launcher.container_image_data - runner_id = multi_stage_build.build( - tmp_path, pytestconfig, container_runtime, extra_build_args=build_args - ) - # Run resulting container and test whether zsh is running - assert host.check_output( - f"{container_runtime.runner_binary} run --rm " - f"{' '.join(get_extra_build_args(pytestconfig))} " - f"{runner_id} zsh -c 'echo $ZSH_VERSION' ", - ) + host.check_output(f"{ci.run_command} zsh -c 'echo $ZSH_VERSION'") diff --git a/tox.ini b/tox.ini index d1034308..d6cb89c9 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ deps = requests # 8.4.0 is borked: https://github.com/jd/tenacity/issues/471 tenacity != 8.4.0 - git+https://github.com/dcermak/pytest_container + git+https://github.com/dcermak/pytest_container@multistage doc: Sphinx [testenv]