Skip to content

Commit ea816dd

Browse files
[CLOUDP-350669] Publish helm chart to OCI registry for dev/staging workflows (#507)
# Summary In an effort to start using OCI compatible container registries as our helm chart repos, this PR makes the change package and publish our helm chart to ECR registry. A new task `publish_helm_chart` is introduced in the build variant `init_test_run` which packages and publishes the chart. The details about the versioning of the chart can be found in the document [here](https://docs.google.com/document/d/1eJ8iKsI0libbpcJakGjxcPfbrTn8lmcZDbQH1UqMR_g/edit?tab=t.gg5ble8qlesq). There were two wrappers (shell scripts) introduced as part of this and the reason was we wanted to call the python scripts (`helm_registry_login.py` and `publish_helm_chart.py`) from `.evergreen-function.yaml` passing an argument that's build scenario. Build scenario is already set as env var in the evg host, but there isn't a way to do this, in other words, we couldn't do below from the `.evergreen-functions.yaml` file ``` binary: binary: run_python.sh scripts/release/helm_registry_login.sh $build_scenario_that_is_set_as_env_on_host ``` And because of that we had to write the wrapper that would call the python script with correct argument after reading the env var. ## Proof of Work [patch](https://spruce.mongodb.com/task/mongodb_kubernetes_init_test_run_publish_helm_chart_patch_749c1a3f3302ef2bc3f70ed683dd2e522bd94bef_68e91082554272000716941c_25_10_10_13_56_20/logs?execution=0) and [staging](https://spruce.mongodb.com/task/mongodb_kubernetes_init_test_run_publish_helm_chart_patch_749c1a3f3302ef2bc3f70ed683dd2e522bd94bef_68e91366a609a6000785b403_25_10_10_14_08_40/logs?execution=0) evg runs. Notice at the end of the log that dev workflow is pushing to diff repository and staging to different. Successful run of `init_test_run` variant in this PR would also work. ## Checklist - [x] Have you linked a jira ticket and/or is the ticket in the title? - [x] Have you checked whether your jira ticket required DOCSP changes? - [x] Have you added changelog file? - use `skip-changelog` label if not needed - refer to [Changelog files and Release Notes](https://github.com/mongodb/mongodb-kubernetes/blob/master/CONTRIBUTING.md#changelog-files-and-release-notes) section in CONTRIBUTING.md for more details
1 parent 7b469c6 commit ea816dd

12 files changed

+279
-11
lines changed

.evergreen-functions.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,16 @@ functions:
222222
working_dir: src/github.com/mongodb/mongodb-kubernetes
223223
binary: scripts/evergreen/setup_docker_sbom.sh
224224

225+
helm_registry_login:
226+
command: subprocess.exec
227+
type: setup
228+
params:
229+
working_dir: src/github.com/mongodb/mongodb-kubernetes
230+
add_to_path:
231+
- ${workdir}/bin
232+
- ${PROJECT_DIR}/bin
233+
binary: scripts/release/helm_registry_login.sh
234+
225235
# Logs into all used registries
226236
configure_docker_auth: &configure_docker_auth
227237
command: subprocess.exec
@@ -491,6 +501,13 @@ functions:
491501
- rh_pyxis
492502
binary: scripts/dev/run_python.sh scripts/preflight_images.py --image ${image_name} --submit "${preflight_submit}"
493503

504+
# publish_helm_chart packages and publishes the MCK helm chart to the OCI container registry
505+
publish_helm_chart:
506+
- command: subprocess.exec
507+
params:
508+
working_dir: src/github.com/mongodb/mongodb-kubernetes
509+
binary: scripts/release/publish_helm_chart.sh
510+
494511
build_multi_cluster_binary:
495512
- command: subprocess.exec
496513
type: setup

.evergreen.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,16 @@ tasks:
453453
- func: setup_building_host
454454
- func: pipeline_version_upgrade_hook
455455

456+
- name: publish_helm_chart
457+
commands:
458+
- func: clone
459+
- func: python_venv
460+
- func: setup_kubectl
461+
- func: setup_aws
462+
- func: prepare_aws
463+
- func: helm_registry_login
464+
- func: publish_helm_chart
465+
456466
- name: prepare_aws
457467
priority: 59
458468
commands:
@@ -1706,6 +1716,7 @@ buildvariants:
17061716
- name: build_readiness_probe_image
17071717
- name: build_version_upgrade_hook_image
17081718
- name: prepare_aws
1719+
- name: publish_helm_chart
17091720

17101721
- name: init_test_run_ibm_power
17111722
display_name: init_test_run_ibm_power

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,6 @@ docs/**/test.sh.run.log
9393
dist
9494
logs
9595
*.run.log
96+
97+
# locally packaged chart
98+
mongodb-kubernetes-*.tgz

build_info.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -379,15 +379,20 @@
379379
"helm-charts": {
380380
"mongodb-kubernetes": {
381381
"patch": {
382-
"repositories": ["268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/helm-charts"]
382+
"registry": "268558157000.dkr.ecr.us-east-1.amazonaws.com",
383+
"region": "us-east-1",
384+
"repository": "dev/mongodb/helm-charts"
383385
},
384386
"staging": {
385387
"sign": true,
386-
"repositories": ["268558157000.dkr.ecr.us-east-1.amazonaws.com/staging/helm-charts"]
388+
"registry": "268558157000.dkr.ecr.us-east-1.amazonaws.com",
389+
"region": "us-east-1",
390+
"repository": "staging/mongodb/helm-charts"
387391
},
388392
"release": {
389393
"sign": true,
390-
"repositories": ["quay.io/mongodb/helm-charts"]
394+
"registry": "quay.io",
395+
"repository": "mongodb/helm-charts"
391396
}
392397
}
393398
}

scripts/release/build/build_info.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ class BinaryInfo:
4040

4141
@dataclass
4242
class HelmChartInfo:
43-
repositories: List[str]
43+
repository: str
44+
registry: str
45+
region: str
4446
sign: bool = False
4547

4648

@@ -106,8 +108,10 @@ def load_build_info(scenario: BuildScenario) -> BuildInfo:
106108
continue
107109

108110
helm_charts[name] = HelmChartInfo(
109-
repositories=scenario_data["repositories"],
111+
repository=scenario_data.get("repository"),
110112
sign=scenario_data.get("sign", False),
113+
registry=scenario_data.get("registry"),
114+
region=scenario_data.get("region")
111115
)
112116

113117
return BuildInfo(images=images, binaries=binaries, helm_charts=helm_charts)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import argparse
2+
import os
3+
import subprocess
4+
import sys
5+
6+
from lib.base_logger import logger
7+
from scripts.release.build.build_info import load_build_info
8+
9+
10+
def helm_registry_login(helm_registry: str, region: str):
11+
logger.info(f"Attempting to log into ECR registry: {helm_registry}, using helm registry login.")
12+
13+
aws_command = ["aws", "ecr", "get-login-password", "--region", region]
14+
15+
# as we can see the password is being provided by stdin, that would mean we will have to
16+
# pipe the aws_command (it figures out the password) into helm_command.
17+
helm_command = ["helm", "registry", "login", "--username", "AWS", "--password-stdin", helm_registry]
18+
19+
try:
20+
logger.info("Starting AWS ECR credential retrieval.")
21+
aws_proc = subprocess.Popen(
22+
aws_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True # Treat input/output as text strings
23+
)
24+
25+
logger.info("Starting Helm registry login.")
26+
helm_proc = subprocess.Popen(
27+
helm_command, stdin=aws_proc.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
28+
)
29+
30+
# Close the stdout stream of aws_proc in the parent process
31+
# to prevent resource leakage (only needed if you plan to do more processing)
32+
aws_proc.stdout.close()
33+
34+
# Wait for the Helm command (helm_proc) to finish and capture its output
35+
helm_stdout, helm_stderr = helm_proc.communicate()
36+
37+
# Wait for the AWS process to finish as well
38+
aws_proc.wait()
39+
40+
if aws_proc.returncode != 0:
41+
_, aws_stderr = aws_proc.communicate()
42+
raise Exception(f"aws command to get password failed. Error: {aws_stderr}")
43+
44+
if helm_proc.returncode == 0:
45+
logger.info("Login to helm registry was successful.")
46+
logger.info(helm_stdout.strip())
47+
else:
48+
raise Exception(
49+
f"Login to helm registry failed, Exit code: {helm_proc.returncode}, Error: {helm_stderr.strip()}"
50+
)
51+
52+
except FileNotFoundError as e:
53+
# This catches errors if 'aws' or 'helm' are not in the PATH
54+
raise Exception(f"Command not found. Please ensure '{e.filename}' is installed and in your system's PATH.")
55+
except Exception as e:
56+
raise Exception(f"An unexpected error occurred: {e}.")
57+
58+
59+
def main():
60+
parser = argparse.ArgumentParser(description="Script to login to the dev/staging helm registries.")
61+
parser.add_argument("--build_scenario", type=str, help="Build scenario (e.g., patch, staging etc).")
62+
args = parser.parse_args()
63+
64+
build_scenario = args.build_scenario
65+
66+
build_info = load_build_info(build_scenario)
67+
68+
registry = build_info.helm_charts["mongodb-kubernetes"].registry
69+
region = build_info.helm_charts["mongodb-kubernetes"].region
70+
return helm_registry_login(registry, region)
71+
72+
73+
if __name__ == "__main__":
74+
try:
75+
main()
76+
except Exception as e:
77+
logger.error(f"Failed while logging in to the helm registry. Error: {e}")
78+
sys.exit(1)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env bash
2+
3+
set -Eeou pipefail
4+
5+
# Instead of calling the publish_helm_chart.py directly from .evergreen-functions.yaml
6+
# we are calling that via this .sh so that we can easily pass build_scenario from env var that
7+
# is set via context files. Using the env vars, set via context files, in .evergreen configuraiton
8+
# is not that straightforward.
9+
source scripts/dev/set_env_context.sh
10+
11+
scripts/dev/run_python.sh scripts/release/helm_registry_login.py --build_scenario "${BUILD_SCENARIO}"
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import argparse
2+
import os
3+
import subprocess
4+
import sys
5+
6+
import yaml
7+
8+
from lib.base_logger import logger
9+
from scripts.release.build.build_info import *
10+
11+
CHART_DIR = "helm_chart"
12+
13+
14+
def run_command(command: list[str]):
15+
try:
16+
# Using capture_output=True to grab stdout/stderr for better error logging.
17+
process = subprocess.run(command, check=True, text=True, capture_output=True)
18+
logger.info(f"Successfully executed: {' '.join(command)}")
19+
if process.stdout:
20+
logger.info(process.stdout)
21+
except subprocess.CalledProcessError as e:
22+
raise RuntimeError(f"Command {' '.join(command)} failed. Stderr: {e.stderr.strip()}") from e
23+
except FileNotFoundError:
24+
raise FileNotFoundError(
25+
f"Error: {command[0]} command not found. Ensure {command[0]} is installed and in your PATH."
26+
)
27+
28+
29+
# update_chart_and_get_metadata updates the helm chart's Chart.yaml and sets the version
30+
# to either evg patch id or commit which is set in OPERATOR_VERSION.
31+
def update_chart_and_get_metadata(chart_dir: str) -> tuple[str, str]:
32+
chart_path = os.path.join(chart_dir, "Chart.yaml")
33+
version_id = os.environ.get("OPERATOR_VERSION")
34+
if not version_id:
35+
raise ValueError(
36+
"Error: Environment variable 'OPERATOR_VERSION' must be set to determine the chart version to publish."
37+
)
38+
39+
new_version = f"0.0.0+{version_id}"
40+
logger.info(f"New helm chart version will be: {new_version}")
41+
42+
if not os.path.exists(chart_path):
43+
raise FileNotFoundError(
44+
f"Error: Chart.yaml not found in directory '{chart_dir}'. "
45+
"Please ensure the directory exists and contains a valid Chart.yaml."
46+
)
47+
48+
try:
49+
with open(chart_path, "r") as f:
50+
data = yaml.safe_load(f)
51+
52+
chart_name = data.get("name")
53+
if not chart_name:
54+
raise ValueError("Chart.yaml is missing required 'name' field.")
55+
56+
data["version"] = new_version
57+
58+
with open(chart_path, "w") as f:
59+
yaml.safe_dump(data, f, sort_keys=False)
60+
61+
logger.info(f"Successfully updated version for chart '{chart_name}' to '{new_version}'.")
62+
return chart_name, new_version
63+
except Exception as e:
64+
raise RuntimeError(f"Failed to read or update Chart.yaml: {e}")
65+
66+
67+
def get_oci_registry(chart_info: HelmChartInfo) -> str:
68+
registry = chart_info.registry
69+
repo = chart_info.repository
70+
71+
if not registry:
72+
raise ValueError("Error: registry doesn't seem to be set in HelmChartInfo.")
73+
74+
if not repo:
75+
raise ValueError("Error: reposiotry doesn't seem to be set in HelmChartInfo.")
76+
77+
oci_registry = f"oci://{registry}/{repo}"
78+
logger.info(f"Determined OCI Registry: {oci_registry}")
79+
return oci_registry
80+
81+
82+
def publish_helm_chart(chart_info: HelmChartInfo):
83+
try:
84+
oci_registry = get_oci_registry(chart_info)
85+
chart_name, chart_version = update_chart_and_get_metadata(CHART_DIR)
86+
tgz_filename = f"{chart_name}-{chart_version}.tgz"
87+
88+
logger.info(f"Packaging chart: {chart_name} with Version: {chart_version}")
89+
package_command = ["helm", "package", CHART_DIR]
90+
run_command(package_command)
91+
92+
logger.info(f"Pushing chart to registry: {oci_registry}")
93+
push_command = ["helm", "push", tgz_filename, oci_registry]
94+
run_command(push_command)
95+
96+
logger.info(f"Helm Chart {chart_name}:{chart_version} was published successfully!")
97+
except Exception as e:
98+
raise Exception(f"Failed publishing the helm chart {e}")
99+
100+
101+
def main():
102+
parser = argparse.ArgumentParser(
103+
description="Script to publish helm chart to the OCI container registry, based on the build scenario."
104+
)
105+
parser.add_argument("--build_scenario", type=str, help="Build scenario (e.g., patch, staging etc).")
106+
args = parser.parse_args()
107+
108+
build_scenario = args.build_scenario
109+
build_info = load_build_info(build_scenario)
110+
111+
return publish_helm_chart(build_info.helm_charts["mongodb-kubernetes"])
112+
113+
114+
if __name__ == "__main__":
115+
try:
116+
main()
117+
except Exception as e:
118+
logger.error(f"Failure in the helm publishing process {e}")
119+
sys.exit(1)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env bash
2+
3+
# Instead of calling the publish_helm_chart.py directly from .evergreen-functions.yaml
4+
# we are calling that via this .sh so that we can easily pass build_scenario from env var that
5+
# is set via context files. Using the env vars, set via context files, in .evergreen configuraiton
6+
# is not that straightforward.
7+
set -Eeou pipefail
8+
9+
source scripts/dev/set_env_context.sh
10+
11+
scripts/dev/run_python.sh scripts/release/publish_helm_chart.py --build_scenario "${BUILD_SCENARIO}"

scripts/release/release_info.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ def convert_to_release_info_json(build_info: BuildInfo) -> dict:
6666

6767
for name, chart in build_info.helm_charts.items():
6868
output["helm-charts"][name] = {
69-
"repositories": chart.repositories,
69+
"registry": chart.registry,
70+
"repository": chart.repository,
7071
"version": DUMMY_VERSION,
7172
}
7273

0 commit comments

Comments
 (0)