sgallagh / centos / centpkg

Forked from centos/centpkg 3 years ago
Clone
Blob Blame History Raw
"""
    Command line behavior for centpkg
"""

#
# Author(s):
#            Jesse Keating <jkeating@redhat.com>
#            Pat Riehecky <riehecky@fnal.gov>
#            Brian Stinson <bstinson@ksu.edu>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
# the full text of the license.

from __future__ import print_function

import argparse
import textwrap

from centpkg.utils import config_get_safely, do_add_remote, do_fork
import centpkg.utils
from pyrpkg.cli import cliClient
from pyrpkg import rpkgError
from urllib.parse import urlparse
from configparser import ConfigParser

import gitlab
import json
import os
import sys

_DEFAULT_API_BASE_URL = "https://gitlab.com"
_DEFAULT_PARENT_NAMESPACE = {
    "centpkg": "redhat/centos-stream",
    "centpkg-sig": "CentOS",
}


class centpkgClient(cliClient):
    def __init__(self, config, name="centpkg"):
        self.DEFAULT_CLI_NAME = name

        # Save the config for utilities such as get_repo_name()
        centpkg.utils.dist_git_config = config
        centpkg.utils.dist_git_config.add_section("__default")
        centpkg.utils.dist_git_config.set("__default", "cli_name", name)

        super(centpkgClient, self).__init__(config, name)

        self.setup_centos_subparsers()

    def setup_centos_subparsers(self):
        self.register_do_fork()
        self.register_request_gated_side_tag()
        self.register_current_state()

    def register_do_fork(self):
        help_msg = "Create a new fork of the current repository"
        distgit_section = "{0}.distgit".format(self.name)
        # uses default dist-git url in case section is not present
        try:
            distgit_api_base_url = config_get_safely(
                self.config, distgit_section, "apibaseurl"
            )
        except rpkgError:
            distgit_api_base_url = _DEFAULT_API_BASE_URL
        description = textwrap.dedent(
            """
            Create a new fork of the current repository

            Before the operation, you need to generate an API token at
            https://{1}/-/user_settings/personal_access_tokens, select the "api"
            scope and save it in your local user configuration located
            at ~/.config/rpkg/{0}.conf. For example:

                [{0}.distgit]
                token = <api_key_here>

            Below is a basic example of the command to fork a current repository:

                {0} fork
        """.format(
                self.name, urlparse(distgit_api_base_url).netloc
            )
        )

        fork_parser = self.subparsers.add_parser(
            "fork",
            formatter_class=argparse.RawDescriptionHelpFormatter,
            help=help_msg,
            description=description,
        )
        fork_parser.set_defaults(command=self.do_distgit_fork)

    def do_distgit_fork(self):
        """create fork of the distgit repository
        That includes creating fork itself using API call and then adding
        remote tracked repository
        """
        distgit_section = "{0}.distgit".format(self.name)
        distgit_api_base_url = config_get_safely(
            self.config, distgit_section, "apibaseurl"
        )
        distgit_token = config_get_safely(self.config, distgit_section, "token")

        ret, repo_path = do_fork(
            logger=self.log,
            base_url=distgit_api_base_url,
            token=distgit_token,
            repo_name=self.cmd.repo_name,
            namespace=self.cmd.ns,
            cli_name=self.name,
        )

        # assemble url of the repo in web browser
        fork_url = "{0}/{1}".format(
            distgit_api_base_url.rstrip("/"),
            repo_path,
        )

        if ret:
            msg = "Fork of the repository has been created: '{0}'"
        else:
            msg = "Repo '{0}' already exists."
        self.log.info(msg.format(fork_url))

        distgit_remote_base_url = self.config.get(
            "{0}".format(self.name),
            "gitbaseurl",
            vars={"repo": "{0}/{1}".format(self.cmd.ns, self.cmd.repo_name)},
        )
        remote_name = repo_path.split("/")[0]

        ret = do_add_remote(
            base_url=distgit_api_base_url,
            remote_base_url=distgit_remote_base_url,
            repo=self.cmd.repo,
            repo_path=repo_path,
            remote_name=remote_name,
        )
        if ret:
            msg = "Adding as remote '{0}'."
        else:
            msg = "Remote with name '{0}' already exists."
        self.log.info(msg.format(remote_name))

    def register_current_state(self):
        """Register command line parser for subcommand current-state"""
        parser = self.subparsers.add_parser(
            "current-state",
            help="Show the phase and state of current dist-git branch - Internal Only",
        )
        parser.set_defaults(command=self.request_current_state)

    # FIXME: This is alot of duplication.
    #        Get it working now.  De-duplicate later
    def request_current_state(self):
        # Only run if we have internal configurations, or scratch
        internal_config_file = "/etc/rpkg/centpkg_internal.conf"
        if os.path.exists(internal_config_file):
            # Get our internal only variables
            cfg = ConfigParser()
            cfg.read(internal_config_file)
            pp_api_url = config_get_safely(cfg, "centpkg.internal", "pp_api_url")
            rhel_dist_git = config_get_safely(cfg, "centpkg.internal", "rhel_dist_git")

            # Find out divergent branch and stabilization
            self.log.info("Checking current state...")
            stream_version = self.cmd.target.split("-")[0]
            rhel_version = centpkg.utils.stream_mapping(stream_version)

            try:
                rhel_state = centpkg.utils.determine_rhel_state(
                    rhel_dist_git=rhel_dist_git,
                    namespace=self.cmd.ns,
                    repo_name=self.cmd.repo_name,
                    cs_branch=stream_version,
                    pp_api_url=pp_api_url,
                )
            except centpkg.utils.RHELError as e:
                self.log.error("Could not determine RHEL state of this package")
                self.log.error(str(e))
                raise SystemExit(1)

            # Output the current state
            self.log.info(centpkg.utils.format_current_state_message(rhel_state))
        else:
            self.log.error("NO INTERNAL CONFIGURATION")
            self.log.error(
                "Hint: If you are internal, install the rhel-packager package from https://download.devel.redhat.com/rel-eng/RCMTOOLS/latest-RCMTOOLS-2-RHEL-9/compose/BaseOS/x86_64/os/Packages/"
            )
            self.log.error("Exiting")
            raise SystemExit(1)

    def get_custom_metadata(self):
        # If custom-user-metadata set, add onto it
        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")
        else:
            temp_custom_user_metadata = dict()

        return temp_custom_user_metadata

    def update_custom_metadata(self, metadata):
        self.args.custom_user_metadata = json.dumps(metadata)

    # 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
             * none - This will not build in brew.  No rhel metadata is set.

        """.format(
                "\n".join(textwrap.wrap(build_parser.description))
            )
        )

        # Now add our additional option
        build_parser.add_argument(
            "--rhel-target",
            choices=["exception", "zstream", "latest", "none"],
            help="Set the rhel-target metadata",
        )

    # Overloaded _build
    def _build(self, sets=None):

        # Only do rhel-target if we are centpkg, not centpkg-sig, or if we are doing scratch
        if not os.path.basename(sys.argv[0]).endswith("-sig") and not self.args.scratch:
            # Only run if we have internal configuraions
            internal_config_file = "/etc/rpkg/centpkg_internal.conf"
            if os.path.exists(internal_config_file):

                # If rhel-target is set, no need to check, just use it
                if hasattr(self.args, "rhel_target") and self.args.rhel_target:
                    # If rhel-target set to none, do nothing with metadata
                    if self.args.rhel_target.lower() != "none":
                        temp_custom_user_metadata = self.get_custom_metadata()
                        temp_custom_user_metadata["rhel-target"] = self.args.rhel_target
                        self.update_custom_metadata(temp_custom_user_metadata)

                else:
                    # Get our internal only variables
                    cfg = ConfigParser()
                    cfg.read(internal_config_file)
                    pp_api_url = config_get_safely(
                        cfg, "centpkg.internal", "pp_api_url"
                    )
                    rhel_dist_git = config_get_safely(
                        cfg, "centpkg.internal", "rhel_dist_git"
                    )

                    # Find out divergent branch and stabalization
                    self.log.info("Checking rhel-target information:")
                    stream_version = self.cmd.target.split("-")[0]
                    rhel_version = centpkg.utils.stream_mapping(stream_version)

                    try:
                        rhel_state = centpkg.utils.determine_rhel_state(
                            rhel_dist_git=rhel_dist_git,
                            namespace=self.cmd.ns,
                            repo_name=self.cmd.repo_name,
                            cs_branch=stream_version,
                            pp_api_url=pp_api_url,
                        )
                    except centpkg.utils.RHELError as e:
                        self.log.error("Could not determine RHEL state of this package")
                        self.log.error(str(e))
                        raise SystemExit(1)

                    except:
                        self.log.error(
                            "  Error: centpkg cannot determine the development phase."
                        )
                        self.log.error(
                            "         Please file an issue at https://git.centos.org/centos/centpkg"
                        )
                        self.log.error("  Workaround: Use the --rhel-target option")
                        self.log.error("Exiting")
                        raise SystemExit(1)

                    # Output the current state
                    self.log.info(
                        centpkg.utils.format_current_state_message(rhel_state)
                    )

                    # Update args.custom_user_metadata
                    temp_custom_user_metadata = self.get_custom_metadata()

                    if (
                        rhel_state.phase == centpkg.utils.pp_phase_stabilization
                        or not rhel_state.rhel_target_default
                    ):
                        # It shouldn't be possible for rhel_target_default
                        # to be None except during Stabilization phase,
                        # but we'll check anyway.
                        self.log.error("We are currently in Stabilization phase")
                        self.log.error("You must set the rhel-target (--rhel-target)")
                        self.log.error("to 'zstream' (for 0day) or 'exception'.")
                        self.log.error("Exiting")
                        raise SystemExit(1)

                    temp_custom_user_metadata["rhel-target"] = (
                        rhel_state.rhel_target_default
                    )
                    self.update_custom_metadata(temp_custom_user_metadata)

                    # Good to know
                    self.log.info(
                        "    rhel-target: "
                        + json.loads(self.args.custom_user_metadata)["rhel-target"]
                    )

            else:
                self.log.error("NO INTERNAL CONFIGURATION")
                self.log.error(
                    "Only scratch builds are allowed without internal configurations"
                )
                self.log.error(
                    "Hint: Install the rhel-packager package from https://download.devel.redhat.com/rel-eng/RCMTOOLS/latest-RCMTOOLS-2-RHEL-9/compose/BaseOS/x86_64/os/Packages/ if you want to build the package via internal (Red Hat) configuration."
                )
                self.log.error("Exiting")
                raise SystemExit(1)

        # 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(
            "request-gated-side-tag",
            help="Create a new dynamic gated side tag",
        )
        parser.add_argument("--base-tag", help="name of base tag")
        parser.set_defaults(command=self.request_gated_side_tag)

    def request_gated_side_tag(self):
        self.args.suffix = "stack-gate"
        super(centpkgClient, self).request_side_tag()

    def clone(self):
        # Since gitlab allows git repos to be checked out with incorrect capitilization
        #   we need to check the spelling when cloning
        gl = gitlab.Gitlab(_DEFAULT_API_BASE_URL)
        parent_namespace = _DEFAULT_PARENT_NAMESPACE[self.DEFAULT_CLI_NAME]

        if "/" in self.args.repo[0]:
            project_name_with_namespace = parent_namespace + "/" + self.args.repo[0]
            short_name = self.args.repo[0].split("/")[-1]
        else:
            project_name_with_namespace = (
                parent_namespace + "/rpms/" + self.args.repo[0]
            )
            short_name = self.args.repo[0]
        try:
            project = gl.projects.get(project_name_with_namespace)
        except gitlab.exceptions.GitlabGetError as e:
            self.log.error(
                "There is no repository with the given name. Did you spell it correctly?"
            )
            self.log.error("Fatal: ")
            self.log.error(e)
            raise SystemExit(1)
        if not project.name == short_name:
            self.log.error("Fatal: ")
            self.log.error(
                "Project name is wrong: "
                + short_name
                + " it should be: "
                + project.name
            )
            raise SystemExit(1)
        super(centpkgClient, self).clone()