diff --git a/gen/proto/go/teleport/lib/teleterm/v1/gateway.pb.go b/gen/proto/go/teleport/lib/teleterm/v1/gateway.pb.go index b7d3e2bb326b3..b1b500b275006 100644 --- a/gen/proto/go/teleport/lib/teleterm/v1/gateway.pb.go +++ b/gen/proto/go/teleport/lib/teleterm/v1/gateway.pb.go @@ -61,13 +61,14 @@ type Gateway struct { LocalPort string `protobuf:"bytes,6,opt,name=local_port,json=localPort,proto3" json:"local_port,omitempty"` // protocol is the gateway protocol Protocol string `protobuf:"bytes,7,opt,name=protocol,proto3" json:"protocol,omitempty"` - // cli_command is a command that the user can execute to connect to the resource within a CLI, - // if the given resource has a CLI client. + // cli_command represents a command that the user can execute to connect to the resource within + // a CLI, if the given resource has a CLI client. // - // Instead of generating those commands in in the frontend code, the tsh daemon returns them. + // Instead of generating those commands in in the frontend code, they are returned from the tsh + // daemon. // This means that the Database Access team can add support for a new protocol and Teleterm will // support it right away without any changes to Teleterm's code. - CliCommand string `protobuf:"bytes,8,opt,name=cli_command,json=cliCommand,proto3" json:"cli_command,omitempty"` + CliCommand *GatewayCLICommand `protobuf:"bytes,8,opt,name=cli_command,json=cliCommand,proto3" json:"cli_command,omitempty"` // target_subresource_name points at a subresource of the remote resource, for example a // database name on a database server. TargetSubresourceName string `protobuf:"bytes,9,opt,name=target_subresource_name,json=targetSubresourceName,proto3" json:"target_subresource_name,omitempty"` @@ -154,11 +155,11 @@ func (x *Gateway) GetProtocol() string { return "" } -func (x *Gateway) GetCliCommand() string { +func (x *Gateway) GetCliCommand() *GatewayCLICommand { if x != nil { return x.CliCommand } - return "" + return nil } func (x *Gateway) GetTargetSubresourceName() string { @@ -168,6 +169,84 @@ func (x *Gateway) GetTargetSubresourceName() string { return "" } +// GatewayCLICommand represents a command that the user can execute to connect to the gateway +// resource. It is a direct translation of os.exec.Cmd. +type GatewayCLICommand struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + Args []string `protobuf:"bytes,2,rep,name=args,proto3" json:"args,omitempty"` + Env []string `protobuf:"bytes,3,rep,name=env,proto3" json:"env,omitempty"` + // preview is used to show the user what command will be executed before they decide to run it. + // It's like os.exec.Cmd.String with two exceptions: + // + // 1) It is prepended with Cmd.Env. + // 2) The command name is relative and not absolute. + Preview string `protobuf:"bytes,4,opt,name=preview,proto3" json:"preview,omitempty"` +} + +func (x *GatewayCLICommand) Reset() { + *x = GatewayCLICommand{} + if protoimpl.UnsafeEnabled { + mi := &file_teleport_lib_teleterm_v1_gateway_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GatewayCLICommand) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GatewayCLICommand) ProtoMessage() {} + +func (x *GatewayCLICommand) ProtoReflect() protoreflect.Message { + mi := &file_teleport_lib_teleterm_v1_gateway_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 GatewayCLICommand.ProtoReflect.Descriptor instead. +func (*GatewayCLICommand) Descriptor() ([]byte, []int) { + return file_teleport_lib_teleterm_v1_gateway_proto_rawDescGZIP(), []int{1} +} + +func (x *GatewayCLICommand) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *GatewayCLICommand) GetArgs() []string { + if x != nil { + return x.Args + } + return nil +} + +func (x *GatewayCLICommand) GetEnv() []string { + if x != nil { + return x.Env + } + return nil +} + +func (x *GatewayCLICommand) GetPreview() string { + if x != nil { + return x.Preview + } + return "" +} + var File_teleport_lib_teleterm_v1_gateway_proto protoreflect.FileDescriptor var file_teleport_lib_teleterm_v1_gateway_proto_rawDesc = []byte{ @@ -175,7 +254,7 @@ var file_teleport_lib_teleterm_v1_gateway_proto_rawDesc = []byte{ 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2f, 0x76, 0x31, 0x2f, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x18, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, - 0x76, 0x31, 0x22, 0xb5, 0x02, 0x0a, 0x07, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x10, + 0x76, 0x31, 0x22, 0xe2, 0x02, 0x0a, 0x07, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4e, 0x61, 0x6d, @@ -189,18 +268,27 @@ var file_teleport_lib_teleterm_v1_gateway_proto_rawDesc = []byte{ 0x70, 0x6f, 0x72, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, - 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6c, 0x69, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6c, 0x69, 0x43, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x12, 0x36, 0x0a, 0x17, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x75, 0x62, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x15, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x75, 0x62, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x42, 0x54, 0x5a, 0x52, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x76, 0x69, 0x74, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, - 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x74, 0x65, 0x6c, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x6c, 0x69, 0x62, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x72, 0x6d, 0x2f, 0x76, 0x31, 0x3b, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x76, 0x31, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6c, 0x12, 0x4c, 0x0a, 0x0b, 0x63, 0x6c, 0x69, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, + 0x31, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x43, 0x4c, 0x49, 0x43, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x52, 0x0a, 0x63, 0x6c, 0x69, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, + 0x36, 0x0a, 0x17, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x75, 0x62, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x15, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x75, 0x62, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x67, 0x0a, 0x11, 0x47, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x43, 0x4c, 0x49, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, + 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, + 0x12, 0x12, 0x0a, 0x04, 0x61, 0x72, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, + 0x61, 0x72, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, + 0x77, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, + 0x42, 0x54, 0x5a, 0x52, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, + 0x72, 0x61, 0x76, 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, + 0x67, 0x6f, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x6c, 0x69, 0x62, 0x2f, + 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2f, 0x76, 0x31, 0x3b, 0x74, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x72, 0x6d, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -215,16 +303,18 @@ func file_teleport_lib_teleterm_v1_gateway_proto_rawDescGZIP() []byte { return file_teleport_lib_teleterm_v1_gateway_proto_rawDescData } -var file_teleport_lib_teleterm_v1_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_teleport_lib_teleterm_v1_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_teleport_lib_teleterm_v1_gateway_proto_goTypes = []interface{}{ - (*Gateway)(nil), // 0: teleport.lib.teleterm.v1.Gateway + (*Gateway)(nil), // 0: teleport.lib.teleterm.v1.Gateway + (*GatewayCLICommand)(nil), // 1: teleport.lib.teleterm.v1.GatewayCLICommand } var file_teleport_lib_teleterm_v1_gateway_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] 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 + 1, // 0: teleport.lib.teleterm.v1.Gateway.cli_command:type_name -> teleport.lib.teleterm.v1.GatewayCLICommand + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name } func init() { file_teleport_lib_teleterm_v1_gateway_proto_init() } @@ -245,6 +335,18 @@ func file_teleport_lib_teleterm_v1_gateway_proto_init() { return nil } } + file_teleport_lib_teleterm_v1_gateway_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GatewayCLICommand); 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{ @@ -252,7 +354,7 @@ func file_teleport_lib_teleterm_v1_gateway_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_teleport_lib_teleterm_v1_gateway_proto_rawDesc, NumEnums: 0, - NumMessages: 1, + NumMessages: 2, NumExtensions: 0, NumServices: 0, }, diff --git a/gen/proto/js/teleport/lib/teleterm/v1/gateway_pb.d.ts b/gen/proto/js/teleport/lib/teleterm/v1/gateway_pb.d.ts index 5f23a862b2c84..2498a30b5924d 100644 --- a/gen/proto/js/teleport/lib/teleterm/v1/gateway_pb.d.ts +++ b/gen/proto/js/teleport/lib/teleterm/v1/gateway_pb.d.ts @@ -28,8 +28,11 @@ export class Gateway extends jspb.Message { getProtocol(): string; setProtocol(value: string): Gateway; - getCliCommand(): string; - setCliCommand(value: string): Gateway; + + hasCliCommand(): boolean; + clearCliCommand(): void; + getCliCommand(): GatewayCLICommand | undefined; + setCliCommand(value?: GatewayCLICommand): Gateway; getTargetSubresourceName(): string; setTargetSubresourceName(value: string): Gateway; @@ -54,7 +57,44 @@ export namespace Gateway { localAddress: string, localPort: string, protocol: string, - cliCommand: string, + cliCommand?: GatewayCLICommand.AsObject, targetSubresourceName: string, } } + +export class GatewayCLICommand extends jspb.Message { + getPath(): string; + setPath(value: string): GatewayCLICommand; + + clearArgsList(): void; + getArgsList(): Array; + setArgsList(value: Array): GatewayCLICommand; + addArgs(value: string, index?: number): string; + + clearEnvList(): void; + getEnvList(): Array; + setEnvList(value: Array): GatewayCLICommand; + addEnv(value: string, index?: number): string; + + getPreview(): string; + setPreview(value: string): GatewayCLICommand; + + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): GatewayCLICommand.AsObject; + static toObject(includeInstance: boolean, msg: GatewayCLICommand): GatewayCLICommand.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: GatewayCLICommand, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): GatewayCLICommand; + static deserializeBinaryFromReader(message: GatewayCLICommand, reader: jspb.BinaryReader): GatewayCLICommand; +} + +export namespace GatewayCLICommand { + export type AsObject = { + path: string, + argsList: Array, + envList: Array, + preview: string, + } +} diff --git a/gen/proto/js/teleport/lib/teleterm/v1/gateway_pb.js b/gen/proto/js/teleport/lib/teleterm/v1/gateway_pb.js index 23b1276794850..6bfc0ac9f858a 100644 --- a/gen/proto/js/teleport/lib/teleterm/v1/gateway_pb.js +++ b/gen/proto/js/teleport/lib/teleterm/v1/gateway_pb.js @@ -16,6 +16,7 @@ var goog = jspb; var global = (function() { return this || window || global || self || Function('return this')(); }).call(null); goog.exportSymbol('proto.teleport.lib.teleterm.v1.Gateway', null, global); +goog.exportSymbol('proto.teleport.lib.teleterm.v1.GatewayCLICommand', null, global); /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -37,6 +38,27 @@ if (goog.DEBUG && !COMPILED) { */ proto.teleport.lib.teleterm.v1.Gateway.displayName = 'proto.teleport.lib.teleterm.v1.Gateway'; } +/** + * 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.lib.teleterm.v1.GatewayCLICommand = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, proto.teleport.lib.teleterm.v1.GatewayCLICommand.repeatedFields_, null); +}; +goog.inherits(proto.teleport.lib.teleterm.v1.GatewayCLICommand, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.teleport.lib.teleterm.v1.GatewayCLICommand.displayName = 'proto.teleport.lib.teleterm.v1.GatewayCLICommand'; +} @@ -76,7 +98,7 @@ proto.teleport.lib.teleterm.v1.Gateway.toObject = function(includeInstance, msg) localAddress: jspb.Message.getFieldWithDefault(msg, 5, ""), localPort: jspb.Message.getFieldWithDefault(msg, 6, ""), protocol: jspb.Message.getFieldWithDefault(msg, 7, ""), - cliCommand: jspb.Message.getFieldWithDefault(msg, 8, ""), + cliCommand: (f = msg.getCliCommand()) && proto.teleport.lib.teleterm.v1.GatewayCLICommand.toObject(includeInstance, f), targetSubresourceName: jspb.Message.getFieldWithDefault(msg, 9, "") }; @@ -143,7 +165,8 @@ proto.teleport.lib.teleterm.v1.Gateway.deserializeBinaryFromReader = function(ms msg.setProtocol(value); break; case 8: - var value = /** @type {string} */ (reader.readString()); + var value = new proto.teleport.lib.teleterm.v1.GatewayCLICommand; + reader.readMessage(value,proto.teleport.lib.teleterm.v1.GatewayCLICommand.deserializeBinaryFromReader); msg.setCliCommand(value); break; case 9: @@ -229,10 +252,11 @@ proto.teleport.lib.teleterm.v1.Gateway.serializeBinaryToWriter = function(messag ); } f = message.getCliCommand(); - if (f.length > 0) { - writer.writeString( + if (f != null) { + writer.writeMessage( 8, - f + f, + proto.teleport.lib.teleterm.v1.GatewayCLICommand.serializeBinaryToWriter ); } f = message.getTargetSubresourceName(); @@ -372,20 +396,39 @@ proto.teleport.lib.teleterm.v1.Gateway.prototype.setProtocol = function(value) { /** - * optional string cli_command = 8; - * @return {string} + * optional GatewayCLICommand cli_command = 8; + * @return {?proto.teleport.lib.teleterm.v1.GatewayCLICommand} */ proto.teleport.lib.teleterm.v1.Gateway.prototype.getCliCommand = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 8, "")); + return /** @type{?proto.teleport.lib.teleterm.v1.GatewayCLICommand} */ ( + jspb.Message.getWrapperField(this, proto.teleport.lib.teleterm.v1.GatewayCLICommand, 8)); }; /** - * @param {string} value + * @param {?proto.teleport.lib.teleterm.v1.GatewayCLICommand|undefined} value * @return {!proto.teleport.lib.teleterm.v1.Gateway} returns this - */ +*/ proto.teleport.lib.teleterm.v1.Gateway.prototype.setCliCommand = function(value) { - return jspb.Message.setProto3StringField(this, 8, value); + return jspb.Message.setWrapperField(this, 8, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.teleport.lib.teleterm.v1.Gateway} returns this + */ +proto.teleport.lib.teleterm.v1.Gateway.prototype.clearCliCommand = function() { + return this.setCliCommand(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.teleport.lib.teleterm.v1.Gateway.prototype.hasCliCommand = function() { + return jspb.Message.getField(this, 8) != null; }; @@ -407,4 +450,269 @@ proto.teleport.lib.teleterm.v1.Gateway.prototype.setTargetSubresourceName = func }; + +/** + * List of repeated fields within this message type. + * @private {!Array} + * @const + */ +proto.teleport.lib.teleterm.v1.GatewayCLICommand.repeatedFields_ = [2,3]; + + + +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.lib.teleterm.v1.GatewayCLICommand.prototype.toObject = function(opt_includeInstance) { + return proto.teleport.lib.teleterm.v1.GatewayCLICommand.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.lib.teleterm.v1.GatewayCLICommand} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.lib.teleterm.v1.GatewayCLICommand.toObject = function(includeInstance, msg) { + var f, obj = { + path: jspb.Message.getFieldWithDefault(msg, 1, ""), + argsList: (f = jspb.Message.getRepeatedField(msg, 2)) == null ? undefined : f, + envList: (f = jspb.Message.getRepeatedField(msg, 3)) == null ? undefined : f, + preview: jspb.Message.getFieldWithDefault(msg, 4, "") + }; + + 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.lib.teleterm.v1.GatewayCLICommand} + */ +proto.teleport.lib.teleterm.v1.GatewayCLICommand.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.teleport.lib.teleterm.v1.GatewayCLICommand; + return proto.teleport.lib.teleterm.v1.GatewayCLICommand.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.teleport.lib.teleterm.v1.GatewayCLICommand} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.teleport.lib.teleterm.v1.GatewayCLICommand} + */ +proto.teleport.lib.teleterm.v1.GatewayCLICommand.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.setPath(value); + break; + case 2: + var value = /** @type {string} */ (reader.readString()); + msg.addArgs(value); + break; + case 3: + var value = /** @type {string} */ (reader.readString()); + msg.addEnv(value); + break; + case 4: + var value = /** @type {string} */ (reader.readString()); + msg.setPreview(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.teleport.lib.teleterm.v1.GatewayCLICommand.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.teleport.lib.teleterm.v1.GatewayCLICommand.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.lib.teleterm.v1.GatewayCLICommand} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.lib.teleterm.v1.GatewayCLICommand.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getPath(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getArgsList(); + if (f.length > 0) { + writer.writeRepeatedString( + 2, + f + ); + } + f = message.getEnvList(); + if (f.length > 0) { + writer.writeRepeatedString( + 3, + f + ); + } + f = message.getPreview(); + if (f.length > 0) { + writer.writeString( + 4, + f + ); + } +}; + + +/** + * optional string path = 1; + * @return {string} + */ +proto.teleport.lib.teleterm.v1.GatewayCLICommand.prototype.getPath = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.teleport.lib.teleterm.v1.GatewayCLICommand} returns this + */ +proto.teleport.lib.teleterm.v1.GatewayCLICommand.prototype.setPath = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * repeated string args = 2; + * @return {!Array} + */ +proto.teleport.lib.teleterm.v1.GatewayCLICommand.prototype.getArgsList = function() { + return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 2)); +}; + + +/** + * @param {!Array} value + * @return {!proto.teleport.lib.teleterm.v1.GatewayCLICommand} returns this + */ +proto.teleport.lib.teleterm.v1.GatewayCLICommand.prototype.setArgsList = function(value) { + return jspb.Message.setField(this, 2, value || []); +}; + + +/** + * @param {string} value + * @param {number=} opt_index + * @return {!proto.teleport.lib.teleterm.v1.GatewayCLICommand} returns this + */ +proto.teleport.lib.teleterm.v1.GatewayCLICommand.prototype.addArgs = function(value, opt_index) { + return jspb.Message.addToRepeatedField(this, 2, value, opt_index); +}; + + +/** + * Clears the list making it empty but non-null. + * @return {!proto.teleport.lib.teleterm.v1.GatewayCLICommand} returns this + */ +proto.teleport.lib.teleterm.v1.GatewayCLICommand.prototype.clearArgsList = function() { + return this.setArgsList([]); +}; + + +/** + * repeated string env = 3; + * @return {!Array} + */ +proto.teleport.lib.teleterm.v1.GatewayCLICommand.prototype.getEnvList = function() { + return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 3)); +}; + + +/** + * @param {!Array} value + * @return {!proto.teleport.lib.teleterm.v1.GatewayCLICommand} returns this + */ +proto.teleport.lib.teleterm.v1.GatewayCLICommand.prototype.setEnvList = function(value) { + return jspb.Message.setField(this, 3, value || []); +}; + + +/** + * @param {string} value + * @param {number=} opt_index + * @return {!proto.teleport.lib.teleterm.v1.GatewayCLICommand} returns this + */ +proto.teleport.lib.teleterm.v1.GatewayCLICommand.prototype.addEnv = function(value, opt_index) { + return jspb.Message.addToRepeatedField(this, 3, value, opt_index); +}; + + +/** + * Clears the list making it empty but non-null. + * @return {!proto.teleport.lib.teleterm.v1.GatewayCLICommand} returns this + */ +proto.teleport.lib.teleterm.v1.GatewayCLICommand.prototype.clearEnvList = function() { + return this.setEnvList([]); +}; + + +/** + * optional string preview = 4; + * @return {string} + */ +proto.teleport.lib.teleterm.v1.GatewayCLICommand.prototype.getPreview = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, "")); +}; + + +/** + * @param {string} value + * @return {!proto.teleport.lib.teleterm.v1.GatewayCLICommand} returns this + */ +proto.teleport.lib.teleterm.v1.GatewayCLICommand.prototype.setPreview = function(value) { + return jspb.Message.setProto3StringField(this, 4, value); +}; + + goog.object.extend(exports, proto.teleport.lib.teleterm.v1); diff --git a/lib/teleterm/gateway/gateway.go b/lib/teleterm/gateway/gateway.go index acb10e9f0efcb..4287b916d2af2 100644 --- a/lib/teleterm/gateway/gateway.go +++ b/lib/teleterm/gateway/gateway.go @@ -29,6 +29,7 @@ import ( "github.com/sirupsen/logrus" "github.com/gravitational/teleport/api/utils/keys" + api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" alpn "github.com/gravitational/teleport/lib/srv/alpnproxy" "github.com/gravitational/teleport/lib/teleterm/api/uri" "github.com/gravitational/teleport/lib/tlsca" @@ -215,13 +216,11 @@ func (g *Gateway) LocalPortInt() int { return port } -// CLICommand returns a command which launches a CLI client pointed at the given gateway. -// It needs to return a relative command as it will be executed from a terminal in Connect, so we -// should let user's env resolve the command rather than depending on os.exec. -func (g *Gateway) CLICommand() (string, error) { +// CLICommand returns a command which launches a CLI client pointed at the gateway. +func (g *Gateway) CLICommand() (*api.GatewayCLICommand, error) { cmd, err := g.cfg.CLICommandProvider.GetCommand(g) if err != nil { - return "", trace.Wrap(err) + return nil, trace.Wrap(err) } cmdString := strings.TrimSpace( @@ -229,7 +228,12 @@ func (g *Gateway) CLICommand() (string, error) { strings.Join(cmd.Env, " "), strings.Join(cmd.Args, " "))) - return cmdString, nil + return &api.GatewayCLICommand{ + Path: cmd.Path, + Args: cmd.Args, + Env: cmd.Env, + Preview: cmdString, + }, nil } // RouteToDatabase returns tlsca.RouteToDatabase based on the config of the gateway. diff --git a/lib/teleterm/gateway/gateway_test.go b/lib/teleterm/gateway/gateway_test.go index 0597a949e560b..3419313a9e0e2 100644 --- a/lib/teleterm/gateway/gateway_test.go +++ b/lib/teleterm/gateway/gateway_test.go @@ -33,7 +33,7 @@ import ( "github.com/gravitational/teleport/lib/tlsca" ) -func TestCLICommandReturnsRelativeCommand(t *testing.T) { +func TestCLICommandPreviewReturnsRelativeCommandWithEnv(t *testing.T) { gateway := Gateway{ cfg: &Config{ TargetName: "foo", @@ -47,9 +47,11 @@ func TestCLICommandReturnsRelativeCommand(t *testing.T) { command, err := gateway.CLICommand() require.NoError(t, err) - args := strings.Split(command, " ") - path := args[0] - require.True(t, filepath.IsLocal(path), "Not a local path: %q", path) + args := strings.Split(command.Preview, " ") + env := args[0] + path := args[1] + require.Equal(t, "FOO=bar", env) + require.Equal(t, "postgres", path) } func TestGatewayStart(t *testing.T) { @@ -161,6 +163,7 @@ func (m mockCLICommandProvider) GetCommand(gateway *Gateway) (*exec.Cmd, error) // whether a command like postgres is installed on the system or not. cmd := exec.Command(gateway.Protocol(), arg) cmd.Path = absPath + cmd.Env = []string{"FOO=bar"} return cmd, nil } diff --git a/proto/teleport/lib/teleterm/v1/gateway.proto b/proto/teleport/lib/teleterm/v1/gateway.proto index 9ff4757366c13..cb5373ba9f0f3 100644 --- a/proto/teleport/lib/teleterm/v1/gateway.proto +++ b/proto/teleport/lib/teleterm/v1/gateway.proto @@ -41,14 +41,29 @@ message Gateway { string local_port = 6; // protocol is the gateway protocol string protocol = 7; - // cli_command is a command that the user can execute to connect to the resource within a CLI, - // if the given resource has a CLI client. + // cli_command represents a command that the user can execute to connect to the resource within + // a CLI, if the given resource has a CLI client. // - // Instead of generating those commands in in the frontend code, the tsh daemon returns them. + // Instead of generating those commands in in the frontend code, they are returned from the tsh + // daemon. // This means that the Database Access team can add support for a new protocol and Teleterm will // support it right away without any changes to Teleterm's code. - string cli_command = 8; + GatewayCLICommand cli_command = 8; // target_subresource_name points at a subresource of the remote resource, for example a // database name on a database server. string target_subresource_name = 9; } + +// GatewayCLICommand represents a command that the user can execute to connect to the gateway +// resource. It is a direct translation of os.exec.Cmd. +message GatewayCLICommand { + string path = 1; + repeated string args = 2; + repeated string env = 3; + // preview is used to show the user what command will be executed before they decide to run it. + // It's like os.exec.Cmd.String with two exceptions: + // + // 1) It is prepended with Cmd.Env. + // 2) The command name is relative and not absolute. + string preview = 4; +} diff --git a/web/packages/teleterm/src/mainProcess/fixtures/mocks.ts b/web/packages/teleterm/src/mainProcess/fixtures/mocks.ts index 36be20374ccaa..318aec0ad0c0a 100644 --- a/web/packages/teleterm/src/mainProcess/fixtures/mocks.ts +++ b/web/packages/teleterm/src/mainProcess/fixtures/mocks.ts @@ -33,7 +33,7 @@ export class MockMainProcessClient implements MainProcessClient { } getRuntimeSettings(): RuntimeSettings { - return { ...defaultRuntimeSettings, ...this.runtimeSettings }; + return makeRuntimeSettings(this.runtimeSettings); } getResolvedChildProcessAddresses = () => @@ -70,7 +70,9 @@ export class MockMainProcessClient implements MainProcessClient { } } -const defaultRuntimeSettings = { +export const makeRuntimeSettings = ( + runtimeSettings?: Partial +): RuntimeSettings => ({ platform: 'darwin' as const, dev: true, userDataDir: '', @@ -95,4 +97,5 @@ const defaultRuntimeSettings = { arch: 'arm64', osVersion: '22.2.0', appVersion: '11.1.0', -}; + ...runtimeSettings, +}); diff --git a/web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.test.ts b/web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.test.ts new file mode 100644 index 0000000000000..86a913c39c13a --- /dev/null +++ b/web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.test.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2023 Gravitational, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeRuntimeSettings } from 'teleterm/mainProcess/fixtures/mocks'; + +import { GatewayCliClientCommand } from '../types'; + +import { getPtyProcessOptions } from './buildPtyOptions'; + +describe('getPtyProcessOptions', () => { + describe('pty.gateway-cli-client', () => { + it('merges process env with the env from cmd', () => { + const processEnv = { + processExclusive: 'process', + shared: 'fromProcess', + }; + const cmd: GatewayCliClientCommand = { + kind: 'pty.gateway-cli-client', + path: 'foo', + args: [], + clusterName: 'bar', + proxyHost: 'baz', + env: { + cmdExclusive: 'cmd', + shared: 'fromCmd', + }, + }; + + const { env } = getPtyProcessOptions( + makeRuntimeSettings(), + cmd, + processEnv + ); + + expect(env.processExclusive).toBe('process'); + expect(env.cmdExclusive).toBe('cmd'); + expect(env.shared).toBe('fromCmd'); + }); + }); +}); diff --git a/web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.ts b/web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.ts index df559c99b1900..600a8fb0eff57 100644 --- a/web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.ts +++ b/web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.ts @@ -18,6 +18,7 @@ import path, { delimiter } from 'path'; import { RuntimeSettings } from 'teleterm/mainProcess/types'; import { PtyProcessOptions } from 'teleterm/sharedProcess/ptyHost'; +import { assertUnreachable } from 'teleterm/ui/utils'; import { PtyCommand, @@ -67,13 +68,13 @@ export async function buildPtyOptions( }); } -function getPtyProcessOptions( +export function getPtyProcessOptions( settings: RuntimeSettings, cmd: PtyCommand, env: typeof process.env ): PtyProcessOptions { switch (cmd.kind) { - case 'pty.shell': + case 'pty.shell': { // Teleport Connect bundles a tsh binary, but the user might have one already on their system. // Since we use our own TELEPORT_HOME which might differ in format with the version that the // user has installed, let's prepend our bin directory to PATH. @@ -94,6 +95,7 @@ function getPtyProcessOptions( env, initCommand: cmd.initCommand, }; + } case 'pty.tsh-kube-login': { const isWindows = settings.platform === 'win32'; @@ -120,7 +122,7 @@ function getPtyProcessOptions( }; } - case 'pty.tsh-login': + case 'pty.tsh-login': { const loginHost = cmd.login ? `${cmd.login}@${cmd.serverId}` : cmd.serverId; @@ -135,8 +137,20 @@ function getPtyProcessOptions( ], env, }; + } + + case 'pty.gateway-cli-client': { + // TODO(ravicious): Set argv0 when node-pty adds support for it. + // https://github.com/microsoft/node-pty/issues/472 + return { + path: cmd.path, + args: cmd.args, + env: { ...env, ...cmd.env }, + }; + } + default: - throw Error(`Unknown pty command: ${cmd}`); + assertUnreachable(cmd); } } diff --git a/web/packages/teleterm/src/services/pty/types.ts b/web/packages/teleterm/src/services/pty/types.ts index 91548a793a988..ef261764b88d2 100644 --- a/web/packages/teleterm/src/services/pty/types.ts +++ b/web/packages/teleterm/src/services/pty/types.ts @@ -63,9 +63,29 @@ export type TshKubeLoginCommand = PtyCommandBase & { leafClusterId?: string; }; +export type GatewayCliClientCommand = PtyCommandBase & { + kind: 'pty.gateway-cli-client'; + // path is an absolute path to the CLI client. It is resolved on tshd side by GO's + // os.exec.LookPath. + // + // It cannot be just the command name such as `psql` because Windows fails to resolve the + // command name if it doesn't include the `.exe` suffix. + path: string; + // args is a Node.js-style list of arguments passed to the command, _without_ the command name as + // the first element. + args: string[]; + // env is a record of additional env variables that need to be set for the given CLI client. It + // will be merged into process env before the client is started. + env: Record; +}; + type PtyCommandBase = { proxyHost: string; clusterName: string; }; -export type PtyCommand = ShellCommand | TshLoginCommand | TshKubeLoginCommand; +export type PtyCommand = + | ShellCommand + | TshLoginCommand + | TshKubeLoginCommand + | GatewayCliClientCommand; diff --git a/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts b/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts index b9d138a700f43..4c22e36fd3988 100644 --- a/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts +++ b/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts @@ -98,15 +98,3 @@ export class MockTshClient implements TshClient { transferFile: () => undefined; reportUsageEvent: () => undefined; } - -export const gateway: Gateway = { - uri: '/gateways/gateway1', - targetName: 'postgres', - targetUri: '/clusters/teleport-local/dbs/postgres', - targetUser: 'alice', - targetSubresourceName: '', - localAddress: 'localhost', - localPort: '59116', - protocol: 'postgres', - cliCommand: 'psql postgres://alice@localhost:59116', -}; diff --git a/web/packages/teleterm/src/services/tshd/gateway.test.ts b/web/packages/teleterm/src/services/tshd/gateway.test.ts new file mode 100644 index 0000000000000..686829de83276 --- /dev/null +++ b/web/packages/teleterm/src/services/tshd/gateway.test.ts @@ -0,0 +1,48 @@ +/** + * Copyright 2023 Gravitational, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getCliCommandArgs, getCliCommandEnv } from './gateway'; +import { GatewayCLICommand } from './types'; + +describe('getCliCommandArgs', () => { + it("extracts Node.js-style args from cliCommand's argsList", () => { + const cliCommand = makeCliCommand(); + + const args = getCliCommandArgs(cliCommand); + + expect(args).toEqual([cliCommand.argsList[1]]); + }); +}); + +describe('getCliCommandEnv', () => { + it('converts Go-style env into a record', () => { + const cliCommand = makeCliCommand(); + + const env = getCliCommandEnv(cliCommand); + + expect(env.foo).toBe('bar'); + expect(env.baz).toBe('quux'); + }); +}); + +const makeCliCommand = (): GatewayCLICommand => { + return { + path: '/Users/foo/Applications/psql.app/MacOS/psql', + argsList: ['psql', 'localhost:1337'], + envList: ['foo=bar', 'baz=quux'], + preview: 'foo=bar baz=quux psql localhost:1337', + }; +}; diff --git a/web/packages/teleterm/src/services/tshd/gateway.ts b/web/packages/teleterm/src/services/tshd/gateway.ts new file mode 100644 index 0000000000000..4c54683cac86f --- /dev/null +++ b/web/packages/teleterm/src/services/tshd/gateway.ts @@ -0,0 +1,49 @@ +/** + * Copyright 2023 Gravitational, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { GatewayCLICommand } from './types'; + +/** + * getCliCommandArgs returns a Node.js-compatible array with args. + * + * In Go, os.exec.Cmd.Args includes argv0 as the first element. Node expects the args array to + * include just the args. + */ +export function getCliCommandArgs(cliCommand: GatewayCLICommand): string[] { + const [, ...args] = cliCommand.argsList; + return args; +} + +/** + * getCliCommandArgv0 returns argv0 from the args list, as documented by os.exec.Cmd.Args. + * We are safe to use this as the presentational command name. + */ +export function getCliCommandArgv0(cliCommand: GatewayCLICommand): string { + return cliCommand.argsList[0]; +} + +/** + * getCliCommandEnv converts from os.exec.Cmd.Env format to a record of strings that Node expects. + * + * ['FOO=bar', 'BAZ=quux'] -> { FOO: 'bar', BAZ: 'quux; } + */ +export function getCliCommandEnv( + cliCommand: GatewayCLICommand +): Record { + return Object.fromEntries( + cliCommand.envList.map(nameEqualsValue => nameEqualsValue.split('=')) + ); +} diff --git a/web/packages/teleterm/src/services/tshd/testHelpers.ts b/web/packages/teleterm/src/services/tshd/testHelpers.ts index 82f76c33ee347..dbb27c3c01ce7 100644 --- a/web/packages/teleterm/src/services/tshd/testHelpers.ts +++ b/web/packages/teleterm/src/services/tshd/testHelpers.ts @@ -82,7 +82,12 @@ export const makeGateway = (props: Partial = {}): tsh.Gateway => ({ localAddress: 'localhost', localPort: '1337', protocol: 'postgres', - cliCommand: 'connect-me-to-db-please', + cliCommand: { + path: '/foo/psql', + argsList: ['psql', 'localhost:1337'], + envList: [], + preview: 'psql localhost:1337', + }, targetSubresourceName: 'bar', ...props, }); diff --git a/web/packages/teleterm/src/services/tshd/types.ts b/web/packages/teleterm/src/services/tshd/types.ts index 2cbce08b2bc3c..bf88b96e71c94 100644 --- a/web/packages/teleterm/src/services/tshd/types.ts +++ b/web/packages/teleterm/src/services/tshd/types.ts @@ -47,8 +47,26 @@ export interface Server extends apiServer.Server.AsObject { export interface Gateway extends apiGateway.Gateway.AsObject { uri: uri.GatewayUri; targetUri: uri.DatabaseUri; + // The type of cliCommand was repeated here just to refer to the type with the JSDoc. + cliCommand: GatewayCLICommand; } +/** + * GatewayCLICommand follows the API of os.exec.Cmd from Go. + * https://pkg.go.dev/os/exec#Cmd + * + * @property {string} path - The absolute path to the CLI client of a gateway if the client is + * in PATH. Otherwise, the name of the program we were trying to find. + * @property {string[]} argsList - A list containing the name of the program as the first element + * and the actual args as the other elements. + * @property {string[]} envList – A list of env vars that need to be set for the command + * invocation. The elements of the list are in the format of NAME=value. + * @property {string} preview - A string showing how the invocation of the command would look like + * if the user was to invoke it manually from the terminal. Should not be actually used to execute + * anything in the shell. + */ +export type GatewayCLICommand = apiGateway.GatewayCLICommand.AsObject; + export type AccessRequest = apiAccessRequest.AccessRequest.AsObject; export type ResourceId = apiAccessRequest.ResourceID.AsObject; export type AccessRequestReview = apiAccessRequest.AccessRequestReview.AsObject; diff --git a/web/packages/teleterm/src/sharedProcess/ptyHost/ptyHostService.ts b/web/packages/teleterm/src/sharedProcess/ptyHost/ptyHostService.ts index cc06caa9ce66e..451afd8f4c6e8 100644 --- a/web/packages/teleterm/src/sharedProcess/ptyHost/ptyHostService.ts +++ b/web/packages/teleterm/src/sharedProcess/ptyHost/ptyHostService.ts @@ -33,9 +33,11 @@ export function createPtyHostService(): IPtyHostServer { const ptyId = unique(); try { const ptyProcess = new PtyProcess({ - ...ptyOptions, - ptyId, + path: ptyOptions.path, args: ptyOptions.argsList, + cwd: ptyOptions.cwd, + initCommand: ptyOptions.initCommand, + ptyId, env: call.request.getEnv()?.toJavaScript() as Record, }); ptyProcesses.set(ptyId, ptyProcess); diff --git a/web/packages/teleterm/src/sharedProcess/ptyHost/ptyProcess.ts b/web/packages/teleterm/src/sharedProcess/ptyHost/ptyProcess.ts index 2fbd427a5e7d1..e933c0be9af14 100644 --- a/web/packages/teleterm/src/sharedProcess/ptyHost/ptyProcess.ts +++ b/web/packages/teleterm/src/sharedProcess/ptyHost/ptyProcess.ts @@ -42,11 +42,15 @@ export class PtyProcess extends EventEmitter implements IPtyProcess { constructor(private options: PtyProcessOptions & { ptyId: string }) { super(); this._logger = new Logger( - `PtyProcess (id: ${options.ptyId} ${options.path} ${options.args})` + `PtyProcess (id: ${options.ptyId} ${options.path} ${options.args.join( + ' ' + )})` ); } start(cols: number, rows: number) { + // TODO(ravicious): Set argv0 when node-pty adds support for it. + // https://github.com/microsoft/node-pty/issues/472 this._process = nodePTY.spawn(this.options.path, this.options.args, { cols, rows, diff --git a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx index dfb186affeafc..3f8a51e89b1fa 100644 --- a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx +++ b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx @@ -20,8 +20,7 @@ import { Box } from 'design'; import { Attempt } from 'shared/hooks/useAsync'; import * as types from 'teleterm/ui/services/clusters/types'; - -import { gateway } from 'teleterm/services/tshd/fixtures/mocks'; +import { makeGateway } from 'teleterm/services/tshd/testHelpers'; import { ClusterLoginPresentation, @@ -323,3 +322,14 @@ const TestContainer: React.FC = ({ children }) => ( ); + +const gateway = makeGateway({ + uri: '/gateways/gateway1', + targetName: 'postgres', + targetUri: '/clusters/teleport-local/dbs/postgres', + targetUser: 'alice', + targetSubresourceName: '', + localAddress: 'localhost', + localPort: '59116', + protocol: 'postgres', +}); diff --git a/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx b/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx index 8345990f56c3f..f498acffd8a6b 100644 --- a/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx +++ b/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx @@ -23,7 +23,7 @@ import { makeSuccessAttempt, } from 'shared/hooks/useAsync'; -import { Gateway } from 'teleterm/services/tshd/types'; +import { makeGateway } from 'teleterm/services/tshd/testHelpers'; import { DocumentGateway, DocumentGatewayProps } from './DocumentGateway'; @@ -31,7 +31,7 @@ export default { title: 'Teleterm/DocumentGateway', }; -const gateway: Gateway = { +const gateway = makeGateway({ uri: '/gateways/bar', targetName: 'sales-production', targetUri: '/clusters/bar/dbs/foo', @@ -39,9 +39,9 @@ const gateway: Gateway = { localAddress: 'localhost', localPort: '1337', protocol: 'postgres', - cliCommand: 'connect-me-to-db-please', targetSubresourceName: 'bar', -}; +}); +gateway.cliCommand.preview = 'connect-me-to-db-please'; const onlineDocumentGatewayProps: DocumentGatewayProps = { gateway: gateway, @@ -63,7 +63,7 @@ export function Online() { } export function OnlineWithLongValues() { - const gateway: Gateway = { + const gateway = makeGateway({ uri: '/gateways/bar', targetName: 'sales-production', targetUri: '/clusters/bar/dbs/foo', @@ -72,11 +72,11 @@ export function OnlineWithLongValues() { localAddress: 'localhost', localPort: '13337', protocol: 'postgres', - cliCommand: - 'connect-me-to-db-please-baz-quux-quuz-foo-baz-quux-quuz-foo-baz-quux-quuz-foo', targetSubresourceName: 'foo-bar-baz-quux-quuz-foo-bar-baz-quux-quuz-foo-bar-baz-quux-quuz', - }; + }); + gateway.cliCommand.preview = + 'connect-me-to-db-please-baz-quux-quuz-foo-baz-quux-quuz-foo-baz-quux-quuz-foo'; return ( The database connection is {statusDescription} + {/* TODO(ravicious): Use doc.status instead of LinearProgress. */} {props.connectAttempt.status === 'processing' && } {props.connectAttempt.status === 'error' && ( diff --git a/web/packages/teleterm/src/ui/DocumentGateway/OnlineDocumentGateway.tsx b/web/packages/teleterm/src/ui/DocumentGateway/OnlineDocumentGateway.tsx index 22d39558d477d..935600c7f0d78 100644 --- a/web/packages/teleterm/src/ui/DocumentGateway/OnlineDocumentGateway.tsx +++ b/web/packages/teleterm/src/ui/DocumentGateway/OnlineDocumentGateway.tsx @@ -105,7 +105,7 @@ export function OnlineDocumentGateway(props: OnlineDocumentGatewayProps) { diff --git a/web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.ts b/web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.ts index 4c8f553c32c20..22003f04de633 100644 --- a/web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.ts +++ b/web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.ts @@ -21,8 +21,8 @@ import { useAsync } from 'shared/hooks/useAsync'; import { useAppContext } from 'teleterm/ui/appContextProvider'; import * as types from 'teleterm/ui/services/workspacesService'; import { useWorkspaceContext } from 'teleterm/ui/Documents'; -import { routing } from 'teleterm/ui/uri'; import { retryWithRelogin } from 'teleterm/ui/utils'; +import * as tshdGateway from 'teleterm/services/tshd/gateway'; export function useDocumentGateway(doc: types.DocumentGateway) { const ctx = useAppContext(); @@ -37,7 +37,6 @@ export function useDocumentGateway(doc: types.DocumentGateway) { const defaultPort = doc.port || ''; const gateway = ctx.clustersService.findGateway(doc.gatewayUri); const connected = !!gateway; - const cluster = ctx.clustersService.findClusterByResource(doc.targetUri); const [connectAttempt, createGateway] = useAsync(async (port: string) => { const gw = await retryWithRelogin(ctx, doc.targetUri, () => @@ -92,14 +91,18 @@ export function useDocumentGateway(doc: types.DocumentGateway) { }); const runCliCommand = () => { - const { rootClusterId, leafClusterId } = routing.parseClusterUri( - cluster.uri - ).params; - workspaceDocumentsService.openNewTerminal({ - initCommand: gateway.cliCommand, - rootClusterId, - leafClusterId, + const command = tshdGateway.getCliCommandArgv0(gateway.cliCommand); + const title = `${command} · ${doc.targetUser}@${doc.targetName}`; + + const cliDoc = workspaceDocumentsService.createGatewayCliDocument({ + title, + targetUri: doc.targetUri, + targetUser: doc.targetUser, + targetName: doc.targetName, + targetProtocol: gateway.protocol, }); + workspaceDocumentsService.add(cliDoc); + workspaceDocumentsService.setLocation(cliDoc.uri); }; useEffect( diff --git a/web/packages/teleterm/src/ui/DocumentGatewayCliClient/DocumentGatewayCliClient.story.tsx b/web/packages/teleterm/src/ui/DocumentGatewayCliClient/DocumentGatewayCliClient.story.tsx new file mode 100644 index 0000000000000..c4b65d20bde51 --- /dev/null +++ b/web/packages/teleterm/src/ui/DocumentGatewayCliClient/DocumentGatewayCliClient.story.tsx @@ -0,0 +1,69 @@ +/** + * Copyright 2023 Gravitational, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; + +import { DocumentGatewayCliClient } from 'teleterm/ui/services/workspacesService'; + +import { WaitingForGatewayContent } from './DocumentGatewayCliClient'; + +export default { + title: 'Teleterm/DocumentGatewayCliClient', +}; + +const doc: DocumentGatewayCliClient = { + uri: '/docs/1234', + title: 'psql', + kind: 'doc.gateway_cli_client', + rootClusterId: 'foo', + leafClusterId: 'bar', + status: '', + targetName: 'postgres', + targetProtocol: 'postgres', + targetUri: '/clusters/foo/dbs/elo', + targetUser: 'alice', +}; + +const docWithLongValues = { + ...doc, + targetName: 'sales-quarterly-fq1-2024-production', + targetUser: + 'quux-quuz-foo-bar-quux-quuz-foo-bar-quux-quuz-foo-bar-quux-quuz-foo-bar', +}; + +const noop = () => {}; + +export const Waiting = (props: { doc?: DocumentGatewayCliClient }) => ( + +); + +export const WaitingTimedOut = (props: { doc?: DocumentGatewayCliClient }) => ( + +); + +export const WaitingWithLongValues = () => ; + +export const WaitingTimedOutWithLongValues = () => ( + +); diff --git a/web/packages/teleterm/src/ui/DocumentGatewayCliClient/DocumentGatewayCliClient.tsx b/web/packages/teleterm/src/ui/DocumentGatewayCliClient/DocumentGatewayCliClient.tsx new file mode 100644 index 0000000000000..5e90c7c969fbb --- /dev/null +++ b/web/packages/teleterm/src/ui/DocumentGatewayCliClient/DocumentGatewayCliClient.tsx @@ -0,0 +1,158 @@ +/** + * Copyright 2023 Gravitational, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState, useEffect } from 'react'; +import styled from 'styled-components'; +import { Flex, Text, ButtonPrimary } from 'design'; + +import Document from 'teleterm/ui/Document'; +import * as types from 'teleterm/ui/services/workspacesService'; +import { useAppContext } from 'teleterm/ui/appContextProvider'; +import { DocumentTerminal } from 'teleterm/ui/DocumentTerminal'; +import { useWorkspaceContext } from 'teleterm/ui/Documents'; +import { connectToDatabase } from 'teleterm/ui/services/workspacesService'; + +/** + * DocumentGatewayCliClient creates a terminal session that targets the given gateway. + * + * It waits for the gateway to be created before starting the terminal session. This typically + * happens only during the app restart. We assume that most of the time both the DocumentGateway and + * DocumentGatewayCliClient tabs will be reopened together. In that case, DocumentGatewayCliClient + * will wait for DocumentGateway to create the gateway first before attempting to start the client. + * + * However, if the user closes just the DocumentGateway tab and then restarts the app with just the + * DocumentGatewayCliClient tab present, the gateway will never be created. In that case, the user + * will be able to click "Open the connection" to manually open a new DocumentGateway tab. + */ +export const DocumentGatewayCliClient = (props: { + visible: boolean; + doc: types.DocumentGatewayCliClient; +}) => { + const { clustersService } = useAppContext(); + clustersService.useState(); + + const { doc, visible } = props; + const [hasRenderedTerminal, setHasRenderedTerminal] = useState(false); + + const gateway = clustersService.findGatewayByConnectionParams( + doc.targetUri, + doc.targetUser + ); + + // Once we render the terminal, we want to keep it visible. Otherwise removing the gateway would + // mean that this document would immediately unmount DocumentTerminal and close the PTY. + // + // After the gateway is closed, the CLI client will not be able to interact with the gateway + // target, but the user might still want to inspect the output. + if (gateway || hasRenderedTerminal) { + if (!hasRenderedTerminal) { + setHasRenderedTerminal(true); + } + + return ; + } + + return ; +}; + +const TIMEOUT_SECONDS = 10; + +const WaitingForGateway = (props: { + doc: types.DocumentGatewayCliClient; + visible: boolean; +}) => { + const { doc, visible } = props; + const ctx = useAppContext(); + const { documentsService } = useWorkspaceContext(); + // If we depended on doc.status for hasTimedOut instead of using a separate state, then on reopen + // the doc would have status set to 'connected' on 'error' and it'd be updated from useEffect, + // meaning that there would be a brief flash of old state. + const [hasTimedOut, setHasTimedOut] = useState(false); + + useEffect(() => { + // Update the doc state to make the progress bar show up in the tab bar. + // Once DocumentTerminal is mounted, it is going to update the status to 'connected' or 'error'. + documentsService.update(doc.uri, { status: 'connecting' }); + + const timeoutId = setTimeout(() => { + setHasTimedOut(true); + documentsService.update(doc.uri, { status: 'error' }); + }, TIMEOUT_SECONDS * 1000); + + return () => { + clearTimeout(timeoutId); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const openConnection = () => { + connectToDatabase( + ctx, + { + uri: doc.targetUri, + name: doc.targetName, + protocol: doc.targetProtocol, + dbUser: doc.targetUser, + }, + { origin: 'reopened_session' } + ); + }; + + return ( + + + + ); +}; + +export const WaitingForGatewayContent = ({ + doc, + hasTimedOut, + openConnection, +}: { + doc: types.DocumentGatewayCliClient; + hasTimedOut: boolean; + openConnection: () => void; +}) => ( + + {hasTimedOut ? ( +
+ + A connection to {doc.targetName} as{' '} + {doc.targetUser} has not been opened up within{' '} + {TIMEOUT_SECONDS} seconds. + + Please try to open the connection manually. +
+ ) : ( + + Waiting for a db connection to {doc.targetName} as{' '} + {doc.targetUser} to be opened up. + + )} + + Open the connection +
+); + +const StyledText = styled(Text).attrs({ + typography: 'h5', + textAlign: 'center', +})``; diff --git a/web/packages/teleterm/src/ui/DocumentGatewayCliClient/index.ts b/web/packages/teleterm/src/ui/DocumentGatewayCliClient/index.ts new file mode 100644 index 0000000000000..ead0090098961 --- /dev/null +++ b/web/packages/teleterm/src/ui/DocumentGatewayCliClient/index.ts @@ -0,0 +1,17 @@ +/** + * Copyright 2023 Gravitational, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { DocumentGatewayCliClient } from './DocumentGatewayCliClient'; diff --git a/web/packages/teleterm/src/ui/DocumentTerminal/DocumentTerminal.tsx b/web/packages/teleterm/src/ui/DocumentTerminal/DocumentTerminal.tsx index 3118055258bb5..e594e6cdc2e35 100644 --- a/web/packages/teleterm/src/ui/DocumentTerminal/DocumentTerminal.tsx +++ b/web/packages/teleterm/src/ui/DocumentTerminal/DocumentTerminal.tsx @@ -30,12 +30,15 @@ import { assertUnreachable } from 'teleterm/ui/utils'; import { isDocumentTshNodeWithServerId } from 'teleterm/ui/services/workspacesService'; import { Terminal } from './Terminal'; -import { Props, useDocumentTerminal } from './useDocumentTerminal'; +import { useDocumentTerminal } from './useDocumentTerminal'; import { useTshFileTransferHandlers } from './useTshFileTransferHandlers'; import type * as types from 'teleterm/ui/services/workspacesService'; -export function DocumentTerminal(props: Props & { visible: boolean }) { +export function DocumentTerminal(props: { + doc: types.DocumentTerminal; + visible: boolean; +}) { const ctx = useAppContext(); const { configService } = ctx.mainProcessClient; const { visible, doc } = props; @@ -169,6 +172,7 @@ function getReconnectCopy(doc: types.DocumentTerminal) { buttonText: 'Reconnect', }; } + case 'doc.gateway_cli_client': case 'doc.terminal_shell': case 'doc.terminal_tsh_kube': { return { diff --git a/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts b/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts index 774c67aee21b9..dcdac1db1ba7f 100644 --- a/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts +++ b/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useCallback, useEffect, useRef } from 'react'; +import { useEffect, useRef } from 'react'; import { useAsync } from 'shared/hooks/useAsync'; import { runOnce } from 'shared/utils/highbar'; @@ -31,6 +31,8 @@ import { PtyCommand, PtyProcessCreationStatus } from 'teleterm/services/pty'; import { AmbiguousHostnameError } from 'teleterm/ui/services/resources'; import { retryWithRelogin } from 'teleterm/ui/utils'; import Logger from 'teleterm/logger'; +import { ClustersService } from 'teleterm/ui/services/clusters'; +import * as tshdGateway from 'teleterm/services/tshd/gateway'; import type * as types from 'teleterm/ui/services/workspacesService'; import type * as uri from 'teleterm/ui/uri'; @@ -41,6 +43,10 @@ export function useDocumentTerminal(doc: types.DocumentTerminal) { const ctx = useAppContext(); const { documentsService } = useWorkspaceContext(); const [attempt, startTerminal] = useAsync(async () => { + if ('status' in doc) { + documentsService.update(doc.uri, { status: 'connecting' }); + } + try { return await startTerminalSession( ctx, @@ -69,14 +75,7 @@ export function useDocumentTerminal(doc: types.DocumentTerminal) { }; }, [attempt]); - const reconnect = useCallback(() => { - if ('status' in doc) { - documentsService.update(doc.uri, { status: 'connecting' }); - } - startTerminal(); - }, [documentsService, doc.uri, startTerminal]); - - return { attempt, reconnect }; + return { attempt, reconnect: startTerminal }; } async function startTerminalSession( @@ -219,7 +218,13 @@ async function setUpPtyProcess( leafClusterId: doc.leafClusterId, }); const rootCluster = ctx.clustersService.findRootClusterByResource(clusterUri); - const cmd = createCmd(doc, rootCluster.proxyHost, getClusterName()); + const cmd = createCmd( + ctx.clustersService, + doc, + rootCluster.proxyHost, + getClusterName() + ); + const ptyProcess = await createPtyProcess(ctx, cmd); if (doc.kind === 'doc.terminal_tsh_node') { @@ -314,7 +319,15 @@ async function createPtyProcess( return process; } +// TODO(ravicious): Instead of creating cmd within useDocumentTerminal, make useDocumentTerminal +// accept it as an argument. This will allow components such as DocumentGatewayCliClient contain +// the logic related to their specific use case. +// +// useDocumentTerminal used to assume that the doc contains everything that's needed to create the +// cmd. In case of the gateway CLI client that's not true – the state of ClustersService needs to be +// inspected to get the correct command for the gateway CLI client. function createCmd( + clustersService: ClustersService, doc: types.DocumentTerminal, proxyHost: string, clusterName: string @@ -346,6 +359,39 @@ function createCmd( }; } + if (doc.kind === 'doc.gateway_cli_client') { + const gateway = clustersService.findGatewayByConnectionParams( + doc.targetUri, + doc.targetUser + ); + if (!gateway) { + // This shouldn't happen as DocumentGatewayCliClient doesn't render DocumentTerminal before + // the gateway is found. In any case, if it does happen for some reason, the user will see + // this message and will be able to retry starting the terminal. + throw new Error( + `No gateway found for ${doc.targetUser} on ${doc.targetUri}` + ); + } + + // Below we convert cliCommand fields from Go conventions to Node.js conventions. + const args = tshdGateway.getCliCommandArgs(gateway.cliCommand); + const env = tshdGateway.getCliCommandEnv(gateway.cliCommand); + // We must not use argsList[0] as the path. Windows expects the executable to end with `.exe`, + // so if we passed just `psql` here, we wouldn't be able to start the process. + // + // Instead, let's use the absolute path resolved by Go. + const path = gateway.cliCommand.path; + + return { + kind: 'pty.gateway-cli-client', + path, + args, + env, + proxyHost, + clusterName, + }; + } + return { ...doc, kind: 'pty.shell', @@ -355,8 +401,3 @@ function createCmd( initCommand: doc.initCommand, }; } - -export type Props = { - doc: types.DocumentTerminal; - visible: boolean; -}; diff --git a/web/packages/teleterm/src/ui/Documents/DocumentsRenderer.tsx b/web/packages/teleterm/src/ui/Documents/DocumentsRenderer.tsx index d908c209cab77..ce1c7ba890cf1 100644 --- a/web/packages/teleterm/src/ui/Documents/DocumentsRenderer.tsx +++ b/web/packages/teleterm/src/ui/Documents/DocumentsRenderer.tsx @@ -21,6 +21,8 @@ import styled from 'styled-components'; // @ts-ignore import { DocumentAccessRequests } from 'e-teleterm/ui/DocumentAccessRequests/DocumentAccessRequests'; +import { DocumentGatewayCliClient } from 'teleterm/ui/DocumentGatewayCliClient'; + import { useAppContext } from 'teleterm/ui/appContextProvider'; import * as types from 'teleterm/ui/services/workspacesService'; import { @@ -96,6 +98,8 @@ function MemoizedDocument(props: { doc: types.Document; visible: boolean }) { return ; case 'doc.gateway': return ; + case 'doc.gateway_cli_client': + return ; case 'doc.terminal_shell': case 'doc.terminal_tsh_node': case 'doc.terminal_tsh_kube': diff --git a/web/packages/teleterm/src/ui/Tabs/Tabs.tsx b/web/packages/teleterm/src/ui/Tabs/Tabs.tsx index 6568d97494a8f..5da1631a423c4 100644 --- a/web/packages/teleterm/src/ui/Tabs/Tabs.tsx +++ b/web/packages/teleterm/src/ui/Tabs/Tabs.tsx @@ -87,13 +87,7 @@ export function Tabs(props: Props) { } function getIsLoading(doc: Document): boolean { - switch (doc.kind) { - case 'doc.terminal_tsh_kube': - case 'doc.terminal_tsh_node': - return doc.status === 'connecting'; - default: - return false; - } + return 'status' in doc && doc.status === 'connecting'; } type Props = { diff --git a/web/packages/teleterm/src/ui/services/clusters/clustersService.test.ts b/web/packages/teleterm/src/ui/services/clusters/clustersService.test.ts index b4443f07f0052..a5688824a1268 100644 --- a/web/packages/teleterm/src/ui/services/clusters/clustersService.test.ts +++ b/web/packages/teleterm/src/ui/services/clusters/clustersService.test.ts @@ -17,6 +17,7 @@ import { NotificationsService } from 'teleterm/ui/services/notifications'; import { UsageService } from 'teleterm/ui/services/usage'; import { MainProcessClient } from 'teleterm/mainProcess/types'; +import { makeGateway } from 'teleterm/services/tshd/testHelpers'; import { ClustersService } from './clustersService'; @@ -66,17 +67,10 @@ const leafClusterMock: tsh.Cluster = { }, }; -const gatewayMock: tsh.Gateway = { +const gatewayMock = makeGateway({ uri: '/gateways/gatewayTestUri', - localAddress: 'localhost', - localPort: '2000', - protocol: 'https', - targetName: 'Name', - targetSubresourceName: '', - targetUser: 'sam', targetUri: `${clusterUri}/dbs/databaseTestUri`, - cliCommand: 'psql postgres://postgres@localhost:5432/postgres', -}; +}); const NotificationsServiceMock = NotificationsService as jest.MockedClass< typeof NotificationsService diff --git a/web/packages/teleterm/src/ui/services/clusters/clustersService.ts b/web/packages/teleterm/src/ui/services/clusters/clustersService.ts index 2806a02cfa89a..8fe0fc6735122 100644 --- a/web/packages/teleterm/src/ui/services/clusters/clustersService.ts +++ b/web/packages/teleterm/src/ui/services/clusters/clustersService.ts @@ -26,6 +26,7 @@ import * as uri from 'teleterm/ui/uri'; import { NotificationsService } from 'teleterm/ui/services/notifications'; import { Cluster, + Gateway, CreateAccessRequestParams, GetRequestableRolesParams, ReviewAccessRequestParams, @@ -409,6 +410,25 @@ export class ClustersService extends ImmutableStore return this.state.gateways.get(gatewayUri); } + findGatewayByConnectionParams( + targetUri: uri.DatabaseUri, + targetUser: string + ) { + let found: Gateway; + + for (const [, gateway] of this.state.gateways) { + if ( + gateway.targetUri === targetUri && + gateway.targetUser === targetUser + ) { + found = gateway; + break; + } + } + + return found; + } + /** * Returns a root cluster or a leaf cluster to which the given resource belongs to. */ diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts index 7c96be9216212..f3bd43b86e8af 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts @@ -27,6 +27,7 @@ import { DocumentAccessRequests, DocumentCluster, DocumentGateway, + DocumentGatewayCliClient, DocumentOrigin, DocumentTshKube, DocumentTshNode, @@ -153,11 +154,40 @@ export class DocumentsService { }; } + createGatewayCliDocument({ + title, + targetUri, + targetUser, + targetName, + targetProtocol, + }: Pick< + DocumentGatewayCliClient, + 'title' | 'targetUri' | 'targetUser' | 'targetName' | 'targetProtocol' + >): DocumentGatewayCliClient { + const clusterUri = routing.ensureClusterUri(targetUri); + const { rootClusterId, leafClusterId } = + routing.parseClusterUri(clusterUri).params; + + return { + kind: 'doc.gateway_cli_client', + uri: routing.getDocUri({ docId: unique() }), + title, + status: 'connecting', + rootClusterId, + leafClusterId, + targetUri, + targetUser, + targetName, + targetProtocol, + }; + } + openNewTerminal(opts: CreateNewTerminalOpts) { const doc = ((): Document => { const activeDocument = this.getActive(); if (activeDocument && activeDocument.kind == 'doc.terminal_shell') { + // Copy activeDocument to use the same cwd in the new doc. return { ...activeDocument, uri: routing.getDocUri({ docId: unique() }), diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsUtils.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsUtils.ts index cb81f5ae31df9..826f7cd528a66 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsUtils.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsUtils.ts @@ -19,6 +19,14 @@ import { assertUnreachable } from 'teleterm/ui/utils'; import { Document, isDocumentTshNodeWithServerId } from './types'; +/** + * getResourceUri returns the URI of the cluster resource that is the subject of the document. + * + * For example, for DocumentGateway it's targetUri rather than gatewayUri because the gateway + * doesn't belong to the cluster. + * + * At the moment it's used only to get the breadcrumbs for the status bar. + */ export function getResourceUri( document: Document ): ClusterOrResourceUri | undefined { @@ -26,6 +34,7 @@ export function getResourceUri( case 'doc.cluster': return document.clusterUri; case 'doc.gateway': + case 'doc.gateway_cli_client': return document.targetUri; case 'doc.terminal_tsh_node': return isDocumentTshNodeWithServerId(document) diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts index f11062b034e20..ad079394cca20 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts @@ -16,14 +16,9 @@ limitations under the License. import * as uri from 'teleterm/ui/uri'; -export type Kind = - | 'doc.access_requests' - | 'doc.cluster' - | 'doc.blank' - | 'doc.gateway' - | 'doc.terminal_shell' - | 'doc.terminal_tsh_node' - | 'doc.terminal_tsh_kube'; +import type * as tsh from 'teleterm/services/tshd/types'; + +export type Kind = Document['kind']; export type DocumentOrigin = | 'resource_table' @@ -110,6 +105,34 @@ export interface DocumentGateway extends DocumentBase { origin: DocumentOrigin; } +/** + * DocumentGatewayCliClient is the tab that opens a CLI tool which targets the given gateway. + * + * The gateway is found by matching targetUri and targetUser rather than gatewayUri. gatewayUri + * changes between app restarts while targetUri and targetUser won't. + */ +export interface DocumentGatewayCliClient extends DocumentBase { + kind: 'doc.gateway_cli_client'; + // rootClusterId and leafClusterId are tech debt. They could be read from targetUri, but + // useDocumentTerminal expects these fields to be set on the doc. + rootClusterId: string; + leafClusterId: string | undefined; + // The four target properties are needed in order to call connectToDatabase from within + // DocumentGatewayCliClient. targetName is needed to set a proper tab title. + // + // targetUri and targetUser are also needed to find a gateway providing the connection to the + // target. + targetUri: tsh.Gateway['targetUri']; + targetUser: tsh.Gateway['targetUser']; + targetName: tsh.Gateway['targetName']; + targetProtocol: tsh.Gateway['protocol']; + // status is used merely to show a progress bar when the doc waits for the gateway to be created. + // It will be changed to 'connected' as soon as the CLI client prints something out. Some clients + // type something out immediately after starting while others only after they actually connect to + // a resource. + status: '' | 'connecting' | 'connected' | 'error'; +} + export interface DocumentCluster extends DocumentBase { kind: 'doc.cluster'; clusterUri: uri.ClusterUri; @@ -132,6 +155,7 @@ export interface DocumentPtySession extends DocumentBase { export type DocumentTerminal = | DocumentPtySession + | DocumentGatewayCliClient | DocumentTshNode | DocumentTshKube;