1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
|
from dataclasses import dataclass
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
@dataclass(frozen=True)
class MissingMembers:
"""Number of members that were missing from the Grafana team, and how many could be added."""
count: int
successfully_added: int
@dataclass(frozen=True)
class SyncFigures:
"""Figures related to a single sync members task run."""
added: MissingMembers
removed: int
class GrafanaTeamSync(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
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", aliases=("graf",), 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(GrafanaTeamSync(bot))
|