Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/test/pkg/integration/publicapi.go
2498 views
1
// Copyright (c) 2024 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 integration
6
7
import (
8
"context"
9
"encoding/json"
10
"fmt"
11
"net/http"
12
"strings"
13
"time"
14
15
"github.com/bufbuild/connect-go"
16
v1 "github.com/gitpod-io/gitpod/components/public-api/go/v1"
17
v1connect "github.com/gitpod-io/gitpod/components/public-api/go/v1/v1connect"
18
"golang.org/x/xerrors"
19
corev1 "k8s.io/api/core/v1"
20
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21
)
22
23
type PAPIClient struct {
24
Configuration v1connect.ConfigurationServiceClient
25
Prebuild v1connect.PrebuildServiceClient
26
Organization v1connect.OrganizationServiceClient
27
}
28
29
// GitpodServer provides access to the Gitpod server API
30
func (c *ComponentAPI) PublicApi(opts ...GitpodServerOpt) (*PAPIClient, error) {
31
var options gitpodServerOpts
32
for _, o := range opts {
33
err := o(&options)
34
if err != nil {
35
return nil, xerrors.Errorf("cannot access Gitpod public API: %q", err)
36
}
37
}
38
39
if cl, ok := c.serverStatus.PAPIClient[options.User]; ok {
40
return cl, nil
41
}
42
43
var res *PAPIClient
44
err := func() error {
45
tkn := c.serverStatus.Token[options.User]
46
if tkn == "" {
47
var err error
48
tkn, err = c.createGitpodToken(options.User, []string{
49
"resource:default",
50
"function:*",
51
})
52
if err != nil {
53
return err
54
}
55
func() {
56
c.serverStatusMu.Lock()
57
defer c.serverStatusMu.Unlock()
58
c.serverStatus.Token[options.User] = tkn
59
}()
60
}
61
62
var pods corev1.PodList
63
err := c.client.Resources(c.namespace).List(context.Background(), &pods, func(opts *metav1.ListOptions) {
64
opts.LabelSelector = "component=server"
65
})
66
if err != nil {
67
return err
68
}
69
70
config, err := GetServerConfig(c.namespace, c.client)
71
if err != nil {
72
return err
73
}
74
75
hostURL := config.HostURL
76
if hostURL == "" {
77
return xerrors.Errorf("server config: empty HostURL")
78
}
79
80
hostURL = strings.ReplaceAll(hostURL, "http://", "")
81
hostURL = strings.ReplaceAll(hostURL, "https://", "")
82
83
httpClient, connOpts, endpoint := getPAPIConnSettings(hostURL, tkn, false)
84
85
Configuration := v1connect.NewConfigurationServiceClient(httpClient, endpoint, connOpts...)
86
Prebuild := v1connect.NewPrebuildServiceClient(httpClient, endpoint, connOpts...)
87
Organization := v1connect.NewOrganizationServiceClient(httpClient, endpoint, connOpts...)
88
89
cl := &PAPIClient{
90
Configuration: Configuration,
91
Prebuild: Prebuild,
92
Organization: Organization,
93
}
94
95
func() {
96
c.serverStatusMu.Lock()
97
defer c.serverStatusMu.Unlock()
98
c.serverStatus.PAPIClient[options.User] = cl
99
}()
100
101
res = cl
102
103
return nil
104
}()
105
if err != nil {
106
return nil, xerrors.Errorf("cannot access Gitpod public API: %q", err)
107
}
108
109
return res, nil
110
}
111
112
func getPAPIConnSettings(gitpodHost, token string, useCookie bool) (*http.Client, []connect.ClientOption, string) {
113
httpClient := &http.Client{
114
Transport: &authenticatedTransport{Token: token, T: http.DefaultTransport, UseCookie: useCookie},
115
}
116
connOpts := []connect.ClientOption{
117
connect.WithInterceptors(
118
connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
119
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
120
if req.Spec().IsClient {
121
if useCookie {
122
req.Header().Set("Cookie", token)
123
} else {
124
req.Header().Set("Authorization", fmt.Sprintf("Bearer %s", token))
125
}
126
}
127
return next(ctx, req)
128
}
129
}),
130
),
131
}
132
papiEndpoint := fmt.Sprintf("https://%s/public-api", gitpodHost)
133
return httpClient, connOpts, papiEndpoint
134
}
135
136
type authenticatedTransport struct {
137
T http.RoundTripper
138
UseCookie bool
139
Token string
140
}
141
142
func (t *authenticatedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
143
if t.UseCookie {
144
req.Header.Add("Cookie", t.Token)
145
} else {
146
req.Header.Add("Authorization", "Bearer "+t.Token)
147
}
148
return t.T.RoundTrip(req)
149
}
150
151
func (c *ComponentAPI) GetTeam(ctx context.Context, papi *PAPIClient) (string, error) {
152
resp, err := papi.Organization.ListOrganizations(ctx, &connect.Request[v1.ListOrganizationsRequest]{})
153
if err != nil {
154
return "", err
155
}
156
if len(resp.Msg.GetOrganizations()) > 0 {
157
return resp.Msg.GetOrganizations()[0].Id, nil
158
}
159
resp2, err := papi.Organization.CreateOrganization(ctx, &connect.Request[v1.CreateOrganizationRequest]{
160
Msg: &v1.CreateOrganizationRequest{
161
Name: "integration-test",
162
},
163
})
164
if err != nil {
165
return "", err
166
}
167
return resp2.Msg.Organization.Id, nil
168
}
169
170
func (c *ComponentAPI) GetProject(ctx context.Context, papi *PAPIClient, teamID, repoName, repoUrl string, prebuildEnabled bool) (string, error) {
171
resp, err := papi.Configuration.ListConfigurations(ctx, &connect.Request[v1.ListConfigurationsRequest]{
172
Msg: &v1.ListConfigurationsRequest{
173
OrganizationId: teamID,
174
SearchTerm: repoName,
175
},
176
})
177
if err != nil {
178
return "", err
179
}
180
projectID := ""
181
for _, cfg := range resp.Msg.Configurations {
182
if cfg.CloneUrl == repoUrl {
183
projectID = cfg.Id
184
}
185
}
186
if projectID == "" {
187
resp, err := papi.Configuration.CreateConfiguration(ctx, &connect.Request[v1.CreateConfigurationRequest]{
188
Msg: &v1.CreateConfigurationRequest{
189
OrganizationId: teamID,
190
Name: repoName,
191
CloneUrl: repoUrl,
192
},
193
})
194
if err != nil {
195
return "", err
196
}
197
projectID = resp.Msg.Configuration.Id
198
}
199
200
if prebuildEnabled {
201
db, err := c.DB()
202
if err != nil {
203
return "", err
204
}
205
prebuildSettings := map[string]interface{}{
206
"prebuilds": map[string]interface{}{
207
"enable": true,
208
"prebuildInterval": 20,
209
},
210
}
211
prebuildSettingsBytes, err := json.Marshal(prebuildSettings)
212
if err != nil {
213
return "", err
214
}
215
216
_, err = db.ExecContext(ctx, `UPDATE d_b_project SET settings=? WHERE id=?`, string(prebuildSettingsBytes), projectID)
217
if err != nil {
218
return "", err
219
}
220
}
221
222
return projectID, nil
223
}
224
225
func (c *ComponentAPI) TriggerPrebuild(ctx context.Context, papi *PAPIClient, projectID, branchName string) (string, error) {
226
resp, err := papi.Prebuild.StartPrebuild(ctx, &connect.Request[v1.StartPrebuildRequest]{
227
Msg: &v1.StartPrebuildRequest{
228
ConfigurationId: projectID,
229
GitRef: branchName,
230
},
231
})
232
if err != nil {
233
return "", err
234
}
235
return resp.Msg.PrebuildId, nil
236
}
237
238
func (c *ComponentAPI) WaitForPrebuild(ctx context.Context, papi *PAPIClient, prebuildID string) (bool, error) {
239
resp, err := papi.Prebuild.WatchPrebuild(ctx, &connect.Request[v1.WatchPrebuildRequest]{
240
Msg: &v1.WatchPrebuildRequest{
241
Scope: &v1.WatchPrebuildRequest_PrebuildId{
242
PrebuildId: prebuildID,
243
},
244
},
245
})
246
if err != nil {
247
return false, fmt.Errorf("watch prebuild failed: %w", err)
248
}
249
// Note: it's not able to close the stream here
250
// defer resp.Close()
251
for ctx.Err() == nil {
252
if ok := resp.Receive(); !ok {
253
return false, fmt.Errorf("watch prebuild failed: %w", resp.Err())
254
}
255
phase := resp.Msg().GetPrebuild().GetStatus().GetPhase().Name
256
if phase == v1.PrebuildPhase_PHASE_BUILDING || phase == v1.PrebuildPhase_PHASE_QUEUED {
257
continue
258
}
259
if phase == v1.PrebuildPhase_PHASE_AVAILABLE {
260
return true, nil
261
}
262
return false, fmt.Errorf("prebuild failed: %s", phase.String())
263
}
264
return false, ctx.Err()
265
}
266
267
func (c *ComponentAPI) WaitForPrebuildWorkspaceToStoppedPhase(ctx context.Context, prebuildID string) error {
268
db, err := c.DB()
269
if err != nil {
270
return err
271
}
272
var phase string
273
for ctx.Err() == nil {
274
if err := db.QueryRowContext(ctx, `SELECT phase FROM d_b_workspace_instance WHERE workspaceId=(SELECT buildWorkspaceId FROM d_b_prebuilt_workspace WHERE id=?) LIMIT 1`, prebuildID).Scan(&phase); err != nil {
275
return err
276
}
277
if phase == "stopped" {
278
return nil
279
}
280
time.Sleep(5 * time.Second)
281
}
282
return ctx.Err()
283
}
284
285