2022-11-03 08:57:08 +01:00

218 lines
6.2 KiB
Go

// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License-AGPL.txt in the project root for license information.
package apiv1
import (
"context"
"errors"
"fmt"
connect "github.com/bufbuild/connect-go"
"github.com/gitpod-io/gitpod/common-go/log"
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
"github.com/gitpod-io/gitpod/public-api-server/pkg/auth"
"github.com/gitpod-io/gitpod/public-api-server/pkg/proxy"
v1 "github.com/gitpod-io/gitpod/public-api/experimental/v1"
"github.com/gitpod-io/gitpod/public-api/experimental/v1/v1connect"
"github.com/relvacode/iso8601"
"google.golang.org/protobuf/types/known/timestamppb"
)
func NewTeamsService(pool proxy.ServerConnectionPool) *TeamService {
return &TeamService{
connectionPool: pool,
}
}
var _ v1connect.TeamsServiceHandler = (*TeamService)(nil)
type TeamService struct {
connectionPool proxy.ServerConnectionPool
v1connect.UnimplementedTeamsServiceHandler
}
func (s *TeamService) CreateTeam(ctx context.Context, req *connect.Request[v1.CreateTeamRequest]) (*connect.Response[v1.CreateTeamResponse], error) {
token := auth.TokenFromContext(ctx)
if req.Msg.GetName() == "" {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Name is a required argument when creating a team."))
}
conn, err := s.connectionPool.Get(ctx, token)
if err != nil {
log.Log.WithError(err).Error("Failed to get connection to server.")
return nil, connect.NewError(connect.CodeInternal, err)
}
created, err := conn.CreateTeam(ctx, req.Msg.GetName())
if err != nil {
log.WithError(err).Error("Failed to create team.")
return nil, proxy.ConvertError(err)
}
team, err := s.toTeamAPIResponse(ctx, conn, created)
if err != nil {
log.WithError(err).Error("Failed to populate team with details.")
return nil, err
}
return connect.NewResponse(&v1.CreateTeamResponse{
Team: team,
}), nil
}
func (s *TeamService) GetTeam(ctx context.Context, req *connect.Request[v1.GetTeamRequest]) (*connect.Response[v1.GetTeamResponse], error) {
if req.Msg.GetTeamId() == "" {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("team ID is a required argument for retrieving a team."))
}
token := auth.TokenFromContext(ctx)
conn, err := s.connectionPool.Get(ctx, token)
if err != nil {
log.Log.WithError(err).Error("Failed to get connection to server.")
return nil, connect.NewError(connect.CodeInternal, err)
}
team, err := conn.GetTeam(ctx, req.Msg.GetTeamId())
if err != nil {
return nil, proxy.ConvertError(err)
}
response, err := s.toTeamAPIResponse(ctx, conn, team)
if err != nil {
return nil, err
}
return connect.NewResponse(&v1.GetTeamResponse{
Team: response,
}), nil
}
func (s *TeamService) ListTeams(ctx context.Context, req *connect.Request[v1.ListTeamsRequest]) (*connect.Response[v1.ListTeamsResponse], error) {
token := auth.TokenFromContext(ctx)
conn, err := s.connectionPool.Get(ctx, token)
if err != nil {
log.Log.WithError(err).Error("Failed to get connection to server.")
return nil, connect.NewError(connect.CodeInternal, err)
}
teams, err := conn.GetTeams(ctx)
if err != nil {
log.WithError(err).Error("Failed to list teams from server.")
return nil, proxy.ConvertError(err)
}
var response []*v1.Team
for _, t := range teams {
team, err := s.toTeamAPIResponse(ctx, conn, t)
if err != nil {
log.WithError(err).Error("Failed to populate team with details.")
return nil, err
}
response = append(response, team)
}
return connect.NewResponse(&v1.ListTeamsResponse{
Teams: response,
}), nil
}
func (s *TeamService) JoinTeam(ctx context.Context, req *connect.Request[v1.JoinTeamRequest]) (*connect.Response[v1.JoinTeamResponse], error) {
if req.Msg.GetInvitationId() == "" {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invitation id is a required argument to join a team"))
}
token := auth.TokenFromContext(ctx)
conn, err := s.connectionPool.Get(ctx, token)
if err != nil {
log.Log.WithError(err).Error("Failed to get connection to server.")
return nil, connect.NewError(connect.CodeInternal, errors.New("failed to establish connection to downstream services"))
}
team, err := conn.JoinTeam(ctx, req.Msg.GetInvitationId())
if err != nil {
return nil, proxy.ConvertError(err)
}
response, err := s.toTeamAPIResponse(ctx, conn, team)
if err != nil {
log.WithError(err).Error("Failed to populate team with details.")
return nil, err
}
return connect.NewResponse(&v1.JoinTeamResponse{
Team: response,
}), nil
}
func (s *TeamService) toTeamAPIResponse(ctx context.Context, conn protocol.APIInterface, team *protocol.Team) (*v1.Team, error) {
members, err := conn.GetTeamMembers(ctx, team.ID)
if err != nil {
log.WithError(err).Error("Failed to get team members.")
return nil, proxy.ConvertError(err)
}
invite, err := conn.GetGenericInvite(ctx, team.ID)
if err != nil {
log.WithError(err).Error("Failed to get generic invite.")
return nil, proxy.ConvertError(err)
}
return teamToAPIResponse(team, members, invite), nil
}
func teamToAPIResponse(team *protocol.Team, members []*protocol.TeamMemberInfo, invite *protocol.TeamMembershipInvite) *v1.Team {
return &v1.Team{
Id: team.ID,
Name: team.Name,
Slug: team.Slug,
Members: teamMembersToAPIResponse(members),
TeamInvitation: &v1.TeamInvitation{
Id: invite.ID,
},
}
}
func teamMembersToAPIResponse(members []*protocol.TeamMemberInfo) []*v1.TeamMember {
var result []*v1.TeamMember
for _, m := range members {
result = append(result, &v1.TeamMember{
UserId: m.UserId,
Role: teamRoleToAPIResponse(m.Role),
MemberSince: parseTimeStamp(m.MemberSince),
AvatarUrl: m.AvatarUrl,
FullName: m.FullName,
PrimaryEmail: m.PrimaryEmail,
})
}
return result
}
func teamRoleToAPIResponse(role protocol.TeamMemberRole) v1.TeamRole {
switch role {
case protocol.TeamMember_Owner:
return v1.TeamRole_TEAM_ROLE_OWNER
case protocol.TeamMember_Member:
return v1.TeamRole_TEAM_ROLE_MEMBER
default:
return v1.TeamRole_TEAM_ROLE_UNSPECIFIED
}
}
func parseTimeStamp(s string) *timestamppb.Timestamp {
parsed, err := iso8601.ParseString(s)
if err != nil {
return &timestamppb.Timestamp{}
}
return timestamppb.New(parsed)
}