diff --git a/.evergreen-functions.yml b/.evergreen-functions.yml index 6a3e41cc5..1abd6b451 100644 --- a/.evergreen-functions.yml +++ b/.evergreen-functions.yml @@ -517,22 +517,6 @@ functions: # docker buildx needs the moby/buildkit image when setting up a builder so we pull it from our mirror docker buildx create --driver=docker-container --driver-opt=image=268558157000.dkr.ecr.eu-west-1.amazonaws.com/docker-hub-mirrors/moby/buildkit:buildx-stable-1 --use docker buildx inspect --bootstrap - - command: ec2.assume_role - display_name: Assume IAM role with permissions to pull Kondukto API token - params: - role_arn: ${kondukto_role_arn} - - command: shell.exec - display_name: Pull Kondukto API token from AWS Secrets Manager and write it to file - params: - silent: true - shell: bash - include_expansions_in_env: [AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN] - script: | - set -e - # use AWS CLI to get the Kondukto API token from AWS Secrets Manager - kondukto_token=$(aws secretsmanager get-secret-value --secret-id "kondukto-token" --region "us-east-1" --query 'SecretString' --output text) - # write the KONDUKTO_TOKEN environment variable to Silkbomb environment file - echo "KONDUKTO_TOKEN=$kondukto_token" > ${workdir}/silkbomb.env - command: subprocess.exec retry_on_failure: true type: setup @@ -540,7 +524,7 @@ functions: shell: bash <<: *e2e_include_expansions_in_env working_dir: src/github.com/mongodb/mongodb-kubernetes - binary: scripts/dev/run_python.sh scripts/release/pipeline_main.py --parallel ${image_name} + binary: scripts/dev/run_python.sh scripts/release/pipeline_main.py --parallel ${image_name} ${all_agents} # TODO: CLOUDP-335471 ; once all image builds are made with the new atomic pipeline, remove the following function legacy_pipeline: diff --git a/.evergreen.yml b/.evergreen.yml index fdf94f589..f2e9fbb7e 100644 --- a/.evergreen.yml +++ b/.evergreen.yml @@ -61,14 +61,14 @@ variables: variant: init_test_run - name: build_test_image variant: init_test_run - - name: build_agent_images_ubi - variant: init_test_run - name: build_readiness_probe_image variant: init_test_run - name: build_upgrade_hook_image variant: init_test_run - name: build_mco_test_image variant: init_test_run + - name: build_agent_images_ubi + variant: init_test_run - &setup_group setup_group_can_fail_task: true @@ -347,19 +347,6 @@ tasks: image_name: init-ops-manager include_tags: release - - name: release_agent_operator_release - tags: [ "image_release" ] - allowed_requesters: [ "patch", "github_tag" ] - commands: - - func: clone - - func: setup_building_host - - func: quay_login - - func: setup_docker_sbom - - func: legacy_pipeline - vars: - image_name: agent - include_tags: release - # pct only triggers this variant once a new agent image is out - name: release_agent # this enables us to run this variant either manually (patch) which pct does or during an OM bump (github_pr) @@ -371,8 +358,7 @@ tasks: - func: setup_docker_sbom - func: legacy_pipeline vars: - image_name: agent-pct - include_tags: release + image_name: agent - name: run_precommit_and_push tags: ["patch-run"] @@ -392,48 +378,17 @@ tasks: working_dir: src/github.com/mongodb/mongodb-kubernetes binary: scripts/evergreen/precommit_bump.sh - # Pct only triggers this variant once a new agent image is out - # these releases the agent with the operator suffix (not patch id) on ecr to allow for digest pinning to pass. - # For this to work, we rely on skip_tags which is used to determine whether - # we want to release on quay or not, in this case - ecr instead. - # We rely on the init_database from ecr for the agent x operator images. - # This runs on agent releases that are not concurrent with operator releases. - - name: release_agents_on_ecr_conditional - commands: - - func: clone - - func: run_task_conditionally - vars: - condition_script: scripts/evergreen/should_release_agents_on_ecr.sh - variant: init_release_agents_on_ecr - task: release_agents_on_ecr - - - name: release_agents_on_ecr - # this enables us to run this variant either manually (patch) which pct does or during an OM bump (github_pr) - allowed_requesters: [ "patch", "github_pr" ] - priority: 70 - commands: - - func: clone - - func: setup_building_host - - func: legacy_pipeline - vars: - image_name: agent-pct - skip_tags: release - - name: release_all_agents_on_ecr - # this enables us to run this manually (patch) and release all agent versions to ECR - # it's needed during operator new version release process - e2e tests (especially olm tests) - # will look for agent with new operator version suffix, but during PR checks we only build - # agent versions for most recent major OM versions and the tests will fail. Before running the PR - # we have to manually release all agents to ECR by triggering this patch + # this enables us to run this manually (patch) and release all agent versions to ECR to verify + # Dockerfile, script changes etc. allowed_requesters: [ "patch" ] commands: - func: clone - func: setup_building_host - - func: legacy_pipeline + - func: pipeline vars: - image_name: agent-pct - skip_tags: release - all_agents: true + image_name: agent + all_agents: "--all-agents" - name: build_test_image commands: @@ -1334,8 +1289,7 @@ buildvariants: variant: init_test_run - name: build_init_database_image_ubi variant: init_test_run - - name: build_agent_images_ubi - variant: init_test_run + tasks: - name: e2e_custom_domain_task_group @@ -1369,8 +1323,7 @@ buildvariants: variant: init_test_run - name: build_init_database_image_ubi variant: init_test_run - - name: build_agent_images_ubi - variant: init_test_run + run_on: - ubuntu2204-small tasks: @@ -1594,6 +1547,8 @@ buildvariants: variant: init_test_run - name: prepare_and_upload_openshift_bundles_for_e2e variant: init_tests_with_olm + - name: build_agent_images_ubi + variant: init_test_run tasks: - name: e2e_kind_olm_group @@ -1619,6 +1574,7 @@ buildvariants: variant: init_test_run - name: build_agent_images_ubi variant: init_test_run + tasks: - name: e2e_kind_olm_group @@ -1683,18 +1639,6 @@ buildvariants: - name: build_upgrade_hook_image - name: prepare_aws - - name: init_release_agents_on_ecr - display_name: init_release_agents_on_ecr - # this enables us to run this variant either manually (patch) which pct does or during an OM bump (github_pr) - allowed_requesters: [ "patch", "github_pr" ] - tags: [ "release_agents_on_ecr" ] - # We want that to run first and finish asap. Digest pinning depends on this to succeed. - priority: 70 - run_on: - - ubuntu2204-large - tasks: - - name: release_agents_on_ecr_conditional - - name: run_pre_commit priority: 70 display_name: run_pre_commit @@ -1722,8 +1666,7 @@ buildvariants: variant: init_test_run - name: build_init_om_images_ubi variant: init_test_run - - name: build_agent_images_ubi - variant: init_test_run + run_on: - ubuntu2204-small tasks: @@ -1809,13 +1752,6 @@ buildvariants: - name: release_init_database - name: release_init_ops_manager - name: release_database - # Once we release the operator, we will also release the init databases, we require them to be out first - # such that we can reference them and retrieve those binaries. - # Since we immediately run daily rebuild after creating the image, we can ensure that the init_database is out - # such that the agent image build can use it. - - name: release_agent_operator_release - depends_on: - - name: release_init_database - name: preflight_release_images display_name: preflight_release_images @@ -1847,13 +1783,13 @@ buildvariants: # It will be called by pct while bumping the agent cloud manager image - name: release_agent - display_name: (Static Containers) Release Agent matrix + display_name: release_agent tags: [ "release_agent" ] run_on: - release-ubuntu2204-large # This is required for CISA attestation https://jira.mongodb.org/browse/DEVPROD-17780 depends_on: - - variant: init_release_agents_on_ecr - name: '*' + - variant: init_test_run + name: build_agent_images_ubi # this ensures the agent gets released to ECR as well - variant: e2e_multi_cluster_kind name: '*' - variant: e2e_static_multi_cluster_2_clusters diff --git a/inventories/agent.yaml b/inventories/agent.yaml index 876e4027b..bcb2d5889 100644 --- a/inventories/agent.yaml +++ b/inventories/agent.yaml @@ -3,7 +3,7 @@ vars: s3_bucket: s3://enterprise-operator-dockerfiles/dockerfiles/mongodb-agent images: - - name: mongodb-agent-ubi + - name: mongodb-agent vars: context: . template_context: docker/mongodb-agent diff --git a/lib/sonar/template.py b/lib/sonar/template.py index 98d8e1a11..e92fbb53a 100644 --- a/lib/sonar/template.py +++ b/lib/sonar/template.py @@ -13,7 +13,7 @@ def render(path: str, template_name: str, parameters: Dict[str, str]) -> str: """ env = jinja2.Environment(loader=jinja2.FileSystemLoader(path), undefined=jinja2.StrictUndefined) - template = "Dockerfile" + template = "Dockerfile.old" if template_name is not None: template = "Dockerfile.{}".format(template_name) diff --git a/pipeline.py b/pipeline.py index 6ebb645bb..2935377f6 100755 --- a/pipeline.py +++ b/pipeline.py @@ -51,6 +51,7 @@ sign_image, verify_signature, ) +from scripts.release.detect_ops_manager_changes import detect_ops_manager_changes TRACER = trace.get_tracer("evergreen-agent") @@ -1202,7 +1203,6 @@ def build_multi_arch_agent_in_sonar( """ logger.info(f"building multi-arch base image for: {image_version}") - is_release = build_configuration.is_release_step_executed() args = { "version": image_version, "tools_version": tools_version, @@ -1232,7 +1232,7 @@ def build_multi_arch_agent_in_sonar( build_image_generic( config=build_configuration, - image_name="mongodb-agent-ubi", + image_name="mongodb-agent", inventory_file="inventories/agent.yaml", multi_arch_args_list=joined_args, with_image_base=False, @@ -1241,22 +1241,11 @@ def build_multi_arch_agent_in_sonar( ) -def build_agent_default_case(build_configuration: BuildConfiguration): - """ - Build the agent only for the latest operator for patches and operator releases. - - See more information in the function: build_agent_on_agent_bump - """ - release_json = get_release() - - is_release = build_configuration.is_release_step_executed() - - # We need to release [all agents x latest operator] on operator releases - if is_release: - agent_versions_to_build = gather_all_supported_agent_versions(release_json) - # We only need [latest agents (for each OM major version and for CM) x patch ID] for patches - else: - agent_versions_to_build = gather_latest_agent_versions(release_json, build_configuration.agent_to_build) +def build_agent(build_configuration: BuildConfiguration): + agent_versions_to_build = detect_ops_manager_changes() + if not agent_versions_to_build: + logger.info("No changes detected, skipping agent build") + return logger.info(f"Building Agent versions: {agent_versions_to_build}") @@ -1267,83 +1256,17 @@ def build_agent_default_case(build_configuration: BuildConfiguration): if build_configuration.parallel_factor > 0: max_workers = build_configuration.parallel_factor with ProcessPoolExecutor(max_workers=max_workers) as executor: - logger.info(f"running with factor of {max_workers}") - for agent_version in agent_versions_to_build: - # We don't need to keep create and push the same image on every build. - # It is enough to create and push the non-operator suffixed images only during releases to ecr and quay. - if build_configuration.is_release_step_executed() or build_configuration.all_agents: - tasks_queue.put( - executor.submit( - build_multi_arch_agent_in_sonar, - build_configuration, - agent_version[0], - agent_version[1], - ) - ) - _add_to_agent_queue(agent_version, build_configuration, executor, tasks_queue) - - queue_exception_handling(tasks_queue) - - -def build_agent_on_agent_bump(build_configuration: BuildConfiguration): - """ - Build the agent matrix (operator version x agent version), triggered by PCT. - - We have three cases where we need to build the agent: - - e2e test runs - - operator releases - - OM/CM bumps via PCT - - In OM/CM bumps, we release a new agent. - """ - release_json = get_release() - is_release = build_configuration.is_release_step_executed() - - if build_configuration.all_agents: - agent_versions_to_build = gather_all_supported_agent_versions(release_json) - else: - # we only need to release the latest images, we don't need to re-push old images, as we don't clean them up anymore. - agent_versions_to_build = gather_latest_agent_versions(release_json, build_configuration.agent_to_build) - - legacy_agent_versions_to_build = release_json["supportedImages"]["mongodb-agent"]["versions"] - - tasks_queue = Queue() - max_workers = 1 - if build_configuration.parallel: - max_workers = None - if build_configuration.parallel_factor > 0: - max_workers = build_configuration.parallel_factor - with ProcessPoolExecutor(max_workers=max_workers) as executor: - logger.info(f"running with factor of {max_workers}") - - # We need to regularly push legacy agents, otherwise ecr lifecycle policy will expire them. - # We only need to push them once in a while to ecr, so no quay required - if not is_release: - for legacy_agent in legacy_agent_versions_to_build: - tasks_queue.put( - executor.submit( - build_multi_arch_agent_in_sonar, - build_configuration, - legacy_agent, - # we assume that all legacy agents are build using that tools version - "100.9.4", - ) - ) - - for agent_version in agent_versions_to_build: + logger.info(f"Running with factor of {max_workers}") + for idx, agent_tools_version in enumerate(agent_versions_to_build): # We don't need to keep create and push the same image on every build. # It is enough to create and push the non-operator suffixed images only during releases to ecr and quay. - if build_configuration.is_release_step_executed() or build_configuration.all_agents: - tasks_queue.put( - executor.submit( - build_multi_arch_agent_in_sonar, - build_configuration, - agent_version[0], - agent_version[1], - ) - ) - logger.info(f"Building Agent versions: {agent_version}") - _add_to_agent_queue(agent_version, build_configuration, executor, tasks_queue) + logger.info(f"======= Building Agent {agent_tools_version} ({idx}/{len(agent_versions_to_build)})") + _build_agents( + agent_tools_version, + build_configuration, + executor, + tasks_queue, + ) queue_exception_handling(tasks_queue) @@ -1384,87 +1307,25 @@ def queue_exception_handling(tasks_queue): ) -def _add_to_agent_queue( - agent_version: Tuple[str, str], +def _build_agents( + agent_tools_version: Tuple[str, str], build_configuration: BuildConfiguration, executor: ProcessPoolExecutor, tasks_queue: Queue, ): - tools_version = agent_version[1] - image_version = f"{agent_version[0]}" + agent_version = agent_tools_version[0] + tools_version = agent_tools_version[1] tasks_queue.put( executor.submit( build_multi_arch_agent_in_sonar, build_configuration, - image_version, + agent_version, tools_version, ) ) -def gather_all_supported_agent_versions(release: Dict) -> List[Tuple[str, str]]: - # This is a list of a tuples - agent version and corresponding tools version - agent_versions_to_build = list() - agent_versions_to_build.append( - ( - release["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["cloud_manager"], - release["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["cloud_manager_tools"], - ) - ) - for _, om in release["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["ops_manager"].items(): - agent_versions_to_build.append((om["agent_version"], om["tools_version"])) - - # lets not build the same image multiple times - return sorted(list(set(agent_versions_to_build))) - - -def gather_latest_agent_versions(release: Dict, agent_to_build: str = "") -> List[Tuple[str, str]]: - """ - This function is used when we release a new agent via OM bump. - That means we will need to release that agent with all supported operators. - Since we don't want to release all agents again, we only release the latest, which will contain the newly added one - :return: the latest agent for each major version - """ - agent_versions_to_build = list() - agent_versions_to_build.append( - ( - release["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["cloud_manager"], - release["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["cloud_manager_tools"], - ) - ) - - latest_versions = {} - - for version in release["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["ops_manager"].keys(): - parsed_version = semver.VersionInfo.parse(version) - major_version = parsed_version.major - if major_version in latest_versions: - latest_parsed_version = semver.VersionInfo.parse(str(latest_versions[major_version])) - latest_versions[major_version] = max(parsed_version, latest_parsed_version) - else: - latest_versions[major_version] = version - - for major_version, latest_version in latest_versions.items(): - agent_versions_to_build.append( - ( - release["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["ops_manager"][str(latest_version)][ - "agent_version" - ], - release["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["ops_manager"][str(latest_version)][ - "tools_version" - ], - ) - ) - - if agent_to_build != "": - for agent_tuple in agent_versions_to_build: - if agent_tuple[0] == agent_to_build: - return [agent_tuple] - - return sorted(list(set(agent_versions_to_build))) - - def get_builder_function_for_image_name() -> Dict[str, Callable]: """Returns a dictionary of image names that can be built.""" @@ -1478,8 +1339,8 @@ def get_builder_function_for_image_name() -> Dict[str, Callable]: "upgrade-hook": build_upgrade_hook_image, "operator-quick": build_operator_image_patch, "database": build_database_image, - "agent-pct": build_agent_on_agent_bump, - "agent": build_agent_default_case, + "agent-pct": build_agent, + "agent": build_agent, # # Init images "init-appdb": build_init_appdb, diff --git a/pipeline_test.py b/pipeline_test.py index cbaf2bd06..61bbed197 100644 --- a/pipeline_test.py +++ b/pipeline_test.py @@ -7,8 +7,6 @@ from pipeline import ( calculate_images_to_build, - gather_all_supported_agent_versions, - gather_latest_agent_versions, get_versions_to_rebuild, get_versions_to_rebuild_per_operator_version, is_version_in_range, @@ -159,23 +157,6 @@ def test_is_release_step_executed(description, case): assert result == case["expected"], f"Test failed: {description}. Expected {case['expected']}, got {result}." -def test_build_latest_agent_versions(): - latest_agents = gather_latest_agent_versions(release_json) - expected_agents = [ - ("107.0.11.8645-1", "100.10.0"), - ("12.0.31.7825-1", "100.9.4"), - ("13.19.0.8937-1", "100.9.4"), - ] - assert latest_agents == expected_agents - - -def test_get_versions_to_rebuild_same_version(): - supported_versions = gather_all_supported_agent_versions(release_json) - agents = get_versions_to_rebuild(supported_versions, "6.0.0_1.26.0", "6.0.0_1.26.0") - assert len(agents) == 1 - assert agents[0] == "6.0.0_1.26.0" - - def test_get_versions_to_rebuild_multiple_versions(): supported_versions = ["6.0.0", "6.0.1", "6.0.21", "6.11.0", "7.0.0"] expected_agents = ["6.0.0", "6.0.1", "6.0.21"] diff --git a/scripts/release/atomic_pipeline.py b/scripts/release/atomic_pipeline.py index b24143dad..8ea5831e1 100755 --- a/scripts/release/atomic_pipeline.py +++ b/scripts/release/atomic_pipeline.py @@ -8,10 +8,9 @@ from concurrent.futures import ProcessPoolExecutor from copy import copy from queue import Queue -from typing import Dict, List, Optional, Tuple +from typing import Dict, Optional, Tuple import requests -import semver from opentelemetry import trace from packaging.version import Version @@ -23,6 +22,10 @@ sign_image, verify_signature, ) +from scripts.release.detect_ops_manager_changes import ( + detect_ops_manager_changes, + get_all_agents_for_rebuild, +) TRACER = trace.get_tracer("evergreen-agent") @@ -273,23 +276,23 @@ def build_upgrade_hook_image(build_configuration: ImageBuildConfiguration): ) -def build_agent_default_case(build_configuration: ImageBuildConfiguration): +def build_agent(build_configuration: ImageBuildConfiguration): """ Build the agent only for the latest operator for patches and operator releases. """ - release = load_release_file() - - # We need to release [all agents x latest operator] on operator releases - if build_configuration.is_release_scenario(): - agent_versions_to_build = gather_all_supported_agent_versions(release) - # We only need [latest agents (for each OM major version and for CM) x patch ID] for patches + if build_configuration.all_agents: + agent_versions_to_build = get_all_agents_for_rebuild() + logger.info("building all agents") else: - agent_versions_to_build = gather_latest_agent_versions(release) + agent_versions_to_build = detect_ops_manager_changes() + logger.info("building agents for changed OM versions") - logger.info( - f"Building Agent versions: {agent_versions_to_build} for Operator versions: {build_configuration.version}" - ) + if not agent_versions_to_build: + logger.info("No changes detected, skipping agent build") + return + + logger.info(f"Building Agent versions: {agent_versions_to_build}") tasks_queue = Queue() max_workers = 1 @@ -304,7 +307,7 @@ def build_agent_default_case(build_configuration: ImageBuildConfiguration): # We don't need to keep create and push the same image on every build. # It is enough to create and push the non-operator suffixed images only during releases to ecr and quay. logger.info(f"======= Building Agent {agent_tools_version} ({idx}/{len(agent_versions_to_build)})") - _build_agent_operator( + _build_agents( agent_tools_version, build_configuration, executor, @@ -314,64 +317,7 @@ def build_agent_default_case(build_configuration: ImageBuildConfiguration): queue_exception_handling(tasks_queue) -def gather_all_supported_agent_versions(release: Dict) -> List[Tuple[str, str]]: - # This is a list of a tuples - agent version and corresponding tools version - agent_versions_to_build = list() - agent_versions_to_build.append( - ( - release["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["cloud_manager"], - release["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["cloud_manager_tools"], - ) - ) - for _, om in release["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["ops_manager"].items(): - agent_versions_to_build.append((om["agent_version"], om["tools_version"])) - - # lets not build the same image multiple times - return sorted(list(set(agent_versions_to_build))) - - -def gather_latest_agent_versions(release: Dict) -> List[Tuple[str, str]]: - """ - This function is used when we release a new agent via OM bump. - That means we will need to release that agent with all supported operators. - Since we don’t want to release all agents again, we only release the latest, which will contain the newly added one - :return: the latest agent for each major version - """ - agent_versions_to_build = list() - agent_versions_to_build.append( - ( - release["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["cloud_manager"], - release["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["cloud_manager_tools"], - ) - ) - - latest_versions = {} - - for version in release["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["ops_manager"].keys(): - parsed_version = semver.VersionInfo.parse(version) - major_version = parsed_version.major - if major_version in latest_versions: - latest_parsed_version = semver.VersionInfo.parse(str(latest_versions[major_version])) - latest_versions[major_version] = max(parsed_version, latest_parsed_version) - else: - latest_versions[major_version] = version - - for major_version, latest_version in latest_versions.items(): - agent_versions_to_build.append( - ( - release["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["ops_manager"][str(latest_version)][ - "agent_version" - ], - release["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["ops_manager"][str(latest_version)][ - "tools_version" - ], - ) - ) - - return sorted(list(set(agent_versions_to_build))) - - -def _build_agent_operator( +def _build_agents( agent_tools_version: Tuple[str, str], build_configuration: ImageBuildConfiguration, executor: ProcessPoolExecutor, diff --git a/scripts/release/build/image_build_configuration.py b/scripts/release/build/image_build_configuration.py index 966b7d02e..72fe8db8d 100644 --- a/scripts/release/build/image_build_configuration.py +++ b/scripts/release/build/image_build_configuration.py @@ -17,6 +17,7 @@ class ImageBuildConfiguration: parallel_factor: int = 0 platforms: Optional[List[str]] = None sign: bool = False + all_agents: bool = False def is_release_scenario(self) -> bool: return self.scenario == BuildScenario.RELEASE diff --git a/scripts/release/detect_ops_manager_changes.py b/scripts/release/detect_ops_manager_changes.py new file mode 100644 index 000000000..5276497d8 --- /dev/null +++ b/scripts/release/detect_ops_manager_changes.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +""" +Detects changes to opsManagerMapping in release.json for triggering agent releases. +Relies on git origin/master vs local release.json +""" +import json +import logging +import subprocess +import sys +from typing import Dict, List, Optional, Tuple + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[logging.StreamHandler(sys.stdout)], +) +logger = logging.getLogger(__name__) + + +def get_content_from_git(commit: str, file_path: str) -> Optional[str]: + try: + result = subprocess.run( + ["git", "show", f"{commit}:{file_path}"], capture_output=True, text=True, check=True, timeout=30 + ) + return result.stdout + except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e: + logger.error(f"Failed to get {file_path} from git commit {commit}: {e}") + return None + + +def load_release_json_from_master() -> Optional[Dict]: + base_revision = "origin/master" + + content = get_content_from_git(base_revision, "release.json") + if not content: + logger.error(f"Could not retrieve release.json from {base_revision}") + return None + + try: + return json.loads(content) + except json.JSONDecodeError as e: + logger.error(f"Invalid JSON in base release.json: {e}") + return None + + +def load_current_release_json() -> Optional[Dict]: + try: + with open("release.json", "r") as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError) as e: + logger.error(f"Could not load current release.json: {e}") + return None + + +def extract_ops_manager_mapping(release_data: Dict) -> Dict: + if not release_data: + return {} + return release_data.get("supportedImages", {}).get("mongodb-agent", {}).get("opsManagerMapping", {}) + + +def get_changed_agents(current_mapping: Dict, base_mapping: Dict) -> List[Tuple[str, str]]: + """Returns list of (agent_version, tools_version) tuples for added/changed agents""" + added_agents = [] + + current_om_mapping = current_mapping.get("ops_manager", {}) + master_om_mapping = base_mapping.get("ops_manager", {}) + + for om_version, agent_tools_version in current_om_mapping.items(): + if om_version not in master_om_mapping or master_om_mapping[om_version] != agent_tools_version: + added_agents.append((agent_tools_version["agent_version"], agent_tools_version["tools_version"])) + + current_cm = current_mapping.get("cloud_manager") + master_cm = base_mapping.get("cloud_manager") + current_cm_tools = current_mapping.get("cloud_manager_tools") + master_cm_tools = base_mapping.get("cloud_manager_tools") + + if current_cm != master_cm or current_cm_tools != master_cm_tools: + added_agents.append((current_cm, current_cm_tools)) + + return list(set(added_agents)) + + +def get_tools_version_for_agent(agent_version: str) -> str: + """Get tools version for a given agent version from release.json""" + release_data = load_current_release_json() + if not release_data: + return "100.12.2" # Default fallback + + ops_manager_mapping = extract_ops_manager_mapping(release_data) + ops_manager_versions = ops_manager_mapping.get("ops_manager", {}) + + # Search through all OM versions to find matching agent version + for om_version, agent_tools in ops_manager_versions.items(): + if agent_tools.get("agent_version") == agent_version: + return agent_tools.get("tools_version", "100.12.2") + + # Check cloud_manager tools version as fallback + return ops_manager_mapping.get("cloud_manager_tools", "100.12.2") + + +def get_all_agents_for_rebuild() -> List[Tuple[str, str]]: + """Returns list of (agent_version, tools_version) tuples for all agents in release.json""" + agents = [] + + release_data = load_current_release_json() + if not release_data: + logger.error("Could not load release.json") + return [] + + ops_manager_mapping = extract_ops_manager_mapping(release_data) + + # Get all ops_manager agents + ops_manager_versions = ops_manager_mapping.get("ops_manager", {}) + for om_version, agent_tools in ops_manager_versions.items(): + agent_version = agent_tools.get("agent_version") + tools_version = agent_tools.get("tools_version") + if agent_version and tools_version: + agents.append((agent_version, tools_version)) + + # Get cloud_manager agent + cloud_manager_agent = ops_manager_mapping.get("cloud_manager") + cloud_manager_tools = ops_manager_mapping.get("cloud_manager_tools") + if cloud_manager_agent and cloud_manager_tools: + agents.append((cloud_manager_agent, cloud_manager_tools)) + + # Get the main agent version from release.json root + main_agent_version = release_data.get("agentVersion") + if main_agent_version: + tools_version = get_tools_version_for_agent(main_agent_version) + agents.append((main_agent_version, tools_version)) + + return list(set(agents)) + + +def detect_ops_manager_changes() -> List[Tuple[str, str]]: + """Returns (has_changes, changed_agents_list)""" + logger.info("=== Detecting OM Mapping Changes (Local vs Base) ===") + + current_release = load_current_release_json() + if not current_release: + logger.error("Could not load current local release.json") + return [] + + master_release = load_release_json_from_master() + if not master_release: + logger.warning("Could not load base release.json, assuming changes exist") + return [] + + current_mapping = extract_ops_manager_mapping(current_release) + base_mapping = extract_ops_manager_mapping(master_release) + + if current_mapping != base_mapping: + return get_changed_agents(current_mapping, base_mapping) + else: + return [] diff --git a/scripts/release/pipeline_main.py b/scripts/release/pipeline_main.py index c7e2c0c97..b5fe22d9d 100644 --- a/scripts/release/pipeline_main.py +++ b/scripts/release/pipeline_main.py @@ -17,7 +17,7 @@ from lib.base_logger import logger from scripts.release.atomic_pipeline import ( - build_agent_default_case, + build_agent, build_database_image, build_init_appdb_image, build_init_database_image, @@ -74,7 +74,7 @@ def get_builder_function_for_image_name() -> Dict[str, Callable]: READINESS_PROBE_IMAGE: build_readiness_probe_image, UPGRADE_HOOK_IMAGE: build_upgrade_hook_image, DATABASE_IMAGE: build_database_image, - AGENT_IMAGE: build_agent_default_case, + AGENT_IMAGE: build_agent, # Init images INIT_APPDB_IMAGE: build_init_appdb_image, INIT_DATABASE_IMAGE: build_init_database_image, @@ -123,6 +123,7 @@ def image_build_config_from_args(args) -> ImageBuildConfiguration: platforms=platforms, sign=sign, parallel_factor=args.parallel_factor, + all_agents=args.all_agents, ) @@ -251,6 +252,11 @@ def main(): type=int, help="Number of agent builds to run in parallel, defaults to number of cores", ) + parser.add_argument( + "--all-agents", + action="store_true", + help="Build all agent images.", + ) args = parser.parse_args() diff --git a/scripts/release/tests/test_detect_ops_manager_changes.py b/scripts/release/tests/test_detect_ops_manager_changes.py new file mode 100644 index 000000000..8606c4424 --- /dev/null +++ b/scripts/release/tests/test_detect_ops_manager_changes.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python3 +""" +Tests for scripts.release.detect_ops_manager_changes.py +""" +import json +import os +import subprocess +import sys +import unittest +from unittest.mock import MagicMock, mock_open, patch + +from scripts.release.detect_ops_manager_changes import ( + detect_ops_manager_changes, + extract_ops_manager_mapping, + get_content_from_git, + load_current_release_json, +) + + +class TestDetectOpsManagerChanges(unittest.TestCase): + + def setUp(self): + """Set up test fixtures""" + self.master_release_data = { + "supportedImages": { + "mongodb-agent": { + "opsManagerMapping": { + "cloud_manager": "13.37.0.9590-1", + "cloud_manager_tools": "100.12.2", + "ops_manager": { + "6.0.26": {"agent_version": "12.0.34.7888-1", "tools_version": "100.10.0"}, + "7.0.11": {"agent_version": "107.0.11.8645-1", "tools_version": "100.10.0"}, + }, + } + } + } + } + + self.current_release_data = { + "supportedImages": { + "mongodb-agent": { + "opsManagerMapping": { + "cloud_manager": "13.37.0.9590-1", + "cloud_manager_tools": "100.12.2", + "ops_manager": { + "6.0.26": {"agent_version": "12.0.34.7888-1", "tools_version": "100.10.0"}, + "7.0.11": {"agent_version": "107.0.11.8645-1", "tools_version": "100.10.0"}, + }, + } + } + } + } + + self.evergreen_content = """ +variables: + - &ops_manager_60_latest 6.0.27 # The order/index is important + - &ops_manager_70_latest 7.0.17 # The order/index is important + - &ops_manager_80_latest 8.0.12 # The order/index is important +""" + + def test_extract_ops_manager_mapping_valid(self): + """Test extracting opsManagerMapping from valid release data""" + mapping = extract_ops_manager_mapping(self.master_release_data) + expected = { + "cloud_manager": "13.37.0.9590-1", + "cloud_manager_tools": "100.12.2", + "ops_manager": { + "6.0.26": {"agent_version": "12.0.34.7888-1", "tools_version": "100.10.0"}, + "7.0.11": {"agent_version": "107.0.11.8645-1", "tools_version": "100.10.0"}, + }, + } + self.assertEqual(mapping, expected) + + def test_extract_ops_manager_mapping_empty(self): + """Test extracting from empty/invalid data""" + self.assertEqual(extract_ops_manager_mapping({}), {}) + self.assertEqual(extract_ops_manager_mapping(None), {}) + + def test_extract_ops_manager_mapping_missing_keys(self): + """Test extracting when keys are missing""" + incomplete_data = {"supportedImages": {}} + self.assertEqual(extract_ops_manager_mapping(incomplete_data), {}) + + @patch("builtins.open", new_callable=mock_open) + @patch("os.path.exists", return_value=True) + def test_load_current_release_json_success(self, mock_exists, mock_file): + """Test successfully loading current release.json""" + mock_file.return_value.read.return_value = json.dumps(self.current_release_data) + + result = load_current_release_json() + self.assertEqual(result, self.current_release_data) + + @patch("builtins.open", side_effect=FileNotFoundError) + def test_load_current_release_json_not_found(self, mock_file): + """Test handling missing release.json""" + result = load_current_release_json() + self.assertIsNone(result) + + @patch("builtins.open", new_callable=mock_open) + @patch("os.path.exists", return_value=True) + def test_load_current_release_json_invalid_json(self, mock_exists, mock_file): + """Test handling invalid JSON in release.json""" + mock_file.return_value.read.return_value = "invalid json" + + result = load_current_release_json() + self.assertIsNone(result) + + @patch("subprocess.run") + def test_safe_git_show_success(self, mock_run): + """Test successful git show operation""" + mock_result = MagicMock() + mock_result.stdout = json.dumps(self.master_release_data) + mock_run.return_value = mock_result + + result = get_content_from_git("abc123", "release.json") + self.assertEqual(result, json.dumps(self.master_release_data)) + + mock_run.assert_called_once_with( + ["git", "show", "abc123:release.json"], capture_output=True, text=True, check=True, timeout=30 + ) + + @patch("subprocess.run", side_effect=subprocess.CalledProcessError(1, "git")) + def test_safe_git_show_failure(self, mock_run): + """Test git show failure handling""" + result = get_content_from_git("abc123", "release.json") + self.assertIsNone(result) + + def test_no_changes_detected(self): + """Test when no changes are detected""" + with ( + patch( + "scripts.release.detect_ops_manager_changes.load_current_release_json", + return_value=self.current_release_data, + ), + patch( + "scripts.release.detect_ops_manager_changes.load_release_json_from_master", + return_value=self.master_release_data, + ), + ): + + changed_agents = detect_ops_manager_changes() + self.assertEqual(changed_agents, []) + + def test_new_ops_manager_version_added(self): + """Test detection when new OM version is added""" + modified_current = json.loads(json.dumps(self.current_release_data)) + modified_current["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["ops_manager"]["8.0.0"] = { + "agent_version": "108.0.0.8694-1", + "tools_version": "100.10.0", + } + + with ( + patch( + "scripts.release.detect_ops_manager_changes.load_current_release_json", return_value=modified_current + ), + patch( + "scripts.release.detect_ops_manager_changes.load_release_json_from_master", + return_value=self.master_release_data, + ), + ): + + changed_agents = detect_ops_manager_changes() + self.assertIn(("108.0.0.8694-1", "100.10.0"), changed_agents) + + def test_ops_manager_version_modified(self): + """Test detection when OM version is modified""" + modified_current = json.loads(json.dumps(self.current_release_data)) + modified_current["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["ops_manager"]["6.0.26"][ + "agent_version" + ] = "12.0.35.7911-1" + + with ( + patch( + "scripts.release.detect_ops_manager_changes.load_current_release_json", return_value=modified_current + ), + patch( + "scripts.release.detect_ops_manager_changes.load_release_json_from_master", + return_value=self.master_release_data, + ), + ): + + changed_agents = detect_ops_manager_changes() + self.assertIn(("12.0.35.7911-1", "100.10.0"), changed_agents) + + def test_cloud_manager_changed(self): + """Test detection when cloud_manager is changed""" + modified_current = json.loads(json.dumps(self.current_release_data)) + modified_current["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["cloud_manager"] = "13.38.0.9600-1" + + with ( + patch( + "scripts.release.detect_ops_manager_changes.load_current_release_json", return_value=modified_current + ), + patch( + "scripts.release.detect_ops_manager_changes.load_release_json_from_master", + return_value=self.master_release_data, + ), + ): + + changed_agents = detect_ops_manager_changes() + self.assertIn(("13.38.0.9600-1", "100.12.2"), changed_agents) + + def test_cloud_manager_tools_changed(self): + """Test detection when cloud_manager_tools is changed""" + modified_current = json.loads(json.dumps(self.current_release_data)) + modified_current["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["cloud_manager_tools"] = "100.13.0" + + with ( + patch( + "scripts.release.detect_ops_manager_changes.load_current_release_json", return_value=modified_current + ), + patch( + "scripts.release.detect_ops_manager_changes.load_release_json_from_master", + return_value=self.master_release_data, + ), + ): + + changed_agents = detect_ops_manager_changes() + self.assertIn(("13.37.0.9590-1", "100.13.0"), changed_agents) + + def test_ops_manager_version_removed(self): + """Test detection when OM version is removed""" + modified_current = json.loads(json.dumps(self.current_release_data)) + del modified_current["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["ops_manager"]["7.0.11"] + + with ( + patch( + "scripts.release.detect_ops_manager_changes.load_current_release_json", return_value=modified_current + ), + patch( + "scripts.release.detect_ops_manager_changes.load_release_json_from_master", + return_value=self.master_release_data, + ), + ): + + changed_agents = detect_ops_manager_changes() + self.assertEqual(changed_agents, []) + + def test_both_om_and_cm_changed(self): + """Test detection when both OM version and cloud manager are changed""" + modified_current = json.loads(json.dumps(self.current_release_data)) + modified_current["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["ops_manager"]["8.0.0"] = { + "agent_version": "108.0.0.8694-1", + "tools_version": "100.10.0", + } + modified_current["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["cloud_manager"] = "13.38.0.9600-1" + + with ( + patch( + "scripts.release.detect_ops_manager_changes.load_current_release_json", return_value=modified_current + ), + patch( + "scripts.release.detect_ops_manager_changes.load_release_json_from_master", + return_value=self.master_release_data, + ), + ): + + changed_agents = detect_ops_manager_changes() + self.assertIn(("108.0.0.8694-1", "100.10.0"), changed_agents) + self.assertIn(("13.38.0.9600-1", "100.12.2"), changed_agents) + self.assertEqual(len(changed_agents), 2) + + def test_current_release_load_failure(self): + """Test handling when current release.json cannot be loaded""" + with ( + patch("scripts.release.detect_ops_manager_changes.load_current_release_json", return_value=None), + patch( + "scripts.release.detect_ops_manager_changes.load_release_json_from_master", + return_value=self.master_release_data, + ), + ): + + changed_agents = detect_ops_manager_changes() + self.assertEqual(changed_agents, []) + + def test_base_release_load_failure_fail_safe(self): + """Test fail-safe behavior when base release.json cannot be loaded""" + with ( + patch( + "scripts.release.detect_ops_manager_changes.load_current_release_json", + return_value=self.current_release_data, + ), + patch("scripts.release.detect_ops_manager_changes.load_release_json_from_master", return_value=None), + ): + + changed_agents = detect_ops_manager_changes() + self.assertEqual(changed_agents, []) + + +if __name__ == "__main__": + unittest.main()