diff --git a/applications/accounts/Dockerfile b/applications/accounts/Dockerfile index ff1a1b736..bed3f0fda 100644 --- a/applications/accounts/Dockerfile +++ b/applications/accounts/Dockerfile @@ -1,4 +1,5 @@ -FROM quay.io/keycloak/keycloak:26.5 +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 b020666d7..bd57812dd 100644 --- a/applications/nfsserver/Dockerfile +++ b/applications/nfsserver/Dockerfile @@ -1,5 +1,7 @@ # compile provisioner -FROM golang:1.26 AS provisioner +ARG GOLANG=golang:1.26 +ARG ROCKYLINUX=rockylinux/rockylinux:10.1-minimal +FROM $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 $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..e2845f312 100644 --- a/applications/sentry/Dockerfile +++ b/applications/sentry/Dockerfile @@ -1,4 +1,5 @@ -FROM sentry:9.1.2 +ARG SENTRY=sentry:9.1.2 +FROM $SENTRY ADD sentryrunner.sh ./sentryrunner.sh RUN chmod +x ./sentryrunner.sh 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 6d0ac6bf0..d1c7bdeda 100644 --- a/docs/base-common-images.md +++ b/docs/base-common-images.md @@ -60,3 +60,75 @@ 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 to change the base image for Python based apps and Node base apps: + +```yaml +# value-template.yaml +source_images: + NODE: "mybaseimg:14.5" + PYTHON: "myotherimage:15.1" +``` + +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 + +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..159fc7c31 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 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 0a4d02061..0497d3cfd 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 NODE=node:22-alpine +FROM $NODE diff --git a/test/test-e2e/Dockerfile b/test/test-e2e/Dockerfile index 7f0ca0215..31e987e4b 100644 --- a/test/test-e2e/Dockerfile +++ b/test/test-e2e/Dockerfile @@ -1,18 +1,19 @@ -FROM ghcr.io/puppeteer/puppeteer:24 +ARG PUPPETEER=ghcr.io/puppeteer/puppeteer:24 +FROM $PUPPETEER 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/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/configurationgenerator.py b/tools/deployment-cli-tools/ch_cli_tools/configurationgenerator.py index 75f8b5e89..eec0d8299 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 2f551434a..8a3a07be0 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 @@ -257,8 +258,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' @@ -315,7 +317,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( @@ -415,7 +417,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 8d24e00c8..989fcbcdf 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 @@ -117,6 +118,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: @@ -131,6 +135,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 @@ -155,6 +162,28 @@ 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] @@ -172,7 +201,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] @@ -288,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] @@ -308,24 +337,21 @@ 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}') - 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) @@ -334,10 +360,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: @@ -349,8 +376,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[ @@ -359,17 +386,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( @@ -408,7 +435,7 @@ 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. @@ -417,7 +444,7 @@ def finalize_app_values(self, app_name, app_path, app_values, base_image_name=No 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]) @@ -434,7 +461,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[ @@ -445,19 +473,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 2229bdc3f..7d710b240 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 @@ -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_source_images(helm_values) + 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,11 @@ 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 + try: + return helm_values.apps[app_key].harness.dockerfile.build_args + except (KeyError, AttributeError): + return {} - 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 +def get_source_images(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 a3deb37dd..765d8e9b8 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 @@ -33,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) @@ -64,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 @@ -557,6 +558,42 @@ 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 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/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/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/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..97b267d8e --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources/deployment-configuration/values-template-nreg.yaml @@ -0,0 +1,13 @@ +mainapp: myapp +domain: myapp.local +name: testprojectname +a: b +database: + auto: true + +source_images: + KEYCLOAK: "myregistry.myapp:15.3" + +events: + kafka: + image: nodocker.io/apache/kafka:4.0.2 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_helm.py b/tools/deployment-cli-tools/tests/test_helm.py index 90914b18e..928dc3804 100644 --- a/tools/deployment-cli-tools/tests/test_helm.py +++ b/tools/deployment-cli-tools/tests/test_helm.py @@ -7,6 +7,15 @@ 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))) @@ -183,6 +192,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": "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' @@ -1384,3 +1405,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 diff --git a/tools/deployment-cli-tools/tests/test_skaffold.py b/tools/deployment-cli-tools/tests/test_skaffold.py index e5fdafbc8..c3006bf0b 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__)) @@ -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["KEYCLOAK"] == "myregistry.myapp:15.3" + assert source_images["NODE"] == "node:22-alpine" + assert get_source_images(values) == { + "KEYCLOAK": "myregistry.myapp:15.3", + "NODE": "node:22-alpine", + } + + +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 samples_buildargs["KEYCLOAK"] == "myregistry.myapp:15.3" + assert samples_buildargs["NODE"] == "node:22-alpine" + + myapp_buildargs = get_buildargs("myapp") + assert myapp_buildargs["KEYCLOAK"] == "myregistry.myapp:15.3" + assert myapp_buildargs["NODE"] == "node:22-alpine" 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) 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