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..4f2d6e0d2 100644 --- a/scripts/release/helm_registry_login.py +++ b/scripts/release/helm_registry_login.py @@ -2,12 +2,18 @@ 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 +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(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] @@ -67,7 +73,45 @@ 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 registry == QUAY_REGISTRY: + return helm_registry_login_to_quay(registry) + + return helm_registry_login_to_ecr(registry, region) + + +def helm_registry_login_to_quay(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.") + + # 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=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 + ) + + 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..ea0e36945 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,17 +29,14 @@ 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: + 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." ) - 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 + new_version = f"0.0.0+{version}" + 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__":