diff options
Diffstat (limited to 'arthur/exts/grafana/github_team_sync.py')
| -rw-r--r-- | arthur/exts/grafana/github_team_sync.py | 159 |
1 files changed, 159 insertions, 0 deletions
diff --git a/arthur/exts/grafana/github_team_sync.py b/arthur/exts/grafana/github_team_sync.py new file mode 100644 index 0000000..873c6c4 --- /dev/null +++ b/arthur/exts/grafana/github_team_sync.py @@ -0,0 +1,159 @@ +import aiohttp +import discord +from discord.ext import commands, tasks + +from arthur.apis import github, grafana +from arthur.bot import KingArthur +from arthur.log import logger + +from . import MissingMembers, SyncFigures + + +class GrafanaGitHubTeamSync(commands.Cog): + """ + Update Grafana team membership to match Github team membership. + + Grafana team name must match Github team slug exactly. + Use `gh api orgs/{org-name}/teams` to get a list of teams in an org + """ + + def __init__(self, bot: KingArthur) -> None: + self.bot = bot + self.sync_github_grafana_teams.start() + + async def _add_missing_members( + self, + grafana_team_id: int, + github_team_members: set[str], + grafana_team_members: set[str], + all_grafana_users: list[dict], + ) -> MissingMembers: + """ + Adds members to the Grafana team if they're in the Github team and not already present. + + Returns the number of missing members, and the number of members it could actually add. + """ + missing_members = github_team_members - grafana_team_members + added_members = 0 + for grafana_user in all_grafana_users: + if grafana_user["login"] not in missing_members: + continue + if grafana_user.get("auth_module") != "oauth_github": + continue + + await grafana.add_user_to_team( + grafana_user["userId"], + grafana_team_id, + self.bot.http_session, + ) + added_members += 1 + return MissingMembers(count=len(missing_members), successfully_added=added_members) + + async def _remove_extra_members( + self, + grafana_team_id: int, + github_team_members: set[str], + grafana_team_members: set[str], + all_grafana_users: list[dict], + ) -> int: + """ + Removes Grafana users from a team if they are not present in the Github team. + + Return how many were removed. + """ + extra_members = grafana_team_members - github_team_members + removed_members = 0 + for grafana_user in all_grafana_users: + if grafana_user["login"] not in extra_members: + continue + await grafana.remove_user_from_team( + grafana_user["userId"], + grafana_team_id, + self.bot.http_session, + ) + removed_members += 1 + return removed_members + + async def _sync_teams(self, team: dict[str, str]) -> SyncFigures: + """ + Ensure members in Github are present in Grafana teams. + + Return the number of members missing from the Grafana team, and the number of members added. + """ + github_team_members = { + member["login"] + for member in await github.list_team_members(team["name"], self.bot.http_session) + } + grafana_team_members = { + member["login"] + for member in await grafana.list_team_members(team["id"], self.bot.http_session) + if member.get("auth_module") == "oauth_github" + } + + all_grafana_users = await grafana.get_all_users(self.bot.http_session) + added_members = await self._add_missing_members( + team["id"], + github_team_members, + grafana_team_members, + all_grafana_users, + ) + removed_members = await self._remove_extra_members( + team["id"], + github_team_members, + grafana_team_members, + all_grafana_users, + ) + + return SyncFigures(added=added_members, removed=removed_members) + + @tasks.loop(hours=12) + async def sync_github_grafana_teams(self, channel: discord.TextChannel | None = None) -> None: + """Update Grafana team membership to match Github team membership.""" + grafana_teams = await grafana.list_teams(self.bot.http_session) + embed = discord.Embed( + title="Sync Stats", + colour=discord.Colour.blue(), + ) + for team in grafana_teams: + logger.debug(f"Processing {team["name"]}") + try: + figures = await self._sync_teams(team) + except aiohttp.ClientResponseError as e: + logger.error(e) + if channel: + await channel.send(e) + continue + + lines = [ + f"Missing: {figures.added.count}", + f"Added: {figures.added.successfully_added}", + f"Removed: {figures.removed}", + ] + embed.add_field( + name=team["name"], + value="\n".join(lines), + inline=False, + ) + + if channel: + await channel.send(embed=embed) + + @sync_github_grafana_teams.error + async def on_task_error(self, error: Exception) -> None: + """Ensure task errors are output.""" + logger.error(error) + + @commands.group(name="grafana_github", invoke_without_command=True) + async def grafana_group(self, ctx: commands.Context) -> None: + """Commands for working with grafana API.""" + await ctx.send_help(ctx.command) + + @grafana_group.command(name="sync") + async def sync_teams(self, ctx: commands.Context) -> None: + """Sync Grafana & Github teams now.""" + await self.sync_github_grafana_teams(ctx.channel) + + +async def setup(bot: KingArthur) -> None: + """Add cog to bot.""" + await bot.add_cog(GrafanaGitHubTeamSync(bot)) |