diff --git a/packages/teleterm/src/services/tshd/createClient.ts b/packages/teleterm/src/services/tshd/createClient.ts index d92c686fe..8e2ce72aa 100644 --- a/packages/teleterm/src/services/tshd/createClient.ts +++ b/packages/teleterm/src/services/tshd/createClient.ts @@ -7,6 +7,7 @@ import Logger from 'teleterm/logger'; import middleware, { withLogging } from './middleware'; import createAbortController from './createAbortController'; +import { createClusterEvenstsStream } from './createClusterEventsStream'; export default function createClient( addr: string, @@ -413,6 +414,11 @@ export default function createClient( }); }); }, + + clusterEvents() { + const stream = tshd.clusterEvents(new api.ClusterEventsRequest()); + return createClusterEvenstsStream(stream); + }, }; return client; diff --git a/packages/teleterm/src/services/tshd/createClusterEventsStream.ts b/packages/teleterm/src/services/tshd/createClusterEventsStream.ts new file mode 100644 index 000000000..96d02e18a --- /dev/null +++ b/packages/teleterm/src/services/tshd/createClusterEventsStream.ts @@ -0,0 +1,64 @@ +import { ClientReadableStream, StatusObject } from '@grpc/grpc-js'; + +import * as api from 'teleterm/services/tshd/v1/service_pb'; + +import * as types from './types'; + +export function createClusterEvenstsStream( + stream: ClientReadableStream +): types.ClusterEventsStream { + return { + onNewGatewayConnectionAccepted( + callback: (event: types.NewGatewayConnectionAccepted) => void + ): void { + stream.addListener('data', (clusterEvent: api.ClusterEvent) => { + if (clusterEvent.hasNewGatewayConnectionAccepted()) { + const event = { + ...clusterEvent.getNewGatewayConnectionAccepted().toObject(), + clusterUri: clusterEvent.getClusterUri(), + }; + callback(event); + } + }); + }, + + /** + * From the docs: + * + * The 'error' event indicates that an error has occurred and the stream has been closed. + * Only one of 'error' or 'end' will be emitted. + * + * https://grpc.io/docs/languages/node/basics/#streaming-rpcs + * + * However, there's an issue in grpc-js where if 'error' is emitted, 'end' is emitted + * afterwards anyway. https://github.com/grpc/grpc-node/issues/1396 + * We should not depend on this behavior. + */ + onError(callback: (error: Error) => void) { + stream.addListener('error', callback); + }, + + /** + * From the docs: + * + * The 'end' event indicates that the server has finished sending and no errors occurred. + * Only one of 'error' or 'end' will be emitted. + * + * https://grpc.io/docs/languages/node/basics/#streaming-rpcs + * + * However, there's an issue in grpc-js where if 'error' is emitted, 'end' is emitted + * afterwards anyway. https://github.com/grpc/grpc-node/issues/1396 + * We should not depend on this behavior. + */ + onEnd(callback: () => void) { + stream.addListener('end', callback); + }, + + /** + * Fired when the server sends the status. + */ + onStatus(callback: (status: StatusObject) => void) { + stream.addListener('status', callback); + }, + }; +} diff --git a/packages/teleterm/src/services/tshd/fixtures/mocks.ts b/packages/teleterm/src/services/tshd/fixtures/mocks.ts index 2bc2281de..71ecece50 100644 --- a/packages/teleterm/src/services/tshd/fixtures/mocks.ts +++ b/packages/teleterm/src/services/tshd/fixtures/mocks.ts @@ -2,6 +2,7 @@ import { Application, AuthSettings, Cluster, + ClusterEventsStream, CreateGatewayParams, Database, Gateway, @@ -55,4 +56,6 @@ export class MockTshClient implements TshClient { abortSignal?: TshAbortSignal ) => Promise; logout: (clusterUri: string) => Promise; + + clusterEvents: () => ClusterEventsStream; } diff --git a/packages/teleterm/src/services/tshd/types.ts b/packages/teleterm/src/services/tshd/types.ts index 90cbee954..41785c0cc 100644 --- a/packages/teleterm/src/services/tshd/types.ts +++ b/packages/teleterm/src/services/tshd/types.ts @@ -1,3 +1,5 @@ +import { StatusObject } from '@grpc/grpc-js'; + import apiCluster from './v1/cluster_pb'; import apiDb from './v1/database_pb'; import apigateway from './v1/gateway_pb'; @@ -7,6 +9,12 @@ import apiApp from './v1/app_pb'; import apiService from './v1/service_pb'; import apiAuthSettings from './v1/auth_settings_pb'; +type RequiredKeys = { + // eslint-disable-next-line @typescript-eslint/ban-types + [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; +}[keyof T]; +type PickRequiredKeys = Pick>; + export type Application = apiApp.App.AsObject; export type Kube = apiKube.Kube.AsObject; export type Server = apiServer.Server.AsObject; @@ -23,6 +31,9 @@ export type GatewayProtocol = | 'redis' | 'sqlserver'; export type Database = apiDb.Database.AsObject; +export type NewGatewayConnectionAccepted = + apiService.NewGatewayConnectionAccepted.AsObject & + PickRequiredKeys; export type Cluster = apiCluster.Cluster.AsObject; export type LoggedInUser = apiCluster.LoggedInUser.AsObject; export type AuthProvider = apiAuthSettings.AuthProvider.AsObject; @@ -88,6 +99,17 @@ export type TshClient = { abortSignal?: TshAbortSignal ) => Promise; logout: (clusterUri: string) => Promise; + + clusterEvents: () => ClusterEventsStream; +}; + +export type ClusterEventsStream = { + onNewGatewayConnectionAccepted( + callback: (event: NewGatewayConnectionAccepted) => void + ): void; + onError(callback: (error: Error) => void): void; + onEnd(callback: () => void): void; + onStatus(callback: (status: StatusObject) => void): void; }; export type TshAbortController = { diff --git a/packages/teleterm/src/services/tshd/v1/service_grpc_pb.d.ts b/packages/teleterm/src/services/tshd/v1/service_grpc_pb.d.ts index 8d15635b2..e7d6d03d1 100644 --- a/packages/teleterm/src/services/tshd/v1/service_grpc_pb.d.ts +++ b/packages/teleterm/src/services/tshd/v1/service_grpc_pb.d.ts @@ -35,6 +35,7 @@ interface ITerminalServiceService extends grpc.ServiceDefinition { @@ -217,6 +218,15 @@ interface ITerminalServiceService_ILogout extends grpc.MethodDefinition; responseDeserialize: grpc.deserialize; } +interface ITerminalServiceService_IClusterEvents extends grpc.MethodDefinition { + path: "/teleport.terminal.v1.TerminalService/ClusterEvents"; + requestStream: false; + responseStream: true; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; +} export const TerminalServiceService: ITerminalServiceService; @@ -241,6 +251,7 @@ export interface ITerminalServiceServer { login: grpc.handleUnaryCall; loginPasswordless: grpc.handleBidiStreamingCall; logout: grpc.handleUnaryCall; + clusterEvents: grpc.handleServerStreamingCall; } export interface ITerminalServiceClient { @@ -304,6 +315,8 @@ export interface ITerminalServiceClient { logout(request: v1_service_pb.LogoutRequest, callback: (error: grpc.ServiceError | null, response: v1_service_pb.EmptyResponse) => void): grpc.ClientUnaryCall; logout(request: v1_service_pb.LogoutRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: v1_service_pb.EmptyResponse) => void): grpc.ClientUnaryCall; logout(request: v1_service_pb.LogoutRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: v1_service_pb.EmptyResponse) => void): grpc.ClientUnaryCall; + clusterEvents(request: v1_service_pb.ClusterEventsRequest, options?: Partial): grpc.ClientReadableStream; + clusterEvents(request: v1_service_pb.ClusterEventsRequest, metadata?: grpc.Metadata, options?: Partial): grpc.ClientReadableStream; } export class TerminalServiceClient extends grpc.Client implements ITerminalServiceClient { @@ -367,4 +380,6 @@ export class TerminalServiceClient extends grpc.Client implements ITerminalServi public logout(request: v1_service_pb.LogoutRequest, callback: (error: grpc.ServiceError | null, response: v1_service_pb.EmptyResponse) => void): grpc.ClientUnaryCall; public logout(request: v1_service_pb.LogoutRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: v1_service_pb.EmptyResponse) => void): grpc.ClientUnaryCall; public logout(request: v1_service_pb.LogoutRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: v1_service_pb.EmptyResponse) => void): grpc.ClientUnaryCall; + public clusterEvents(request: v1_service_pb.ClusterEventsRequest, options?: Partial): grpc.ClientReadableStream; + public clusterEvents(request: v1_service_pb.ClusterEventsRequest, metadata?: grpc.Metadata, options?: Partial): grpc.ClientReadableStream; } diff --git a/packages/teleterm/src/services/tshd/v1/service_grpc_pb.js b/packages/teleterm/src/services/tshd/v1/service_grpc_pb.js index 5c2658121..45c51aa55 100644 --- a/packages/teleterm/src/services/tshd/v1/service_grpc_pb.js +++ b/packages/teleterm/src/services/tshd/v1/service_grpc_pb.js @@ -59,6 +59,28 @@ function deserialize_teleport_terminal_v1_Cluster(buffer_arg) { return v1_cluster_pb.Cluster.deserializeBinary(new Uint8Array(buffer_arg)); } +function serialize_teleport_terminal_v1_ClusterEvent(arg) { + if (!(arg instanceof v1_service_pb.ClusterEvent)) { + throw new Error('Expected argument of type teleport.terminal.v1.ClusterEvent'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_teleport_terminal_v1_ClusterEvent(buffer_arg) { + return v1_service_pb.ClusterEvent.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_teleport_terminal_v1_ClusterEventsRequest(arg) { + if (!(arg instanceof v1_service_pb.ClusterEventsRequest)) { + throw new Error('Expected argument of type teleport.terminal.v1.ClusterEventsRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_teleport_terminal_v1_ClusterEventsRequest(buffer_arg) { + return v1_service_pb.ClusterEventsRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + function serialize_teleport_terminal_v1_CreateGatewayRequest(arg) { if (!(arg instanceof v1_service_pb.CreateGatewayRequest)) { throw new Error('Expected argument of type teleport.terminal.v1.CreateGatewayRequest'); @@ -642,6 +664,18 @@ logout: { responseSerialize: serialize_teleport_terminal_v1_EmptyResponse, responseDeserialize: deserialize_teleport_terminal_v1_EmptyResponse, }, + // TODO: Add comment. +clusterEvents: { + path: '/teleport.terminal.v1.TerminalService/ClusterEvents', + requestStream: false, + responseStream: true, + requestType: v1_service_pb.ClusterEventsRequest, + responseType: v1_service_pb.ClusterEvent, + requestSerialize: serialize_teleport_terminal_v1_ClusterEventsRequest, + requestDeserialize: deserialize_teleport_terminal_v1_ClusterEventsRequest, + responseSerialize: serialize_teleport_terminal_v1_ClusterEvent, + responseDeserialize: deserialize_teleport_terminal_v1_ClusterEvent, + }, }; exports.TerminalServiceClient = grpc.makeGenericClientConstructor(TerminalServiceService); diff --git a/packages/teleterm/src/services/tshd/v1/service_pb.d.ts b/packages/teleterm/src/services/tshd/v1/service_pb.d.ts index 23f8e87e8..dba3ee685 100644 --- a/packages/teleterm/src/services/tshd/v1/service_pb.d.ts +++ b/packages/teleterm/src/services/tshd/v1/service_pb.d.ts @@ -838,6 +838,112 @@ export namespace GetAuthSettingsRequest { } } +export class ClusterEventsRequest extends jspb.Message { + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): ClusterEventsRequest.AsObject; + static toObject(includeInstance: boolean, msg: ClusterEventsRequest): ClusterEventsRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: ClusterEventsRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): ClusterEventsRequest; + static deserializeBinaryFromReader(message: ClusterEventsRequest, reader: jspb.BinaryReader): ClusterEventsRequest; +} + +export namespace ClusterEventsRequest { + export type AsObject = { + } +} + +export class ClusterEvent extends jspb.Message { + getClusterUri(): string; + setClusterUri(value: string): ClusterEvent; + + + hasCertExpired(): boolean; + clearCertExpired(): void; + getCertExpired(): CertExpired | undefined; + setCertExpired(value?: CertExpired): ClusterEvent; + + + hasNewGatewayConnectionAccepted(): boolean; + clearNewGatewayConnectionAccepted(): void; + getNewGatewayConnectionAccepted(): NewGatewayConnectionAccepted | undefined; + setNewGatewayConnectionAccepted(value?: NewGatewayConnectionAccepted): ClusterEvent; + + + getEventCase(): ClusterEvent.EventCase; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): ClusterEvent.AsObject; + static toObject(includeInstance: boolean, msg: ClusterEvent): ClusterEvent.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: ClusterEvent, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): ClusterEvent; + static deserializeBinaryFromReader(message: ClusterEvent, reader: jspb.BinaryReader): ClusterEvent; +} + +export namespace ClusterEvent { + export type AsObject = { + clusterUri: string, + certExpired?: CertExpired.AsObject, + newGatewayConnectionAccepted?: NewGatewayConnectionAccepted.AsObject, + } + + export enum EventCase { + EVENT_NOT_SET = 0, + + CERT_EXPIRED = 2, + + NEW_GATEWAY_CONNECTION_ACCEPTED = 3, + + } + +} + +export class CertExpired extends jspb.Message { + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): CertExpired.AsObject; + static toObject(includeInstance: boolean, msg: CertExpired): CertExpired.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: CertExpired, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): CertExpired; + static deserializeBinaryFromReader(message: CertExpired, reader: jspb.BinaryReader): CertExpired; +} + +export namespace CertExpired { + export type AsObject = { + } +} + +export class NewGatewayConnectionAccepted extends jspb.Message { + getGatewayUri(): string; + setGatewayUri(value: string): NewGatewayConnectionAccepted; + + getTargetUri(): string; + setTargetUri(value: string): NewGatewayConnectionAccepted; + + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): NewGatewayConnectionAccepted.AsObject; + static toObject(includeInstance: boolean, msg: NewGatewayConnectionAccepted): NewGatewayConnectionAccepted.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: NewGatewayConnectionAccepted, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): NewGatewayConnectionAccepted; + static deserializeBinaryFromReader(message: NewGatewayConnectionAccepted, reader: jspb.BinaryReader): NewGatewayConnectionAccepted; +} + +export namespace NewGatewayConnectionAccepted { + export type AsObject = { + gatewayUri: string, + targetUri: string, + } +} + export class EmptyResponse extends jspb.Message { serializeBinary(): Uint8Array; diff --git a/packages/teleterm/src/services/tshd/v1/service_pb.js b/packages/teleterm/src/services/tshd/v1/service_pb.js index 6a8d8a65f..90752fc1b 100644 --- a/packages/teleterm/src/services/tshd/v1/service_pb.js +++ b/packages/teleterm/src/services/tshd/v1/service_pb.js @@ -30,6 +30,10 @@ goog.object.extend(proto, v1_kube_pb); var v1_server_pb = require('../v1/server_pb.js'); goog.object.extend(proto, v1_server_pb); goog.exportSymbol('proto.teleport.terminal.v1.AddClusterRequest', null, global); +goog.exportSymbol('proto.teleport.terminal.v1.CertExpired', null, global); +goog.exportSymbol('proto.teleport.terminal.v1.ClusterEvent', null, global); +goog.exportSymbol('proto.teleport.terminal.v1.ClusterEvent.EventCase', null, global); +goog.exportSymbol('proto.teleport.terminal.v1.ClusterEventsRequest', null, global); goog.exportSymbol('proto.teleport.terminal.v1.CreateGatewayRequest', null, global); goog.exportSymbol('proto.teleport.terminal.v1.CredentialInfo', null, global); goog.exportSymbol('proto.teleport.terminal.v1.EmptyResponse', null, global); @@ -61,6 +65,7 @@ goog.exportSymbol('proto.teleport.terminal.v1.LoginRequest.LocalParams', null, g goog.exportSymbol('proto.teleport.terminal.v1.LoginRequest.ParamsCase', null, global); goog.exportSymbol('proto.teleport.terminal.v1.LoginRequest.SsoParams', null, global); goog.exportSymbol('proto.teleport.terminal.v1.LogoutRequest', null, global); +goog.exportSymbol('proto.teleport.terminal.v1.NewGatewayConnectionAccepted', null, global); goog.exportSymbol('proto.teleport.terminal.v1.PasswordlessPrompt', null, global); goog.exportSymbol('proto.teleport.terminal.v1.RemoveClusterRequest', null, global); goog.exportSymbol('proto.teleport.terminal.v1.RemoveGatewayRequest', null, global); @@ -781,6 +786,90 @@ if (goog.DEBUG && !COMPILED) { */ proto.teleport.terminal.v1.GetAuthSettingsRequest.displayName = 'proto.teleport.terminal.v1.GetAuthSettingsRequest'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.teleport.terminal.v1.ClusterEventsRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.teleport.terminal.v1.ClusterEventsRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.teleport.terminal.v1.ClusterEventsRequest.displayName = 'proto.teleport.terminal.v1.ClusterEventsRequest'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.teleport.terminal.v1.ClusterEvent = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, proto.teleport.terminal.v1.ClusterEvent.oneofGroups_); +}; +goog.inherits(proto.teleport.terminal.v1.ClusterEvent, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.teleport.terminal.v1.ClusterEvent.displayName = 'proto.teleport.terminal.v1.ClusterEvent'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.teleport.terminal.v1.CertExpired = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.teleport.terminal.v1.CertExpired, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.teleport.terminal.v1.CertExpired.displayName = 'proto.teleport.terminal.v1.CertExpired'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.teleport.terminal.v1.NewGatewayConnectionAccepted = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.teleport.terminal.v1.NewGatewayConnectionAccepted, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.teleport.terminal.v1.NewGatewayConnectionAccepted.displayName = 'proto.teleport.terminal.v1.NewGatewayConnectionAccepted'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -6006,6 +6095,626 @@ proto.teleport.terminal.v1.GetAuthSettingsRequest.prototype.setClusterUri = func +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.teleport.terminal.v1.ClusterEventsRequest.prototype.toObject = function(opt_includeInstance) { + return proto.teleport.terminal.v1.ClusterEventsRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.teleport.terminal.v1.ClusterEventsRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.terminal.v1.ClusterEventsRequest.toObject = function(includeInstance, msg) { + var f, obj = { + + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.teleport.terminal.v1.ClusterEventsRequest} + */ +proto.teleport.terminal.v1.ClusterEventsRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.teleport.terminal.v1.ClusterEventsRequest; + return proto.teleport.terminal.v1.ClusterEventsRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.teleport.terminal.v1.ClusterEventsRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.teleport.terminal.v1.ClusterEventsRequest} + */ +proto.teleport.terminal.v1.ClusterEventsRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.teleport.terminal.v1.ClusterEventsRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.teleport.terminal.v1.ClusterEventsRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.teleport.terminal.v1.ClusterEventsRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.terminal.v1.ClusterEventsRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; +}; + + + +/** + * Oneof group definitions for this message. Each group defines the field + * numbers belonging to that group. When of these fields' value is set, all + * other fields in the group are cleared. During deserialization, if multiple + * fields are encountered for a group, only the last value seen will be kept. + * @private {!Array>} + * @const + */ +proto.teleport.terminal.v1.ClusterEvent.oneofGroups_ = [[2,3]]; + +/** + * @enum {number} + */ +proto.teleport.terminal.v1.ClusterEvent.EventCase = { + EVENT_NOT_SET: 0, + CERT_EXPIRED: 2, + NEW_GATEWAY_CONNECTION_ACCEPTED: 3 +}; + +/** + * @return {proto.teleport.terminal.v1.ClusterEvent.EventCase} + */ +proto.teleport.terminal.v1.ClusterEvent.prototype.getEventCase = function() { + return /** @type {proto.teleport.terminal.v1.ClusterEvent.EventCase} */(jspb.Message.computeOneofCase(this, proto.teleport.terminal.v1.ClusterEvent.oneofGroups_[0])); +}; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.teleport.terminal.v1.ClusterEvent.prototype.toObject = function(opt_includeInstance) { + return proto.teleport.terminal.v1.ClusterEvent.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.teleport.terminal.v1.ClusterEvent} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.terminal.v1.ClusterEvent.toObject = function(includeInstance, msg) { + var f, obj = { + clusterUri: jspb.Message.getFieldWithDefault(msg, 1, ""), + certExpired: (f = msg.getCertExpired()) && proto.teleport.terminal.v1.CertExpired.toObject(includeInstance, f), + newGatewayConnectionAccepted: (f = msg.getNewGatewayConnectionAccepted()) && proto.teleport.terminal.v1.NewGatewayConnectionAccepted.toObject(includeInstance, f) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.teleport.terminal.v1.ClusterEvent} + */ +proto.teleport.terminal.v1.ClusterEvent.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.teleport.terminal.v1.ClusterEvent; + return proto.teleport.terminal.v1.ClusterEvent.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.teleport.terminal.v1.ClusterEvent} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.teleport.terminal.v1.ClusterEvent} + */ +proto.teleport.terminal.v1.ClusterEvent.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setClusterUri(value); + break; + case 2: + var value = new proto.teleport.terminal.v1.CertExpired; + reader.readMessage(value,proto.teleport.terminal.v1.CertExpired.deserializeBinaryFromReader); + msg.setCertExpired(value); + break; + case 3: + var value = new proto.teleport.terminal.v1.NewGatewayConnectionAccepted; + reader.readMessage(value,proto.teleport.terminal.v1.NewGatewayConnectionAccepted.deserializeBinaryFromReader); + msg.setNewGatewayConnectionAccepted(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.teleport.terminal.v1.ClusterEvent.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.teleport.terminal.v1.ClusterEvent.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.teleport.terminal.v1.ClusterEvent} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.terminal.v1.ClusterEvent.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getClusterUri(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getCertExpired(); + if (f != null) { + writer.writeMessage( + 2, + f, + proto.teleport.terminal.v1.CertExpired.serializeBinaryToWriter + ); + } + f = message.getNewGatewayConnectionAccepted(); + if (f != null) { + writer.writeMessage( + 3, + f, + proto.teleport.terminal.v1.NewGatewayConnectionAccepted.serializeBinaryToWriter + ); + } +}; + + +/** + * optional string cluster_uri = 1; + * @return {string} + */ +proto.teleport.terminal.v1.ClusterEvent.prototype.getClusterUri = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.teleport.terminal.v1.ClusterEvent} returns this + */ +proto.teleport.terminal.v1.ClusterEvent.prototype.setClusterUri = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * optional CertExpired cert_expired = 2; + * @return {?proto.teleport.terminal.v1.CertExpired} + */ +proto.teleport.terminal.v1.ClusterEvent.prototype.getCertExpired = function() { + return /** @type{?proto.teleport.terminal.v1.CertExpired} */ ( + jspb.Message.getWrapperField(this, proto.teleport.terminal.v1.CertExpired, 2)); +}; + + +/** + * @param {?proto.teleport.terminal.v1.CertExpired|undefined} value + * @return {!proto.teleport.terminal.v1.ClusterEvent} returns this +*/ +proto.teleport.terminal.v1.ClusterEvent.prototype.setCertExpired = function(value) { + return jspb.Message.setOneofWrapperField(this, 2, proto.teleport.terminal.v1.ClusterEvent.oneofGroups_[0], value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.teleport.terminal.v1.ClusterEvent} returns this + */ +proto.teleport.terminal.v1.ClusterEvent.prototype.clearCertExpired = function() { + return this.setCertExpired(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.teleport.terminal.v1.ClusterEvent.prototype.hasCertExpired = function() { + return jspb.Message.getField(this, 2) != null; +}; + + +/** + * optional NewGatewayConnectionAccepted new_gateway_connection_accepted = 3; + * @return {?proto.teleport.terminal.v1.NewGatewayConnectionAccepted} + */ +proto.teleport.terminal.v1.ClusterEvent.prototype.getNewGatewayConnectionAccepted = function() { + return /** @type{?proto.teleport.terminal.v1.NewGatewayConnectionAccepted} */ ( + jspb.Message.getWrapperField(this, proto.teleport.terminal.v1.NewGatewayConnectionAccepted, 3)); +}; + + +/** + * @param {?proto.teleport.terminal.v1.NewGatewayConnectionAccepted|undefined} value + * @return {!proto.teleport.terminal.v1.ClusterEvent} returns this +*/ +proto.teleport.terminal.v1.ClusterEvent.prototype.setNewGatewayConnectionAccepted = function(value) { + return jspb.Message.setOneofWrapperField(this, 3, proto.teleport.terminal.v1.ClusterEvent.oneofGroups_[0], value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.teleport.terminal.v1.ClusterEvent} returns this + */ +proto.teleport.terminal.v1.ClusterEvent.prototype.clearNewGatewayConnectionAccepted = function() { + return this.setNewGatewayConnectionAccepted(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.teleport.terminal.v1.ClusterEvent.prototype.hasNewGatewayConnectionAccepted = function() { + return jspb.Message.getField(this, 3) != null; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.teleport.terminal.v1.CertExpired.prototype.toObject = function(opt_includeInstance) { + return proto.teleport.terminal.v1.CertExpired.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.teleport.terminal.v1.CertExpired} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.terminal.v1.CertExpired.toObject = function(includeInstance, msg) { + var f, obj = { + + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.teleport.terminal.v1.CertExpired} + */ +proto.teleport.terminal.v1.CertExpired.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.teleport.terminal.v1.CertExpired; + return proto.teleport.terminal.v1.CertExpired.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.teleport.terminal.v1.CertExpired} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.teleport.terminal.v1.CertExpired} + */ +proto.teleport.terminal.v1.CertExpired.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.teleport.terminal.v1.CertExpired.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.teleport.terminal.v1.CertExpired.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.teleport.terminal.v1.CertExpired} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.terminal.v1.CertExpired.serializeBinaryToWriter = function(message, writer) { + var f = undefined; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.teleport.terminal.v1.NewGatewayConnectionAccepted.prototype.toObject = function(opt_includeInstance) { + return proto.teleport.terminal.v1.NewGatewayConnectionAccepted.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.teleport.terminal.v1.NewGatewayConnectionAccepted} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.terminal.v1.NewGatewayConnectionAccepted.toObject = function(includeInstance, msg) { + var f, obj = { + gatewayUri: jspb.Message.getFieldWithDefault(msg, 1, ""), + targetUri: jspb.Message.getFieldWithDefault(msg, 2, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.teleport.terminal.v1.NewGatewayConnectionAccepted} + */ +proto.teleport.terminal.v1.NewGatewayConnectionAccepted.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.teleport.terminal.v1.NewGatewayConnectionAccepted; + return proto.teleport.terminal.v1.NewGatewayConnectionAccepted.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.teleport.terminal.v1.NewGatewayConnectionAccepted} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.teleport.terminal.v1.NewGatewayConnectionAccepted} + */ +proto.teleport.terminal.v1.NewGatewayConnectionAccepted.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setGatewayUri(value); + break; + case 2: + var value = /** @type {string} */ (reader.readString()); + msg.setTargetUri(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.teleport.terminal.v1.NewGatewayConnectionAccepted.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.teleport.terminal.v1.NewGatewayConnectionAccepted.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.teleport.terminal.v1.NewGatewayConnectionAccepted} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.terminal.v1.NewGatewayConnectionAccepted.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getGatewayUri(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getTargetUri(); + if (f.length > 0) { + writer.writeString( + 2, + f + ); + } +}; + + +/** + * optional string gateway_uri = 1; + * @return {string} + */ +proto.teleport.terminal.v1.NewGatewayConnectionAccepted.prototype.getGatewayUri = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.teleport.terminal.v1.NewGatewayConnectionAccepted} returns this + */ +proto.teleport.terminal.v1.NewGatewayConnectionAccepted.prototype.setGatewayUri = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * optional string target_uri = 2; + * @return {string} + */ +proto.teleport.terminal.v1.NewGatewayConnectionAccepted.prototype.getTargetUri = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * @param {string} value + * @return {!proto.teleport.terminal.v1.NewGatewayConnectionAccepted} returns this + */ +proto.teleport.terminal.v1.NewGatewayConnectionAccepted.prototype.setTargetUri = function(value) { + return jspb.Message.setProto3StringField(this, 2, value); +}; + + + + + if (jspb.Message.GENERATE_TO_OBJECT) { /** * Creates an object representation of this proto. diff --git a/packages/teleterm/src/ui/appContext.ts b/packages/teleterm/src/ui/appContext.ts index cfc64dd26..7cb0d63c6 100644 --- a/packages/teleterm/src/ui/appContext.ts +++ b/packages/teleterm/src/ui/appContext.ts @@ -83,6 +83,10 @@ export default class AppContext implements IAppContext { } async init(): Promise { + // The stream for cluster events needs to be initialized first, otherwise the code in tshd that + // writes to it will block execution. + // TODO: Expand the comment above. + await this.clustersService.initializeClusterEventsStream(); await this.clustersService.syncRootClusters(); this.workspacesService.restorePersistedState(); } diff --git a/packages/teleterm/src/ui/services/clusters/clustersService.ts b/packages/teleterm/src/ui/services/clusters/clustersService.ts index 68d077005..e4af6d39b 100644 --- a/packages/teleterm/src/ui/services/clusters/clustersService.ts +++ b/packages/teleterm/src/ui/services/clusters/clustersService.ts @@ -1,5 +1,4 @@ import { useStore } from 'shared/libs/stores'; - import isMatch from 'design/utils/match'; import { makeLabelTag } from 'teleport/components/formatters'; import { Label } from 'teleport/types'; @@ -7,8 +6,9 @@ import { Label } from 'teleport/types'; import { routing } from 'teleterm/ui/uri'; import { NotificationsService } from 'teleterm/ui/services/notifications'; import { Cluster } from 'teleterm/services/tshd/types'; - -import { ImmutableStore } from '../immutableStore'; +import { ImmutableStore } from 'teleterm/ui/services/immutableStore'; +import Logger from 'teleterm/logger'; +import { unique } from 'teleterm/ui/utils'; import { AuthSettings, @@ -481,6 +481,46 @@ export class ClustersService extends ImmutableStore { return gateway; } + // TODO: Move this elsewhere, perhaps ClusterEventsService? + async initializeClusterEventsStream() { + const uid = unique(5); + const logger = new Logger(`Cluster events ${uid}`); + + logger.info('Initializing cluster events stream'); + const clusterEventsStream = this.client.clusterEvents(); + + clusterEventsStream.onNewGatewayConnectionAccepted(() => { + logger.info('New connection accepted!'); + }); + + clusterEventsStream.onError((error: Error) => { + // TODO: Add gRPC stream interceptor in lib/teleterm/apiserver so that error codes are + // properly propagated. Then match on AlreadyExists code rather than the error message. + if (error.message.includes('another stream is already active')) { + logger.error('Another cluster events stream is already open!'); + return; + } + + logger.error( + `Restarting cluster events stream because of an error`, + error + ); + + this.initializeClusterEventsStream(); + }); + + clusterEventsStream.onEnd(() => { + logger.info('Cluster events stream has been closed by the server'); + }); + + clusterEventsStream.onStatus(status => { + logger.info( + 'Cluster events stream received status from the server', + status + ); + }); + } + findCluster(clusterUri: string) { return this.state.clusters.get(clusterUri); }