diff --git a/.evergreen-functions.yml b/.evergreen-functions.yml index 4b0131b34..e459c3955 100644 --- a/.evergreen-functions.yml +++ b/.evergreen-functions.yml @@ -512,7 +512,7 @@ functions: - command: subprocess.exec params: working_dir: src/github.com/mongodb/mongodb-kubernetes - binary: scripts/dev/run_python.sh scripts/release/kubectl-mongodb/python/build_kubectl_plugin.py + binary: scripts/dev/run_python.sh scripts/release/kubectl_mongodb/python/build_kubectl_plugin.py build_and_push_appdb_database: - command: subprocess.exec @@ -888,7 +888,7 @@ functions: release_kubectl_mongodb_plugin: - command: github.generate_token params: - expansion_name: generated_token + expansion_name: GH_TOKEN - command: shell.exec type: setup params: @@ -911,10 +911,5 @@ functions: GOROOT: "/opt/golang/go1.24" MACOS_NOTARY_KEY: ${macos_notary_keyid} MACOS_NOTARY_SECRET: ${macos_notary_secret} - # shell.exec EVG Task doesn't have add_to_path, so we need to explicitly add the path export below. - script: | - set -Eeu pipefail - export GORELEASER_CURRENT_TAG=${OPERATOR_VERSION|*triggered_by_git_tag} - export PATH=$GOROOT/bin:$PATH - export GITHUB_TOKEN=${generated_token} - ${workdir}/goreleaser release --clean + GH_TOKEN: ${GH_TOKEN} + script: scripts/dev/run_python.sh scripts/release/kubectl_mongodb/python/promote_kubectl_plugin.py diff --git a/.evergreen-release.yml b/.evergreen-release.yml index 592adf44e..6453a4cb1 100644 --- a/.evergreen-release.yml +++ b/.evergreen-release.yml @@ -121,6 +121,7 @@ tasks: - func: clone - func: install_goreleaser - func: install_macos_notarization_service + - func: python_venv - func: release_kubectl_mongodb_plugin - name: create_chart_release_pr diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 884ae3fc4..221b1f5bd 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -19,9 +19,9 @@ builds: hooks: # This will notarize Apple binaries and replace goreleaser bins with the notarized ones post: - - cmd: ./scripts/release/kubectl-mongodb/kubectl_mac_notarize.sh + - cmd: ./scripts/release/kubectl_mongodb/kubectl_mac_notarize.sh output: true - - cmd: ./scripts/release/kubectl-mongodb/sign.sh {{ .Path }} + - cmd: ./scripts/release/kubectl_mongodb/sign.sh {{ .Path }} env: - GRS_USERNAME={{ .Env.GRS_USERNAME }} - GRS_PASSWORD={{ .Env.GRS_PASSWORD }} @@ -30,7 +30,7 @@ builds: - SIGNING_IMAGE_URI={{ .Env.SIGNING_IMAGE_URI }} - ARTIFACTORY_USERNAME=mongodb-enterprise-kubernetes-operator - ARTIFACTORY_PASSWORD={{ .Env.ARTIFACTORY_PASSWORD }} - - cmd: ./scripts/release/kubectl-mongodb/verify.sh {{ .Path }} && echo "VERIFIED OK" + - cmd: ./scripts/release/kubectl_mongodb/verify.sh {{ .Path }} && echo "VERIFIED OK" archives: - format: tar.gz diff --git a/scripts/release/kubectl_mongodb/install_istio_separate_network.sh b/scripts/release/kubectl_mongodb/install_istio_separate_network.sh new file mode 100755 index 000000000..adda0ff92 --- /dev/null +++ b/scripts/release/kubectl_mongodb/install_istio_separate_network.sh @@ -0,0 +1,188 @@ +#!/usr/bin/env bash + +set -eux + +# define here or provide the cluster names externally +export CTX_CLUSTER1=${CTX_CLUSTER1} +export CTX_CLUSTER2=${CTX_CLUSTER2} +export CTX_CLUSTER3=${CTX_CLUSTER3} +export ISTIO_VERSION=${ISTIO_VERSION} + +# download Istio under the path +curl -L https://istio.io/downloadIstio | sh - + +# checks if external IP has been assigned to a service object, in our case we are interested in east-west gateway +function_check_external_ip_assigned() { + while : ; do + ip=$(kubectl --context="$1" get svc istio-eastwestgateway -n istio-system --output jsonpath='{.status.loadBalancer.ingress[0].ip}') + if [ -n "${ip}" ] + then + echo "external ip assigned ${ip}" + break + else + echo "waiting for external ip to be assigned" + fi +done +} + +cd "istio-${ISTIO_VERSION}" +mkdir -p certs +pushd certs + +# create root trust for the clusters +make -f ../tools/certs/Makefile.selfsigned.mk root-ca +make -f ../tools/certs/Makefile.selfsigned.mk "${CTX_CLUSTER1}-cacerts" +make -f ../tools/certs/Makefile.selfsigned.mk "${CTX_CLUSTER2}-cacerts" +make -f ../tools/certs/Makefile.selfsigned.mk "${CTX_CLUSTER3}-cacerts" + +kubectl --context="${CTX_CLUSTER1}" create ns istio-system +kubectl --context="${CTX_CLUSTER1}" create secret generic cacerts -n istio-system \ + --from-file="${CTX_CLUSTER1}/ca-cert.pem" \ + --from-file="${CTX_CLUSTER1}/ca-key.pem" \ + --from-file="${CTX_CLUSTER1}/root-cert.pem" \ + --from-file="${CTX_CLUSTER1}/cert-chain.pem" + +kubectl --context="${CTX_CLUSTER2}" create ns istio-system +kubectl --context="${CTX_CLUSTER2}" create secret generic cacerts -n istio-system \ + --from-file="${CTX_CLUSTER2}/ca-cert.pem" \ + --from-file="${CTX_CLUSTER2}/ca-key.pem" \ + --from-file="${CTX_CLUSTER2}/root-cert.pem" \ + --from-file="${CTX_CLUSTER2}/cert-chain.pem" + +kubectl --context="${CTX_CLUSTER3}" create ns istio-system +kubectl --context="${CTX_CLUSTER3}" create secret generic cacerts -n istio-system \ + --from-file="${CTX_CLUSTER3}/ca-cert.pem" \ + --from-file="${CTX_CLUSTER3}/ca-key.pem" \ + --from-file="${CTX_CLUSTER3}/root-cert.pem" \ + --from-file="${CTX_CLUSTER3}/cert-chain.pem" +popd + +# label namespace in cluster1 +kubectl --context="${CTX_CLUSTER1}" get namespace istio-system && \ + kubectl --context="${CTX_CLUSTER1}" label namespace istio-system topology.istio.io/network=network1 + +cat < cluster1.yaml +apiVersion: install.istio.io/v1alpha1 +kind: IstioOperator +spec: + values: + global: + meshID: mesh1 + multiCluster: + clusterName: cluster1 + network: network1 +EOF +bin/istioctl install --context="${CTX_CLUSTER1}" -f cluster1.yaml +samples/multicluster/gen-eastwest-gateway.sh \ + --mesh mesh1 --cluster cluster1 --network network1 | \ + bin/istioctl --context="${CTX_CLUSTER1}" install -y -f - + + +# check if external IP is assigned to east-west gateway in cluster1 +function_check_external_ip_assigned "${CTX_CLUSTER1}" + + +# expose services in cluster1 +kubectl --context="${CTX_CLUSTER1}" apply -n istio-system -f \ + samples/multicluster/expose-services.yaml + + +kubectl --context="${CTX_CLUSTER2}" get namespace istio-system && \ + kubectl --context="${CTX_CLUSTER2}" label namespace istio-system topology.istio.io/network=network2 + + +cat < cluster2.yaml +apiVersion: install.istio.io/v1alpha1 +kind: IstioOperator +spec: + values: + global: + meshID: mesh1 + multiCluster: + clusterName: cluster2 + network: network2 +EOF + +bin/istioctl install --context="${CTX_CLUSTER2}" -f cluster2.yaml + +samples/multicluster/gen-eastwest-gateway.sh \ + --mesh mesh1 --cluster cluster2 --network network2 | \ + bin/istioctl --context="${CTX_CLUSTER2}" install -y -f - + +# check if external IP is assigned to east-west gateway in cluster2 +function_check_external_ip_assigned "${CTX_CLUSTER2}" + +kubectl --context="${CTX_CLUSTER2}" apply -n istio-system -f \ + samples/multicluster/expose-services.yaml + +# cluster3 +kubectl --context="${CTX_CLUSTER3}" get namespace istio-system && \ + kubectl --context="${CTX_CLUSTER3}" label namespace istio-system topology.istio.io/network=network3 + +cat < cluster3.yaml +apiVersion: install.istio.io/v1alpha1 +kind: IstioOperator +spec: + values: + global: + meshID: mesh1 + multiCluster: + clusterName: cluster3 + network: network3 +EOF + +bin/istioctl install --context="${CTX_CLUSTER3}" -f cluster3.yaml + +samples/multicluster/gen-eastwest-gateway.sh \ + --mesh mesh1 --cluster cluster3 --network network3 | \ + bin/istioctl --context="${CTX_CLUSTER3}" install -y -f - + + +# check if external IP is assigned to east-west gateway in cluster3 +function_check_external_ip_assigned "${CTX_CLUSTER3}" + +kubectl --context="${CTX_CLUSTER3}" apply -n istio-system -f \ + samples/multicluster/expose-services.yaml + + +# enable endpoint discovery +bin/istioctl x create-remote-secret \ + --context="${CTX_CLUSTER1}" \ + -n istio-system \ + --name=cluster1 | \ + kubectl apply -f - --context="${CTX_CLUSTER2}" + +bin/istioctl x create-remote-secret \ + --context="${CTX_CLUSTER1}" \ + -n istio-system \ + --name=cluster1 | \ + kubectl apply -f - --context="${CTX_CLUSTER3}" + +bin/istioctl x create-remote-secret \ + --context="${CTX_CLUSTER2}" \ + -n istio-system \ + --name=cluster2 | \ + kubectl apply -f - --context="${CTX_CLUSTER1}" + +bin/istioctl x create-remote-secret \ + --context="${CTX_CLUSTER2}" \ + -n istio-system \ + --name=cluster2 | \ + kubectl apply -f - --context="${CTX_CLUSTER3}" + +bin/istioctl x create-remote-secret \ + --context="${CTX_CLUSTER3}" \ + -n istio-system \ + --name=cluster3 | \ + kubectl apply -f - --context="${CTX_CLUSTER1}" + +bin/istioctl x create-remote-secret \ + --context="${CTX_CLUSTER3}" \ + -n istio-system \ + --name=cluster3 | \ + kubectl apply -f - --context="${CTX_CLUSTER2}" + + # cleanup: delete the istio repo at the end +cd .. +rm -r "istio-${ISTIO_VERSION}" +rm -f cluster1.yaml cluster2.yaml cluster3.yaml diff --git a/scripts/release/kubectl-mongodb/kubectl_mac_notarize.sh b/scripts/release/kubectl_mongodb/kubectl_mac_notarize.sh similarity index 50% rename from scripts/release/kubectl-mongodb/kubectl_mac_notarize.sh rename to scripts/release/kubectl_mongodb/kubectl_mac_notarize.sh index 06a69e640..9f00c170b 100755 --- a/scripts/release/kubectl-mongodb/kubectl_mac_notarize.sh +++ b/scripts/release/kubectl_mongodb/kubectl_mac_notarize.sh @@ -20,16 +20,27 @@ set -Eeou pipefail # This depends on binaries being generated in a goreleaser manner and gon being set up. # goreleaser should already take care of calling this script as a hook. -if [[ -f "./dist/kubectl-mongodb_darwin_amd64_v1/kubectl-mongodb" && -f "./dist/kubectl-mongodb_darwin_arm64/kubectl-mongodb" && ! -f "./dist/kubectl-mongodb_macos_signed.zip" ]]; then +if [ -z "${1-}" ]; then + echo "Error: Missing required argument as first positional parameter to script" + echo "Usage: ./kubectl_mac_notarize.sh " + exit 1 +fi + +version=$1 + +darwin_amd64_dir="./artifacts/kubectl-mongodb_${version}_darwin_amd64" +darwin_arm64_dir="./artifacts/kubectl-mongodb_${version}_darwin_arm64" + +if [[ -f "${darwin_amd64_dir}/kubectl-mongodb" && -f "${darwin_arm64_dir}/kubectl-mongodb" && ! -f "./artifacts/kubectl-mongodb_macos_signed.zip" ]]; then echo "notarizing macOs binaries" - zip -r ./dist/kubectl-mongodb_amd64_arm64_bin.zip ./dist/kubectl-mongodb_darwin_amd64_v1/kubectl-mongodb ./dist/kubectl-mongodb_darwin_arm64/kubectl-mongodb # The Notarization Service takes an archive as input + zip -r ./artifacts/kubectl-mongodb_amd64_arm64_bin.zip "${darwin_amd64_dir}/kubectl-mongodb" "${darwin_arm64_dir}/kubectl-mongodb" # The Notarization Service takes an archive as input "${workdir:-.}"/linux_amd64/macnotary \ - -f ./dist/kubectl-mongodb_amd64_arm64_bin.zip \ + -f ./artifacts/kubectl-mongodb_amd64_arm64_bin.zip \ -m notarizeAndSign -u https://dev.macos-notary.build.10gen.cc/api \ -b com.mongodb.mongodb-kubectl-mongodb \ - -o ./dist/kubectl-mongodb_macos_signed.zip + -o ./artifacts/kubectl-mongodb_macos_signed.zip echo "replacing original files" - unzip -oj ./dist/kubectl-mongodb_macos_signed.zip dist/kubectl-mongodb_darwin_amd64_v1/kubectl-mongodb -d ./dist/kubectl-mongodb_darwin_amd64_v1/ - unzip -oj ./dist/kubectl-mongodb_macos_signed.zip dist/kubectl-mongodb_darwin_arm64/kubectl-mongodb -d ./dist/kubectl-mongodb_darwin_arm64/ + unzip -oj ./artifacts/kubectl-mongodb_macos_signed.zip "artifacts/kubectl-mongodb_${version}_darwin_amd64/kubectl-mongodb" -d "${darwin_amd64_dir}/" + unzip -oj ./artifacts/kubectl-mongodb_macos_signed.zip "artifacts/kubectl-mongodb_${version}_darwin_arm64/kubectl-mongodb" -d "${darwin_arm64_dir}/" fi diff --git a/scripts/release/kubectl-mongodb/python/build_kubectl_plugin.py b/scripts/release/kubectl_mongodb/python/build_kubectl_plugin.py similarity index 98% rename from scripts/release/kubectl-mongodb/python/build_kubectl_plugin.py rename to scripts/release/kubectl_mongodb/python/build_kubectl_plugin.py index 5e0d9b3bb..bf63ea875 100755 --- a/scripts/release/kubectl-mongodb/python/build_kubectl_plugin.py +++ b/scripts/release/kubectl_mongodb/python/build_kubectl_plugin.py @@ -9,13 +9,10 @@ from scripts.release.build.build_info import ( load_build_info, ) +from scripts.release.kubectl_mongodb.python.consts import * -AWS_REGION = "eu-north-1" -KUBECTL_PLUGIN_BINARY_NAME = "kubectl-mongodb" S3_BUCKET_KUBECTL_PLUGIN_SUBPATH = KUBECTL_PLUGIN_BINARY_NAME -GORELEASER_DIST_DIR = "dist" - def run_goreleaser(): try: diff --git a/scripts/release/kubectl_mongodb/python/consts.py b/scripts/release/kubectl_mongodb/python/consts.py new file mode 100644 index 000000000..3e3efbcac --- /dev/null +++ b/scripts/release/kubectl_mongodb/python/consts.py @@ -0,0 +1,12 @@ +AWS_REGION = "eu-north-1" +KUBECTL_PLUGIN_BINARY_NAME = "kubectl-mongodb" + +GITHUB_REPO = "mongodb/mongodb-kubernetes" + +LOCAL_ARTIFACTS_DIR = "artifacts" +CHECKSUMS_PATH = f"{LOCAL_ARTIFACTS_DIR}/checksums.txt" + +GORELEASER_DIST_DIR = "dist" + +BUILD_SCENARIO_RELEASE = "release" +BUILD_SCENARIO_STAGING = "staging" diff --git a/scripts/release/kubectl_mongodb/python/promote_kubectl_plugin.py b/scripts/release/kubectl_mongodb/python/promote_kubectl_plugin.py new file mode 100644 index 000000000..73aacd239 --- /dev/null +++ b/scripts/release/kubectl_mongodb/python/promote_kubectl_plugin.py @@ -0,0 +1,310 @@ +import argparse +import hashlib +import os +import subprocess +import sys +import tarfile +from pathlib import Path + +import boto3 +from botocore.exceptions import ClientError, NoCredentialsError, PartialCredentialsError +from github import Github, GithubException + +from lib.base_logger import logger +from scripts.release.build.build_info import ( + load_build_info, +) +from scripts.release.kubectl_mongodb.python.consts import * + +GITHUB_TOKEN = os.environ.get("GH_TOKEN") + +S3_BUCKET_KUBECTL_PLUGIN_SUBPATH = KUBECTL_PLUGIN_BINARY_NAME + + +def main(): + release_version = os.environ.get("OPERATOR_VERSION") + + kubectl_plugin_release_info = load_build_info(BUILD_SCENARIO_RELEASE).binaries[KUBECTL_PLUGIN_BINARY_NAME] + release_scenario_bucket_name = kubectl_plugin_release_info.s3_store + + kubectl_plugin_staging_info = load_build_info(BUILD_SCENARIO_STAGING).binaries[KUBECTL_PLUGIN_BINARY_NAME] + staging_scenario_bucket_name = kubectl_plugin_staging_info.s3_store + + download_artifacts_from_s3(release_version, get_commit_from_tag(release_version), staging_scenario_bucket_name) + + notarize_artifacts(release_version) + + sign_and_verify_artifacts() + + artifacts_tar = create_tarballs() + + artifacts = generate_checksums(artifacts_tar) + + promote_artifacts(artifacts, release_version, release_scenario_bucket_name) + + upload_assets_to_github_release(artifacts, release_version) + + +# get_commit_from_tag gets the commit associated with a release tag, so that we can use that +# commit to pull the artifacts from staging bucket. +def get_commit_from_tag(tag: str) -> str: + try: + subprocess.run(["git", "fetch", "--tags"], capture_output=True, text=True, check=True) + + result = subprocess.run( + # using --short because that's how staging version is figured out for staging build scenario + # https://github.com/mongodb/mongodb-kubernetes/blob/1.5.0/scripts/dev/contexts/evg-private-context#L137 + ["git", "rev-parse", "--short", f"{tag}^{{commit}}"], # git rev-parse v1.1.1^{commit} + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + + except subprocess.CalledProcessError as e: + logger.info(f"Failed to get commit for tag: {tag}, err: {e.stderr.strip()}") + sys.exit(1) + + +# generate_checksums generates checksums for the artifacts that we are going to upload to github release as assets. +# It's formatted: checksum artifact_name +def generate_checksums(artifacts: list[str]): + checksums_path = Path(CHECKSUMS_PATH) + + with checksums_path.open("w") as out_file: + for artifact in artifacts: + artifact_path = Path(artifact) + if not artifact_path.is_file() or not artifact_path.name.endswith(".tar.gz"): + logger.info(f"skipping invalid tar file: {artifact_path}") + continue + + sha256 = hashlib.sha256() + with open(artifact_path, "rb") as f: + # read chunk of 8192 bites until end of file (b"") is received + for chunk in iter(lambda: f.read(8192), b""): + sha256.update(chunk) + + checksum_line = f"{sha256.hexdigest()} {artifact_path.name}" + out_file.write(checksum_line + "\n") + + logger.info(f"Checksums written to {checksums_path}") + all_artifacts = list(artifacts) + [str(checksums_path.resolve())] + return all_artifacts + + +# promote_artifacts promotes (copies) the downloaded staging artifacts to release S3 bucket. +def promote_artifacts(artifacts: list[str], release_version: str, release_scenario_bucket_name: str): + s3_client = boto3.client("s3", region_name=AWS_REGION) + for file in artifacts: + if not os.path.isfile(file) or not file.endswith((".tar.gz", ".txt")): + logger.info(f"Skipping invalid or non-tar/checksum file: {file}") + continue + + file_name = os.path.basename(file) + s3_key = os.path.join(S3_BUCKET_KUBECTL_PLUGIN_SUBPATH, release_version, file_name) + + try: + s3_client.upload_file(file, release_scenario_bucket_name, s3_key) + logger.debug( + f"Plugin file {file} was promoted to release bucket {release_scenario_bucket_name}/{s3_key} successfully" + ) + except ClientError as e: + logger.debug(f"failed to upload the file {file}: {e}") + sys.exit(1) + + logger.info("Artifacts were promoted to release bucket successfully") + + +# notarize_artifacts notarizes the darwin goreleaser binaries in-place. +def notarize_artifacts(release_version: str): + notarize_result = subprocess.run( + ["scripts/release/kubectl_mongodb/kubectl_mac_notarize.sh", release_version], capture_output=True, text=True + ) + if notarize_result.returncode == 0: + logger.info("Notarization of artifacts was successful") + else: + logger.debug( + f"Notarization of artifacts failed. \nstdout: {notarize_result.stdout} \nstderr: {notarize_result.stderr}" + ) + sys.exit(1) + + +# sign_and_verify_artifacts iterates over the goreleaser artifacts, that have been downloaded from S3, and +# signs and verifies them. +def sign_and_verify_artifacts(): + cwd = os.getcwd() + artifacts_dir = os.path.join(cwd, LOCAL_ARTIFACTS_DIR) + + for subdir in os.listdir(artifacts_dir): + subdir_path = os.path.join(artifacts_dir, subdir) + + # just work on dirs and not files + if os.path.isdir(subdir_path): + for file in os.listdir(subdir_path): + file_path = os.path.join(subdir_path, file) + + if os.path.isfile(file_path): + # signing an already signed artifact fails with `Signature already exixts. Displaying proof`. + sign_result = subprocess.run( + ["scripts/release/kubectl_mongodb/sign.sh", file_path], capture_output=True, text=True + ) + if sign_result.returncode == 0: + logger.info(f"Artifact {file_path} was signed successfully") + else: + logger.debug( + f"Signing the artifact {file_path} failed. \nstdout: {sign_result.stdout} \nstderr: {sign_result.stderr}" + ) + sys.exit(1) + + verify_result = subprocess.run( + ["scripts/release/kubectl_mongodb/verify.sh", file_path], capture_output=True, text=True + ) + if verify_result.returncode == 0: + logger.info(f"Artifact {file_path} was verified successfully") + else: + logger.debug( + f"Verification of the artifact {file_path} failed. \nstdout: {verify_result.stdout} \nstderr: {verify_result.stderr}" + ) + sys.exit(1) + + +def s3_artifacts_path_to_local_path(release_version: str, commit_sha: str): + s3_common_path = f"{S3_BUCKET_KUBECTL_PLUGIN_SUBPATH}/{commit_sha}/dist" + return { + f"{s3_common_path}/kubectl-mongodb_darwin_amd64_v1/": f"kubectl-mongodb_{release_version}_darwin_amd64", + f"{s3_common_path}/kubectl-mongodb_darwin_arm64/": f"kubectl-mongodb_{release_version}_darwin_arm64", + f"{s3_common_path}/kubectl-mongodb_linux_amd64_v1/": f"kubectl-mongodb_{release_version}_linux_amd64", + f"{s3_common_path}/kubectl-mongodb_linux_arm64/": f"kubectl-mongodb_{release_version}_linux_arm64", + f"{s3_common_path}/kubectl-mongodb_linux_ppc64le/": f"kubectl-mongodb_{release_version}_linux_ppc64le", + f"{s3_common_path}/kubectl-mongodb_linux_s390x/": f"kubectl-mongodb_{release_version}_linux_s390x", + } + + +# download_artifacts_from_s3 downloads the staging artifacts (only that ones that we would later promote) from S3 and puts +# them in the local dir LOCAL_ARTIFACTS_DIR. +# ToDo: if the artifacts are not present at correct location, this is going to fail silently, we should instead fail this +def download_artifacts_from_s3(release_version: str, commit_sha: str, staging_s3_bucket_name: str): + logger.info(f"Starting download of artifacts from staging S3 bucket: {staging_s3_bucket_name}") + + try: + s3_client = boto3.client("s3", region_name=AWS_REGION) + except (NoCredentialsError, PartialCredentialsError): + logger.debug("ERROR: AWS credentials were not set.") + sys.exit(1) + except Exception as e: + logger.debug(f"An error occurred connecting to S3: {e}") + sys.exit(1) + + artifacts_to_promote = s3_artifacts_path_to_local_path(release_version, commit_sha) + + # Create the local temporary directory if it doesn't exist + os.makedirs(LOCAL_ARTIFACTS_DIR, exist_ok=True) + + download_count = 0 + for s3_artifact_dir, local_subdir in artifacts_to_promote.items(): + try: + paginator = s3_client.get_paginator("list_objects_v2") + pages = paginator.paginate(Bucket=staging_s3_bucket_name, Prefix=s3_artifact_dir) + for page in pages: + # "Contents" corresponds to the directory in the S3 bucket + if "Contents" not in page: + continue + for obj in page["Contents"]: + # obj is the S3 object in page["Contents"] directory + s3_key = obj["Key"] + if s3_key.endswith("/"): + # it's a directory + continue + + # Get the path of the file relative to its S3 prefix, this would mostly be the object name itself + # if s3_artifact_dir doesn't container directories and has just the objects. + relative_path = os.path.relpath(s3_key, s3_artifact_dir) + + final_local_path = os.path.join(LOCAL_ARTIFACTS_DIR, local_subdir, relative_path) + + # Create the local directory structure if it doesn't exist + os.makedirs(os.path.dirname(final_local_path), exist_ok=True) + + logger.info(f"Downloading staging artifact {s3_key} to {final_local_path}") + s3_client.download_file(staging_s3_bucket_name, s3_key, final_local_path) + download_count += 1 + + except ClientError as e: + logger.debug(f"ERROR: Failed to list or download from prefix '{s3_artifact_dir}'. S3 Client Error: {e}") + return False + + if download_count == 0: + logger.info( + f"Couldn't download artifacts from staging S3 bucket {staging_s3_bucket_name}, please verify that artifacts are available under dir: {commit_sha}" + ) + sys.exit(1) + + logger.info("All the artifacts have been downloaded successfully.") + return True + + +# create_tarballs creates `.tar.gz` archives for the artifacts that before promoting them. +def create_tarballs(): + logger.info(f"Creating archives for subdirectories in {LOCAL_ARTIFACTS_DIR}") + created_archives = [] + original_cwd = os.getcwd() + try: + os.chdir(LOCAL_ARTIFACTS_DIR) + + for dir_name in os.listdir("."): + if os.path.isdir(dir_name): + archive_name = f"{dir_name}.tar.gz" + + with tarfile.open(archive_name, "w:gz") as tar: + tar.add(dir_name) + + full_archive_path = os.path.join(original_cwd, LOCAL_ARTIFACTS_DIR, archive_name) + logger.info(f"Successfully created archive at {full_archive_path}") + created_archives.append(full_archive_path) + + except Exception as e: + logger.debug(f"ERROR: Failed to create tar.gz archives: {e}") + sys.exit(1) + finally: + os.chdir(original_cwd) + + return created_archives + + +# upload_assets_to_github_release uploads the release artifacts (downloaded notarized/signed staging artifacts) to +# the github release as assets. +def upload_assets_to_github_release(asset_paths, release_version: str): + if not GITHUB_TOKEN: + logger.info("ERROR: GITHUB_TOKEN environment variable not set.") + sys.exit(1) + + try: + g = Github(GITHUB_TOKEN) + repo = g.get_repo(GITHUB_REPO) + except GithubException as e: + logger.info(f"ERROR: Could not connect to GitHub or find repository '{GITHUB_REPO}', Error {e}.") + sys.exit(1) + + try: + release = repo.get_release(release_version) + except GithubException as e: + logger.debug( + f"ERROR: Could not find release with tag '{release_version}'. Please ensure release exists already. Error: {e}" + ) + sys.exit(2) + + for asset_path in asset_paths: + asset_name = os.path.basename(asset_path) + logger.info(f"Uploading artifact '{asset_name}' to github release as asset") + try: + release.upload_asset(path=asset_path, name=asset_name, content_type="application/gzip") + except GithubException as e: + logger.debug(f"ERROR: Failed to upload asset {asset_name}. Error: {e}") + sys.exit(2) + except Exception as e: + logger.debug(f"An unexpected error occurred during upload of {asset_name}: {e}") + sys.exit(2) + + +if __name__ == "__main__": + main() diff --git a/scripts/release/kubectl-mongodb/setup_tls.sh b/scripts/release/kubectl_mongodb/setup_tls.sh similarity index 100% rename from scripts/release/kubectl-mongodb/setup_tls.sh rename to scripts/release/kubectl_mongodb/setup_tls.sh diff --git a/scripts/release/kubectl-mongodb/sign.sh b/scripts/release/kubectl_mongodb/sign.sh similarity index 100% rename from scripts/release/kubectl-mongodb/sign.sh rename to scripts/release/kubectl_mongodb/sign.sh diff --git a/scripts/release/kubectl-mongodb/verify.sh b/scripts/release/kubectl_mongodb/verify.sh similarity index 100% rename from scripts/release/kubectl-mongodb/verify.sh rename to scripts/release/kubectl_mongodb/verify.sh