"""A service for working with multiversion testing.""" from __future__ import annotations import re from bisect import bisect_left, bisect_right from typing import List, NamedTuple, Optional import structlog import yaml from packaging.version import Version from pydantic import BaseModel, Field VERSION_RE = re.compile(r"^[0-9]+\.[0-9]+") LOGGER = structlog.getLogger(__name__) def tag_str(version: Version) -> str: """Return a tag for the given version.""" return f"requires_fcv_{version.major}{version.minor}" def version_str(version: Version) -> str: """Return a string of the given version in 'MAJOR.MINOR' form.""" return f"{version.major}.{version.minor}" class VersionConstantValues(NamedTuple): """ Object to hold the calculated Version constants. * latest: Latest FCV. * last_continuous: Last continuous FCV. * last_lts: Last LTS FCV. * requires_fcv_tag_list: List of FCVs that we need to generate a tag for against LTS versions. * requires_fcv_tag_list_continuous: List of FCVs that we need to generate a tag for against continuous versions. * fcvs_less_than_latest: List of all FCVs that are less than latest, starting from v4.0. * eols: List of stable MongoDB versions since v2.0 that have been EOL'd. """ latest: Version last_continuous: Version last_lts: Version requires_fcv_tag_list: List[Version] requires_fcv_tag_list_continuous: List[Version] fcvs_less_than_latest: List[Version] eols: List[Version] def get_fcv_tag_list(self) -> str: """Get a comma joined string of all the fcv tags.""" return ",".join([tag_str(tag) for tag in self.requires_fcv_tag_list]) def get_lts_fcv_tag_list(self) -> str: """Get a comma joined string of all the LTS fcv tags.""" # Note: the LTS tag list is the default used above, so this function is the same as # `get_fcv_tag_list`. This function was added to make it explicit when we want to use # the LTS version vs the default. return ",".join([tag_str(tag) for tag in self.requires_fcv_tag_list]) def get_continuous_fcv_tag_list(self) -> str: """Get a comma joined string of all the continuous fcv tags.""" return ",".join([tag_str(tag) for tag in self.requires_fcv_tag_list_continuous]) def get_latest_tag(self) -> str: """Get a string version of the latest FCV.""" return tag_str(self.latest) def get_last_lts_fcv(self) -> str: """Get a string version of the last LTS FCV.""" return version_str(self.last_lts) def get_last_continuous_fcv(self) -> str: """Get a string version of the last continuous FCV.""" return version_str(self.last_continuous) def get_latest_fcv(self) -> str: """Get a string version of the latest FCV.""" return version_str(self.latest) def get_fcv_tags_less_than_latest(self) -> List[str]: """Get the list of all fcv tags less than the latest.""" return [tag_str(fcv) for fcv in self.fcvs_less_than_latest] def build_last_lts_binary(self, base_name: str) -> str: """ Build the name of the binary that the LTS version of the given tool will have. :param base_name: Base name of binary (mongo, mongod, mongos). :return: Name of LTS version of the given tool. """ last_lts = self.get_last_lts_fcv() return f"{base_name}-{last_lts}" def build_last_continuous_binary(self, base_name: str) -> str: """ Build the name of the binary that the continuous version of the given tool will have. :param base_name: Base name of binary (mongo, mongod, mongos). :return: Name of continuous version of the given tool. """ last_continuous = self.get_last_continuous_fcv() return f"{base_name}-{last_continuous}" def get_eols(self) -> List[str]: """Get EOL'd versions as list of strings.""" return [version_str(eol) for eol in self.eols] class MongoVersion(BaseModel): """ The mongo version being tested. * mongo_version: The mongo version being tested. """ mongo_version: str @classmethod def from_yaml_file(cls, yaml_file: str) -> MongoVersion: """ Read the mongo version from the given yaml file. :param yaml_file: Path to yaml file. :return: MongoVersion read from file. """ mongo_version_yml_file = open(yaml_file, "r", encoding="utf8") return cls(**yaml.safe_load(mongo_version_yml_file)) def get_version(self) -> Version: """Get the Version representation of the mongo version being tested.""" version_match = VERSION_RE.match(self.mongo_version) if version_match is None: raise ValueError( f"Could not determine version from mongo version string '{self.mongo_version}'" ) return Version(version_match.group(0)) class MongoReleases(BaseModel): """ Information about the FCVs and LTS release version since v4.0. * feature_compatibility_version: All FCVs starting with 4.0. * long_term_support_releases: All LTS releases starting with 4.0. * eol_versions: List of stable MongoDB versions since 2.0 that have been EOL'd. * generate_fcv_lower_bound_override: Extend FCV generation down to the previous value of last LTS. """ feature_compatibility_versions: List[str] = Field(alias="featureCompatibilityVersions") long_term_support_releases: List[str] = Field(alias="longTermSupportReleases") eol_versions: List[str] = Field(alias="eolVersions") generate_fcv_lower_bound_override: Optional[str] = Field( None, alias="generateFCVLowerBoundOverride" ) @classmethod def from_yaml_file(cls, yaml_file: str) -> MongoReleases: """ Read the mongo release information from the given yaml file. :param yaml_file: Path to yaml file. :return: MongoReleases read from file. """ with open(yaml_file, "r", encoding="utf8") as mongo_releases_file: yaml_contents = mongo_releases_file.read() safe_load_result = yaml.safe_load(yaml_contents) try: return cls(**safe_load_result) except: LOGGER.info( "MongoReleases.from_yaml_file() failed\n" f"yaml_file = {yaml_file}\n" f"yaml_contents = {yaml_contents}\n" f"safe_load_result = {safe_load_result}" ) raise def get_fcv_versions(self) -> List[Version]: """Get the Version representation of all fcv versions.""" return [Version(fcv) for fcv in self.feature_compatibility_versions] def get_lts_versions(self) -> List[Version]: """Get the Version representation of the lts versions.""" return [Version(lts) for lts in self.long_term_support_releases] def get_eol_versions(self) -> List[Version]: """Get the Version representation of the EOL versions.""" return [Version(eol) for eol in self.eol_versions] class MultiversionService: """A service for working with multiversion information.""" def __init__(self, mongo_version: MongoVersion, mongo_releases: MongoReleases) -> None: """ Initialize the service. :param mongo_version: Contents of the Mongo Version file. :param mongo_releases: Contents of the Mongo Releases file. """ self.mongo_version = mongo_version self.mongo_releases = mongo_releases def calculate_version_constants(self) -> VersionConstantValues: """Calculate multiversion constants from data files.""" latest = self.mongo_version.get_version() fcvs = self.mongo_releases.get_fcv_versions() lts = self.mongo_releases.get_lts_versions() eols = self.mongo_releases.get_eol_versions() # Highest release less than latest. last_continuous = fcvs[bisect_left(fcvs, latest) - 1] # Highest LTS release less than latest. last_lts = lts[bisect_left(lts, latest) - 1] # All FCVs greater than last LTS, up to latest. requires_fcv_tag_list = fcvs[bisect_right(fcvs, last_lts) : bisect_right(fcvs, latest)] requires_fcv_tag_list_continuous = [latest] # All FCVs less than latest. fcvs_less_than_latest = fcvs[: bisect_left(fcvs, latest)] return VersionConstantValues( latest=latest, last_continuous=last_continuous, last_lts=last_lts, requires_fcv_tag_list=requires_fcv_tag_list, requires_fcv_tag_list_continuous=requires_fcv_tag_list_continuous, fcvs_less_than_latest=fcvs_less_than_latest, eols=eols, )