Huiwen 394dcae20f
[papi,server] implement restricted_workspace_classes in server (#19481)
* [papi,server] implement `restricted_workspace_classes` in server

* Fix test case input

* nit
2024-02-28 22:37:09 +02:00

200 lines
6.0 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"
"strings"
connect "github.com/bufbuild/connect-go"
"github.com/gitpod-io/gitpod/common-go/log"
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
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"
"github.com/google/uuid"
)
func NewProjectsService(pool proxy.ServerConnectionPool) *ProjectsService {
return &ProjectsService{
connectionPool: pool,
}
}
type ProjectsService struct {
connectionPool proxy.ServerConnectionPool
v1connect.UnimplementedProjectsServiceHandler
}
func (s *ProjectsService) CreateProject(ctx context.Context, req *connect.Request[v1.CreateProjectRequest]) (*connect.Response[v1.CreateProjectResponse], error) {
spec := req.Msg.GetProject()
name := strings.TrimSpace(spec.GetName())
if name == "" {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Name is a required argument."))
}
cloneURL := strings.TrimSpace(spec.GetCloneUrl())
if cloneURL == "" {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Clone URL is a required argument."))
}
teamID := spec.GetTeamId()
_, err := uuid.Parse(teamID)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Team ID is not a valid UUID."))
}
conn, err := s.getConnection(ctx)
if err != nil {
return nil, err
}
project, err := conn.CreateProject(ctx, &protocol.CreateProjectOptions{
Name: name,
TeamID: teamID,
CloneURL: cloneURL,
AppInstallationID: "undefined", // sadly that's how we store cases where there is no AppInstallationID
})
if err != nil {
return nil, proxy.ConvertError(err)
}
return connect.NewResponse(&v1.CreateProjectResponse{
Project: projectToAPIResponse(project),
}), nil
}
func (s *ProjectsService) ListProjects(ctx context.Context, req *connect.Request[v1.ListProjectsRequest]) (*connect.Response[v1.ListProjectsResponse], error) {
teamID := req.Msg.GetTeamId()
if teamID == "" {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Organization ID not specified."))
}
conn, err := s.getConnection(ctx)
if err != nil {
return nil, err
}
var projects []*protocol.Project
if teamID != "" {
_, err := uuid.Parse(teamID)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Organization ID is not a valid UUID."))
}
projects, err = conn.GetTeamProjects(ctx, teamID)
if err != nil {
return nil, proxy.ConvertError(err)
}
}
// We're extracting a particular page of results from the full set of results.
// This is wasteful, but necessary, until we either:
// * Add new APIs to server which support pagination
// * Port the query logic to Public API
results := pageFromResults(projects, req.Msg.GetPagination())
return connect.NewResponse(&v1.ListProjectsResponse{
Projects: projectsToAPIResponse(results),
TotalResults: int32(len(projects)),
}), nil
}
func (s *ProjectsService) DeleteProject(ctx context.Context, req *connect.Request[v1.DeleteProjectRequest]) (*connect.Response[v1.DeleteProjectResponse], error) {
projectID, err := validateProjectID(ctx, req.Msg.GetProjectId())
if err != nil {
return nil, err
}
conn, err := s.getConnection(ctx)
if err != nil {
return nil, err
}
err = conn.DeleteProject(ctx, projectID.String())
if err != nil {
return nil, proxy.ConvertError(err)
}
return connect.NewResponse(&v1.DeleteProjectResponse{}), nil
}
func (s *ProjectsService) getConnection(ctx context.Context) (protocol.APIInterface, error) {
token, err := auth.TokenFromContext(ctx)
if err != nil {
return nil, connect.NewError(connect.CodeUnauthenticated, fmt.Errorf("No credentials present on request."))
}
conn, err := s.connectionPool.Get(ctx, token)
if err != nil {
log.Extract(ctx).WithError(err).Error("Failed to get connection to server.")
return nil, connect.NewError(connect.CodeInternal, errors.New("Failed to establish connection to downstream services. If this issue persists, please contact Gitpod Support."))
}
return conn, nil
}
func projectsToAPIResponse(ps []*protocol.Project) []*v1.Project {
var projects []*v1.Project
for _, p := range ps {
projects = append(projects, projectToAPIResponse(p))
}
return projects
}
func projectToAPIResponse(p *protocol.Project) *v1.Project {
return &v1.Project{
Id: p.ID,
TeamId: p.TeamID,
Name: p.Name,
CloneUrl: p.CloneURL,
CreationTime: parseGitpodTimeStampOrDefault(p.CreationTime),
Settings: projectSettingsToAPIResponse(p.Settings),
}
}
func projectSettingsToAPIResponse(s *protocol.ProjectSettings) *v1.ProjectSettings {
if s == nil {
return &v1.ProjectSettings{}
}
settings := &v1.ProjectSettings{
Prebuild: &v1.PrebuildSettings{},
Workspace: &v1.WorkspaceSettings{
WorkspaceClass: workspaceClassesToAPIResponse(s.WorkspaceClasses),
},
}
if s.PrebuildSettings != nil {
settings.Prebuild.EnablePrebuilds = s.PrebuildSettings.Enable
settings.Prebuild.BranchStrategy = s.PrebuildSettings.BranchStrategy
settings.Prebuild.BranchMatchingPattern = s.PrebuildSettings.BranchMatchingPattern
settings.Prebuild.PrebuildInterval = s.PrebuildSettings.PrebuildInterval
settings.Prebuild.WorkspaceClass = s.PrebuildSettings.WorkspaceClass
}
if s.RestrictedWorkspaceClasses != nil {
settings.Workspace.RestrictedWorkspaceClasses = *s.RestrictedWorkspaceClasses
}
return settings
}
func workspaceClassesToAPIResponse(s *protocol.WorkspaceClassesSettings) *v1.WorkspaceClassSettings {
if s == nil {
return &v1.WorkspaceClassSettings{}
}
return &v1.WorkspaceClassSettings{
Regular: s.Regular,
Prebuild: s.Prebuild,
}
}