Beispiel #1
0
 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) =>
Beispiel #2
0
    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>()
        };
    }
Beispiel #3
0
    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);
    });