@gitpod/gitpod-protocol 0.1.5-release-2022.6.1.0 → 0.1.5-release-2022.9.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/data/gitpod-schema.json +63 -129
  2. package/lib/accounting-protocol.d.ts +1 -0
  3. package/lib/accounting-protocol.d.ts.map +1 -1
  4. package/lib/accounting-protocol.js +4 -0
  5. package/lib/accounting-protocol.js.map +1 -1
  6. package/lib/admin-protocol.d.ts +7 -0
  7. package/lib/admin-protocol.d.ts.map +1 -1
  8. package/lib/admin-protocol.js.map +1 -1
  9. package/lib/attribution.d.ts +20 -0
  10. package/lib/attribution.d.ts.map +1 -0
  11. package/lib/attribution.js +40 -0
  12. package/lib/attribution.js.map +1 -0
  13. package/lib/billing-mode.d.ts +41 -0
  14. package/lib/billing-mode.d.ts.map +1 -0
  15. package/lib/billing-mode.js +44 -0
  16. package/lib/billing-mode.js.map +1 -0
  17. package/lib/blocked-repositories-protocol.d.ts +13 -0
  18. package/lib/blocked-repositories-protocol.d.ts.map +1 -0
  19. package/lib/blocked-repositories-protocol.js +8 -0
  20. package/lib/blocked-repositories-protocol.js.map +1 -0
  21. package/lib/encryption/encryption-engine.d.ts.map +1 -1
  22. package/lib/encryption/encryption-engine.js +3 -3
  23. package/lib/encryption/encryption-engine.js.map +1 -1
  24. package/lib/encryption/encryption-engine.spec.js +1 -1
  25. package/lib/encryption/encryption-engine.spec.js.map +1 -1
  26. package/lib/encryption/key-provider.js +1 -1
  27. package/lib/encryption/key-provider.js.map +1 -1
  28. package/lib/experiments/configcat-server.d.ts +2 -0
  29. package/lib/experiments/configcat-server.d.ts.map +1 -1
  30. package/lib/experiments/configcat-server.js +3 -1
  31. package/lib/experiments/configcat-server.js.map +1 -1
  32. package/lib/experiments/configcat.d.ts +4 -2
  33. package/lib/experiments/configcat.d.ts.map +1 -1
  34. package/lib/experiments/configcat.js +13 -3
  35. package/lib/experiments/configcat.js.map +1 -1
  36. package/lib/experiments/types.d.ts +8 -3
  37. package/lib/experiments/types.d.ts.map +1 -1
  38. package/lib/experiments/types.js +2 -0
  39. package/lib/experiments/types.js.map +1 -1
  40. package/lib/gitpod-service.d.ts +32 -15
  41. package/lib/gitpod-service.d.ts.map +1 -1
  42. package/lib/gitpod-service.js +13 -1
  43. package/lib/gitpod-service.js.map +1 -1
  44. package/lib/ide-protocol.d.ts +8 -0
  45. package/lib/ide-protocol.d.ts.map +1 -1
  46. package/lib/index.d.ts +1 -0
  47. package/lib/index.d.ts.map +1 -1
  48. package/lib/index.js +1 -0
  49. package/lib/index.js.map +1 -1
  50. package/lib/messaging/client-call-metrics.d.ts +1 -17
  51. package/lib/messaging/client-call-metrics.d.ts.map +1 -1
  52. package/lib/messaging/client-call-metrics.js +1 -2
  53. package/lib/messaging/client-call-metrics.js.map +1 -1
  54. package/lib/messaging/error.d.ts +5 -1
  55. package/lib/messaging/error.d.ts.map +1 -1
  56. package/lib/messaging/error.js +11 -3
  57. package/lib/messaging/error.js.map +1 -1
  58. package/lib/plans.d.ts +1 -0
  59. package/lib/plans.d.ts.map +1 -1
  60. package/lib/plans.js +4 -0
  61. package/lib/plans.js.map +1 -1
  62. package/lib/protocol.d.ts +88 -4
  63. package/lib/protocol.d.ts.map +1 -1
  64. package/lib/protocol.js +116 -3
  65. package/lib/protocol.js.map +1 -1
  66. package/lib/protocol.spec.d.ts +7 -0
  67. package/lib/protocol.spec.d.ts.map +1 -0
  68. package/lib/protocol.spec.js +127 -0
  69. package/lib/protocol.spec.js.map +1 -0
  70. package/lib/team-subscription-protocol.d.ts +1 -0
  71. package/lib/team-subscription-protocol.d.ts.map +1 -1
  72. package/lib/team-subscription-protocol.js +4 -0
  73. package/lib/team-subscription-protocol.js.map +1 -1
  74. package/lib/teams-projects-protocol.d.ts +13 -1
  75. package/lib/teams-projects-protocol.d.ts.map +1 -1
  76. package/lib/teams-projects-protocol.js.map +1 -1
  77. package/lib/usage.d.ts +61 -0
  78. package/lib/usage.d.ts.map +1 -0
  79. package/lib/usage.js +14 -0
  80. package/lib/usage.js.map +1 -0
  81. package/lib/util/garbage-collected-cache.d.ts.map +1 -1
  82. package/lib/util/garbage-collected-cache.js +5 -0
  83. package/lib/util/garbage-collected-cache.js.map +1 -1
  84. package/lib/util/gitpod-host-url.d.ts +3 -0
  85. package/lib/util/gitpod-host-url.d.ts.map +1 -1
  86. package/lib/util/gitpod-host-url.js +18 -0
  87. package/lib/util/gitpod-host-url.js.map +1 -1
  88. package/lib/util/grpc.d.ts +19 -0
  89. package/lib/util/grpc.d.ts.map +1 -1
  90. package/lib/util/grpc.js +73 -1
  91. package/lib/util/grpc.js.map +1 -1
  92. package/lib/util/logging.d.ts.map +1 -1
  93. package/lib/util/logging.js +8 -1
  94. package/lib/util/logging.js.map +1 -1
  95. package/lib/util/logging.spec.d.ts +7 -0
  96. package/lib/util/logging.spec.d.ts.map +1 -0
  97. package/lib/util/logging.spec.js +52 -0
  98. package/lib/util/logging.spec.js.map +1 -0
  99. package/lib/util/timeutil.d.ts +1 -0
  100. package/lib/util/timeutil.d.ts.map +1 -1
  101. package/lib/util/timeutil.js +7 -1
  102. package/lib/util/timeutil.js.map +1 -1
  103. package/lib/util/timeutil.spec.d.ts +2 -0
  104. package/lib/util/timeutil.spec.d.ts.map +1 -1
  105. package/lib/util/timeutil.spec.js +34 -0
  106. package/lib/util/timeutil.spec.js.map +1 -1
  107. package/lib/webhook-event.d.ts +44 -0
  108. package/lib/webhook-event.d.ts.map +1 -0
  109. package/lib/webhook-event.js +8 -0
  110. package/lib/webhook-event.js.map +1 -0
  111. package/lib/workspace-class.d.ts +14 -0
  112. package/lib/workspace-class.d.ts.map +1 -0
  113. package/lib/workspace-class.js +8 -0
  114. package/lib/workspace-class.js.map +1 -0
  115. package/lib/workspace-cluster.d.ts +5 -7
  116. package/lib/workspace-cluster.d.ts.map +1 -1
  117. package/lib/workspace-cluster.js.map +1 -1
  118. package/lib/workspace-instance.d.ts +2 -0
  119. package/lib/workspace-instance.d.ts.map +1 -1
  120. package/lib/wsready.d.ts +3 -3
  121. package/lib/wsready.d.ts.map +1 -1
  122. package/lib/wsready.js +0 -2
  123. package/lib/wsready.js.map +1 -1
  124. package/package.json +5 -2
  125. package/pkg-yarn.lock +3 -2
  126. package/provenance-bundle.jsonl +1 -1
  127. package/src/accounting-protocol.ts +3 -0
  128. package/src/admin-protocol.ts +10 -0
  129. package/src/attribution.ts +48 -0
  130. package/src/billing-mode.ts +77 -0
  131. package/src/blocked-repositories-protocol.ts +13 -0
  132. package/src/encryption/encryption-engine.spec.ts +1 -1
  133. package/src/encryption/encryption-engine.ts +7 -3
  134. package/src/encryption/key-provider.ts +1 -1
  135. package/src/experiments/configcat-server.ts +4 -0
  136. package/src/experiments/configcat.ts +14 -5
  137. package/src/experiments/types.ts +13 -7
  138. package/src/gitpod-service.ts +54 -14
  139. package/src/ide-protocol.ts +10 -0
  140. package/src/index.ts +1 -0
  141. package/src/messaging/client-call-metrics.ts +1 -20
  142. package/src/messaging/error.ts +16 -4
  143. package/src/plans.ts +4 -0
  144. package/src/protocol.spec.ts +97 -0
  145. package/src/protocol.ts +192 -6
  146. package/src/team-subscription-protocol.ts +3 -0
  147. package/src/teams-projects-protocol.ts +14 -1
  148. package/src/usage.ts +71 -0
  149. package/src/util/garbage-collected-cache.ts +5 -0
  150. package/src/util/gitpod-host-url.ts +21 -0
  151. package/src/util/grpc.ts +89 -0
  152. package/src/util/logging.spec.ts +23 -0
  153. package/src/util/logging.ts +11 -2
  154. package/src/util/timeutil.spec.ts +28 -1
  155. package/src/util/timeutil.ts +6 -0
  156. package/src/webhook-event.ts +55 -0
  157. package/src/workspace-class.ts +14 -0
  158. package/src/workspace-cluster.ts +2 -4
  159. package/src/workspace-instance.ts +8 -0
  160. package/src/wsready.ts +5 -4
package/src/protocol.ts CHANGED
@@ -8,6 +8,7 @@ import { WorkspaceInstance, PortVisibility } from "./workspace-instance";
8
8
  import { RoleOrPermission } from "./permission";
9
9
  import { Project } from "./teams-projects-protocol";
10
10
  import { createHash } from "crypto";
11
+ import { AttributionId } from "./attribution";
11
12
 
12
13
  export interface UserInfo {
13
14
  name?: string;
@@ -45,6 +46,15 @@ export interface User {
45
46
  markedDeleted?: boolean;
46
47
 
47
48
  additionalData?: AdditionalUserData;
49
+
50
+ // Identifies an explicit team or user ID to which all the user's workspace usage should be attributed to (e.g. for billing purposes)
51
+ usageAttributionId?: string;
52
+
53
+ // The last time this user got verified somehow. The user is not verified if this is empty.
54
+ lastVerificationTime?: string;
55
+
56
+ // The phone number used for the last phone verification.
57
+ verificationPhoneNumber?: string;
48
58
  }
49
59
 
50
60
  export namespace User {
@@ -70,11 +80,14 @@ export namespace User {
70
80
  }
71
81
 
72
82
  /**
73
- * Tries to return the primaryEmail of the first identity this user signed up with.
83
+ * Returns the stored email or if it doesn't exist returns the primaryEmail of the first identity this user signed up with.
74
84
  * @param user
75
85
  * @returns A primaryEmail, or undefined if there is none.
76
86
  */
77
87
  export function getPrimaryEmail(user: User): string | undefined {
88
+ if (user.additionalData?.profile?.emailAddress) {
89
+ return user.additionalData?.profile?.emailAddress;
90
+ }
78
91
  const identities = user.identities.filter((i) => !!i.primaryEmail);
79
92
  if (identities.length <= 0) {
80
93
  return undefined;
@@ -136,6 +149,50 @@ export namespace User {
136
149
  }
137
150
  user.additionalData.ideSettings = newIDESettings;
138
151
  }
152
+
153
+ export function getProfile(user: User): Profile {
154
+ return {
155
+ name: User.getName(user!) || "",
156
+ email: User.getPrimaryEmail(user!) || "",
157
+ company: user?.additionalData?.profile?.companyName,
158
+ avatarURL: user?.avatarUrl,
159
+ };
160
+ }
161
+
162
+ export function setProfile(user: User, profile: Profile): User {
163
+ user.fullName = profile.name;
164
+ user.avatarUrl = profile.avatarURL;
165
+
166
+ if (!user.additionalData) {
167
+ user.additionalData = {};
168
+ }
169
+ if (!user.additionalData.profile) {
170
+ user.additionalData.profile = {};
171
+ }
172
+ user.additionalData.profile.emailAddress = profile.email;
173
+ user.additionalData.profile.companyName = profile.company;
174
+ user.additionalData.profile.lastUpdatedDetailsNudge = new Date().toISOString();
175
+
176
+ return user;
177
+ }
178
+
179
+ // The actual Profile of a User
180
+ export interface Profile {
181
+ name: string;
182
+ email: string;
183
+ company?: string;
184
+ avatarURL?: string;
185
+ }
186
+ export namespace Profile {
187
+ export function hasChanges(before: Profile, after: Profile) {
188
+ return (
189
+ before.name !== after.name ||
190
+ before.email !== after.email ||
191
+ before.company !== after.company ||
192
+ before.avatarURL !== after.avatarURL
193
+ );
194
+ }
195
+ }
139
196
  }
140
197
 
141
198
  export interface AdditionalUserData {
@@ -150,9 +207,36 @@ export interface AdditionalUserData {
150
207
  oauthClientsApproved?: { [key: string]: string };
151
208
  // to remember GH Orgs the user installed/updated the GH App for
152
209
  knownGitHubOrgs?: string[];
153
-
154
210
  // Git clone URL pointing to the user's dotfile repo
155
211
  dotfileRepo?: string;
212
+ // preferred workspace classes
213
+ workspaceClasses?: WorkspaceClasses;
214
+ // additional user profile data
215
+ profile?: ProfileDetails;
216
+ }
217
+ export namespace AdditionalUserData {
218
+ export function set(user: User, partialData: Partial<AdditionalUserData>): User {
219
+ if (!user.additionalData) {
220
+ user.additionalData = {
221
+ ...partialData,
222
+ };
223
+ } else {
224
+ user.additionalData = {
225
+ ...user.additionalData,
226
+ ...partialData,
227
+ };
228
+ }
229
+ return user;
230
+ }
231
+ }
232
+ // The format in which we store User Profiles in
233
+ export interface ProfileDetails {
234
+ // when was the last time the user updated their profile information or has been nudged to do so.
235
+ lastUpdatedDetailsNudge?: string;
236
+ // the user's company name
237
+ companyName?: string;
238
+ // the user's email
239
+ emailAddress?: string;
156
240
  }
157
241
 
158
242
  export interface EmailNotificationSettings {
@@ -171,6 +255,11 @@ export type IDESettings = {
171
255
  useLatestVersion?: boolean;
172
256
  };
173
257
 
258
+ export interface WorkspaceClasses {
259
+ regular: string;
260
+ prebuild: string;
261
+ }
262
+
174
263
  export interface UserPlatform {
175
264
  uid: string;
176
265
  userAgent: string;
@@ -197,16 +286,29 @@ export interface UserFeatureSettings {
197
286
  permanentWSFeatureFlags?: NamedWorkspaceFeatureFlag[];
198
287
  }
199
288
 
289
+ export type BillingTier = "paid" | "free";
290
+
200
291
  /**
201
292
  * The values of this type MUST MATCH enum values in WorkspaceFeatureFlag from ws-manager/client/core_pb.d.ts
202
293
  * If they don't we'll break things during workspace startup.
203
294
  */
204
295
  export const WorkspaceFeatureFlags = {
205
296
  full_workspace_backup: undefined,
206
- fixed_resources: undefined,
207
297
  persistent_volume_claim: undefined,
298
+ protected_secrets: undefined,
299
+ workspace_class_limiting: undefined,
300
+ workspace_connection_limiting: undefined,
208
301
  };
209
302
  export type NamedWorkspaceFeatureFlag = keyof typeof WorkspaceFeatureFlags;
303
+ export namespace NamedWorkspaceFeatureFlag {
304
+ export const WORKSPACE_PERSISTED_FEATTURE_FLAGS: NamedWorkspaceFeatureFlag[] = [
305
+ "full_workspace_backup",
306
+ "persistent_volume_claim",
307
+ ];
308
+ export function isWorkspacePersisted(ff: NamedWorkspaceFeatureFlag): boolean {
309
+ return WORKSPACE_PERSISTED_FEATTURE_FLAGS.includes(ff);
310
+ }
311
+ }
210
312
 
211
313
  export interface EnvVarWithValue {
212
314
  name: string;
@@ -356,6 +458,68 @@ export namespace UserEnvVar {
356
458
  }
357
459
  }
358
460
 
461
+ export interface SSHPublicKeyValue {
462
+ name: string;
463
+ key: string;
464
+ }
465
+ export interface UserSSHPublicKey extends SSHPublicKeyValue {
466
+ id: string;
467
+ key: string;
468
+ userId: string;
469
+ fingerprint: string;
470
+ creationTime: string;
471
+ lastUsedTime?: string;
472
+ }
473
+
474
+ export type UserSSHPublicKeyValue = Omit<UserSSHPublicKey, "key" | "userId">;
475
+
476
+ export namespace SSHPublicKeyValue {
477
+ export function validate(value: SSHPublicKeyValue): string | undefined {
478
+ if (value.name.length === 0) {
479
+ return "Title must not be empty.";
480
+ }
481
+ if (value.name.length > 255) {
482
+ return "Title too long. Maximum value length is 255 characters.";
483
+ }
484
+ if (value.key.length === 0) {
485
+ return "Key must not be empty.";
486
+ }
487
+ try {
488
+ getData(value);
489
+ } catch (e) {
490
+ return "Key is invalid. You must supply a key in OpenSSH public key format.";
491
+ }
492
+ return;
493
+ }
494
+
495
+ export function getData(value: SSHPublicKeyValue) {
496
+ // Begins with 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519', 'sk-ecdsa-sha2-nistp256@openssh.com', or 'sk-ssh-ed25519@openssh.com'.
497
+ const regex =
498
+ /^(?<type>ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519|sk-ecdsa-sha2-nistp256@openssh\.com|sk-ssh-ed25519@openssh\.com) (?<key>.*?)( (?<email>.*?))?$/;
499
+ const resultGroup = regex.exec(value.key.trim());
500
+ if (!resultGroup) {
501
+ throw new Error("Key is invalid.");
502
+ }
503
+ return {
504
+ type: resultGroup.groups?.["type"] as string,
505
+ key: resultGroup.groups?.["key"] as string,
506
+ email: resultGroup.groups?.["email"] || undefined,
507
+ };
508
+ }
509
+
510
+ export function getFingerprint(value: SSHPublicKeyValue) {
511
+ const data = getData(value);
512
+ let buf = Buffer.from(data.key, "base64");
513
+ // gitlab style
514
+ // const hash = createHash("md5").update(buf).digest("hex");
515
+ // github style
516
+ const hash = createHash("sha256").update(buf).digest("base64");
517
+ return hash;
518
+ }
519
+
520
+ export const MAXIMUM_KEY_LENGTH = 5;
521
+ }
522
+
359
523
  export interface GitpodToken {
360
524
  /** Hash value (SHA256) of the token (primary key). */
361
525
  tokenHash: string;
@@ -564,7 +728,7 @@ export interface Workspace {
564
728
 
565
729
  export type WorkspaceSoftDeletion = "user" | "gc";
566
730
 
567
- export type WorkspaceType = "regular" | "prebuild" | "probe";
731
+ export type WorkspaceType = "regular" | "prebuild" | "probe"; // TODO(gpl) Removed prove here
568
732
 
569
733
  export namespace Workspace {
570
734
  export function getFullRepositoryName(ws: Workspace): string | undefined {
@@ -651,6 +815,12 @@ export interface RepositoryCloneInformation {
651
815
  checkoutLocation?: string;
652
816
  }
653
817
 
818
+ export interface CoreDumpConfig {
819
+ enabled?: boolean;
820
+ softLimit?: number;
821
+ hardLimit?: number;
822
+ }
823
+
654
824
  export interface WorkspaceConfig {
655
825
  mainConfiguration?: string;
656
826
  additionalRepositories?: RepositoryCloneInformation[];
@@ -663,6 +833,7 @@ export interface WorkspaceConfig {
663
833
  github?: GithubAppConfig;
664
834
  vscode?: VSCodeConfig;
665
835
  jetbrains?: JetBrainsConfig;
836
+ coreDump?: CoreDumpConfig;
666
837
 
667
838
  /** deprecated. Enabled by default **/
668
839
  experimentalNetwork?: boolean;
@@ -671,13 +842,12 @@ export interface WorkspaceConfig {
671
842
  * Where the config object originates from.
672
843
  *
673
844
  * repo - from the repository
674
- * project-db - from the "Project" stored in the database
675
845
  * definitly-gp - from github.com/gitpod-io/definitely-gp
676
846
  * derived - computed based on analyzing the repository
677
847
  * additional-content - config comes from additional content, usually provided through the project's configuration
678
848
  * default - our static catch-all default config
679
849
  */
680
- _origin?: "repo" | "project-db" | "definitely-gp" | "derived" | "additional-content" | "default";
850
+ _origin?: "repo" | "definitely-gp" | "derived" | "additional-content" | "default";
681
851
 
682
852
  /**
683
853
  * Set of automatically infered feature flags. That's not something the user can set, but
@@ -1351,3 +1521,19 @@ export interface Terms {
1351
1521
  readonly content: string;
1352
1522
  readonly formElements?: object;
1353
1523
  }
1524
+
1525
+ export interface StripeConfig {
1526
+ usageProductPriceIds: { [currency: string]: string };
1527
+ individualUsagePriceIds?: { [currency: string]: string };
1528
+ teamUsagePriceIds?: { [currency: string]: string };
1529
+ }
1530
+
1531
+ export type BillingStrategy = "other" | "stripe";
1532
+ export interface CostCenter {
1533
+ readonly id: AttributionId;
1534
+ /**
1535
+ * Unit: credits
1536
+ */
1537
+ spendingLimit: number;
1538
+ billingStrategy: BillingStrategy;
1539
+ }
@@ -55,6 +55,9 @@ export namespace TeamSubscription2 {
55
55
  export const isActive = (ts2: TeamSubscription2, date: string): boolean => {
56
56
  return ts2.startDate <= date && (ts2.endDate === undefined || date < ts2.endDate);
57
57
  };
58
+ export function isCancelled(s: TeamSubscription2, date: string): boolean {
59
+ return (!!s.cancellationDate && s.cancellationDate < date) || (!!s.endDate && s.endDate < date); // This edge case is meant to handle bad data: If for whatever reason cancellationDate has not been set: treat endDate as such
60
+ }
58
61
  }
59
62
 
60
63
  /**
@@ -7,6 +7,7 @@
7
7
  import { PrebuiltWorkspaceState } from "./protocol";
8
8
  import { v4 as uuidv4 } from "uuid";
9
9
  import { DeepPartial } from "./util/deep-partial";
10
+ import { WebhookEvent } from "./webhook-event";
10
11
 
11
12
  export interface ProjectConfig {
12
13
  ".gitpod.yml": string;
@@ -15,6 +16,7 @@ export interface ProjectConfig {
15
16
  export interface ProjectSettings {
16
17
  useIncrementalPrebuilds?: boolean;
17
18
  usePersistentVolumeClaim?: boolean;
19
+ keepOutdatedPrebuildsRunning?: boolean;
18
20
  }
19
21
 
20
22
  export interface Project {
@@ -25,7 +27,6 @@ export interface Project {
25
27
  teamId?: string;
26
28
  userId?: string;
27
29
  appInstallationId: string;
28
- config?: ProjectConfig;
29
30
  settings?: ProjectSettings;
30
31
  creationTime: string;
31
32
  /** This is a flag that triggers the HARD DELETION of this entity */
@@ -151,3 +152,15 @@ export interface TeamMembershipInvite {
151
152
  /** This is a flag that triggers the HARD DELETION of this entity */
152
153
  deleted?: boolean;
153
154
  }
155
+
156
+ export interface PrebuildEvent {
157
+ id: string;
158
+ creationTime: string;
159
+ status: WebhookEvent.Status | WebhookEvent.PrebuildStatus;
160
+ message?: string;
161
+ prebuildId?: string;
162
+ projectId?: string;
163
+ cloneUrl?: string;
164
+ branch?: string;
165
+ commit?: string;
166
+ }
package/src/usage.ts ADDED
@@ -0,0 +1,71 @@
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 { WorkspaceType } from "./protocol";
8
+
9
+ // types below are copied over from components/usage-api/typescript/src/usage/v1/usage_pb.d.ts
10
+ export interface ListUsageRequest {
11
+ attributionId: string;
12
+ from?: number;
13
+ to?: number;
14
+ order: Ordering;
15
+ pagination?: PaginationRequest;
16
+ }
17
+
18
+ export enum Ordering {
19
+ ORDERING_DESCENDING = 0,
20
+ ORDERING_ASCENDING = 1,
21
+ }
22
+
23
+ export interface PaginationRequest {
24
+ perPage: number;
25
+ page: number;
26
+ }
27
+
28
+ export interface ListUsageResponse {
29
+ usageEntriesList: Usage[];
30
+ pagination?: PaginationResponse;
31
+ creditsUsed: number;
32
+ }
33
+
34
+ export interface PaginationResponse {
35
+ perPage: number;
36
+ totalPages: number;
37
+ total: number;
38
+ page: number;
39
+ }
40
+
41
+ export type UsageKind = "workspaceinstance" | "invoice";
42
+ export interface Usage {
43
+ id: string;
44
+ attributionId: string;
45
+ description: string;
46
+ credits: number;
47
+ effectiveTime?: number;
48
+ kind: UsageKind;
49
+ workspaceInstanceId: string;
50
+ draft: boolean;
51
+ metadata: WorkspaceInstanceUsageData | InvoiceUsageData;
52
+ }
53
+
54
+ // the equivalent golang shape is maintained in `/workspace/gitpod/`components/usage/pkg/db/usage.go`
55
+ export interface WorkspaceInstanceUsageData {
56
+ workspaceId: string;
57
+ workspaceType: WorkspaceType;
58
+ workspaceClass: string;
59
+ contextURL: string;
60
+ startTime: string;
61
+ endTime?: string;
62
+ userId: string;
63
+ userName: string;
64
+ userAvatarURL: string;
65
+ }
66
+
67
+ export interface InvoiceUsageData {
68
+ invoiceId: string;
69
+ startDate: string;
70
+ endDate: string;
71
+ }
@@ -37,6 +37,11 @@ export class GarbageCollectedCache<T> {
37
37
  if (!entry) {
38
38
  return undefined;
39
39
  }
40
+ // Still valid?
41
+ if (entry.expiryDate < Date.now()) {
42
+ this.store.delete(entry.key);
43
+ return undefined;
44
+ }
40
45
  return entry.value;
41
46
  }
42
47
 
@@ -85,6 +85,10 @@ export class GitpodHostUrl {
85
85
  return this.with((url) => ({ protocol: url.protocol === "https:" ? "wss:" : "ws:" }));
86
86
  }
87
87
 
88
+ asWorkspacePage(): GitpodHostUrl {
89
+ return this.with((url) => ({ pathname: "/workspaces" }));
90
+ }
91
+
88
92
  asDashboard(): GitpodHostUrl {
89
93
  return this.with((url) => ({ pathname: "/" }));
90
94
  }
@@ -171,4 +175,21 @@ export class GitpodHostUrl {
171
175
  asApiLogout(): GitpodHostUrl {
172
176
  return this.withApi((url) => ({ pathname: "/logout/" }));
173
177
  }
178
+
179
+ asIDEProxy(): GitpodHostUrl {
180
+ const hostSegments = this.url.host.split(".");
181
+ if (hostSegments[0] === "ide") {
182
+ return this;
183
+ }
184
+ return this.with((url) => ({ host: "ide." + url.host }));
185
+ }
186
+
187
+ asIDEMetrics(): GitpodHostUrl {
188
+ let newUrl: GitpodHostUrl = this;
189
+ const hostSegments = this.url.host.split(".");
190
+ if (hostSegments[0] !== "ide") {
191
+ newUrl = newUrl.asIDEProxy();
192
+ }
193
+ return newUrl.with((url) => ({ pathname: "/metrics-api" }));
194
+ }
174
195
  }
package/src/util/grpc.ts CHANGED
@@ -4,6 +4,9 @@
4
4
  * See License-AGPL.txt in the project root for license information.
5
5
  */
6
6
 
7
+ import * as grpc from "@grpc/grpc-js";
8
+ import { Status } from "@grpc/grpc-js/build/src/constants";
9
+
7
10
  export const defaultGRPCOptions = {
8
11
  "grpc.keepalive_timeout_ms": 10000,
9
12
  "grpc.keepalive_time_ms": 60000,
@@ -13,3 +16,89 @@ export const defaultGRPCOptions = {
13
16
  "grpc.max_reconnect_backoff_ms": 5000,
14
17
  "grpc.max_receive_message_length": 1024 * 1024 * 16,
15
18
  };
19
+
20
+ export type GrpcMethodType = "unary" | "client_stream" | "server_stream" | "bidi_stream";
21
+
22
+ export interface IGrpcCallMetricsLabels {
23
+ service: string;
24
+ method: string;
25
+ type: GrpcMethodType;
26
+ }
27
+
28
+ export interface IGrpcCallMetricsLabelsWithCode extends IGrpcCallMetricsLabels {
29
+ code: string;
30
+ }
31
+
32
+ export const IClientCallMetrics = Symbol("IClientCallMetrics");
33
+
34
+ export interface IClientCallMetrics {
35
+ started(labels: IGrpcCallMetricsLabels): void;
36
+ sent(labels: IGrpcCallMetricsLabels): void;
37
+ received(labels: IGrpcCallMetricsLabels): void;
38
+ handled(labels: IGrpcCallMetricsLabelsWithCode): void;
39
+ }
40
+
41
+ export function getGrpcMethodType(requestStream: boolean, responseStream: boolean): GrpcMethodType {
42
+ if (requestStream) {
43
+ if (responseStream) {
44
+ return "bidi_stream";
45
+ } else {
46
+ return "client_stream";
47
+ }
48
+ } else {
49
+ if (responseStream) {
50
+ return "server_stream";
51
+ } else {
52
+ return "unary";
53
+ }
54
+ }
55
+ }
56
+
57
+ export function createClientCallMetricsInterceptor(metrics: IClientCallMetrics): grpc.Interceptor {
58
+ return (options, nextCall): grpc.InterceptingCall => {
59
+ const methodDef = options.method_definition;
60
+ const method = methodDef.path.substring(methodDef.path.lastIndexOf("/") + 1);
61
+ const service = methodDef.path.substring(1, methodDef.path.length - method.length - 1);
62
+ const labels = {
63
+ service,
64
+ method,
65
+ type: getGrpcMethodType(options.method_definition.requestStream, options.method_definition.responseStream),
66
+ };
67
+ const requester = new grpc.RequesterBuilder()
68
+ .withStart((metadata, listener, next) => {
69
+ const newListener = new grpc.ListenerBuilder()
70
+ .withOnReceiveStatus((status, next) => {
71
+ try {
72
+ metrics.handled({
73
+ ...labels,
74
+ code: Status[status.code],
75
+ });
76
+ } finally {
77
+ next(status);
78
+ }
79
+ })
80
+ .withOnReceiveMessage((message, next) => {
81
+ try {
82
+ metrics.received(labels);
83
+ } finally {
84
+ next(message);
85
+ }
86
+ })
87
+ .build();
88
+ try {
89
+ metrics.started(labels);
90
+ } finally {
91
+ next(metadata, newListener);
92
+ }
93
+ })
94
+ .withSendMessage((message, next) => {
95
+ try {
96
+ metrics.sent(labels);
97
+ } finally {
98
+ next(message);
99
+ }
100
+ })
101
+ .build();
102
+ return new grpc.InterceptingCall(nextCall(options), requester);
103
+ };
104
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Copyright (c) 2021 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 { suite, test } from "mocha-typescript";
8
+ import { log } from "./logging";
9
+
10
+ @suite
11
+ class TestLogging {
12
+ @test public async testLogInfo_output() {
13
+ const testObj = {
14
+ null: null,
15
+ undefined: undefined,
16
+ empty: "",
17
+ foo: "bar",
18
+ number: 0,
19
+ };
20
+ log.info("info logging test", testObj);
21
+ }
22
+ }
23
+ module.exports = new TestLogging();
@@ -6,6 +6,7 @@
6
6
 
7
7
  const inspect: (object: any) => string = require("util").inspect; // undefined in frontend
8
8
 
9
+ let plainLogging: boolean = false; // set to true during development to get non JSON output
9
10
  let jsonLogging: boolean = false;
10
11
  let component: string | undefined;
11
12
  let version: string | undefined;
@@ -309,7 +310,7 @@ function makeLogItem(
309
310
  }
310
311
 
311
312
  const payload: any = payloadArgs.length == 0 ? undefined : payloadArgs.length == 1 ? payloadArgs[0] : payloadArgs;
312
- const logItem: any = {
313
+ const logItem = {
313
314
  // undefined fields get eliminated in JSON.stringify()
314
315
  ...reportedErrorEvent,
315
316
  component,
@@ -321,11 +322,19 @@ function makeLogItem(
321
322
  payload,
322
323
  loggedViaConsole: calledViaConsole ? true : undefined,
323
324
  };
325
+ if (plainLogging) {
326
+ return `[${logItem.severity}] [${logItem.component}] ${logItem.message}
327
+ ${JSON.stringify(payload || "", undefined, " ")}
328
+ ${error || ""}
329
+ `.trim();
330
+ }
324
331
  let result: string = stringifyLogItem(logItem);
325
332
 
326
333
  if (result.length > maxAllowedLogItemLength && payload !== undefined) {
327
334
  delete logItem.payload;
328
- logItem.payloadStub = `Payload stripped as log item was longer than ${maxAllowedLogItemLength} characters`;
335
+ (<any>(
336
+ logItem
337
+ )).payloadStub = `Payload stripped as log item was longer than ${maxAllowedLogItemLength} characters`;
329
338
 
330
339
  result = stringifyLogItem(logItem);
331
340
 
@@ -7,10 +7,16 @@
7
7
  import * as chai from "chai";
8
8
  const expect = chai.expect;
9
9
  import { suite, test } from "mocha-typescript";
10
- import { oneMonthLater } from "./timeutil";
10
+ import { daysBefore, hoursBefore, oneMonthLater } from "./timeutil";
11
11
 
12
12
  @suite()
13
13
  export class TimeutilSpec {
14
+ @test
15
+ testDaysBefore() {
16
+ const now = new Date().toISOString();
17
+ expect(daysBefore(now, 2)).to.be.eq(hoursBefore(now, 48));
18
+ }
19
+
14
20
  @test
15
21
  testTimeutil() {
16
22
  // targeting a 1st, 1th of Jan => 1st of Feb
@@ -39,4 +45,25 @@ export class TimeutilSpec {
39
45
  const later = oneMonthLater(from.toISOString(), day);
40
46
  expect(later, `expected ${later} to be equal ${expectation}`).to.be.equal(expectation.toISOString());
41
47
  }
48
+
49
+ @test
50
+ testDaysBefore2() {
51
+ const tests: { date: Date; daysEarlier: number; expectation: string }[] = [
52
+ {
53
+ date: new Date("2021-07-13T00:00:00.000Z"),
54
+ daysEarlier: 365,
55
+ expectation: "2020-07-13T00:00:00.000Z",
56
+ },
57
+ {
58
+ date: new Date("2019-02-01T00:00:00.000Z"),
59
+ daysEarlier: 365,
60
+ expectation: "2018-02-01T00:00:00.000Z",
61
+ },
62
+ ];
63
+
64
+ for (const t of tests) {
65
+ const actual = daysBefore(t.date.toISOString(), t.daysEarlier);
66
+ expect(actual).to.equal(t.expectation, `expected ${actual} to be equal ${t.expectation}`);
67
+ }
68
+ }
42
69
  }
@@ -47,6 +47,12 @@ export const orderAsc = (d1: string, d2: string): number => liftDate(d1, d2, (d1
47
47
  export const liftDate1 = <T>(d1: string, f: (d1: Date) => T): T => f(new Date(d1));
48
48
  export const liftDate = <T>(d1: string, d2: string, f: (d1: Date, d2: Date) => T): T => f(new Date(d1), new Date(d2));
49
49
 
50
+ export function daysBefore(date: string, days: number): string {
51
+ const result = new Date(date);
52
+ result.setDate(result.getDate() - days);
53
+ return result.toISOString();
54
+ }
55
+
50
56
  export function hoursBefore(date: string, hours: number): string {
51
57
  const result = new Date(date);
52
58
  result.setHours(result.getHours() - hours);