diff --git a/integration/proxy/teleterm_test.go b/integration/proxy/teleterm_test.go index ac71dc1e4f633..fbdf603d310b1 100644 --- a/integration/proxy/teleterm_test.go +++ b/integration/proxy/teleterm_test.go @@ -332,7 +332,10 @@ func testKubeGatewayCertRenewal(t *testing.T, suite *Suite, albAddr string, kube kubeGateway, err := gateway.AsKube(gw) require.NoError(t, err) - client = kubeClientForLocalProxy(t, kubeGateway.KubeconfigPath(), teleportCluster, kubeCluster) + kubeconfigPath := kubeGateway.KubeconfigPath() + checkKubeconfigPathInCommandEnv(t, gw, kubeconfigPath) + + client = kubeClientForLocalProxy(t, kubeconfigPath, teleportCluster, kubeCluster) }) mustGetKubePod(t, client, kubePodName) @@ -350,3 +353,11 @@ func testKubeGatewayCertRenewal(t *testing.T, suite *Suite, albAddr string, kube ) } + +func checkKubeconfigPathInCommandEnv(t *testing.T, gw gateway.Gateway, wantKubeconfigPath string) { + t.Helper() + + cmd, err := gw.CLICommand() + require.NoError(t, err) + require.Equal(t, cmd.Env, []string{"KUBECONFIG=" + wantKubeconfigPath}) +} diff --git a/lib/teleterm/gateway/base.go b/lib/teleterm/gateway/base.go index 183083ff1b6b6..9e274e5eb8931 100644 --- a/lib/teleterm/gateway/base.go +++ b/lib/teleterm/gateway/base.go @@ -193,7 +193,10 @@ func (b *base) CLICommand() (*api.GatewayCLICommand, error) { if err != nil { return nil, trace.Wrap(err) } + return makeCLICommand(cmd), nil +} +func makeCLICommand(cmd *exec.Cmd) *api.GatewayCLICommand { cmdString := strings.TrimSpace( fmt.Sprintf("%s %s", strings.Join(cmd.Env, " "), @@ -204,7 +207,7 @@ func (b *base) CLICommand() (*api.GatewayCLICommand, error) { Args: cmd.Args, Env: cmd.Env, Preview: cmdString, - }, nil + } } // ReloadCert loads the key pair from cfg.CertPath & cfg.KeyPath and updates the cert of the running diff --git a/lib/teleterm/gateway/kube.go b/lib/teleterm/gateway/kube.go index 610da0b1843e6..61eed706c9346 100644 --- a/lib/teleterm/gateway/kube.go +++ b/lib/teleterm/gateway/kube.go @@ -26,6 +26,7 @@ import ( "github.com/gravitational/teleport/api/utils/keypaths" "github.com/gravitational/teleport/api/utils/keys" + api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/kube/kubeconfig" "github.com/gravitational/teleport/lib/srv/alpnproxy" @@ -209,3 +210,15 @@ func (k *kube) writeKubeconfig(key *keys.PrivateKey, cas map[string]tls.Certific }) return nil } + +func (k *kube) CLICommand() (*api.GatewayCLICommand, error) { + // TODO(greedy52) currently kube must implement CLICommand in order to pass + // Kube to CLICommandProvider. We should revisit gateway design/flows like + // this. For example, one alternative is to move gateway.CLICommand to + // daemon.GatewayCLICommand as daemon owns all CLICommandProvider. + cmd, err := k.cfg.CLICommandProvider.GetCommand(k) + if err != nil { + return nil, trace.Wrap(err) + } + return makeCLICommand(cmd), nil +} diff --git a/web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.test.ts b/web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.test.ts index 86a913c39c13a..c5d120f3b0c23 100644 --- a/web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.test.ts +++ b/web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.test.ts @@ -16,7 +16,7 @@ import { makeRuntimeSettings } from 'teleterm/mainProcess/fixtures/mocks'; -import { GatewayCliClientCommand } from '../types'; +import { ShellCommand, GatewayCliClientCommand } from '../types'; import { getPtyProcessOptions } from './buildPtyOptions'; @@ -50,4 +50,32 @@ describe('getPtyProcessOptions', () => { expect(env.shared).toBe('fromCmd'); }); }); + + describe('pty.shell', () => { + it('merges process env with the env from cmd', () => { + const processEnv = { + processExclusive: 'process', + shared: 'fromProcess', + }; + const cmd: ShellCommand = { + kind: 'pty.shell', + 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 134f600942a08..ad2d58d1acd08 100644 --- a/web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.ts +++ b/web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.ts @@ -92,7 +92,8 @@ export function getPtyProcessOptions( path: settings.defaultShell, args: [], cwd: cmd.cwd, - env, + env: { ...env, ...cmd.env }, + initMessage: cmd.initMessage, }; } diff --git a/web/packages/teleterm/src/services/pty/ptyHost/ptyHostClient.ts b/web/packages/teleterm/src/services/pty/ptyHost/ptyHostClient.ts index 1f2313dc6cd2b..ba234946ca7cb 100644 --- a/web/packages/teleterm/src/services/pty/ptyHost/ptyHostClient.ts +++ b/web/packages/teleterm/src/services/pty/ptyHost/ptyHostClient.ts @@ -42,6 +42,9 @@ export function createPtyHostClient( if (ptyOptions.cwd) { request.setCwd(ptyOptions.cwd); } + if (ptyOptions.initMessage) { + request.setInitMessage(ptyOptions.initMessage); + } return new Promise((resolve, reject) => { client.createPtyProcess(request, (error, response) => { diff --git a/web/packages/teleterm/src/services/pty/types.ts b/web/packages/teleterm/src/services/pty/types.ts index 1ec7314652c05..45bb82afdf61a 100644 --- a/web/packages/teleterm/src/services/pty/types.ts +++ b/web/packages/teleterm/src/services/pty/types.ts @@ -41,6 +41,15 @@ export type PtyServiceClient = { export type ShellCommand = PtyCommandBase & { kind: 'pty.shell'; cwd?: string; + // env is a record of additional env variables that need to be set for the shell terminal and it + // will be merged with process env. + env?: Record; + // initMessage is a help message presented to the user at the beginning of + // the shell to provide extra context. + // + // The initMessage is rendered on the terminal UI without being written or + // read by the underlying PTY. + initMessage?: string; }; export type TshLoginCommand = PtyCommandBase & { diff --git a/web/packages/teleterm/src/services/tshd/testHelpers.ts b/web/packages/teleterm/src/services/tshd/testHelpers.ts index 5d731ec91c84f..65fe27d3ad2f0 100644 --- a/web/packages/teleterm/src/services/tshd/testHelpers.ts +++ b/web/packages/teleterm/src/services/tshd/testHelpers.ts @@ -27,6 +27,7 @@ export const makeServer = (props: Partial = {}): tsh.Server => ({ }); export const databaseUri = '/clusters/teleport-local/dbs/foo'; +export const kubeUri = '/clusters/teleport-local/kubes/foo'; export const makeDatabase = ( props: Partial = {} @@ -74,7 +75,9 @@ export const makeRootCluster = ( ...props, }); -export const makeGateway = (props: Partial = {}): tsh.Gateway => ({ +export const makeDatabaseGateway = ( + props: Partial = {} +): tsh.Gateway => ({ uri: '/gateways/foo', targetName: 'sales-production', targetUri: databaseUri, @@ -91,3 +94,23 @@ export const makeGateway = (props: Partial = {}): tsh.Gateway => ({ targetSubresourceName: 'bar', ...props, }); + +export const makeKubeGateway = ( + props: Partial = {} +): tsh.Gateway => ({ + uri: '/gateways/foo', + targetName: 'foo', + targetUri: kubeUri, + targetUser: '', + localAddress: 'localhost', + localPort: '1337', + protocol: '', + gatewayCliCommand: { + path: '/bin/kubectl', + argsList: ['version'], + envList: ['KUBECONFIG=/path/to/kubeconfig'], + preview: 'KUBECONFIG=/path/to/kubeconfig /bin/kubectl version', + }, + targetSubresourceName: '', + ...props, +}); diff --git a/web/packages/teleterm/src/services/tshd/types.ts b/web/packages/teleterm/src/services/tshd/types.ts index 3a07079862f77..76ff457e2a6a6 100644 --- a/web/packages/teleterm/src/services/tshd/types.ts +++ b/web/packages/teleterm/src/services/tshd/types.ts @@ -46,7 +46,7 @@ export interface Server extends apiServer.Server.AsObject { export interface Gateway extends apiGateway.Gateway.AsObject { uri: uri.GatewayUri; - targetUri: uri.DatabaseUri; + targetUri: uri.DatabaseUri | uri.KubeUri; // The type of gatewayCliCommand was repeated here just to refer to the type with the JSDoc. gatewayCliCommand: GatewayCLICommand; } @@ -272,7 +272,7 @@ export interface LoginPasswordlessParams extends LoginParamsBase { } export type CreateGatewayParams = { - targetUri: uri.DatabaseUri; + targetUri: uri.DatabaseUri | uri.KubeUri; port?: string; user: string; subresource_name?: string; diff --git a/web/packages/teleterm/src/services/tshdNotifications.ts b/web/packages/teleterm/src/services/tshdNotifications.ts index 1fb0e138a2db1..2d94aefcbaf56 100644 --- a/web/packages/teleterm/src/services/tshdNotifications.ts +++ b/web/packages/teleterm/src/services/tshdNotifications.ts @@ -1,4 +1,4 @@ -/** +/* * Copyright 2023 Gravitational, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,27 +31,31 @@ export class TshdNotificationsService { request.cannotProxyGatewayConnection; const gateway = this.clustersService.findGateway(gatewayUri); const clusterName = routing.parseClusterName(targetUri); - let shortTargetDesc: string; - let longTargetDesc: string; + let targetName: string; + let targetUser: string; + let targetDesc: string; + // Try to get target name and user from gateway object. if (gateway) { - shortTargetDesc = `${gateway.targetName} as ${gateway.targetUser}`; - longTargetDesc = shortTargetDesc; + targetName = gateway.targetName; + targetUser = gateway.targetUser; } else { - const targetName = routing.parseDbUri(targetUri)?.params['dbId']; + // Try to get target name from target URI. + targetName = + routing.parseDbUri(targetUri)?.params['dbId'] || + routing.parseKubeUri(targetUri)?.params['kubeId'] || + targetUri; + } - if (targetName) { - shortTargetDesc = targetName; - longTargetDesc = shortTargetDesc; - } else { - shortTargetDesc = 'a database server'; - longTargetDesc = `a database server under ${targetUri}`; - } + if (targetUser) { + targetDesc = `${targetName} as ${targetUser}`; + } else { + targetDesc = targetName; } const notificationContent = { - title: `Cannot connect to ${shortTargetDesc} (${clusterName})`, - description: `You tried to connect to ${longTargetDesc} but we encountered an unexpected error: ${error}`, + title: `Cannot connect to ${targetDesc} (${clusterName})`, + description: `You tried to connect to ${targetDesc} but we encountered an unexpected error: ${error}`, }; this.notificationsService.notifyError(notificationContent); diff --git a/web/packages/teleterm/src/sharedProcess/api/proto/ptyHostService.proto b/web/packages/teleterm/src/sharedProcess/api/proto/ptyHostService.proto index 66e533a4fcb1f..c7a33ec57871a 100644 --- a/web/packages/teleterm/src/sharedProcess/api/proto/ptyHostService.proto +++ b/web/packages/teleterm/src/sharedProcess/api/proto/ptyHostService.proto @@ -36,6 +36,7 @@ message PtyCreate { reserved 6; reserved "init_command"; google.protobuf.Struct env = 7; + string init_message = 8; } message PtyClientEvent { diff --git a/web/packages/teleterm/src/sharedProcess/api/protogen/ptyHostService_pb.d.ts b/web/packages/teleterm/src/sharedProcess/api/protogen/ptyHostService_pb.d.ts index 66135f4acb761..ea53d0743e90b 100644 --- a/web/packages/teleterm/src/sharedProcess/api/protogen/ptyHostService_pb.d.ts +++ b/web/packages/teleterm/src/sharedProcess/api/protogen/ptyHostService_pb.d.ts @@ -57,6 +57,8 @@ export class PtyCreate extends jspb.Message { clearEnv(): void; getEnv(): google_protobuf_struct_pb.Struct | undefined; setEnv(value?: google_protobuf_struct_pb.Struct): PtyCreate; + getInitMessage(): string; + setInitMessage(value: string): PtyCreate; serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): PtyCreate.AsObject; @@ -74,6 +76,7 @@ export namespace PtyCreate { argsList: Array, cwd: string, env?: google_protobuf_struct_pb.Struct.AsObject, + initMessage: string, } } diff --git a/web/packages/teleterm/src/sharedProcess/api/protogen/ptyHostService_pb.js b/web/packages/teleterm/src/sharedProcess/api/protogen/ptyHostService_pb.js index 50e6f30c1e3a3..8fe3572655761 100644 --- a/web/packages/teleterm/src/sharedProcess/api/protogen/ptyHostService_pb.js +++ b/web/packages/teleterm/src/sharedProcess/api/protogen/ptyHostService_pb.js @@ -439,7 +439,8 @@ proto.PtyCreate.toObject = function(includeInstance, msg) { path: jspb.Message.getFieldWithDefault(msg, 3, ""), argsList: (f = jspb.Message.getRepeatedField(msg, 4)) == null ? undefined : f, cwd: jspb.Message.getFieldWithDefault(msg, 5, ""), - env: (f = msg.getEnv()) && google_protobuf_struct_pb.Struct.toObject(includeInstance, f) + env: (f = msg.getEnv()) && google_protobuf_struct_pb.Struct.toObject(includeInstance, f), + initMessage: jspb.Message.getFieldWithDefault(msg, 8, "") }; if (includeInstance) { @@ -493,6 +494,10 @@ proto.PtyCreate.deserializeBinaryFromReader = function(msg, reader) { reader.readMessage(value,google_protobuf_struct_pb.Struct.deserializeBinaryFromReader); msg.setEnv(value); break; + case 8: + var value = /** @type {string} */ (reader.readString()); + msg.setInitMessage(value); + break; default: reader.skipField(); break; @@ -551,6 +556,13 @@ proto.PtyCreate.serializeBinaryToWriter = function(message, writer) { google_protobuf_struct_pb.Struct.serializeBinaryToWriter ); } + f = message.getInitMessage(); + if (f.length > 0) { + writer.writeString( + 8, + f + ); + } }; @@ -664,6 +676,24 @@ proto.PtyCreate.prototype.hasEnv = function() { }; +/** + * optional string init_message = 8; + * @return {string} + */ +proto.PtyCreate.prototype.getInitMessage = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 8, "")); +}; + + +/** + * @param {string} value + * @return {!proto.PtyCreate} returns this + */ +proto.PtyCreate.prototype.setInitMessage = function(value) { + return jspb.Message.setProto3StringField(this, 8, value); +}; + + /** * Oneof group definitions for this message. Each group defines the field diff --git a/web/packages/teleterm/src/sharedProcess/ptyHost/ptyHostService.ts b/web/packages/teleterm/src/sharedProcess/ptyHost/ptyHostService.ts index 3d83bb45c27c8..dc4962bb3e5de 100644 --- a/web/packages/teleterm/src/sharedProcess/ptyHost/ptyHostService.ts +++ b/web/packages/teleterm/src/sharedProcess/ptyHost/ptyHostService.ts @@ -38,6 +38,7 @@ export function createPtyHostService(): IPtyHostServer { cwd: ptyOptions.cwd, ptyId, env: call.request.getEnv()?.toJavaScript() as Record, + initMessage: ptyOptions.initMessage, }); ptyProcesses.set(ptyId, ptyProcess); } catch (error) { diff --git a/web/packages/teleterm/src/sharedProcess/ptyHost/ptyProcess.ts b/web/packages/teleterm/src/sharedProcess/ptyHost/ptyProcess.ts index 9814d52ff5cd6..06b56850bfd6e 100644 --- a/web/packages/teleterm/src/sharedProcess/ptyHost/ptyProcess.ts +++ b/web/packages/teleterm/src/sharedProcess/ptyHost/ptyProcess.ts @@ -76,6 +76,13 @@ export class PtyProcess extends EventEmitter implements IPtyProcess { this._setStatus('open'); this.emit(TermEventEnum.Open); + // Emit the init/help message before registering data handler. This ensures + // the message is printed first and will not conflict with data coming from + // the PTY. + if (this.options.initMessage) { + this.emit(TermEventEnum.Data, this.options.initMessage); + } + this._process.onData(data => this._handleData(data)); this._process.onExit(ev => this._handleExit(ev)); } diff --git a/web/packages/teleterm/src/sharedProcess/ptyHost/types.ts b/web/packages/teleterm/src/sharedProcess/ptyHost/types.ts index eafeefb7c9c2f..4488ed1ebd743 100644 --- a/web/packages/teleterm/src/sharedProcess/ptyHost/types.ts +++ b/web/packages/teleterm/src/sharedProcess/ptyHost/types.ts @@ -19,6 +19,7 @@ export type PtyProcessOptions = { path: string; args: string[]; cwd?: string; + initMessage?: string; }; export type IPtyProcess = { 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 3f8a51e89b1fa..ff574e213c1a7 100644 --- a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx +++ b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx @@ -20,7 +20,10 @@ import { Box } from 'design'; import { Attempt } from 'shared/hooks/useAsync'; import * as types from 'teleterm/ui/services/clusters/types'; -import { makeGateway } from 'teleterm/services/tshd/testHelpers'; +import { + makeDatabaseGateway, + makeKubeGateway, +} from 'teleterm/services/tshd/testHelpers'; import { ClusterLoginPresentation, @@ -115,14 +118,31 @@ export const LocalOnly = () => { ); }; -export const LocalOnlyWithReasonGatewayCertExpiredWithGateway = () => { +export const LocalOnlyWithReasonGatewayCertExpiredWithDbGateway = () => { const props = makeProps(); props.initAttempt.data.secondFactor = 'off'; props.initAttempt.data.allowPasswordless = false; props.reason = { kind: 'reason.gateway-cert-expired', - targetUri: gateway.targetUri, - gateway: gateway, + targetUri: dbGateway.targetUri, + gateway: dbGateway, + }; + + return ( + + + + ); +}; + +export const LocalOnlyWithReasonGatewayCertExpiredWithKubeGateway = () => { + const props = makeProps(); + props.initAttempt.data.secondFactor = 'off'; + props.initAttempt.data.allowPasswordless = false; + props.reason = { + kind: 'reason.gateway-cert-expired', + targetUri: kubeGateway.targetUri, + gateway: kubeGateway, }; return ( @@ -138,7 +158,7 @@ export const LocalOnlyWithReasonGatewayCertExpiredWithoutGateway = () => { props.initAttempt.data.allowPasswordless = false; props.reason = { kind: 'reason.gateway-cert-expired', - targetUri: gateway.targetUri, + targetUri: dbGateway.targetUri, gateway: undefined, }; @@ -323,7 +343,7 @@ const TestContainer: React.FC = ({ children }) => ( ); -const gateway = makeGateway({ +const dbGateway = makeDatabaseGateway({ uri: '/gateways/gateway1', targetName: 'postgres', targetUri: '/clusters/teleport-local/dbs/postgres', @@ -333,3 +353,12 @@ const gateway = makeGateway({ localPort: '59116', protocol: 'postgres', }); + +const kubeGateway = makeKubeGateway({ + uri: '/gateways/gateway2', + targetName: 'minikube', + targetUri: '/clusters/teleport-local/kubes/minikube', + targetSubresourceName: '', + localAddress: 'localhost', + localPort: '59117', +}); diff --git a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.tsx b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.tsx index 4056866f1889c..866f0bfcc0efd 100644 --- a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.tsx +++ b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.tsx @@ -118,8 +118,13 @@ function Reason({ reason }: { reason: ClusterConnectReason }) { if (gateway) { $targetDesc = ( <> - {gateway.targetName} as{' '} - {gateway.targetUser} + {gateway.targetName} + {gateway.targetUser && ( + <> + {' '} + as {gateway.targetUser} + + )} ); } else { diff --git a/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx b/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx index 37f54f10c0a42..3cb633d68e66e 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 { makeGateway } from 'teleterm/services/tshd/testHelpers'; +import { makeDatabaseGateway } from 'teleterm/services/tshd/testHelpers'; import { DocumentGateway, DocumentGatewayProps } from './DocumentGateway'; @@ -31,7 +31,7 @@ export default { title: 'Teleterm/DocumentGateway', }; -const gateway = makeGateway({ +const gateway = makeDatabaseGateway({ uri: '/gateways/bar', targetName: 'sales-production', targetUri: '/clusters/bar/dbs/foo', @@ -63,7 +63,7 @@ export function Online() { } export function OnlineWithLongValues() { - const gateway = makeGateway({ + const gateway = makeDatabaseGateway({ uri: '/gateways/bar', targetName: 'sales-production', targetUri: '/clusters/bar/dbs/foo', diff --git a/web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.test.tsx b/web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.test.tsx index 69c69199fbec7..f5d4d563dca5a 100644 --- a/web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.test.tsx +++ b/web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.test.tsx @@ -19,10 +19,11 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { makeRootCluster, - makeGateway, + makeDatabaseGateway, } from 'teleterm/services/tshd/testHelpers'; import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; import { DocumentGateway } from 'teleterm/ui/services/workspacesService'; +import { DatabaseUri } from 'teleterm/ui/uri'; import { WorkspaceContextProvider } from '../Documents'; import { MockAppContextProvider } from '../fixtures/MockAppContextProvider'; @@ -108,12 +109,12 @@ it('does not attempt to create a gateway immediately after closing it if the gat const testSetup = () => { const appContext = new MockAppContext(); const cluster = makeRootCluster({ connected: true }); - const gateway = makeGateway(); + const gateway = makeDatabaseGateway(); const doc: DocumentGateway = { uri: '/docs/1', kind: 'doc.gateway', targetName: gateway.targetName, - targetUri: gateway.targetUri, + targetUri: gateway.targetUri as DatabaseUri, targetUser: gateway.targetUser, targetSubresourceName: gateway.targetSubresourceName, gatewayUri: gateway.uri, diff --git a/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.story.tsx b/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.story.tsx new file mode 100644 index 0000000000000..c3827fb921da0 --- /dev/null +++ b/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.story.tsx @@ -0,0 +1,33 @@ +/** + * 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 { Reconnect } from './Reconnect'; + +export default { + title: 'Teleterm/DocumentGatewayKube', +}; + +export function Retry() { + return ( + {}} + /> + ); +} diff --git a/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.tsx b/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.tsx new file mode 100644 index 0000000000000..8cd2be36fda56 --- /dev/null +++ b/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.tsx @@ -0,0 +1,100 @@ +/** + * 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, { useEffect } from 'react'; + +import { useAsync } from 'shared/hooks/useAsync'; + +import * as types from 'teleterm/ui/services/workspacesService'; +import { useAppContext } from 'teleterm/ui/appContextProvider'; +import { useWorkspaceContext } from 'teleterm/ui/Documents'; +import { retryWithRelogin } from 'teleterm/ui/utils'; +import Document from 'teleterm/ui/Document'; +import { DocumentTerminal } from 'teleterm/ui/DocumentTerminal'; +import { routing } from 'teleterm/ui/uri'; + +import { Reconnect } from './Reconnect'; + +/** + * DocumentGatewayKube creates a terminal session that presets KUBECONFIG env + * var to a kubeconfig that can be used to connect the kube gateway. + * + * It first tries to create a kube gateway by calling the clusterService. Once + * connected, it will render DocumentTerminal. + * + * TODO(greedy52) doc.gateway_kube replaces doc.terminal_tsh_kube when opening + * a new kube tab. However, the old doc.terminal_tsh_kube is kept to handle the + * case where doc.terminal_tsh_kube tabs are saved on disk by the old version + * of Teleport Connect and need to be reopen by the new version of Teleport + * Connect. The old doc.terminal_tsh_kube can be DELETED in the next major + * version (15.0.0) assuming migration should be done by then. Here is the + * discussion reference: + * https://github.com/gravitational/teleport/pull/28312#discussion_r1253214517 + */ +export const DocumentGatewayKube = (props: { + visible: boolean; + doc: types.DocumentGatewayKube; +}) => { + const { doc, visible } = props; + const ctx = useAppContext(); + const { documentsService } = useWorkspaceContext(); + const { params } = routing.parseKubeUri(doc.targetUri); + const [connectAttempt, createGateway] = useAsync(async () => { + documentsService.update(doc.uri, { status: 'connecting' }); + + try { + await retryWithRelogin(ctx, doc.targetUri, () => + // Creating a kube gateway twice with the same params is a noop. tshd + // will return the URI of an already existing gateway. + ctx.clustersService.createGateway({ + targetUri: doc.targetUri, + user: '', + }) + ); + } catch (error) { + documentsService.update(doc.uri, { status: 'error' }); + throw error; + } + }); + + useEffect(() => { + if (connectAttempt.status === '') { + createGateway(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + switch (connectAttempt.status) { + case 'success': { + return ; + } + + case 'error': { + return ( + + ); + } + + default: { + // Show waiting animation. + return ; + } + } +}; diff --git a/web/packages/teleterm/src/ui/DocumentGatewayKube/Reconnect.tsx b/web/packages/teleterm/src/ui/DocumentGatewayKube/Reconnect.tsx new file mode 100644 index 0000000000000..a7485e67857e2 --- /dev/null +++ b/web/packages/teleterm/src/ui/DocumentGatewayKube/Reconnect.tsx @@ -0,0 +1,43 @@ +/** + * 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 { Danger } from 'design/Alert'; +import { Flex, Text, ButtonPrimary } from 'design'; + +export function Reconnect(props: { + kubeId: string; + statusText: string; + reconnect: () => void; +}) { + return ( + + + A connection to {props.kubeId} has failed. + + + + + {props.statusText} + + + + Retry + + + + ); +} diff --git a/web/packages/teleterm/src/ui/DocumentGatewayKube/index.ts b/web/packages/teleterm/src/ui/DocumentGatewayKube/index.ts new file mode 100644 index 0000000000000..4a6abc7041aa9 --- /dev/null +++ b/web/packages/teleterm/src/ui/DocumentGatewayKube/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 { DocumentGatewayKube } from './DocumentGatewayKube'; diff --git a/web/packages/teleterm/src/ui/DocumentTerminal/Reconnect.tsx b/web/packages/teleterm/src/ui/DocumentTerminal/Reconnect.tsx index eb313b5a493df..7d8d0f8fb1f93 100644 --- a/web/packages/teleterm/src/ui/DocumentTerminal/Reconnect.tsx +++ b/web/packages/teleterm/src/ui/DocumentTerminal/Reconnect.tsx @@ -71,6 +71,7 @@ function getReconnectCopy(docKind: types.DocumentTerminal['kind']) { }; } case 'doc.gateway_cli_client': + case 'doc.gateway_kube': 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 db556d57cc823..0a4d6ac5b020b 100644 --- a/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts +++ b/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts @@ -233,14 +233,19 @@ async function setUpPtyProcess( if (doc.kind === 'doc.terminal_tsh_node') { ctx.usageService.captureProtocolUse(clusterUri, 'ssh', doc.origin); } - if (doc.kind === 'doc.terminal_tsh_kube') { + if (doc.kind === 'doc.terminal_tsh_kube' || doc.kind === 'doc.gateway_kube') { ctx.usageService.captureProtocolUse(clusterUri, 'kube', doc.origin); } const openContextMenu = () => ctx.mainProcessClient.openTerminalContextMenu(); const refreshTitle = async () => { - if (cmd.kind !== 'pty.shell') { + // TODO(ravicious): Enable updating cwd in doc.gateway_kube titles by + // moving title-updating logic to DocumentsService. The logic behind + // updating the title should be encapsulated in a single place, so that + // useDocumentTerminal doesn't need to know the details behind the title of + // each document kind. + if (doc.kind !== 'doc.terminal_shell') { return; } @@ -347,6 +352,7 @@ function createCmd( }; } + // DELETE IN 15.0.0. See DocumentGatewayKube for more details. if (doc.kind === 'doc.terminal_tsh_kube') { return { ...doc, @@ -389,6 +395,39 @@ function createCmd( }; } + if (doc.kind === 'doc.gateway_kube') { + const gateway = clustersService.findGatewayByConnectionParams( + doc.targetUri, + '' + ); + if (!gateway) { + throw new Error(`No gateway found for ${doc.targetUri}`); + } + + const env = tshdGateway.getCliCommandEnv(gateway.gatewayCliCommand); + + if ('KUBECONFIG' in env === false) { + // This shouldn't happen as 'KUBECONFIG' is the sole purpose of the CLI + // command for a kube gateway. + throw new Error( + `No KUBECONFIG provided for gateway ${gateway.targetUri}` + ); + } + const initMessage = + `Started a local proxy for Kubernetes cluster "${gateway.targetName}".\r\n\r\n` + + 'The KUBECONFIG env var can be used with third-party tools as long as the proxy is running.\r\n' + + 'Close the proxy from Connections in the top left corner or by closing Teleport Connect.\r\n\r\n' + + 'Try "kubectl version" to test the connection.\r\n\r\n'; + + return { + kind: 'pty.shell', + proxyHost, + clusterName, + env, + initMessage, + }; + } + return { ...doc, kind: 'pty.shell', diff --git a/web/packages/teleterm/src/ui/Documents/DocumentsRenderer.tsx b/web/packages/teleterm/src/ui/Documents/DocumentsRenderer.tsx index d5bce83781644..22cd0fb4c4cca 100644 --- a/web/packages/teleterm/src/ui/Documents/DocumentsRenderer.tsx +++ b/web/packages/teleterm/src/ui/Documents/DocumentsRenderer.tsx @@ -33,6 +33,7 @@ import DocumentCluster from 'teleterm/ui/DocumentCluster'; import DocumentGateway from 'teleterm/ui/DocumentGateway'; import { DocumentTerminal } from 'teleterm/ui/DocumentTerminal'; import { DocumentConnectMyComputerSetup } from 'teleterm/ui/ConnectMyComputer'; +import { DocumentGatewayKube } from 'teleterm/ui/DocumentGatewayKube'; import Document from 'teleterm/ui/Document'; import { RootClusterUri } from 'teleterm/ui/uri'; @@ -101,8 +102,12 @@ function MemoizedDocument(props: { doc: types.Document; visible: boolean }) { return ; case 'doc.gateway_cli_client': return ; + case 'doc.gateway_kube': + return ; case 'doc.terminal_shell': case 'doc.terminal_tsh_node': + return ; + // DELETE IN 15.0.0. See DocumentGatewayKube for more details. case 'doc.terminal_tsh_kube': return ; case 'doc.access_requests': 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 02fc13600d169..425f26b6daf65 100644 --- a/web/packages/teleterm/src/ui/services/clusters/clustersService.test.ts +++ b/web/packages/teleterm/src/ui/services/clusters/clustersService.test.ts @@ -17,7 +17,10 @@ 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 { + makeDatabaseGateway, + makeKubeGateway, +} from 'teleterm/services/tshd/testHelpers'; import { ClustersService } from './clustersService'; @@ -67,7 +70,7 @@ const leafClusterMock: tsh.Cluster = { }, }; -const gatewayMock = makeGateway({ +const gatewayMock = makeDatabaseGateway({ uri: '/gateways/gatewayTestUri', targetUri: `${clusterUri}/dbs/databaseTestUri`, }); @@ -119,15 +122,15 @@ test('add cluster', async () => { test('remove cluster', async () => { const { removeGateway } = getClientMocks(); const service = createService({ removeGateway }); - const gatewayFromRootCluster = makeGateway({ + const gatewayFromRootCluster = makeDatabaseGateway({ uri: '/gateways/1', targetUri: `${clusterMock.uri}/dbs/foo`, }); - const gatewayFromLeafCluster = makeGateway({ + const gatewayFromLeafCluster = makeDatabaseGateway({ uri: '/gateways/2', targetUri: `${leafClusterMock.uri}/dbs/foo`, }); - const gatewayFromOtherCluster = makeGateway({ + const gatewayFromOtherCluster = makeDatabaseGateway({ uri: '/gateways/3', targetUri: `/clusters/bogus-cluster/dbs/foo`, }); @@ -242,6 +245,30 @@ test('remove a gateway', async () => { expect(service.findGateway(gatewayUri)).toBeUndefined(); }); +test('remove a kube gateway', async () => { + const { removeGateway } = getClientMocks(); + const service = createService({ + removeGateway, + }); + const kubeGatewayMock = makeKubeGateway({ + uri: '/gateways/gatewayTestUri', + targetUri: `${clusterUri}/kubes/testKubeId`, + }); + + service.setState(draftState => { + draftState.gateways = new Map([[kubeGatewayMock.uri, kubeGatewayMock]]); + }); + + await service.removeKubeGateway(kubeGatewayMock.targetUri as uri.KubeUri); + expect(removeGateway).toHaveBeenCalledTimes(1); + expect(removeGateway).toHaveBeenCalledWith(kubeGatewayMock.uri); + expect(service.findGateway(kubeGatewayMock.uri)).toBeUndefined(); + + // Calling it again should not increase mock calls. + await service.removeKubeGateway(kubeGatewayMock.targetUri as uri.KubeUri); + expect(removeGateway).toHaveBeenCalledTimes(1); +}); + test('sync gateways', async () => { const { listGateways } = getClientMocks(); const service = createService({ diff --git a/web/packages/teleterm/src/ui/services/clusters/clustersService.ts b/web/packages/teleterm/src/ui/services/clusters/clustersService.ts index 17e21ab8dfdc4..8d872e114af07 100644 --- a/web/packages/teleterm/src/ui/services/clusters/clustersService.ts +++ b/web/packages/teleterm/src/ui/services/clusters/clustersService.ts @@ -379,6 +379,13 @@ export class ClustersService extends ImmutableStore } } + async removeKubeGateway(kubeUri: uri.KubeUri) { + const gateway = this.findGatewayByConnectionParams(kubeUri, ''); + if (gateway) { + await this.removeGateway(gateway.uri); + } + } + async setGatewayTargetSubresourceName( gatewayUri: uri.GatewayUri, targetSubresourceName: string @@ -425,7 +432,7 @@ export class ClustersService extends ImmutableStore } findGatewayByConnectionParams( - targetUri: uri.DatabaseUri, + targetUri: uri.DatabaseUri | uri.KubeUri, targetUser: string ) { let found: Gateway; diff --git a/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.ts b/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.ts index 48b4daa1de545..5fa1e079d276d 100644 --- a/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.ts +++ b/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.ts @@ -19,6 +19,7 @@ import Logger, { NullService } from 'teleterm/logger'; import { Document, DocumentGateway, + DocumentGatewayKube, DocumentTshNodeWithLoginHost, DocumentTshNodeWithServerId, WorkspacesService, @@ -76,6 +77,13 @@ it('removeItemsBelongingToRootCluster removes connections', () => { targetSubresourceName: 'pg', gatewayUri: '/gateways/4f68927b-579c-47a8-b965-efa8159203c9', }, + { + kind: 'connection.kube', + connected: true, + id: 'root-cluster-kube-id', + title: 'test-kube-id', + kubeUri: '/clusters/localhost/kubes/test-kube-id', + }, { kind: 'connection.server', connected: true, @@ -90,7 +98,7 @@ it('removeItemsBelongingToRootCluster removes connections', () => { const service = getTestSetupWithMockedConnections({ connections }); service.removeItemsBelongingToRootCluster('/clusters/localhost'); expect(service.getConnections()).toEqual([ - { clusterName: 'remote_leaf', ...connections[3] }, + { clusterName: 'remote_leaf', ...connections[4] }, ]); }); @@ -182,6 +190,32 @@ it('ignores doc.terminal_tsh_node docs with no serverUri', () => { expect(connectionTrackerService.getConnections()).toEqual([]); }); +it('creates a kube connection for doc.gateway_kube', () => { + const document: DocumentGatewayKube = { + kind: 'doc.gateway_kube', + uri: '/docs/test-kube-id', + title: 'Test title', + rootClusterId: 'localhost', + leafClusterId: undefined, + targetUri: '/clusters/localhost/kubes/test-kube-id', + origin: 'resource_table', + }; + + const { connectionTrackerService } = getTestSetupWithMockedDocuments([ + document, + ]); + + const connection = + connectionTrackerService.findConnectionByDocument(document); + expect(connection).toEqual({ + kind: 'connection.kube', + id: expect.any(String), + title: document.title, + connected: true, + kubeUri: '/clusters/localhost/kubes/test-kube-id', + }); +}); + function getTestSetupWithMockedConnections({ connections, }: { diff --git a/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts b/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts index 3343447d78947..36e886680c765 100644 --- a/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts +++ b/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts @@ -32,10 +32,12 @@ import { TrackedConnectionOperationsFactory } from './trackedConnectionOperation import { createGatewayConnection, createKubeConnection, + createGatewayKubeConnection, createServerConnection, getGatewayConnectionByDocument, getKubeConnectionByDocument, getServerConnectionByDocument, + getGatewayKubeConnectionByDocument, } from './trackedConnectionUtils'; import { ExtendedTrackedConnection, @@ -110,6 +112,10 @@ export class ConnectionTrackerService extends ImmutableStore { // assign default "connected" values draft.connections.forEach(i => { - if (i.kind === 'connection.gateway') { - i.connected = !!this._clusterService.findGateway(i.gatewayUri); - } else { - i.connected = false; + switch (i.kind) { + case 'connection.gateway': { + i.connected = !!this._clusterService.findGateway(i.gatewayUri); + break; + } + case 'connection.kube': { + i.connected = !!this._clusterService.findGatewayByConnectionParams( + i.kubeUri, + '' + ); + break; + } + default: { + i.connected = false; + break; + } } }); @@ -186,6 +204,7 @@ export class ConnectionTrackerService extends ImmutableStore d.kind === 'doc.gateway' || + d.kind === 'doc.gateway_kube' || d.kind === 'doc.terminal_tsh_node' || d.kind === 'doc.terminal_tsh_kube' ); @@ -220,6 +239,24 @@ export class ConnectionTrackerService extends ImmutableStore { + disconnect: async () => { return this._clustersService .removeGateway(connection.gatewayUri) .then(() => { @@ -151,7 +152,7 @@ export class TrackedConnectionOperationsFactory { }; } - private getConnectionKubeOperations( + private getConnectionGatewayKubeOperations( connection: TrackedKubeConnection ): TrackedConnectionOperations { const { rootClusterId, leafClusterId } = routing.parseKubeUri( @@ -169,34 +170,41 @@ export class TrackedConnectionOperationsFactory { rootClusterUri, leafClusterUri, activate: params => { - let kubeConn = documentsService + let gwDoc = documentsService .getDocuments() - .find(getKubeDocumentByConnection(connection)); + .find(getGatewayKubeDocumentByConnection(connection)); - if (!kubeConn) { - kubeConn = documentsService.createTshKubeDocument({ - kubeUri: connection.kubeUri, - kubeConfigRelativePath: connection.kubeConfigRelativePath, + if (!gwDoc) { + gwDoc = documentsService.createGatewayKubeDocument({ + targetUri: connection.kubeUri, origin: params.origin, }); - - documentsService.add(kubeConn); + documentsService.add(gwDoc); } - documentsService.open(kubeConn.uri); + documentsService.open(gwDoc.uri); }, disconnect: async () => { - documentsService - .getDocuments() - .filter(getKubeDocumentByConnection(connection)) - .forEach(document => { - documentsService.close(document.uri); + return this._clustersService + .removeKubeGateway(connection.kubeUri) + .then(() => { + documentsService + .getDocuments() + .filter(getGatewayKubeDocumentByConnection(connection)) + .forEach(document => { + documentsService.close(document.uri); + }); + + // Remove deprecated doc.terminal_tsh_kube documents. + // DELETE IN 15.0.0. See DocumentGatewayKube for more details. + documentsService + .getDocuments() + .filter(getKubeDocumentByConnection(connection)) + .forEach(document => { + documentsService.close(document.uri); + }); }); }, - remove: () => { - return this._clustersService.removeKubeConfig( - connection.kubeConfigRelativePath - ); - }, + remove: async () => {}, }; } diff --git a/web/packages/teleterm/src/ui/services/connectionTracker/trackedConnectionUtils.ts b/web/packages/teleterm/src/ui/services/connectionTracker/trackedConnectionUtils.ts index b48c532df5f41..7a084bf9b31f5 100644 --- a/web/packages/teleterm/src/ui/services/connectionTracker/trackedConnectionUtils.ts +++ b/web/packages/teleterm/src/ui/services/connectionTracker/trackedConnectionUtils.ts @@ -16,6 +16,7 @@ import { DocumentGateway, + DocumentGatewayKube, DocumentTshKube, DocumentTshNode, DocumentTshNodeWithServerId, @@ -44,11 +45,19 @@ export function getServerConnectionByDocument(document: DocumentTshNode) { i.login === document.login; } +// DELETE IN 15.0.0. See DocumentGatewayKube for more details. export function getKubeConnectionByDocument(document: DocumentTshKube) { return (i: TrackedKubeConnection) => i.kind === 'connection.kube' && i.kubeUri === document.kubeUri; } +export function getGatewayKubeConnectionByDocument( + document: DocumentGatewayKube +) { + return (i: TrackedKubeConnection) => + i.kind === 'connection.kube' && i.kubeUri === document.targetUri; +} + export function getGatewayDocumentByConnection( connection: TrackedGatewayConnection ) { @@ -58,6 +67,14 @@ export function getGatewayDocumentByConnection( i.targetUser === connection.targetUser; } +export function getGatewayKubeDocumentByConnection( + connection: TrackedKubeConnection +) { + return (i: DocumentGatewayKube) => + i.kind === 'doc.gateway_kube' && i.targetUri === connection.kubeUri; +} + +// DELETE IN 15.0.0. See DocumentGatewayKube for more details. export function getKubeDocumentByConnection(connection: TrackedKubeConnection) { return (i: DocumentTshKube) => i.kind === 'doc.terminal_tsh_kube' && i.kubeUri === connection.kubeUri; @@ -115,3 +132,15 @@ export function createKubeConnection( kubeUri: document.kubeUri, }; } + +export function createGatewayKubeConnection( + document: DocumentGatewayKube +): TrackedKubeConnection { + return { + kind: 'connection.kube', + connected: true, + id: unique(), + title: document.title, + kubeUri: document.targetUri, + }; +} diff --git a/web/packages/teleterm/src/ui/services/connectionTracker/types.ts b/web/packages/teleterm/src/ui/services/connectionTracker/types.ts index 7dc1f4177b83e..89f51a41b54c3 100644 --- a/web/packages/teleterm/src/ui/services/connectionTracker/types.ts +++ b/web/packages/teleterm/src/ui/services/connectionTracker/types.ts @@ -41,7 +41,10 @@ export interface TrackedGatewayConnection extends TrackedConnectionBase { export interface TrackedKubeConnection extends TrackedConnectionBase { kind: 'connection.kube'; - kubeConfigRelativePath: string; + /** + * @deprecated Used only by connections created by doc.terminal_tsh_kube. + */ + kubeConfigRelativePath?: string; kubeUri: KubeUri; } diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToKube.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToKube.ts index fb596dbb4bd31..b9599e425a7d1 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToKube.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToKube.ts @@ -16,7 +16,6 @@ import { KubeUri, routing } from 'teleterm/ui/uri'; import { IAppContext } from 'teleterm/ui/types'; -import { TrackedKubeConnection } from 'teleterm/ui/services/connectionTracker'; import { DocumentOrigin } from './types'; @@ -28,20 +27,12 @@ export async function connectToKube( const rootClusterUri = routing.ensureRootClusterUri(target.uri); const documentsService = ctx.workspacesService.getWorkspaceDocumentService(rootClusterUri); - const kubeDoc = documentsService.createTshKubeDocument({ - kubeUri: target.uri, + const doc = documentsService.createGatewayKubeDocument({ + targetUri: target.uri, origin: telemetry.origin, }); - const connection = ctx.connectionTracker.findConnectionByDocument( - kubeDoc - ) as TrackedKubeConnection; await ctx.workspacesService.setActiveWorkspace(rootClusterUri); - - documentsService.add({ - ...kubeDoc, - kubeConfigRelativePath: - connection?.kubeConfigRelativePath || kubeDoc.kubeConfigRelativePath, - }); - documentsService.open(kubeDoc.uri); + documentsService.add(doc); + documentsService.open(doc.uri); } 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 444a6e798827a..052321e5b9ef6 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts @@ -21,6 +21,7 @@ import { paths, routing, RootClusterUri, + KubeUri, } from 'teleterm/ui/uri'; import { @@ -33,6 +34,7 @@ import { DocumentCluster, DocumentConnectMyComputerSetup, DocumentGateway, + DocumentGatewayKube, DocumentGatewayCliClient, DocumentOrigin, DocumentTshKube, @@ -85,6 +87,10 @@ export class DocumentsService { }; } + /** + * @deprecated Use createGatewayKubeDocument instead. + * DELETE IN 15.0.0. See DocumentGatewayKube for more details. + */ createTshKubeDocument( options: CreateTshKubeDocumentOptions ): DocumentTshKube { @@ -188,6 +194,27 @@ export class DocumentsService { }; } + createGatewayKubeDocument({ + targetUri, + origin, + }: { + targetUri: KubeUri; + origin: DocumentOrigin; + }): DocumentGatewayKube { + const uri = routing.getDocUri({ docId: unique() }); + const { params } = routing.parseKubeUri(targetUri); + + return { + uri, + kind: 'doc.gateway_kube', + rootClusterId: params.rootClusterId, + leafClusterId: params.leafClusterId, + targetUri, + title: `${params.kubeId}`, + origin, + }; + } + createConnectMyComputerSetupDocument(opts: { // URI of the root cluster could be passed to the `DocumentsService` // constructor and then to the document, instead of being taken from the parameter. 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 6f4b09a763103..ad01be69e61bc 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsUtils.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsUtils.ts @@ -35,6 +35,7 @@ export function getResourceUri( return document.clusterUri; case 'doc.gateway': case 'doc.gateway_cli_client': + case 'doc.gateway_kube': 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 fa84444d11d73..a3215e767d131 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts @@ -82,6 +82,7 @@ export interface DocumentTshNodeWithLoginHost extends DocumentTshNodeBase { // force places which use DocumentTshNode to narrow down the type before using it. } +// DELETE IN 15.0.0. See DocumentGatewayKube for more details. export interface DocumentTshKube extends DocumentBase { kind: 'doc.terminal_tsh_kube'; // status is used merely to show a progress bar when the document is being set up. @@ -122,7 +123,7 @@ export interface DocumentGatewayCliClient extends DocumentBase { // // targetUri and targetUser are also needed to find a gateway providing the connection to the // target. - targetUri: tsh.Gateway['targetUri']; + targetUri: uri.DatabaseUri; targetUser: tsh.Gateway['targetUser']; targetName: tsh.Gateway['targetName']; targetProtocol: tsh.Gateway['protocol']; @@ -133,6 +134,14 @@ export interface DocumentGatewayCliClient extends DocumentBase { status: '' | 'connecting' | 'connected' | 'error'; } +export interface DocumentGatewayKube extends DocumentBase { + kind: 'doc.gateway_kube'; + rootClusterId: string; + leafClusterId: string | undefined; + targetUri: uri.KubeUri; + origin: DocumentOrigin; +} + export interface DocumentCluster extends DocumentBase { kind: 'doc.cluster'; clusterUri: uri.ClusterUri; @@ -164,7 +173,8 @@ export type DocumentTerminal = | DocumentPtySession | DocumentGatewayCliClient | DocumentTshNode - | DocumentTshKube; + | DocumentTshKube + | DocumentGatewayKube; export type Document = | DocumentAccessRequests