Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/service/public-api.ts
2500 views
1
/**
2
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
3
* Licensed under the GNU Affero General Public License (AGPL).
4
* See License.AGPL.txt in the project root for license information.
5
*/
6
7
import { PartialMessage } from "@bufbuild/protobuf";
8
import { MethodKind, ServiceType } from "@bufbuild/protobuf";
9
import { CallOptions, Code, ConnectError, PromiseClient, createPromiseClient } from "@connectrpc/connect";
10
import { createConnectTransport } from "@connectrpc/connect-web";
11
import { Disposable } from "@gitpod/gitpod-protocol";
12
import { PublicAPIConverter } from "@gitpod/public-api-common/lib/public-api-converter";
13
import { HelloService } from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_connect";
14
import { OIDCService } from "@gitpod/public-api/lib/gitpod/experimental/v1/oidc_connect";
15
import { TokensService } from "@gitpod/public-api/lib/gitpod/experimental/v1/tokens_connect";
16
import { OrganizationService } from "@gitpod/public-api/lib/gitpod/v1/organization_connect";
17
import { WorkspaceService } from "@gitpod/public-api/lib/gitpod/v1/workspace_connect";
18
import { ConfigurationService } from "@gitpod/public-api/lib/gitpod/v1/configuration_connect";
19
import { PrebuildService } from "@gitpod/public-api/lib/gitpod/v1/prebuild_connect";
20
import { getMetricsInterceptor } from "@gitpod/gitpod-protocol/lib/metrics";
21
import { getExperimentsClient } from "../experiments/client";
22
import { JsonRpcOrganizationClient } from "./json-rpc-organization-client";
23
import { JsonRpcWorkspaceClient } from "./json-rpc-workspace-client";
24
import { JsonRpcAuthProviderClient } from "./json-rpc-authprovider-client";
25
import { AuthProviderService } from "@gitpod/public-api/lib/gitpod/v1/authprovider_connect";
26
import { EnvironmentVariableService } from "@gitpod/public-api/lib/gitpod/v1/envvar_connect";
27
import { JsonRpcEnvvarClient } from "./json-rpc-envvar-client";
28
import { Prebuild, WatchPrebuildRequest, WatchPrebuildResponse } from "@gitpod/public-api/lib/gitpod/v1/prebuild_pb";
29
import { JsonRpcPrebuildClient } from "./json-rpc-prebuild-client";
30
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
31
import { JsonRpcScmClient } from "./json-rpc-scm-client";
32
import { SCMService } from "@gitpod/public-api/lib/gitpod/v1/scm_connect";
33
import { SSHService } from "@gitpod/public-api/lib/gitpod/v1/ssh_connect";
34
import { UserService } from "@gitpod/public-api/lib/gitpod/v1/user_connect";
35
import { JsonRpcSSHClient } from "./json-rpc-ssh-client";
36
import { JsonRpcVerificationClient } from "./json-rpc-verification-client";
37
import { VerificationService } from "@gitpod/public-api/lib/gitpod/v1/verification_connect";
38
import { JsonRpcInstallationClient } from "./json-rpc-installation-client";
39
import { InstallationService } from "@gitpod/public-api/lib/gitpod/v1/installation_connect";
40
import { JsonRpcUserClient } from "./json-rpc-user-client";
41
import { Timeout } from "@gitpod/gitpod-protocol/lib/util/timeout";
42
43
const transport = createConnectTransport({
44
baseUrl: `${window.location.protocol}//${window.location.host}/public-api`,
45
interceptors: [getMetricsInterceptor()],
46
});
47
48
export const converter = new PublicAPIConverter();
49
50
export const helloService = createServiceClient(HelloService);
51
export const personalAccessTokensService = createPromiseClient(TokensService, transport);
52
53
export const oidcService = createPromiseClient(OIDCService, transport);
54
55
export const workspaceClient = createServiceClient(WorkspaceService, {
56
client: new JsonRpcWorkspaceClient(),
57
featureFlagSuffix: "workspace",
58
});
59
export const organizationClient = createServiceClient(OrganizationService, {
60
client: new JsonRpcOrganizationClient(),
61
featureFlagSuffix: "organization",
62
});
63
64
// No jsonrpc client for the configuration service as it's only used in new UI of the dashboard
65
export const configurationClient = createServiceClient(ConfigurationService);
66
export const prebuildClient = createServiceClient(PrebuildService, {
67
client: new JsonRpcPrebuildClient(),
68
featureFlagSuffix: "prebuild",
69
});
70
71
export const authProviderClient = createServiceClient(AuthProviderService, {
72
client: new JsonRpcAuthProviderClient(),
73
featureFlagSuffix: "authprovider",
74
});
75
76
export const scmClient = createServiceClient(SCMService, {
77
client: new JsonRpcScmClient(),
78
featureFlagSuffix: "scm",
79
});
80
81
export const envVarClient = createServiceClient(EnvironmentVariableService, {
82
client: new JsonRpcEnvvarClient(),
83
featureFlagSuffix: "envvar",
84
});
85
86
export const userClient = createServiceClient(UserService, {
87
client: new JsonRpcUserClient(),
88
featureFlagSuffix: "user",
89
});
90
91
export const sshClient = createServiceClient(SSHService, {
92
client: new JsonRpcSSHClient(),
93
featureFlagSuffix: "ssh",
94
});
95
96
export const verificationClient = createServiceClient(VerificationService, {
97
client: new JsonRpcVerificationClient(),
98
featureFlagSuffix: "verification",
99
});
100
101
export const installationClient = createServiceClient(InstallationService, {
102
client: new JsonRpcInstallationClient(),
103
featureFlagSuffix: "installation",
104
});
105
106
let user: { id: string; email?: string } | undefined;
107
export function updateUserForExperiments(newUser?: { id: string; email?: string }) {
108
user = newUser;
109
}
110
111
function createServiceClient<T extends ServiceType>(
112
type: T,
113
jsonRpcOptions?: {
114
client: PromiseClient<T>;
115
featureFlagSuffix: string;
116
},
117
): PromiseClient<T> {
118
return new Proxy(createPromiseClient(type, transport), {
119
get(grpcClient, prop) {
120
const experimentsClient = getExperimentsClient();
121
// TODO(ak) remove after migration
122
async function resolveClient(preferJsonRpc?: boolean): Promise<PromiseClient<T>> {
123
if (!jsonRpcOptions) {
124
return grpcClient;
125
}
126
if (preferJsonRpc) {
127
return jsonRpcOptions.client;
128
}
129
const featureFlags = [`dashboard_public_api_${jsonRpcOptions.featureFlagSuffix}_enabled`];
130
const resolvedFlags = await Promise.all(
131
featureFlags.map((ff) =>
132
experimentsClient.getValueAsync(ff, false, {
133
user,
134
gitpodHost: window.location.host,
135
}),
136
),
137
);
138
if (resolvedFlags.every((f) => f === true)) {
139
return grpcClient;
140
}
141
return jsonRpcOptions.client;
142
}
143
return (...args: any[]) => {
144
const requestContext = {
145
requestMethod: `${type.typeName}/${prop as string}`,
146
};
147
const callOptions: CallOptions = { ...args[1] };
148
const originalOnHeader = callOptions.onHeader;
149
callOptions.onHeader = (headers) => {
150
if (originalOnHeader) {
151
originalOnHeader(headers);
152
}
153
const requestId = headers.get("x-request-id") || undefined;
154
if (requestId) {
155
Object.assign(requestContext, { requestId });
156
}
157
};
158
args = [args[0], callOptions];
159
160
function handleError(e: any): unknown {
161
if (e instanceof ConnectError) {
162
e = converter.fromError(e);
163
}
164
165
Object.assign(e, { requestContext });
166
throw e;
167
}
168
169
const method = type.methods[prop as string];
170
if (!method) {
171
handleError(new ConnectError("unimplemented", Code.Unimplemented));
172
}
173
174
// TODO(ak) default timeouts
175
// TODO(ak) retry on unavailable?
176
177
if (method.kind === MethodKind.Unary || method.kind === MethodKind.ClientStreaming) {
178
return (async () => {
179
try {
180
const client = await resolveClient();
181
const result = await Reflect.apply(client[prop as any], client, args);
182
return result;
183
} catch (e) {
184
handleError(e);
185
}
186
})();
187
}
188
return (async function* () {
189
try {
190
// for server streaming, we prefer jsonRPC
191
const client = await resolveClient(true);
192
const generator = Reflect.apply(client[prop as any], client, args) as AsyncGenerator<any>;
193
for await (const item of generator) {
194
yield item;
195
}
196
} catch (e) {
197
handleError(e);
198
}
199
})();
200
};
201
},
202
});
203
}
204
205
export function watchPrebuild(
206
request: PartialMessage<WatchPrebuildRequest>,
207
cb: (prebuild: Prebuild) => void,
208
): Disposable {
209
return stream<WatchPrebuildResponse>(
210
(options) => prebuildClient.watchPrebuild(request, options),
211
(response) => cb(response.prebuild!),
212
);
213
}
214
215
export function stream<Response>(
216
factory: (options: CallOptions) => AsyncIterable<Response>,
217
cb: (response: Response) => void,
218
): Disposable {
219
const MAX_BACKOFF = 60000;
220
const BASE_BACKOFF = 3000;
221
let backoff = BASE_BACKOFF;
222
const abort = new AbortController();
223
(async () => {
224
// Only timeout after 10 seconds with no data in some environments
225
const experiments = getExperimentsClient();
226
const enableTimeout = await experiments.getValueAsync("supervisor_check_ready_retry", false, {});
227
228
while (!abort.signal.aborted) {
229
const connectionTimeout = new Timeout(10_000, () => enableTimeout);
230
try {
231
connectionTimeout.start();
232
connectionTimeout.signal?.addEventListener("abort", () => {
233
console.error("Connection timed out after no response for 10s");
234
});
235
236
for await (const response of factory({
237
signal: AbortSignal.any([abort.signal, connectionTimeout.signal!]),
238
// GCP timeout is 10 minutes, we timeout 3 mins earlier
239
// to avoid unknown network errors and reconnect gracefully
240
timeoutMs: 7 * 60 * 1000,
241
})) {
242
connectionTimeout.clear(); // connection is alive now, clear timeout
243
244
backoff = BASE_BACKOFF;
245
cb(response);
246
}
247
} catch (e) {
248
if (abort.signal.aborted) {
249
// client aborted, don't reconnect, early exit
250
return;
251
}
252
if (
253
ApplicationError.hasErrorCode(e) &&
254
(e.code === ErrorCodes.DEADLINE_EXCEEDED ||
255
// library aborted: https://github.com/connectrpc/connect-es/issues/954
256
// (clean up when fixed, on server abort we should rather backoff with jitter)
257
e.code === ErrorCodes.CANCELLED)
258
) {
259
// timeout is expected, reconnect with base backoff
260
backoff = BASE_BACKOFF;
261
} else {
262
backoff = Math.min(2 * backoff, MAX_BACKOFF);
263
console.error(e);
264
}
265
} finally {
266
connectionTimeout.clear();
267
}
268
const jitter = Math.random() * 0.3 * backoff;
269
const delay = backoff + jitter;
270
await new Promise((resolve) => setTimeout(resolve, delay));
271
}
272
})();
273
274
return Disposable.create(() => abort.abort());
275
}
276
277