Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/public-api-server/pkg/apiv1/project.go
2499 views
1
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package apiv1
6
7
import (
8
"context"
9
"errors"
10
"fmt"
11
"strings"
12
13
connect "github.com/bufbuild/connect-go"
14
"github.com/gitpod-io/gitpod/common-go/log"
15
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
16
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
17
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
18
"github.com/gitpod-io/gitpod/public-api-server/pkg/auth"
19
"github.com/gitpod-io/gitpod/public-api-server/pkg/proxy"
20
"github.com/google/uuid"
21
)
22
23
func NewProjectsService(pool proxy.ServerConnectionPool) *ProjectsService {
24
return &ProjectsService{
25
connectionPool: pool,
26
}
27
}
28
29
type ProjectsService struct {
30
connectionPool proxy.ServerConnectionPool
31
32
v1connect.UnimplementedProjectsServiceHandler
33
}
34
35
func (s *ProjectsService) CreateProject(ctx context.Context, req *connect.Request[v1.CreateProjectRequest]) (*connect.Response[v1.CreateProjectResponse], error) {
36
spec := req.Msg.GetProject()
37
38
name := strings.TrimSpace(spec.GetName())
39
if name == "" {
40
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Name is a required argument."))
41
}
42
43
cloneURL := strings.TrimSpace(spec.GetCloneUrl())
44
if cloneURL == "" {
45
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Clone URL is a required argument."))
46
}
47
48
teamID := spec.GetTeamId()
49
_, err := uuid.Parse(teamID)
50
if err != nil {
51
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Team ID is not a valid UUID."))
52
}
53
54
conn, err := s.getConnection(ctx)
55
if err != nil {
56
return nil, err
57
}
58
59
project, err := conn.CreateProject(ctx, &protocol.CreateProjectOptions{
60
Name: name,
61
TeamID: teamID,
62
CloneURL: cloneURL,
63
AppInstallationID: "undefined", // sadly that's how we store cases where there is no AppInstallationID
64
})
65
if err != nil {
66
return nil, proxy.ConvertError(err)
67
}
68
69
return connect.NewResponse(&v1.CreateProjectResponse{
70
Project: projectToAPIResponse(project),
71
}), nil
72
}
73
74
func (s *ProjectsService) ListProjects(ctx context.Context, req *connect.Request[v1.ListProjectsRequest]) (*connect.Response[v1.ListProjectsResponse], error) {
75
teamID := req.Msg.GetTeamId()
76
if teamID == "" {
77
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Organization ID not specified."))
78
}
79
80
conn, err := s.getConnection(ctx)
81
if err != nil {
82
return nil, err
83
}
84
85
var projects []*protocol.Project
86
87
if teamID != "" {
88
_, err := uuid.Parse(teamID)
89
if err != nil {
90
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Organization ID is not a valid UUID."))
91
}
92
93
projects, err = conn.GetTeamProjects(ctx, teamID)
94
if err != nil {
95
return nil, proxy.ConvertError(err)
96
}
97
}
98
99
// We're extracting a particular page of results from the full set of results.
100
// This is wasteful, but necessary, until we either:
101
// * Add new APIs to server which support pagination
102
// * Port the query logic to Public API
103
results := pageFromResults(projects, req.Msg.GetPagination())
104
105
return connect.NewResponse(&v1.ListProjectsResponse{
106
Projects: projectsToAPIResponse(results),
107
TotalResults: int32(len(projects)),
108
}), nil
109
}
110
111
func (s *ProjectsService) DeleteProject(ctx context.Context, req *connect.Request[v1.DeleteProjectRequest]) (*connect.Response[v1.DeleteProjectResponse], error) {
112
projectID, err := validateProjectID(ctx, req.Msg.GetProjectId())
113
if err != nil {
114
return nil, err
115
}
116
117
conn, err := s.getConnection(ctx)
118
if err != nil {
119
return nil, err
120
}
121
122
err = conn.DeleteProject(ctx, projectID.String())
123
if err != nil {
124
return nil, proxy.ConvertError(err)
125
}
126
127
return connect.NewResponse(&v1.DeleteProjectResponse{}), nil
128
}
129
130
func (s *ProjectsService) getConnection(ctx context.Context) (protocol.APIInterface, error) {
131
token, err := auth.TokenFromContext(ctx)
132
if err != nil {
133
return nil, connect.NewError(connect.CodeUnauthenticated, fmt.Errorf("No credentials present on request."))
134
}
135
136
conn, err := s.connectionPool.Get(ctx, token)
137
if err != nil {
138
log.Extract(ctx).WithError(err).Error("Failed to get connection to server.")
139
return nil, connect.NewError(connect.CodeInternal, errors.New("Failed to establish connection to downstream services. If this issue persists, please contact Gitpod Support."))
140
}
141
142
return conn, nil
143
}
144
145
func projectsToAPIResponse(ps []*protocol.Project) []*v1.Project {
146
var projects []*v1.Project
147
for _, p := range ps {
148
projects = append(projects, projectToAPIResponse(p))
149
}
150
151
return projects
152
}
153
154
func projectToAPIResponse(p *protocol.Project) *v1.Project {
155
return &v1.Project{
156
Id: p.ID,
157
TeamId: p.TeamID,
158
Name: p.Name,
159
CloneUrl: p.CloneURL,
160
CreationTime: parseGitpodTimeStampOrDefault(p.CreationTime),
161
Settings: projectSettingsToAPIResponse(p.Settings),
162
}
163
}
164
165
func projectSettingsToAPIResponse(s *protocol.ProjectSettings) *v1.ProjectSettings {
166
if s == nil {
167
return &v1.ProjectSettings{}
168
}
169
170
settings := &v1.ProjectSettings{
171
Prebuild: &v1.PrebuildSettings{},
172
Workspace: &v1.WorkspaceSettings{
173
WorkspaceClass: workspaceClassesToAPIResponse(s.WorkspaceClasses),
174
},
175
}
176
if s.PrebuildSettings != nil {
177
settings.Prebuild.EnablePrebuilds = s.PrebuildSettings.Enable
178
settings.Prebuild.BranchStrategy = s.PrebuildSettings.BranchStrategy
179
settings.Prebuild.BranchMatchingPattern = s.PrebuildSettings.BranchMatchingPattern
180
settings.Prebuild.PrebuildInterval = s.PrebuildSettings.PrebuildInterval
181
settings.Prebuild.WorkspaceClass = s.PrebuildSettings.WorkspaceClass
182
}
183
if s.RestrictedWorkspaceClasses != nil {
184
settings.Workspace.RestrictedWorkspaceClasses = *s.RestrictedWorkspaceClasses
185
}
186
187
return settings
188
}
189
190
func workspaceClassesToAPIResponse(s *protocol.WorkspaceClassesSettings) *v1.WorkspaceClassSettings {
191
if s == nil {
192
return &v1.WorkspaceClassSettings{}
193
}
194
195
return &v1.WorkspaceClassSettings{
196
Regular: s.Regular,
197
Prebuild: s.Prebuild,
198
}
199
}
200
201