diff --git a/webhook_to_fedora_messaging/migration_from_gh2fm/gh2fm.py b/webhook_to_fedora_messaging/migration_from_gh2fm/gh2fm.py index 488892d..fd3024d 100644 --- a/webhook_to_fedora_messaging/migration_from_gh2fm/gh2fm.py +++ b/webhook_to_fedora_messaging/migration_from_gh2fm/gh2fm.py @@ -1,6 +1,16 @@ from contextlib import asynccontextmanager -from sqlalchemy import Boolean, Column, ForeignKey, Integer, MetaData, String, Table, Unicode +from sqlalchemy import ( + Boolean, + Column, + DateTime, + ForeignKey, + Integer, + MetaData, + String, + Table, + Unicode, +) from sqlalchemy.ext.asyncio import create_async_engine @@ -14,6 +24,7 @@ Column("emails", Unicode), Column("full_name", String), Column("oauth_access_token", Unicode), + Column("created_on", DateTime), ) repos = Table( diff --git a/webhook_to_fedora_messaging/migration_from_gh2fm/import_repo.py b/webhook_to_fedora_messaging/migration_from_gh2fm/import_repo.py index 8b04548..875282a 100644 --- a/webhook_to_fedora_messaging/migration_from_gh2fm/import_repo.py +++ b/webhook_to_fedora_messaging/migration_from_gh2fm/import_repo.py @@ -1,9 +1,12 @@ import asyncio import logging from contextlib import asynccontextmanager +from functools import lru_cache from uuid import uuid4 import click +from fasjson_client import Client as FasjsonClient +from fasjson_client.errors import APIError as FasjsonApiError from sqlalchemy import select from sqlalchemy.exc import IntegrityError, NoResultFound @@ -33,9 +36,9 @@ help="The URL to Github2Fedmsg's DB", ) @click.option("-d", "--debug", is_flag=True, help="Show more information") -@click.argument("fas_username", required=True) @click.argument("repo", required=True) -def main(config_path, github2fedmsg_db_url, debug, fas_username, repo): +@click.argument("fas_username", required=False) +def main(config_path, github2fedmsg_db_url, debug, repo, fas_username): if config_path: set_config_file(config_path) logging.basicConfig( @@ -46,10 +49,6 @@ def main(config_path, github2fedmsg_db_url, debug, fas_username, repo): log.addHandler(log.root.handlers[0]) log.propagate = False - if repo.count("/") != 1: - raise click.BadArgumentUsage( - "The repo argument must be in the 'user_or_org/repo_name' format." - ) asyncio.run(_main(repo, fas_username, github2fedmsg_db_url)) @@ -57,23 +56,40 @@ def main(config_path, github2fedmsg_db_url, debug, fas_username, repo): async def _main(repo_full_name, fas_username, github2fedmsg_db_url): - user_or_org, repo_name = repo_full_name.split("/") + if "/" in repo_full_name: + user_or_org, repo_name = repo_full_name.split("/") + else: + user_or_org, repo_name = repo_full_name, None + query = ( + select(gh2fm.repos, gh2fm.users.c.username.label("fas_username")) + .join_from(gh2fm.repos, gh2fm.users) + .where(gh2fm.repos.c.username == user_or_org) + ) + if repo_name is not None: + query = query.where(gh2fm.repos.c.name == repo_name) + else: + query = query.where(gh2fm.repos.c.enabled.is_(True)) + async with gh2fm.get_session(github2fedmsg_db_url) as gh2fm_session: - query = ( - select(gh2fm.repos, gh2fm.users.c.username.label("fas_username")) - .join_from(gh2fm.repos, gh2fm.users) - .where(gh2fm.repos.c.username == user_or_org, gh2fm.repos.c.name == repo_name) - ) result = await gh2fm_session.execute(query) try: - repo = result.one() + if repo_name is not None: + repo = result.one() + else: + repo = result.first() + if repo is None: + raise NoResultFound except NoResultFound: log.error("Could not find repo %r in Github2Fedmsg's database", repo_full_name) return if not repo.enabled: - log.info("The repo is disabled, skipping.") + log.info("The repo %s/%s is disabled, skipping.", repo.username, repo.name) + print(query) return - if not repo.fas_username.startswith("github_org_") and repo.fas_username != fas_username: + original_fas_username = await _get_owner(gh2fm_session, repo) + if not fas_username: + fas_username = original_fas_username + if original_fas_username != fas_username: log.warning( "The repo %s used to be linked to FAS user %s, it will now be linked " "to FAS user %s", @@ -82,10 +98,58 @@ async def _main(repo_full_name, fas_username, github2fedmsg_db_url): fas_username, ) async with with_db_session() as db_session: - await import_repo(db_session, repo, fas_username) + await import_repo(db_session, repo_full_name, fas_username) + + +def _get_fasjson_url(): + from configparser import ConfigParser + + ipa_config = ConfigParser() + ipa_config.read("/etc/ipa/default.conf") + domain = ipa_config.get("global", "domain") + return f"https://fasjson.{domain}" + + +async def _get_owner(gh2fm_session, repo): + owner = repo.fas_username + if owner.startswith("github_org_"): + org_name = owner[len("github_org_") :] + try: + owner = ( + await gh2fm_session.execute( + select(gh2fm.users.c.username) + .join( + gh2fm.org_to_user, + gh2fm.org_to_user.c.org_id == gh2fm.users.c.github_username, + ) + .where(gh2fm.org_to_user.c.usr_id == org_name) + .order_by(gh2fm.users.c.created_on) + ) + ).scalar() + except NoResultFound as e: + log.error("No owner specified for github org %s", org_name) + raise click.Abort() from e + # Check that it actually exists + return _get_fas_username(owner) -async def import_repo(db, repo, username): +@lru_cache +def _get_fasjson_client(): + return FasjsonClient(_get_fasjson_url()) + + +@lru_cache +def _get_fas_username(username): + fasjson = _get_fasjson_client() + try: + fas_user = fasjson.get_user(username=username).result + except FasjsonApiError as e: + log.error("FASJSON error on user %s: %s", username, e) + raise click.Abort() from e + return fas_user["username"] + + +async def import_repo(db, repo_full_name, username, repo_description=""): db_user = await db.scalar(select(User).where(User.name == username)) if not db_user: db_user = User(name=username) @@ -93,10 +157,10 @@ async def import_repo(db, repo, username): log.debug("Adding user %s", username) await db.flush() db_service = Service( - name=f"{repo.username}/{repo.name}", + name=f"{repo_full_name}", uuid=uuid4().hex[0:8], type="github", - desc=f"Migrated from GitHub2FedMsg. {repo.description}", + desc=f"Migrated from GitHub2FedMsg. {repo_description}".strip(), disabled=False, ) db.add(db_service) @@ -109,6 +173,32 @@ async def import_repo(db, repo, username): stmt = owners_table.insert().values({"service_id": db_service.id, "user_id": db_user.id}) await db.execute(stmt) log.info("Service %s created", db_service.name) + print( + f"""Hi @{username} ! + +You can now add the following webhook to the """ + '{"repo" if "/" in repo_full_name else "organization"} ' + "mentioned above." + ) + if "/" in repo_full_name: + user_or_org, repo_name = repo_full_name.split("/") + print( + f"Note that you can also add the webhook at the organization level (`{user_or_org}`), " + "which will make it active for all the organization's repos. I would suggest doing " + "that to save some effort and automatically cover repos you may add in the future " + "(make sure however than another teammate hasn't already added it)." + ) + print( + f""" +``` +Webhook URL: https://webhook.fedoraproject.org/api/v1/messages/{db_service.uuid} +Secret: {db_service.token} +Webhook content-type: application/json +``` + +Please tell us if there are any issues! Thanks! +""" + ) if __name__ == "__main__":