From cee0025441e40ddc36eea5c5b1de4e6afde5110a Mon Sep 17 00:00:00 2001 From: aranega Date: Wed, 19 Aug 2026 04:03:11 -0600 Subject: [PATCH 01/14] CH-287 Add "source-images" and injection into sk buildargs --- .../cloudharness_utils/constants.py | 1 + .../ch_cli_tools/skaffold.py | 31 +++++--- .../values-template-nreg.yaml | 15 ++++ .../tests/test_skaffold.py | 74 +++++++++++++++++++ 4 files changed, 110 insertions(+), 11 deletions(-) create mode 100644 tools/deployment-cli-tools/tests/resources/deployment-configuration/values-template-nreg.yaml diff --git a/libraries/cloudharness-utils/cloudharness_utils/constants.py b/libraries/cloudharness-utils/cloudharness_utils/constants.py index b4540072c..fdb7b3c8f 100644 --- a/libraries/cloudharness-utils/cloudharness_utils/constants.py +++ b/libraries/cloudharness-utils/cloudharness_utils/constants.py @@ -31,6 +31,7 @@ VALUE_TEMPLATE_PATH = f'{DEPLOYMENT_CONFIGURATION_PATH}/value-template.yaml' CH_BASE_IMAGES = {'cloudharness-base': 'python:3.9.10'} +CH_BASE_IMAGE_CONVENTIONAL_KEY = "BASE_IMAGE" CD_BUILD_STEP_PARALLEL = 'build_application_images' diff --git a/tools/deployment-cli-tools/ch_cli_tools/skaffold.py b/tools/deployment-cli-tools/ch_cli_tools/skaffold.py index 2229bdc3f..55ec9c81c 100644 --- a/tools/deployment-cli-tools/ch_cli_tools/skaffold.py +++ b/tools/deployment-cli-tools/ch_cli_tools/skaffold.py @@ -7,7 +7,8 @@ from cloudharness_model import ApplicationTestConfig, HarnessMainConfig, GitDependencyConfig from cloudharness_utils.constants import APPS_PATH, DEPLOYMENT_CONFIGURATION_PATH, \ - BASE_IMAGES_PATH, STATIC_IMAGES_PATH, HELM_ENGINE, COMPOSE_ENGINE + BASE_IMAGES_PATH, STATIC_IMAGES_PATH, HELM_ENGINE, COMPOSE_ENGINE, \ + CH_BASE_IMAGE_CONVENTIONAL_KEY from .helm import KEY_APPS, KEY_HARNESS, KEY_DEPLOYMENT, KEY_TASK_IMAGES from .utils import get_template, dict_merge, find_dockerfiles_paths, app_name_from_path, yaml, \ find_file_paths, guess_build_dependencies_from_dockerfile, get_json_template, clean_image_name @@ -59,9 +60,9 @@ def resolve_dockerfile_name(dockerfile_dir_rel): def build_artifact( app_name: str, context_path: str, - requirements: list[str] = None, + requirements: list[str] | None = None, dockerfile_path: str = '', - additional_build_args: dict[str, str] = None, + additional_build_args: dict[str, str] | None = None, ) -> dict: build_args = { 'DEBUG': 'true' if helm_values.local or helm_values.debug else '' @@ -107,12 +108,14 @@ def process_build_dockerfile( builds[app_name] = context_path base_images.add(clean_image_name(app_name)) + additional_build_args = get_additional_build_args(helm_values, app_key) | get_image_source(helm_values, app_key) + artifacts[app_name] = build_artifact( app_name, context_path, dockerfile_path=relpath(dockerfile_path, output_path), requirements=requirements or guess_build_dependencies_from_dockerfile(dockerfile_path), - additional_build_args=get_additional_build_args(helm_values, app_key), + additional_build_args=additional_build_args, ) if app_key in helm_values.apps and helm_values.apps[app_key].harness.dependencies and helm_values.apps[app_key].harness.dependencies.git: @@ -328,10 +331,16 @@ def get_image_tag(name): def get_additional_build_args(helm_values: HarnessMainConfig, app_key: str) -> dict[str, str]: - if app_key not in helm_values.apps: - return None - - if not (helm_values.apps[app_key].harness.dockerfile and helm_values.apps[app_key].harness.dockerfile.build_args): - return None - - return helm_values.apps[app_key].harness.dockerfile.build_args + try: + return helm_values.apps[app_key].harness.dockerfile.build_args + except (KeyError, AttributeError): + return {} + + +def get_image_source(helm_values: HarnessMainConfig, app_key: str) -> dict[str, str]: + source_image = helm_values.get("source-images", {}).get(app_key) + if isinstance(source_image, dict): + return source_image + elif source_image: + return {CH_BASE_IMAGE_CONVENTIONAL_KEY: source_image} + return {} diff --git a/tools/deployment-cli-tools/tests/resources/deployment-configuration/values-template-nreg.yaml b/tools/deployment-cli-tools/tests/resources/deployment-configuration/values-template-nreg.yaml new file mode 100644 index 000000000..673826dfe --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources/deployment-configuration/values-template-nreg.yaml @@ -0,0 +1,15 @@ +mainapp: myapp +domain: myapp.local +name: testprojectname +a: b +database: + auto: true + +source-images: + myapp: "myregistry.myapp:15.3" + samples: + BASEIMAGE: myother.image:14 + +events: + kafka: + image: nodocker.io/apache/kafka:4.0.2 diff --git a/tools/deployment-cli-tools/tests/test_skaffold.py b/tools/deployment-cli-tools/tests/test_skaffold.py index e5fdafbc8..f6ca34829 100644 --- a/tools/deployment-cli-tools/tests/test_skaffold.py +++ b/tools/deployment-cli-tools/tests/test_skaffold.py @@ -363,3 +363,77 @@ def test_skaffold_builds_cross_app_task_image(tmp_path): shutil.rmtree(tmp_path) shutil.rmtree(BUILD_DIR) + + +def test_skaffold_imgarg_retrieval(tmp_path): + out_folder = tmp_path / "test_skaffold_imgarg_retrieval" + + values = create_helm_chart( + [CLOUDHARNESS_ROOT, RESOURCES], + output_path=out_folder, + include=["samples", "myapp"], + domain="my.local", + namespace="test", + env="nreg", + local=False, + tag=1, + registry="reg", + ) + + assert values.get("events").kafka.image == "nodocker.io/apache/kafka:4.0.2" + + # Ensure in the test that the Helm is well formed + source_images = values.get("source-images") + assert len(source_images) == 2 + assert source_images["myapp"] == "myregistry.myapp:15.3" + assert source_images["samples"] == {"BASEIMAGE": "myother.image:14"} + + assert get_image_source(values, "myapp") == {CH_BASE_IMAGE_CONVENTIONAL_KEY: "myregistry.myapp:15.3"} + assert get_image_source(values, "samples") == {"BASEIMAGE": "myother.image:14"} + assert get_image_source(values, "events") == {} + + +def test_skaffold_imgarg(tmp_path): + out_folder = tmp_path / "test_skaffold_imgarg" + + values = create_helm_chart( + [CLOUDHARNESS_ROOT, RESOURCES], + output_path=out_folder, + include=["samples", "myapp"], + domain="my.local", + namespace="test", + env="nreg", + local=False, + tag=1, + registry="reg", + ) + + assert values.get("events").kafka.image == "nodocker.io/apache/kafka:4.0.2" + + BUILD_DIR = "/tmp/build" + root_paths = preprocess_build_overrides( + root_paths=[CLOUDHARNESS_ROOT, RESOURCES], + helm_values=values, + merge_build_path=BUILD_DIR, + ) + + sk = create_skaffold_configuration( + root_paths=root_paths, helm_values=values, output_path=out_folder + ) + + # Look in sk + sk.get("build").get("artifacts") + + def get_buildargs(name) -> dict[str, str]: + f = [e["docker"]["buildArgs"] for e in sk["build"]["artifacts"] if f"applications/{name}" in e["context"]] + if len(f) > 0: + return f[0] + return {} + + samples_buildargs = get_buildargs("samples") + assert "BASEIMAGE" in samples_buildargs + assert samples_buildargs["BASEIMAGE"] == "myother.image:14" + + myapp_buildargs = get_buildargs("myapp") + assert CH_BASE_IMAGE_CONVENTIONAL_KEY in myapp_buildargs + assert myapp_buildargs[CH_BASE_IMAGE_CONVENTIONAL_KEY] == "myregistry.myapp:15.3" From 5f8b3fe7c402e31ab580a9a15a1960d1d058f7ed Mon Sep 17 00:00:00 2001 From: aranega Date: Wed, 19 Aug 2026 04:37:11 -0600 Subject: [PATCH 02/14] CH-287 Add baseimg arg to the dockerfile which are missing it --- applications/accounts/Dockerfile | 3 ++- applications/nfsserver/Dockerfile | 6 ++++-- applications/sentry/Dockerfile | 3 ++- infrastructure/base-images/cloudharness-base/Dockerfile | 4 ++-- .../base-images/cloudharness-frontend-build/Dockerfile | 5 ++--- .../cloudharness-utils/cloudharness_utils/constants.py | 2 +- test/test-e2e/Dockerfile | 9 +++++---- 7 files changed, 18 insertions(+), 14 deletions(-) diff --git a/applications/accounts/Dockerfile b/applications/accounts/Dockerfile index ff1a1b736..66366c583 100644 --- a/applications/accounts/Dockerfile +++ b/applications/accounts/Dockerfile @@ -1,4 +1,5 @@ -FROM quay.io/keycloak/keycloak:26.5 +ARG CLOUDHARNESS_BASE=quay.io/keycloak/keycloak:26.5 +FROM $CLOUDHARNESS_BASE EXPOSE 9000 EXPOSE 8080 diff --git a/applications/nfsserver/Dockerfile b/applications/nfsserver/Dockerfile index e772f79d5..f0e63399d 100644 --- a/applications/nfsserver/Dockerfile +++ b/applications/nfsserver/Dockerfile @@ -1,5 +1,7 @@ # compile provisioner -FROM golang:1.26.2 AS provisioner +ARG CLOUDHARNESS_BASE_GOLANG=golang:1.26.2 +ARG CLOUDHARNESS_BASE_ROCKYLINUX=golang:rockylinux/rockylinux:10.1-minimal +FROM $CLOUDHARNESS_BASE_GOLANG AS provisioner WORKDIR /usr/src/app COPY nfs-subdir-external-provisioner ./ @@ -17,7 +19,7 @@ RUN go mod tidy && go build -o /usr/local/bin/nfsvol . # Upstream continuation of the old k8s.gcr.io/volume-nfs:0.8 image, maintained # by the Kubernetes project at test/images/volume/nfs. CentOS Stream 9 base # with nfs-utils already installed; we only need to add e2fsprogs for mkfs.ext4. -FROM rockylinux/rockylinux:10.1-minimal +FROM $CLOUDHARNESS_BASE_ROCKYLINUX RUN microdnf install -y \ e2fsprogs \ diff --git a/applications/sentry/Dockerfile b/applications/sentry/Dockerfile index 2607f5172..fe19146c5 100644 --- a/applications/sentry/Dockerfile +++ b/applications/sentry/Dockerfile @@ -1,4 +1,5 @@ -FROM sentry:9.1.2 +ARG CLOUDHARNESS_BASE=sentry:9.1.2 +FROM $CLOUDHARNESS_BASE ADD sentryrunner.sh ./sentryrunner.sh RUN chmod +x ./sentryrunner.sh diff --git a/infrastructure/base-images/cloudharness-base/Dockerfile b/infrastructure/base-images/cloudharness-base/Dockerfile index 58cb04bae..f6bcc8d79 100644 --- a/infrastructure/base-images/cloudharness-base/Dockerfile +++ b/infrastructure/base-images/cloudharness-base/Dockerfile @@ -1,5 +1,5 @@ -ARG PARENT=python:3.12-slim-trixie -FROM ${PARENT} +ARG CLOUDHARNESS_BASE=python:3.12-slim-trixie +FROM $CLOUDHARNESS_BASE RUN apt update && apt install -y --no-install-recommends \ gcc \ diff --git a/infrastructure/base-images/cloudharness-frontend-build/Dockerfile b/infrastructure/base-images/cloudharness-frontend-build/Dockerfile index 0a4d02061..cfd434fd9 100644 --- a/infrastructure/base-images/cloudharness-frontend-build/Dockerfile +++ b/infrastructure/base-images/cloudharness-frontend-build/Dockerfile @@ -1,3 +1,2 @@ -FROM node:22-alpine - - +ARG CLOUDHARNESS_BASE=node:22-alpine +FROM $CLOUDHARNESS_BASE diff --git a/libraries/cloudharness-utils/cloudharness_utils/constants.py b/libraries/cloudharness-utils/cloudharness_utils/constants.py index fdb7b3c8f..7952097cf 100644 --- a/libraries/cloudharness-utils/cloudharness_utils/constants.py +++ b/libraries/cloudharness-utils/cloudharness_utils/constants.py @@ -31,7 +31,7 @@ VALUE_TEMPLATE_PATH = f'{DEPLOYMENT_CONFIGURATION_PATH}/value-template.yaml' CH_BASE_IMAGES = {'cloudharness-base': 'python:3.9.10'} -CH_BASE_IMAGE_CONVENTIONAL_KEY = "BASE_IMAGE" +CH_BASE_IMAGE_CONVENTIONAL_KEY = "CLOUDHARNESS_BASE" CD_BUILD_STEP_PARALLEL = 'build_application_images' diff --git a/test/test-e2e/Dockerfile b/test/test-e2e/Dockerfile index 7f0ca0215..46c9fa642 100644 --- a/test/test-e2e/Dockerfile +++ b/test/test-e2e/Dockerfile @@ -1,18 +1,19 @@ -FROM ghcr.io/puppeteer/puppeteer:24 +ARG CLOUDHARNESS_BASE=ghcr.io/puppeteer/puppeteer:24 +FROM $CLOUDHARNESS_BASE USER root RUN mkdir -p /home/test RUN chown -R pptruser /home/test WORKDIR /home/test # Install Chrome for the root user: Codefresh runs the container as root -RUN npx puppeteer browsers install chrome +RUN npx puppeteer browsers install chrome USER pptruser COPY --chown=pptruser package.json . COPY --chown=pptruser yarn.lock . RUN yarn install --timeout 99999999 -COPY --chown=pptruser . . +COPY --chown=pptruser . . ENV APP_URL=https://github.com -CMD ["yarn", "test"] \ No newline at end of file +CMD ["yarn", "test"] From c8c118585fd4d65abc69f7bb95c3e6047c69e2f7 Mon Sep 17 00:00:00 2001 From: aranega Date: Wed, 19 Aug 2026 06:06:58 -0600 Subject: [PATCH 03/14] CH-287 Add documentation for the new feature --- applications/accounts/Dockerfile | 4 +- applications/sentry/Dockerfile | 4 +- docs/base-common-images.md | 75 +++++++++++++++++++ .../base-images/cloudharness-base/Dockerfile | 4 +- .../cloudharness-frontend-build/Dockerfile | 4 +- .../cloudharness_utils/constants.py | 2 +- test/test-e2e/Dockerfile | 4 +- 7 files changed, 86 insertions(+), 11 deletions(-) diff --git a/applications/accounts/Dockerfile b/applications/accounts/Dockerfile index 66366c583..2f14718f2 100644 --- a/applications/accounts/Dockerfile +++ b/applications/accounts/Dockerfile @@ -1,5 +1,5 @@ -ARG CLOUDHARNESS_BASE=quay.io/keycloak/keycloak:26.5 -FROM $CLOUDHARNESS_BASE +ARG CLOUDHARNESS_ROOT_BASE=quay.io/keycloak/keycloak:26.5 +FROM $CLOUDHARNESS_ROOT_BASE EXPOSE 9000 EXPOSE 8080 diff --git a/applications/sentry/Dockerfile b/applications/sentry/Dockerfile index fe19146c5..99b310b3b 100644 --- a/applications/sentry/Dockerfile +++ b/applications/sentry/Dockerfile @@ -1,5 +1,5 @@ -ARG CLOUDHARNESS_BASE=sentry:9.1.2 -FROM $CLOUDHARNESS_BASE +ARG CLOUDHARNESS_ROOT_BASE=sentry:9.1.2 +FROM $CLOUDHARNESS_ROOT_BASE ADD sentryrunner.sh ./sentryrunner.sh RUN chmod +x ./sentryrunner.sh diff --git a/docs/base-common-images.md b/docs/base-common-images.md index 6d0ac6bf0..2277e9a55 100644 --- a/docs/base-common-images.md +++ b/docs/base-common-images.md @@ -60,3 +60,78 @@ libraries or to provide new libraries to share within all applications. To override cloudharness-base, create a directory `MY_SOLUTION/infrastructure/base-images/cloudharness-base` then run `harness-deployment cloudharness MY_SOLUTION` + + +## Change the base image of any application (`FROM [xxx]`) + +It's possible to change the image each dockerfile inherits by using the `FROM` directive. +Depending on the kind of application, changing the base image is done in two different ways. +There is basically two main situations: + +. your application has a `Dockerfile` (e.g: Keycloak, the app you're building); +. your application doesn't have a `Dockerfile` (e.g: gogatekeeper, Kafka, etc), and the image is directly injected inside the helm chart when the helm chart is generated. + +In both situations, the modification is done in the `value-template.yaml` of your CloudHarness projects, but the YAML path to modify depends on the app your targetting then. + +### Change the base image for applications with a `Dockerfile` + +Changing the base image for your application or applications which have a `Dockerfile` is done through the `source-images` entry of your `value-template.yaml` file. +This entry defines a mapping between the `ARG` of your `Dockerfile` and the value you want to inject. +Here is an example of a declared mapping for two applications: + +```yaml +# value-template.yaml +image-sources: + myapp: "mybaseimg:14.5" + samples: + CLOUDHARNESS_FRONTEND_BUILD: "myfrontendbaseimg:15.6" +``` + +The first entry for `myapp` will change the base image for `mybaseimg:14.5`. +As nothing is given as detail for the base image key inside the `Dockerfile`, by convention, CloudHarness is going to set `CLOUDHARNESS_ROOT_BASE` to `mybaseimg:14.5`. +If your `Dockerfile` doesn't use the `CLOUDHARNESS_ROOT_BASE` `ARG` as variable for your `FROM ...` directive, you can precise the name of the variable which needs to be changed by declaring it and the value it needs to have as a YAML dictionnary. +The second entry is in this situation, the base `ARG` which is used in the `Dockerfile` for the `FROM ...` directive is `CLOUDHARNESS_FRONTEND_BUILD`. + +### Change the base image for applications which inject their image in the helm chart + +Those applications are not providing a Dockerfile, but directly an image which is injcted in the helm chart from the helm template. +Each of those applications considers a specific path from the helm configuration (i.e: `values.yaml` or `value-template.yaml`). +To change the base image for those, you need to know this path and change it in the `value-template.yaml` by redefining it. +The redefinition will override the base value from the sub-`values.yaml` files on configuration merging. +Here is an example for the gatekeeper and the Argo controller: + +```yaml +# value-template.yaml +harness: + proxy: + gatekeeper: + image: YOUR_NEW_IMG + +# or, depending on your application +argo: + controller: + image: + registry: YOUR_REG + tag: YOUR TAG +``` + +Here is a table of the identified applications inside of CloudHarness which do not own a `Dockerfile`. + +| Application | Image path | +| --- | --- | +| gatekeeper | harness.proxy.gatekeeper.image | +| Argo controller | argo.controller.image.{repository, tag} | +| Argo executor | argo.executor.image.{repository, tag} | +| Argo server | argo.server.image.{repository, tag} | +| Elasticsearch | elasticsearch.image | +| Events | events.image.{repository, tag, nullPolicy} | +| Events kafka | events.kafka.image | +| JupyterHub | jupyterhub.singleuser.image.{pullSecrets, name, tag} | +| JupyterHub proxy | jupyterhub.proxy.chp.image.{name, tag, pullPolicy} | +| JupyterHub scheduling | jupyterhub.scheduling.userPlaceHolder.image.{name, pullPolicy, tag} | +| NFS server | nfsserver.harness.deployment.image | +| Sentry redis | sentry.redis.image | +| MongoDB | harness.database.mongo.image | +| Neo4J | harness.database.neo4j.image | +| Neo4J reverseProxy | neo4j.reverseProxy.image | +| Postgres | harness.database.postgres.image | diff --git a/infrastructure/base-images/cloudharness-base/Dockerfile b/infrastructure/base-images/cloudharness-base/Dockerfile index f6bcc8d79..95440d548 100644 --- a/infrastructure/base-images/cloudharness-base/Dockerfile +++ b/infrastructure/base-images/cloudharness-base/Dockerfile @@ -1,5 +1,5 @@ -ARG CLOUDHARNESS_BASE=python:3.12-slim-trixie -FROM $CLOUDHARNESS_BASE +ARG CLOUDHARNESS_ROOT_BASE=python:3.12-slim-trixie +FROM $CLOUDHARNESS_ROOT_BASE RUN apt update && apt install -y --no-install-recommends \ gcc \ diff --git a/infrastructure/base-images/cloudharness-frontend-build/Dockerfile b/infrastructure/base-images/cloudharness-frontend-build/Dockerfile index cfd434fd9..f7bdbdbca 100644 --- a/infrastructure/base-images/cloudharness-frontend-build/Dockerfile +++ b/infrastructure/base-images/cloudharness-frontend-build/Dockerfile @@ -1,2 +1,2 @@ -ARG CLOUDHARNESS_BASE=node:22-alpine -FROM $CLOUDHARNESS_BASE +ARG CLOUDHARNESS_ROOT_BASE=node:22-alpine +FROM $CLOUDHARNESS_ROOT_BASE diff --git a/libraries/cloudharness-utils/cloudharness_utils/constants.py b/libraries/cloudharness-utils/cloudharness_utils/constants.py index 7952097cf..943185af0 100644 --- a/libraries/cloudharness-utils/cloudharness_utils/constants.py +++ b/libraries/cloudharness-utils/cloudharness_utils/constants.py @@ -31,7 +31,7 @@ VALUE_TEMPLATE_PATH = f'{DEPLOYMENT_CONFIGURATION_PATH}/value-template.yaml' CH_BASE_IMAGES = {'cloudharness-base': 'python:3.9.10'} -CH_BASE_IMAGE_CONVENTIONAL_KEY = "CLOUDHARNESS_BASE" +CH_BASE_IMAGE_CONVENTIONAL_KEY = "CLOUDHARNESS_ROOT_BASE" CD_BUILD_STEP_PARALLEL = 'build_application_images' diff --git a/test/test-e2e/Dockerfile b/test/test-e2e/Dockerfile index 46c9fa642..1c1a09bc2 100644 --- a/test/test-e2e/Dockerfile +++ b/test/test-e2e/Dockerfile @@ -1,5 +1,5 @@ -ARG CLOUDHARNESS_BASE=ghcr.io/puppeteer/puppeteer:24 -FROM $CLOUDHARNESS_BASE +ARG CLOUDHARNESS_ROOT_BASE=ghcr.io/puppeteer/puppeteer:24 +FROM $CLOUDHARNESS_ROOT_BASE USER root RUN mkdir -p /home/test From ebb64d8e4bae81715a9cfed65ce0bec6bb9939a1 Mon Sep 17 00:00:00 2001 From: aranega Date: Tue, 1 Sep 2026 03:39:00 -0600 Subject: [PATCH 04/14] CH-287 Update base name for dockerfile images --- applications/accounts/Dockerfile | 4 ++-- applications/nfsserver/Dockerfile | 8 ++++---- applications/sentry/Dockerfile | 4 ++-- infrastructure/base-images/cloudharness-base/Dockerfile | 4 ++-- .../base-images/cloudharness-frontend-build/Dockerfile | 4 ++-- test/test-e2e/Dockerfile | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/applications/accounts/Dockerfile b/applications/accounts/Dockerfile index 2f14718f2..bed3f0fda 100644 --- a/applications/accounts/Dockerfile +++ b/applications/accounts/Dockerfile @@ -1,5 +1,5 @@ -ARG CLOUDHARNESS_ROOT_BASE=quay.io/keycloak/keycloak:26.5 -FROM $CLOUDHARNESS_ROOT_BASE +ARG KEYCLOAK=quay.io/keycloak/keycloak:26.5 +FROM $KEYCLOAK EXPOSE 9000 EXPOSE 8080 diff --git a/applications/nfsserver/Dockerfile b/applications/nfsserver/Dockerfile index 1b7f590bd..0f3299b9c 100644 --- a/applications/nfsserver/Dockerfile +++ b/applications/nfsserver/Dockerfile @@ -1,7 +1,7 @@ # compile provisioner -ARG CLOUDHARNESS_BASE_GOLANG=golang:1.26 -ARG CLOUDHARNESS_BASE_ROCKYLINUX=golang:rockylinux/rockylinux:10.1-minimal -FROM $CLOUDHARNESS_BASE_GOLANG AS provisioner +ARG GOLANG=golang:1.26 +ARG ROCKYLINUX=golang:rockylinux/rockylinux:10.1-minimal +FROM $GOLANG AS provisioner WORKDIR /usr/src/app COPY nfs-subdir-external-provisioner ./ @@ -19,7 +19,7 @@ RUN go mod tidy && go build -o /usr/local/bin/nfsvol . # Upstream continuation of the old k8s.gcr.io/volume-nfs:0.8 image, maintained # by the Kubernetes project at test/images/volume/nfs. CentOS Stream 9 base # with nfs-utils already installed; we only need to add e2fsprogs for mkfs.ext4. -FROM $CLOUDHARNESS_BASE_ROCKYLINUX +FROM $ROCKYLINUX RUN microdnf install -y \ e2fsprogs \ diff --git a/applications/sentry/Dockerfile b/applications/sentry/Dockerfile index 99b310b3b..e2845f312 100644 --- a/applications/sentry/Dockerfile +++ b/applications/sentry/Dockerfile @@ -1,5 +1,5 @@ -ARG CLOUDHARNESS_ROOT_BASE=sentry:9.1.2 -FROM $CLOUDHARNESS_ROOT_BASE +ARG SENTRY=sentry:9.1.2 +FROM $SENTRY ADD sentryrunner.sh ./sentryrunner.sh RUN chmod +x ./sentryrunner.sh diff --git a/infrastructure/base-images/cloudharness-base/Dockerfile b/infrastructure/base-images/cloudharness-base/Dockerfile index 95440d548..159fc7c31 100644 --- a/infrastructure/base-images/cloudharness-base/Dockerfile +++ b/infrastructure/base-images/cloudharness-base/Dockerfile @@ -1,5 +1,5 @@ -ARG CLOUDHARNESS_ROOT_BASE=python:3.12-slim-trixie -FROM $CLOUDHARNESS_ROOT_BASE +ARG PYTHON=python:3.12-slim-trixie +FROM $PYTHON RUN apt update && apt install -y --no-install-recommends \ gcc \ diff --git a/infrastructure/base-images/cloudharness-frontend-build/Dockerfile b/infrastructure/base-images/cloudharness-frontend-build/Dockerfile index f7bdbdbca..0497d3cfd 100644 --- a/infrastructure/base-images/cloudharness-frontend-build/Dockerfile +++ b/infrastructure/base-images/cloudharness-frontend-build/Dockerfile @@ -1,2 +1,2 @@ -ARG CLOUDHARNESS_ROOT_BASE=node:22-alpine -FROM $CLOUDHARNESS_ROOT_BASE +ARG NODE=node:22-alpine +FROM $NODE diff --git a/test/test-e2e/Dockerfile b/test/test-e2e/Dockerfile index 1c1a09bc2..31e987e4b 100644 --- a/test/test-e2e/Dockerfile +++ b/test/test-e2e/Dockerfile @@ -1,5 +1,5 @@ -ARG CLOUDHARNESS_ROOT_BASE=ghcr.io/puppeteer/puppeteer:24 -FROM $CLOUDHARNESS_ROOT_BASE +ARG PUPPETEER=ghcr.io/puppeteer/puppeteer:24 +FROM $PUPPETEER USER root RUN mkdir -p /home/test From 90dbfbde0b295e3844d2a22c71af905d0154b331 Mon Sep 17 00:00:00 2001 From: aranega Date: Tue, 1 Sep 2026 05:24:39 -0600 Subject: [PATCH 05/14] CH-287 Add function to parse the ARG/FROM from Dockerfiles --- .../ch_cli_tools/utils.py | 34 ++++++++++++ .../resources/applications/newapp1/Dockerfile | 7 +++ .../deployment-cli-tools/tests/test_utils.py | 54 +++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 tools/deployment-cli-tools/tests/resources/applications/newapp1/Dockerfile diff --git a/tools/deployment-cli-tools/ch_cli_tools/utils.py b/tools/deployment-cli-tools/ch_cli_tools/utils.py index a3deb37dd..0f8b85bb2 100644 --- a/tools/deployment-cli-tools/ch_cli_tools/utils.py +++ b/tools/deployment-cli-tools/ch_cli_tools/utils.py @@ -17,6 +17,7 @@ import logging import fileinput import pathspec +from pathlib import Path from cloudharness_utils.constants import NEUTRAL_PATHS, DEPLOYMENT_CONFIGURATION_PATH, BASE_IMAGES_PATH, STATIC_IMAGES_PATH, \ APPS_PATH, EXCLUDE_PATHS @@ -557,6 +558,39 @@ def guess_build_dependencies_from_dockerfile(filename): return dependencies +@cache +def get_dockerfile_baseimg_args(filename: str) -> dict[str, str]: + """Gets the ARGS from a Dockerfile image (if ARGS is used directly in the FROM of the Dockerfile)""" + file = Path(filename) + if file.is_dir() and file.name != "Dockerfile": + file /= "Dockerfile" + if not file.exists(): + return {} + content = file.read_text() + found_args = {} + args: dict[str, str] = {} + for line in content.splitlines(): + line = line.strip() + if not line: + continue + cmd, *rest = line.split() + arg = rest[0] if len(rest) > 0 else "" + match cmd: + case "ARG" if "=" in rest[0]: + key, val = arg.split("=") + found_args[key] = val + case "FROM" if arg[1:] in found_args: # $NAME case + key = arg[1:] + args[key] = found_args[key] + case "FROM" if arg[2:-1] in found_args: # ${NAME} case + key = arg[2:-1] + args[key] = found_args[key] + case _: + continue + return args + + + def check_response_200(endpoint_url, headers=None): resp = requests.get(endpoint_url, headers=headers, timeout=5) return resp.status_code == 200 diff --git a/tools/deployment-cli-tools/tests/resources/applications/newapp1/Dockerfile b/tools/deployment-cli-tools/tests/resources/applications/newapp1/Dockerfile new file mode 100644 index 000000000..430feb2ed --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources/applications/newapp1/Dockerfile @@ -0,0 +1,7 @@ +# Dockerfile with ARG/FROM and an ARG not used by a FROM +ARG mybase=foo:bar +ARG mybase2=spam:egg +ARG mybase3=not:referenced + +FROM ${mybase} +FROM $mybase2 diff --git a/tools/deployment-cli-tools/tests/test_utils.py b/tools/deployment-cli-tools/tests/test_utils.py index 885c7cae8..8a788db6c 100644 --- a/tools/deployment-cli-tools/tests/test_utils.py +++ b/tools/deployment-cli-tools/tests/test_utils.py @@ -5,6 +5,8 @@ from ch_cli_tools.utils import * +from pathlib import Path + HERE = os.path.dirname(os.path.realpath(__file__)).replace(os.path.sep, '/') @@ -191,3 +193,55 @@ def test_replaces_in_values_within_nested_dict(_): 'c': 'xxxbar', 'e': ['xxx', 'bar', 'xxxbar'] } + + +def test__get_dockerfile_baseimg_args__no_file(): + resources = Path(HERE) / "resources" + + # no file + args = get_dockerfile_baseimg_args(resources / "doesnotexist" / "Dockerfile") + assert args == {} + + # no file from folder + args = get_dockerfile_baseimg_args(resources / "doesnotexist") + assert args == {} + + +def test__get_dockerfile_baseimg_args__no_dep(): + resources = Path(HERE) / "resources" + + # no real base dependance + nodep_dockerfile = resources / "applications" / "dependantapp" / "Dockerfile" + + args = get_dockerfile_baseimg_args(nodep_dockerfile) + assert args == {} + + # no real base dependance from folder + nodep_dockerfile = resources / "applications" / "dependantapp" + + args = get_dockerfile_baseimg_args(nodep_dockerfile) + assert args == {} + + +def test__get_dockerfile_baseimg_args__with_deps(): + resources = Path(HERE) / "resources" + + # with deps + dockerfile = resources / "applications" / "newapp1" / "Dockerfile" + + args = get_dockerfile_baseimg_args(dockerfile) + assert "mybase" in args + assert args["mybase"] == "foo:bar" + assert "mybase2" in args + assert args["mybase2"] == "spam:egg" + assert "mybase3" not in args + + # with deps from folder + dockerfile = resources / "applications" / "newapp1" + + args = get_dockerfile_baseimg_args(dockerfile) + assert "mybase" in args + assert args["mybase"] == "foo:bar" + assert "mybase2" in args + assert args["mybase2"] == "spam:egg" + assert "mybase3" not in args From dcb4249e6b9448da098d4d9061636e741ba95694 Mon Sep 17 00:00:00 2001 From: aranega Date: Tue, 1 Sep 2026 05:28:30 -0600 Subject: [PATCH 06/14] CH-287 Switch "source-images" for "sources_images" --- docs/base-common-images.md | 2 +- tools/deployment-cli-tools/ch_cli_tools/skaffold.py | 2 +- .../deployment-configuration/values-template-nreg.yaml | 2 +- tools/deployment-cli-tools/tests/test_skaffold.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/base-common-images.md b/docs/base-common-images.md index 2277e9a55..e1db0fb4e 100644 --- a/docs/base-common-images.md +++ b/docs/base-common-images.md @@ -75,7 +75,7 @@ In both situations, the modification is done in the `value-template.yaml` of you ### Change the base image for applications with a `Dockerfile` -Changing the base image for your application or applications which have a `Dockerfile` is done through the `source-images` entry of your `value-template.yaml` file. +Changing the base image for your application or applications which have a `Dockerfile` is done through the `source_images` entry of your `value-template.yaml` file. This entry defines a mapping between the `ARG` of your `Dockerfile` and the value you want to inject. Here is an example of a declared mapping for two applications: diff --git a/tools/deployment-cli-tools/ch_cli_tools/skaffold.py b/tools/deployment-cli-tools/ch_cli_tools/skaffold.py index 55ec9c81c..2c71fa5c8 100644 --- a/tools/deployment-cli-tools/ch_cli_tools/skaffold.py +++ b/tools/deployment-cli-tools/ch_cli_tools/skaffold.py @@ -338,7 +338,7 @@ def get_additional_build_args(helm_values: HarnessMainConfig, app_key: str) -> d def get_image_source(helm_values: HarnessMainConfig, app_key: str) -> dict[str, str]: - source_image = helm_values.get("source-images", {}).get(app_key) + source_image = helm_values.get("source_images", {}).get(app_key) if isinstance(source_image, dict): return source_image elif source_image: diff --git a/tools/deployment-cli-tools/tests/resources/deployment-configuration/values-template-nreg.yaml b/tools/deployment-cli-tools/tests/resources/deployment-configuration/values-template-nreg.yaml index 673826dfe..157ed8d9e 100644 --- a/tools/deployment-cli-tools/tests/resources/deployment-configuration/values-template-nreg.yaml +++ b/tools/deployment-cli-tools/tests/resources/deployment-configuration/values-template-nreg.yaml @@ -5,7 +5,7 @@ a: b database: auto: true -source-images: +source_images: myapp: "myregistry.myapp:15.3" samples: BASEIMAGE: myother.image:14 diff --git a/tools/deployment-cli-tools/tests/test_skaffold.py b/tools/deployment-cli-tools/tests/test_skaffold.py index f6ca34829..af582ccda 100644 --- a/tools/deployment-cli-tools/tests/test_skaffold.py +++ b/tools/deployment-cli-tools/tests/test_skaffold.py @@ -383,7 +383,7 @@ def test_skaffold_imgarg_retrieval(tmp_path): assert values.get("events").kafka.image == "nodocker.io/apache/kafka:4.0.2" # Ensure in the test that the Helm is well formed - source_images = values.get("source-images") + source_images = values.get("source_images") assert len(source_images) == 2 assert source_images["myapp"] == "myregistry.myapp:15.3" assert source_images["samples"] == {"BASEIMAGE": "myother.image:14"} From c7b038370e4212af7f1a13042d0134f96e331883 Mon Sep 17 00:00:00 2001 From: aranega Date: Tue, 1 Sep 2026 08:58:25 -0600 Subject: [PATCH 07/14] CH-287 Add arg/from prepopulation in values.yaml --- .../cloudharness_utils/constants.py | 1 - .../ch_cli_tools/configurationgenerator.py | 40 ++++----- .../ch_cli_tools/dockercompose.py | 10 ++- .../deployment-cli-tools/ch_cli_tools/helm.py | 87 +++++++++++++------ .../ch_cli_tools/skaffold.py | 14 +-- .../ch_cli_tools/utils.py | 6 +- .../values-template-nreg.yaml | 4 +- .../tests/test_skaffold.py | 24 ++--- 8 files changed, 106 insertions(+), 80 deletions(-) diff --git a/libraries/cloudharness-utils/cloudharness_utils/constants.py b/libraries/cloudharness-utils/cloudharness_utils/constants.py index 943185af0..b4540072c 100644 --- a/libraries/cloudharness-utils/cloudharness_utils/constants.py +++ b/libraries/cloudharness-utils/cloudharness_utils/constants.py @@ -31,7 +31,6 @@ VALUE_TEMPLATE_PATH = f'{DEPLOYMENT_CONFIGURATION_PATH}/value-template.yaml' CH_BASE_IMAGES = {'cloudharness-base': 'python:3.9.10'} -CH_BASE_IMAGE_CONVENTIONAL_KEY = "CLOUDHARNESS_ROOT_BASE" CD_BUILD_STEP_PARALLEL = 'build_application_images' diff --git a/tools/deployment-cli-tools/ch_cli_tools/configurationgenerator.py b/tools/deployment-cli-tools/ch_cli_tools/configurationgenerator.py index b8193b3d9..1e50fe6bb 100644 --- a/tools/deployment-cli-tools/ch_cli_tools/configurationgenerator.py +++ b/tools/deployment-cli-tools/ch_cli_tools/configurationgenerator.py @@ -16,7 +16,7 @@ from . import HERE, CH_ROOT from cloudharness_utils.constants import TEST_IMAGES_PATH, HELM_CHART_PATH, APPS_PATH, HELM_PATH, \ DEPLOYMENT_CONFIGURATION_PATH, BASE_IMAGES_PATH, STATIC_IMAGES_PATH -from .utils import get_cluster_ip, env_variable, get_sub_paths, guess_build_dependencies_from_dockerfile, image_name_from_dockerfile_path, \ +from .utils import get_cluster_ip, env_variable, get_dockerfile_baseimg_args, get_sub_paths, guess_build_dependencies_from_dockerfile, image_name_from_dockerfile_path, \ get_template, merge_configuration_directories, dict_merge, app_name_from_path, \ find_dockerfiles_paths, get_git_commit_hash from .secrets import secret_definition_error @@ -43,7 +43,7 @@ def __init__(self, root_paths: List[str], tag: Union[str, int, None] = 'latest', self.root_paths = [Path(r) for r in root_paths] self.tag = str(tag) if tag else None if registry and not registry.endswith('/'): - self.registry = f'{registry}/' + self.registry: str = f'{registry}/' else: self.registry = registry self.local = local @@ -67,11 +67,11 @@ def __init__(self, root_paths: List[str], tag: Union[str, int, None] = 'latest', self.__init_deployment() self.static_images = set() - self.base_images = {} + self.base_images: dict[str, tuple[str, dict[str, str]]] = {} self.all_images = {} @abc.abstractmethod - def create_app_values_spec(self, app_name, app_path, base_image_name=None, helm_values={}): + def create_app_values_spec(self, app_name: str, app_path: Path, base_image_name: str | None=None, helm_values: dict | None=None) -> dict: ... @abc.abstractmethod @@ -173,7 +173,7 @@ def _finalize_included_app_values(self, helm_values, base_image_name=None): helm_values[KEY_APPS][app_name] = dict_merge( helm_values[KEY_APPS][app_name], finalized) - def collect_app_values(self, app_base_path, base_image_name=None, helm_values=None): + def collect_app_values(self, app_base_path: Path, base_image_name=None, helm_values=None): values = {} for app_path in app_base_path.glob("*/"): # We get the sub-files that are directories @@ -196,14 +196,14 @@ def _init_static_images(self, base_image_name): base_name = base_image_name for static_img_dockerfile in find_dockerfiles_paths(os.path.join(root_path, STATIC_IMAGES_PATH)): self.static_images.add(static_img_dockerfile) - - img_name = image_name_from_dockerfile_path(os.path.basename( - static_img_dockerfile), base_name=base_name) + dockerfile_basename = os.path.basename(static_img_dockerfile) + img_name = image_name_from_dockerfile_path(dockerfile_basename, base_name=base_name) # Static images have context where the Dockerfile is located - self.base_images[os.path.basename(static_img_dockerfile)] = self.image_tag( + image_tag = self.image_tag( img_name, build_context_path=static_img_dockerfile, dependencies=guess_build_dependencies_from_dockerfile(static_img_dockerfile) ) + self.base_images[dockerfile_basename] = (image_tag, get_dockerfile_baseimg_args(static_img_dockerfile)) def _assign_static_build_dependencies(self, helm_values): static_consumers = {} @@ -216,7 +216,7 @@ def _assign_static_build_dependencies(self, helm_values): if dep in self.exclude and dep in helm_values[KEY_TASK_IMAGES] and key not in self.exclude: static_consumers.setdefault(dep, set()).add(key) if dep in self.base_images and dep not in helm_values[KEY_TASK_IMAGES]: - helm_values[KEY_TASK_IMAGES][dep] = self.base_images[dep] + helm_values[KEY_TASK_IMAGES][dep] = self.base_images[dep][0] # helm_values.setdefault(KEY_TASK_IMAGES_BUILD, {})[dep] = { # 'context': os.path.relpath(static_img_dockerfile, self.dest_deployment_path.parent), # 'dockerfile': 'Dockerfile', @@ -265,21 +265,22 @@ def _prune_excluded_task_images(self, values, extra_consumers=None): continue del task_images[image_name] - def _init_base_images(self, base_image_name): + def _init_base_images(self, base_image_name: str) -> dict[str, tuple[str, dict[str, str]]]: """Initialize base images (infrastructure/base-images/) with root context.""" for i in range(len(self.root_paths)): - root_path = self.root_paths[i] + root_path = Path(self.root_paths[i]) base_name = base_image_name - for base_img_dockerfile in find_dockerfiles_paths(os.path.join(root_path, BASE_IMAGES_PATH)): + for base_img_dockerfile in find_dockerfiles_paths(f"{root_path / BASE_IMAGES_PATH}"): + dockerfile_basename = os.path.basename(base_img_dockerfile) img_name = image_name_from_dockerfile_path( - os.path.basename(base_img_dockerfile), base_name=base_name) + dockerfile_basename, base_name=base_name) # Base images have context at root - self.base_images[os.path.basename(base_img_dockerfile)] = self.image_tag( + image_tag = self.image_tag( img_name, build_context_path=root_path, dependencies=guess_build_dependencies_from_dockerfile(base_img_dockerfile) ) - + self.base_images[dockerfile_basename] = (image_tag, get_dockerfile_baseimg_args(base_img_dockerfile)) return self.base_images def _init_test_images(self, base_image_name): @@ -398,10 +399,9 @@ def _clear_unused_db_configuration(self, harness_config): if database_type != db: del database_config[db] - def image_tag(self, image_name, build_context_path=None, dependencies=()): - tag = self.tag - - return self.registry + image_name + (f':{tag}' if tag else '') + def image_tag(self, image_name: str, build_context_path=None, dependencies=()) -> str: + tag = f":{self.tag}" if self.tag else '' + return f"{self.registry}{image_name}{tag}" def get_included_applications(values, include): diff --git a/tools/deployment-cli-tools/ch_cli_tools/dockercompose.py b/tools/deployment-cli-tools/ch_cli_tools/dockercompose.py index bcae37641..71a577520 100644 --- a/tools/deployment-cli-tools/ch_cli_tools/dockercompose.py +++ b/tools/deployment-cli-tools/ch_cli_tools/dockercompose.py @@ -1,6 +1,7 @@ """ Utilities to create a helm chart from a CloudHarness directory structure """ +from pathlib import Path from typing import Union import yaml from ruamel.yaml import YAML @@ -256,8 +257,9 @@ def __finish_helm_values(self, values, defer_task_images=False): create_env_variables(values) return values, self.include - def create_app_values_spec(self, app_name, app_path, base_image_name=None, helm_values={}): - logging.info('Generating values script for ' + app_name) + def create_app_values_spec(self, app_name: str, app_path: Path, base_image_name: str | None=None, helm_values: dict | None=None): + logging.info(f'Generating values script for {app_name}') + helm_values = helm_values or {} deploy_path = app_path / 'deploy' specific_template_path = deploy_path / 'values.yaml' @@ -314,7 +316,7 @@ def create_app_values_spec(self, app_name, app_path, base_image_name=None, helm_ if build_dependencies: for build_dependency in values[KEY_HARNESS]['dependencies']['build']: if build_dependency in self.base_images: - values[KEY_TASK_IMAGES][build_dependency] = self.base_images[build_dependency] + values[KEY_TASK_IMAGES][build_dependency] = self.base_images[build_dependency][0] for task_path in task_images_paths: task_name = app_name_from_path(os.path.relpath( @@ -414,7 +416,7 @@ def finalize_app_values(self, app_name, app_path, app_values, base_image_name=No if build_dependencies: for build_dependency in values[KEY_HARNESS]['dependencies']['build']: if build_dependency in self.base_images: - values[KEY_TASK_IMAGES][build_dependency] = self.base_images[build_dependency] + values[KEY_TASK_IMAGES][build_dependency] = self.base_images[build_dependency][0] for task_path in task_images_paths: task_name = app_name_from_path(os.path.relpath( diff --git a/tools/deployment-cli-tools/ch_cli_tools/helm.py b/tools/deployment-cli-tools/ch_cli_tools/helm.py index d20fed3e3..0c4a0ffdc 100644 --- a/tools/deployment-cli-tools/ch_cli_tools/helm.py +++ b/tools/deployment-cli-tools/ch_cli_tools/helm.py @@ -1,6 +1,7 @@ """ Utilities to create a helm chart from a CloudHarness directory structure """ +from pathlib import Path from typing import Union import yaml import os @@ -9,7 +10,7 @@ import subprocess from cloudharness_utils.constants import VALUES_MANUAL_PATH, HELM_CHART_PATH -from .utils import get_cluster_ip, get_git_commit_hash, get_image_name, image_name_from_dockerfile_path, \ +from .utils import get_cluster_ip, get_dockerfile_baseimg_args, get_git_commit_hash, get_image_name, image_name_from_dockerfile_path, \ get_template, merge_to_yaml_file, dict_merge, app_name_from_path, \ find_dockerfiles_paths @@ -116,6 +117,9 @@ def process_values(self) -> HarnessMainConfig: # Now aggregate task images from the finalized included apps self._aggregate_task_images(helm_values) + # Collect all source_images and move them to the root + self._aggregate_source_images(self.base_images, helm_values) + # Remove build-only deps from apps — they belong in task_images only build_only = set(helm_values[KEY_APPS].keys()) - self.include for name in build_only: @@ -130,6 +134,9 @@ def process_values(self) -> HarnessMainConfig: values, include = self.__finish_helm_values(values=helm_values, defer_task_images=False) + # Collect all source_images and move them to the root + self._aggregate_source_images(self.base_images, helm_values) + self.create_tls_certificate(helm_values) # Adjust dependencies from static (common) images @@ -154,6 +161,30 @@ def process_values(self) -> HarnessMainConfig: registry["secret"] = None return HarnessMainConfig.from_dict(merged_values) + + def _aggregate_source_images(self, base_images, helm_values): + """Collects all the source_images and set them at the config root""" + all_source_images = {} + all_apps = helm_values["apps"] + for app, app_config in all_apps.items(): + try: + source_images = app_config["source_images"] + if source_images: + all_source_images |= source_images + del all_apps[app]["source_images"] + except KeyError: + continue + + for _, source_images in base_images.values(): + try: + if source_images: + all_source_images |= source_images + except KeyError: + continue + + helm_values["source_images"] = all_source_images | dict(helm_values.get("source_images", {})) + + def _aggregate_task_images(self, values): """Aggregate task images from included apps after finalization.""" apps = values[KEY_APPS] @@ -171,7 +202,7 @@ def _aggregate_task_images(self, values): values[KEY_TASK_IMAGES][dep_name] = image app_name = dep_name elif dep_name in self.base_images: - values[KEY_TASK_IMAGES][dep_name] = self.base_images[dep_name] + values[KEY_TASK_IMAGES][dep_name] = self.base_images[dep_name][0] elif owner in apps: app_name = owner values[KEY_TASK_IMAGES][dep_name] = apps[app_name][KEY_TASK_IMAGES][dep_name] @@ -286,7 +317,7 @@ def __finish_helm_values(self, values, defer_task_images=False): values[KEY_TASK_IMAGES][dep_name] = image app_name = dep_name elif dep_name in self.base_images: - values[KEY_TASK_IMAGES][dep_name] = self.base_images[dep_name] + values[KEY_TASK_IMAGES][dep_name] = self.base_images[dep_name][0] elif owner in apps: # task image owned by an application that is not part of the deployment app_name = owner values[KEY_TASK_IMAGES][dep_name] = apps[app_name][KEY_TASK_IMAGES][dep_name] @@ -306,24 +337,22 @@ def __finish_helm_values(self, values, defer_task_images=False): create_env_variables(values) return values, self.include - def create_app_values_spec(self, app_name, app_path, base_image_name=None, helm_values={}): - logging.info('Generating values script for ' + app_name) + def create_app_values_spec(self, app_name: str, app_path: Path, base_image_name: str | None=None, helm_values: dict | None=None): + logging.info(f'Generating values script for {app_name}') + helm_values = helm_values or {} - specific_template_path = os.path.join(app_path, 'deploy', 'values.yaml') - if os.path.exists(specific_template_path): - logging.info("Specific values template found: " + - specific_template_path) + specific_template_path = app_path / "deploy" / "values.yaml" + if specific_template_path.exists(): + logging.info(f"Specific values template found: {specific_template_path}") values = get_template(specific_template_path) else: values = {} for e in self.env: - specific_template_path = os.path.join( - app_path, 'deploy', f'values-{e}.yaml') - if os.path.exists(specific_template_path): - logging.info( - "Specific environment values template found: " + specific_template_path) - with open(specific_template_path) as f: + specific_template_path = app_path / "deploy" / f"values-{e}.yaml" + if specific_template_path.exists(): + logging.info(f"Specific environment values template found: {specific_template_path}") + with specific_template_path.open("r") as f: values_env_specific = yaml.safe_load(f) values = dict_merge(values, values_env_specific) @@ -332,10 +361,11 @@ def create_app_values_spec(self, app_name, app_path, base_image_name=None, helm_ values[KEY_HARNESS]['name']) image_paths = [path for path in find_dockerfiles_paths( - app_path) if 'tasks/' not in path and 'subapps' not in path] + f"{app_path}") if 'tasks/' not in path and 'subapps' not in path] if len(image_paths) > 1: logging.warning('Multiple Dockerfiles found in application %s. Picking the first one: %s', app_name, image_paths[0]) + if KEY_HARNESS in values and 'dependencies' in values[KEY_HARNESS] and 'build' in values[KEY_HARNESS]['dependencies']: build_dependencies = values[KEY_HARNESS]['dependencies']['build'] else: @@ -347,8 +377,8 @@ def create_app_values_spec(self, app_name, app_path, base_image_name=None, helm_ image_name = get_image_name(values.get(KEY_HARNESS, {}).get('image_name', ''), base_image_name) if len(image_paths) > 0 and not deployment_image: - - image_name = image_name or image_name_from_dockerfile_path(os.path.relpath(image_paths[0], os.path.dirname(app_path)), base_image_name) + values['source_images'] = get_dockerfile_baseimg_args(app_path) + image_name = image_name or image_name_from_dockerfile_path(os.path.relpath(image_paths[0], app_path.parent), base_name=base_image_name) values['image'] = self.image_tag( image_name, build_context_path=app_path, dependencies=build_dependencies) elif KEY_HARNESS in values and not deployment_image and values[ @@ -357,17 +387,17 @@ def create_app_values_spec(self, app_name, app_path, base_image_name=None, helm_ f"Specify harness.deployment.image value if you intend to use a prebuilt image.") task_images_paths = [path for path in find_dockerfiles_paths( - app_path) if 'tasks/' in path] + f"{app_path}") if 'tasks/' in path] values[KEY_TASK_IMAGES] = values.get(KEY_TASK_IMAGES, {}) if build_dependencies: for build_dependency in values[KEY_HARNESS]['dependencies']['build']: if build_dependency in self.base_images: - values[KEY_TASK_IMAGES][build_dependency] = self.base_images[build_dependency] + values[KEY_TASK_IMAGES][build_dependency] = self.base_images[build_dependency][0] for task_path in task_images_paths: - task_name = app_name_from_path(os.path.relpath( - task_path, os.path.dirname(app_path))) + values['source_images'] = get_dockerfile_baseimg_args(app_path) + task_name = app_name_from_path(os.path.relpath(task_path, app_path.parent)) task_img_name = "-".join([image_name, os.path.basename(task_path)]) if image_name else image_name_from_dockerfile_path(task_path, base_image_name) values[KEY_TASK_IMAGES][task_name] = self.image_tag( @@ -406,16 +436,17 @@ def load_app_values(self, app_name, app_path, helm_values={}): return values - def finalize_app_values(self, app_name, app_path, app_values, base_image_name=None, helm_values={}): + def finalize_app_values(self, app_name: str, app_path: Path, app_values, base_image_name: str | None=None, helm_values: dict | None=None): """Expensive finalization: Dockerfile discovery, image tagging, task images. Called only for apps that survive the include filter. """ logging.info('Finalizing values for ' + app_name) + helm_values = helm_values or {} values = app_values image_paths = [path for path in find_dockerfiles_paths( - app_path) if 'tasks/' not in path and 'subapps' not in path] + f"{app_path}") if 'tasks/' not in path and 'subapps' not in path] if len(image_paths) > 1: logging.warning('Multiple Dockerfiles found in application %s. Picking the first one: %s', app_name, image_paths[0]) @@ -432,7 +463,8 @@ def finalize_app_values(self, app_name, app_path, app_values, base_image_name=No image_name = get_image_name(values.get(KEY_HARNESS, {}).get('image_name', ''), base_image_name) if len(image_paths) > 0 and not deployment_image: values['build'] = True - image_name = image_name or image_name_from_dockerfile_path(os.path.relpath(image_paths[0], os.path.dirname(app_path)), base_image_name) + values['source_images'] = get_dockerfile_baseimg_args(app_path) + image_name = image_name or image_name_from_dockerfile_path(os.path.relpath(image_paths[0], app_path.parent), base_image_name) values['image'] = self.image_tag( image_name, build_context_path=app_path, dependencies=build_dependencies) elif KEY_HARNESS in values and not deployment_image and values[ @@ -443,19 +475,20 @@ def finalize_app_values(self, app_name, app_path, app_values, base_image_name=No values['build'] = not bool(deployment_image) task_images_paths = [path for path in find_dockerfiles_paths( - app_path) if 'tasks/' in path] + f"{app_path}") if 'tasks/' in path] values[KEY_TASK_IMAGES] = values.get(KEY_TASK_IMAGES, {}) if build_dependencies: for build_dependency in values[KEY_HARNESS]['dependencies']['build']: if build_dependency in self.base_images: - values[KEY_TASK_IMAGES][build_dependency] = self.base_images[build_dependency] + values[KEY_TASK_IMAGES][build_dependency] = self.base_images[build_dependency][0] for task_path in task_images_paths: task_name = app_name_from_path(os.path.relpath( task_path, os.path.dirname(app_path))) task_img_name = "-".join([image_name, os.path.basename(task_path)]) if image_name else image_name_from_dockerfile_path(task_path, base_image_name) + values['source_images'] = get_dockerfile_baseimg_args(app_path) values[KEY_TASK_IMAGES][task_name] = self.image_tag( task_img_name, build_context_path=task_path, dependencies=values[KEY_TASK_IMAGES].keys()) diff --git a/tools/deployment-cli-tools/ch_cli_tools/skaffold.py b/tools/deployment-cli-tools/ch_cli_tools/skaffold.py index 2c71fa5c8..37b4bb334 100644 --- a/tools/deployment-cli-tools/ch_cli_tools/skaffold.py +++ b/tools/deployment-cli-tools/ch_cli_tools/skaffold.py @@ -7,8 +7,7 @@ from cloudharness_model import ApplicationTestConfig, HarnessMainConfig, GitDependencyConfig from cloudharness_utils.constants import APPS_PATH, DEPLOYMENT_CONFIGURATION_PATH, \ - BASE_IMAGES_PATH, STATIC_IMAGES_PATH, HELM_ENGINE, COMPOSE_ENGINE, \ - CH_BASE_IMAGE_CONVENTIONAL_KEY + BASE_IMAGES_PATH, STATIC_IMAGES_PATH, HELM_ENGINE, COMPOSE_ENGINE from .helm import KEY_APPS, KEY_HARNESS, KEY_DEPLOYMENT, KEY_TASK_IMAGES from .utils import get_template, dict_merge, find_dockerfiles_paths, app_name_from_path, yaml, \ find_file_paths, guess_build_dependencies_from_dockerfile, get_json_template, clean_image_name @@ -108,7 +107,7 @@ def process_build_dockerfile( builds[app_name] = context_path base_images.add(clean_image_name(app_name)) - additional_build_args = get_additional_build_args(helm_values, app_key) | get_image_source(helm_values, app_key) + additional_build_args = get_additional_build_args(helm_values, app_key) | get_image_source(helm_values) artifacts[app_name] = build_artifact( app_name, @@ -337,10 +336,5 @@ def get_additional_build_args(helm_values: HarnessMainConfig, app_key: str) -> d return {} -def get_image_source(helm_values: HarnessMainConfig, app_key: str) -> dict[str, str]: - source_image = helm_values.get("source_images", {}).get(app_key) - if isinstance(source_image, dict): - return source_image - elif source_image: - return {CH_BASE_IMAGE_CONVENTIONAL_KEY: source_image} - return {} +def get_image_source(helm_values: HarnessMainConfig) -> dict[str, str]: + return helm_values.get("source_images", {}) diff --git a/tools/deployment-cli-tools/ch_cli_tools/utils.py b/tools/deployment-cli-tools/ch_cli_tools/utils.py index 0f8b85bb2..3e400085c 100644 --- a/tools/deployment-cli-tools/ch_cli_tools/utils.py +++ b/tools/deployment-cli-tools/ch_cli_tools/utils.py @@ -34,7 +34,7 @@ SKIP_DIRS = ('node_modules',) -def image_name_from_dockerfile_path(dockerfile_path, base_name=None): +def image_name_from_dockerfile_path(dockerfile_path, base_name=None) -> str: return get_image_name(app_name_from_path(dockerfile_path), base_name) @@ -65,12 +65,12 @@ def find_subdirs(base_path): return tuple() -def find_dockerfiles_paths(base_directory): +def find_dockerfiles_paths(base_directory: str) -> tuple[str, ...]: all_dockerfiles = find_file_paths(base_directory, 'Dockerfile') # We want to remove all dockerfiles that are not in a git repository # This will exclude the cloned dependencies and other repos cloned for convenience - dockerfiles_without_git = [] + dockerfiles_without_git: list[str] = [] for dockerfile in all_dockerfiles: directory = dockerfile diff --git a/tools/deployment-cli-tools/tests/resources/deployment-configuration/values-template-nreg.yaml b/tools/deployment-cli-tools/tests/resources/deployment-configuration/values-template-nreg.yaml index 157ed8d9e..97b267d8e 100644 --- a/tools/deployment-cli-tools/tests/resources/deployment-configuration/values-template-nreg.yaml +++ b/tools/deployment-cli-tools/tests/resources/deployment-configuration/values-template-nreg.yaml @@ -6,9 +6,7 @@ database: auto: true source_images: - myapp: "myregistry.myapp:15.3" - samples: - BASEIMAGE: myother.image:14 + KEYCLOAK: "myregistry.myapp:15.3" events: kafka: diff --git a/tools/deployment-cli-tools/tests/test_skaffold.py b/tools/deployment-cli-tools/tests/test_skaffold.py index af582ccda..ebe951cda 100644 --- a/tools/deployment-cli-tools/tests/test_skaffold.py +++ b/tools/deployment-cli-tools/tests/test_skaffold.py @@ -1,8 +1,8 @@ -import shutil import os +import shutil -from ch_cli_tools.preprocessing import preprocess_build_overrides from ch_cli_tools.helm import * +from ch_cli_tools.preprocessing import preprocess_build_overrides from ch_cli_tools.skaffold import * HERE = os.path.dirname(os.path.realpath(__file__)) @@ -385,12 +385,12 @@ def test_skaffold_imgarg_retrieval(tmp_path): # Ensure in the test that the Helm is well formed source_images = values.get("source_images") assert len(source_images) == 2 - assert source_images["myapp"] == "myregistry.myapp:15.3" - assert source_images["samples"] == {"BASEIMAGE": "myother.image:14"} - - assert get_image_source(values, "myapp") == {CH_BASE_IMAGE_CONVENTIONAL_KEY: "myregistry.myapp:15.3"} - assert get_image_source(values, "samples") == {"BASEIMAGE": "myother.image:14"} - assert get_image_source(values, "events") == {} + assert source_images["KEYCLOAK"] == "myregistry.myapp:15.3" + assert source_images["NODE"] == "node:22-alpine" + assert get_image_source(values) == { + "KEYCLOAK": "myregistry.myapp:15.3", + "NODE": "node:22-alpine", + } def test_skaffold_imgarg(tmp_path): @@ -431,9 +431,9 @@ def get_buildargs(name) -> dict[str, str]: return {} samples_buildargs = get_buildargs("samples") - assert "BASEIMAGE" in samples_buildargs - assert samples_buildargs["BASEIMAGE"] == "myother.image:14" + assert samples_buildargs["KEYCLOAK"] == "myregistry.myapp:15.3" + assert samples_buildargs["NODE"] == "node:22-alpine" myapp_buildargs = get_buildargs("myapp") - assert CH_BASE_IMAGE_CONVENTIONAL_KEY in myapp_buildargs - assert myapp_buildargs[CH_BASE_IMAGE_CONVENTIONAL_KEY] == "myregistry.myapp:15.3" + assert myapp_buildargs["KEYCLOAK"] == "myregistry.myapp:15.3" + assert myapp_buildargs["NODE"] == "node:22-alpine" From 874276a2b0402ea800e0626dcbb8b207781b8267 Mon Sep 17 00:00:00 2001 From: aranega Date: Tue, 1 Sep 2026 09:29:22 -0600 Subject: [PATCH 08/14] CH-287 Add tests for source_images in the values.yaml file --- .../values-template-dev.yaml | 5 +- tools/deployment-cli-tools/tests/test_helm.py | 47 +++++++++++++++++-- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/tools/deployment-cli-tools/tests/resources/deployment-configuration/values-template-dev.yaml b/tools/deployment-cli-tools/tests/resources/deployment-configuration/values-template-dev.yaml index 44673bfe8..48d7fd5c9 100644 --- a/tools/deployment-cli-tools/tests/resources/deployment-configuration/values-template-dev.yaml +++ b/tools/deployment-cli-tools/tests/resources/deployment-configuration/values-template-dev.yaml @@ -1,3 +1,6 @@ a: dev database: - auto: false \ No newline at end of file + auto: false + +source_images: + KEYCLOAK: "myregistry.mykeycloak:99.9" diff --git a/tools/deployment-cli-tools/tests/test_helm.py b/tools/deployment-cli-tools/tests/test_helm.py index fb18d81d5..f704b0f13 100644 --- a/tools/deployment-cli-tools/tests/test_helm.py +++ b/tools/deployment-cli-tools/tests/test_helm.py @@ -1,11 +1,15 @@ -from ch_cli_tools.helm import * -from ch_cli_tools.configurationgenerator import * -from ch_cli_tools import configurationgenerator -from ch_cli_tools.preprocessing import preprocess_build_overrides, generate_hash_based_image_tags -import pytest import shutil import subprocess +import pytest +from ch_cli_tools import configurationgenerator +from ch_cli_tools.configurationgenerator import * +from ch_cli_tools.helm import * +from ch_cli_tools.preprocessing import ( + generate_hash_based_image_tags, + preprocess_build_overrides, +) + HERE = os.path.dirname(os.path.realpath(__file__)) RESOURCES = os.path.join(HERE, 'resources') CLOUDHARNESS_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) @@ -182,6 +186,18 @@ def test_collect_helm_values_noreg_noinclude(tmp_path): assert values[KEY_TASK_IMAGES]['myapp-mytask'] == 'testprojectname/myapp-mytask:1' assert values[KEY_TASK_IMAGES]['my-common'] == 'testprojectname/my-common:1' + # Check source images + # KEYCLOAK is overriden and mybase and mybase2 should appear as they have been collected + assert values["source_images"] == { + "GOLANG": "golang:1.26", + "ROCKYLINUX": "golang:rockylinux/rockylinux:10.1-minimal", + "SENTRY": "sentry:9.1.2", + "KEYCLOAK": "myregistry.mykeycloak:99.9", + "mybase": "foo:bar", + "mybase2": "spam:egg", + "NODE": "node:22-alpine", + } + def test_collect_helm_values_precedence(tmp_path): out_folder = tmp_path / 'test_collect_helm_values_precedence' @@ -1150,3 +1166,24 @@ def test_validate_secrets_rejects_malformed_definitions(): with pytest.raises(ValuesValidationException, match="expected a map of secret definitions"): validate_secrets(secret_values(['a', 'b'])) + + +def test_collect_helm_values_source_images_merge(tmp_path): + out_path = tmp_path / 'test_collect_helm_values_source_images_merge' + values = create_helm_chart([CLOUDHARNESS_ROOT, RESOURCES], output_path=out_path, + include=["samples", "myapp"], domain="my.local", + namespace='test', env='nreg', local=False, tag=1, registry='reg') + + source_images = values.get("source_images") + assert source_images["KEYCLOAK"] == "myregistry.myapp:15.3" + assert "NODE" in source_images + + +def test_collect_helm_values_source_images_merge_no_include(tmp_path): + out_path = tmp_path / 'test_collect_helm_values_source_images_merge' + values = create_helm_chart([CLOUDHARNESS_ROOT, RESOURCES], output_path=out_path, domain="my.local", + namespace='test', env='nreg', local=False, tag=1, registry='reg') + + source_images = values.get("source_images") + assert source_images["KEYCLOAK"] == "myregistry.myapp:15.3" + assert "NODE" in source_images From 6b5a0ce3b46e41fc6c3658fc1544fbc22778c624 Mon Sep 17 00:00:00 2001 From: aranega Date: Tue, 1 Sep 2026 09:34:15 -0600 Subject: [PATCH 09/14] CH-287 Update documentation for "source_images" --- docs/base-common-images.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/docs/base-common-images.md b/docs/base-common-images.md index e1db0fb4e..95a173c13 100644 --- a/docs/base-common-images.md +++ b/docs/base-common-images.md @@ -77,20 +77,17 @@ In both situations, the modification is done in the `value-template.yaml` of you Changing the base image for your application or applications which have a `Dockerfile` is done through the `source_images` entry of your `value-template.yaml` file. This entry defines a mapping between the `ARG` of your `Dockerfile` and the value you want to inject. -Here is an example of a declared mapping for two applications: +Here is an example of a declared mapping to change the base image for Python based apps and Node base apps: ```yaml # value-template.yaml -image-sources: - myapp: "mybaseimg:14.5" - samples: - CLOUDHARNESS_FRONTEND_BUILD: "myfrontendbaseimg:15.6" +image_sources: + NODE: "mybaseimg:14.5" + PYTHON: "myotherimage:15.1" ``` -The first entry for `myapp` will change the base image for `mybaseimg:14.5`. -As nothing is given as detail for the base image key inside the `Dockerfile`, by convention, CloudHarness is going to set `CLOUDHARNESS_ROOT_BASE` to `mybaseimg:14.5`. -If your `Dockerfile` doesn't use the `CLOUDHARNESS_ROOT_BASE` `ARG` as variable for your `FROM ...` directive, you can precise the name of the variable which needs to be changed by declaring it and the value it needs to have as a YAML dictionnary. -The second entry is in this situation, the base `ARG` which is used in the `Dockerfile` for the `FROM ...` directive is `CLOUDHARNESS_FRONTEND_BUILD`. +The list of the base images variables and the value they resolve to is automatically generated in the `helm/values.yaml` file which is generated by the `harness-deployment` command under the `source_images` key. You can then copy/paste the entries you need in your `value-template.yaml` file and tweak the values there. + ### Change the base image for applications which inject their image in the helm chart From f43b6d281b01884e59e91022f75c512abd5dbcf2 Mon Sep 17 00:00:00 2001 From: aranega Date: Tue, 1 Sep 2026 09:41:21 -0600 Subject: [PATCH 10/14] CH-287 Remove unused variable --- tools/deployment-cli-tools/ch_cli_tools/helm.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tools/deployment-cli-tools/ch_cli_tools/helm.py b/tools/deployment-cli-tools/ch_cli_tools/helm.py index 0c4a0ffdc..ca8a79b63 100644 --- a/tools/deployment-cli-tools/ch_cli_tools/helm.py +++ b/tools/deployment-cli-tools/ch_cli_tools/helm.py @@ -339,7 +339,6 @@ def __finish_helm_values(self, values, defer_task_images=False): def create_app_values_spec(self, app_name: str, app_path: Path, base_image_name: str | None=None, helm_values: dict | None=None): logging.info(f'Generating values script for {app_name}') - helm_values = helm_values or {} specific_template_path = app_path / "deploy" / "values.yaml" if specific_template_path.exists(): @@ -442,7 +441,6 @@ def finalize_app_values(self, app_name: str, app_path: Path, app_values, base_im Called only for apps that survive the include filter. """ logging.info('Finalizing values for ' + app_name) - helm_values = helm_values or {} values = app_values image_paths = [path for path in find_dockerfiles_paths( From c2e8cdf4cf5a1f7069936f05f9d7371094697e70 Mon Sep 17 00:00:00 2001 From: aranega Date: Tue, 1 Sep 2026 09:46:23 -0600 Subject: [PATCH 11/14] CH-287 Fix formatting issues --- .../ch_cli_tools/configurationgenerator.py | 2 +- tools/deployment-cli-tools/ch_cli_tools/dockercompose.py | 2 +- tools/deployment-cli-tools/ch_cli_tools/helm.py | 6 ++---- tools/deployment-cli-tools/ch_cli_tools/skaffold.py | 2 +- tools/deployment-cli-tools/ch_cli_tools/utils.py | 1 - tools/deployment-cli-tools/tests/test_helm.py | 8 ++++---- 6 files changed, 9 insertions(+), 12 deletions(-) diff --git a/tools/deployment-cli-tools/ch_cli_tools/configurationgenerator.py b/tools/deployment-cli-tools/ch_cli_tools/configurationgenerator.py index 1e50fe6bb..f5076a436 100644 --- a/tools/deployment-cli-tools/ch_cli_tools/configurationgenerator.py +++ b/tools/deployment-cli-tools/ch_cli_tools/configurationgenerator.py @@ -71,7 +71,7 @@ def __init__(self, root_paths: List[str], tag: Union[str, int, None] = 'latest', self.all_images = {} @abc.abstractmethod - def create_app_values_spec(self, app_name: str, app_path: Path, base_image_name: str | None=None, helm_values: dict | None=None) -> dict: + def create_app_values_spec(self, app_name: str, app_path: Path, base_image_name: str | None = None, helm_values: dict | None = None) -> dict: ... @abc.abstractmethod diff --git a/tools/deployment-cli-tools/ch_cli_tools/dockercompose.py b/tools/deployment-cli-tools/ch_cli_tools/dockercompose.py index 71a577520..4564d6554 100644 --- a/tools/deployment-cli-tools/ch_cli_tools/dockercompose.py +++ b/tools/deployment-cli-tools/ch_cli_tools/dockercompose.py @@ -257,7 +257,7 @@ def __finish_helm_values(self, values, defer_task_images=False): create_env_variables(values) return values, self.include - def create_app_values_spec(self, app_name: str, app_path: Path, base_image_name: str | None=None, helm_values: dict | None=None): + def create_app_values_spec(self, app_name: str, app_path: Path, base_image_name: str | None = None, helm_values: dict | None = None): logging.info(f'Generating values script for {app_name}') helm_values = helm_values or {} diff --git a/tools/deployment-cli-tools/ch_cli_tools/helm.py b/tools/deployment-cli-tools/ch_cli_tools/helm.py index ca8a79b63..197a9a278 100644 --- a/tools/deployment-cli-tools/ch_cli_tools/helm.py +++ b/tools/deployment-cli-tools/ch_cli_tools/helm.py @@ -161,7 +161,6 @@ def process_values(self) -> HarnessMainConfig: registry["secret"] = None return HarnessMainConfig.from_dict(merged_values) - def _aggregate_source_images(self, base_images, helm_values): """Collects all the source_images and set them at the config root""" all_source_images = {} @@ -184,7 +183,6 @@ def _aggregate_source_images(self, base_images, helm_values): helm_values["source_images"] = all_source_images | dict(helm_values.get("source_images", {})) - def _aggregate_task_images(self, values): """Aggregate task images from included apps after finalization.""" apps = values[KEY_APPS] @@ -337,7 +335,7 @@ def __finish_helm_values(self, values, defer_task_images=False): create_env_variables(values) return values, self.include - def create_app_values_spec(self, app_name: str, app_path: Path, base_image_name: str | None=None, helm_values: dict | None=None): + def create_app_values_spec(self, app_name: str, app_path: Path, base_image_name: str | None = None, helm_values: dict | None = None): logging.info(f'Generating values script for {app_name}') specific_template_path = app_path / "deploy" / "values.yaml" @@ -435,7 +433,7 @@ def load_app_values(self, app_name, app_path, helm_values={}): return values - def finalize_app_values(self, app_name: str, app_path: Path, app_values, base_image_name: str | None=None, helm_values: dict | None=None): + def finalize_app_values(self, app_name: str, app_path: Path, app_values, base_image_name: str | None = None, helm_values: dict | None = None): """Expensive finalization: Dockerfile discovery, image tagging, task images. Called only for apps that survive the include filter. diff --git a/tools/deployment-cli-tools/ch_cli_tools/skaffold.py b/tools/deployment-cli-tools/ch_cli_tools/skaffold.py index 37b4bb334..17ca8893f 100644 --- a/tools/deployment-cli-tools/ch_cli_tools/skaffold.py +++ b/tools/deployment-cli-tools/ch_cli_tools/skaffold.py @@ -59,7 +59,7 @@ def resolve_dockerfile_name(dockerfile_dir_rel): def build_artifact( app_name: str, context_path: str, - requirements: list[str] | None = None, + requirements: list[str] | None = None, dockerfile_path: str = '', additional_build_args: dict[str, str] | None = None, ) -> dict: diff --git a/tools/deployment-cli-tools/ch_cli_tools/utils.py b/tools/deployment-cli-tools/ch_cli_tools/utils.py index 3e400085c..30ead39a5 100644 --- a/tools/deployment-cli-tools/ch_cli_tools/utils.py +++ b/tools/deployment-cli-tools/ch_cli_tools/utils.py @@ -590,7 +590,6 @@ def get_dockerfile_baseimg_args(filename: str) -> dict[str, str]: return args - def check_response_200(endpoint_url, headers=None): resp = requests.get(endpoint_url, headers=headers, timeout=5) return resp.status_code == 200 diff --git a/tools/deployment-cli-tools/tests/test_helm.py b/tools/deployment-cli-tools/tests/test_helm.py index f704b0f13..ba837320d 100644 --- a/tools/deployment-cli-tools/tests/test_helm.py +++ b/tools/deployment-cli-tools/tests/test_helm.py @@ -496,7 +496,7 @@ def test_statefulset_option(tmp_path): assert 'myapp-data' not in claims assert sts['spec']['volumeClaimTemplates'][0]['metadata']['name'] == 'myapp-data' assert not any(m for m in manifests - if m.get('kind') == 'PersistentVolumeClaim' and m.get('metadata', {}).get('name') == 'myapp-data') + if m.get('kind') == 'PersistentVolumeClaim' and m.get('metadata', {}).get('name') == 'myapp-data') db_sts = find_manifest(manifests, 'StatefulSet', db_name) assert db_sts['spec']['serviceName'] == db_name @@ -505,7 +505,7 @@ def test_statefulset_option(tmp_path): assert 'initContainers' not in db_sts['spec']['template']['spec'] assert db_sts['spec']['volumeClaimTemplates'][0]['metadata']['name'] == db_name assert not any(m for m in manifests - if m.get('kind') == 'PersistentVolumeClaim' and m.get('metadata', {}).get('name') == db_name) + if m.get('kind') == 'PersistentVolumeClaim' and m.get('metadata', {}).get('name') == db_name) find_manifest(manifests, 'Service', db_name) # without a legacy PVC no migration resources are rendered assert not any(m for m in manifests if 'volume-migration' in m.get('metadata', {}).get('name', '')) @@ -572,9 +572,9 @@ def ingress_paths(manifests): manifests = render_helm_chart(helm_path) assert not any(m for m in manifests - if m.get('kind') == 'Service' and m.get('metadata', {}).get('name') == rw_name) + if m.get('kind') == 'Service' and m.get('metadata', {}).get('name') == rw_name) assert not any(p for p in ingress_paths(manifests) - if p['backend']['service']['name'] == rw_name) + if p['backend']['service']['name'] == rw_name) harness['deployment']['statefulset'] = True with open(values_path, 'w') as values_file: From 48edc8e2ea8e6589565459685f5835173f9985b6 Mon Sep 17 00:00:00 2001 From: aranega Date: Tue, 1 Sep 2026 11:07:40 -0600 Subject: [PATCH 12/14] CH-287 Fix typo for the nfsserver docker image --- applications/nfsserver/Dockerfile | 2 +- tools/deployment-cli-tools/tests/test_helm.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/nfsserver/Dockerfile b/applications/nfsserver/Dockerfile index 0f3299b9c..bd57812dd 100644 --- a/applications/nfsserver/Dockerfile +++ b/applications/nfsserver/Dockerfile @@ -1,6 +1,6 @@ # compile provisioner ARG GOLANG=golang:1.26 -ARG ROCKYLINUX=golang:rockylinux/rockylinux:10.1-minimal +ARG ROCKYLINUX=rockylinux/rockylinux:10.1-minimal FROM $GOLANG AS provisioner WORKDIR /usr/src/app diff --git a/tools/deployment-cli-tools/tests/test_helm.py b/tools/deployment-cli-tools/tests/test_helm.py index ba837320d..abb686135 100644 --- a/tools/deployment-cli-tools/tests/test_helm.py +++ b/tools/deployment-cli-tools/tests/test_helm.py @@ -190,7 +190,7 @@ def test_collect_helm_values_noreg_noinclude(tmp_path): # KEYCLOAK is overriden and mybase and mybase2 should appear as they have been collected assert values["source_images"] == { "GOLANG": "golang:1.26", - "ROCKYLINUX": "golang:rockylinux/rockylinux:10.1-minimal", + "ROCKYLINUX": "rockylinux/rockylinux:10.1-minimal", "SENTRY": "sentry:9.1.2", "KEYCLOAK": "myregistry.mykeycloak:99.9", "mybase": "foo:bar", From 67f2fd4add905564737d12870a0797c2bdc5cdcd Mon Sep 17 00:00:00 2001 From: aranega Date: Wed, 2 Sep 2026 04:32:22 -0600 Subject: [PATCH 13/14] CH-287 Fix typo "image_sources" vs "source_images" --- docs/base-common-images.md | 2 +- tools/deployment-cli-tools/ch_cli_tools/skaffold.py | 4 ++-- tools/deployment-cli-tools/tests/test_skaffold.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/base-common-images.md b/docs/base-common-images.md index 95a173c13..d1c7bdeda 100644 --- a/docs/base-common-images.md +++ b/docs/base-common-images.md @@ -81,7 +81,7 @@ Here is an example of a declared mapping to change the base image for Python bas ```yaml # value-template.yaml -image_sources: +source_images: NODE: "mybaseimg:14.5" PYTHON: "myotherimage:15.1" ``` diff --git a/tools/deployment-cli-tools/ch_cli_tools/skaffold.py b/tools/deployment-cli-tools/ch_cli_tools/skaffold.py index 17ca8893f..053db1cb3 100644 --- a/tools/deployment-cli-tools/ch_cli_tools/skaffold.py +++ b/tools/deployment-cli-tools/ch_cli_tools/skaffold.py @@ -107,7 +107,7 @@ def process_build_dockerfile( builds[app_name] = context_path base_images.add(clean_image_name(app_name)) - additional_build_args = get_additional_build_args(helm_values, app_key) | get_image_source(helm_values) + additional_build_args = get_additional_build_args(helm_values, app_key) | get_source_images(helm_values) artifacts[app_name] = build_artifact( app_name, @@ -336,5 +336,5 @@ def get_additional_build_args(helm_values: HarnessMainConfig, app_key: str) -> d return {} -def get_image_source(helm_values: HarnessMainConfig) -> dict[str, str]: +def get_source_images(helm_values: HarnessMainConfig) -> dict[str, str]: return helm_values.get("source_images", {}) diff --git a/tools/deployment-cli-tools/tests/test_skaffold.py b/tools/deployment-cli-tools/tests/test_skaffold.py index ebe951cda..c3006bf0b 100644 --- a/tools/deployment-cli-tools/tests/test_skaffold.py +++ b/tools/deployment-cli-tools/tests/test_skaffold.py @@ -387,7 +387,7 @@ def test_skaffold_imgarg_retrieval(tmp_path): assert len(source_images) == 2 assert source_images["KEYCLOAK"] == "myregistry.myapp:15.3" assert source_images["NODE"] == "node:22-alpine" - assert get_image_source(values) == { + assert get_source_images(values) == { "KEYCLOAK": "myregistry.myapp:15.3", "NODE": "node:22-alpine", } From 6f0ee397dc19b966095b63766c1190c8e375dec3 Mon Sep 17 00:00:00 2001 From: Filippo Ledda Date: Wed, 2 Sep 2026 13:36:30 +0200 Subject: [PATCH 14/14] CH-287 fix codefresh and tilt build arguments --- deployment/codefresh-test.yaml | 101 +++++++++++++++++- docs/base-common-images.md | 2 +- .../ch_cli_tools/codefresh.py | 12 ++- .../ch_cli_tools/skaffold.py | 7 +- .../deployment-cli-tools/ch_cli_tools/tilt.py | 4 +- .../ch_cli_tools/utils.py | 4 + .../tests/test_codefresh.py | 59 ++++++++++ tools/deployment-cli-tools/tests/test_tilt.py | 74 +++++++++++++ 8 files changed, 252 insertions(+), 11 deletions(-) create mode 100644 tools/deployment-cli-tools/tests/test_tilt.py diff --git a/deployment/codefresh-test.yaml b/deployment/codefresh-test.yaml index c4821d6ae..5fa047db3 100644 --- a/deployment/codefresh-test.yaml +++ b/deployment/codefresh-test.yaml @@ -55,6 +55,11 @@ steps: buildkit: true build_arguments: - NOCACHE=${{CF_BUILD_ID}} + - KEYCLOAK=quay.io/keycloak/keycloak:26.5 + - GOLANG=golang:1.26 + - ROCKYLINUX=rockylinux/rockylinux:10.1-minimal + - NODE=node:22-alpine + - PYTHON=python:3.12-slim-trixie image_name: cloud-harness/accounts title: Accounts working_directory: ./applications/accounts @@ -77,6 +82,11 @@ steps: buildkit: true build_arguments: - NOCACHE=${{CF_BUILD_ID}} + - KEYCLOAK=quay.io/keycloak/keycloak:26.5 + - GOLANG=golang:1.26 + - ROCKYLINUX=rockylinux/rockylinux:10.1-minimal + - NODE=node:22-alpine + - PYTHON=python:3.12-slim-trixie image_name: cloud-harness/cloudharness-base title: Cloudharness base working_directory: ./. @@ -99,6 +109,11 @@ steps: buildkit: true build_arguments: - NOCACHE=${{CF_BUILD_ID}} + - KEYCLOAK=quay.io/keycloak/keycloak:26.5 + - GOLANG=golang:1.26 + - ROCKYLINUX=rockylinux/rockylinux:10.1-minimal + - NODE=node:22-alpine + - PYTHON=python:3.12-slim-trixie image_name: cloud-harness/cloudharness-frontend-build title: Cloudharness frontend build working_directory: ./. @@ -121,6 +136,11 @@ steps: buildkit: true build_arguments: - NOCACHE=${{CF_BUILD_ID}} + - KEYCLOAK=quay.io/keycloak/keycloak:26.5 + - GOLANG=golang:1.26 + - ROCKYLINUX=rockylinux/rockylinux:10.1-minimal + - NODE=node:22-alpine + - PYTHON=python:3.12-slim-trixie image_name: cloud-harness/nfsserver title: Nfsserver working_directory: ./applications/nfsserver @@ -143,6 +163,11 @@ steps: buildkit: true build_arguments: - NOCACHE=${{CF_BUILD_ID}} + - KEYCLOAK=quay.io/keycloak/keycloak:26.5 + - GOLANG=golang:1.26 + - ROCKYLINUX=rockylinux/rockylinux:10.1-minimal + - NODE=node:22-alpine + - PYTHON=python:3.12-slim-trixie image_name: cloud-harness/test-e2e title: Test e2e working_directory: ./test/test-e2e @@ -172,6 +197,11 @@ steps: build_arguments: - NOCACHE=${{CF_BUILD_ID}} - CLOUDHARNESS_BASE=${{REGISTRY}}/cloud-harness/cloudharness-base:${{CLOUDHARNESS_BASE_TAG}} + - KEYCLOAK=quay.io/keycloak/keycloak:26.5 + - GOLANG=golang:1.26 + - ROCKYLINUX=rockylinux/rockylinux:10.1-minimal + - NODE=node:22-alpine + - PYTHON=python:3.12-slim-trixie image_name: cloud-harness/cloudharness-django title: Cloudharness django working_directory: ./infrastructure/common-images/cloudharness-django @@ -195,6 +225,11 @@ steps: build_arguments: - NOCACHE=${{CF_BUILD_ID}} - CLOUDHARNESS_BASE=${{REGISTRY}}/cloud-harness/cloudharness-base:${{CLOUDHARNESS_BASE_TAG}} + - KEYCLOAK=quay.io/keycloak/keycloak:26.5 + - GOLANG=golang:1.26 + - ROCKYLINUX=rockylinux/rockylinux:10.1-minimal + - NODE=node:22-alpine + - PYTHON=python:3.12-slim-trixie image_name: cloud-harness/cloudharness-flask title: Cloudharness flask working_directory: ./infrastructure/common-images/cloudharness-flask @@ -218,6 +253,11 @@ steps: build_arguments: - NOCACHE=${{CF_BUILD_ID}} - CLOUDHARNESS_BASE=${{REGISTRY}}/cloud-harness/cloudharness-base:${{CLOUDHARNESS_BASE_TAG}} + - KEYCLOAK=quay.io/keycloak/keycloak:26.5 + - GOLANG=golang:1.26 + - ROCKYLINUX=rockylinux/rockylinux:10.1-minimal + - NODE=node:22-alpine + - PYTHON=python:3.12-slim-trixie image_name: cloud-harness/jupyterhub title: Jupyterhub working_directory: ./applications/jupyterhub @@ -241,6 +281,11 @@ steps: build_arguments: - NOCACHE=${{CF_BUILD_ID}} - CLOUDHARNESS_BASE=${{REGISTRY}}/cloud-harness/cloudharness-base:${{CLOUDHARNESS_BASE_TAG}} + - KEYCLOAK=quay.io/keycloak/keycloak:26.5 + - GOLANG=golang:1.26 + - ROCKYLINUX=rockylinux/rockylinux:10.1-minimal + - NODE=node:22-alpine + - PYTHON=python:3.12-slim-trixie image_name: cloud-harness/sampleapp-print-file title: Samples print file working_directory: ./applications/samples/tasks/print-file @@ -264,6 +309,11 @@ steps: build_arguments: - NOCACHE=${{CF_BUILD_ID}} - CLOUDHARNESS_BASE=${{REGISTRY}}/cloud-harness/cloudharness-base:${{CLOUDHARNESS_BASE_TAG}} + - KEYCLOAK=quay.io/keycloak/keycloak:26.5 + - GOLANG=golang:1.26 + - ROCKYLINUX=rockylinux/rockylinux:10.1-minimal + - NODE=node:22-alpine + - PYTHON=python:3.12-slim-trixie image_name: cloud-harness/sampleapp-secret title: Samples secret working_directory: ./applications/samples/tasks/secret @@ -287,6 +337,11 @@ steps: build_arguments: - NOCACHE=${{CF_BUILD_ID}} - CLOUDHARNESS_BASE=${{REGISTRY}}/cloud-harness/cloudharness-base:${{CLOUDHARNESS_BASE_TAG}} + - KEYCLOAK=quay.io/keycloak/keycloak:26.5 + - GOLANG=golang:1.26 + - ROCKYLINUX=rockylinux/rockylinux:10.1-minimal + - NODE=node:22-alpine + - PYTHON=python:3.12-slim-trixie image_name: cloud-harness/test-api title: Test api working_directory: ./. @@ -311,6 +366,11 @@ steps: build_arguments: - NOCACHE=${{CF_BUILD_ID}} - CLOUDHARNESS_BASE=${{REGISTRY}}/cloud-harness/cloudharness-base:${{CLOUDHARNESS_BASE_TAG}} + - KEYCLOAK=quay.io/keycloak/keycloak:26.5 + - GOLANG=golang:1.26 + - ROCKYLINUX=rockylinux/rockylinux:10.1-minimal + - NODE=node:22-alpine + - PYTHON=python:3.12-slim-trixie image_name: cloud-harness/workflows-extract-download title: Workflows extract download working_directory: ./applications/workflows/tasks/extract-download @@ -334,6 +394,11 @@ steps: build_arguments: - NOCACHE=${{CF_BUILD_ID}} - CLOUDHARNESS_BASE=${{REGISTRY}}/cloud-harness/cloudharness-base:${{CLOUDHARNESS_BASE_TAG}} + - KEYCLOAK=quay.io/keycloak/keycloak:26.5 + - GOLANG=golang:1.26 + - ROCKYLINUX=rockylinux/rockylinux:10.1-minimal + - NODE=node:22-alpine + - PYTHON=python:3.12-slim-trixie image_name: cloud-harness/workflows-notify-queue title: Workflows notify queue working_directory: ./applications/workflows/tasks/notify-queue @@ -357,6 +422,11 @@ steps: build_arguments: - NOCACHE=${{CF_BUILD_ID}} - CLOUDHARNESS_BASE=${{REGISTRY}}/cloud-harness/cloudharness-base:${{CLOUDHARNESS_BASE_TAG}} + - KEYCLOAK=quay.io/keycloak/keycloak:26.5 + - GOLANG=golang:1.26 + - ROCKYLINUX=rockylinux/rockylinux:10.1-minimal + - NODE=node:22-alpine + - PYTHON=python:3.12-slim-trixie image_name: cloud-harness/workflows-send-result-event title: Workflows send result event working_directory: ./applications/workflows/tasks/send-result-event @@ -385,6 +455,11 @@ steps: build_arguments: - NOCACHE=${{CF_BUILD_ID}} - CLOUDHARNESS_FLASK=${{REGISTRY}}/cloud-harness/cloudharness-flask:${{CLOUDHARNESS_FLASK_TAG}} + - KEYCLOAK=quay.io/keycloak/keycloak:26.5 + - GOLANG=golang:1.26 + - ROCKYLINUX=rockylinux/rockylinux:10.1-minimal + - NODE=node:22-alpine + - PYTHON=python:3.12-slim-trixie image_name: cloud-harness/common title: Common working_directory: ./applications/common/server @@ -409,6 +484,11 @@ steps: - NOCACHE=${{CF_BUILD_ID}} - CLOUDHARNESS_FRONTEND_BUILD=${{REGISTRY}}/cloud-harness/cloudharness-frontend-build:${{CLOUDHARNESS_FRONTEND_BUILD_TAG}} - CLOUDHARNESS_FLASK=${{REGISTRY}}/cloud-harness/cloudharness-flask:${{CLOUDHARNESS_FLASK_TAG}} + - KEYCLOAK=quay.io/keycloak/keycloak:26.5 + - GOLANG=golang:1.26 + - ROCKYLINUX=rockylinux/rockylinux:10.1-minimal + - NODE=node:22-alpine + - PYTHON=python:3.12-slim-trixie image_name: cloud-harness/sampleapp title: Samples working_directory: ./applications/samples @@ -432,6 +512,11 @@ steps: build_arguments: - NOCACHE=${{CF_BUILD_ID}} - CLOUDHARNESS_FLASK=${{REGISTRY}}/cloud-harness/cloudharness-flask:${{CLOUDHARNESS_FLASK_TAG}} + - KEYCLOAK=quay.io/keycloak/keycloak:26.5 + - GOLANG=golang:1.26 + - ROCKYLINUX=rockylinux/rockylinux:10.1-minimal + - NODE=node:22-alpine + - PYTHON=python:3.12-slim-trixie image_name: cloud-harness/volumemanager title: Volumemanager working_directory: ./applications/volumemanager/server @@ -455,6 +540,11 @@ steps: build_arguments: - NOCACHE=${{CF_BUILD_ID}} - CLOUDHARNESS_FLASK=${{REGISTRY}}/cloud-harness/cloudharness-flask:${{CLOUDHARNESS_FLASK_TAG}} + - KEYCLOAK=quay.io/keycloak/keycloak:26.5 + - GOLANG=golang:1.26 + - ROCKYLINUX=rockylinux/rockylinux:10.1-minimal + - NODE=node:22-alpine + - PYTHON=python:3.12-slim-trixie image_name: cloud-harness/workflows title: Workflows working_directory: ./applications/workflows/server @@ -483,6 +573,11 @@ steps: build_arguments: - NOCACHE=${{CF_BUILD_ID}} - SAMPLES=${{REGISTRY}}/cloud-harness/sampleapp:${{SAMPLES_TAG}} + - KEYCLOAK=quay.io/keycloak/keycloak:26.5 + - GOLANG=golang:1.26 + - ROCKYLINUX=rockylinux/rockylinux:10.1-minimal + - NODE=node:22-alpine + - PYTHON=python:3.12-slim-trixie image_name: cloud-harness/sampleapp-sum title: Samples sum working_directory: ./applications/samples/tasks/sum @@ -519,7 +614,7 @@ steps: kube_context: ${{CLUSTER_NAME}} namespace: test-${{NAMESPACE_BASENAME}} chart_version: ${{CF_SHORT_REVISION}} - cmd_ps: --timeout 600s --create-namespace + cmd_ps: --timeout 200s --create-namespace custom_value_files: - ./deployment/helm/values.yaml custom_values: @@ -532,11 +627,11 @@ steps: commands: - kubectl config use-context ${{CLUSTER_NAME}} - kubectl config set-context --current --namespace=test-${{NAMESPACE_BASENAME}} + - kubectl rollout status statefulset/samples - kubectl rollout status deployment/common - - kubectl rollout status deployment/workflows - kubectl rollout status deployment/volumemanager - - kubectl rollout status statefulset/samples - kubectl rollout status deployment/accounts + - kubectl rollout status deployment/workflows - sleep 60 tests_nfs_failover: stage: qa diff --git a/docs/base-common-images.md b/docs/base-common-images.md index 95a173c13..d1c7bdeda 100644 --- a/docs/base-common-images.md +++ b/docs/base-common-images.md @@ -81,7 +81,7 @@ Here is an example of a declared mapping to change the base image for Python bas ```yaml # value-template.yaml -image_sources: +source_images: NODE: "mybaseimg:14.5" PYTHON: "myotherimage:15.1" ``` diff --git a/tools/deployment-cli-tools/ch_cli_tools/codefresh.py b/tools/deployment-cli-tools/ch_cli_tools/codefresh.py index d5b7b91f1..08b81dead 100644 --- a/tools/deployment-cli-tools/ch_cli_tools/codefresh.py +++ b/tools/deployment-cli-tools/ch_cli_tools/codefresh.py @@ -14,7 +14,7 @@ from .configurationgenerator import KEY_APPS, KEY_TASK_IMAGES, KEY_TEST_IMAGES from .secrets import is_cloudharness_managed, is_secret_config, secret_value from .utils import check_image_exists_in_registry, find_dockerfiles_paths, get_app_relative_to_base_path, guess_build_dependencies_from_dockerfile, \ - get_template, dict_merge, app_name_from_path, clean_path, strip_registry_tag + get_template, dict_merge, app_name_from_path, clean_path, strip_registry_tag, get_image_source from cloudharness_utils.testing.api import get_api_filename, get_schemathesis_command, get_urls_from_api_file logging.getLogger().setLevel(logging.INFO) @@ -264,6 +264,16 @@ def add_arg_dependencies(dependencies): except (KeyError, AttributeError): add_arg_dependencies(helm_values['task-images']) + source_images = get_image_source(helm_values) + if source_images: + existing_arg_keys = {arg.split('=', 1)[0]: i for i, arg in enumerate(build['build_arguments'])} + for key, value in source_images.items(): + arg = f"{key}={value}" + if key in existing_arg_keys: + build['build_arguments'][existing_arg_keys[key]] = arg + else: + build['build_arguments'].append(arg) + when_condition = existing_build_when_condition(tag) build["when"] = when_condition return build diff --git a/tools/deployment-cli-tools/ch_cli_tools/skaffold.py b/tools/deployment-cli-tools/ch_cli_tools/skaffold.py index 17ca8893f..c7ecccfda 100644 --- a/tools/deployment-cli-tools/ch_cli_tools/skaffold.py +++ b/tools/deployment-cli-tools/ch_cli_tools/skaffold.py @@ -10,7 +10,8 @@ BASE_IMAGES_PATH, STATIC_IMAGES_PATH, HELM_ENGINE, COMPOSE_ENGINE from .helm import KEY_APPS, KEY_HARNESS, KEY_DEPLOYMENT, KEY_TASK_IMAGES from .utils import get_template, dict_merge, find_dockerfiles_paths, app_name_from_path, yaml, \ - find_file_paths, guess_build_dependencies_from_dockerfile, get_json_template, clean_image_name + find_file_paths, guess_build_dependencies_from_dockerfile, get_json_template, clean_image_name, \ + get_image_source from . import HERE @@ -334,7 +335,3 @@ def get_additional_build_args(helm_values: HarnessMainConfig, app_key: str) -> d return helm_values.apps[app_key].harness.dockerfile.build_args except (KeyError, AttributeError): return {} - - -def get_image_source(helm_values: HarnessMainConfig) -> dict[str, str]: - return helm_values.get("source_images", {}) diff --git a/tools/deployment-cli-tools/ch_cli_tools/tilt.py b/tools/deployment-cli-tools/ch_cli_tools/tilt.py index 9222ab0e2..3ae3d57c2 100644 --- a/tools/deployment-cli-tools/ch_cli_tools/tilt.py +++ b/tools/deployment-cli-tools/ch_cli_tools/tilt.py @@ -8,7 +8,7 @@ STATIC_IMAGES_PATH from .helm import KEY_TASK_IMAGES from .utils import find_dockerfiles_paths, app_name_from_path, get_image_name, \ - guess_build_dependencies_from_dockerfile + guess_build_dependencies_from_dockerfile, get_image_source from . import HERE, CH_ROOT @@ -73,6 +73,8 @@ def build_artifact( for req in requirements: build_args.update({req.replace('-', '_').upper(): get_image_tag(req)}) + build_args.update(get_image_source(helm_values)) + image_name = get_image_tag(app_key) artifact_spec = { diff --git a/tools/deployment-cli-tools/ch_cli_tools/utils.py b/tools/deployment-cli-tools/ch_cli_tools/utils.py index 30ead39a5..765d8e9b8 100644 --- a/tools/deployment-cli-tools/ch_cli_tools/utils.py +++ b/tools/deployment-cli-tools/ch_cli_tools/utils.py @@ -590,6 +590,10 @@ def get_dockerfile_baseimg_args(filename: str) -> dict[str, str]: return args +def get_image_source(helm_values) -> dict[str, str]: + return helm_values.get("source_images", {}) + + def check_response_200(endpoint_url, headers=None): resp = requests.get(endpoint_url, headers=headers, timeout=5) return resp.status_code == 200 diff --git a/tools/deployment-cli-tools/tests/test_codefresh.py b/tools/deployment-cli-tools/tests/test_codefresh.py index 863f9750e..93f6d2dd8 100644 --- a/tools/deployment-cli-tools/tests/test_codefresh.py +++ b/tools/deployment-cli-tools/tests/test_codefresh.py @@ -1082,3 +1082,62 @@ def test_codefresh_secret_managers_from_parsed_values(): assert is_secret_config(definitions['rich_secret']) assert not is_secret_config(definitions['plain_secret']) assert secret_value(definitions['static_random']) == '' + + +def test_codefresh_source_images_build_args(): + """source_images entries (global base-image overrides) must be injected as build + arguments for every build step, matching the behaviour of skaffold and Tilt.""" + values = create_helm_chart( + [CLOUDHARNESS_ROOT, RESOURCES], + output_path=OUT, + include=['samples', 'myapp'], + exclude=['events'], + domain="my.local", + namespace='test', + env='dev', + local=False, + tag=1, + registry='reg' + ) + + source_images = values.get("source_images") + assert source_images["KEYCLOAK"] == "myregistry.mykeycloak:99.9" + assert source_images["NODE"] == "node:22-alpine" + + # CLOUDHARNESS_FLASK is also a build dependency of samples and myapp: a source_images + # override for it must win over the dependency-derived build argument, not be shadowed by it. + values["source_images"] = dict(source_images) | {"CLOUDHARNESS_FLASK": "myoverride/cloudharness-flask:9.9"} + + try: + root_paths = preprocess_build_overrides( + root_paths=[CLOUDHARNESS_ROOT, RESOURCES], + helm_values=values, + merge_build_path=BUILD_MERGE_DIR + ) + build_included = [app['harness']['name'] + for app in values['apps'].values() if 'harness' in app] + + cf = create_codefresh_deployment_scripts(root_paths, include=build_included, + envs=["dev"], + base_image_name=values['name'], + helm_values=values, save=False) + + def get_build_step(name): + for step_name, step in cf['steps'].items(): + if step_name.startswith(CD_BUILD_STEP_PARALLEL) and name in step.get('steps', {}): + return step['steps'][name] + return None + + samples_build = get_build_step('samples') + assert samples_build is not None, "samples build step not found" + assert "KEYCLOAK=myregistry.mykeycloak:99.9" in samples_build['build_arguments'] + assert "NODE=node:22-alpine" in samples_build['build_arguments'] + assert "CLOUDHARNESS_FLASK=myoverride/cloudharness-flask:9.9" in samples_build['build_arguments'] + + myapp_build = get_build_step('myapp') + assert myapp_build is not None, "myapp build step not found" + assert "KEYCLOAK=myregistry.mykeycloak:99.9" in myapp_build['build_arguments'] + assert "NODE=node:22-alpine" in myapp_build['build_arguments'] + assert "CLOUDHARNESS_FLASK=myoverride/cloudharness-flask:9.9" in myapp_build['build_arguments'] + finally: + shutil.rmtree(BUILD_MERGE_DIR, ignore_errors=True) diff --git a/tools/deployment-cli-tools/tests/test_tilt.py b/tools/deployment-cli-tools/tests/test_tilt.py new file mode 100644 index 000000000..9ed09817c --- /dev/null +++ b/tools/deployment-cli-tools/tests/test_tilt.py @@ -0,0 +1,74 @@ +import ast +import os +import re +import shutil + +from ch_cli_tools.helm import * +from ch_cli_tools.preprocessing import preprocess_build_overrides +from ch_cli_tools.tilt import create_tilt_configuration + +HERE = os.path.dirname(os.path.realpath(__file__)) +RESOURCES = os.path.join(HERE, 'resources') + +CLOUDHARNESS_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) + + +def test_tilt_imgarg_retrieval(tmp_path): + """source_images entries (global base-image overrides) must be injected as build + arguments for every docker_build call in the generated Tiltfile, matching skaffold.""" + out_folder = tmp_path / "test_tilt_imgarg_retrieval" + + values = create_helm_chart( + [CLOUDHARNESS_ROOT, RESOURCES], + output_path=out_folder, + include=["samples", "myapp"], + domain="my.local", + namespace="test", + env="nreg", + local=False, + tag=1, + registry="reg", + ) + + source_images = values.get("source_images") + assert source_images["KEYCLOAK"] == "myregistry.myapp:15.3" + assert source_images["NODE"] == "node:22-alpine" + + # CLOUDHARNESS_FLASK is also a build dependency of samples and myapp: a source_images + # override for it must win over the dependency-derived build arg, not be shadowed by it. + values["source_images"] = dict(source_images) | {"CLOUDHARNESS_FLASK": "myoverride/cloudharness-flask:9.9"} + + BUILD_DIR = "/tmp/build_tilt_imgarg" + root_paths = preprocess_build_overrides( + root_paths=[CLOUDHARNESS_ROOT, RESOURCES], + helm_values=values, + merge_build_path=BUILD_DIR, + ) + + create_tilt_configuration( + root_paths=root_paths, helm_values=values, output_path=str(out_folder) + ) + + tiltfile_content = (out_folder / "Tiltfile").read_text() + + def get_buildargs(name) -> dict: + pattern = re.compile( + r"docker_build\(ref='[^']*', context='([^']*)', dockerfile='[^']*', build_args=(\{[^}]*\})" + ) + for context, args_repr in pattern.findall(tiltfile_content): + if f"applications/{name}" in context: + return ast.literal_eval(args_repr) + return {} + + samples_buildargs = get_buildargs("samples") + assert samples_buildargs["KEYCLOAK"] == "myregistry.myapp:15.3" + assert samples_buildargs["NODE"] == "node:22-alpine" + assert samples_buildargs["CLOUDHARNESS_FLASK"] == "myoverride/cloudharness-flask:9.9" + + myapp_buildargs = get_buildargs("myapp") + assert myapp_buildargs["KEYCLOAK"] == "myregistry.myapp:15.3" + assert myapp_buildargs["NODE"] == "node:22-alpine" + assert myapp_buildargs["CLOUDHARNESS_FLASK"] == "myoverride/cloudharness-flask:9.9" + + shutil.rmtree(out_folder) + shutil.rmtree(BUILD_DIR)