From d0ae384a2fa07ca512c7592df6234166da715b38 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Mon, 20 Oct 2025 15:08:31 +0200 Subject: [PATCH 1/6] Add a build variant to release chart to OCI registry --- .evergreen-functions.yml | 3 ++ .evergreen-release.yml | 19 ++++++++++ scripts/release/helm_registry_login.py | 48 ++++++++++++++++++++++++-- scripts/release/publish_helm_chart.py | 22 ++++++++---- 4 files changed, 83 insertions(+), 9 deletions(-) diff --git a/.evergreen-functions.yml b/.evergreen-functions.yml index d7b2414c3..a87637cd6 100644 --- a/.evergreen-functions.yml +++ b/.evergreen-functions.yml @@ -227,6 +227,9 @@ functions: type: setup params: working_dir: src/github.com/mongodb/mongodb-kubernetes + include_expansions_in_env: + - quay_prod_username + - quay_prod_robot_token add_to_path: - ${workdir}/bin - ${PROJECT_DIR}/bin diff --git a/.evergreen-release.yml b/.evergreen-release.yml index 592adf44e..9602fc40a 100644 --- a/.evergreen-release.yml +++ b/.evergreen-release.yml @@ -129,6 +129,16 @@ tasks: - func: clone - func: python_venv - func: create_chart_release_pr + + - name: release_chart_to_oci_registry + tags: [ "release_chart_to_oci_registry" ] + commands: + - func: clone + - func: python_venv + - func: setup_kubectl + - func: setup_aws + - func: helm_registry_login + - func: publish_helm_chart ### Release build variants buildvariants: @@ -255,3 +265,12 @@ buildvariants: allowed_requesters: [ "patch", "github_tag" ] tasks: - name: create_chart_release_pr + + - name: release_chart_to_oci_registry + display_name: release_chart_to_oci_registry + tags: [ "release" ] + run_on: + - release-ubuntu2404-small # This is required for CISA attestation https://jira.mongodb.org/browse/DEVPROD-17780 + allowed_requesters: [ "patch", "github_tag" ] + tasks: + - name: release_chart_to_oci_registry diff --git a/scripts/release/helm_registry_login.py b/scripts/release/helm_registry_login.py index 4abf08864..96e807252 100644 --- a/scripts/release/helm_registry_login.py +++ b/scripts/release/helm_registry_login.py @@ -6,8 +6,11 @@ from lib.base_logger import logger from scripts.release.build.build_info import load_build_info +BUILD_SCENARIO_RELEASE="release" +QUAY_USERNAME_ENV_VAR = "quay_prod_username" +QUAY_PASSWORD_ENV_VAR = "quay_prod_robot_token" -def helm_registry_login(helm_registry: str, region: str): +def helm_registry_login_to_nonrelease(helm_registry: str, region: str): logger.info(f"Attempting to log into ECR registry: {helm_registry}, using helm registry login.") aws_command = ["aws", "ecr", "get-login-password", "--region", region] @@ -67,7 +70,48 @@ def main(): registry = build_info.helm_charts["mongodb-kubernetes"].registry region = build_info.helm_charts["mongodb-kubernetes"].region - return helm_registry_login(registry, region) + + if build_scenario == BUILD_SCENARIO_RELEASE: + helm_registry_login_to_release(registry) + + return helm_registry_login_to_nonrelease(registry, region) + +def helm_registry_login_to_release(registry): + username = os.environ.get(QUAY_USERNAME_ENV_VAR) + password = os.environ.get(QUAY_PASSWORD_ENV_VAR) + + if not username: + raise Exception(f"Env var {QUAY_USERNAME_ENV_VAR} must be set with the quay username.") + if not password: + raise Exception(f"Env var {QUAY_PASSWORD_ENV_VAR} must be set with the quay password.") + + command = [ + "helm", + "registry", + "login", + "--username", username, + "--password-stdin", + registry + ] + + try: + result = subprocess.run( + command, + input=password, # Pass the password as input bytes + capture_output=True, + text=True, + check=False # Do not raise an exception on non-zero exit code + ) + + if result.returncode == 0: + logger.info(f"Successfully logged into helm continer registry {registry}.") + else: + raise Exception(f"Helm registry login failed to {registry}. Stdout: {result.stderr.strip()}, Stderr: {result.stderr.strip()}") + + except FileNotFoundError: + raise Exception("Error: 'helm' command not found. Ensure Helm CLI is installed and in your PATH.") + except Exception as e: + raise Exception(f"An unexpected error occurred during execution: {e}") if __name__ == "__main__": diff --git a/scripts/release/publish_helm_chart.py b/scripts/release/publish_helm_chart.py index 93aad9db9..11427a8e5 100644 --- a/scripts/release/publish_helm_chart.py +++ b/scripts/release/publish_helm_chart.py @@ -7,6 +7,7 @@ from lib.base_logger import logger from scripts.release.build.build_info import * +from scripts.release.helm_registry_login import BUILD_SCENARIO_RELEASE CHART_DIR = "helm_chart" @@ -28,7 +29,7 @@ def run_command(command: list[str]): # update_chart_and_get_metadata updates the helm chart's Chart.yaml and sets the version # to either evg patch id or commit which is set in OPERATOR_VERSION. -def update_chart_and_get_metadata(chart_dir: str) -> tuple[str, str]: +def update_chart_and_get_metadata(chart_dir: str, build_scenario) -> tuple[str, str]: chart_path = os.path.join(chart_dir, "Chart.yaml") version_id = os.environ.get("OPERATOR_VERSION") if not version_id: @@ -36,9 +37,6 @@ def update_chart_and_get_metadata(chart_dir: str) -> tuple[str, str]: "Error: Environment variable 'OPERATOR_VERSION' must be set to determine the chart version to publish." ) - new_version = f"0.0.0+{version_id}" - logger.info(f"New helm chart version will be: {new_version}") - if not os.path.exists(chart_path): raise FileNotFoundError( f"Error: Chart.yaml not found in directory '{chart_dir}'. " @@ -52,7 +50,17 @@ def update_chart_and_get_metadata(chart_dir: str) -> tuple[str, str]: chart_name = data.get("name") if not chart_name: raise ValueError("Chart.yaml is missing required 'name' field.") + except Exception as e: + raise Exception(f"Unable to load Chart.yaml from dir {chart_path}") + # if build_scenario is release, the chart.yaml would already have correct chart version + if build_scenario == BUILD_SCENARIO_RELEASE: + return chart_name, version_id + + new_version = f"0.0.0+{version_id}" + logger.info(f"New helm chart version will be: {new_version}") + + try: data["version"] = new_version with open(chart_path, "w") as f: @@ -79,10 +87,10 @@ def get_oci_registry(chart_info: HelmChartInfo) -> str: return oci_registry -def publish_helm_chart(chart_info: HelmChartInfo): +def publish_helm_chart(chart_info: HelmChartInfo, build_scenario): try: oci_registry = get_oci_registry(chart_info) - chart_name, chart_version = update_chart_and_get_metadata(CHART_DIR) + chart_name, chart_version = update_chart_and_get_metadata(CHART_DIR, build_scenario) tgz_filename = f"{chart_name}-{chart_version}.tgz" logger.info(f"Packaging chart: {chart_name} with Version: {chart_version}") @@ -108,7 +116,7 @@ def main(): build_scenario = args.build_scenario build_info = load_build_info(build_scenario) - return publish_helm_chart(build_info.helm_charts["mongodb-kubernetes"]) + return publish_helm_chart(build_info.helm_charts["mongodb-kubernetes"], build_scenario) if __name__ == "__main__": From 9459e3c2fd36b33c3afa9b37f4472644262981de Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Mon, 20 Oct 2025 21:27:08 +0200 Subject: [PATCH 2/6] Run precommit --- scripts/release/helm_registry_login.py | 29 ++++++++++++-------------- scripts/release/publish_helm_chart.py | 2 +- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/scripts/release/helm_registry_login.py b/scripts/release/helm_registry_login.py index 96e807252..8a86a98c6 100644 --- a/scripts/release/helm_registry_login.py +++ b/scripts/release/helm_registry_login.py @@ -6,10 +6,11 @@ from lib.base_logger import logger from scripts.release.build.build_info import load_build_info -BUILD_SCENARIO_RELEASE="release" +BUILD_SCENARIO_RELEASE = "release" QUAY_USERNAME_ENV_VAR = "quay_prod_username" QUAY_PASSWORD_ENV_VAR = "quay_prod_robot_token" + def helm_registry_login_to_nonrelease(helm_registry: str, region: str): logger.info(f"Attempting to log into ECR registry: {helm_registry}, using helm registry login.") @@ -76,6 +77,7 @@ def main(): return helm_registry_login_to_nonrelease(registry, region) + def helm_registry_login_to_release(registry): username = os.environ.get(QUAY_USERNAME_ENV_VAR) password = os.environ.get(QUAY_PASSWORD_ENV_VAR) @@ -85,28 +87,23 @@ def helm_registry_login_to_release(registry): if not password: raise Exception(f"Env var {QUAY_PASSWORD_ENV_VAR} must be set with the quay password.") - command = [ - "helm", - "registry", - "login", - "--username", username, - "--password-stdin", - registry - ] + command = ["helm", "registry", "login", "--username", username, "--password-stdin", registry] try: result = subprocess.run( - command, - input=password, # Pass the password as input bytes - capture_output=True, - text=True, - check=False # Do not raise an exception on non-zero exit code - ) + command, + input=password, # Pass the password as input bytes + capture_output=True, + text=True, + check=False, # Do not raise an exception on non-zero exit code + ) if result.returncode == 0: logger.info(f"Successfully logged into helm continer registry {registry}.") else: - raise Exception(f"Helm registry login failed to {registry}. Stdout: {result.stderr.strip()}, Stderr: {result.stderr.strip()}") + raise Exception( + f"Helm registry login failed to {registry}. Stdout: {result.stderr.strip()}, Stderr: {result.stderr.strip()}" + ) except FileNotFoundError: raise Exception("Error: 'helm' command not found. Ensure Helm CLI is installed and in your PATH.") diff --git a/scripts/release/publish_helm_chart.py b/scripts/release/publish_helm_chart.py index 11427a8e5..049550325 100644 --- a/scripts/release/publish_helm_chart.py +++ b/scripts/release/publish_helm_chart.py @@ -56,7 +56,7 @@ def update_chart_and_get_metadata(chart_dir: str, build_scenario) -> tuple[str, # if build_scenario is release, the chart.yaml would already have correct chart version if build_scenario == BUILD_SCENARIO_RELEASE: return chart_name, version_id - + new_version = f"0.0.0+{version_id}" logger.info(f"New helm chart version will be: {new_version}") From 801168710c075325915dfb034d57f4b57bed1eec Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Tue, 21 Oct 2025 11:59:00 +0200 Subject: [PATCH 3/6] Early return after logging in to release workflow --- scripts/release/helm_registry_login.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release/helm_registry_login.py b/scripts/release/helm_registry_login.py index 8a86a98c6..b9a349b45 100644 --- a/scripts/release/helm_registry_login.py +++ b/scripts/release/helm_registry_login.py @@ -73,7 +73,7 @@ def main(): region = build_info.helm_charts["mongodb-kubernetes"].region if build_scenario == BUILD_SCENARIO_RELEASE: - helm_registry_login_to_release(registry) + return helm_registry_login_to_release(registry) return helm_registry_login_to_nonrelease(registry, region) From e6f7e34936f017cd8956b14674fa750c8faf0288 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Thu, 23 Oct 2025 19:09:42 +0200 Subject: [PATCH 4/6] Rename `version_id` to `version` --- scripts/release/publish_helm_chart.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/release/publish_helm_chart.py b/scripts/release/publish_helm_chart.py index 049550325..ea0e36945 100644 --- a/scripts/release/publish_helm_chart.py +++ b/scripts/release/publish_helm_chart.py @@ -31,8 +31,8 @@ def run_command(command: list[str]): # to either evg patch id or commit which is set in OPERATOR_VERSION. def update_chart_and_get_metadata(chart_dir: str, build_scenario) -> tuple[str, str]: chart_path = os.path.join(chart_dir, "Chart.yaml") - version_id = os.environ.get("OPERATOR_VERSION") - if not version_id: + version = os.environ.get("OPERATOR_VERSION") + if not version: raise ValueError( "Error: Environment variable 'OPERATOR_VERSION' must be set to determine the chart version to publish." ) @@ -55,9 +55,9 @@ def update_chart_and_get_metadata(chart_dir: str, build_scenario) -> tuple[str, # if build_scenario is release, the chart.yaml would already have correct chart version if build_scenario == BUILD_SCENARIO_RELEASE: - return chart_name, version_id + return chart_name, version - new_version = f"0.0.0+{version_id}" + new_version = f"0.0.0+{version}" logger.info(f"New helm chart version will be: {new_version}") try: From f09afa834029311e3553650f74b8148322f26a1c Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Fri, 24 Oct 2025 11:19:57 +0200 Subject: [PATCH 5/6] Address review comments --- scripts/release/helm_registry_login.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/release/helm_registry_login.py b/scripts/release/helm_registry_login.py index b9a349b45..b79df6bdc 100644 --- a/scripts/release/helm_registry_login.py +++ b/scripts/release/helm_registry_login.py @@ -9,9 +9,10 @@ BUILD_SCENARIO_RELEASE = "release" QUAY_USERNAME_ENV_VAR = "quay_prod_username" QUAY_PASSWORD_ENV_VAR = "quay_prod_robot_token" +QUAY_REGISTRY = "quay.io" -def helm_registry_login_to_nonrelease(helm_registry: str, region: str): +def helm_registry_login_to_ecr(helm_registry: str, region: str): logger.info(f"Attempting to log into ECR registry: {helm_registry}, using helm registry login.") aws_command = ["aws", "ecr", "get-login-password", "--region", region] @@ -72,13 +73,13 @@ def main(): registry = build_info.helm_charts["mongodb-kubernetes"].registry region = build_info.helm_charts["mongodb-kubernetes"].region - if build_scenario == BUILD_SCENARIO_RELEASE: - return helm_registry_login_to_release(registry) + if registry == QUAY_REGISTRY: + return helm_registry_login_to_quay(registry) - return helm_registry_login_to_nonrelease(registry, region) + return helm_registry_login_to_ecr(registry, region) -def helm_registry_login_to_release(registry): +def helm_registry_login_to_quay(registry): username = os.environ.get(QUAY_USERNAME_ENV_VAR) password = os.environ.get(QUAY_PASSWORD_ENV_VAR) From b863e04ed444642f83113a188b5c5462261d6e5a Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Fri, 24 Oct 2025 11:59:39 +0200 Subject: [PATCH 6/6] Fix semgrep issues --- scripts/release/helm_registry_login.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/release/helm_registry_login.py b/scripts/release/helm_registry_login.py index b79df6bdc..4f2d6e0d2 100644 --- a/scripts/release/helm_registry_login.py +++ b/scripts/release/helm_registry_login.py @@ -2,6 +2,7 @@ import os import subprocess import sys +from shlex import quote from lib.base_logger import logger from scripts.release.build.build_info import load_build_info @@ -88,12 +89,13 @@ def helm_registry_login_to_quay(registry): if not password: raise Exception(f"Env var {QUAY_PASSWORD_ENV_VAR} must be set with the quay password.") - command = ["helm", "registry", "login", "--username", username, "--password-stdin", registry] + # using quote will help us avoid command injectin issues, was reported by semggrep in PR + command = ["helm", "registry", "login", "--username", quote(username), "--password-stdin", quote(registry)] try: result = subprocess.run( command, - input=password, # Pass the password as input bytes + input=quote(password), # Pass the password as input bytes capture_output=True, text=True, check=False, # Do not raise an exception on non-zero exit code