diff --git a/applications/accounts/Dockerfile b/applications/accounts/Dockerfile index ff1a1b736..2f14718f2 100644 --- a/applications/accounts/Dockerfile +++ b/applications/accounts/Dockerfile @@ -1,4 +1,5 @@ -FROM quay.io/keycloak/keycloak:26.5 +ARG CLOUDHARNESS_ROOT_BASE=quay.io/keycloak/keycloak:26.5 +FROM $CLOUDHARNESS_ROOT_BASE EXPOSE 9000 EXPOSE 8080 diff --git a/applications/nfsserver/Dockerfile b/applications/nfsserver/Dockerfile index b020666d7..1b7f590bd 100644 --- a/applications/nfsserver/Dockerfile +++ b/applications/nfsserver/Dockerfile @@ -1,5 +1,7 @@ # compile provisioner -FROM golang:1.26 AS provisioner +ARG CLOUDHARNESS_BASE_GOLANG=golang:1.26 +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,10 +19,9 @@ 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.2-minimal +FROM $CLOUDHARNESS_BASE_ROCKYLINUX -RUN microdnf update -y \ - && microdnf install -y \ +RUN microdnf install -y \ e2fsprogs \ rpcbind \ nfs-utils \ diff --git a/applications/sentry/Dockerfile b/applications/sentry/Dockerfile index 2607f5172..99b310b3b 100644 --- a/applications/sentry/Dockerfile +++ b/applications/sentry/Dockerfile @@ -1,4 +1,5 @@ -FROM sentry:9.1.2 +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 58cb04bae..95440d548 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_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 0a4d02061..f7bdbdbca 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_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 b4540072c..943185af0 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 = "CLOUDHARNESS_ROOT_BASE" CD_BUILD_STEP_PARALLEL = 'build_application_images' diff --git a/test/test-e2e/Dockerfile b/test/test-e2e/Dockerfile index 7f0ca0215..1c1a09bc2 100644 --- a/test/test-e2e/Dockerfile +++ b/test/test-e2e/Dockerfile @@ -1,18 +1,19 @@ -FROM ghcr.io/puppeteer/puppeteer:24 +ARG CLOUDHARNESS_ROOT_BASE=ghcr.io/puppeteer/puppeteer:24 +FROM $CLOUDHARNESS_ROOT_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"] 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"