mirror of https://github.com/mongodb/mongo
327 lines
11 KiB
Python
327 lines
11 KiB
Python
"""Helper functions to interact with evergreen."""
|
|
|
|
import os
|
|
import pathlib
|
|
from collections import deque
|
|
from pathlib import Path
|
|
from typing import Deque, Iterator, List, Optional, Set, Union
|
|
|
|
import requests
|
|
import structlog
|
|
from requests import HTTPError
|
|
|
|
from buildscripts.resmokelib.setup_multiversion.config import SetupMultiversionConfig
|
|
from evergreen import Patch, RetryingEvergreenApi, Task, Version
|
|
|
|
EVERGREEN_HOST = "https://evergreen.mongodb.com"
|
|
EVERGREEN_CONFIG_LOCATIONS = (
|
|
# Common for machines in Evergreen
|
|
os.path.join(os.getcwd(), ".evergreen.yml"),
|
|
# Common for local machines
|
|
os.path.expanduser(os.path.join("~", ".evergreen.yml")),
|
|
)
|
|
|
|
GENERIC_EDITION = "base"
|
|
GENERIC_PLATFORM = "linux_x86_64"
|
|
GENERIC_ARCHITECTURE = "x86_64"
|
|
|
|
LOGGER = structlog.getLogger(__name__)
|
|
|
|
|
|
class EvergreenConnError(Exception):
|
|
"""Errors in evergreen_conn.py."""
|
|
|
|
pass
|
|
|
|
|
|
def _find_evergreen_yaml_candidates() -> List[str]:
|
|
# Common for machines in Evergreen
|
|
candidates: List[Union[str, Path]] = [os.getcwd()]
|
|
|
|
cwd = pathlib.Path(os.getcwd())
|
|
# add every path that is the parent of CWD as well
|
|
for parent in cwd.parents:
|
|
candidates.append(parent)
|
|
|
|
# Common for local machines
|
|
candidates.append(os.path.expanduser(os.path.join("~", ".evergreen.yml")))
|
|
|
|
out = []
|
|
for path in candidates:
|
|
file = os.path.join(path, ".evergreen.yml")
|
|
if os.path.isfile(file):
|
|
out.append(file)
|
|
|
|
return out
|
|
|
|
|
|
def get_evergreen_api(evergreen_config=None):
|
|
"""Return evergreen API."""
|
|
if evergreen_config:
|
|
possible_configs = [evergreen_config]
|
|
else:
|
|
possible_configs = _find_evergreen_yaml_candidates()
|
|
|
|
if not possible_configs:
|
|
LOGGER.error("Could not find .evergreen.yml", candidates=possible_configs)
|
|
raise RuntimeError("Could not find .evergreen.yml")
|
|
|
|
last_ex = None
|
|
for config in possible_configs:
|
|
try:
|
|
return RetryingEvergreenApi.get_api(config_file=config)
|
|
#
|
|
except Exception as ex:
|
|
last_ex = ex
|
|
continue
|
|
|
|
LOGGER.error(
|
|
"Could not connect to Evergreen with any .evergreen.yml files available on this system",
|
|
config_file_candidates=possible_configs,
|
|
)
|
|
raise last_ex
|
|
|
|
|
|
def get_buildvariant_name(
|
|
config: SetupMultiversionConfig, edition, platform, architecture, major_minor_version
|
|
):
|
|
"""Return Evergreen buildvariant name."""
|
|
|
|
buildvariant_name = ""
|
|
evergreen_buildvariants = config.evergreen_buildvariants
|
|
|
|
for buildvariant in evergreen_buildvariants:
|
|
if (
|
|
buildvariant.edition == edition
|
|
and buildvariant.platform == platform
|
|
and buildvariant.architecture == architecture
|
|
):
|
|
versions = buildvariant.versions
|
|
if major_minor_version in versions:
|
|
buildvariant_name = buildvariant.name
|
|
break
|
|
elif not versions:
|
|
buildvariant_name = buildvariant.name
|
|
|
|
return buildvariant_name
|
|
|
|
|
|
def get_patch_module_diffs(evg_api: RetryingEvergreenApi, version_id):
|
|
"""Get the raw git diffs for all modules."""
|
|
evg_url = evg_api._create_url(f"/patches/{version_id}")
|
|
try:
|
|
res = evg_api._call_api(evg_url)
|
|
except requests.exceptions.HTTPError as err:
|
|
err_res = err.response
|
|
if err_res.status_code == 400:
|
|
LOGGER.debug(
|
|
"Not a patch build task, skipping applying patch", version_id_of_task=version_id
|
|
)
|
|
return None
|
|
else:
|
|
raise
|
|
|
|
patch = Patch(res.json(), evg_api)
|
|
|
|
patch_module_diff = {}
|
|
for module_code_change in patch.module_code_changes:
|
|
git_diff_link = module_code_change.raw_link
|
|
raw = evg_api._call_api(git_diff_link)
|
|
diff = raw.text
|
|
patch_module_diff[module_code_change.branch_name] = diff
|
|
|
|
return patch_module_diff
|
|
|
|
|
|
def get_generic_buildvariant_name(config: SetupMultiversionConfig, major_minor_version):
|
|
"""Return Evergreen buildvariant name for generic platform."""
|
|
|
|
LOGGER.info(
|
|
"Falling back to generic architecture.",
|
|
edition=GENERIC_EDITION,
|
|
platform=GENERIC_PLATFORM,
|
|
architecture=GENERIC_ARCHITECTURE,
|
|
)
|
|
|
|
generic_buildvariant_name = get_buildvariant_name(
|
|
config=config,
|
|
edition=GENERIC_EDITION,
|
|
platform=GENERIC_PLATFORM,
|
|
architecture=GENERIC_ARCHITECTURE,
|
|
major_minor_version=major_minor_version,
|
|
)
|
|
|
|
if not generic_buildvariant_name:
|
|
raise EvergreenConnError("Generic architecture buildvariant not found.")
|
|
|
|
return generic_buildvariant_name
|
|
|
|
|
|
def get_evergreen_version(evg_api: RetryingEvergreenApi, evg_ref: str) -> Optional[Version]:
|
|
"""Return evergreen version by reference (commit_hash or evergreen_version_id)."""
|
|
from buildscripts.resmokelib import multiversionconstants
|
|
|
|
# Evergreen reference as evergreen_version_id
|
|
evg_refs = [evg_ref]
|
|
# Evergreen reference as {project_name}_{commit_hash}
|
|
evg_refs.extend(
|
|
f"{proj.replace('-', '_')}_{evg_ref}" for proj in multiversionconstants.EVERGREEN_PROJECTS
|
|
)
|
|
|
|
for ref in evg_refs:
|
|
try:
|
|
evg_version = evg_api.version_by_id(ref)
|
|
except HTTPError:
|
|
continue
|
|
else:
|
|
LOGGER.debug(
|
|
"Found evergreen version.",
|
|
evergreen_version=f"{EVERGREEN_HOST}/version/{evg_version.version_id}",
|
|
)
|
|
return evg_version
|
|
|
|
return None
|
|
|
|
|
|
def get_evergreen_versions(evg_api: RetryingEvergreenApi, evg_project: str) -> Iterator[Version]:
|
|
"""Return the list of evergreen versions by evergreen project name."""
|
|
return evg_api.versions_by_project(evg_project)
|
|
|
|
|
|
def get_compile_artifact_urls(
|
|
evg_api: RetryingEvergreenApi, evg_version: Version, buildvariant_name, ignore_failed_push=False
|
|
):
|
|
"""Return compile urls from buildvariant in Evergreen version."""
|
|
try:
|
|
build_id = evg_version.build_variants_map[buildvariant_name]
|
|
except KeyError:
|
|
raise EvergreenConnError(f"Buildvariant {buildvariant_name} not found.")
|
|
|
|
evg_build = evg_api.build_by_id(build_id)
|
|
LOGGER.debug("Found evergreen build.", evergreen_build=f"{EVERGREEN_HOST}/build/{build_id}")
|
|
evg_tasks: Deque[Union[Task, str]] = deque(evg_build.get_tasks())
|
|
tasks_wrapper = _filter_successful_tasks(evg_api, evg_tasks)
|
|
LOGGER.info(
|
|
"Found the following multiversion tasks",
|
|
symbols_task=tasks_wrapper.symbols_task,
|
|
binary_task=tasks_wrapper.binary_task,
|
|
push_task=tasks_wrapper.push_task,
|
|
)
|
|
|
|
# Ignore push tasks if specified as such, else return no results if push does not exist.
|
|
if ignore_failed_push:
|
|
tasks_wrapper.push_task = None
|
|
elif tasks_wrapper.push_task is None:
|
|
return {}
|
|
|
|
return _get_multiversion_urls(tasks_wrapper)
|
|
|
|
|
|
class _MultiversionTasks(object):
|
|
"""Tasks relevant for multiversion setup."""
|
|
|
|
def __init__(
|
|
self, symbols: Union[Task, None], binary: Union[Task, None], push: Union[Task, None]
|
|
):
|
|
"""Init function."""
|
|
self.symbols_task = symbols
|
|
self.binary_task = binary
|
|
self.push_task = push
|
|
|
|
|
|
def _get_multiversion_urls(tasks_wrapper: _MultiversionTasks):
|
|
compile_artifact_urls = {}
|
|
|
|
binary = tasks_wrapper.binary_task
|
|
push = tasks_wrapper.push_task
|
|
symbols = tasks_wrapper.symbols_task
|
|
|
|
required_tasks = [binary, push] if push is not None else [binary]
|
|
|
|
if all(task and task.status == "success" for task in required_tasks):
|
|
LOGGER.info(
|
|
"Required evergreen task(s) were successful.",
|
|
required_tasks=f"{required_tasks}",
|
|
task_id=f"{EVERGREEN_HOST}/task/{required_tasks[0].task_id}",
|
|
)
|
|
evg_artifacts = binary.artifacts
|
|
for artifact in evg_artifacts:
|
|
compile_artifact_urls[artifact.name] = artifact.url
|
|
|
|
if symbols and symbols.status == "success":
|
|
for artifact in symbols.artifacts:
|
|
compile_artifact_urls[artifact.name] = artifact.url
|
|
elif symbols and symbols.task_id:
|
|
LOGGER.warning(
|
|
"debug symbol archive was unsuccessful",
|
|
archive_symbols_task=f"{EVERGREEN_HOST}/task/{symbols.task_id}",
|
|
)
|
|
|
|
# Tack on the project id for generating a friendly decompressed name for the artifacts.
|
|
compile_artifact_urls["project_identifier"] = binary.project_identifier
|
|
|
|
elif all(task for task in required_tasks):
|
|
LOGGER.warning(
|
|
"Required Evergreen task(s) were not successful.",
|
|
required_tasks=f"{required_tasks}",
|
|
task_id=f"{EVERGREEN_HOST}/task/{required_tasks[0].task_id}",
|
|
)
|
|
else:
|
|
LOGGER.error("There are no `compile` and/or 'push' tasks in the evergreen build")
|
|
|
|
return compile_artifact_urls
|
|
|
|
|
|
def _filter_successful_tasks(
|
|
evg_api: RetryingEvergreenApi, evg_tasks: Deque[Union[Task, str]]
|
|
) -> _MultiversionTasks:
|
|
"""
|
|
We want to filter successful tasks in order by variant then by dependent tasks to find the compile tasks.
|
|
|
|
evg_tasks: A queue of Tasks or task_ids (str)
|
|
"""
|
|
compile_task = None
|
|
archive_symbols_task = None
|
|
push_task = None
|
|
seen_task_ids: Set[str] = set()
|
|
while evg_tasks:
|
|
evg_task = evg_tasks.popleft()
|
|
|
|
# If we have checked this task before skip it
|
|
task_id = evg_task if isinstance(evg_task, str) else evg_task.task_id
|
|
if task_id in seen_task_ids:
|
|
continue
|
|
seen_task_ids.add(task_id)
|
|
|
|
if isinstance(evg_task, str):
|
|
evg_task = evg_api.task_by_id(evg_task)
|
|
|
|
# Only set the compile task if there isn't one already, otherwise
|
|
# newer tasks like "archive_dist_test_debug" take precedence.
|
|
# Use `get_execution_or_self` to prevent grabbing an unfinished restarted executed task.
|
|
if (
|
|
evg_task.display_name
|
|
in ("compile", "archive_dist_test", "archive_dist_test_future_git_tag_multiversion")
|
|
and compile_task is None
|
|
):
|
|
compile_task = evg_task.get_execution_or_self(0)
|
|
# archive_dist_test_debug might not be in the dep chain
|
|
# it should always be in the same build variant as the compile task
|
|
evg_tasks.extend(evg_api.tasks_by_build(compile_task.build_id))
|
|
|
|
elif evg_task.display_name == "push":
|
|
push_task = evg_task.get_execution_or_self(0)
|
|
elif (
|
|
evg_task.display_name
|
|
in ("archive_dist_test_debug", "archive_dist_test_debug_future_git_tag_multiversion")
|
|
and archive_symbols_task is None
|
|
):
|
|
archive_symbols_task = evg_task.get_execution_or_self(0)
|
|
if compile_task and push_task and archive_symbols_task:
|
|
break
|
|
|
|
dependent_tasks = evg_task.depends_on if evg_task.depends_on else []
|
|
for dep_task in dependent_tasks:
|
|
evg_tasks.append(dep_task["id"])
|
|
return _MultiversionTasks(symbols=archive_symbols_task, binary=compile_task, push=push_task)
|