Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions otterdog/models/github_organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,8 +581,18 @@ async def _load_teams() -> None:
and default_org.get_team(team_name) is None
):
continue
team_members = await provider.get_org_team_members(github_id, team_slug)
team["members"] = team_members

# Team-Sync is only available for Enterprise organizations
if org.settings.plan == "enterprise":
team_sync = await provider.get_org_team_sync_mapping(github_id, team_slug)
else:
team_sync = None

if team_sync:
team["team_sync"] = team_sync
else:
team_members = await provider.get_org_team_members(github_id, team_slug)
team["members"] = team_members
org.add_team(Team.from_provider_data(github_id, team))
else:
_logger.debug("not reading teams, no default config available")
Expand Down
14 changes: 12 additions & 2 deletions otterdog/models/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
ModelObject,
ValidationContext,
)
from otterdog.providers.github.rest import TeamSyncMapping
from otterdog.utils import UNSET, is_set_and_valid, unwrap

if TYPE_CHECKING:
Expand All @@ -42,7 +43,8 @@ class Team(ModelObject, abc.ABC):
description: str | None
privacy: str
notifications: bool
members: list[str]
team_sync: list[TeamSyncMapping] | None
members: list[str] | None # members remain unset if team-sync is enabled
skip_members: bool = dataclasses.field(metadata={"model_only": True}, default=False)
skip_non_organization_members: bool = dataclasses.field(metadata={"model_only": True}, default=False)

Expand All @@ -55,7 +57,8 @@ def get_jsonnet_template_function(self, jsonnet_config: JsonnetConfig, extend: b

def include_field_for_diff_computation(self, field: dataclasses.Field) -> bool:
if field.name == "members":
return not self.skip_members
# if team-sync is enabled, members are managed externally and should not be part of diff computation
return not (self.team_sync or self.skip_members)

return True

Expand Down Expand Up @@ -98,6 +101,13 @@ def validate(self, context: ValidationContext, parent_object: Any) -> None:
f"but 'members' contains user '{member}' who is not an organization member.",
)

if self.team_sync and self.members:
context.add_failure(
FailureType.ERROR,
f"{self.get_model_header(parent_object)} has 'team_sync' enabled, "
f"but 'members' is also set to {self.members}.",
)

@classmethod
def get_mapping_from_provider(cls, org_id: str, data: dict[str, Any]) -> dict[str, Any]:
mapping = super().get_mapping_from_provider(org_id, data)
Expand Down
4 changes: 4 additions & 0 deletions otterdog/providers/github/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from importlib_resources import files

from otterdog import resources
from otterdog.providers.github.rest import TeamSyncMapping
from otterdog.utils import get_logger, is_ghsa_repo, is_set_and_present

if TYPE_CHECKING:
Expand Down Expand Up @@ -163,6 +164,9 @@ async def delete_org_custom_role(self, org_id: str, role_id: int, role_name: str
async def get_org_teams(self, org_id: str) -> list[dict[str, Any]]:
return await self.rest_api.team.get_teams(org_id)

async def get_org_team_sync_mapping(self, org_id: str, team_slug: str) -> list[TeamSyncMapping]:
return await self.rest_api.team.get_team_sync_mapping(org_id, team_slug)

async def get_org_team_members(self, org_id: str, team_slug: str) -> list[dict[str, Any]]:
return await self.rest_api.team.get_team_members(org_id, team_slug)

Expand Down
9 changes: 9 additions & 0 deletions otterdog/providers/github/rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from functools import cached_property
from typing import TYPE_CHECKING
Expand Down Expand Up @@ -166,3 +167,11 @@ def encrypt_value(public_key: str, secret_value: str) -> str:

def parse_iso_date_string(date: str) -> datetime:
return datetime.fromisoformat(date)

@dataclass
class TeamSyncMapping:
group_id: str
group_name: str
group_description: str
status: str | None
synced_at: str | None
13 changes: 12 additions & 1 deletion otterdog/providers/github/rest/team_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from otterdog.logging import get_logger
from otterdog.providers.github.exception import GitHubException
from otterdog.providers.github.rest import RestApi, RestClient
from otterdog.providers.github.rest import RestApi, RestClient, TeamSyncMapping

_logger = get_logger(__name__)

Expand Down Expand Up @@ -99,6 +99,17 @@ async def update_team(self, org_id: str, team_slug: str, team: dict[str, Any]) -
except GitHubException as ex:
raise RuntimeError(f"failed to update team '{team_slug}':\n{ex}") from ex

async def get_team_sync_mapping(self, org_id: str, team_slug: str) -> list[TeamSyncMapping]:
_logger.debug("retrieving team-sync for team '%s/%s'", org_id, team_slug)

try:
# https://docs.github.com/en/enterprise-cloud@latest/rest/teams/team-sync?apiVersion=2022-11-28#list-idp-groups-for-a-team
data: dict[str, list[dict[str, str]]] = await self.requester.request_json("GET", f"/orgs/{org_id}/teams/{team_slug}/team-sync/group-mappings")
except GitHubException as ex:
raise RuntimeError(f"failed retrieving team-sync for team '{org_id}/{team_slug}':\n{ex}") from ex

return [TeamSyncMapping(**group) for group in data.get("groups", [])]

async def get_team_members(self, org_id: str, team_slug: str) -> list[dict[str, Any]]:
_logger.debug("retrieving team members for team '%s/%s'", org_id, team_slug)

Expand Down
13 changes: 13 additions & 0 deletions otterdog/resources/schemas/team.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@
"type": "array",
"items": { "type": "string" }
},
"team_sync": {
"type": "array",
"items": {
"type": "object",
"properties": {
"group_id": { "type": "string" },
"group_name": { "type": "string" },
"group_description": { "type": "string" }
},
"required": [ "group_id", "group_name", "group_description" ],
"additionalProperties": false
}
},
"privacy": { "type": "string" },
"notifications": { "type": "boolean" },
"skip_members": { "type": "boolean" },
Expand Down
Loading