From 352484b34df70361c3754354a6770b84b4bb7557 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Fri, 15 Sep 2023 13:46:54 +0200 Subject: [PATCH] node grpc spike dashboard to server (#18691) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [public-api] add dummy service for testing * [public-api] proxy dummy to server * [public-api] hello service server impl * [server] fix API contribution bindings * [dashboard] emulate unary call * only if actually called * [dummy] auth * fix tests * [server] add interceptor to public api * add server side observability * fix port name * change to unimplemented for unknown methods * [public-api] client metrics * fix metrics imports * align server metrics * actually fix metrics * add feature flags * fix server side streams * [dashboard] hook error reporting * rebase and fix imports * feature flagged metrics from dashboard * revert GRPC_TYPE * address feedback --- components/dashboard/package.json | 16 +- components/dashboard/src/index.tsx | 3 + components/dashboard/src/service/metrics.ts | 84 ++++ .../dashboard/src/service/public-api.ts | 7 +- components/dashboard/src/service/service.tsx | 75 +++- components/dashboard/src/teams/NewTeam.tsx | 2 +- components/proxy/conf/Caddyfile | 11 + .../gitpod/experimental/v1/dummy.proto | 28 ++ .../public-api/go/experimental/v1/dummy.pb.go | 379 ++++++++++++++++ .../go/experimental/v1/dummy_grpc.pb.go | 181 ++++++++ .../v1/v1connect/dummy.connect.go | 120 +++++ .../v1/v1connect/dummy.proxy.connect.go | 30 ++ components/public-api/typescript/package.json | 5 +- .../experimental/v1/dummy_connectweb.ts | 49 +++ .../src/gitpod/experimental/v1/dummy_pb.ts | 173 ++++++++ .../public-api/typescript/src/metrics.ts | 411 ++++++++++++++++++ .../public-api/typescript/tsconfig.json | 5 +- components/server/package.json | 3 +- components/server/src/api/dummy.ts | 40 ++ .../src/api/handler-context-augmentation.d.ts | 13 + components/server/src/api/server.ts | 175 +++++++- components/server/src/api/teams.spec.db.ts | 38 +- components/server/src/container-module.ts | 42 +- components/server/src/prometheus-metrics.ts | 18 + components/server/src/server.ts | 9 +- components/server/src/session-handler.ts | 35 +- install/installer/pkg/common/constants.go | 1 + .../pkg/components/ide-metrics/configmap.go | 7 +- .../pkg/components/server/constants.go | 3 + .../pkg/components/server/deployment.go | 3 + .../pkg/components/server/networkpolicy.go | 4 + .../pkg/components/server/objects.go | 5 + yarn.lock | 40 +- 33 files changed, 1891 insertions(+), 124 deletions(-) create mode 100644 components/dashboard/src/service/metrics.ts create mode 100644 components/public-api/gitpod/experimental/v1/dummy.proto create mode 100644 components/public-api/go/experimental/v1/dummy.pb.go create mode 100644 components/public-api/go/experimental/v1/dummy_grpc.pb.go create mode 100644 components/public-api/go/experimental/v1/v1connect/dummy.connect.go create mode 100644 components/public-api/go/experimental/v1/v1connect/dummy.proxy.connect.go create mode 100644 components/public-api/typescript/src/gitpod/experimental/v1/dummy_connectweb.ts create mode 100644 components/public-api/typescript/src/gitpod/experimental/v1/dummy_pb.ts create mode 100644 components/public-api/typescript/src/metrics.ts create mode 100644 components/server/src/api/dummy.ts create mode 100644 components/server/src/api/handler-context-augmentation.d.ts diff --git a/components/dashboard/package.json b/components/dashboard/package.json index aea8950da1ccb3..3b3a0f62c4a6f9 100644 --- a/components/dashboard/package.json +++ b/components/dashboard/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "private": true, "dependencies": { - "@bufbuild/connect-web": "^0.2.1", + "@bufbuild/connect-web": "^0.13.0", "@gitpod/gitpod-protocol": "0.1.5", "@gitpod/public-api": "0.1.5", "@stripe/react-stripe-js": "^1.7.2", @@ -14,9 +14,11 @@ "@tanstack/react-query-devtools": "^4.29.19", "@tanstack/react-query-persist-client": "^4.29.19", "@types/react-datepicker": "^4.8.0", + "buffer": "^4.3.0", "classnames": "^2.3.1", "configcat-js": "^6.0.0", "countries-list": "^2.6.1", + "crypto-browserify": "3.12.0", "dayjs": "^1.11.5", "file-saver": "^2.0.5", "idb-keyval": "^6.2.0", @@ -25,6 +27,7 @@ "monaco-editor": "^0.25.2", "p-throttle": "^5.1.0", "pretty-bytes": "^6.1.0", + "process": "^0.11.10", "query-string": "^7.1.1", "react": "^17.0.1", "react-confetti": "^6.1.0", @@ -37,16 +40,13 @@ "react-popper": "^2.3.0", "react-portal": "^4.2.2", "react-router-dom": "^5.2.0", - "validator": "^13.9.0", - "xterm": "^4.11.0", - "xterm-addon-fit": "^0.5.0", - "crypto-browserify": "3.12.0", + "setimmediate": "^1.0.5", "stream-browserify": "^2.0.1", "url": "^0.11.1", "util": "^0.11.1", - "buffer": "^4.3.0", - "process": "^0.11.10", - "setimmediate": "^1.0.5" + "validator": "^13.9.0", + "xterm": "^4.11.0", + "xterm-addon-fit": "^0.5.0" }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", diff --git a/components/dashboard/src/index.tsx b/components/dashboard/src/index.tsx index 1a53fc4684907a..b8cfa7d1d59fba 100644 --- a/components/dashboard/src/index.tsx +++ b/components/dashboard/src/index.tsx @@ -4,6 +4,9 @@ * See License.AGPL.txt in the project root for license information. */ +// this should stay at the top to enable monitoring as soon as possible +import "./service/metrics"; + import "setimmediate"; // important!, required by vscode-jsonrpc import dayjs from "dayjs"; import duration from "dayjs/plugin/duration"; diff --git a/components/dashboard/src/service/metrics.ts b/components/dashboard/src/service/metrics.ts new file mode 100644 index 00000000000000..a5b0d9ad168ef1 --- /dev/null +++ b/components/dashboard/src/service/metrics.ts @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2023 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. + */ + +import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url"; +import { MetricsReporter } from "@gitpod/public-api/lib/metrics"; +import { getExperimentsClient } from "../experiments/client"; + +const originalConsoleError = console.error; + +const options = { + gitpodUrl: new GitpodHostUrl(window.location.href).withoutWorkspacePrefix().toString(), + clientName: "dashboard", + clientVersion: "", + logError: originalConsoleError.bind(console), + isEnabled: () => getExperimentsClient().getValueAsync("dashboard_metrics_enabled", false, {}), +}; +fetch("/api/version").then(async (res) => { + const version = await res.text(); + options.clientVersion = version; +}); +const metricsReporter = new MetricsReporter(options); +metricsReporter.startReporting(); + +window.addEventListener("unhandledrejection", (event) => { + reportError("Unhandled promise rejection", event.reason); +}); +window.addEventListener("error", (event) => { + let message = "Unhandled error"; + if (event.message) { + message += ": " + event.message; + } + reportError(message, event.error); +}); + +console.error = function (...args) { + originalConsoleError.apply(console, args); + reportError(...args); +}; + +function reportError(...args: any[]) { + let err = undefined; + let details = undefined; + if (args[0] instanceof Error) { + err = args[0]; + details = args[1]; + } else if (typeof args[0] === "string") { + err = new Error(args[0]); + if (args[1] instanceof Error) { + err.message += ": " + args[1].message; + err.name = args[1].name; + err.stack = args[1].stack; + details = args[2]; + } else if (typeof args[1] === "string") { + err.message += ": " + args[1]; + details = args[2]; + } else { + details = args[1]; + } + } + + let data = undefined; + if (details && typeof details === "object") { + data = Object.fromEntries( + Object.entries(details) + .filter(([key, value]) => { + return ( + typeof value === "string" || + typeof value === "number" || + typeof value === "boolean" || + value === null || + typeof value === "undefined" + ); + }) + .map(([key, value]) => [key, String(value)]), + ); + } + + if (err) { + metricsReporter.reportError(err, data); + } +} diff --git a/components/dashboard/src/service/public-api.ts b/components/dashboard/src/service/public-api.ts index a8544fd56f1f1f..73bce3c8e9e76f 100644 --- a/components/dashboard/src/service/public-api.ts +++ b/components/dashboard/src/service/public-api.ts @@ -4,13 +4,16 @@ * See License.AGPL.txt in the project root for license information. */ -import { createConnectTransport, createPromiseClient } from "@bufbuild/connect-web"; +import { createPromiseClient } from "@bufbuild/connect"; +import { createConnectTransport } from "@bufbuild/connect-web"; import { Project as ProtocolProject, Team as ProtocolTeam } from "@gitpod/gitpod-protocol/lib/teams-projects-protocol"; +import { HelloService } from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_connectweb"; import { TeamsService } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_connectweb"; import { TokensService } from "@gitpod/public-api/lib/gitpod/experimental/v1/tokens_connectweb"; import { ProjectsService } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_connectweb"; import { WorkspacesService } from "@gitpod/public-api/lib/gitpod/experimental/v1/workspaces_connectweb"; import { OIDCService } from "@gitpod/public-api/lib/gitpod/experimental/v1/oidc_connectweb"; +import { getMetricsInterceptor } from "@gitpod/public-api/lib/metrics"; import { Team } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_pb"; import { TeamMemberInfo, TeamMemberRole } from "@gitpod/gitpod-protocol"; import { TeamMember, TeamRole } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_pb"; @@ -18,8 +21,10 @@ import { Project } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_ const transport = createConnectTransport({ baseUrl: `${window.location.protocol}//${window.location.host}/public-api`, + interceptors: [getMetricsInterceptor()], }); +export const helloService = createPromiseClient(HelloService, transport); export const teamsService = createPromiseClient(TeamsService, transport); export const personalAccessTokensService = createPromiseClient(TokensService, transport); export const projectsService = createPromiseClient(ProjectsService, transport); diff --git a/components/dashboard/src/service/service.tsx b/components/dashboard/src/service/service.tsx index 44aaedfeaaf903..8aed99d1c89bd0 100644 --- a/components/dashboard/src/service/service.tsx +++ b/components/dashboard/src/service/service.tsx @@ -19,6 +19,8 @@ import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url" import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; import { IDEFrontendDashboardService } from "@gitpod/gitpod-protocol/lib/frontend-dashboard-service"; import { RemoteTrackMessage } from "@gitpod/gitpod-protocol/lib/analytics"; +import { helloService } from "./public-api"; +import { getExperimentsClient } from "../experiments/client"; export const gitpodHostUrl = new GitpodHostUrl(window.location.toString()); @@ -56,10 +58,81 @@ export function getGitpodService(): GitpodService { const service = _gp.gitpodService || (_gp.gitpodService = require("./service-mock").gitpodServiceMock); return service; } - const service = _gp.gitpodService || (_gp.gitpodService = createGitpodService()); + let service = _gp.gitpodService; + if (!service) { + service = _gp.gitpodService = createGitpodService(); + testPublicAPI(service); + } return service; } +/** + * Emulates getWorkspace calls and listen to workspace statuses with Public API. + * // TODO(ak): remove after reliability of Public API is confirmed + */ +function testPublicAPI(service: any): void { + let user: any; + service.server = new Proxy(service.server, { + get(target, propKey) { + return async function (...args: any[]) { + if (propKey === "getLoggedInUser") { + user = await target[propKey](...args); + return user; + } + if (propKey === "getWorkspace") { + try { + return await target[propKey](...args); + } finally { + const grpcType = "unary"; + // emulates frequent unary calls to public API + const isTest = await getExperimentsClient().getValueAsync( + "public_api_dummy_reliability_test", + false, + { + user, + gitpodHost: window.location.host, + }, + ); + if (isTest) { + helloService.sayHello({}).catch((e) => { + console.error(e, { + userId: user?.id, + workspaceId: args[0], + grpcType, + }); + }); + } + } + } + return target[propKey](...args); + }; + }, + }); + (async () => { + const grpcType = "server-stream"; + // emulates server side streaming with public API + while (true) { + const isTest = await getExperimentsClient().getValueAsync("public_api_dummy_reliability_test", false, { + user, + gitpodHost: window.location.host, + }); + if (isTest) { + try { + let previousCount = 0; + for await (const reply of helloService.lotsOfReplies({ previousCount })) { + previousCount = reply.count; + } + } catch (e) { + console.error(e, { + userId: user?.id, + grpcType, + }); + } + } + await new Promise((resolve) => setTimeout(resolve, 3000)); + } + })(); +} let ideFrontendService: IDEFrontendService | undefined; export function getIDEFrontendService(workspaceID: string, sessionId: string, service: GitpodService) { if (!ideFrontendService) { diff --git a/components/dashboard/src/teams/NewTeam.tsx b/components/dashboard/src/teams/NewTeam.tsx index 57415de593652b..c1ffa373c03ccd 100644 --- a/components/dashboard/src/teams/NewTeam.tsx +++ b/components/dashboard/src/teams/NewTeam.tsx @@ -4,7 +4,7 @@ * See License.AGPL.txt in the project root for license information. */ -import { ConnectError } from "@bufbuild/connect-web"; +import { ConnectError } from "@bufbuild/connect"; import { FormEvent, useState } from "react"; import { useHistory } from "react-router-dom"; import { Heading1, Heading3, Subheading } from "../components/typography/headings"; diff --git a/components/proxy/conf/Caddyfile b/components/proxy/conf/Caddyfile index 38b28fd099c370..eb8055af75c8b4 100644 --- a/components/proxy/conf/Caddyfile +++ b/components/proxy/conf/Caddyfile @@ -195,6 +195,17 @@ https://{$GITPOD_DOMAIN} { import ssl_configuration import security_headers + @proxy_server_public_api path /public-api/gitpod.experimental.v1.HelloService* + handle @proxy_server_public_api { + uri strip_prefix /public-api + # TODO(ak) verify that it only enabled for json content-type, not grpc + import compression + + reverse_proxy server.{$KUBE_NAMESPACE}.{$KUBE_DOMAIN}:3001 { + import upstream_connection + } + } + @proxy_public_api path /public-api* handle @proxy_public_api { uri strip_prefix /public-api diff --git a/components/public-api/gitpod/experimental/v1/dummy.proto b/components/public-api/gitpod/experimental/v1/dummy.proto new file mode 100644 index 00000000000000..275859b149ee6d --- /dev/null +++ b/components/public-api/gitpod/experimental/v1/dummy.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +package gitpod.experimental.v1; + +option go_package = "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"; + +// HelloService is a dummy service that says hello. It is used for reliability +// testing. +service HelloService { + // Unary RPCs where the client sends a single request to the server and gets a + // single response back, just like a normal function call. + rpc SayHello(SayHelloRequest) returns (SayHelloResponse); + // Server streaming RPCs where the client sends a request to the server and + // gets a stream to read a sequence of messages back. + rpc LotsOfReplies(LotsOfRepliesRequest) + returns (stream LotsOfRepliesResponse); +} + +message SayHelloRequest {} +message SayHelloResponse { string reply = 1; } + +message LotsOfRepliesRequest { + int32 previous_count = 1; +} +message LotsOfRepliesResponse { + string reply = 1; + int32 count = 2; +} diff --git a/components/public-api/go/experimental/v1/dummy.pb.go b/components/public-api/go/experimental/v1/dummy.pb.go new file mode 100644 index 00000000000000..56f43f3d85f584 --- /dev/null +++ b/components/public-api/go/experimental/v1/dummy.pb.go @@ -0,0 +1,379 @@ +// Copyright (c) 2023 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. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc (unknown) +// source: gitpod/experimental/v1/dummy.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SayHelloRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Greeting string `protobuf:"bytes,1,opt,name=greeting,proto3" json:"greeting,omitempty"` +} + +func (x *SayHelloRequest) Reset() { + *x = SayHelloRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_gitpod_experimental_v1_dummy_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SayHelloRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SayHelloRequest) ProtoMessage() {} + +func (x *SayHelloRequest) ProtoReflect() protoreflect.Message { + mi := &file_gitpod_experimental_v1_dummy_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SayHelloRequest.ProtoReflect.Descriptor instead. +func (*SayHelloRequest) Descriptor() ([]byte, []int) { + return file_gitpod_experimental_v1_dummy_proto_rawDescGZIP(), []int{0} +} + +func (x *SayHelloRequest) GetGreeting() string { + if x != nil { + return x.Greeting + } + return "" +} + +type SayHelloResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Reply string `protobuf:"bytes,1,opt,name=reply,proto3" json:"reply,omitempty"` +} + +func (x *SayHelloResponse) Reset() { + *x = SayHelloResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_gitpod_experimental_v1_dummy_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SayHelloResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SayHelloResponse) ProtoMessage() {} + +func (x *SayHelloResponse) ProtoReflect() protoreflect.Message { + mi := &file_gitpod_experimental_v1_dummy_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SayHelloResponse.ProtoReflect.Descriptor instead. +func (*SayHelloResponse) Descriptor() ([]byte, []int) { + return file_gitpod_experimental_v1_dummy_proto_rawDescGZIP(), []int{1} +} + +func (x *SayHelloResponse) GetReply() string { + if x != nil { + return x.Reply + } + return "" +} + +type LotsOfRepliesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Greeting string `protobuf:"bytes,1,opt,name=greeting,proto3" json:"greeting,omitempty"` + PreviousCount int32 `protobuf:"varint,2,opt,name=previous_count,json=previousCount,proto3" json:"previous_count,omitempty"` +} + +func (x *LotsOfRepliesRequest) Reset() { + *x = LotsOfRepliesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_gitpod_experimental_v1_dummy_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LotsOfRepliesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LotsOfRepliesRequest) ProtoMessage() {} + +func (x *LotsOfRepliesRequest) ProtoReflect() protoreflect.Message { + mi := &file_gitpod_experimental_v1_dummy_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LotsOfRepliesRequest.ProtoReflect.Descriptor instead. +func (*LotsOfRepliesRequest) Descriptor() ([]byte, []int) { + return file_gitpod_experimental_v1_dummy_proto_rawDescGZIP(), []int{2} +} + +func (x *LotsOfRepliesRequest) GetGreeting() string { + if x != nil { + return x.Greeting + } + return "" +} + +func (x *LotsOfRepliesRequest) GetPreviousCount() int32 { + if x != nil { + return x.PreviousCount + } + return 0 +} + +type LotsOfRepliesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Reply string `protobuf:"bytes,1,opt,name=reply,proto3" json:"reply,omitempty"` + Count int32 `protobuf:"varint,2,opt,name=count,proto3" json:"count,omitempty"` +} + +func (x *LotsOfRepliesResponse) Reset() { + *x = LotsOfRepliesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_gitpod_experimental_v1_dummy_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LotsOfRepliesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LotsOfRepliesResponse) ProtoMessage() {} + +func (x *LotsOfRepliesResponse) ProtoReflect() protoreflect.Message { + mi := &file_gitpod_experimental_v1_dummy_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LotsOfRepliesResponse.ProtoReflect.Descriptor instead. +func (*LotsOfRepliesResponse) Descriptor() ([]byte, []int) { + return file_gitpod_experimental_v1_dummy_proto_rawDescGZIP(), []int{3} +} + +func (x *LotsOfRepliesResponse) GetReply() string { + if x != nil { + return x.Reply + } + return "" +} + +func (x *LotsOfRepliesResponse) GetCount() int32 { + if x != nil { + return x.Count + } + return 0 +} + +var File_gitpod_experimental_v1_dummy_proto protoreflect.FileDescriptor + +var file_gitpod_experimental_v1_dummy_proto_rawDesc = []byte{ + 0x0a, 0x22, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2f, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, + 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2f, 0x76, 0x31, 0x2f, 0x64, 0x75, 0x6d, 0x6d, 0x79, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x16, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, + 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x22, 0x2d, 0x0a, 0x0f, + 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x1a, 0x0a, 0x08, 0x67, 0x72, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x67, 0x72, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x28, 0x0a, 0x10, 0x53, + 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x14, 0x0a, 0x05, 0x72, 0x65, 0x70, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x72, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x59, 0x0a, 0x14, 0x4c, 0x6f, 0x74, 0x73, 0x4f, 0x66, 0x52, + 0x65, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, + 0x08, 0x67, 0x72, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x67, 0x72, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x65, + 0x76, 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x0d, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x22, 0x43, 0x0a, 0x15, 0x4c, 0x6f, 0x74, 0x73, 0x4f, 0x66, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x65, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x70, + 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x65, 0x70, 0x6c, 0x79, 0x12, + 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x32, 0xdd, 0x01, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5d, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, + 0x6c, 0x6f, 0x12, 0x27, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, + 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x61, 0x79, 0x48, + 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x67, 0x69, + 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, + 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6e, 0x0a, 0x0d, 0x4c, 0x6f, 0x74, 0x73, 0x4f, 0x66, 0x52, + 0x65, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x12, 0x2c, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, + 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, + 0x4c, 0x6f, 0x74, 0x73, 0x4f, 0x66, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, + 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, + 0x74, 0x73, 0x4f, 0x66, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, 0x46, 0x5a, 0x44, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2d, 0x69, 0x6f, 0x2f, 0x67, 0x69, + 0x74, 0x70, 0x6f, 0x64, 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2f, + 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2d, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x65, 0x78, + 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_gitpod_experimental_v1_dummy_proto_rawDescOnce sync.Once + file_gitpod_experimental_v1_dummy_proto_rawDescData = file_gitpod_experimental_v1_dummy_proto_rawDesc +) + +func file_gitpod_experimental_v1_dummy_proto_rawDescGZIP() []byte { + file_gitpod_experimental_v1_dummy_proto_rawDescOnce.Do(func() { + file_gitpod_experimental_v1_dummy_proto_rawDescData = protoimpl.X.CompressGZIP(file_gitpod_experimental_v1_dummy_proto_rawDescData) + }) + return file_gitpod_experimental_v1_dummy_proto_rawDescData +} + +var file_gitpod_experimental_v1_dummy_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_gitpod_experimental_v1_dummy_proto_goTypes = []interface{}{ + (*SayHelloRequest)(nil), // 0: gitpod.experimental.v1.SayHelloRequest + (*SayHelloResponse)(nil), // 1: gitpod.experimental.v1.SayHelloResponse + (*LotsOfRepliesRequest)(nil), // 2: gitpod.experimental.v1.LotsOfRepliesRequest + (*LotsOfRepliesResponse)(nil), // 3: gitpod.experimental.v1.LotsOfRepliesResponse +} +var file_gitpod_experimental_v1_dummy_proto_depIdxs = []int32{ + 0, // 0: gitpod.experimental.v1.HelloService.SayHello:input_type -> gitpod.experimental.v1.SayHelloRequest + 2, // 1: gitpod.experimental.v1.HelloService.LotsOfReplies:input_type -> gitpod.experimental.v1.LotsOfRepliesRequest + 1, // 2: gitpod.experimental.v1.HelloService.SayHello:output_type -> gitpod.experimental.v1.SayHelloResponse + 3, // 3: gitpod.experimental.v1.HelloService.LotsOfReplies:output_type -> gitpod.experimental.v1.LotsOfRepliesResponse + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_gitpod_experimental_v1_dummy_proto_init() } +func file_gitpod_experimental_v1_dummy_proto_init() { + if File_gitpod_experimental_v1_dummy_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_gitpod_experimental_v1_dummy_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SayHelloRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gitpod_experimental_v1_dummy_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SayHelloResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gitpod_experimental_v1_dummy_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LotsOfRepliesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gitpod_experimental_v1_dummy_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LotsOfRepliesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_gitpod_experimental_v1_dummy_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_gitpod_experimental_v1_dummy_proto_goTypes, + DependencyIndexes: file_gitpod_experimental_v1_dummy_proto_depIdxs, + MessageInfos: file_gitpod_experimental_v1_dummy_proto_msgTypes, + }.Build() + File_gitpod_experimental_v1_dummy_proto = out.File + file_gitpod_experimental_v1_dummy_proto_rawDesc = nil + file_gitpod_experimental_v1_dummy_proto_goTypes = nil + file_gitpod_experimental_v1_dummy_proto_depIdxs = nil +} diff --git a/components/public-api/go/experimental/v1/dummy_grpc.pb.go b/components/public-api/go/experimental/v1/dummy_grpc.pb.go new file mode 100644 index 00000000000000..f3e9070720a9d2 --- /dev/null +++ b/components/public-api/go/experimental/v1/dummy_grpc.pb.go @@ -0,0 +1,181 @@ +// Copyright (c) 2023 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. + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc (unknown) +// source: gitpod/experimental/v1/dummy.proto + +package v1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// HelloServiceClient is the client API for HelloService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type HelloServiceClient interface { + // Unary RPCs where the client sends a single request to the server and gets a + // single response back, just like a normal function call. + SayHello(ctx context.Context, in *SayHelloRequest, opts ...grpc.CallOption) (*SayHelloResponse, error) + // Server streaming RPCs where the client sends a request to the server and + // gets a stream to read a sequence of messages back. + LotsOfReplies(ctx context.Context, in *LotsOfRepliesRequest, opts ...grpc.CallOption) (HelloService_LotsOfRepliesClient, error) +} + +type helloServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewHelloServiceClient(cc grpc.ClientConnInterface) HelloServiceClient { + return &helloServiceClient{cc} +} + +func (c *helloServiceClient) SayHello(ctx context.Context, in *SayHelloRequest, opts ...grpc.CallOption) (*SayHelloResponse, error) { + out := new(SayHelloResponse) + err := c.cc.Invoke(ctx, "/gitpod.experimental.v1.HelloService/SayHello", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *helloServiceClient) LotsOfReplies(ctx context.Context, in *LotsOfRepliesRequest, opts ...grpc.CallOption) (HelloService_LotsOfRepliesClient, error) { + stream, err := c.cc.NewStream(ctx, &HelloService_ServiceDesc.Streams[0], "/gitpod.experimental.v1.HelloService/LotsOfReplies", opts...) + if err != nil { + return nil, err + } + x := &helloServiceLotsOfRepliesClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type HelloService_LotsOfRepliesClient interface { + Recv() (*LotsOfRepliesResponse, error) + grpc.ClientStream +} + +type helloServiceLotsOfRepliesClient struct { + grpc.ClientStream +} + +func (x *helloServiceLotsOfRepliesClient) Recv() (*LotsOfRepliesResponse, error) { + m := new(LotsOfRepliesResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// HelloServiceServer is the server API for HelloService service. +// All implementations must embed UnimplementedHelloServiceServer +// for forward compatibility +type HelloServiceServer interface { + // Unary RPCs where the client sends a single request to the server and gets a + // single response back, just like a normal function call. + SayHello(context.Context, *SayHelloRequest) (*SayHelloResponse, error) + // Server streaming RPCs where the client sends a request to the server and + // gets a stream to read a sequence of messages back. + LotsOfReplies(*LotsOfRepliesRequest, HelloService_LotsOfRepliesServer) error + mustEmbedUnimplementedHelloServiceServer() +} + +// UnimplementedHelloServiceServer must be embedded to have forward compatible implementations. +type UnimplementedHelloServiceServer struct { +} + +func (UnimplementedHelloServiceServer) SayHello(context.Context, *SayHelloRequest) (*SayHelloResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented") +} +func (UnimplementedHelloServiceServer) LotsOfReplies(*LotsOfRepliesRequest, HelloService_LotsOfRepliesServer) error { + return status.Errorf(codes.Unimplemented, "method LotsOfReplies not implemented") +} +func (UnimplementedHelloServiceServer) mustEmbedUnimplementedHelloServiceServer() {} + +// UnsafeHelloServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to HelloServiceServer will +// result in compilation errors. +type UnsafeHelloServiceServer interface { + mustEmbedUnimplementedHelloServiceServer() +} + +func RegisterHelloServiceServer(s grpc.ServiceRegistrar, srv HelloServiceServer) { + s.RegisterService(&HelloService_ServiceDesc, srv) +} + +func _HelloService_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SayHelloRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HelloServiceServer).SayHello(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gitpod.experimental.v1.HelloService/SayHello", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HelloServiceServer).SayHello(ctx, req.(*SayHelloRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _HelloService_LotsOfReplies_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(LotsOfRepliesRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(HelloServiceServer).LotsOfReplies(m, &helloServiceLotsOfRepliesServer{stream}) +} + +type HelloService_LotsOfRepliesServer interface { + Send(*LotsOfRepliesResponse) error + grpc.ServerStream +} + +type helloServiceLotsOfRepliesServer struct { + grpc.ServerStream +} + +func (x *helloServiceLotsOfRepliesServer) Send(m *LotsOfRepliesResponse) error { + return x.ServerStream.SendMsg(m) +} + +// HelloService_ServiceDesc is the grpc.ServiceDesc for HelloService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var HelloService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "gitpod.experimental.v1.HelloService", + HandlerType: (*HelloServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SayHello", + Handler: _HelloService_SayHello_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "LotsOfReplies", + Handler: _HelloService_LotsOfReplies_Handler, + ServerStreams: true, + }, + }, + Metadata: "gitpod/experimental/v1/dummy.proto", +} diff --git a/components/public-api/go/experimental/v1/v1connect/dummy.connect.go b/components/public-api/go/experimental/v1/v1connect/dummy.connect.go new file mode 100644 index 00000000000000..dd552b7c57df53 --- /dev/null +++ b/components/public-api/go/experimental/v1/v1connect/dummy.connect.go @@ -0,0 +1,120 @@ +// Copyright (c) 2023 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. + +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: gitpod/experimental/v1/dummy.proto + +package v1connect + +import ( + context "context" + errors "errors" + connect_go "github.com/bufbuild/connect-go" + v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect_go.IsAtLeastVersion0_1_0 + +const ( + // HelloServiceName is the fully-qualified name of the HelloService service. + HelloServiceName = "gitpod.experimental.v1.HelloService" +) + +// HelloServiceClient is a client for the gitpod.experimental.v1.HelloService service. +type HelloServiceClient interface { + // Unary RPCs where the client sends a single request to the server and gets a + // single response back, just like a normal function call. + SayHello(context.Context, *connect_go.Request[v1.SayHelloRequest]) (*connect_go.Response[v1.SayHelloResponse], error) + // Server streaming RPCs where the client sends a request to the server and + // gets a stream to read a sequence of messages back. + LotsOfReplies(context.Context, *connect_go.Request[v1.LotsOfRepliesRequest]) (*connect_go.ServerStreamForClient[v1.LotsOfRepliesResponse], error) +} + +// NewHelloServiceClient constructs a client for the gitpod.experimental.v1.HelloService service. By +// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, +// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the +// connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewHelloServiceClient(httpClient connect_go.HTTPClient, baseURL string, opts ...connect_go.ClientOption) HelloServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + return &helloServiceClient{ + sayHello: connect_go.NewClient[v1.SayHelloRequest, v1.SayHelloResponse]( + httpClient, + baseURL+"/gitpod.experimental.v1.HelloService/SayHello", + opts..., + ), + lotsOfReplies: connect_go.NewClient[v1.LotsOfRepliesRequest, v1.LotsOfRepliesResponse]( + httpClient, + baseURL+"/gitpod.experimental.v1.HelloService/LotsOfReplies", + opts..., + ), + } +} + +// helloServiceClient implements HelloServiceClient. +type helloServiceClient struct { + sayHello *connect_go.Client[v1.SayHelloRequest, v1.SayHelloResponse] + lotsOfReplies *connect_go.Client[v1.LotsOfRepliesRequest, v1.LotsOfRepliesResponse] +} + +// SayHello calls gitpod.experimental.v1.HelloService.SayHello. +func (c *helloServiceClient) SayHello(ctx context.Context, req *connect_go.Request[v1.SayHelloRequest]) (*connect_go.Response[v1.SayHelloResponse], error) { + return c.sayHello.CallUnary(ctx, req) +} + +// LotsOfReplies calls gitpod.experimental.v1.HelloService.LotsOfReplies. +func (c *helloServiceClient) LotsOfReplies(ctx context.Context, req *connect_go.Request[v1.LotsOfRepliesRequest]) (*connect_go.ServerStreamForClient[v1.LotsOfRepliesResponse], error) { + return c.lotsOfReplies.CallServerStream(ctx, req) +} + +// HelloServiceHandler is an implementation of the gitpod.experimental.v1.HelloService service. +type HelloServiceHandler interface { + // Unary RPCs where the client sends a single request to the server and gets a + // single response back, just like a normal function call. + SayHello(context.Context, *connect_go.Request[v1.SayHelloRequest]) (*connect_go.Response[v1.SayHelloResponse], error) + // Server streaming RPCs where the client sends a request to the server and + // gets a stream to read a sequence of messages back. + LotsOfReplies(context.Context, *connect_go.Request[v1.LotsOfRepliesRequest], *connect_go.ServerStream[v1.LotsOfRepliesResponse]) error +} + +// NewHelloServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewHelloServiceHandler(svc HelloServiceHandler, opts ...connect_go.HandlerOption) (string, http.Handler) { + mux := http.NewServeMux() + mux.Handle("/gitpod.experimental.v1.HelloService/SayHello", connect_go.NewUnaryHandler( + "/gitpod.experimental.v1.HelloService/SayHello", + svc.SayHello, + opts..., + )) + mux.Handle("/gitpod.experimental.v1.HelloService/LotsOfReplies", connect_go.NewServerStreamHandler( + "/gitpod.experimental.v1.HelloService/LotsOfReplies", + svc.LotsOfReplies, + opts..., + )) + return "/gitpod.experimental.v1.HelloService/", mux +} + +// UnimplementedHelloServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedHelloServiceHandler struct{} + +func (UnimplementedHelloServiceHandler) SayHello(context.Context, *connect_go.Request[v1.SayHelloRequest]) (*connect_go.Response[v1.SayHelloResponse], error) { + return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("gitpod.experimental.v1.HelloService.SayHello is not implemented")) +} + +func (UnimplementedHelloServiceHandler) LotsOfReplies(context.Context, *connect_go.Request[v1.LotsOfRepliesRequest], *connect_go.ServerStream[v1.LotsOfRepliesResponse]) error { + return connect_go.NewError(connect_go.CodeUnimplemented, errors.New("gitpod.experimental.v1.HelloService.LotsOfReplies is not implemented")) +} diff --git a/components/public-api/go/experimental/v1/v1connect/dummy.proxy.connect.go b/components/public-api/go/experimental/v1/v1connect/dummy.proxy.connect.go new file mode 100644 index 00000000000000..ec2e591a3b0a4e --- /dev/null +++ b/components/public-api/go/experimental/v1/v1connect/dummy.proxy.connect.go @@ -0,0 +1,30 @@ +// Copyright (c) 2023 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. + +// Code generated by protoc-proxy-gen. DO NOT EDIT. + +package v1connect + +import ( + context "context" + connect_go "github.com/bufbuild/connect-go" + v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1" +) + +var _ HelloServiceHandler = (*ProxyHelloServiceHandler)(nil) + +type ProxyHelloServiceHandler struct { + Client v1.HelloServiceClient + UnimplementedHelloServiceHandler +} + +func (s *ProxyHelloServiceHandler) SayHello(ctx context.Context, req *connect_go.Request[v1.SayHelloRequest]) (*connect_go.Response[v1.SayHelloResponse], error) { + resp, err := s.Client.SayHello(ctx, req.Msg) + if err != nil { + // TODO(milan): Convert to correct status code + return nil, err + } + + return connect_go.NewResponse(resp), nil +} diff --git a/components/public-api/typescript/package.json b/components/public-api/typescript/package.json index 5a768aeb7a5eb0..b296bc2f61a54e 100644 --- a/components/public-api/typescript/package.json +++ b/components/public-api/typescript/package.json @@ -14,10 +14,11 @@ "test:brk": "yarn test --inspect-brk" }, "dependencies": { - "@bufbuild/connect-web": "^0.2.1", + "@bufbuild/connect": "^0.13.0", "@bufbuild/protobuf": "^0.1.1", "@bufbuild/protoc-gen-connect-web": "^0.2.1", - "@bufbuild/protoc-gen-es": "^0.1.1" + "@bufbuild/protoc-gen-es": "^0.1.1", + "prom-client": "^14.2.0" }, "devDependencies": { "@testdeck/mocha": "0.1.2", diff --git a/components/public-api/typescript/src/gitpod/experimental/v1/dummy_connectweb.ts b/components/public-api/typescript/src/gitpod/experimental/v1/dummy_connectweb.ts new file mode 100644 index 00000000000000..abeca25d02364e --- /dev/null +++ b/components/public-api/typescript/src/gitpod/experimental/v1/dummy_connectweb.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2023 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. + */ + +// @generated by protoc-gen-connect-web v0.2.1 with parameter "target=ts" +// @generated from file gitpod/experimental/v1/dummy.proto (package gitpod.experimental.v1, syntax proto3) +/* eslint-disable */ +/* @ts-nocheck */ + +import {LotsOfRepliesRequest, LotsOfRepliesResponse, SayHelloRequest, SayHelloResponse} from "./dummy_pb.js"; +import {MethodKind} from "@bufbuild/protobuf"; + +/** + * HelloService is a dummy service that says hello. It is used for reliability + * testing. + * + * @generated from service gitpod.experimental.v1.HelloService + */ +export const HelloService = { + typeName: "gitpod.experimental.v1.HelloService", + methods: { + /** + * Unary RPCs where the client sends a single request to the server and gets a + * single response back, just like a normal function call. + * + * @generated from rpc gitpod.experimental.v1.HelloService.SayHello + */ + sayHello: { + name: "SayHello", + I: SayHelloRequest, + O: SayHelloResponse, + kind: MethodKind.Unary, + }, + /** + * Server streaming RPCs where the client sends a request to the server and + * gets a stream to read a sequence of messages back. + * + * @generated from rpc gitpod.experimental.v1.HelloService.LotsOfReplies + */ + lotsOfReplies: { + name: "LotsOfReplies", + I: LotsOfRepliesRequest, + O: LotsOfRepliesResponse, + kind: MethodKind.ServerStreaming, + }, + } +} as const; diff --git a/components/public-api/typescript/src/gitpod/experimental/v1/dummy_pb.ts b/components/public-api/typescript/src/gitpod/experimental/v1/dummy_pb.ts new file mode 100644 index 00000000000000..7f3f712efc15ab --- /dev/null +++ b/components/public-api/typescript/src/gitpod/experimental/v1/dummy_pb.ts @@ -0,0 +1,173 @@ +/** + * Copyright (c) 2023 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. + */ + +// @generated by protoc-gen-es v0.1.1 with parameter "target=ts" +// @generated from file gitpod/experimental/v1/dummy.proto (package gitpod.experimental.v1, syntax proto3) +/* eslint-disable */ +/* @ts-nocheck */ + +import type {BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage} from "@bufbuild/protobuf"; +import {Message, proto3} from "@bufbuild/protobuf"; + +/** + * @generated from message gitpod.experimental.v1.SayHelloRequest + */ +export class SayHelloRequest extends Message { + /** + * @generated from field: string greeting = 1; + */ + greeting = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime = proto3; + static readonly typeName = "gitpod.experimental.v1.SayHelloRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "greeting", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): SayHelloRequest { + return new SayHelloRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): SayHelloRequest { + return new SayHelloRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): SayHelloRequest { + return new SayHelloRequest().fromJsonString(jsonString, options); + } + + static equals(a: SayHelloRequest | PlainMessage | undefined, b: SayHelloRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(SayHelloRequest, a, b); + } +} + +/** + * @generated from message gitpod.experimental.v1.SayHelloResponse + */ +export class SayHelloResponse extends Message { + /** + * @generated from field: string reply = 1; + */ + reply = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime = proto3; + static readonly typeName = "gitpod.experimental.v1.SayHelloResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "reply", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): SayHelloResponse { + return new SayHelloResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): SayHelloResponse { + return new SayHelloResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): SayHelloResponse { + return new SayHelloResponse().fromJsonString(jsonString, options); + } + + static equals(a: SayHelloResponse | PlainMessage | undefined, b: SayHelloResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(SayHelloResponse, a, b); + } +} + +/** + * @generated from message gitpod.experimental.v1.LotsOfRepliesRequest + */ +export class LotsOfRepliesRequest extends Message { + /** + * @generated from field: string greeting = 1; + */ + greeting = ""; + + /** + * @generated from field: int32 previous_count = 2; + */ + previousCount = 0; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime = proto3; + static readonly typeName = "gitpod.experimental.v1.LotsOfRepliesRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "greeting", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "previous_count", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): LotsOfRepliesRequest { + return new LotsOfRepliesRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): LotsOfRepliesRequest { + return new LotsOfRepliesRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): LotsOfRepliesRequest { + return new LotsOfRepliesRequest().fromJsonString(jsonString, options); + } + + static equals(a: LotsOfRepliesRequest | PlainMessage | undefined, b: LotsOfRepliesRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(LotsOfRepliesRequest, a, b); + } +} + +/** + * @generated from message gitpod.experimental.v1.LotsOfRepliesResponse + */ +export class LotsOfRepliesResponse extends Message { + /** + * @generated from field: string reply = 1; + */ + reply = ""; + + /** + * @generated from field: int32 count = 2; + */ + count = 0; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime = proto3; + static readonly typeName = "gitpod.experimental.v1.LotsOfRepliesResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "reply", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "count", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): LotsOfRepliesResponse { + return new LotsOfRepliesResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): LotsOfRepliesResponse { + return new LotsOfRepliesResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): LotsOfRepliesResponse { + return new LotsOfRepliesResponse().fromJsonString(jsonString, options); + } + + static equals(a: LotsOfRepliesResponse | PlainMessage | undefined, b: LotsOfRepliesResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(LotsOfRepliesResponse, a, b); + } +} diff --git a/components/public-api/typescript/src/metrics.ts b/components/public-api/typescript/src/metrics.ts new file mode 100644 index 00000000000000..e9b3a218373be1 --- /dev/null +++ b/components/public-api/typescript/src/metrics.ts @@ -0,0 +1,411 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * prom-client is node library, we onyl import some types and values + * not default node metrics + */ +import type { + Registry as PromRegistry, + Counter as PromCounter, + Histogram as PromHistorgram, + MetricObjectWithValues, + MetricValue, + MetricValueWithName, +} from "prom-client"; + +const Registry: typeof PromRegistry = require("prom-client/lib/registry"); +const Counter: typeof PromCounter = require("prom-client/lib/counter"); +const Histogram: typeof PromHistorgram = require("prom-client/lib/histogram"); + +import { MethodKind } from "@bufbuild/protobuf"; +import { + StreamResponse, + UnaryResponse, + Code, + ConnectError, + Interceptor, + StreamRequest, + UnaryRequest, +} from "@bufbuild/connect"; + +type GrpcMethodType = "unary" | "client_stream" | "server_stream" | "bidi_stream"; + +interface IGrpcCallMetricsLabels { + service: string; + method: string; + type: GrpcMethodType; +} + +interface IGrpcCallMetricsLabelsWithCode extends IGrpcCallMetricsLabels { + code: string; +} + +const register = new Registry(); + +class PrometheusClientCallMetrics { + readonly startedCounter: PromCounter; + readonly sentCounter: PromCounter; + readonly receivedCounter: PromCounter; + readonly handledCounter: PromCounter; + readonly handledSecondsHistogram: PromHistorgram; + + constructor() { + this.startedCounter = new Counter({ + name: "grpc_client_started_total", + help: "Total number of RPCs started on the client.", + labelNames: ["grpc_service", "grpc_method", "grpc_type"], + registers: [register], + }); + this.sentCounter = new Counter({ + name: "grpc_client_msg_sent_total", + help: " Total number of gRPC stream messages sent by the client.", + labelNames: ["grpc_service", "grpc_method", "grpc_type"], + registers: [register], + }); + this.receivedCounter = new Counter({ + name: "grpc_client_msg_received_total", + help: "Total number of RPC stream messages received by the client.", + labelNames: ["grpc_service", "grpc_method", "grpc_type"], + registers: [register], + }); + this.handledCounter = new Counter({ + name: "grpc_client_handled_total", + help: "Total number of RPCs completed by the client, regardless of success or failure.", + labelNames: ["grpc_service", "grpc_method", "grpc_type", "grpc_code"], + registers: [register], + }); + this.handledSecondsHistogram = new Histogram({ + name: "grpc_client_handling_seconds", + help: "Histogram of response latency (seconds) of the gRPC until it is finished by the application.", + labelNames: ["grpc_service", "grpc_method", "grpc_type", "grpc_code"], + buckets: [0.1, 0.2, 0.5, 1, 2, 5, 10], // it should be aligned with https://github.com/gitpod-io/gitpod/blob/84ed1a0672d91446ba33cb7b504cfada769271a8/install/installer/pkg/components/ide-metrics/configmap.go#L315 + registers: [register], + }); + } + + started(labels: IGrpcCallMetricsLabels): void { + this.startedCounter.inc({ + grpc_service: labels.service, + grpc_method: labels.method, + grpc_type: labels.type, + }); + } + + sent(labels: IGrpcCallMetricsLabels): void { + this.sentCounter.inc({ + grpc_service: labels.service, + grpc_method: labels.method, + grpc_type: labels.type, + }); + } + + received(labels: IGrpcCallMetricsLabels): void { + this.receivedCounter.inc({ + grpc_service: labels.service, + grpc_method: labels.method, + grpc_type: labels.type, + }); + } + + handled(labels: IGrpcCallMetricsLabelsWithCode): void { + this.handledCounter.inc({ + grpc_service: labels.service, + grpc_method: labels.method, + grpc_type: labels.type, + grpc_code: labels.code, + }); + } + + startHandleTimer( + labels: IGrpcCallMetricsLabels, + ): (endLabels?: Partial> | undefined) => number { + const startLabels = { + grpc_service: labels.service, + grpc_method: labels.method, + grpc_type: labels.type, + }; + if (typeof window !== "undefined") { + const start = performance.now(); + return (endLabels) => { + const delta = performance.now() - start; + const value = delta / 1e9; + this.handledSecondsHistogram.labels(Object.assign(startLabels, endLabels)).observe(value); + return value; + }; + } + return this.handledSecondsHistogram.startTimer(startLabels); + } +} + +const GRPCMetrics = new PrometheusClientCallMetrics(); + +export function getMetricsInterceptor(): Interceptor { + const getLabels = (req: UnaryRequest | StreamRequest): IGrpcCallMetricsLabels => { + let type: GrpcMethodType; + switch (req.method.kind) { + case MethodKind.Unary: + type = "unary"; + break; + case MethodKind.ServerStreaming: + type = "server_stream"; + break; + case MethodKind.ClientStreaming: + type = "client_stream"; + break; + case MethodKind.BiDiStreaming: + type = "bidi_stream"; + break; + } + return { + type, + service: req.service.typeName, + method: req.method.name, + }; + }; + + return (next) => async (req) => { + async function* incrementStreamMessagesCounter( + iterable: AsyncIterable, + callback: () => void, + handleMetrics: boolean, + ): AsyncIterable { + let status: Code | undefined; + try { + for await (const item of iterable) { + callback(); + yield item; + } + } catch (e) { + const err = ConnectError.from(e); + status = err.code; + throw e; + } finally { + if (handleMetrics && !settled) { + stopTimer({ grpc_code: status ? Code[status] : "OK" }); + GRPCMetrics.handled({ ...labels, code: status ? Code[status] : "OK" }); + } + } + } + + const labels = getLabels(req); + GRPCMetrics.started(labels); + const stopTimer = GRPCMetrics.startHandleTimer(labels); + + let settled = false; + let status: Code | undefined; + try { + let request: UnaryRequest | StreamRequest; + if (!req.stream) { + request = req; + } else { + request = { + ...req, + message: incrementStreamMessagesCounter( + req.message, + GRPCMetrics.sent.bind(GRPCMetrics, labels), + false, + ), + }; + } + + const res = await next(request); + + let response: UnaryResponse | StreamResponse; + if (!res.stream) { + response = res; + settled = true; + } else { + response = { + ...res, + message: incrementStreamMessagesCounter( + res.message, + GRPCMetrics.received.bind(GRPCMetrics, labels), + true, + ), + }; + } + + return response; + } catch (e) { + settled = true; + const err = ConnectError.from(e); + status = err.code; + throw e; + } finally { + if (settled) { + stopTimer({ grpc_code: status ? Code[status] : "OK" }); + GRPCMetrics.handled({ ...labels, code: status ? Code[status] : "OK" }); + } + } + }; +} + +export class MetricsReporter { + private static readonly REPORT_INTERVAL = 10000; + + private intervalHandler: NodeJS.Timer | undefined; + + private readonly metricsHost: string; + + constructor( + private readonly options: { + gitpodUrl: string; + clientName: string; + clientVersion: string; + logError: typeof console.error; + isEnabled: () => Promise; + }, + ) { + this.metricsHost = `ide.${new URL(options.gitpodUrl).hostname}`; + } + + startReporting() { + if (this.intervalHandler) { + return; + } + this.intervalHandler = setInterval( + () => this.report().catch((e) => this.options.logError("metrics: error while reporting", e)), + MetricsReporter.REPORT_INTERVAL, + ); + } + + stopReporting() { + if (this.intervalHandler) { + clearInterval(this.intervalHandler); + } + } + + private async report() { + const enabled = await this.options.isEnabled(); + if (!enabled) { + return; + } + const metrics = await register.getMetricsAsJSON(); + register.resetMetrics(); + for (const m of metrics) { + if (m.name === "grpc_client_msg_sent_total" || m.name === "grpc_client_msg_received_total") { + // Skip these as thy are filtered by ide metrics + continue; + } + + const type = m.type as unknown as string; + if (type === "counter") { + this.syncReportCounter(m); + } else if (type === "histogram") { + this.syncReportHistogram(m); + } + } + } + + private syncReportCounter(metric: MetricObjectWithValues>) { + for (const { value, labels } of metric.values) { + if (value > 0) { + this.post("metrics/counter/add/" + metric.name, { + name: metric.name, + labels, + value, + }); + } + } + } + + private syncReportHistogram(metric: MetricObjectWithValues>) { + let sum = 0; + let buckets: number[] = []; + for (const { value, labels, metricName } of metric.values) { + if (!metricName) { + continue; + } + // metricName are in the following order _bucket, _sum, _count + // We report on _count as it's the last + // https://github.com/siimon/prom-client/blob/eee34858d2ef4198ff94f56a278d7b81f65e9c63/lib/histogram.js#L222-L235 + if (metricName.endsWith("_bucket")) { + if (labels["le"] !== "+Inf") { + buckets.push(value); + } + } else if (metricName.endsWith("_sum")) { + sum = value; + } else if (metricName.endsWith("_count")) { + if (value > 0) { + this.post("metrics/histogram/add/" + metric.name, { + name: metric.name, + labels, + count: value, + sum, + buckets, + }); + } + sum = 0; + buckets = []; + } + } + } + + reportError( + error: Error, + data?: { + userId?: string; + workspaceId?: string; + instanceId?: string; + [key: string]: string | undefined; + }, + ): void { + this.asyncReportError(error, data); + } + + private async asyncReportError( + error: Error, + data?: { + userId?: string; + workspaceId?: string; + instanceId?: string; + [key: string]: string | undefined; + }, + ): Promise { + const enabled = await this.options.isEnabled(); + if (!enabled) { + return; + } + const properties = { ...data }; + properties["error_name"] = error.name; + properties["error_message"] = error.message; + + delete properties["workspaceId"]; + delete properties["instanceId"]; + delete properties["userId"]; + + await this.post("reportError", { + component: this.options.clientName, + errorStack: error.stack ?? String(error), + version: this.options.clientVersion, + workspaceId: properties["workspaceId"] ?? "", + instanceId: properties["instanceId"] ?? "", + userId: properties["userId"] ?? "", + properties, + }); + } + + private async post(endpoint: string, data: any): Promise { + try { + const response = await fetch(`https://${this.metricsHost}/metrics-api/` + endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Client": this.options.clientName, + "X-Client-Version": this.options.clientVersion, + }, + body: JSON.stringify(data), + credentials: "omit", + }); + + if (!response.ok) { + this.options.logError(`metrics: endpoint responded with ${response.status} ${response.statusText}`); + } + } catch (e) { + this.options.logError("metrics: failed to post", e); + } + } +} diff --git a/components/public-api/typescript/tsconfig.json b/components/public-api/typescript/tsconfig.json index aceb1f2656b4b9..54d59a7b574202 100644 --- a/components/public-api/typescript/tsconfig.json +++ b/components/public-api/typescript/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "lib", "lib": [ "es6", - "esnext.asynciterable" + "esnext.asynciterable", + "DOM" ], "strict": true, "noEmitOnError": false, @@ -26,4 +27,4 @@ "include": [ "src" ] -} \ No newline at end of file +} diff --git a/components/server/package.json b/components/server/package.json index 31d33fc8a3cc93..3df3b0ec61aeef 100644 --- a/components/server/package.json +++ b/components/server/package.json @@ -35,8 +35,7 @@ ], "dependencies": { "@authzed/authzed-node": "^0.12.1", - "@bufbuild/connect": "^0.8.1", - "@bufbuild/connect-express": "^0.8.1", + "@bufbuild/connect-express": "^0.13.0", "@gitbeaker/rest": "^39.12.0", "@gitpod/content-service": "0.1.5", "@gitpod/gitpod-db": "0.1.5", diff --git a/components/server/src/api/dummy.ts b/components/server/src/api/dummy.ts new file mode 100644 index 00000000000000..7c5874ab7c60f0 --- /dev/null +++ b/components/server/src/api/dummy.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2023 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. + */ + +import { HandlerContext, ServiceImpl } from "@bufbuild/connect"; +import { User } from "@gitpod/gitpod-protocol"; +import { HelloService } from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_connectweb"; +import { + LotsOfRepliesRequest, + LotsOfRepliesResponse, + SayHelloRequest, + SayHelloResponse, +} from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_pb"; +import { injectable } from "inversify"; + +@injectable() +export class APIHelloService implements ServiceImpl { + async sayHello(req: SayHelloRequest, context: HandlerContext): Promise { + const response = new SayHelloResponse(); + response.reply = "Hello " + this.getSubject(context); + return response; + } + async *lotsOfReplies(req: LotsOfRepliesRequest, context: HandlerContext): AsyncGenerator { + let count = req.previousCount || 0; + while (true) { + const response = new LotsOfRepliesResponse(); + response.reply = `Hello ${this.getSubject(context)} ${count}`; + response.count = count; + yield response; + count++; + await new Promise((resolve) => setTimeout(resolve, 30000)); + } + } + + private getSubject(context: HandlerContext): string { + return User.getName(context.user) || "World"; + } +} diff --git a/components/server/src/api/handler-context-augmentation.d.ts b/components/server/src/api/handler-context-augmentation.d.ts new file mode 100644 index 00000000000000..f18ff7e446695b --- /dev/null +++ b/components/server/src/api/handler-context-augmentation.d.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2023 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. + */ + +import { User } from "@gitpod/gitpod-protocol"; + +declare module "@bufbuild/connect" { + interface HandlerContext { + user: User; + } +} diff --git a/components/server/src/api/server.ts b/components/server/src/api/server.ts index bbd690ea749f0e..f893695e18814b 100644 --- a/components/server/src/api/server.ts +++ b/components/server/src/api/server.ts @@ -4,41 +4,64 @@ * See License.AGPL.txt in the project root for license information. */ -import * as http from "http"; -import express from "express"; -import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; -import { inject, injectable } from "inversify"; -import { APITeamsService } from "./teams"; -import { APIUserService } from "./user"; -import { ConnectRouter } from "@bufbuild/connect"; +import { Code, ConnectError, ConnectRouter, HandlerContext, ServiceImpl } from "@bufbuild/connect"; import { expressConnectMiddleware } from "@bufbuild/connect-express"; -import { UserService as UserServiceDefinition } from "@gitpod/public-api/lib/gitpod/experimental/v1/user_connectweb"; +import { MethodKind, ServiceType } from "@bufbuild/protobuf"; +import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; +import { HelloService } from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_connectweb"; +import { StatsService } from "@gitpod/public-api/lib/gitpod/experimental/v1/stats_connectweb"; import { TeamsService as TeamsServiceDefinition } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_connectweb"; +import { UserService as UserServiceDefinition } from "@gitpod/public-api/lib/gitpod/experimental/v1/user_connectweb"; import { WorkspacesService as WorkspacesServiceDefinition } from "@gitpod/public-api/lib/gitpod/experimental/v1/workspaces_connectweb"; -import { StatsService } from "@gitpod/public-api/lib/gitpod/experimental/v1/stats_connectweb"; +import express from "express"; +import * as http from "http"; +import { inject, injectable, interfaces } from "inversify"; import { AddressInfo } from "net"; -import { APIWorkspacesService } from "./workspaces"; +import { grpcServerHandled, grpcServerHandling, grpcServerStarted } from "../prometheus-metrics"; +import { SessionHandler } from "../session-handler"; +import { APIHelloService } from "./dummy"; import { APIStatsService } from "./stats"; +import { APITeamsService } from "./teams"; +import { APIUserService } from "./user"; +import { APIWorkspacesService } from "./workspaces"; +import { Deferred } from "@gitpod/gitpod-protocol/lib/util/deferred"; + +function service(type: T, impl: ServiceImpl): [T, ServiceImpl] { + return [type, impl]; +} @injectable() export class API { - @inject(APIUserService) protected readonly apiUserService: APIUserService; - @inject(APITeamsService) protected readonly apiTeamService: APITeamsService; - @inject(APIWorkspacesService) protected readonly apiWorkspacesService: APIWorkspacesService; - @inject(APIStatsService) protected readonly apiStatsService: APIStatsService; + @inject(APIUserService) private readonly apiUserService: APIUserService; + @inject(APITeamsService) private readonly apiTeamService: APITeamsService; + @inject(APIWorkspacesService) private readonly apiWorkspacesService: APIWorkspacesService; + @inject(APIStatsService) private readonly apiStatsService: APIStatsService; + @inject(APIHelloService) private readonly apiHelloService: APIHelloService; + @inject(SessionHandler) private readonly sessionHandler: SessionHandler; - public listen(): http.Server { + listenPrivate(): http.Server { const app = express(); - this.register(app); + this.registerPrivate(app); const server = app.listen(9877, () => { - log.info(`Connect API server listening on port: ${(server.address() as AddressInfo).port}`); + log.info(`Connect Private API server listening on port: ${(server.address() as AddressInfo).port}`); }); return server; } - private register(app: express.Application) { + listen(): http.Server { + const app = express(); + this.register(app); + + const server = app.listen(3001, () => { + log.info(`Connect Public API server listening on port: ${(server.address() as AddressInfo).port}`); + }); + + return server; + } + + private registerPrivate(app: express.Application) { app.use( expressConnectMiddleware({ routes: (router: ConnectRouter) => { @@ -50,4 +73,120 @@ export class API { }), ); } + + private register(app: express.Application) { + app.use( + expressConnectMiddleware({ + routes: (router: ConnectRouter) => { + for (const [type, impl] of [service(HelloService, this.apiHelloService)]) { + router.service(type, new Proxy(impl, this.interceptService(type))); + } + }, + }), + ); + // TODO(al) cover unhandled cases + } + + /** + * intercept handles cross-cutting concerns for all calls: + * - authentication + * - server-side observability + * TODO(ak): + * - rate limitting + * - logging context + * - tracing + */ + private interceptService(type: T): ProxyHandler> { + const grpc_service = type.typeName; + const self = this; + return { + get(target, prop) { + return (...args: any[]) => { + const method = type.methods[prop as any]; + if (!method) { + // Increment metrics for unknown method attempts + console.warn("public api: unknown method", grpc_service, prop); + const code = Code.Unimplemented; + grpcServerStarted.labels(grpc_service, "unknown", "unknown").inc(); + grpcServerHandled.labels(grpc_service, "unknown", "unknown", Code[code]).inc(); + grpcServerHandling.labels(grpc_service, "unknown", "unknown", Code[code]).observe(0); + throw new ConnectError("unimplemented", code); + } + const grpc_method = method.name; + let grpc_type = "unknown"; + if (method.kind === MethodKind.Unary) { + grpc_type = "unary"; + } else if (method.kind === MethodKind.ServerStreaming) { + grpc_type = "server_stream"; + } else if (method.kind === MethodKind.ClientStreaming) { + grpc_type = "client_stream"; + } else if (method.kind === MethodKind.BiDiStreaming) { + grpc_type = "bidi_stream"; + } + + grpcServerStarted.labels(grpc_service, grpc_method, grpc_type).inc(); + const stopTimer = grpcServerHandling.startTimer({ grpc_service, grpc_method, grpc_type }); + const deferred = new Deferred(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + deferred.promise.then((err) => { + const grpc_code = err ? Code[err.code] : "OK"; + grpcServerHandled.labels(grpc_service, grpc_method, grpc_type, grpc_code).inc(); + stopTimer({ grpc_code }); + }); + + const context = args[1] as HandlerContext; + async function call(): Promise { + const user = await self.verify(context); + context.user = user; + + return (target[prop as any] as Function).apply(target, args); + } + if (grpc_type === "unary" || grpc_type === "client_stream") { + return (async () => { + try { + const promise = await call>(); + const result = await promise; + deferred.resolve(undefined); + return result; + } catch (e) { + const err = ConnectError.from(e); + deferred.resolve(e); + throw err; + } + })(); + } + return (async function* () { + try { + const generator = await call>(); + for await (const item of generator) { + yield item; + } + deferred.resolve(undefined); + } catch (e) { + const err = ConnectError.from(e); + deferred.resolve(err); + throw err; + } + })(); + }; + }, + }; + } + + private async verify(context: HandlerContext) { + const user = await this.sessionHandler.verify(context.requestHeader.get("cookie")); + if (!user) { + throw new ConnectError("unauthenticated", Code.Unauthenticated); + } + return user; + } + + static bindAPI(bind: interfaces.Bind): void { + bind(APIHelloService).toSelf().inSingletonScope(); + bind(APIUserService).toSelf().inSingletonScope(); + bind(APITeamsService).toSelf().inSingletonScope(); + bind(APIWorkspacesService).toSelf().inSingletonScope(); + bind(APIStatsService).toSelf().inSingletonScope(); + bind(API).toSelf().inSingletonScope(); + } } diff --git a/components/server/src/api/teams.spec.db.ts b/components/server/src/api/teams.spec.db.ts index f1912caf5af06d..38b6819696f089 100644 --- a/components/server/src/api/teams.spec.db.ts +++ b/components/server/src/api/teams.spec.db.ts @@ -3,27 +3,24 @@ * Licensed under the GNU Affero General Public License (AGPL). * See License.AGPL.txt in the project root for license information. */ -import { suite, test, timeout } from "@testdeck/mocha"; -import { APIUserService } from "./user"; -import { Container } from "inversify"; -import { TeamDB, TypeORM, UserDB, testContainer } from "@gitpod/gitpod-db/lib"; -import { API } from "./server"; -import * as http from "http"; -import { createConnectTransport } from "@bufbuild/connect-node"; import { Code, ConnectError, PromiseClient, createPromiseClient } from "@bufbuild/connect"; -import { AddressInfo } from "net"; +import { createConnectTransport } from "@bufbuild/connect-node"; +import { Timestamp } from "@bufbuild/protobuf"; +import { TeamDB, TypeORM, UserDB, testContainer } from "@gitpod/gitpod-db/lib"; +import { DBTeam } from "@gitpod/gitpod-db/lib/typeorm/entity/db-team"; import { TeamsService as TeamsServiceDefinition } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_connectweb"; -import { UserAuthentication } from "../user/user-authentication"; -import { APITeamsService } from "./teams"; -import { v4 as uuidv4 } from "uuid"; -import * as chai from "chai"; import { GetTeamRequest, Team, TeamMember, TeamRole } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_pb"; -import { DBTeam } from "@gitpod/gitpod-db/lib/typeorm/entity/db-team"; +import { suite, test, timeout } from "@testdeck/mocha"; +import * as chai from "chai"; +import * as http from "http"; +import { Container } from "inversify"; +import { AddressInfo } from "net"; import { Connection } from "typeorm"; -import { Timestamp } from "@bufbuild/protobuf"; -import { APIWorkspacesService } from "./workspaces"; -import { APIStatsService } from "./stats"; +import { v4 as uuidv4 } from "uuid"; +import { UserAuthentication } from "../user/user-authentication"; import { WorkspaceService } from "../workspace/workspace-service"; +import { API } from "./server"; +import { SessionHandler } from "../session-handler"; const expect = chai.expect; @@ -37,14 +34,11 @@ export class APITeamsServiceSpec { async before() { this.container = testContainer.createChild(); - this.container.bind(API).toSelf().inSingletonScope(); - this.container.bind(APIUserService).toSelf().inSingletonScope(); - this.container.bind(APITeamsService).toSelf().inSingletonScope(); - this.container.bind(APIWorkspacesService).toSelf().inSingletonScope(); - this.container.bind(APIStatsService).toSelf().inSingletonScope(); + API.bindAPI(this.container.bind.bind(this.container)); this.container.bind(WorkspaceService).toConstantValue({} as WorkspaceService); this.container.bind(UserAuthentication).toConstantValue({} as UserAuthentication); + this.container.bind(SessionHandler).toConstantValue({} as SessionHandler); // Clean-up database const typeorm = testContainer.get(TypeORM); @@ -52,7 +46,7 @@ export class APITeamsServiceSpec { await this.dbConn.getRepository(DBTeam).delete({}); // Start an actual server for tests - this.server = this.container.get(API).listen(); + this.server = this.container.get(API).listenPrivate(); // Construct a client to point against our server const address = this.server.address() as AddressInfo; diff --git a/components/server/src/container-module.ts b/components/server/src/container-module.ts index 9e34dd09d59223..05f2f2c3183672 100644 --- a/components/server/src/container-module.ts +++ b/components/server/src/container-module.ts @@ -6,6 +6,7 @@ import { ContainerModule } from "inversify"; +import { RedisPublisher, newRedisClient } from "@gitpod/gitpod-db/lib"; import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics"; import { GitpodFileParser } from "@gitpod/gitpod-protocol/lib/gitpod-file-parser"; import { PrometheusClientCallMetrics } from "@gitpod/gitpod-protocol/lib/messaging/client-call-metrics"; @@ -32,13 +33,10 @@ import { WorkspaceManagerClientProviderSource, } from "@gitpod/ws-manager/lib/client-provider-source"; import * as grpc from "@grpc/grpc-js"; +import { Redis } from "ioredis"; import { createChannel, createClient, createClientFactory } from "nice-grpc"; import { retryMiddleware } from "nice-grpc-client-middleware-retry"; import { API } from "./api/server"; -import { APIStatsService } from "./api/stats"; -import { APITeamsService } from "./api/teams"; -import { APIUserService } from "./api/user"; -import { APIWorkspacesService } from "./api/workspaces"; import { AuthProviderParams } from "./auth/auth-provider"; import { AuthProviderService } from "./auth/auth-provider-service"; import { Authenticator } from "./auth/authenticator"; @@ -50,10 +48,14 @@ import { AuthJWT, SignInJWT } from "./auth/jwt"; import { LoginCompletionHandler } from "./auth/login-completion-handler"; import { VerificationService } from "./auth/verification-service"; import { Authorizer, createInitializingAuthorizer } from "./authorization/authorizer"; +import { RelationshipUpdater } from "./authorization/relationship-updater"; +import { RelationshipUpdateJob } from "./authorization/relationship-updater-job"; import { SpiceDBClientProvider, spiceDBConfigFromEnv } from "./authorization/spicedb"; +import { SpiceDBAuthorizer } from "./authorization/spicedb-authorizer"; import { BillingModes } from "./billing/billing-mode"; import { EntitlementService, EntitlementServiceImpl } from "./billing/entitlement-service"; import { EntitlementServiceUBP } from "./billing/entitlement-service-ubp"; +import { StripeService } from "./billing/stripe-service"; import { BitbucketAppSupport } from "./bitbucket/bitbucket-app-support"; import { CodeSyncService } from "./code-sync/code-sync-service"; import { Config, ConfigFile } from "./config"; @@ -71,9 +73,12 @@ import { WebhookEventGarbageCollector } from "./jobs/webhook-gc"; import { WorkspaceGarbageCollector } from "./jobs/workspace-gc"; import { LinkedInService } from "./linkedin-service"; import { LivenessController } from "./liveness/liveness-controller"; +import { RedisSubscriber } from "./messaging/redis-subscriber"; import { MonitoringEndpointsApp } from "./monitoring-endpoints"; import { OAuthController } from "./oauth-server/oauth-controller"; import { OneTimeSecretServer } from "./one-time-secret-server"; +import { OrganizationService } from "./orgs/organization-service"; +import { UsageService } from "./orgs/usage-service"; import { BitbucketApp } from "./prebuilds/bitbucket-app"; import { BitbucketServerApp } from "./prebuilds/bitbucket-server-app"; import { GithubApp } from "./prebuilds/github-app"; @@ -84,20 +89,23 @@ import { IncrementalPrebuildsService } from "./prebuilds/incremental-prebuilds-s import { PrebuildManager } from "./prebuilds/prebuild-manager"; import { PrebuildStatusMaintainer } from "./prebuilds/prebuilt-status-maintainer"; import { ProjectsService } from "./projects/projects-service"; +import { ScmService } from "./projects/scm-service"; import { RedisMutex } from "./redis/mutex"; import { Server } from "./server"; import { SessionHandler } from "./session-handler"; import { ContentServiceStorageClient } from "./storage/content-service-client"; import { StorageClient } from "./storage/storage-client"; import { AuthorizationService, AuthorizationServiceImpl } from "./user/authorization-service"; +import { EnvVarService } from "./user/env-var-service"; +import { GitpodTokenService } from "./user/gitpod-token-service"; import { NewsletterSubscriptionController } from "./user/newsletter-subscription-controller"; -import { StripeService } from "./billing/stripe-service"; +import { SSHKeyService } from "./user/sshkey-service"; import { TokenProvider } from "./user/token-provider"; import { TokenService } from "./user/token-service"; -import { UsageService } from "./orgs/usage-service"; +import { UserAuthentication } from "./user/user-authentication"; import { ServerFactory, UserController } from "./user/user-controller"; import { UserDeletionService } from "./user/user-deletion-service"; -import { UserAuthentication } from "./user/user-authentication"; +import { UserService } from "./user/user-service"; import { contentServiceBinder } from "./util/content-service-sugar"; import { WebsocketConnectionManager } from "./websocket/websocket-connection-manager"; import { ConfigProvider } from "./workspace/config-provider"; @@ -118,21 +126,9 @@ import { SnapshotService } from "./workspace/snapshot-service"; import { WorkspaceClusterImagebuilderClientProvider } from "./workspace/workspace-cluster-imagebuilder-client-provider"; import { WorkspaceDownloadService } from "./workspace/workspace-download-service"; import { WorkspaceFactory } from "./workspace/workspace-factory"; -import { WorkspaceStarter } from "./workspace/workspace-starter"; -import { SpiceDBAuthorizer } from "./authorization/spicedb-authorizer"; -import { OrganizationService } from "./orgs/organization-service"; -import { RedisSubscriber } from "./messaging/redis-subscriber"; -import { Redis } from "ioredis"; -import { RedisPublisher, newRedisClient } from "@gitpod/gitpod-db/lib"; -import { UserService } from "./user/user-service"; -import { RelationshipUpdater } from "./authorization/relationship-updater"; import { WorkspaceService } from "./workspace/workspace-service"; -import { SSHKeyService } from "./user/sshkey-service"; -import { GitpodTokenService } from "./user/gitpod-token-service"; -import { EnvVarService } from "./user/env-var-service"; -import { ScmService } from "./projects/scm-service"; -import { RelationshipUpdateJob } from "./authorization/relationship-updater-job"; import { WorkspaceStartController } from "./workspace/workspace-start-controller"; +import { WorkspaceStarter } from "./workspace/workspace-starter"; export const productionContainerModule = new ContainerModule( (bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation) => { @@ -331,11 +327,7 @@ export const productionContainerModule = new ContainerModule( bind(RelationshipUpdater).toSelf().inSingletonScope(); // grpc / Connect API - bind(APIUserService).toSelf().inSingletonScope(); - bind(APITeamsService).toSelf().inSingletonScope(); - bind(APIWorkspacesService).toSelf().inSingletonScope(); - bind(APIStatsService).toSelf().inSingletonScope(); - bind(API).toSelf().inSingletonScope(); + API.bindAPI(bind); bind(AuthJWT).toSelf().inSingletonScope(); bind(SignInJWT).toSelf().inSingletonScope(); diff --git a/components/server/src/prometheus-metrics.ts b/components/server/src/prometheus-metrics.ts index bf96ea8415a8a9..cbd82941aefdc8 100644 --- a/components/server/src/prometheus-metrics.ts +++ b/components/server/src/prometheus-metrics.ts @@ -34,8 +34,26 @@ export function registerServerMetrics(registry: prometheusClient.Registry) { registry.registerMetric(updateSubscribersRegistered); registry.registerMetric(dbConnectionsTotal); registry.registerMetric(dbConnectionsFree); + registry.registerMetric(grpcServerStarted); + registry.registerMetric(grpcServerHandling); } +export const grpcServerStarted = new prometheusClient.Counter({ + name: "grpc_server_started_total", + help: "Total number of RPCs started on the server.", + labelNames: ["grpc_service", "grpc_method", "grpc_type"], +}); +export const grpcServerHandled = new prometheusClient.Counter({ + name: "grpc_server_handled_total", + help: "Total number of RPCs completed on the server, regardless of success or failure.", + labelNames: ["grpc_service", "grpc_method", "grpc_type", "grpc_code"], +}); +export const grpcServerHandling = new prometheusClient.Histogram({ + name: "grpc_server_handling_seconds", + help: "Histogram of response latency (seconds) of gRPC that had been application-level handled by the server.", + labelNames: ["grpc_service", "grpc_method", "grpc_type", "grpc_code"], +}); + export const dbConnectionsTotal = new prometheusClient.Gauge({ name: "gitpod_typeorm_total_connections", help: "Total number of connections in TypeORM pool", diff --git a/components/server/src/server.ts b/components/server/src/server.ts index 7bfeb58c5b9548..bccdef9f530194 100644 --- a/components/server/src/server.ts +++ b/components/server/src/server.ts @@ -56,7 +56,8 @@ export class Server { protected iamSessionApp?: express.Application; protected iamSessionAppServer?: http.Server; - protected apiServer?: http.Server; + protected publicApiServer?: http.Server; + protected privateApiServer?: http.Server; protected readonly eventEmitter = new EventEmitter(); protected app?: express.Application; @@ -346,7 +347,8 @@ export class Server { }); } - this.apiServer = this.api.listen(); + this.publicApiServer = this.api.listen(); + this.privateApiServer = this.api.listenPrivate(); this.debugApp.start(); } @@ -376,7 +378,8 @@ export class Server { race(this.stopServer(this.iamSessionAppServer), "stop iamsessionapp"), race(this.stopServer(this.monitoringHttpServer), "stop monitoringapp"), race(this.stopServer(this.httpServer), "stop httpserver"), - race(this.stopServer(this.apiServer), "stop api server"), + race(this.stopServer(this.privateApiServer), "stop private api server"), + race(this.stopServer(this.publicApiServer), "stop public api server"), race((async () => this.disposables.dispose())(), "dispose disposables"), ]); diff --git a/components/server/src/session-handler.ts b/components/server/src/session-handler.ts index 910402e5ff55c0..4bbc1520b4fd04 100644 --- a/components/server/src/session-handler.ts +++ b/components/server/src/session-handler.ts @@ -8,6 +8,7 @@ import express from "express"; import { inject, injectable } from "inversify"; import websocket from "ws"; +import { User } from "@gitpod/gitpod-protocol"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; import { AuthJWT } from "./auth/jwt"; import { Config } from "./config"; @@ -90,38 +91,38 @@ export class SessionHandler { // On failure, the next handler is called and the `req.user` is not set. Some APIs/Websocket RPCs do // not require authentication, and as such we cannot fail the request at this stage. protected async handler(req: express.Request, next: express.NextFunction): Promise { - const cookies = parseCookieHeader(req.headers.cookie || ""); + const user = await this.verify(req.headers.cookie || ""); + if (user) { + // We set the user object on the request to signal the user is authenticated. + // Passport uses the `user` property on the request to determine if the session + // is authenticated. + req.user = user; + } + + next(); + } + + async verify(cookie: string): Promise { + const cookies = parseCookieHeader(cookie); const jwtToken = cookies[this.getJWTCookieName(this.config)]; if (!jwtToken) { log.debug("No JWT session present on request"); - next(); - return; + return undefined; } - try { const claims = await this.authJWT.verify(jwtToken); - log.debug("JWT Session token verified", { - claims, - }); + log.debug("JWT Session token verified", { claims }); const subject = claims.sub; if (!subject) { throw new Error("Subject is missing from JWT session claims"); } - const user = await this.userService.findUserById(subject, subject); - - // We set the user object on the request to signal the user is authenticated. - // Passport uses the `user` property on the request to determine if the session - // is authenticated. - req.user = user; - - // Trigger the next middleware in the chain. - next(); + return await this.userService.findUserById(subject, subject); } catch (err) { log.warn("Failed to authenticate user with JWT Session", err); // Remove the existing cookie, to force the user to re-sing in, and hence refresh it - next(); + return undefined; } } diff --git a/install/installer/pkg/common/constants.go b/install/installer/pkg/common/constants.go index acc14cc997e556..9f3cb482af6e2b 100644 --- a/install/installer/pkg/common/constants.go +++ b/install/installer/pkg/common/constants.go @@ -41,6 +41,7 @@ const ( ServerIAMSessionPort = 9876 ServerInstallationAdminPort = 9000 ServerGRPCAPIPort = 9877 + ServerPublicAPIPort = 3001 SystemNodeCritical = "system-node-critical" PublicApiComponent = "public-api-server" UsageComponent = "usage" diff --git a/install/installer/pkg/components/ide-metrics/configmap.go b/install/installer/pkg/components/ide-metrics/configmap.go index 5d2cca57d330c8..4730e9509238f2 100644 --- a/install/installer/pkg/components/ide-metrics/configmap.go +++ b/install/installer/pkg/components/ide-metrics/configmap.go @@ -202,7 +202,7 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) { }, Client: &config.ClientAllowList{ Name: "metric_client", - AllowValues: []string{"vscode-desktop-extension", "supervisor", "unknown"}, + AllowValues: []string{"dashboard", "vscode-desktop-extension", "supervisor", "unknown"}, DefaultValue: "unknown", }, }, { @@ -228,7 +228,7 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) { }, Client: &config.ClientAllowList{ Name: "metric_client", - AllowValues: []string{"vscode-desktop-extension", "supervisor", "unknown"}, + AllowValues: []string{"dashboard", "vscode-desktop-extension", "supervisor", "unknown"}, DefaultValue: "unknown", }, }, @@ -398,7 +398,7 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) { Buckets: []float64{0.1, 0.2, 0.5, 1, 2, 5, 10}, Client: &config.ClientAllowList{ Name: "metric_client", - AllowValues: []string{"vscode-desktop-extension", "supervisor", "unknown"}, + AllowValues: []string{"dashboard", "vscode-desktop-extension", "supervisor", "unknown"}, DefaultValue: "unknown", }, }, { @@ -432,6 +432,7 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) { "gitpod-web", "gitpod-remote-ssh", "vscode-desktop-extension", + "dashboard", }, } diff --git a/install/installer/pkg/components/server/constants.go b/install/installer/pkg/components/server/constants.go index 8a0cdf6ba7da98..8e024ada3ad2f9 100644 --- a/install/installer/pkg/components/server/constants.go +++ b/install/installer/pkg/components/server/constants.go @@ -32,4 +32,7 @@ const ( GRPCAPIName = "grpc" GRPCAPIPort = common.ServerGRPCAPIPort + + PublicAPIName = "public-api" + PublicAPIPort = common.ServerPublicAPIPort ) diff --git a/install/installer/pkg/components/server/deployment.go b/install/installer/pkg/components/server/deployment.go index 48279f77b5cf5d..d9bfd4fd653902 100644 --- a/install/installer/pkg/components/server/deployment.go +++ b/install/installer/pkg/components/server/deployment.go @@ -382,6 +382,9 @@ func deployment(ctx *common.RenderContext) ([]runtime.Object, error) { }, { Name: GRPCAPIName, ContainerPort: GRPCAPIPort, + }, { + Name: PublicAPIName, + ContainerPort: PublicAPIPort, }, }, // todo(sje): do we need to cater for serverContainer.env from values.yaml? diff --git a/install/installer/pkg/components/server/networkpolicy.go b/install/installer/pkg/components/server/networkpolicy.go index 41451d57bb95ee..f8f401dee0d1ba 100644 --- a/install/installer/pkg/components/server/networkpolicy.go +++ b/install/installer/pkg/components/server/networkpolicy.go @@ -36,6 +36,10 @@ func Networkpolicy(ctx *common.RenderContext, component string) ([]runtime.Objec Protocol: common.TCPProtocol, Port: &intstr.IntOrString{IntVal: ContainerPort}, }, + { + Protocol: common.TCPProtocol, + Port: &intstr.IntOrString{IntVal: PublicAPIPort}, + }, }, From: []networkingv1.NetworkPolicyPeer{ { diff --git a/install/installer/pkg/components/server/objects.go b/install/installer/pkg/components/server/objects.go index 214fe315fe13d3..b6a38dbef34349 100644 --- a/install/installer/pkg/components/server/objects.go +++ b/install/installer/pkg/components/server/objects.go @@ -59,6 +59,11 @@ var Objects = common.CompositeRenderFunc( ContainerPort: GRPCAPIPort, ServicePort: GRPCAPIPort, }, + { + Name: PublicAPIName, + ContainerPort: PublicAPIPort, + ServicePort: PublicAPIPort, + }, }), common.DefaultServiceAccount(Component), ) diff --git a/yarn.lock b/yarn.lock index 71003c56714cdd..e16e4ee2bdff8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1277,32 +1277,34 @@ resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@bufbuild/connect-express@^0.8.1": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@bufbuild/connect-express/-/connect-express-0.8.1.tgz#54eb548896fad2488bc9cd16b968713e0799c955" - integrity sha512-DZkPfMYmL1doR8XaeQdwHI0YmKyWz7sG9HaZYEOcBoag+lnlbqXGaakx5gbMnE1OSX7qkSVEqrv7kB/B1tSXOQ== +"@bufbuild/connect-express@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@bufbuild/connect-express/-/connect-express-0.13.0.tgz#fe96fdfd8a5f57ccca03de8637717b93bce62508" + integrity sha512-wXW904jC4CftsCnUarSfLRL+7gkHLoA5qtKSqm3O45N1wyjfq999B+5jeHyLPY+bTOjCxHZxFO6vyDOcYzwK6Q== dependencies: - "@bufbuild/connect" "0.8.1" - "@bufbuild/connect-node" "^0.8.1" + "@bufbuild/connect" "0.13.0" + "@bufbuild/connect-node" "^0.13.0" "@types/express" "^4.17.17" -"@bufbuild/connect-node@^0.8.1": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@bufbuild/connect-node/-/connect-node-0.8.1.tgz#db371506a9c54cac78b0b73b25dd531d86dbe45a" - integrity sha512-yIdXWekNaKDBFVWY6S6L0js6Szh2fhunmVxxCd5taOL4KekO5joIfuA9eLuunTDlp1ie0fPPm7Dc5KlxWgOn0Q== +"@bufbuild/connect-node@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@bufbuild/connect-node/-/connect-node-0.13.0.tgz#09c67da00a684d1152be281b3a0e957f698b1ed2" + integrity sha512-l9tYVWTD9qdVr3PD8iTLmJZoWQY4bYl4UXIyKqvNuqoaAm3pZZvrQXrz0ptS5NRWTVUKPV9DZp6Wbbch5/u55Q== dependencies: - "@bufbuild/connect" "0.8.1" + "@bufbuild/connect" "0.13.0" headers-polyfill "^3.1.2" -"@bufbuild/connect-web@^0.2.1": - version "0.2.1" - resolved "https://registry.yarnpkg.com/@bufbuild/connect-web/-/connect-web-0.2.1.tgz#a7ee2914bf1b77d640fc4ee3c3a89d626f3015fa" - integrity sha512-L580cL9VZCXcjwXMCvIvdFBqdQofVBQcL+jmSis7m8ZxPj5NQ4p7fUhQRTsZMWHkyWINdlZnr7WsHQL0BT7wPQ== +"@bufbuild/connect-web@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@bufbuild/connect-web/-/connect-web-0.13.0.tgz#87301c92d49d3c3f9acb99729c2f7505d739aa4a" + integrity sha512-Ys9VFDWYktD9yFQSLOlkpsD42LonDNMCysLCfjXFuxlupYuf4f7qg0zkT5bESyTfqk4xtRDSSGR3xygaj/ONIQ== + dependencies: + "@bufbuild/connect" "0.13.0" -"@bufbuild/connect@0.8.1", "@bufbuild/connect@^0.8.1": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@bufbuild/connect/-/connect-0.8.1.tgz#71afa90bf56bb833a7f3e2a492e6f9f83c2bebeb" - integrity sha512-cQA0jstYcLknJecTE7KbU4ePNBqiCNviBEcUCbFLve3x+vcSmtoH6jb8z39MeBqFy42ZoWhTGGc3RNCeOx2QUA== +"@bufbuild/connect@0.13.0", "@bufbuild/connect@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@bufbuild/connect/-/connect-0.13.0.tgz#97a84a2cac747c7a52d4421a3382d8d165f61c99" + integrity sha512-eZSMbVLyUFtXiZNORgCEvv580xKZeYQdMOWj2i/nxOcpXQcrEzTMTA7SZzWv4k4gveWCOSRoWmYDeOhfWXJv0g== "@bufbuild/protobuf@0.1.1", "@bufbuild/protobuf@^0.1.1": version "0.1.1"