From 1e7ef8239acb86e77b95756f423b6b9b04d82d55 Mon Sep 17 00:00:00 2001 From: Troy Dawson Date: Feb 23 2023 18:01:39 +0000 Subject: Require --rhel-target --- diff --git a/src/centpkg/cli.py b/src/centpkg/cli.py index 0d5a58e..199cbc9 100755 --- a/src/centpkg/cli.py +++ b/src/centpkg/cli.py @@ -23,7 +23,11 @@ import centpkg.utils from pyrpkg.cli import cliClient from pyrpkg import rpkgError from six.moves.urllib_parse import urlparse +import six.moves.configparser as ConfigParser +import json +import koji +import os _DEFAULT_API_BASE_URL = 'https://gitlab.com' @@ -126,6 +130,122 @@ class centpkgClient(cliClient): msg = "Remote with name '{0}' already exists." self.log.info(msg.format(remote_name)) + # Overloaded build + def register_build(self): + # Do all the work from the super class + super(centpkgClient, self).register_build() + build_parser = self.subparsers.choices['build'] + build_parser.formatter_class = argparse.RawDescriptionHelpFormatter + build_parser.description = textwrap.dedent(''' + {0} + + centpkg now sets the rhel metadata with --rhel-target. + * exception - This will build for the current in-development Y-stream release. + It is equivalent to passing latest when not in the Blocker and Exception Phase. + * zstream - If pre-GA of a y-stream release, this will build for 0day. + If post-GA of a Y-stream release, this will build for the Z-stream of that release. + * latest - This will always build for the next Y-stream release + + '''.format('\n'.join(textwrap.wrap(build_parser.description)))) + + # Now add our additional option + build_parser.add_argument( + '--rhel-target', + choices=['exception', 'zstream', 'latest'], + default='latest', + help='Set the rhel-target metadata') + + # Overloaded _build + def _build(self, sets=None): + + # Only run if we have internal configuraions, or scratch + internal_config_file = "/etc/rpkg/centpkg_internal.conf" + if os.path.exists(internal_config_file): + # Get our internal only variables + cfg = ConfigParser.SafeConfigParser() + cfg.read(internal_config_file) + pp_api_url = config_get_safely(cfg, "centpkg.internal", 'pp_api_url') + gitbz_query_url = config_get_safely(cfg, "centpkg.internal", 'gitbz_query_url') + rhel_dist_git = config_get_safely(cfg, "centpkg.internal", 'rhel_dist_git') + + # Find out divergent branch and stabalization + stream_version = self.cmd.target.split('-')[0] + rhel_version = centpkg.utils.stream_mapping(stream_version) + active_y, in_stabilization = centpkg.utils.determine_active_y_version(rhel_version, pp_api_url) + divergent_branch = centpkg.utils.does_divergent_branch_exist( + self.cmd.repo_name, + rhel_version, + rhel_dist_git, + pp_api_url, + "rpms") + + # Good to know + if divergent_branch : + print("divergent_branch: TRUE") + else: + print("divergent_branch: FALSE") + if in_stabilization : + print("in_stabilization: TRUE") + else: + print("in_stabilization: FALSE") + + # Update args.custom_user_metadata + if hasattr(self.args, 'custom_user_metadata') and self.args.custom_user_metadata: + try: + temp_custom_user_metadata = json.loads(self.args.custom_user_metadata) + # Use ValueError instead of json.JSONDecodeError for Python 2 and 3 compatibility + except ValueError as e: + self.parser.error("--custom-user-metadata is not valid JSON: %s" % e) + if not isinstance(temp_custom_user_metadata, dict): + self.parser.error("--custom-user-metadata must be a JSON object") + if hasattr(self.args, 'rhel_target') and self.args.rhel_target: + temp_custom_user_metadata["rhel-target"] = self.args.rhel_target + else: + if divergent_branch and not in_stabilization : + temp_custom_user_metadata["rhel-target"] = "latest" + elif not divergent_branch and not in_stabilization : + temp_custom_user_metadata["rhel-target"] = "zstream" + else: + print("We are currently in Stabalization mode") + print("You must either set the rhel-target (--rhel-target)") + print("or branch for the previous version.") + print("Exiting") + raise SystemExit + self.args.custom_user_metadata = json.dumps(temp_custom_user_metadata) + else: + if hasattr(self.args, 'rhel_target') and self.args.rhel_target: + temp_custom_user_metadata = {"rhel-target": self.args.rhel_target} + self.args.custom_user_metadata = json.dumps(temp_custom_user_metadata) + else: + if divergent_branch and not in_stabilization : + self.args.custom_user_metadata = '{"rhel-target": "latest"}' + elif not divergent_branch and not in_stabilization : + self.args.custom_user_metadata = '{"rhel-target": "zstream"}' + else: + print("We are currently in Stabalization mode") + print("You must either set the rhel-target (--rhel-target)") + print("or branch for the previous version.") + print("Exiting") + raise SystemExit + + + # Good to know, but take out for final cut + print('Metadata: %r', self.args.custom_user_metadata) + + # Purposely fail during testing so we do not have so many builds + #self.args.custom_user_metadata = json.loads(self.args.custom_user_metadata) + + else: + if not self.args.scratch: + print("NO SCRATCH BUILD") + print("Only scratch builds are allowed without internal configurations") + print("Exiting") + raise SystemExit + + # Proceed with build + return super(centpkgClient, self)._build(sets) + + def register_request_gated_side_tag(self): """Register command line parser for subcommand request-gated-side-tag""" parser = self.subparsers.add_parser( diff --git a/src/centpkg/utils.py b/src/centpkg/utils.py index 1f9524d..b42b2ea 100644 --- a/src/centpkg/utils.py +++ b/src/centpkg/utils.py @@ -10,16 +10,22 @@ # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. -import re -import json - import git +import json +import logging +import os +import pytz +import re import requests +import sys +from datetime import date, datetime from pyrpkg import rpkgError from requests.exceptions import ConnectionError from six.moves.configparser import NoOptionError, NoSectionError from six.moves.urllib.parse import quote_plus, urlparse +import git as gitpython + dist_git_config = None def do_fork(logger, base_url, token, repo_name, namespace, cli_name): @@ -255,3 +261,174 @@ def get_repo_name(name, org='rpms'): repo_name = get_canonical_repo_name(dist_git_config, name) return '%s/%s' % (org, repo_name) + +def stream_mapping(csname): + """ + Given a CentOS Stream name, map it to the corresponding RHEL name. + + Parameters + ---------- + csname: str + The CentOS Stream name. + + Returns + ------- + str + Correspoinding RHEL name. + """ + if csname == "c8s" or csname == "cs8" : + return "rhel-8" + if csname == "c9s" or csname == "cs9" : + return "rhel-9" + if csname == "c10s" or csname == "cs10" : + return "rhel-10" + if csname == "c11s" or csname == "cs11" : + return "rhel-11" + return None + +def does_divergent_branch_exist(repo_name, rhel_version, rhel_dist_git, pp_api_url, namespace): + logger = logging.getLogger(__name__) + + # Determine if the Y-1 branch exists for this repo + + # Look up the Y-1 branch name + divergent_branch = determine_divergent_branch( + rhel_version, + pp_api_url, + namespace, + ) + logger.debug("Divergent branch: {}".format(divergent_branch)) + + g = gitpython.cmd.Git() + try: + g.ls_remote( + "--exit-code", + os.path.join(rhel_dist_git, namespace, repo_name), + divergent_branch, + ) + branch_exists = True + except gitpython.GitCommandError as e: + t, v, tb = sys.exc_info() + # `git ls-remote --exit-code` returns "2" if it cannot find the ref + if e.status == 2: + branch_exists = False + else: + raise + return branch_exists + +def determine_divergent_branch(rhel_version, pp_api_url, namespace): + logger = logging.getLogger(__name__) + + # Query the "package pages" API for the current active Y-stream release + # Phase 230 is "Planning / Development / Testing" (AKA DeveTestDoc) + request_params = { + "phase": 230, + "product__shortname": "rhel", + "relgroup__shortname": rhel_version, + "format": "json", + } + + res = requests.get( + os.path.join(pp_api_url, "latest", "releases"), + params=request_params, + timeout=60, + ) + res.raise_for_status() + payload = json.loads(res.text) + logger.debug( + "Response from PP API: {}".format(json.dumps(payload, indent=2)) + ) + if len(payload) < 1: + raise RuntimeError("Received zero potential release matches)") + + active_y_version = -1 + for entry in payload: + shortname = entry["shortname"] + + # The shortname is in the form rhel-9-1.0 + # Extract the active Y-stream version + m = re.search("(?<={}-)\d+(?=\.0)".format(rhel_version), shortname) + if not m: + raise RuntimeError( + "Could not determine active Y-stream version from shortname" + ) + y_version = int(m.group(0)) + if y_version > active_y_version: + active_y_version = y_version + + # The divergent branch is Y-1 + return "{}.{}.0".format(rhel_version, active_y_version - 1) + +def _datesplit(isodate): + date_string_tuple = isodate.split('-') + return [ int(x) for x in date_string_tuple ] + + +def determine_active_y_version(rhel_version, pp_api_url): + """ + Returns: A 2-tuple of the active Y-stream version(int) and whether we are + in the Exception Phase(bool) + """ + logger = logging.getLogger(__name__) + + # Query the "package pages" API for the current active Y-stream release + # Phase 230 is "Planning / Development / Testing" (AKA DeveTestDoc) + request_params = { + "phase": 230, + "product__shortname": "rhel", + "relgroup__shortname": rhel_version, + "format": "json", + } + + res = requests.get( + os.path.join(pp_api_url, "latest", "releases"), + params=request_params, + timeout=60, + ) + res.raise_for_status() + payload = json.loads(res.text) + logger.debug( + "Response from PP API: {}".format(json.dumps(payload, indent=2)) + ) + if len(payload) < 1: + raise RuntimeError("Received zero potential release matches)") + + release_id = -1 + active_y_version = -1 + for entry in payload: + shortname = entry["shortname"] + + # The shortname is in the form rhel-9-1.0 + # Extract the active Y-stream version + m = re.search("(?<={}-)\d+(?=\.0)".format(rhel_version), shortname) + if not m: + raise RuntimeError( + "Could not determine active Y-stream version from shortname" + ) + y_version = int(m.group(0)) + if y_version > active_y_version: + active_y_version = y_version + release_id = entry["id"] + + # Now look up whether we are in the Exception Phase for this Y-stream release + request_params = { + "name__regex": "Exception Phase", + "format": "json", + } + res = requests.get(os.path.join(pp_api_url, "latest", "releases", str(release_id), "schedule-tasks"), params=request_params) + res.raise_for_status() + payload = json.loads(res.text) + + # This lookup *must* return exactly one value or the Product Pages are + # wrong and must be fixed. + assert len(payload) == 1 + + # Determine if this Y-stream release is in the exception phase + today = datetime.now(tz=pytz.utc).date() + exception_start_date = date(*_datesplit(payload[0]["date_start"])) + in_exception_phase = today >= exception_start_date + + logger.debug("Active Y-stream: {}, Enforcing: {}".format(active_y_version, in_exception_phase)) + + return active_y_version, in_exception_phase +