protected override Task <Component> CreateComponentAsync(Component component, Organization componentOrganization, DeploymentScope componentDeploymentScope, Project componentProject, User contextUser, IAsyncCollector <ICommand> commandQueue) => ExecuteAsync(component, contextUser, commandQueue, async(client, ownerLogin, project, memberTeam, adminTeam) =>
private async Task SynchronizeTeamMembersAsync(GitHubClient client, Team team, IEnumerable <User> users, Component component, User contextUser, IAsyncCollector <ICommand> commandQueue) { var userIds = new HashSet <string>(await users .ToAsyncEnumerable() .SelectMany(user => ResolveUserIdsAsync(user)) .ToArrayAsync() .ConfigureAwait(false)); var teamUsers = await userIds .Select(userId => { var user = users.SingleOrDefault(u => userId.Equals(u.Id)); return(user is null ? EnsureUserAsync(userId) : InitializeUserAsync(user)); }) .WhenAll() .ConfigureAwait(false); var logins = teamUsers .Select(user => user.AlternateIdentities.TryGetValue(this.Type, out var alternateIdentity) ? alternateIdentity.Login : null) .Where(login => !string.IsNullOrWhiteSpace(login)) .Distinct(StringComparer.OrdinalIgnoreCase); var members = await client.Organization.Team .GetAllMembers(team.Id) .ConfigureAwait(false); var membershipTasks = new List <Task>(); membershipTasks.AddRange(logins .Except(members.Select(m => m.Login), StringComparer.OrdinalIgnoreCase) .Select(login => client.Organization.Team.AddOrEditMembership(team.Id, login, new UpdateTeamMembership(TeamRole.Member)))); membershipTasks.AddRange(members .Select(m => m.Login) .Except(logins, StringComparer.OrdinalIgnoreCase) .Select(login => client.Organization.Team.RemoveMembership(team.Id, login))); await membershipTasks .WhenAll() .ConfigureAwait(false); async Task <User> EnsureUserAsync(string userId) { var user = await userRepository .GetAsync(component.Organization, userId, true) .ConfigureAwait(false); if (user is null) { user = new User() { Id = userId, Role = OrganizationUserRole.None }; user.AlternateIdentities[this.Type] = new AlternateIdentity(); await commandQueue .AddAsync(new OrganizationUserCreateCommand(contextUser, user)) .ConfigureAwait(false); } else { user = await InitializeUserAsync(user) .ConfigureAwait(false); } return(user); } async Task <User> InitializeUserAsync(User user) { if (user?.AlternateIdentities.TryAdd(Type, new AlternateIdentity()) ?? false) { await commandQueue .AddAsync(new OrganizationUserUpdateCommand(contextUser, user)) .ConfigureAwait(false); } return(user); } IAsyncEnumerable <string> ResolveUserIdsAsync(User user) => user.UserType switch { // return the given user id as async enumeration UserType.User => AsyncEnumerable.Repeat(user.Id, 1), // return user ids based on the given user that represents a group UserType.Group => graphService.GetGroupMembersAsync(user.Id, true), // not supported user type _ => AsyncEnumerable.Empty <string>() }; }
private Task <Component> ExecuteAsync(Component component, User contextUser, IAsyncCollector <ICommand> commandQueue, Func <GitHubClient, string, Octokit.Project, Octokit.Team, Octokit.Team, Task <Component> > callback) { if (component is null) { throw new ArgumentNullException(nameof(component)); } if (contextUser is null) { throw new ArgumentNullException(nameof(contextUser)); } if (commandQueue is null) { throw new ArgumentNullException(nameof(commandQueue)); } if (callback is null) { throw new ArgumentNullException(nameof(callback)); } return(base.WithContextAsync(component, async(componentOrganization, componentDeploymentScope, componentProject) => { var token = await TokenClient .GetAsync <GitHubToken>(componentDeploymentScope) .ConfigureAwait(false); var client = await CreateClientAsync(token) .ConfigureAwait(false); var teams = await client.Organization.Team .GetAll(token.OwnerLogin) .ConfigureAwait(false); var tasks = new List <Task>(); var teamsByName = teams .ToDictionary(k => k.Name, v => v); var organizationTeamName = $"TeamCloud-{componentOrganization.Slug}"; var organizationTeam = await EnsureTeamAsync(organizationTeamName, null, new NewTeam(organizationTeamName) { Description = $"TeamCloud {componentOrganization.DisplayName} organization.", Privacy = TeamPrivacy.Closed // Parent and nested child teams must use Closed }).ConfigureAwait(false); var organizationAdminTeamName = $"{organizationTeamName}-Admins"; var organizationAdminTeam = await EnsureTeamAsync(organizationAdminTeamName, organizationTeam, new NewTeam(organizationAdminTeamName) { Description = $"TeamCloud {componentOrganization.DisplayName} organization admins.", Privacy = TeamPrivacy.Closed, // Parent and nested child teams must use Closed Permission = Permission.Admin }).ConfigureAwait(false); var projectTeamName = $"{organizationTeamName}-{componentProject.Slug}"; var projectTeam = await EnsureTeamAsync(projectTeamName, organizationTeam, new NewTeam(projectTeamName) { Description = $"TeamCloud project {componentProject.DisplayName} in the {componentOrganization.DisplayName} organization.", Privacy = TeamPrivacy.Closed // Parent and nested child teams must use Closed }).ConfigureAwait(false); var projectAdminsTeamName = $"{projectTeamName}-Admins"; var projectAdminsTeam = await EnsureTeamAsync(projectAdminsTeamName, projectTeam, new NewTeam(projectAdminsTeamName) { Description = $"TeamCloud project {componentProject.DisplayName} admins in the {componentOrganization.DisplayName} organization.", Privacy = TeamPrivacy.Closed, // Parent and nested child teams must use Closed Permission = Permission.Admin }).ConfigureAwait(false); var projectName = $"TeamCloud-{componentOrganization.Slug}-{componentProject.Slug}"; var project = await EnsureProjectAsync(projectName, new NewProject(projectName) { Body = $"Project for TeamCloud project {componentProject.DisplayName} in organization {componentOrganization.DisplayName}" }).ConfigureAwait(false); var organizationOwners = await userRepository .ListOwnersAsync(component.Organization) .ToArrayAsync() .ConfigureAwait(false); var organizationAdmins = await userRepository .ListAdminsAsync(component.Organization) .ToArrayAsync() .ConfigureAwait(false); await Task.WhenAll( EnsurePermissionAsync(projectTeam, project, "write"), EnsurePermissionAsync(projectAdminsTeam, project, "admin"), SynchronizeTeamMembersAsync(client, organizationAdminTeam, Enumerable.Concat(organizationOwners, organizationAdmins).Distinct(), component, contextUser, commandQueue) ).ConfigureAwait(false); return await callback(client, token.OwnerLogin, project, projectTeam, projectAdminsTeam).ConfigureAwait(false); async Task <Team> EnsureTeamAsync(string teamName, Team parentTeam, NewTeam teamDefinition) { if (!teamsByName.TryGetValue(teamName, out var team)) { await using var adapterLock = await AcquireLockAsync(nameof(GitHubAdapter), component.DeploymentScopeId).ConfigureAwait(false); teamDefinition.ParentTeamId = parentTeam?.Id; try { team = await client.Organization.Team .Create(token.OwnerLogin, teamDefinition) .ConfigureAwait(false); } catch (ApiException exc) when(exc.StatusCode == HttpStatusCode.UnprocessableEntity) // yes, thats the status code if the team already exists { var teams = await client.Organization.Team .GetAll(token.OwnerLogin) .ConfigureAwait(false); team = teams .FirstOrDefault(t => t.Name.Equals(teamName, StringComparison.Ordinal)); if (team is null) { throw; } } } if (team.Parent?.Id != parentTeam?.Id) { await using var adapterLock = await AcquireLockAsync(nameof(GitHubAdapter), component.DeploymentScopeId).ConfigureAwait(false); team = await client.Organization.Team.Update(team.Id, new UpdateTeam(team.Name) { ParentTeamId = parentTeam?.Id ?? 0 }).ConfigureAwait(false); } return team; } async Task <Octokit.Project> EnsureProjectAsync(string projectName, NewProject projectDefinition) { var projects = await client.Repository.Project .GetAllForOrganization(token.OwnerLogin) .ConfigureAwait(false); var project = projects .FirstOrDefault(t => t.Name.Equals(projectName, StringComparison.Ordinal)); try { project ??= await client.Repository.Project .CreateForOrganization(token.OwnerLogin, projectDefinition) .ConfigureAwait(false); } catch (ApiException exc) when(exc.StatusCode == HttpStatusCode.UnprocessableEntity) { // the project already exists - try to re-fetch project information projects = await client.Repository.Project .GetAllForOrganization(token.OwnerLogin) .ConfigureAwait(false); project = projects .FirstOrDefault(t => t.Name.Equals(projectName, StringComparison.Ordinal)); if (project is null) { throw new ApplicationException($"Duplicate project ({projectName}) under unknown ownership detected", exc); } } catch (ApiException exc) when(exc.StatusCode == HttpStatusCode.NotFound) { throw new ApplicationException($"Organization level projects disabled in {client.Connection.BaseAddress}"); } return project; } async Task EnsurePermissionAsync(Octokit.Team team, Octokit.Project project, string permission) { var inertiaClient = await CreateClientAsync(component, "inertia-preview").ConfigureAwait(false); var url = new Uri($"/orgs/{token.OwnerLogin}/teams/{team.Slug}/projects/{project.Id}", UriKind.Relative); _ = await inertiaClient.Connection .Put <string>(url, new { Permission = permission }) .ConfigureAwait(false); } })); }
protected override Task <Component> UpdateComponentAsync(Component component, Organization organization, DeploymentScope deploymentScope, Project project, User contextUser, IAsyncCollector <ICommand> commandQueue) => WithKubernetesContext(component, deploymentScope, (client, data, roleDefinition, serviceAccount) => { //TODO: implement some update logic - e.g. permission management for project users return(Task.FromResult(component)); });
protected override Task <Component> DeleteComponentAsync(Component component, Organization organization, DeploymentScope deploymentScope, Project project, User contextUser, IAsyncCollector <ICommand> commandQueue) => WithKubernetesContext(component, deploymentScope, async(client, data, roleDefinition, serviceAccount) => { try { await client .DeleteNamespaceAsync($"{data.Namespace}-{component.Id}") .ConfigureAwait(false); } catch (HttpOperationException exc) when(exc.Response.StatusCode == System.Net.HttpStatusCode.NotFound) { // swallow - the namespace was already deleted } return(component); });
protected override Task <Component> CreateComponentAsync(Component component, Organization organization, DeploymentScope deploymentScope, Project project, User contextUser, IAsyncCollector <ICommand> commandQueue) => WithKubernetesContext(component, deploymentScope, async(client, data, roleDefinition, serviceAccount) => { var componentNamespace = new V1Namespace() { Metadata = new V1ObjectMeta() { Name = $"{data.Namespace}-{component.Id}" } }; try { componentNamespace = await client .CreateNamespaceAsync(componentNamespace) .ConfigureAwait(false); } catch (HttpOperationException exc) when(exc.Response.StatusCode == System.Net.HttpStatusCode.Conflict) { componentNamespace = await client .ReadNamespaceAsync(componentNamespace.Metadata.Name) .ConfigureAwait(false); } var roleBinding = new V1RoleBinding() { Metadata = new V1ObjectMeta() { Name = "runner" }, RoleRef = new V1RoleRef() { ApiGroup = roleDefinition.ApiGroup(), Kind = roleDefinition.Kind, Name = roleDefinition.Name() }, Subjects = new List <V1Subject>() { new V1Subject() { ApiGroup = serviceAccount.ApiGroup(), Kind = serviceAccount.Kind, Name = serviceAccount.Name(), NamespaceProperty = serviceAccount.Namespace() } } }; try { await client .CreateNamespacedRoleBindingAsync(roleBinding, componentNamespace.Metadata.Name) .ConfigureAwait(false); } catch (HttpOperationException exc) when(exc.Response.StatusCode == System.Net.HttpStatusCode.Conflict) { await client .ReplaceNamespacedRoleBindingAsync(roleBinding, roleBinding.Metadata.Name, componentNamespace.Metadata.Name) .ConfigureAwait(false); } return(component); });