From 68d510a8ebdbb9a101df8c430bd11160fa4c5c39 Mon Sep 17 00:00:00 2001 From: Adar Nimrod <nimrod@shore.co.il> Date: Sat, 12 Mar 2022 19:02:34 +0200 Subject: [PATCH] git manage tfinit and a little bit more. - Add a new git manage command, tfinit, that initializes a Terraform backend managed by GitLab for this repository (based on the GitLab remote). - Better handling of cases when running in a subdirectory of the repo in the git module. Use git to check if a directory is a repository and to find the top level of a repository. - Use the top level functions' docstring for the CLI help message. - Use the sub-parser to output better error messages. --- Documents/bin/git-manage | 105 ++++++++++++++++++++++++++--------- Documents/bin/rcfiles/git.py | 25 ++++++--- 2 files changed, 94 insertions(+), 36 deletions(-) diff --git a/Documents/bin/git-manage b/Documents/bin/git-manage index 4b99db8..8f8ee5d 100755 --- a/Documents/bin/git-manage +++ b/Documents/bin/git-manage @@ -11,6 +11,7 @@ import github3.exceptions # pylint: disable=import-error import gitlab.exceptions # pylint: disable=import-error import passhole.passhole # pylint: disable=import-error import sh # pylint: disable=import-error +from sh import terraform # pylint: disable=import-error sys.path.append(os.path.expanduser("~/Documents/bin")) @@ -21,6 +22,13 @@ import rcfiles.gitlab # noqa: E402 pylint: disable=wrong-import-position GH_MIRROR_PREFIX = "https://*****@github.com/" +def error(message): + """Print an error message with the current subparser's usage and exit.""" + # pylint: disable=protected-access + sub_parser = arg_parser._subparsers._actions[1].choices[_args.command] + sub_parser.error(message) + + def github_mirrors(project, gh_conn): """Return a list of GitHub repositories that are mirrored from the GitLab project.""" @@ -139,9 +147,9 @@ def guess_remote(remote_type="gitlab", gl_conn=None): def guess_name(args, gh_conn=None, gl_conn=None): """Try to guess the name from the arguments and the repository remotes.""" - if args.name is None: + if "name" not in args or args.name is None: if not rcfiles.git.in_repo(): - arg_parser.error("Name not provided and not in a Git repo.") + error("Name not provided and not in a Git repo.") remote = guess_remote( remote_type="github" @@ -151,13 +159,9 @@ def guess_name(args, gh_conn=None, gl_conn=None): ) if remote is None: if "github" in args and args.github: - arg_parser.error( - "Name not provided and could not find a GitHub remote." - ) + error("Name not provided and could not find a GitHub remote.") else: - arg_parser.error( - "Name not provided and could not find a GitLab remote." - ) + error("Name not provided and could not find a GitLab remote.") else: if "github" in args and args.github: name = rcfiles.github.url_to_name(remote["url"]) @@ -198,15 +202,13 @@ def create_github_repo(args): - Commits an initial empty commit. """ if "/" in args.name: - arg_parser.error("Can't specify an organization.") + error("Can't specify an organization.") if args.internal or args.private: - arg_parser.error( - "Can't create internal or private GitHub repositories." - ) + error("Can't create internal or private GitHub repositories.") try: conn = rcfiles.github.connect() except Exception as e: # pylint: disable=broad-except - arg_parser.error(f"Failed to connect to GitHub: {e}") + error(f"Failed to connect to GitHub: {e}") repo = conn.create_repository( args.name, @@ -238,11 +240,11 @@ def create_gitlab_repo(args): - Adds the mirror remote. """ if args.private and args.internal: - arg_parser.error("Repository can be internal or private, not both.") + error("Repository can be internal or private, not both.") try: conn = rcfiles.gitlab.connect() except Exception as e: # pylint: disable=broad-except - arg_parser.error(f"Failed to connect to GitLab: {e}") + error(f"Failed to connect to GitLab: {e}") if args.private: visibility = "private" @@ -296,7 +298,7 @@ def create_gitlab_repo(args): def create_repo(args): """Create a new repository.""" if args.mirror and args.github: - arg_parser.error("Can't mirror from GitHub to GitLab.") + error("Can't mirror from GitHub to GitLab.") if args.github: create_github_repo(args) else: @@ -313,18 +315,18 @@ def mirror_repo(args): try: gh_conn = rcfiles.github.connect() except Exception as e: # pylint: disable=broad-except - arg_parser.error(f"Failed to connect to GitHub: {e}") + error(f"Failed to connect to GitHub: {e}") try: gl_conn = rcfiles.gitlab.connect() except Exception as e: # pylint: disable=broad-except - arg_parser.error(f"Failed to connect to GitLab: {e}") + error(f"Failed to connect to GitLab: {e}") name = guess_name(args, gh_conn, gl_conn) try: project = gl_conn.projects.get(name) except gitlab.exceptions.GitlabGetError: - arg_parser.error(f"Could not find GitLab project {name}.") + error(f"Could not find GitLab project {name}.") gh_repo = mirror_project(project, gh_conn, get_mirror_token()) print( @@ -349,12 +351,12 @@ def archive_repo(args): try: gh_conn = rcfiles.github.connect() except Exception as e: # pylint: disable=broad-except - arg_parser.error(f"Failed to connect to GitHub: {e}") + error(f"Failed to connect to GitHub: {e}") if not args.github: try: gl_conn = rcfiles.gitlab.connect() except Exception as e: # pylint: disable=broad-except - arg_parser.error(f"Failed to connect to GitLab: {e}") + error(f"Failed to connect to GitLab: {e}") if args.github: owner, name = guess_name(args, gh_conn=gh_conn, gl_conn=None).split( @@ -379,7 +381,7 @@ def archive_repo(args): def fork_repo(args): - """Forks a GitHub repository. + """Fork a GitHub repository. Does the following: - Forks the GitHub repository. @@ -389,10 +391,10 @@ def fork_repo(args): try: conn = rcfiles.github.connect() except Exception as e: # pylint: disable=broad-except - arg_parser.error(f"Failed to connect to GitHub: {e}") + error(f"Failed to connect to GitHub: {e}") if "/" not in args.name: - arg_parser.error("Must provide a full repository name.") + error("Must provide a full repository name.") org, name = args.name.split("/") upstream = conn.repository(org, name) @@ -410,6 +412,43 @@ def fork_repo(args): print("Added an upstream remote.", file=sys.stderr) +def terraform_init(args): + """Initialize a GitLab-managed Terraform state.""" + if not rcfiles.git.in_repo(): + error("This command must be run from inside a Git repository.") + try: + conn = rcfiles.gitlab.connect() + except Exception as e: # pylint: disable=broad-except + error(f"Failed to connect to GitLab: {e}") + remotes = rcfiles.git.get_all_remotes() + for remote in remotes.values(): + if rcfiles.gitlab.is_gitlab_url(conn, remote["url"]): + break + else: + error( + "This command must be run from inside a Git repository with a GitLab remote." # noqa: E501 + ) + username = rcfiles.gitlab.me(conn) + url = rcfiles.gitlab.http_url(conn) + name = guess_name(args, gl_conn=conn) + project = conn.projects.get(name) + address = ( + f"{url}/api/v4/projects/{project.id}/terraform/state/{args.state}" + ) + # fmt: off + terraform.init( + "-backend-config", f"address={address}", + "-backend-config", f"lock_address={address}/lock", + "-backend-config", f"unlock_address={address}/lock", + "-backend-config", f"username={username}", + "-backend-config", f"password={conn.private_token}", + "-backend-config", "lock_method=POST", + "-backend-config", "unlock_method=DELETE", + "-reconfigure", + ) + # fmt: on + + def build_arg_parser(): """Builds the argument parser.""" parser = argparse.ArgumentParser(description=__doc__) @@ -418,7 +457,7 @@ def build_arg_parser(): ) parser_create = subparsers.add_parser( - "new", help="Create a new repository." + "new", help=create_repo.__doc__.splitlines()[0] ) parser_create.set_defaults(func=create_repo) parser_create.add_argument("name", help="Name of the repository.") @@ -446,7 +485,7 @@ def build_arg_parser(): ) parser_mirror = subparsers.add_parser( - "mirror", help="Mirror a GitLab repository to GitHub." + "mirror", help=mirror_repo.__doc__.splitlines()[0] ) parser_mirror.set_defaults(func=mirror_repo) parser_mirror.add_argument( @@ -454,7 +493,7 @@ def build_arg_parser(): ) parser_archive = subparsers.add_parser( - "archive", help="Archive a repository." + "archive", help=archive_repo.__doc__.splitlines()[0] ) parser_archive.set_defaults(func=archive_repo) parser_archive.add_argument( @@ -469,6 +508,18 @@ def build_arg_parser(): ) parser_fork.set_defaults(func=fork_repo) parser_fork.add_argument("name", help="Name of the repository.") + + parser_tfinit = subparsers.add_parser( + "tfinit", help=terraform_init.__doc__.splitlines()[0] + ) + parser_tfinit.set_defaults(func=terraform_init) + parser_tfinit.add_argument( + "state", + help="The Terraform state name", + default="default", + nargs="?", + ) + return parser diff --git a/Documents/bin/rcfiles/git.py b/Documents/bin/rcfiles/git.py index 45cc5f4..74f3fe8 100644 --- a/Documents/bin/rcfiles/git.py +++ b/Documents/bin/rcfiles/git.py @@ -10,20 +10,27 @@ from sh.contrib import git # pylint: disable=import-error def is_repo(path): """Returns a boolean if the path is a Git repo.""" - return os.path.isdir(path) and pathlib.Path(path, ".git").is_dir() + try: + git("-C", path, "rev-parse", "--is-inside-work-tree") + except sh.ErrorReturnCode: + return False + return True def in_repo(): - """Is the current working directory a git repo? - - Because we invoke the command as a Git command (git foo) it is run from - the root of the repository if inside a repository so there's no need to - traverse up the directory hierarchy to find if we're in a Git repository, - it's enough to just check if the .git directory exists where we are. - """ + """Is the current working directory a git repo?""" return is_repo(".") +def find_repo_toplevel(path): + """Return the repository's top level directory (the root of the repo).""" + if not is_repo(path): + return None + return pathlib.Path( + git("-C", path, "rev-parse", "--show-toplevel").strip() + ) + + def get_all_remotes(): """Return a dictionary of remotes and their URL. @@ -33,7 +40,7 @@ def get_all_remotes(): return None config = configparser.ConfigParser() - config.read(".git/config") + config.read(find_repo_toplevel(".") / ".git/config") remotes = { x.removeprefix('remote "').removesuffix('"'): { -- GitLab