From e98698039f4ea6835323027541c2feb5df3fc8e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dirk=20M=C3=BCller?= Date: Wed, 9 Oct 2024 23:08:08 +0200 Subject: [PATCH] Introduce a branch_version concept in BCI Now that we have build_flavors, we can add branch_versions, describing the full hierachy: Each container can have a single `name`, one or many `branch_versions` and each of them one or many `build_flavors`. this means our naming should become {name}-{branch_version}-{build_flavor} Introduce this for the AppCollection containers first. This should eventually replace tag_version, which is a broken concept as it mixes namings, versions and flavors into and hence makes everything convoluted. This commit mixes also a hook in Registry to use version, rather than tag_version for the build_version, which is a must-have for the AppCollection integration. Add tests for ApplicationStackContainers in unit test, which was entirely untested so far. Removal of the deprecated tag_version will happen in a later followup. --- src/bci_build/package/__init__.py | 60 ++++++++++++------- src/bci_build/package/apache_tomcat.py | 6 +- src/bci_build/registry.py | 26 +++++++++ tests/test_build_recipe.py | 80 ++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 23 deletions(-) diff --git a/src/bci_build/package/__init__.py b/src/bci_build/package/__init__.py index cd651523f..b9a3771e9 100644 --- a/src/bci_build/package/__init__.py +++ b/src/bci_build/package/__init__.py @@ -14,7 +14,6 @@ from typing import overload import jinja2 -from packaging import version from bci_build.containercrate import ContainerCrate from bci_build.os_version import ALL_OS_LTSS_VERSIONS @@ -1214,10 +1213,14 @@ class DevelopmentContainer(BaseContainerImage): #: used for `org.opencontainers.image.version` version: str | int = "" - #: the version-$variant to use in the first build_tag. defaults to version - #: if not set. + #: the version-$variant to use in the first build_tag. defaults to version(-$build_flavor) + #: if not set. Deprecated; use branch_version instead. tag_version: str | None = None + #: the version branch for this container to disambiguate the build name and uid + #: defaults to tag_version if not set. + branch_version: str | None = None + # a rolling stability tag like 'stable' or 'oldstable' that will be added first stability_tag: str | None = None @@ -1231,7 +1234,11 @@ def __post_init__(self) -> None: super().__post_init__() if self.version and not self.tag_version: - self.tag_version = self.version + self.tag_version = ( + f"{self.version}-{self.build_flavor}" + if self.build_flavor + else self.version + ) if not self.tag_version: raise ValueError("A development container requires a tag_version") @@ -1253,9 +1260,21 @@ def image_type(self) -> ImageType: def oci_version(self) -> str: return str(self.version) + @property + def _variant(self) -> str: + """return the variant of this container in the name/branch/flavor hierarchy.""" + branch_version = ( + self.branch_version if self.branch_version else self.tag_version + ) + return ( + f"{branch_version}-{self.build_flavor}" + if self.build_flavor + else branch_version + ) + @property def uid(self) -> str: - return f"{self.name}-{self.tag_version}" if self.version_in_uid else self.name + return f"{self.name}-{self._variant}" if self.version_in_uid else self.name @property def _stability_suffix(self) -> str: @@ -1335,26 +1354,27 @@ def reference(self) -> str: @property def pretty_reference(self) -> str: - return f"{self.registry}/{self.registry_prefix}/{self.name}:{self.tag_version}" + return f"{self.registry}/{self.registry_prefix}/{self.name}:{self._variant}" + + @property + def build_name(self) -> str | None: + """Handles a build_name with respecting branch_version.""" + if self.build_tags: + build_name: str = self.build_tags[0] + # Prefer branch_version over build_tags[0] (which is tag_version derived) + if self.branch_version: + build_name = f"{self.registry_prefix}/{self.name}-{self._variant}" + if self.is_singleton_image: + build_name = build_name.partition(":")[0] + return build_name.replace("/", ":").replace(":", "-") + + return None @property def build_version(self) -> str | None: build_ver = super().build_version if build_ver: - container_version: str = self.tag_version - # if container_version is a numeric version and not a macro, then - # version.parse() returns a `Version` object => then we concatenate - # it with the existing build_version - # for non PEP440 versions, we'll get an exception and just return - # the parent's classes build_version - try: - version.parse(str(container_version)) - stability_suffix: str = "" - if self._stability_suffix: - stability_suffix = "." + self._stability_suffix - return f"{build_ver}.{container_version}{stability_suffix}" - except version.InvalidVersion: - return build_ver + return self.publish_registry.build_version(build_ver, self) return None diff --git a/src/bci_build/package/apache_tomcat.py b/src/bci_build/package/apache_tomcat.py index 8f8f7f300..a89a1e00d 100644 --- a/src/bci_build/package/apache_tomcat.py +++ b/src/bci_build/package/apache_tomcat.py @@ -73,12 +73,12 @@ def _get_sac_supported_until( and os_version.is_tumbleweed ), version="%%tomcat_version%%", - tag_version=f"{tomcat_ver}-openjdk{jre_version}", + branch_version=tomcat_ver, + build_flavor=f"openjdk{jre_version}", + additional_versions=[f"{tomcat_ver}-openjdk{jre_version}"], supported_until=_get_sac_supported_until( os_version=os_version, tomcat_ver=tomcat_ver, jre_major=jre_version ), - build_flavor=f"openjdk{jre_version}", - additional_versions=[f"%%tomcat_version%%-openjdk{jre_version}"], from_target_image=f"{_build_tag_prefix(os_version)}/bci-micro:{OsContainer.version_to_container_os_version(os_version)}", package_list=[ tomcat_pkg := ( diff --git a/src/bci_build/registry.py b/src/bci_build/registry.py index 04368399b..aab8430f7 100644 --- a/src/bci_build/registry.py +++ b/src/bci_build/registry.py @@ -6,6 +6,8 @@ from dataclasses import dataclass from typing import TYPE_CHECKING +import packaging + from bci_build.os_version import OsVersion if TYPE_CHECKING: @@ -44,6 +46,26 @@ def registry_prefix(*, is_application: bool) -> str: """ + @staticmethod + def build_version(base_version, container: "BaseContainerImage") -> str: + """Return the build version to set for this container build.""" + container_version: str = ( + container._variant if container.branch_version else container.tag_version + ) + # if container_version is a numeric version and not a macro, then + # version.parse() returns a `Version` object => then we concatenate + # it with the existing build_version + # for non PEP440 versions, we'll get an exception and just return + # the parent's classes build_version + try: + packaging.version.parse(str(container_version)) + stability_suffix: str = "" + if container._stability_suffix: + stability_suffix = "." + container._stability_suffix + return f"{base_version}.{container_version}{stability_suffix}" + except packaging.version.InvalidVersion: + return base_version + class ApplicationCollectionRegistry(Registry): """Registry for the Rancher Application Collection Distribution Platform.""" @@ -61,6 +83,10 @@ def url(container: "BaseContainerImage") -> str: def registry_prefix(*, is_application: bool) -> str: return "containers" + @staticmethod + def build_version(base_version, container: "BaseContainerImage") -> str: + return container.version + class SUSERegistry(Registry): """Registry for the SUSE Registry.""" diff --git a/tests/test_build_recipe.py b/tests/test_build_recipe.py index a3374ca79..c84ee08ad 100644 --- a/tests/test_build_recipe.py +++ b/tests/test_build_recipe.py @@ -3,6 +3,7 @@ import pytest from bci_build.os_version import OsVersion +from bci_build.package import ApplicationStackContainer from bci_build.package import Arch from bci_build.package import BuildType from bci_build.package import DevelopmentContainer @@ -10,6 +11,8 @@ from bci_build.package import Package from bci_build.package import PackageType from bci_build.package import SupportLevel +from bci_build.package import _build_tag_prefix +from bci_build.registry import publish_registry from bci_build.templates import DOCKERFILE_TEMPLATE from bci_build.templates import KIWI_TEMPLATE @@ -606,3 +609,80 @@ def test_build_recipe_templates( ) def test_os_build_recipe_templates(kiwi_xml: str, image: OsContainer) -> None: assert KIWI_TEMPLATE.render(image=image, INFOHEADER="Copyright header") == kiwi_xml + + +@pytest.mark.parametrize( + "dockerfile,image", + [ + ( + """# SPDX-License-Identifier: MIT + +# Copyright header + +#!UseOBSRepositories +#!ExclusiveArch: aarch64 x86_64 +#!BuildTag: containers/test:%%emacs_version%% +#!BuildTag: containers/test:%%emacs_version%%-%RELEASE% +#!ForceMultiVersion +#!BuildName: containers-test-42 +#!BuildVersion: %%emacs_version%% +#!BuildRelease: 35 +FROM registry.suse.com/bci/bci-micro:15.6 AS target +FROM bci/bci-base:15.6 AS builder +COPY --from=target / /target + +RUN \\ + zypper -n --installroot /target --gpg-auto-import-keys install --no-recommends emacs; \\ + zypper -n clean; \\ + ##LOGCLEAN## +FROM registry.suse.com/bci/bci-micro:15.6 +COPY --from=builder /target / +# Define labels according to https://en.opensuse.org/Building_derived_containers +# labelprefix=com.suse.application.test +LABEL org.opencontainers.image.title="Test" +LABEL org.opencontainers.image.description="Test container based on the SLE Base Container Image." +LABEL org.opencontainers.image.version="%%emacs_version%%" +LABEL org.opencontainers.image.url="https://apps.rancher.io/applications/test" +LABEL org.opencontainers.image.created="%BUILDTIME%" +LABEL org.opencontainers.image.vendor="SUSE LLC" +LABEL org.opencontainers.image.source="%SOURCEURL%" +LABEL org.opencontainers.image.ref.name="%%emacs_version%%-%RELEASE%" +LABEL org.opensuse.reference="dp.apps.rancher.io/containers/test:%%emacs_version%%-%RELEASE%" +LABEL org.openbuildservice.disturl="%DISTURL%" +LABEL com.suse.supportlevel="techpreview" +LABEL com.suse.supportlevel.until="2024-02-01" +LABEL com.suse.eula="sle-eula" +LABEL com.suse.lifecycle-url="https://www.suse.com/lifecycle#suse-linux-enterprise-server-15" +LABEL com.suse.release-stage="released" +# endlabelprefix +LABEL org.opencontainers.image.base.name="%BASE_REFNAME%" +LABEL org.opencontainers.image.base.digest="%BASE_DIGEST%" +LABEL io.artifacthub.package.readme-url="%SOURCEURL%/README.md" +""", + ApplicationStackContainer( + name="test", + pretty_name="Test", + supported_until=date(2024, 2, 1), + package_list=["emacs"], + package_name="test-image", + os_version=(os_version := OsVersion.SP6), + _publish_registry=publish_registry(os_version, app_collection=True), + from_target_image=f"{_build_tag_prefix(os_version)}/bci-micro:{OsContainer.version_to_container_os_version(os_version)}", + version="%%emacs_version%%", + branch_version=42, + ), + ) + ], +) +def test_appcollection_app_templates( + dockerfile: str, image: ApplicationStackContainer +) -> None: + assert ( + DOCKERFILE_TEMPLATE.render( + DOCKERFILE_RUN="RUN", + image=image, + INFOHEADER="# Copyright header", + LOG_CLEAN="##LOGCLEAN##", + ) + == dockerfile + )