diff --git a/.evergreen-functions.yml b/.evergreen-functions.yml index b56dd947a..83b6f4d08 100644 --- a/.evergreen-functions.yml +++ b/.evergreen-functions.yml @@ -496,6 +496,7 @@ functions: IMAGE_NAME: ${image_name} BUILD_SCENARIO_OVERRIDE: ${build_scenario} FLAGS: ${flags} + AGENT_VERSION_OVERRIDE: ${agent_version} teardown_cloud_qa_all: - command: shell.exec diff --git a/.evergreen.yml b/.evergreen.yml index 3b0caf6b1..bc6c0906d 100644 --- a/.evergreen.yml +++ b/.evergreen.yml @@ -287,19 +287,21 @@ tasks: commands: - func: lint_repo - # 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) - allowed_requesters: [ "patch", "github_pr" ] + # Runs on every merge - detects release.json changes and releases appropriate images + - name: release_om_and_agents + allowed_requesters: [ "patch" , "commit"] commands: - func: clone + - func: python_venv - func: setup_building_host - func: quay_login - func: setup_docker_sbom - - func: pipeline - vars: - image_name: agent - build_scenario: release + - command: subprocess.exec + params: + working_dir: src/github.com/mongodb/mongodb-kubernetes + binary: scripts/dev/run_python.sh + args: + - scripts/release/release_om_and_agents.py - name: migrate_all_agents # this enables us to run this variant manually to build all the agents for the new agent registry @@ -343,8 +345,9 @@ tasks: - func: quay_login - func: pipeline vars: + agent_version: all image_name: agent - flags: "--parallel --all-agents --skip-if-exists=false" + flags: "--parallel --skip-if-exists=false" - name: rebuild_currently_used_agents # this enables us to run this manually (patch) and rebuild current agent versions to verify @@ -356,8 +359,9 @@ tasks: - func: quay_login - func: pipeline vars: + agent_version: current image_name: agent - flags: "--parallel --current-agents --skip-if-exists=false" + flags: "--parallel --skip-if-exists=false" - name: build_kubectl_mongodb_plugin commands: @@ -463,7 +467,8 @@ tasks: - func: pipeline vars: image_name: agent - flags: "--parallel --all-agents" + agent_version: all + flags: "--parallel" - name: build_init_database_image_ubi commands: @@ -749,7 +754,7 @@ task_groups: - e2e_sharded_cluster_scram_sha_256_switch_project - e2e_replica_set_scram_sha_1_switch_project - e2e_sharded_cluster_scram_sha_1_switch_project - # TODO CLOUDP-349093 - Disabled these tests as they don't use the password secret, and project migrations aren't fully supported yet. + # TODO CLOUDP-349093 - Disabled these tests as they don't use the password secret, and project migrations aren't fully supported yet. # e2e_sharded_cluster_x509_switch_project # e2e_replica_set_x509_switch_project # e2e_replica_set_ldap_switch_project @@ -1893,26 +1898,6 @@ buildvariants: tasks: - name: build_om_images - # It will be called by pct while bumping the agent cloud manager image - - name: release_agent - display_name: release_agent - tags: [ "manual_patch", "release_agent" ] - run_on: - - release-ubuntu2404-small # This is required for CISA attestation https://jira.mongodb.org/browse/DEVPROD-17780 - depends_on: - - 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 - name: '*' - - variant: e2e_mdb_kind_ubi_cloudqa - name: '*' - - variant: e2e_static_mdb_kind_ubi_cloudqa - name: '*' - tasks: - - name: release_agent - # Only called manually, It's used for testing the task release_agents in case the release.json # has not changed, and you still want to push the images to registry. - name: manual_release_all_agents @@ -1946,51 +1931,6 @@ buildvariants: - name: backup_csv_images_limit_3 - name: backup_csv_images_all - - name: publish_om60_images - display_name: publish_om60_images - tags: [ "manual_patch" ] - allowed_requesters: [ "patch", "github_pr" ] - run_on: - - release-ubuntu2404-small # This is required for CISA attestation https://jira.mongodb.org/browse/DEVPROD-17780 - depends_on: - - variant: e2e_om60_kind_ubi - name: '*' - - variant: e2e_static_om60_kind_ubi - name: '*' - tasks: - - name: publish_ops_manager - - name: release_agent - - - name: publish_om70_images - display_name: publish_om70_images - tags: [ "manual_patch" ] - allowed_requesters: [ "patch", "github_pr" ] - run_on: - - release-ubuntu2404-small # This is required for CISA attestation https://jira.mongodb.org/browse/DEVPROD-17780 - depends_on: - - variant: e2e_om70_kind_ubi - name: '*' - - variant: e2e_static_om70_kind_ubi - name: '*' - tasks: - - name: publish_ops_manager - - name: release_agent - - - name: publish_om80_images - display_name: publish_om80_images - tags: [ "manual_patch" ] - allowed_requesters: [ "patch", "github_pr" ] - run_on: - - release-ubuntu2404-small # This is required for CISA attestation https://jira.mongodb.org/browse/DEVPROD-17780 - depends_on: - - variant: e2e_om80_kind_ubi - name: '*' - - variant: e2e_static_om80_kind_ubi - name: '*' - tasks: - - name: publish_ops_manager - - name: release_agent - - name: migrate_all_agents display_name: migrate_all_agents tags: [ "manual_patch" ] @@ -1999,3 +1939,13 @@ buildvariants: - ubuntu2404-large tasks: - name: migrate_all_agents + + # Runs on every merge to master and releases images auxiliary to the operator release like OM and the Agent + - name: release_om_and_agents + display_name: release_om_and_agents + allowed_requesters: [ "patch" , "commit"] + run_on: + - release-ubuntu2404-small + patchable: false # Only run on commit builds + tasks: + - name: release_om_and_agents diff --git a/scripts/release/agent/detect_ops_manager_changes.py b/scripts/release/agent/agents_to_rebuild.py similarity index 71% rename from scripts/release/agent/detect_ops_manager_changes.py rename to scripts/release/agent/agents_to_rebuild.py index cf5b70740..b1f447385 100644 --- a/scripts/release/agent/detect_ops_manager_changes.py +++ b/scripts/release/agent/agents_to_rebuild.py @@ -19,32 +19,6 @@ 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: @@ -60,28 +34,6 @@ def extract_ops_manager_mapping(release_data: Dict) -> Dict: 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() @@ -206,26 +158,3 @@ def get_currently_used_agents() -> List[Tuple[str, str]]: except Exception as e: logger.error(f"Error getting currently used agents: {e}") return [] - - -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/atomic_pipeline.py b/scripts/release/atomic_pipeline.py index e94ed8709..19c936fdd 100755 --- a/scripts/release/atomic_pipeline.py +++ b/scripts/release/atomic_pipeline.py @@ -15,8 +15,7 @@ from opentelemetry import trace from lib.base_logger import logger -from scripts.release.agent.detect_ops_manager_changes import ( - detect_ops_manager_changes, +from scripts.release.agent.agents_to_rebuild import ( get_all_agents_for_rebuild, get_currently_used_agents, ) @@ -330,22 +329,23 @@ def build_upgrade_hook_image(build_configuration: ImageBuildConfiguration): def build_agent(build_configuration: ImageBuildConfiguration): - """ - Build the agent only for the latest operator for patches and operator releases. + """Build the agent image(s). Validation happens in pipeline.py.""" + version = build_configuration.version - """ - if build_configuration.all_agents: + if version == "all": agent_versions_to_build = get_all_agents_for_rebuild() logger.info("building all agents") - elif build_configuration.currently_used_agents: + elif version == "current": agent_versions_to_build = get_currently_used_agents() - logger.info("building current used agents") + logger.info("building currently used agents") + elif version and build_configuration.agent_tools_version: + agent_versions_to_build = [(version, build_configuration.agent_tools_version)] + logger.info(f"building agent {version} with tools {build_configuration.agent_tools_version}") else: - agent_versions_to_build = detect_ops_manager_changes() - logger.info("building agents for changed OM versions") + raise ValueError("No agent selection provided - this should be caught by pipeline.py validation") if not agent_versions_to_build: - logger.info("No changes detected, skipping agent build") + logger.warning("No agent versions found to build") return logger.info(f"Building Agent versions: {agent_versions_to_build}") diff --git a/scripts/release/build/image_build_configuration.py b/scripts/release/build/image_build_configuration.py index 16c8f7766..da1007235 100644 --- a/scripts/release/build/image_build_configuration.py +++ b/scripts/release/build/image_build_configuration.py @@ -4,8 +4,7 @@ from scripts.release.build.build_scenario import BuildScenario from scripts.release.build.image_build_process import ImageBuilder -SUPPORTED_PLATFORMS = ["darwin/amd64", "darwin/arm64", "linux/amd64", "linux/arm64", "linux/s390x", - "linux/ppc64le"] +SUPPORTED_PLATFORMS = ["darwin/amd64", "darwin/arm64", "linux/amd64", "linux/arm64", "linux/s390x", "linux/ppc64le"] @dataclass @@ -24,9 +23,8 @@ class ImageBuildConfiguration: # Agent specific parallel: bool = False parallel_factor: int = 0 - all_agents: bool = False - currently_used_agents: bool = False architecture_suffix: bool = False + agent_tools_version: Optional[str] = None # Explicit tools version for agent builds def is_release_scenario(self) -> bool: return self.scenario == BuildScenario.RELEASE diff --git a/scripts/release/pipeline.py b/scripts/release/pipeline.py index 32c2db904..1048a983f 100644 --- a/scripts/release/pipeline.py +++ b/scripts/release/pipeline.py @@ -62,6 +62,9 @@ ) from scripts.release.build.image_build_process import PodmanImageBuilder +CURRENT_AGENTS = "current" +ALL_AGENTS = "all" + """ The goal of main.py, image_build_configuration.py and build_context.py is to provide a single source of truth for the build configuration. All parameters that depend on the the build environment (local dev, evg, etc) should be resolved here and @@ -139,9 +142,25 @@ def image_build_config_from_args(args) -> ImageBuildConfiguration: if type(builder) is PodmanImageBuilder and len(platforms) > 1: raise ValueError("Cannot use Podman builder with multi-platform builds") - # Validate version - only agent can have None version as the versions are managed by the agent - # which are externally retrieved from release.json - if version is None and image != "agent": + # Get agent_tools_version for agent builds (from --agent-tools-version arg) + agent_tools_version = getattr(args, "agent_tools_version", None) + + # Validate version requirements + if image == "agent": + # Agent builds: version can be "all", "current", or explicit version (requires agent_tools_version) + if version is None: + raise ValueError( + "Agent build requires --version. Use one of:\n" + " --version all (for all agents in release.json)\n" + " --version current (for currently used agents)\n" + " --version --agent-tools-version (for specific agent)" + ) + is_special_version = version in (ALL_AGENTS, CURRENT_AGENTS) + if not is_special_version and agent_tools_version is None: + raise ValueError( + f"For agent builds with explicit version '{version}', --agent-tools-version must also be provided." + ) + elif version is None: raise ValueError(f"Version cannot be empty for {image}.") return ImageBuildConfiguration( @@ -157,9 +176,8 @@ def image_build_config_from_args(args) -> ImageBuildConfiguration: skip_if_exists=skip_if_exists, parallel=args.parallel, parallel_factor=args.parallel_factor, - all_agents=args.all_agents, - currently_used_agents=args.current_agents, architecture_suffix=architecture_suffix, + agent_tools_version=agent_tools_version, ) @@ -274,14 +292,11 @@ def main(): 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.", - ) - parser.add_argument( - "--current-agents", - action="store_true", - help="Build all currently used agent images.", + "--agent-tools-version", + metavar="", + action="store", + type=str, + help="Tools version to use when building agent image. Required when --version is an explicit version (not 'all' or 'current').", ) parser.add_argument( "--architecture-suffix", diff --git a/scripts/release/pipeline.sh b/scripts/release/pipeline.sh index f28a00556..53b965173 100755 --- a/scripts/release/pipeline.sh +++ b/scripts/release/pipeline.sh @@ -9,7 +9,9 @@ args+=(--build-scenario "${BUILD_SCENARIO_OVERRIDE:-${BUILD_SCENARIO}}") case ${IMAGE_NAME} in "agent") - IMAGE_VERSION="" + # For agent: version can be "all", "current", or explicit version (requires TOOLS_VERSION) + # AGENT_VERSION_OVERRIDE takes precedence, then AGENT_VERSION from context. + IMAGE_VERSION="${AGENT_VERSION_OVERRIDE:-${AGENT_VERSION:-}}" ;; "ops-manager") @@ -33,6 +35,11 @@ if [[ "${IMAGE_VERSION:-}" != "" ]]; then args+=(--version "${IMAGE_VERSION}") fi +# For agent builds, pass tools version if explicitly provided +if [[ "${IMAGE_NAME}" == "agent" && "${TOOLS_VERSION:-}" != "" ]]; then + args+=(--agent-tools-version "${TOOLS_VERSION}") +fi + if [[ "${FLAGS:-}" != "" ]]; then IFS=" " read -ra flags <<< "${FLAGS}" args+=("${flags[@]}") diff --git a/scripts/release/release_om_and_agents.py b/scripts/release/release_om_and_agents.py new file mode 100644 index 000000000..a5b261bce --- /dev/null +++ b/scripts/release/release_om_and_agents.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +""" +Releases all agent and ops-manager images on every merge to master. + +Releases: +1. cloud_manager agent (from release.json) +2. For each OM version defined as anchors in .evergreen.yml: + - The ops-manager image + - The corresponding agent (from release.json opsManagerMapping) + +skip_if_exists handles already-published images. + +Usage: + python release_om_and_agents.py [--dry-run] +""" +import argparse +import json +import logging +import subprocess +import sys +from pathlib import Path +from typing import Dict, List, Set + +import semver +import yaml + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + +# Track releases for summary +_releases: List[Dict[str, str]] = [] + + +def run_command(cmd: List[str], dry_run: bool = False) -> bool: + cmd_str = " ".join(cmd) + logger.info(f"Running: {cmd_str}") + if dry_run: + return True + try: + subprocess.run(cmd, check=True) + return True + except subprocess.CalledProcessError as e: + logger.error(f"Command failed with exit code {e.returncode}") + return False + + +def track_release(release_type: str, version: str, status: str, context: str = ""): + _releases.append( + { + "type": release_type, + "version": version, + "status": status, + "context": context, + } + ) + + +def print_summary(dry_run: bool = False): + """Print summary of all releases.""" + if not _releases: + return + + def icon(status: str) -> str: + if dry_run: + return "○" + return "✓" if status == "success" else "✗" + + print("\n" + "=" * 60) + print("DRY RUN SUMMARY:" if dry_run else "RELEASE SUMMARY:") + print("=" * 60) + + for release_type, label in [("agent", "Agents"), ("ops-manager", "Ops Manager")]: + items = [r for r in _releases if r["type"] == release_type] + if items: + print(f"\n{label}:") + for r in items: + ctx = f" ({r['context']})" if r.get("context") else "" + print(f" {icon(r['status'])} {r['version']}{ctx}") + + print(f"\nTotal: {len(_releases)} releases") + print("=" * 60 + "\n") + + +def load_release_json() -> Dict: + with open("release.json", "r") as f: + return json.load(f) + + +def get_ops_manager_mapping(release_data: Dict) -> Dict: + return release_data.get("supportedImages", {}).get("mongodb-agent", {}).get("opsManagerMapping", {}) + + +def get_latest_om_versions_from_evergreen_yaml() -> Set[str]: + """ + Extract OM versions from .evergreen.yml anchors using YAML parsing and semver. + + Returns: {"6.0.27", "7.0.19", "8.0.16"} + Raises: RuntimeError if file is missing, malformed, or contains no valid versions + """ + versions = set() + evergreen_path = Path(".evergreen.yml") + + if not evergreen_path.exists(): + raise RuntimeError(f".evergreen.yml not found at {evergreen_path.absolute()}") + + try: + with open(evergreen_path, "r") as f: + data = yaml.safe_load(f) + except yaml.YAMLError as e: + raise RuntimeError(f"Failed to parse .evergreen.yml: {e}") from e + except Exception as e: + raise RuntimeError(f"Failed to read .evergreen.yml: {e}") from e + + # Extract version strings from the variables list + # The YAML structure is: variables: [6.0.27, 7.0.19, 8.0.16, ...] + variables = data.get("variables", []) + + if not variables: + raise RuntimeError("No 'variables' list found in .evergreen.yml") + + for var in variables: + if not isinstance(var, str): + logger.debug(f"Skipping non-string variable: {type(var).__name__}") + continue + + try: + semver.VersionInfo.parse(var) + except (ValueError, TypeError) as e: + logger.debug(f"Skipping non-semver variable '{var}': {e}") + continue + + versions.add(var) + + if not versions: + raise RuntimeError( + "No valid OM versions found in .evergreen.yml variables list. " + "Expected semver strings like '6.0.27', '7.0.19', '8.0.16'" + ) + + return versions + + +def release_agent(agent_version: str, tools_version: str, context: str, dry_run: bool = False) -> bool: + """Release an agent image.""" + cmd = [ + "python", + "scripts/release/pipeline.py", + "agent", + "--build-scenario", + "release", + "--version", + agent_version, + "--agent-tools-version", + tools_version, + ] + success = run_command(cmd, dry_run) + status = "pending" if dry_run else ("success" if success else "failed") + track_release("agent", agent_version, status, context) + return success + + +def release_ops_manager(om_version: str, dry_run: bool = False) -> bool: + """Release an ops-manager image.""" + cmd = [ + "python", + "scripts/release/pipeline.py", + "ops-manager", + "--build-scenario", + "release", + "--version", + om_version, + ] + success = run_command(cmd, dry_run) + status = "pending" if dry_run else ("success" if success else "failed") + track_release("ops-manager", om_version, status) + return success + + +def main(): + parser = argparse.ArgumentParser(description="Release all images on merge") + parser.add_argument("--dry-run", action="store_true", help="Print commands without executing") + args = parser.parse_args() + + release_data = load_release_json() + ops_manager_mapping = get_ops_manager_mapping(release_data) + latest_om_versions = get_latest_om_versions_from_evergreen_yaml() + + release_cm_agent(args, ops_manager_mapping) + release_om_and_agent(args, latest_om_versions, ops_manager_mapping) + + # Always print summary + print_summary(args.dry_run) + return 0 + + +def release_cm_agent(args, ops_manager_mapping: dict): + # 1. Release latest cloud_manager agent + cloud_manager_agent = ops_manager_mapping.get("cloud_manager") + cloud_manager_tools = ops_manager_mapping.get("cloud_manager_tools") + + if not cloud_manager_agent or not cloud_manager_tools: + raise RuntimeError("cloud_manager agent or tools not found in release.json opsManagerMapping") + + logger.info(f"=== Releasing cloud_manager agent: {cloud_manager_agent} ===") + if not release_agent(cloud_manager_agent, cloud_manager_tools, "cloud_manager", args.dry_run): + raise RuntimeError(f"Failed to release cloud_manager agent {cloud_manager_agent}") + + +def release_om_and_agent(args, latest_om_versions: set[str], ops_manager_mapping: dict): + # 2. Release each OM version and its agent + om_mapping = ops_manager_mapping.get("ops_manager", {}) + + for om_version in sorted(latest_om_versions): + logger.info(f"=== Processing OM {om_version} ===") + agent_info = om_mapping.get(om_version, {}) + agent_version = agent_info.get("agent_version") + tools_version = agent_info.get("tools_version") + + if not agent_version or not tools_version: + raise RuntimeError( + f"Agent mapping incomplete for OM {om_version} in release.json. " + f"Found: agent_version={agent_version}, tools_version={tools_version}" + ) + + # Release ops-manager image + logger.info(f"Releasing ops-manager {om_version}") + if not release_ops_manager(om_version, args.dry_run): + raise RuntimeError(f"Failed to release ops-manager {om_version}") + + # Release agent for this OM version + logger.info(f"Releasing agent {agent_version} for OM {om_version}") + if not release_agent(agent_version, tools_version, f"OM {om_version}", args.dry_run): + raise RuntimeError(f"Failed to release agent {agent_version} for OM {om_version}") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/release/tests/test_agents_to_rebuild.py b/scripts/release/tests/test_agents_to_rebuild.py new file mode 100644 index 000000000..77e8bc135 --- /dev/null +++ b/scripts/release/tests/test_agents_to_rebuild.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +Tests for scripts.release.agent.agents_to_rebuild.py +""" +import json +import unittest +from unittest.mock import mock_open, patch + +from scripts.release.agent.agents_to_rebuild import ( + extract_ops_manager_mapping, + get_all_agents_for_rebuild, + get_currently_used_agents, + get_tools_version_for_agent, + 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 +""" + + @patch("scripts.release.agent.agents_to_rebuild.load_current_release_json") + def test_get_all_agents_for_rebuild(self, mock_load): + """Test getting all agents for rebuild from release.json""" + release_data = { + "agentVersion": "13.37.0.9590-1", + "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"}, + }, + } + } + }, + } + mock_load.return_value = release_data + result = get_all_agents_for_rebuild() + + # Should contain ops_manager agents, cloud_manager agent, and main agent + self.assertIn(("12.0.34.7888-1", "100.10.0"), result) + self.assertIn(("107.0.11.8645-1", "100.10.0"), result) + self.assertIn(("13.37.0.9590-1", "100.12.2"), result) + + @patch("scripts.release.agent.agents_to_rebuild.glob.glob") + @patch("scripts.release.agent.agents_to_rebuild.load_current_release_json") + @patch("builtins.open", new_callable=mock_open) + @patch("os.path.isfile", return_value=True) + def test_get_currently_used_agents_with_context_files(self, mock_isfile, mock_file, mock_load, mock_glob): + """Test getting currently used agents from context files""" + release_data = { + "agentVersion": "13.37.0.9590-1", + "supportedImages": { + "mongodb-agent": { + "opsManagerMapping": { + "cloud_manager": "13.37.0.9590-1", + "cloud_manager_tools": "100.12.2", + "ops_manager": {}, + } + } + }, + } + mock_load.return_value = release_data + mock_glob.return_value = ["scripts/dev/contexts/test_context"] + mock_file.return_value.read.return_value = "export AGENT_VERSION=12.0.34.7888-1\n" + + result = get_currently_used_agents() + + self.assertIn(("12.0.34.7888-1", "100.12.2"), result) # falls back to cloud_manager_tools + self.assertIn(("13.37.0.9590-1", "100.12.2"), result) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/release/tests/test_detect_ops_manager_changes.py b/scripts/release/tests/test_detect_ops_manager_changes.py deleted file mode 100644 index 35b199cb5..000000000 --- a/scripts/release/tests/test_detect_ops_manager_changes.py +++ /dev/null @@ -1,255 +0,0 @@ -#!/usr/bin/env python3 -""" -Tests for scripts.release.agent.detect_ops_manager_changes.py -""" -import json -import subprocess -import unittest -from unittest.mock import MagicMock, mock_open, patch - -from scripts.release.agent.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.agent.detect_ops_manager_changes.load_current_release_json", - return_value=self.current_release_data, - ), - patch( - "scripts.release.agent.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.agent.detect_ops_manager_changes.load_current_release_json", - return_value=modified_current, - ), - patch( - "scripts.release.agent.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_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.agent.detect_ops_manager_changes.load_current_release_json", - return_value=modified_current, - ), - patch( - "scripts.release.agent.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.agent.detect_ops_manager_changes.load_current_release_json", - return_value=modified_current, - ), - patch( - "scripts.release.agent.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_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.agent.detect_ops_manager_changes.load_current_release_json", - return_value=modified_current, - ), - patch( - "scripts.release.agent.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.agent.detect_ops_manager_changes.load_current_release_json", return_value=None), - patch( - "scripts.release.agent.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.agent.detect_ops_manager_changes.load_current_release_json", - return_value=self.current_release_data, - ), - patch("scripts.release.agent.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()