diff --git a/api/profile/profile.go b/api/profile/profile.go
index 5045a71265875..c01ccad841fa9 100644
--- a/api/profile/profile.go
+++ b/api/profile/profile.go
@@ -177,7 +177,7 @@ func (p *Profile) TLSConfig() (*tls.Config, error) {
// Expiry returns the credential expiry.
func (p *Profile) Expiry() (time.Time, bool) {
- certPEMBlock, err := os.ReadFile(p.TLSCertPath())
+ certPEMBlock, err := p.TLSCert()
if err != nil {
return time.Time{}, false
}
@@ -188,6 +188,12 @@ func (p *Profile) Expiry() (time.Time, bool) {
return cert.NotAfter, true
}
+// TLSCert returns the profile's TLS certificate.
+func (p *Profile) TLSCert() ([]byte, error) {
+ certPEMBlock, err := os.ReadFile(p.TLSCertPath())
+ return certPEMBlock, trace.Wrap(err)
+}
+
// RequireKubeLocalProxy returns true if this profile indicates a local proxy
// is required for kube access.
func (p *Profile) RequireKubeLocalProxy() bool {
@@ -386,6 +392,10 @@ func profileFromFile(filePath string) (*Profile, error) {
// Older versions of tsh did not always store the cluster name in the
// profile. If no cluster name is found, fallback to the name of the profile
// for backward compatibility.
+ //
+ // TODO(gzdunek): A profile name is not the same thing as a site name, and they differ when the proxy hostname is different
+ // from the cluster name.
+ // Instead, tsh should be able to handle an empty site name, or this default should be changed.
if p.SiteName == "" {
p.SiteName = p.Name()
}
diff --git a/docs/pages/connect-your-client/teleport-connect.mdx b/docs/pages/connect-your-client/teleport-connect.mdx
index 287c5c80a897f..41eda10aeb331 100644
--- a/docs/pages/connect-your-client/teleport-connect.mdx
+++ b/docs/pages/connect-your-client/teleport-connect.mdx
@@ -566,11 +566,17 @@ $ tctl lock --server-id=SERVER_UUID --message="Using Connect My Computer is forb
Teleport Connect ships with its own bundled version of tsh. Teleport Connect will always use this
version of tsh for any actions performed within the app.
-Teleport Connect makes tsh available to use in your terminal of choice as well. Please note that at
-the moment tsh and Teleport Connect operate on different sets of profiles, as Teleport Connect sets
-a custom home location through [the `TELEPORT_HOME` environment
-variable](../reference/cli/tsh.mdx#tsh-environment-variables). For example, logging in to a new cluster
-through tsh will not make that cluster show up in Teleport Connect.
+The bundled tsh tool is also available to use directly in your terminal. By default, Teleport Connect and tsh share
+the same home directory, so any profiles you create or update in one tool are immediately reflected in the other.
+If you prefer a different directory, you can configure it via the `tshHome` property in [the configuration](#configuration).
+
+Teleport Connect actively monitors the tsh directory, so updates made with tsh (such as logging in, logging out,
+or adding new clusters) will automatically appear in the app.
+
+
+Older versions of Teleport Connect used a separate set of profiles stored in an internal tsh directory.
+On the first launch, the app will automatically migrate your profiles to the new location.
+
@@ -631,6 +637,7 @@ Below is the list of the supported config properties.
| `theme` | `system` | Color theme for the app. Available modes: `light`, `dark`, `system`. |
| `skipVersionCheck` | `false` | Skips the version check and hides the version compatibility warning when logging in to a cluster. |
| `runInBackground` | `true` on macOS/Windows, `false` on Linux | Keeps the app running in the menu bar/system tray even when the main window is closed. On Linux, displaying the system tray icon may require installing shell extensions. |
+| `tshHome` | `~/.tsh` | Home location for tsh configuration and data. |
| `terminal.fontFamily` | `Menlo, Monaco, monospace` on macOS `Consolas, monospace` on Windows `'Droid Sans Mono', monospace` on Linux | Font family for the terminal. |
| `terminal.fontSize` | `15` | Font size for the terminal. |
| `terminal.windowsBackend` | `auto` | `auto` uses modern [ConPTY](https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/) system if available, which requires Windows 10 (19H1) or above. Set to `winpty` to use winpty even if ConPTY is available. |
@@ -723,27 +730,30 @@ To reset the state related to a particular cluster:
1. Close Teleport Connect.
1. Open Teleport Connect, then log back in to the cluster.
-To completely wipe all app state:
+To completely wipe all app state, close Teleport Connect and run the following commands:
-1. Close Teleport Connect.
-1. Remove the internal `tsh` folder and the `app_state.json` file to log out of all clusters and
- clear all remembered tabs and connections.
+{/* TODO: DELETE IN 20.0.0: */}
+{/* Remove cleaning up the internal Teleport Connect/tsh directory from the commands. */}
+{/* Also remove the tsh home migration in Connect's main.ts */}
```code
$ rm -rf ~/Library/Application\ Support/Teleport\ Connect/{tsh,app_state.json}
+$ rm -rf ~/.tsh
```
```code
$ rm -rf ~/.config/Teleport\ Connect/{tsh,app_state.json}
+$ rm -rf ~/.tsh
```
```code
-$ rmdir /s /q C:\Users\%UserName%\AppData\Roaming\"Teleport Connect"\tsh
$ del C:\Users\%UserName%\AppData\Roaming\"Teleport Connect"\app_state.json
+$ rmdir /s /q C:\Users\%UserName%\AppData\Roaming\"Teleport Connect"\tsh
+$ rmdir /s /q C:\Users\%UserName%\.tsh
```
@@ -840,23 +850,35 @@ Remove Teleport Connect for macOS from the Applications directory with this comm
$ sudo rm -rf /Applications/Teleport\ Connect.app
```
-To remove the local user data directory:
+To remove the local user data directory which holds app configuration, state, and logs:
```code
$ rm -rf ~/Library/Application\ Support/Teleport\ Connect
```
+To remove the tsh directory which holds cluster credentials (note: this directory is also used by the tsh tool):
+
+```code
+$ rm -rf ~/.tsh
+```
+
(!docs/pages/includes/uninstall-teleport-connect-windows.mdx!)
-To remove the local user data directory:
+To remove the local user data directory which holds app configuration, state, and logs:
```powershell
$ rmdir /s /q "%APPDATA%\Teleport Connect"
```
+To remove the tsh directory which holds cluster credentials (note: this directory is also used by the tsh tool):
+
+```code
+$ rmdir /s /q C:\Users\%UserName%\.tsh
+```
+
@@ -872,6 +894,12 @@ For RPM installations uninstall Teleport Connect using YUM:
$ sudo yum remove teleport-connect
```
+To remove the tsh directory which holds cluster credentials (note: this directory is also used by the tsh tool):
+
+```code
+$ rm -rf ~/.tsh
+```
+
Installs based on a tarball should remove the
`teleport-connect` directory and any copied/linked executables.
diff --git a/gen/proto/go/teleport/lib/teleterm/v1/cluster.pb.go b/gen/proto/go/teleport/lib/teleterm/v1/cluster.pb.go
index 785dffa38870d..9183e559219e2 100644
--- a/gen/proto/go/teleport/lib/teleterm/v1/cluster.pb.go
+++ b/gen/proto/go/teleport/lib/teleterm/v1/cluster.pb.go
@@ -27,6 +27,7 @@ import (
types "github.com/gravitational/teleport/api/types"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
@@ -328,8 +329,10 @@ type LoggedInUser struct {
IsDeviceTrusted bool `protobuf:"varint,9,opt,name=is_device_trusted,json=isDeviceTrusted,proto3" json:"is_device_trusted,omitempty"`
// Indicates whether access may be hindered by the lack of a trusted device.
TrustedDeviceRequirement types.TrustedDeviceRequirement `protobuf:"varint,10,opt,name=trusted_device_requirement,json=trustedDeviceRequirement,proto3,enum=types.TrustedDeviceRequirement" json:"trusted_device_requirement,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
+ // Expiration time of the certificate.
+ ValidUntil *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=valid_until,json=validUntil,proto3" json:"valid_until,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
}
func (x *LoggedInUser) Reset() {
@@ -425,6 +428,13 @@ func (x *LoggedInUser) GetTrustedDeviceRequirement() types.TrustedDeviceRequirem
return types.TrustedDeviceRequirement(0)
}
+func (x *LoggedInUser) GetValidUntil() *timestamppb.Timestamp {
+ if x != nil {
+ return x.ValidUntil
+ }
+ return nil
+}
+
// ACL is the access control list of the user
type ACL struct {
state protoimpl.MessageState `protogen:"open.v1"`
@@ -756,7 +766,7 @@ var File_teleport_lib_teleterm_v1_cluster_proto protoreflect.FileDescriptor
const file_teleport_lib_teleterm_v1_cluster_proto_rawDesc = "" +
"\n" +
- "&teleport/lib/teleterm/v1/cluster.proto\x12\x18teleport.lib.teleterm.v1\x1a6teleport/legacy/types/trusted_device_requirement.proto\"\xf8\x03\n" +
+ "&teleport/lib/teleterm/v1/cluster.proto\x12\x18teleport.lib.teleterm.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a6teleport/legacy/types/trusted_device_requirement.proto\"\xf8\x03\n" +
"\aCluster\x12\x10\n" +
"\x03uri\x18\x01 \x01(\tR\x03uri\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x1d\n" +
@@ -771,7 +781,7 @@ const file_teleport_lib_teleterm_v1_cluster_proto_rawDesc = "" +
" \x01(\tR\fproxyVersion\x12N\n" +
"\x0eshow_resources\x18\v \x01(\x0e2'.teleport.lib.teleterm.v1.ShowResourcesR\rshowResources\x120\n" +
"\x14profile_status_error\x18\f \x01(\tR\x12profileStatusError\x12\x19\n" +
- "\bsso_host\x18\r \x01(\tR\assoHost\"\xaa\x04\n" +
+ "\bsso_host\x18\r \x01(\tR\assoHost\"\xe7\x04\n" +
"\fLoggedInUser\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +
"\x05roles\x18\x02 \x03(\tR\x05roles\x12/\n" +
@@ -782,7 +792,9 @@ const file_teleport_lib_teleterm_v1_cluster_proto_rawDesc = "" +
"\tuser_type\x18\b \x01(\x0e2/.teleport.lib.teleterm.v1.LoggedInUser.UserTypeR\buserType\x12*\n" +
"\x11is_device_trusted\x18\t \x01(\bR\x0fisDeviceTrusted\x12]\n" +
"\x1atrusted_device_requirement\x18\n" +
- " \x01(\x0e2\x1f.types.TrustedDeviceRequirementR\x18trustedDeviceRequirement\"M\n" +
+ " \x01(\x0e2\x1f.types.TrustedDeviceRequirementR\x18trustedDeviceRequirement\x12;\n" +
+ "\vvalid_until\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\n" +
+ "validUntil\"M\n" +
"\bUserType\x12\x19\n" +
"\x15USER_TYPE_UNSPECIFIED\x10\x00\x12\x13\n" +
"\x0fUSER_TYPE_LOCAL\x10\x01\x12\x11\n" +
@@ -844,6 +856,7 @@ var file_teleport_lib_teleterm_v1_cluster_proto_goTypes = []any{
(*ResourceAccess)(nil), // 5: teleport.lib.teleterm.v1.ResourceAccess
(*Features)(nil), // 6: teleport.lib.teleterm.v1.Features
(types.TrustedDeviceRequirement)(0), // 7: types.TrustedDeviceRequirement
+ (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp
}
var file_teleport_lib_teleterm_v1_cluster_proto_depIdxs = []int32{
3, // 0: teleport.lib.teleterm.v1.Cluster.logged_in_user:type_name -> teleport.lib.teleterm.v1.LoggedInUser
@@ -852,24 +865,25 @@ var file_teleport_lib_teleterm_v1_cluster_proto_depIdxs = []int32{
4, // 3: teleport.lib.teleterm.v1.LoggedInUser.acl:type_name -> teleport.lib.teleterm.v1.ACL
1, // 4: teleport.lib.teleterm.v1.LoggedInUser.user_type:type_name -> teleport.lib.teleterm.v1.LoggedInUser.UserType
7, // 5: teleport.lib.teleterm.v1.LoggedInUser.trusted_device_requirement:type_name -> types.TrustedDeviceRequirement
- 5, // 6: teleport.lib.teleterm.v1.ACL.auth_connectors:type_name -> teleport.lib.teleterm.v1.ResourceAccess
- 5, // 7: teleport.lib.teleterm.v1.ACL.roles:type_name -> teleport.lib.teleterm.v1.ResourceAccess
- 5, // 8: teleport.lib.teleterm.v1.ACL.users:type_name -> teleport.lib.teleterm.v1.ResourceAccess
- 5, // 9: teleport.lib.teleterm.v1.ACL.trusted_clusters:type_name -> teleport.lib.teleterm.v1.ResourceAccess
- 5, // 10: teleport.lib.teleterm.v1.ACL.events:type_name -> teleport.lib.teleterm.v1.ResourceAccess
- 5, // 11: teleport.lib.teleterm.v1.ACL.tokens:type_name -> teleport.lib.teleterm.v1.ResourceAccess
- 5, // 12: teleport.lib.teleterm.v1.ACL.servers:type_name -> teleport.lib.teleterm.v1.ResourceAccess
- 5, // 13: teleport.lib.teleterm.v1.ACL.apps:type_name -> teleport.lib.teleterm.v1.ResourceAccess
- 5, // 14: teleport.lib.teleterm.v1.ACL.dbs:type_name -> teleport.lib.teleterm.v1.ResourceAccess
- 5, // 15: teleport.lib.teleterm.v1.ACL.kubeservers:type_name -> teleport.lib.teleterm.v1.ResourceAccess
- 5, // 16: teleport.lib.teleterm.v1.ACL.access_requests:type_name -> teleport.lib.teleterm.v1.ResourceAccess
- 5, // 17: teleport.lib.teleterm.v1.ACL.recorded_sessions:type_name -> teleport.lib.teleterm.v1.ResourceAccess
- 5, // 18: teleport.lib.teleterm.v1.ACL.active_sessions:type_name -> teleport.lib.teleterm.v1.ResourceAccess
- 19, // [19:19] is the sub-list for method output_type
- 19, // [19:19] is the sub-list for method input_type
- 19, // [19:19] is the sub-list for extension type_name
- 19, // [19:19] is the sub-list for extension extendee
- 0, // [0:19] is the sub-list for field type_name
+ 8, // 6: teleport.lib.teleterm.v1.LoggedInUser.valid_until:type_name -> google.protobuf.Timestamp
+ 5, // 7: teleport.lib.teleterm.v1.ACL.auth_connectors:type_name -> teleport.lib.teleterm.v1.ResourceAccess
+ 5, // 8: teleport.lib.teleterm.v1.ACL.roles:type_name -> teleport.lib.teleterm.v1.ResourceAccess
+ 5, // 9: teleport.lib.teleterm.v1.ACL.users:type_name -> teleport.lib.teleterm.v1.ResourceAccess
+ 5, // 10: teleport.lib.teleterm.v1.ACL.trusted_clusters:type_name -> teleport.lib.teleterm.v1.ResourceAccess
+ 5, // 11: teleport.lib.teleterm.v1.ACL.events:type_name -> teleport.lib.teleterm.v1.ResourceAccess
+ 5, // 12: teleport.lib.teleterm.v1.ACL.tokens:type_name -> teleport.lib.teleterm.v1.ResourceAccess
+ 5, // 13: teleport.lib.teleterm.v1.ACL.servers:type_name -> teleport.lib.teleterm.v1.ResourceAccess
+ 5, // 14: teleport.lib.teleterm.v1.ACL.apps:type_name -> teleport.lib.teleterm.v1.ResourceAccess
+ 5, // 15: teleport.lib.teleterm.v1.ACL.dbs:type_name -> teleport.lib.teleterm.v1.ResourceAccess
+ 5, // 16: teleport.lib.teleterm.v1.ACL.kubeservers:type_name -> teleport.lib.teleterm.v1.ResourceAccess
+ 5, // 17: teleport.lib.teleterm.v1.ACL.access_requests:type_name -> teleport.lib.teleterm.v1.ResourceAccess
+ 5, // 18: teleport.lib.teleterm.v1.ACL.recorded_sessions:type_name -> teleport.lib.teleterm.v1.ResourceAccess
+ 5, // 19: teleport.lib.teleterm.v1.ACL.active_sessions:type_name -> teleport.lib.teleterm.v1.ResourceAccess
+ 20, // [20:20] is the sub-list for method output_type
+ 20, // [20:20] is the sub-list for method input_type
+ 20, // [20:20] is the sub-list for extension type_name
+ 20, // [20:20] is the sub-list for extension extendee
+ 0, // [0:20] is the sub-list for field type_name
}
func init() { file_teleport_lib_teleterm_v1_cluster_proto_init() }
diff --git a/gen/proto/go/teleport/lib/teleterm/v1/service.pb.go b/gen/proto/go/teleport/lib/teleterm/v1/service.pb.go
index e7512de53b669..7703b7d64d913 100644
--- a/gen/proto/go/teleport/lib/teleterm/v1/service.pb.go
+++ b/gen/proto/go/teleport/lib/teleterm/v1/service.pb.go
@@ -239,28 +239,28 @@ func (*EmptyResponse) Descriptor() ([]byte, []int) {
return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{0}
}
-// RemoveClusterRequest describes RemoveClusterRequest
-type RemoveClusterRequest struct {
+// GetClusterRequest describes GetClusterRequest
+type GetClusterRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ClusterUri string `protobuf:"bytes,1,opt,name=cluster_uri,json=clusterUri,proto3" json:"cluster_uri,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
-func (x *RemoveClusterRequest) Reset() {
- *x = RemoveClusterRequest{}
+func (x *GetClusterRequest) Reset() {
+ *x = GetClusterRequest{}
mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
-func (x *RemoveClusterRequest) String() string {
+func (x *GetClusterRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*RemoveClusterRequest) ProtoMessage() {}
+func (*GetClusterRequest) ProtoMessage() {}
-func (x *RemoveClusterRequest) ProtoReflect() protoreflect.Message {
+func (x *GetClusterRequest) ProtoReflect() protoreflect.Message {
mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -272,40 +272,41 @@ func (x *RemoveClusterRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use RemoveClusterRequest.ProtoReflect.Descriptor instead.
-func (*RemoveClusterRequest) Descriptor() ([]byte, []int) {
+// Deprecated: Use GetClusterRequest.ProtoReflect.Descriptor instead.
+func (*GetClusterRequest) Descriptor() ([]byte, []int) {
return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{1}
}
-func (x *RemoveClusterRequest) GetClusterUri() string {
+func (x *GetClusterRequest) GetClusterUri() string {
if x != nil {
return x.ClusterUri
}
return ""
}
-// GetClusterRequest describes GetClusterRequest
-type GetClusterRequest struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- ClusterUri string `protobuf:"bytes,1,opt,name=cluster_uri,json=clusterUri,proto3" json:"cluster_uri,omitempty"`
+type LogoutRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ ClusterUri string `protobuf:"bytes,1,opt,name=cluster_uri,json=clusterUri,proto3" json:"cluster_uri,omitempty"`
+ // Whether to remove the associated YAML profile after logout.
+ RemoveProfile bool `protobuf:"varint,2,opt,name=remove_profile,json=removeProfile,proto3" json:"remove_profile,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
-func (x *GetClusterRequest) Reset() {
- *x = GetClusterRequest{}
+func (x *LogoutRequest) Reset() {
+ *x = LogoutRequest{}
mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
-func (x *GetClusterRequest) String() string {
+func (x *LogoutRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*GetClusterRequest) ProtoMessage() {}
+func (*LogoutRequest) ProtoMessage() {}
-func (x *GetClusterRequest) ProtoReflect() protoreflect.Message {
+func (x *LogoutRequest) ProtoReflect() protoreflect.Message {
mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -317,40 +318,46 @@ func (x *GetClusterRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use GetClusterRequest.ProtoReflect.Descriptor instead.
-func (*GetClusterRequest) Descriptor() ([]byte, []int) {
+// Deprecated: Use LogoutRequest.ProtoReflect.Descriptor instead.
+func (*LogoutRequest) Descriptor() ([]byte, []int) {
return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{2}
}
-func (x *GetClusterRequest) GetClusterUri() string {
+func (x *LogoutRequest) GetClusterUri() string {
if x != nil {
return x.ClusterUri
}
return ""
}
-// LogoutRequest describes LogoutRequest
-type LogoutRequest struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- ClusterUri string `protobuf:"bytes,1,opt,name=cluster_uri,json=clusterUri,proto3" json:"cluster_uri,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
+func (x *LogoutRequest) GetRemoveProfile() bool {
+ if x != nil {
+ return x.RemoveProfile
+ }
+ return false
}
-func (x *LogoutRequest) Reset() {
- *x = LogoutRequest{}
+type ClearStaleClusterClientsRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ RootClusterUri string `protobuf:"bytes,1,opt,name=root_cluster_uri,json=rootClusterUri,proto3" json:"root_cluster_uri,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ClearStaleClusterClientsRequest) Reset() {
+ *x = ClearStaleClusterClientsRequest{}
mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
-func (x *LogoutRequest) String() string {
+func (x *ClearStaleClusterClientsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*LogoutRequest) ProtoMessage() {}
+func (*ClearStaleClusterClientsRequest) ProtoMessage() {}
-func (x *LogoutRequest) ProtoReflect() protoreflect.Message {
+func (x *ClearStaleClusterClientsRequest) ProtoReflect() protoreflect.Message {
mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -362,18 +369,54 @@ func (x *LogoutRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use LogoutRequest.ProtoReflect.Descriptor instead.
-func (*LogoutRequest) Descriptor() ([]byte, []int) {
+// Deprecated: Use ClearStaleClusterClientsRequest.ProtoReflect.Descriptor instead.
+func (*ClearStaleClusterClientsRequest) Descriptor() ([]byte, []int) {
return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{3}
}
-func (x *LogoutRequest) GetClusterUri() string {
+func (x *ClearStaleClusterClientsRequest) GetRootClusterUri() string {
if x != nil {
- return x.ClusterUri
+ return x.RootClusterUri
}
return ""
}
+type ClearStaleClusterClientsResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ClearStaleClusterClientsResponse) Reset() {
+ *x = ClearStaleClusterClientsResponse{}
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ClearStaleClusterClientsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ClearStaleClusterClientsResponse) ProtoMessage() {}
+
+func (x *ClearStaleClusterClientsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[4]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ClearStaleClusterClientsResponse.ProtoReflect.Descriptor instead.
+func (*ClearStaleClusterClientsResponse) Descriptor() ([]byte, []int) {
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{4}
+}
+
type StartHeadlessWatcherRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
RootClusterUri string `protobuf:"bytes,1,opt,name=root_cluster_uri,json=rootClusterUri,proto3" json:"root_cluster_uri,omitempty"`
@@ -383,7 +426,7 @@ type StartHeadlessWatcherRequest struct {
func (x *StartHeadlessWatcherRequest) Reset() {
*x = StartHeadlessWatcherRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[4]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -395,7 +438,7 @@ func (x *StartHeadlessWatcherRequest) String() string {
func (*StartHeadlessWatcherRequest) ProtoMessage() {}
func (x *StartHeadlessWatcherRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[4]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -408,7 +451,7 @@ func (x *StartHeadlessWatcherRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use StartHeadlessWatcherRequest.ProtoReflect.Descriptor instead.
func (*StartHeadlessWatcherRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{4}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{5}
}
func (x *StartHeadlessWatcherRequest) GetRootClusterUri() string {
@@ -426,7 +469,7 @@ type StartHeadlessWatcherResponse struct {
func (x *StartHeadlessWatcherResponse) Reset() {
*x = StartHeadlessWatcherResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[5]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -438,7 +481,7 @@ func (x *StartHeadlessWatcherResponse) String() string {
func (*StartHeadlessWatcherResponse) ProtoMessage() {}
func (x *StartHeadlessWatcherResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[5]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -451,7 +494,7 @@ func (x *StartHeadlessWatcherResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use StartHeadlessWatcherResponse.ProtoReflect.Descriptor instead.
func (*StartHeadlessWatcherResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{5}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{6}
}
type GetAccessRequestRequest struct {
@@ -465,7 +508,7 @@ type GetAccessRequestRequest struct {
func (x *GetAccessRequestRequest) Reset() {
*x = GetAccessRequestRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[6]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -477,7 +520,7 @@ func (x *GetAccessRequestRequest) String() string {
func (*GetAccessRequestRequest) ProtoMessage() {}
func (x *GetAccessRequestRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[6]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -490,7 +533,7 @@ func (x *GetAccessRequestRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetAccessRequestRequest.ProtoReflect.Descriptor instead.
func (*GetAccessRequestRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{6}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{7}
}
func (x *GetAccessRequestRequest) GetClusterUri() string {
@@ -517,7 +560,7 @@ type GetAccessRequestsRequest struct {
func (x *GetAccessRequestsRequest) Reset() {
*x = GetAccessRequestsRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[7]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -529,7 +572,7 @@ func (x *GetAccessRequestsRequest) String() string {
func (*GetAccessRequestsRequest) ProtoMessage() {}
func (x *GetAccessRequestsRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[7]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -542,7 +585,7 @@ func (x *GetAccessRequestsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetAccessRequestsRequest.ProtoReflect.Descriptor instead.
func (*GetAccessRequestsRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{7}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{8}
}
func (x *GetAccessRequestsRequest) GetClusterUri() string {
@@ -561,7 +604,7 @@ type GetAccessRequestResponse struct {
func (x *GetAccessRequestResponse) Reset() {
*x = GetAccessRequestResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[8]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -573,7 +616,7 @@ func (x *GetAccessRequestResponse) String() string {
func (*GetAccessRequestResponse) ProtoMessage() {}
func (x *GetAccessRequestResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[8]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -586,7 +629,7 @@ func (x *GetAccessRequestResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetAccessRequestResponse.ProtoReflect.Descriptor instead.
func (*GetAccessRequestResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{8}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{9}
}
func (x *GetAccessRequestResponse) GetRequest() *AccessRequest {
@@ -605,7 +648,7 @@ type GetAccessRequestsResponse struct {
func (x *GetAccessRequestsResponse) Reset() {
*x = GetAccessRequestsResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[9]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -617,7 +660,7 @@ func (x *GetAccessRequestsResponse) String() string {
func (*GetAccessRequestsResponse) ProtoMessage() {}
func (x *GetAccessRequestsResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[9]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -630,7 +673,7 @@ func (x *GetAccessRequestsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetAccessRequestsResponse.ProtoReflect.Descriptor instead.
func (*GetAccessRequestsResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{9}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{10}
}
func (x *GetAccessRequestsResponse) GetRequests() []*AccessRequest {
@@ -650,7 +693,7 @@ type DeleteAccessRequestRequest struct {
func (x *DeleteAccessRequestRequest) Reset() {
*x = DeleteAccessRequestRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[10]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -662,7 +705,7 @@ func (x *DeleteAccessRequestRequest) String() string {
func (*DeleteAccessRequestRequest) ProtoMessage() {}
func (x *DeleteAccessRequestRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[10]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -675,7 +718,7 @@ func (x *DeleteAccessRequestRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeleteAccessRequestRequest.ProtoReflect.Descriptor instead.
func (*DeleteAccessRequestRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{10}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{11}
}
func (x *DeleteAccessRequestRequest) GetRootClusterUri() string {
@@ -718,7 +761,7 @@ type CreateAccessRequestRequest struct {
func (x *CreateAccessRequestRequest) Reset() {
*x = CreateAccessRequestRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[11]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -730,7 +773,7 @@ func (x *CreateAccessRequestRequest) String() string {
func (*CreateAccessRequestRequest) ProtoMessage() {}
func (x *CreateAccessRequestRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[11]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -743,7 +786,7 @@ func (x *CreateAccessRequestRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateAccessRequestRequest.ProtoReflect.Descriptor instead.
func (*CreateAccessRequestRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{11}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{12}
}
func (x *CreateAccessRequestRequest) GetRootClusterUri() string {
@@ -818,7 +861,7 @@ type CreateAccessRequestResponse struct {
func (x *CreateAccessRequestResponse) Reset() {
*x = CreateAccessRequestResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[12]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -830,7 +873,7 @@ func (x *CreateAccessRequestResponse) String() string {
func (*CreateAccessRequestResponse) ProtoMessage() {}
func (x *CreateAccessRequestResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[12]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -843,7 +886,7 @@ func (x *CreateAccessRequestResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateAccessRequestResponse.ProtoReflect.Descriptor instead.
func (*CreateAccessRequestResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{12}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{13}
}
func (x *CreateAccessRequestResponse) GetRequest() *AccessRequest {
@@ -864,7 +907,7 @@ type AssumeRoleRequest struct {
func (x *AssumeRoleRequest) Reset() {
*x = AssumeRoleRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[13]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -876,7 +919,7 @@ func (x *AssumeRoleRequest) String() string {
func (*AssumeRoleRequest) ProtoMessage() {}
func (x *AssumeRoleRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[13]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -889,7 +932,7 @@ func (x *AssumeRoleRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use AssumeRoleRequest.ProtoReflect.Descriptor instead.
func (*AssumeRoleRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{13}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{14}
}
func (x *AssumeRoleRequest) GetRootClusterUri() string {
@@ -923,7 +966,7 @@ type GetRequestableRolesRequest struct {
func (x *GetRequestableRolesRequest) Reset() {
*x = GetRequestableRolesRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[14]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -935,7 +978,7 @@ func (x *GetRequestableRolesRequest) String() string {
func (*GetRequestableRolesRequest) ProtoMessage() {}
func (x *GetRequestableRolesRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[14]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[15]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -948,7 +991,7 @@ func (x *GetRequestableRolesRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetRequestableRolesRequest.ProtoReflect.Descriptor instead.
func (*GetRequestableRolesRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{14}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{15}
}
func (x *GetRequestableRolesRequest) GetClusterUri() string {
@@ -975,7 +1018,7 @@ type GetRequestableRolesResponse struct {
func (x *GetRequestableRolesResponse) Reset() {
*x = GetRequestableRolesResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[15]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -987,7 +1030,7 @@ func (x *GetRequestableRolesResponse) String() string {
func (*GetRequestableRolesResponse) ProtoMessage() {}
func (x *GetRequestableRolesResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[15]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1000,7 +1043,7 @@ func (x *GetRequestableRolesResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetRequestableRolesResponse.ProtoReflect.Descriptor instead.
func (*GetRequestableRolesResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{15}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{16}
}
func (x *GetRequestableRolesResponse) GetRoles() []string {
@@ -1032,7 +1075,7 @@ type ReviewAccessRequestRequest struct {
func (x *ReviewAccessRequestRequest) Reset() {
*x = ReviewAccessRequestRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[16]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1044,7 +1087,7 @@ func (x *ReviewAccessRequestRequest) String() string {
func (*ReviewAccessRequestRequest) ProtoMessage() {}
func (x *ReviewAccessRequestRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[16]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1057,7 +1100,7 @@ func (x *ReviewAccessRequestRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ReviewAccessRequestRequest.ProtoReflect.Descriptor instead.
func (*ReviewAccessRequestRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{16}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{17}
}
func (x *ReviewAccessRequestRequest) GetRootClusterUri() string {
@@ -1111,7 +1154,7 @@ type ReviewAccessRequestResponse struct {
func (x *ReviewAccessRequestResponse) Reset() {
*x = ReviewAccessRequestResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[17]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1123,7 +1166,7 @@ func (x *ReviewAccessRequestResponse) String() string {
func (*ReviewAccessRequestResponse) ProtoMessage() {}
func (x *ReviewAccessRequestResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[17]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1136,7 +1179,7 @@ func (x *ReviewAccessRequestResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ReviewAccessRequestResponse.ProtoReflect.Descriptor instead.
func (*ReviewAccessRequestResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{17}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{18}
}
func (x *ReviewAccessRequestResponse) GetRequest() *AccessRequest {
@@ -1158,7 +1201,7 @@ type PromoteAccessRequestRequest struct {
func (x *PromoteAccessRequestRequest) Reset() {
*x = PromoteAccessRequestRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[18]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1170,7 +1213,7 @@ func (x *PromoteAccessRequestRequest) String() string {
func (*PromoteAccessRequestRequest) ProtoMessage() {}
func (x *PromoteAccessRequestRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[18]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1183,7 +1226,7 @@ func (x *PromoteAccessRequestRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use PromoteAccessRequestRequest.ProtoReflect.Descriptor instead.
func (*PromoteAccessRequestRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{18}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{19}
}
func (x *PromoteAccessRequestRequest) GetRootClusterUri() string {
@@ -1223,7 +1266,7 @@ type PromoteAccessRequestResponse struct {
func (x *PromoteAccessRequestResponse) Reset() {
*x = PromoteAccessRequestResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[19]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1235,7 +1278,7 @@ func (x *PromoteAccessRequestResponse) String() string {
func (*PromoteAccessRequestResponse) ProtoMessage() {}
func (x *PromoteAccessRequestResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[19]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1248,7 +1291,7 @@ func (x *PromoteAccessRequestResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use PromoteAccessRequestResponse.ProtoReflect.Descriptor instead.
func (*PromoteAccessRequestResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{19}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{20}
}
func (x *PromoteAccessRequestResponse) GetRequest() *AccessRequest {
@@ -1268,7 +1311,7 @@ type GetSuggestedAccessListsRequest struct {
func (x *GetSuggestedAccessListsRequest) Reset() {
*x = GetSuggestedAccessListsRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[20]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1280,7 +1323,7 @@ func (x *GetSuggestedAccessListsRequest) String() string {
func (*GetSuggestedAccessListsRequest) ProtoMessage() {}
func (x *GetSuggestedAccessListsRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[20]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1293,7 +1336,7 @@ func (x *GetSuggestedAccessListsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetSuggestedAccessListsRequest.ProtoReflect.Descriptor instead.
func (*GetSuggestedAccessListsRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{20}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{21}
}
func (x *GetSuggestedAccessListsRequest) GetRootClusterUri() string {
@@ -1319,7 +1362,7 @@ type GetSuggestedAccessListsResponse struct {
func (x *GetSuggestedAccessListsResponse) Reset() {
*x = GetSuggestedAccessListsResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[21]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1331,7 +1374,7 @@ func (x *GetSuggestedAccessListsResponse) String() string {
func (*GetSuggestedAccessListsResponse) ProtoMessage() {}
func (x *GetSuggestedAccessListsResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[21]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1344,7 +1387,7 @@ func (x *GetSuggestedAccessListsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetSuggestedAccessListsResponse.ProtoReflect.Descriptor instead.
func (*GetSuggestedAccessListsResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{21}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{22}
}
func (x *GetSuggestedAccessListsResponse) GetAccessLists() []*v1.AccessList {
@@ -1383,7 +1426,7 @@ type ListKubernetesResourcesRequest struct {
func (x *ListKubernetesResourcesRequest) Reset() {
*x = ListKubernetesResourcesRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[22]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1395,7 +1438,7 @@ func (x *ListKubernetesResourcesRequest) String() string {
func (*ListKubernetesResourcesRequest) ProtoMessage() {}
func (x *ListKubernetesResourcesRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[22]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[23]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1408,7 +1451,7 @@ func (x *ListKubernetesResourcesRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListKubernetesResourcesRequest.ProtoReflect.Descriptor instead.
func (*ListKubernetesResourcesRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{22}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{23}
}
func (x *ListKubernetesResourcesRequest) GetClusterUri() string {
@@ -1483,7 +1526,7 @@ type ListKubernetesResourcesResponse struct {
func (x *ListKubernetesResourcesResponse) Reset() {
*x = ListKubernetesResourcesResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[23]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1495,7 +1538,7 @@ func (x *ListKubernetesResourcesResponse) String() string {
func (*ListKubernetesResourcesResponse) ProtoMessage() {}
func (x *ListKubernetesResourcesResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[23]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[24]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1508,7 +1551,7 @@ func (x *ListKubernetesResourcesResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListKubernetesResourcesResponse.ProtoReflect.Descriptor instead.
func (*ListKubernetesResourcesResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{23}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{24}
}
func (x *ListKubernetesResourcesResponse) GetResources() []*KubeResource {
@@ -1535,7 +1578,7 @@ type ListKubernetesServersRequest struct {
func (x *ListKubernetesServersRequest) Reset() {
*x = ListKubernetesServersRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[24]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1547,7 +1590,7 @@ func (x *ListKubernetesServersRequest) String() string {
func (*ListKubernetesServersRequest) ProtoMessage() {}
func (x *ListKubernetesServersRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[24]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[25]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1560,7 +1603,7 @@ func (x *ListKubernetesServersRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListKubernetesServersRequest.ProtoReflect.Descriptor instead.
func (*ListKubernetesServersRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{24}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{25}
}
func (x *ListKubernetesServersRequest) GetPageSize() int32 {
@@ -1604,7 +1647,7 @@ type ListKubernetesServersResponse struct {
func (x *ListKubernetesServersResponse) Reset() {
*x = ListKubernetesServersResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[25]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1616,7 +1659,7 @@ func (x *ListKubernetesServersResponse) String() string {
func (*ListKubernetesServersResponse) ProtoMessage() {}
func (x *ListKubernetesServersResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[25]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[26]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1629,7 +1672,7 @@ func (x *ListKubernetesServersResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListKubernetesServersResponse.ProtoReflect.Descriptor instead.
func (*ListKubernetesServersResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{25}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{26}
}
func (x *ListKubernetesServersResponse) GetResources() []*KubeServer {
@@ -1656,7 +1699,7 @@ type CredentialInfo struct {
func (x *CredentialInfo) Reset() {
*x = CredentialInfo{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[26]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1668,7 +1711,7 @@ func (x *CredentialInfo) String() string {
func (*CredentialInfo) ProtoMessage() {}
func (x *CredentialInfo) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[26]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[27]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1681,7 +1724,7 @@ func (x *CredentialInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use CredentialInfo.ProtoReflect.Descriptor instead.
func (*CredentialInfo) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{26}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{27}
}
func (x *CredentialInfo) GetUsername() string {
@@ -1703,7 +1746,7 @@ type LoginPasswordlessResponse struct {
func (x *LoginPasswordlessResponse) Reset() {
*x = LoginPasswordlessResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[27]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1715,7 +1758,7 @@ func (x *LoginPasswordlessResponse) String() string {
func (*LoginPasswordlessResponse) ProtoMessage() {}
func (x *LoginPasswordlessResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[27]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[28]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1728,7 +1771,7 @@ func (x *LoginPasswordlessResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use LoginPasswordlessResponse.ProtoReflect.Descriptor instead.
func (*LoginPasswordlessResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{27}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{28}
}
func (x *LoginPasswordlessResponse) GetPrompt() PasswordlessPrompt {
@@ -1760,7 +1803,7 @@ type LoginPasswordlessRequest struct {
func (x *LoginPasswordlessRequest) Reset() {
*x = LoginPasswordlessRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[28]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[29]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1772,7 +1815,7 @@ func (x *LoginPasswordlessRequest) String() string {
func (*LoginPasswordlessRequest) ProtoMessage() {}
func (x *LoginPasswordlessRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[28]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[29]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1785,7 +1828,7 @@ func (x *LoginPasswordlessRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use LoginPasswordlessRequest.ProtoReflect.Descriptor instead.
func (*LoginPasswordlessRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{28}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{29}
}
func (x *LoginPasswordlessRequest) GetRequest() isLoginPasswordlessRequest_Request {
@@ -1861,7 +1904,7 @@ type FileTransferRequest struct {
func (x *FileTransferRequest) Reset() {
*x = FileTransferRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[29]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[30]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1873,7 +1916,7 @@ func (x *FileTransferRequest) String() string {
func (*FileTransferRequest) ProtoMessage() {}
func (x *FileTransferRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[29]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[30]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1886,7 +1929,7 @@ func (x *FileTransferRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FileTransferRequest.ProtoReflect.Descriptor instead.
func (*FileTransferRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{29}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{30}
}
func (x *FileTransferRequest) GetLogin() string {
@@ -1933,7 +1976,7 @@ type FileTransferProgress struct {
func (x *FileTransferProgress) Reset() {
*x = FileTransferProgress{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[30]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[31]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1945,7 +1988,7 @@ func (x *FileTransferProgress) String() string {
func (*FileTransferProgress) ProtoMessage() {}
func (x *FileTransferProgress) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[30]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[31]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1958,7 +2001,7 @@ func (x *FileTransferProgress) ProtoReflect() protoreflect.Message {
// Deprecated: Use FileTransferProgress.ProtoReflect.Descriptor instead.
func (*FileTransferProgress) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{30}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{31}
}
func (x *FileTransferProgress) GetPercentage() uint32 {
@@ -1984,7 +2027,7 @@ type LoginRequest struct {
func (x *LoginRequest) Reset() {
*x = LoginRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[31]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[32]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1996,7 +2039,7 @@ func (x *LoginRequest) String() string {
func (*LoginRequest) ProtoMessage() {}
func (x *LoginRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[31]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[32]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2009,7 +2052,7 @@ func (x *LoginRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use LoginRequest.ProtoReflect.Descriptor instead.
func (*LoginRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{31}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{32}
}
func (x *LoginRequest) GetClusterUri() string {
@@ -2071,7 +2114,7 @@ type AddClusterRequest struct {
func (x *AddClusterRequest) Reset() {
*x = AddClusterRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[32]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[33]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2083,7 +2126,7 @@ func (x *AddClusterRequest) String() string {
func (*AddClusterRequest) ProtoMessage() {}
func (x *AddClusterRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[32]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[33]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2096,7 +2139,7 @@ func (x *AddClusterRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use AddClusterRequest.ProtoReflect.Descriptor instead.
func (*AddClusterRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{32}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{33}
}
func (x *AddClusterRequest) GetName() string {
@@ -2114,7 +2157,7 @@ type ListClustersRequest struct {
func (x *ListClustersRequest) Reset() {
*x = ListClustersRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[33]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[34]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2126,7 +2169,7 @@ func (x *ListClustersRequest) String() string {
func (*ListClustersRequest) ProtoMessage() {}
func (x *ListClustersRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[33]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[34]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2139,7 +2182,7 @@ func (x *ListClustersRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListClustersRequest.ProtoReflect.Descriptor instead.
func (*ListClustersRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{33}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{34}
}
type ListClustersResponse struct {
@@ -2151,7 +2194,7 @@ type ListClustersResponse struct {
func (x *ListClustersResponse) Reset() {
*x = ListClustersResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[34]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[35]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2163,7 +2206,7 @@ func (x *ListClustersResponse) String() string {
func (*ListClustersResponse) ProtoMessage() {}
func (x *ListClustersResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[34]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[35]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2176,7 +2219,7 @@ func (x *ListClustersResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListClustersResponse.ProtoReflect.Descriptor instead.
func (*ListClustersResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{34}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{35}
}
func (x *ListClustersResponse) GetClusters() []*Cluster {
@@ -2195,7 +2238,7 @@ type ListLeafClustersRequest struct {
func (x *ListLeafClustersRequest) Reset() {
*x = ListLeafClustersRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[35]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[36]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2207,7 +2250,7 @@ func (x *ListLeafClustersRequest) String() string {
func (*ListLeafClustersRequest) ProtoMessage() {}
func (x *ListLeafClustersRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[35]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[36]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2220,7 +2263,7 @@ func (x *ListLeafClustersRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListLeafClustersRequest.ProtoReflect.Descriptor instead.
func (*ListLeafClustersRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{35}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{36}
}
func (x *ListLeafClustersRequest) GetClusterUri() string {
@@ -2239,7 +2282,7 @@ type ListDatabaseUsersRequest struct {
func (x *ListDatabaseUsersRequest) Reset() {
*x = ListDatabaseUsersRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[36]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[37]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2251,7 +2294,7 @@ func (x *ListDatabaseUsersRequest) String() string {
func (*ListDatabaseUsersRequest) ProtoMessage() {}
func (x *ListDatabaseUsersRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[36]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[37]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2264,7 +2307,7 @@ func (x *ListDatabaseUsersRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListDatabaseUsersRequest.ProtoReflect.Descriptor instead.
func (*ListDatabaseUsersRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{36}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{37}
}
func (x *ListDatabaseUsersRequest) GetDbUri() string {
@@ -2283,7 +2326,7 @@ type ListDatabaseUsersResponse struct {
func (x *ListDatabaseUsersResponse) Reset() {
*x = ListDatabaseUsersResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[37]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[38]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2295,7 +2338,7 @@ func (x *ListDatabaseUsersResponse) String() string {
func (*ListDatabaseUsersResponse) ProtoMessage() {}
func (x *ListDatabaseUsersResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[37]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[38]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2308,7 +2351,7 @@ func (x *ListDatabaseUsersResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListDatabaseUsersResponse.ProtoReflect.Descriptor instead.
func (*ListDatabaseUsersResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{37}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{38}
}
func (x *ListDatabaseUsersResponse) GetUsers() []string {
@@ -2335,7 +2378,7 @@ type ListResourcesParams struct {
func (x *ListResourcesParams) Reset() {
*x = ListResourcesParams{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[38]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[39]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2347,7 +2390,7 @@ func (x *ListResourcesParams) String() string {
func (*ListResourcesParams) ProtoMessage() {}
func (x *ListResourcesParams) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[38]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[39]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2360,7 +2403,7 @@ func (x *ListResourcesParams) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListResourcesParams.ProtoReflect.Descriptor instead.
func (*ListResourcesParams) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{38}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{39}
}
func (x *ListResourcesParams) GetStartKey() string {
@@ -2401,7 +2444,7 @@ type ListDatabaseServersRequest struct {
func (x *ListDatabaseServersRequest) Reset() {
*x = ListDatabaseServersRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[39]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[40]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2413,7 +2456,7 @@ func (x *ListDatabaseServersRequest) String() string {
func (*ListDatabaseServersRequest) ProtoMessage() {}
func (x *ListDatabaseServersRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[39]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[40]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2426,7 +2469,7 @@ func (x *ListDatabaseServersRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListDatabaseServersRequest.ProtoReflect.Descriptor instead.
func (*ListDatabaseServersRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{39}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{40}
}
func (x *ListDatabaseServersRequest) GetClusterUri() string {
@@ -2453,7 +2496,7 @@ type ListDatabaseServersResponse struct {
func (x *ListDatabaseServersResponse) Reset() {
*x = ListDatabaseServersResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[40]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[41]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2465,7 +2508,7 @@ func (x *ListDatabaseServersResponse) String() string {
func (*ListDatabaseServersResponse) ProtoMessage() {}
func (x *ListDatabaseServersResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[40]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[41]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2478,7 +2521,7 @@ func (x *ListDatabaseServersResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListDatabaseServersResponse.ProtoReflect.Descriptor instead.
func (*ListDatabaseServersResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{40}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{41}
}
func (x *ListDatabaseServersResponse) GetResources() []*DatabaseServer {
@@ -2507,7 +2550,7 @@ type CreateGatewayRequest struct {
func (x *CreateGatewayRequest) Reset() {
*x = CreateGatewayRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[41]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[42]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2519,7 +2562,7 @@ func (x *CreateGatewayRequest) String() string {
func (*CreateGatewayRequest) ProtoMessage() {}
func (x *CreateGatewayRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[41]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[42]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2532,7 +2575,7 @@ func (x *CreateGatewayRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateGatewayRequest.ProtoReflect.Descriptor instead.
func (*CreateGatewayRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{41}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{42}
}
func (x *CreateGatewayRequest) GetTargetUri() string {
@@ -2571,7 +2614,7 @@ type ListGatewaysRequest struct {
func (x *ListGatewaysRequest) Reset() {
*x = ListGatewaysRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[42]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[43]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2583,7 +2626,7 @@ func (x *ListGatewaysRequest) String() string {
func (*ListGatewaysRequest) ProtoMessage() {}
func (x *ListGatewaysRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[42]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[43]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2596,7 +2639,7 @@ func (x *ListGatewaysRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListGatewaysRequest.ProtoReflect.Descriptor instead.
func (*ListGatewaysRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{42}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{43}
}
type ListGatewaysResponse struct {
@@ -2608,7 +2651,7 @@ type ListGatewaysResponse struct {
func (x *ListGatewaysResponse) Reset() {
*x = ListGatewaysResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[43]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[44]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2620,7 +2663,7 @@ func (x *ListGatewaysResponse) String() string {
func (*ListGatewaysResponse) ProtoMessage() {}
func (x *ListGatewaysResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[43]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[44]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2633,7 +2676,7 @@ func (x *ListGatewaysResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListGatewaysResponse.ProtoReflect.Descriptor instead.
func (*ListGatewaysResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{43}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{44}
}
func (x *ListGatewaysResponse) GetGateways() []*Gateway {
@@ -2652,7 +2695,7 @@ type RemoveGatewayRequest struct {
func (x *RemoveGatewayRequest) Reset() {
*x = RemoveGatewayRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[44]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[45]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2664,7 +2707,7 @@ func (x *RemoveGatewayRequest) String() string {
func (*RemoveGatewayRequest) ProtoMessage() {}
func (x *RemoveGatewayRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[44]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[45]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2677,7 +2720,7 @@ func (x *RemoveGatewayRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use RemoveGatewayRequest.ProtoReflect.Descriptor instead.
func (*RemoveGatewayRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{44}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{45}
}
func (x *RemoveGatewayRequest) GetGatewayUri() string {
@@ -2697,7 +2740,7 @@ type SetGatewayTargetSubresourceNameRequest struct {
func (x *SetGatewayTargetSubresourceNameRequest) Reset() {
*x = SetGatewayTargetSubresourceNameRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[45]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[46]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2709,7 +2752,7 @@ func (x *SetGatewayTargetSubresourceNameRequest) String() string {
func (*SetGatewayTargetSubresourceNameRequest) ProtoMessage() {}
func (x *SetGatewayTargetSubresourceNameRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[45]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[46]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2722,7 +2765,7 @@ func (x *SetGatewayTargetSubresourceNameRequest) ProtoReflect() protoreflect.Mes
// Deprecated: Use SetGatewayTargetSubresourceNameRequest.ProtoReflect.Descriptor instead.
func (*SetGatewayTargetSubresourceNameRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{45}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{46}
}
func (x *SetGatewayTargetSubresourceNameRequest) GetGatewayUri() string {
@@ -2749,7 +2792,7 @@ type SetGatewayLocalPortRequest struct {
func (x *SetGatewayLocalPortRequest) Reset() {
*x = SetGatewayLocalPortRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[46]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[47]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2761,7 +2804,7 @@ func (x *SetGatewayLocalPortRequest) String() string {
func (*SetGatewayLocalPortRequest) ProtoMessage() {}
func (x *SetGatewayLocalPortRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[46]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[47]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2774,7 +2817,7 @@ func (x *SetGatewayLocalPortRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SetGatewayLocalPortRequest.ProtoReflect.Descriptor instead.
func (*SetGatewayLocalPortRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{46}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{47}
}
func (x *SetGatewayLocalPortRequest) GetGatewayUri() string {
@@ -2800,7 +2843,7 @@ type GetAuthSettingsRequest struct {
func (x *GetAuthSettingsRequest) Reset() {
*x = GetAuthSettingsRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[47]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[48]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2812,7 +2855,7 @@ func (x *GetAuthSettingsRequest) String() string {
func (*GetAuthSettingsRequest) ProtoMessage() {}
func (x *GetAuthSettingsRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[47]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[48]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2825,7 +2868,7 @@ func (x *GetAuthSettingsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetAuthSettingsRequest.ProtoReflect.Descriptor instead.
func (*GetAuthSettingsRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{47}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{48}
}
func (x *GetAuthSettingsRequest) GetClusterUri() string {
@@ -2844,7 +2887,7 @@ type UpdateTshdEventsServerAddressRequest struct {
func (x *UpdateTshdEventsServerAddressRequest) Reset() {
*x = UpdateTshdEventsServerAddressRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[48]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[49]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2856,7 +2899,7 @@ func (x *UpdateTshdEventsServerAddressRequest) String() string {
func (*UpdateTshdEventsServerAddressRequest) ProtoMessage() {}
func (x *UpdateTshdEventsServerAddressRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[48]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[49]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2869,7 +2912,7 @@ func (x *UpdateTshdEventsServerAddressRequest) ProtoReflect() protoreflect.Messa
// Deprecated: Use UpdateTshdEventsServerAddressRequest.ProtoReflect.Descriptor instead.
func (*UpdateTshdEventsServerAddressRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{48}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{49}
}
func (x *UpdateTshdEventsServerAddressRequest) GetAddress() string {
@@ -2887,7 +2930,7 @@ type UpdateTshdEventsServerAddressResponse struct {
func (x *UpdateTshdEventsServerAddressResponse) Reset() {
*x = UpdateTshdEventsServerAddressResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[49]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[50]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2899,7 +2942,7 @@ func (x *UpdateTshdEventsServerAddressResponse) String() string {
func (*UpdateTshdEventsServerAddressResponse) ProtoMessage() {}
func (x *UpdateTshdEventsServerAddressResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[49]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[50]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2912,7 +2955,7 @@ func (x *UpdateTshdEventsServerAddressResponse) ProtoReflect() protoreflect.Mess
// Deprecated: Use UpdateTshdEventsServerAddressResponse.ProtoReflect.Descriptor instead.
func (*UpdateTshdEventsServerAddressResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{49}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{50}
}
type UpdateHeadlessAuthenticationStateRequest struct {
@@ -2926,7 +2969,7 @@ type UpdateHeadlessAuthenticationStateRequest struct {
func (x *UpdateHeadlessAuthenticationStateRequest) Reset() {
*x = UpdateHeadlessAuthenticationStateRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[50]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[51]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2938,7 +2981,7 @@ func (x *UpdateHeadlessAuthenticationStateRequest) String() string {
func (*UpdateHeadlessAuthenticationStateRequest) ProtoMessage() {}
func (x *UpdateHeadlessAuthenticationStateRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[50]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[51]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2951,7 +2994,7 @@ func (x *UpdateHeadlessAuthenticationStateRequest) ProtoReflect() protoreflect.M
// Deprecated: Use UpdateHeadlessAuthenticationStateRequest.ProtoReflect.Descriptor instead.
func (*UpdateHeadlessAuthenticationStateRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{50}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{51}
}
func (x *UpdateHeadlessAuthenticationStateRequest) GetRootClusterUri() string {
@@ -2983,7 +3026,7 @@ type UpdateHeadlessAuthenticationStateResponse struct {
func (x *UpdateHeadlessAuthenticationStateResponse) Reset() {
*x = UpdateHeadlessAuthenticationStateResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[51]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[52]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2995,7 +3038,7 @@ func (x *UpdateHeadlessAuthenticationStateResponse) String() string {
func (*UpdateHeadlessAuthenticationStateResponse) ProtoMessage() {}
func (x *UpdateHeadlessAuthenticationStateResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[51]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[52]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3008,7 +3051,7 @@ func (x *UpdateHeadlessAuthenticationStateResponse) ProtoReflect() protoreflect.
// Deprecated: Use UpdateHeadlessAuthenticationStateResponse.ProtoReflect.Descriptor instead.
func (*UpdateHeadlessAuthenticationStateResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{51}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{52}
}
type CreateConnectMyComputerRoleRequest struct {
@@ -3020,7 +3063,7 @@ type CreateConnectMyComputerRoleRequest struct {
func (x *CreateConnectMyComputerRoleRequest) Reset() {
*x = CreateConnectMyComputerRoleRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[52]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[53]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3032,7 +3075,7 @@ func (x *CreateConnectMyComputerRoleRequest) String() string {
func (*CreateConnectMyComputerRoleRequest) ProtoMessage() {}
func (x *CreateConnectMyComputerRoleRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[52]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[53]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3045,7 +3088,7 @@ func (x *CreateConnectMyComputerRoleRequest) ProtoReflect() protoreflect.Message
// Deprecated: Use CreateConnectMyComputerRoleRequest.ProtoReflect.Descriptor instead.
func (*CreateConnectMyComputerRoleRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{52}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{53}
}
func (x *CreateConnectMyComputerRoleRequest) GetRootClusterUri() string {
@@ -3066,7 +3109,7 @@ type CreateConnectMyComputerRoleResponse struct {
func (x *CreateConnectMyComputerRoleResponse) Reset() {
*x = CreateConnectMyComputerRoleResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[53]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[54]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3078,7 +3121,7 @@ func (x *CreateConnectMyComputerRoleResponse) String() string {
func (*CreateConnectMyComputerRoleResponse) ProtoMessage() {}
func (x *CreateConnectMyComputerRoleResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[53]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[54]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3091,7 +3134,7 @@ func (x *CreateConnectMyComputerRoleResponse) ProtoReflect() protoreflect.Messag
// Deprecated: Use CreateConnectMyComputerRoleResponse.ProtoReflect.Descriptor instead.
func (*CreateConnectMyComputerRoleResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{53}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{54}
}
func (x *CreateConnectMyComputerRoleResponse) GetCertsReloaded() bool {
@@ -3110,7 +3153,7 @@ type CreateConnectMyComputerNodeTokenRequest struct {
func (x *CreateConnectMyComputerNodeTokenRequest) Reset() {
*x = CreateConnectMyComputerNodeTokenRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[54]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[55]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3122,7 +3165,7 @@ func (x *CreateConnectMyComputerNodeTokenRequest) String() string {
func (*CreateConnectMyComputerNodeTokenRequest) ProtoMessage() {}
func (x *CreateConnectMyComputerNodeTokenRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[54]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[55]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3135,7 +3178,7 @@ func (x *CreateConnectMyComputerNodeTokenRequest) ProtoReflect() protoreflect.Me
// Deprecated: Use CreateConnectMyComputerNodeTokenRequest.ProtoReflect.Descriptor instead.
func (*CreateConnectMyComputerNodeTokenRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{54}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{55}
}
func (x *CreateConnectMyComputerNodeTokenRequest) GetRootClusterUri() string {
@@ -3154,7 +3197,7 @@ type CreateConnectMyComputerNodeTokenResponse struct {
func (x *CreateConnectMyComputerNodeTokenResponse) Reset() {
*x = CreateConnectMyComputerNodeTokenResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[55]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[56]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3166,7 +3209,7 @@ func (x *CreateConnectMyComputerNodeTokenResponse) String() string {
func (*CreateConnectMyComputerNodeTokenResponse) ProtoMessage() {}
func (x *CreateConnectMyComputerNodeTokenResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[55]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[56]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3179,7 +3222,7 @@ func (x *CreateConnectMyComputerNodeTokenResponse) ProtoReflect() protoreflect.M
// Deprecated: Use CreateConnectMyComputerNodeTokenResponse.ProtoReflect.Descriptor instead.
func (*CreateConnectMyComputerNodeTokenResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{55}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{56}
}
func (x *CreateConnectMyComputerNodeTokenResponse) GetToken() string {
@@ -3198,7 +3241,7 @@ type WaitForConnectMyComputerNodeJoinRequest struct {
func (x *WaitForConnectMyComputerNodeJoinRequest) Reset() {
*x = WaitForConnectMyComputerNodeJoinRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[56]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[57]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3210,7 +3253,7 @@ func (x *WaitForConnectMyComputerNodeJoinRequest) String() string {
func (*WaitForConnectMyComputerNodeJoinRequest) ProtoMessage() {}
func (x *WaitForConnectMyComputerNodeJoinRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[56]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[57]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3223,7 +3266,7 @@ func (x *WaitForConnectMyComputerNodeJoinRequest) ProtoReflect() protoreflect.Me
// Deprecated: Use WaitForConnectMyComputerNodeJoinRequest.ProtoReflect.Descriptor instead.
func (*WaitForConnectMyComputerNodeJoinRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{56}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{57}
}
func (x *WaitForConnectMyComputerNodeJoinRequest) GetRootClusterUri() string {
@@ -3242,7 +3285,7 @@ type WaitForConnectMyComputerNodeJoinResponse struct {
func (x *WaitForConnectMyComputerNodeJoinResponse) Reset() {
*x = WaitForConnectMyComputerNodeJoinResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[57]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[58]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3254,7 +3297,7 @@ func (x *WaitForConnectMyComputerNodeJoinResponse) String() string {
func (*WaitForConnectMyComputerNodeJoinResponse) ProtoMessage() {}
func (x *WaitForConnectMyComputerNodeJoinResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[57]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[58]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3267,7 +3310,7 @@ func (x *WaitForConnectMyComputerNodeJoinResponse) ProtoReflect() protoreflect.M
// Deprecated: Use WaitForConnectMyComputerNodeJoinResponse.ProtoReflect.Descriptor instead.
func (*WaitForConnectMyComputerNodeJoinResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{57}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{58}
}
func (x *WaitForConnectMyComputerNodeJoinResponse) GetServer() *Server {
@@ -3286,7 +3329,7 @@ type DeleteConnectMyComputerNodeRequest struct {
func (x *DeleteConnectMyComputerNodeRequest) Reset() {
*x = DeleteConnectMyComputerNodeRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[58]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[59]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3298,7 +3341,7 @@ func (x *DeleteConnectMyComputerNodeRequest) String() string {
func (*DeleteConnectMyComputerNodeRequest) ProtoMessage() {}
func (x *DeleteConnectMyComputerNodeRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[58]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[59]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3311,7 +3354,7 @@ func (x *DeleteConnectMyComputerNodeRequest) ProtoReflect() protoreflect.Message
// Deprecated: Use DeleteConnectMyComputerNodeRequest.ProtoReflect.Descriptor instead.
func (*DeleteConnectMyComputerNodeRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{58}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{59}
}
func (x *DeleteConnectMyComputerNodeRequest) GetRootClusterUri() string {
@@ -3329,7 +3372,7 @@ type DeleteConnectMyComputerNodeResponse struct {
func (x *DeleteConnectMyComputerNodeResponse) Reset() {
*x = DeleteConnectMyComputerNodeResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[59]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[60]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3341,7 +3384,7 @@ func (x *DeleteConnectMyComputerNodeResponse) String() string {
func (*DeleteConnectMyComputerNodeResponse) ProtoMessage() {}
func (x *DeleteConnectMyComputerNodeResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[59]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[60]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3354,7 +3397,7 @@ func (x *DeleteConnectMyComputerNodeResponse) ProtoReflect() protoreflect.Messag
// Deprecated: Use DeleteConnectMyComputerNodeResponse.ProtoReflect.Descriptor instead.
func (*DeleteConnectMyComputerNodeResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{59}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{60}
}
type GetConnectMyComputerNodeNameRequest struct {
@@ -3366,7 +3409,7 @@ type GetConnectMyComputerNodeNameRequest struct {
func (x *GetConnectMyComputerNodeNameRequest) Reset() {
*x = GetConnectMyComputerNodeNameRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[60]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[61]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3378,7 +3421,7 @@ func (x *GetConnectMyComputerNodeNameRequest) String() string {
func (*GetConnectMyComputerNodeNameRequest) ProtoMessage() {}
func (x *GetConnectMyComputerNodeNameRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[60]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[61]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3391,7 +3434,7 @@ func (x *GetConnectMyComputerNodeNameRequest) ProtoReflect() protoreflect.Messag
// Deprecated: Use GetConnectMyComputerNodeNameRequest.ProtoReflect.Descriptor instead.
func (*GetConnectMyComputerNodeNameRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{60}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{61}
}
func (x *GetConnectMyComputerNodeNameRequest) GetRootClusterUri() string {
@@ -3410,7 +3453,7 @@ type GetConnectMyComputerNodeNameResponse struct {
func (x *GetConnectMyComputerNodeNameResponse) Reset() {
*x = GetConnectMyComputerNodeNameResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[61]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[62]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3422,7 +3465,7 @@ func (x *GetConnectMyComputerNodeNameResponse) String() string {
func (*GetConnectMyComputerNodeNameResponse) ProtoMessage() {}
func (x *GetConnectMyComputerNodeNameResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[61]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[62]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3435,7 +3478,7 @@ func (x *GetConnectMyComputerNodeNameResponse) ProtoReflect() protoreflect.Messa
// Deprecated: Use GetConnectMyComputerNodeNameResponse.ProtoReflect.Descriptor instead.
func (*GetConnectMyComputerNodeNameResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{61}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{62}
}
func (x *GetConnectMyComputerNodeNameResponse) GetName() string {
@@ -3475,7 +3518,7 @@ type ListUnifiedResourcesRequest struct {
func (x *ListUnifiedResourcesRequest) Reset() {
*x = ListUnifiedResourcesRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[62]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[63]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3487,7 +3530,7 @@ func (x *ListUnifiedResourcesRequest) String() string {
func (*ListUnifiedResourcesRequest) ProtoMessage() {}
func (x *ListUnifiedResourcesRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[62]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[63]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3500,7 +3543,7 @@ func (x *ListUnifiedResourcesRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListUnifiedResourcesRequest.ProtoReflect.Descriptor instead.
func (*ListUnifiedResourcesRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{62}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{63}
}
func (x *ListUnifiedResourcesRequest) GetClusterUri() string {
@@ -3585,7 +3628,7 @@ type SortBy struct {
func (x *SortBy) Reset() {
*x = SortBy{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[63]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[64]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3597,7 +3640,7 @@ func (x *SortBy) String() string {
func (*SortBy) ProtoMessage() {}
func (x *SortBy) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[63]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[64]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3610,7 +3653,7 @@ func (x *SortBy) ProtoReflect() protoreflect.Message {
// Deprecated: Use SortBy.ProtoReflect.Descriptor instead.
func (*SortBy) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{63}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{64}
}
func (x *SortBy) GetIsDesc() bool {
@@ -3639,7 +3682,7 @@ type ListUnifiedResourcesResponse struct {
func (x *ListUnifiedResourcesResponse) Reset() {
*x = ListUnifiedResourcesResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[64]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[65]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3651,7 +3694,7 @@ func (x *ListUnifiedResourcesResponse) String() string {
func (*ListUnifiedResourcesResponse) ProtoMessage() {}
func (x *ListUnifiedResourcesResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[64]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[65]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3664,7 +3707,7 @@ func (x *ListUnifiedResourcesResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListUnifiedResourcesResponse.ProtoReflect.Descriptor instead.
func (*ListUnifiedResourcesResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{64}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{65}
}
func (x *ListUnifiedResourcesResponse) GetResources() []*PaginatedResource {
@@ -3698,7 +3741,7 @@ type PaginatedResource struct {
func (x *PaginatedResource) Reset() {
*x = PaginatedResource{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[65]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[66]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3710,7 +3753,7 @@ func (x *PaginatedResource) String() string {
func (*PaginatedResource) ProtoMessage() {}
func (x *PaginatedResource) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[65]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[66]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3723,7 +3766,7 @@ func (x *PaginatedResource) ProtoReflect() protoreflect.Message {
// Deprecated: Use PaginatedResource.ProtoReflect.Descriptor instead.
func (*PaginatedResource) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{65}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{66}
}
func (x *PaginatedResource) GetResource() isPaginatedResource_Resource {
@@ -3828,7 +3871,7 @@ type GetUserPreferencesRequest struct {
func (x *GetUserPreferencesRequest) Reset() {
*x = GetUserPreferencesRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[66]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[67]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3840,7 +3883,7 @@ func (x *GetUserPreferencesRequest) String() string {
func (*GetUserPreferencesRequest) ProtoMessage() {}
func (x *GetUserPreferencesRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[66]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[67]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3853,7 +3896,7 @@ func (x *GetUserPreferencesRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetUserPreferencesRequest.ProtoReflect.Descriptor instead.
func (*GetUserPreferencesRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{66}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{67}
}
func (x *GetUserPreferencesRequest) GetClusterUri() string {
@@ -3872,7 +3915,7 @@ type GetUserPreferencesResponse struct {
func (x *GetUserPreferencesResponse) Reset() {
*x = GetUserPreferencesResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[67]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[68]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3884,7 +3927,7 @@ func (x *GetUserPreferencesResponse) String() string {
func (*GetUserPreferencesResponse) ProtoMessage() {}
func (x *GetUserPreferencesResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[67]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[68]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3897,7 +3940,7 @@ func (x *GetUserPreferencesResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetUserPreferencesResponse.ProtoReflect.Descriptor instead.
func (*GetUserPreferencesResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{67}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{68}
}
func (x *GetUserPreferencesResponse) GetUserPreferences() *UserPreferences {
@@ -3917,7 +3960,7 @@ type UpdateUserPreferencesRequest struct {
func (x *UpdateUserPreferencesRequest) Reset() {
*x = UpdateUserPreferencesRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[68]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[69]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3929,7 +3972,7 @@ func (x *UpdateUserPreferencesRequest) String() string {
func (*UpdateUserPreferencesRequest) ProtoMessage() {}
func (x *UpdateUserPreferencesRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[68]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[69]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3942,7 +3985,7 @@ func (x *UpdateUserPreferencesRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use UpdateUserPreferencesRequest.ProtoReflect.Descriptor instead.
func (*UpdateUserPreferencesRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{68}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{69}
}
func (x *UpdateUserPreferencesRequest) GetClusterUri() string {
@@ -3968,7 +4011,7 @@ type UpdateUserPreferencesResponse struct {
func (x *UpdateUserPreferencesResponse) Reset() {
*x = UpdateUserPreferencesResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[69]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[70]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3980,7 +4023,7 @@ func (x *UpdateUserPreferencesResponse) String() string {
func (*UpdateUserPreferencesResponse) ProtoMessage() {}
func (x *UpdateUserPreferencesResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[69]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[70]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3993,7 +4036,7 @@ func (x *UpdateUserPreferencesResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use UpdateUserPreferencesResponse.ProtoReflect.Descriptor instead.
func (*UpdateUserPreferencesResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{69}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{70}
}
func (x *UpdateUserPreferencesResponse) GetUserPreferences() *UserPreferences {
@@ -4015,7 +4058,7 @@ type UserPreferences struct {
func (x *UserPreferences) Reset() {
*x = UserPreferences{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[70]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[71]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4027,7 +4070,7 @@ func (x *UserPreferences) String() string {
func (*UserPreferences) ProtoMessage() {}
func (x *UserPreferences) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[70]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[71]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4040,7 +4083,7 @@ func (x *UserPreferences) ProtoReflect() protoreflect.Message {
// Deprecated: Use UserPreferences.ProtoReflect.Descriptor instead.
func (*UserPreferences) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{70}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{71}
}
func (x *UserPreferences) GetClusterPreferences() *v11.ClusterUserPreferences {
@@ -4070,7 +4113,7 @@ type AuthenticateWebDeviceRequest struct {
func (x *AuthenticateWebDeviceRequest) Reset() {
*x = AuthenticateWebDeviceRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[71]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[72]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4082,7 +4125,7 @@ func (x *AuthenticateWebDeviceRequest) String() string {
func (*AuthenticateWebDeviceRequest) ProtoMessage() {}
func (x *AuthenticateWebDeviceRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[71]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[72]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4095,7 +4138,7 @@ func (x *AuthenticateWebDeviceRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use AuthenticateWebDeviceRequest.ProtoReflect.Descriptor instead.
func (*AuthenticateWebDeviceRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{71}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{72}
}
func (x *AuthenticateWebDeviceRequest) GetDeviceWebToken() *v12.DeviceWebToken {
@@ -4124,7 +4167,7 @@ type AuthenticateWebDeviceResponse struct {
func (x *AuthenticateWebDeviceResponse) Reset() {
*x = AuthenticateWebDeviceResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[72]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[73]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4136,7 +4179,7 @@ func (x *AuthenticateWebDeviceResponse) String() string {
func (*AuthenticateWebDeviceResponse) ProtoMessage() {}
func (x *AuthenticateWebDeviceResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[72]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[73]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4149,7 +4192,7 @@ func (x *AuthenticateWebDeviceResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use AuthenticateWebDeviceResponse.ProtoReflect.Descriptor instead.
func (*AuthenticateWebDeviceResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{72}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{73}
}
func (x *AuthenticateWebDeviceResponse) GetConfirmationToken() *v12.DeviceConfirmationToken {
@@ -4168,7 +4211,7 @@ type GetAppRequest struct {
func (x *GetAppRequest) Reset() {
*x = GetAppRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[73]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[74]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4180,7 +4223,7 @@ func (x *GetAppRequest) String() string {
func (*GetAppRequest) ProtoMessage() {}
func (x *GetAppRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[73]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[74]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4193,7 +4236,7 @@ func (x *GetAppRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetAppRequest.ProtoReflect.Descriptor instead.
func (*GetAppRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{73}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{74}
}
func (x *GetAppRequest) GetAppUri() string {
@@ -4212,7 +4255,7 @@ type GetAppResponse struct {
func (x *GetAppResponse) Reset() {
*x = GetAppResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[74]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[75]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4224,7 +4267,7 @@ func (x *GetAppResponse) String() string {
func (*GetAppResponse) ProtoMessage() {}
func (x *GetAppResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[74]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[75]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4237,7 +4280,7 @@ func (x *GetAppResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetAppResponse.ProtoReflect.Descriptor instead.
func (*GetAppResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{74}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{75}
}
func (x *GetAppResponse) GetApp() *App {
@@ -4260,7 +4303,7 @@ type TargetDesktop struct {
func (x *TargetDesktop) Reset() {
*x = TargetDesktop{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[75]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[76]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4272,7 +4315,7 @@ func (x *TargetDesktop) String() string {
func (*TargetDesktop) ProtoMessage() {}
func (x *TargetDesktop) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[75]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[76]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4285,7 +4328,7 @@ func (x *TargetDesktop) ProtoReflect() protoreflect.Message {
// Deprecated: Use TargetDesktop.ProtoReflect.Descriptor instead.
func (*TargetDesktop) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{75}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{76}
}
func (x *TargetDesktop) GetDesktopUri() string {
@@ -4316,7 +4359,7 @@ type ConnectToDesktopRequest struct {
func (x *ConnectToDesktopRequest) Reset() {
*x = ConnectToDesktopRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[76]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[77]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4328,7 +4371,7 @@ func (x *ConnectToDesktopRequest) String() string {
func (*ConnectToDesktopRequest) ProtoMessage() {}
func (x *ConnectToDesktopRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[76]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[77]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4341,7 +4384,7 @@ func (x *ConnectToDesktopRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ConnectToDesktopRequest.ProtoReflect.Descriptor instead.
func (*ConnectToDesktopRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{76}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{77}
}
func (x *ConnectToDesktopRequest) GetData() []byte {
@@ -4369,7 +4412,7 @@ type ConnectToDesktopResponse struct {
func (x *ConnectToDesktopResponse) Reset() {
*x = ConnectToDesktopResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[77]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[78]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4381,7 +4424,7 @@ func (x *ConnectToDesktopResponse) String() string {
func (*ConnectToDesktopResponse) ProtoMessage() {}
func (x *ConnectToDesktopResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[77]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[78]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4394,7 +4437,7 @@ func (x *ConnectToDesktopResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ConnectToDesktopResponse.ProtoReflect.Descriptor instead.
func (*ConnectToDesktopResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{77}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{78}
}
func (x *ConnectToDesktopResponse) GetData() []byte {
@@ -4419,7 +4462,7 @@ type SetSharedDirectoryForDesktopSessionRequest struct {
func (x *SetSharedDirectoryForDesktopSessionRequest) Reset() {
*x = SetSharedDirectoryForDesktopSessionRequest{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[78]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[79]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4431,7 +4474,7 @@ func (x *SetSharedDirectoryForDesktopSessionRequest) String() string {
func (*SetSharedDirectoryForDesktopSessionRequest) ProtoMessage() {}
func (x *SetSharedDirectoryForDesktopSessionRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[78]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[79]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4444,7 +4487,7 @@ func (x *SetSharedDirectoryForDesktopSessionRequest) ProtoReflect() protoreflect
// Deprecated: Use SetSharedDirectoryForDesktopSessionRequest.ProtoReflect.Descriptor instead.
func (*SetSharedDirectoryForDesktopSessionRequest) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{78}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{79}
}
func (x *SetSharedDirectoryForDesktopSessionRequest) GetDesktopUri() string {
@@ -4477,7 +4520,7 @@ type SetSharedDirectoryForDesktopSessionResponse struct {
func (x *SetSharedDirectoryForDesktopSessionResponse) Reset() {
*x = SetSharedDirectoryForDesktopSessionResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[79]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[80]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4489,7 +4532,7 @@ func (x *SetSharedDirectoryForDesktopSessionResponse) String() string {
func (*SetSharedDirectoryForDesktopSessionResponse) ProtoMessage() {}
func (x *SetSharedDirectoryForDesktopSessionResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[79]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[80]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4502,7 +4545,7 @@ func (x *SetSharedDirectoryForDesktopSessionResponse) ProtoReflect() protoreflec
// Deprecated: Use SetSharedDirectoryForDesktopSessionResponse.ProtoReflect.Descriptor instead.
func (*SetSharedDirectoryForDesktopSessionResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{79}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{80}
}
// LoginPasswordlessRequestInit contains fields needed to init the stream request.
@@ -4516,7 +4559,7 @@ type LoginPasswordlessRequest_LoginPasswordlessRequestInit struct {
func (x *LoginPasswordlessRequest_LoginPasswordlessRequestInit) Reset() {
*x = LoginPasswordlessRequest_LoginPasswordlessRequestInit{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[80]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[81]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4528,7 +4571,7 @@ func (x *LoginPasswordlessRequest_LoginPasswordlessRequestInit) String() string
func (*LoginPasswordlessRequest_LoginPasswordlessRequestInit) ProtoMessage() {}
func (x *LoginPasswordlessRequest_LoginPasswordlessRequestInit) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[80]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[81]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4541,7 +4584,7 @@ func (x *LoginPasswordlessRequest_LoginPasswordlessRequestInit) ProtoReflect() p
// Deprecated: Use LoginPasswordlessRequest_LoginPasswordlessRequestInit.ProtoReflect.Descriptor instead.
func (*LoginPasswordlessRequest_LoginPasswordlessRequestInit) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{28, 0}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{29, 0}
}
func (x *LoginPasswordlessRequest_LoginPasswordlessRequestInit) GetClusterUri() string {
@@ -4562,7 +4605,7 @@ type LoginPasswordlessRequest_LoginPasswordlessPINResponse struct {
func (x *LoginPasswordlessRequest_LoginPasswordlessPINResponse) Reset() {
*x = LoginPasswordlessRequest_LoginPasswordlessPINResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[81]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[82]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4574,7 +4617,7 @@ func (x *LoginPasswordlessRequest_LoginPasswordlessPINResponse) String() string
func (*LoginPasswordlessRequest_LoginPasswordlessPINResponse) ProtoMessage() {}
func (x *LoginPasswordlessRequest_LoginPasswordlessPINResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[81]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[82]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4587,7 +4630,7 @@ func (x *LoginPasswordlessRequest_LoginPasswordlessPINResponse) ProtoReflect() p
// Deprecated: Use LoginPasswordlessRequest_LoginPasswordlessPINResponse.ProtoReflect.Descriptor instead.
func (*LoginPasswordlessRequest_LoginPasswordlessPINResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{28, 1}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{29, 1}
}
func (x *LoginPasswordlessRequest_LoginPasswordlessPINResponse) GetPin() string {
@@ -4610,7 +4653,7 @@ type LoginPasswordlessRequest_LoginPasswordlessCredentialResponse struct {
func (x *LoginPasswordlessRequest_LoginPasswordlessCredentialResponse) Reset() {
*x = LoginPasswordlessRequest_LoginPasswordlessCredentialResponse{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[82]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[83]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4622,7 +4665,7 @@ func (x *LoginPasswordlessRequest_LoginPasswordlessCredentialResponse) String()
func (*LoginPasswordlessRequest_LoginPasswordlessCredentialResponse) ProtoMessage() {}
func (x *LoginPasswordlessRequest_LoginPasswordlessCredentialResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[82]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[83]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4635,7 +4678,7 @@ func (x *LoginPasswordlessRequest_LoginPasswordlessCredentialResponse) ProtoRefl
// Deprecated: Use LoginPasswordlessRequest_LoginPasswordlessCredentialResponse.ProtoReflect.Descriptor instead.
func (*LoginPasswordlessRequest_LoginPasswordlessCredentialResponse) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{28, 2}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{29, 2}
}
func (x *LoginPasswordlessRequest_LoginPasswordlessCredentialResponse) GetIndex() int64 {
@@ -4660,7 +4703,7 @@ type LoginRequest_LocalParams struct {
func (x *LoginRequest_LocalParams) Reset() {
*x = LoginRequest_LocalParams{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[83]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[84]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4672,7 +4715,7 @@ func (x *LoginRequest_LocalParams) String() string {
func (*LoginRequest_LocalParams) ProtoMessage() {}
func (x *LoginRequest_LocalParams) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[83]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[84]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4685,7 +4728,7 @@ func (x *LoginRequest_LocalParams) ProtoReflect() protoreflect.Message {
// Deprecated: Use LoginRequest_LocalParams.ProtoReflect.Descriptor instead.
func (*LoginRequest_LocalParams) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{31, 0}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{32, 0}
}
func (x *LoginRequest_LocalParams) GetUser() string {
@@ -4722,7 +4765,7 @@ type LoginRequest_SsoParams struct {
func (x *LoginRequest_SsoParams) Reset() {
*x = LoginRequest_SsoParams{}
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[84]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[85]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4734,7 +4777,7 @@ func (x *LoginRequest_SsoParams) String() string {
func (*LoginRequest_SsoParams) ProtoMessage() {}
func (x *LoginRequest_SsoParams) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[84]
+ mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[85]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4747,7 +4790,7 @@ func (x *LoginRequest_SsoParams) ProtoReflect() protoreflect.Message {
// Deprecated: Use LoginRequest_SsoParams.ProtoReflect.Descriptor instead.
func (*LoginRequest_SsoParams) Descriptor() ([]byte, []int) {
- return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{31, 1}
+ return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{32, 1}
}
func (x *LoginRequest_SsoParams) GetProviderType() string {
@@ -4769,16 +4812,17 @@ var File_teleport_lib_teleterm_v1_service_proto protoreflect.FileDescriptor
const file_teleport_lib_teleterm_v1_service_proto_rawDesc = "" +
"\n" +
"&teleport/lib/teleterm/v1/service.proto\x12\x18teleport.lib.teleterm.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a'teleport/accesslist/v1/accesslist.proto\x1a7teleport/devicetrust/v1/device_confirmation_token.proto\x1a.teleport/devicetrust/v1/device_web_token.proto\x1a-teleport/lib/teleterm/v1/access_request.proto\x1a\"teleport/lib/teleterm/v1/app.proto\x1a,teleport/lib/teleterm/v1/auth_settings.proto\x1a&teleport/lib/teleterm/v1/cluster.proto\x1a'teleport/lib/teleterm/v1/database.proto\x1a&teleport/lib/teleterm/v1/gateway.proto\x1a#teleport/lib/teleterm/v1/kube.proto\x1a%teleport/lib/teleterm/v1/server.proto\x1a+teleport/lib/teleterm/v1/usage_events.proto\x1a.teleport/lib/teleterm/v1/windows_desktop.proto\x1a5teleport/userpreferences/v1/cluster_preferences.proto\x1a>teleport/userpreferences/v1/unified_resource_preferences.proto\"\x0f\n" +
- "\rEmptyResponse\"7\n" +
- "\x14RemoveClusterRequest\x12\x1f\n" +
- "\vcluster_uri\x18\x01 \x01(\tR\n" +
- "clusterUri\"4\n" +
+ "\rEmptyResponse\"4\n" +
"\x11GetClusterRequest\x12\x1f\n" +
"\vcluster_uri\x18\x01 \x01(\tR\n" +
- "clusterUri\"0\n" +
+ "clusterUri\"W\n" +
"\rLogoutRequest\x12\x1f\n" +
"\vcluster_uri\x18\x01 \x01(\tR\n" +
- "clusterUri\"G\n" +
+ "clusterUri\x12%\n" +
+ "\x0eremove_profile\x18\x02 \x01(\bR\rremoveProfile\"K\n" +
+ "\x1fClearStaleClusterClientsRequest\x12(\n" +
+ "\x10root_cluster_uri\x18\x01 \x01(\tR\x0erootClusterUri\"\"\n" +
+ " ClearStaleClusterClientsResponse\"G\n" +
"\x1bStartHeadlessWatcherRequest\x12(\n" +
"\x10root_cluster_uri\x18\x01 \x01(\tR\x0erootClusterUri\"\x1e\n" +
"\x1cStartHeadlessWatcherResponse\"f\n" +
@@ -5064,7 +5108,7 @@ const file_teleport_lib_teleterm_v1_service_proto_rawDesc = "" +
")HEADLESS_AUTHENTICATION_STATE_UNSPECIFIED\x10\x00\x12)\n" +
"%HEADLESS_AUTHENTICATION_STATE_PENDING\x10\x01\x12(\n" +
"$HEADLESS_AUTHENTICATION_STATE_DENIED\x10\x02\x12*\n" +
- "&HEADLESS_AUTHENTICATION_STATE_APPROVED\x10\x032\xb4,\n" +
+ "&HEADLESS_AUTHENTICATION_STATE_APPROVED\x10\x032\xde,\n" +
"\x0fTerminalService\x12\xa0\x01\n" +
"\x1dUpdateTshdEventsServerAddress\x12>.teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressRequest\x1a?.teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressResponse\x12q\n" +
"\x10ListRootClusters\x12-.teleport.lib.teleterm.v1.ListClustersRequest\x1a..teleport.lib.teleterm.v1.ListClustersResponse\x12u\n" +
@@ -5085,8 +5129,7 @@ const file_teleport_lib_teleterm_v1_service_proto_rawDesc = "" +
"\x17ListKubernetesResources\x128.teleport.lib.teleterm.v1.ListKubernetesResourcesRequest\x1a9.teleport.lib.teleterm.v1.ListKubernetesResourcesResponse\x12\x88\x01\n" +
"\x15ListKubernetesServers\x126.teleport.lib.teleterm.v1.ListKubernetesServersRequest\x1a7.teleport.lib.teleterm.v1.ListKubernetesServersResponse\x12\\\n" +
"\n" +
- "AddCluster\x12+.teleport.lib.teleterm.v1.AddClusterRequest\x1a!.teleport.lib.teleterm.v1.Cluster\x12h\n" +
- "\rRemoveCluster\x12..teleport.lib.teleterm.v1.RemoveClusterRequest\x1a'.teleport.lib.teleterm.v1.EmptyResponse\x12m\n" +
+ "AddCluster\x12+.teleport.lib.teleterm.v1.AddClusterRequest\x1a!.teleport.lib.teleterm.v1.Cluster\x12m\n" +
"\fListGateways\x12-.teleport.lib.teleterm.v1.ListGatewaysRequest\x1a..teleport.lib.teleterm.v1.ListGatewaysResponse\x12b\n" +
"\rCreateGateway\x12..teleport.lib.teleterm.v1.CreateGatewayRequest\x1a!.teleport.lib.teleterm.v1.Gateway\x12h\n" +
"\rRemoveGateway\x12..teleport.lib.teleterm.v1.RemoveGatewayRequest\x1a'.teleport.lib.teleterm.v1.EmptyResponse\x12\x86\x01\n" +
@@ -5097,7 +5140,8 @@ const file_teleport_lib_teleterm_v1_service_proto_rawDesc = "" +
"GetCluster\x12+.teleport.lib.teleterm.v1.GetClusterRequest\x1a!.teleport.lib.teleterm.v1.Cluster\x12X\n" +
"\x05Login\x12&.teleport.lib.teleterm.v1.LoginRequest\x1a'.teleport.lib.teleterm.v1.EmptyResponse\x12\x80\x01\n" +
"\x11LoginPasswordless\x122.teleport.lib.teleterm.v1.LoginPasswordlessRequest\x1a3.teleport.lib.teleterm.v1.LoginPasswordlessResponse(\x010\x01\x12Z\n" +
- "\x06Logout\x12'.teleport.lib.teleterm.v1.LogoutRequest\x1a'.teleport.lib.teleterm.v1.EmptyResponse\x12o\n" +
+ "\x06Logout\x12'.teleport.lib.teleterm.v1.LogoutRequest\x1a'.teleport.lib.teleterm.v1.EmptyResponse\x12\x91\x01\n" +
+ "\x18ClearStaleClusterClients\x129.teleport.lib.teleterm.v1.ClearStaleClusterClientsRequest\x1a:.teleport.lib.teleterm.v1.ClearStaleClusterClientsResponse\x12o\n" +
"\fTransferFile\x12-.teleport.lib.teleterm.v1.FileTransferRequest\x1a..teleport.lib.teleterm.v1.FileTransferProgress0\x01\x12n\n" +
"\x10ReportUsageEvent\x121.teleport.lib.teleterm.v1.ReportUsageEventRequest\x1a'.teleport.lib.teleterm.v1.EmptyResponse\x12\xac\x01\n" +
"!UpdateHeadlessAuthenticationState\x12B.teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateRequest\x1aC.teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateResponse\x12\x9a\x01\n" +
@@ -5127,251 +5171,252 @@ func file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP() []byte {
}
var file_teleport_lib_teleterm_v1_service_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
-var file_teleport_lib_teleterm_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 85)
+var file_teleport_lib_teleterm_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 86)
var file_teleport_lib_teleterm_v1_service_proto_goTypes = []any{
(PasswordlessPrompt)(0), // 0: teleport.lib.teleterm.v1.PasswordlessPrompt
(FileTransferDirection)(0), // 1: teleport.lib.teleterm.v1.FileTransferDirection
(HeadlessAuthenticationState)(0), // 2: teleport.lib.teleterm.v1.HeadlessAuthenticationState
(*EmptyResponse)(nil), // 3: teleport.lib.teleterm.v1.EmptyResponse
- (*RemoveClusterRequest)(nil), // 4: teleport.lib.teleterm.v1.RemoveClusterRequest
- (*GetClusterRequest)(nil), // 5: teleport.lib.teleterm.v1.GetClusterRequest
- (*LogoutRequest)(nil), // 6: teleport.lib.teleterm.v1.LogoutRequest
- (*StartHeadlessWatcherRequest)(nil), // 7: teleport.lib.teleterm.v1.StartHeadlessWatcherRequest
- (*StartHeadlessWatcherResponse)(nil), // 8: teleport.lib.teleterm.v1.StartHeadlessWatcherResponse
- (*GetAccessRequestRequest)(nil), // 9: teleport.lib.teleterm.v1.GetAccessRequestRequest
- (*GetAccessRequestsRequest)(nil), // 10: teleport.lib.teleterm.v1.GetAccessRequestsRequest
- (*GetAccessRequestResponse)(nil), // 11: teleport.lib.teleterm.v1.GetAccessRequestResponse
- (*GetAccessRequestsResponse)(nil), // 12: teleport.lib.teleterm.v1.GetAccessRequestsResponse
- (*DeleteAccessRequestRequest)(nil), // 13: teleport.lib.teleterm.v1.DeleteAccessRequestRequest
- (*CreateAccessRequestRequest)(nil), // 14: teleport.lib.teleterm.v1.CreateAccessRequestRequest
- (*CreateAccessRequestResponse)(nil), // 15: teleport.lib.teleterm.v1.CreateAccessRequestResponse
- (*AssumeRoleRequest)(nil), // 16: teleport.lib.teleterm.v1.AssumeRoleRequest
- (*GetRequestableRolesRequest)(nil), // 17: teleport.lib.teleterm.v1.GetRequestableRolesRequest
- (*GetRequestableRolesResponse)(nil), // 18: teleport.lib.teleterm.v1.GetRequestableRolesResponse
- (*ReviewAccessRequestRequest)(nil), // 19: teleport.lib.teleterm.v1.ReviewAccessRequestRequest
- (*ReviewAccessRequestResponse)(nil), // 20: teleport.lib.teleterm.v1.ReviewAccessRequestResponse
- (*PromoteAccessRequestRequest)(nil), // 21: teleport.lib.teleterm.v1.PromoteAccessRequestRequest
- (*PromoteAccessRequestResponse)(nil), // 22: teleport.lib.teleterm.v1.PromoteAccessRequestResponse
- (*GetSuggestedAccessListsRequest)(nil), // 23: teleport.lib.teleterm.v1.GetSuggestedAccessListsRequest
- (*GetSuggestedAccessListsResponse)(nil), // 24: teleport.lib.teleterm.v1.GetSuggestedAccessListsResponse
- (*ListKubernetesResourcesRequest)(nil), // 25: teleport.lib.teleterm.v1.ListKubernetesResourcesRequest
- (*ListKubernetesResourcesResponse)(nil), // 26: teleport.lib.teleterm.v1.ListKubernetesResourcesResponse
- (*ListKubernetesServersRequest)(nil), // 27: teleport.lib.teleterm.v1.ListKubernetesServersRequest
- (*ListKubernetesServersResponse)(nil), // 28: teleport.lib.teleterm.v1.ListKubernetesServersResponse
- (*CredentialInfo)(nil), // 29: teleport.lib.teleterm.v1.CredentialInfo
- (*LoginPasswordlessResponse)(nil), // 30: teleport.lib.teleterm.v1.LoginPasswordlessResponse
- (*LoginPasswordlessRequest)(nil), // 31: teleport.lib.teleterm.v1.LoginPasswordlessRequest
- (*FileTransferRequest)(nil), // 32: teleport.lib.teleterm.v1.FileTransferRequest
- (*FileTransferProgress)(nil), // 33: teleport.lib.teleterm.v1.FileTransferProgress
- (*LoginRequest)(nil), // 34: teleport.lib.teleterm.v1.LoginRequest
- (*AddClusterRequest)(nil), // 35: teleport.lib.teleterm.v1.AddClusterRequest
- (*ListClustersRequest)(nil), // 36: teleport.lib.teleterm.v1.ListClustersRequest
- (*ListClustersResponse)(nil), // 37: teleport.lib.teleterm.v1.ListClustersResponse
- (*ListLeafClustersRequest)(nil), // 38: teleport.lib.teleterm.v1.ListLeafClustersRequest
- (*ListDatabaseUsersRequest)(nil), // 39: teleport.lib.teleterm.v1.ListDatabaseUsersRequest
- (*ListDatabaseUsersResponse)(nil), // 40: teleport.lib.teleterm.v1.ListDatabaseUsersResponse
- (*ListResourcesParams)(nil), // 41: teleport.lib.teleterm.v1.ListResourcesParams
- (*ListDatabaseServersRequest)(nil), // 42: teleport.lib.teleterm.v1.ListDatabaseServersRequest
- (*ListDatabaseServersResponse)(nil), // 43: teleport.lib.teleterm.v1.ListDatabaseServersResponse
- (*CreateGatewayRequest)(nil), // 44: teleport.lib.teleterm.v1.CreateGatewayRequest
- (*ListGatewaysRequest)(nil), // 45: teleport.lib.teleterm.v1.ListGatewaysRequest
- (*ListGatewaysResponse)(nil), // 46: teleport.lib.teleterm.v1.ListGatewaysResponse
- (*RemoveGatewayRequest)(nil), // 47: teleport.lib.teleterm.v1.RemoveGatewayRequest
- (*SetGatewayTargetSubresourceNameRequest)(nil), // 48: teleport.lib.teleterm.v1.SetGatewayTargetSubresourceNameRequest
- (*SetGatewayLocalPortRequest)(nil), // 49: teleport.lib.teleterm.v1.SetGatewayLocalPortRequest
- (*GetAuthSettingsRequest)(nil), // 50: teleport.lib.teleterm.v1.GetAuthSettingsRequest
- (*UpdateTshdEventsServerAddressRequest)(nil), // 51: teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressRequest
- (*UpdateTshdEventsServerAddressResponse)(nil), // 52: teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressResponse
- (*UpdateHeadlessAuthenticationStateRequest)(nil), // 53: teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateRequest
- (*UpdateHeadlessAuthenticationStateResponse)(nil), // 54: teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateResponse
- (*CreateConnectMyComputerRoleRequest)(nil), // 55: teleport.lib.teleterm.v1.CreateConnectMyComputerRoleRequest
- (*CreateConnectMyComputerRoleResponse)(nil), // 56: teleport.lib.teleterm.v1.CreateConnectMyComputerRoleResponse
- (*CreateConnectMyComputerNodeTokenRequest)(nil), // 57: teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenRequest
- (*CreateConnectMyComputerNodeTokenResponse)(nil), // 58: teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenResponse
- (*WaitForConnectMyComputerNodeJoinRequest)(nil), // 59: teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinRequest
- (*WaitForConnectMyComputerNodeJoinResponse)(nil), // 60: teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinResponse
- (*DeleteConnectMyComputerNodeRequest)(nil), // 61: teleport.lib.teleterm.v1.DeleteConnectMyComputerNodeRequest
- (*DeleteConnectMyComputerNodeResponse)(nil), // 62: teleport.lib.teleterm.v1.DeleteConnectMyComputerNodeResponse
- (*GetConnectMyComputerNodeNameRequest)(nil), // 63: teleport.lib.teleterm.v1.GetConnectMyComputerNodeNameRequest
- (*GetConnectMyComputerNodeNameResponse)(nil), // 64: teleport.lib.teleterm.v1.GetConnectMyComputerNodeNameResponse
- (*ListUnifiedResourcesRequest)(nil), // 65: teleport.lib.teleterm.v1.ListUnifiedResourcesRequest
- (*SortBy)(nil), // 66: teleport.lib.teleterm.v1.SortBy
- (*ListUnifiedResourcesResponse)(nil), // 67: teleport.lib.teleterm.v1.ListUnifiedResourcesResponse
- (*PaginatedResource)(nil), // 68: teleport.lib.teleterm.v1.PaginatedResource
- (*GetUserPreferencesRequest)(nil), // 69: teleport.lib.teleterm.v1.GetUserPreferencesRequest
- (*GetUserPreferencesResponse)(nil), // 70: teleport.lib.teleterm.v1.GetUserPreferencesResponse
- (*UpdateUserPreferencesRequest)(nil), // 71: teleport.lib.teleterm.v1.UpdateUserPreferencesRequest
- (*UpdateUserPreferencesResponse)(nil), // 72: teleport.lib.teleterm.v1.UpdateUserPreferencesResponse
- (*UserPreferences)(nil), // 73: teleport.lib.teleterm.v1.UserPreferences
- (*AuthenticateWebDeviceRequest)(nil), // 74: teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest
- (*AuthenticateWebDeviceResponse)(nil), // 75: teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse
- (*GetAppRequest)(nil), // 76: teleport.lib.teleterm.v1.GetAppRequest
- (*GetAppResponse)(nil), // 77: teleport.lib.teleterm.v1.GetAppResponse
- (*TargetDesktop)(nil), // 78: teleport.lib.teleterm.v1.TargetDesktop
- (*ConnectToDesktopRequest)(nil), // 79: teleport.lib.teleterm.v1.ConnectToDesktopRequest
- (*ConnectToDesktopResponse)(nil), // 80: teleport.lib.teleterm.v1.ConnectToDesktopResponse
- (*SetSharedDirectoryForDesktopSessionRequest)(nil), // 81: teleport.lib.teleterm.v1.SetSharedDirectoryForDesktopSessionRequest
- (*SetSharedDirectoryForDesktopSessionResponse)(nil), // 82: teleport.lib.teleterm.v1.SetSharedDirectoryForDesktopSessionResponse
- (*LoginPasswordlessRequest_LoginPasswordlessRequestInit)(nil), // 83: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessRequestInit
- (*LoginPasswordlessRequest_LoginPasswordlessPINResponse)(nil), // 84: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessPINResponse
- (*LoginPasswordlessRequest_LoginPasswordlessCredentialResponse)(nil), // 85: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessCredentialResponse
- (*LoginRequest_LocalParams)(nil), // 86: teleport.lib.teleterm.v1.LoginRequest.LocalParams
- (*LoginRequest_SsoParams)(nil), // 87: teleport.lib.teleterm.v1.LoginRequest.SsoParams
- (*AccessRequest)(nil), // 88: teleport.lib.teleterm.v1.AccessRequest
- (*ResourceID)(nil), // 89: teleport.lib.teleterm.v1.ResourceID
- (*timestamppb.Timestamp)(nil), // 90: google.protobuf.Timestamp
- (*v1.AccessList)(nil), // 91: teleport.accesslist.v1.AccessList
- (*KubeResource)(nil), // 92: teleport.lib.teleterm.v1.KubeResource
- (*KubeServer)(nil), // 93: teleport.lib.teleterm.v1.KubeServer
- (*Cluster)(nil), // 94: teleport.lib.teleterm.v1.Cluster
- (*DatabaseServer)(nil), // 95: teleport.lib.teleterm.v1.DatabaseServer
- (*Gateway)(nil), // 96: teleport.lib.teleterm.v1.Gateway
- (*Server)(nil), // 97: teleport.lib.teleterm.v1.Server
- (*Database)(nil), // 98: teleport.lib.teleterm.v1.Database
- (*Kube)(nil), // 99: teleport.lib.teleterm.v1.Kube
- (*App)(nil), // 100: teleport.lib.teleterm.v1.App
- (*WindowsDesktop)(nil), // 101: teleport.lib.teleterm.v1.WindowsDesktop
- (*v11.ClusterUserPreferences)(nil), // 102: teleport.userpreferences.v1.ClusterUserPreferences
- (*v11.UnifiedResourcePreferences)(nil), // 103: teleport.userpreferences.v1.UnifiedResourcePreferences
- (*v12.DeviceWebToken)(nil), // 104: teleport.devicetrust.v1.DeviceWebToken
- (*v12.DeviceConfirmationToken)(nil), // 105: teleport.devicetrust.v1.DeviceConfirmationToken
- (*ReportUsageEventRequest)(nil), // 106: teleport.lib.teleterm.v1.ReportUsageEventRequest
- (*AuthSettings)(nil), // 107: teleport.lib.teleterm.v1.AuthSettings
+ (*GetClusterRequest)(nil), // 4: teleport.lib.teleterm.v1.GetClusterRequest
+ (*LogoutRequest)(nil), // 5: teleport.lib.teleterm.v1.LogoutRequest
+ (*ClearStaleClusterClientsRequest)(nil), // 6: teleport.lib.teleterm.v1.ClearStaleClusterClientsRequest
+ (*ClearStaleClusterClientsResponse)(nil), // 7: teleport.lib.teleterm.v1.ClearStaleClusterClientsResponse
+ (*StartHeadlessWatcherRequest)(nil), // 8: teleport.lib.teleterm.v1.StartHeadlessWatcherRequest
+ (*StartHeadlessWatcherResponse)(nil), // 9: teleport.lib.teleterm.v1.StartHeadlessWatcherResponse
+ (*GetAccessRequestRequest)(nil), // 10: teleport.lib.teleterm.v1.GetAccessRequestRequest
+ (*GetAccessRequestsRequest)(nil), // 11: teleport.lib.teleterm.v1.GetAccessRequestsRequest
+ (*GetAccessRequestResponse)(nil), // 12: teleport.lib.teleterm.v1.GetAccessRequestResponse
+ (*GetAccessRequestsResponse)(nil), // 13: teleport.lib.teleterm.v1.GetAccessRequestsResponse
+ (*DeleteAccessRequestRequest)(nil), // 14: teleport.lib.teleterm.v1.DeleteAccessRequestRequest
+ (*CreateAccessRequestRequest)(nil), // 15: teleport.lib.teleterm.v1.CreateAccessRequestRequest
+ (*CreateAccessRequestResponse)(nil), // 16: teleport.lib.teleterm.v1.CreateAccessRequestResponse
+ (*AssumeRoleRequest)(nil), // 17: teleport.lib.teleterm.v1.AssumeRoleRequest
+ (*GetRequestableRolesRequest)(nil), // 18: teleport.lib.teleterm.v1.GetRequestableRolesRequest
+ (*GetRequestableRolesResponse)(nil), // 19: teleport.lib.teleterm.v1.GetRequestableRolesResponse
+ (*ReviewAccessRequestRequest)(nil), // 20: teleport.lib.teleterm.v1.ReviewAccessRequestRequest
+ (*ReviewAccessRequestResponse)(nil), // 21: teleport.lib.teleterm.v1.ReviewAccessRequestResponse
+ (*PromoteAccessRequestRequest)(nil), // 22: teleport.lib.teleterm.v1.PromoteAccessRequestRequest
+ (*PromoteAccessRequestResponse)(nil), // 23: teleport.lib.teleterm.v1.PromoteAccessRequestResponse
+ (*GetSuggestedAccessListsRequest)(nil), // 24: teleport.lib.teleterm.v1.GetSuggestedAccessListsRequest
+ (*GetSuggestedAccessListsResponse)(nil), // 25: teleport.lib.teleterm.v1.GetSuggestedAccessListsResponse
+ (*ListKubernetesResourcesRequest)(nil), // 26: teleport.lib.teleterm.v1.ListKubernetesResourcesRequest
+ (*ListKubernetesResourcesResponse)(nil), // 27: teleport.lib.teleterm.v1.ListKubernetesResourcesResponse
+ (*ListKubernetesServersRequest)(nil), // 28: teleport.lib.teleterm.v1.ListKubernetesServersRequest
+ (*ListKubernetesServersResponse)(nil), // 29: teleport.lib.teleterm.v1.ListKubernetesServersResponse
+ (*CredentialInfo)(nil), // 30: teleport.lib.teleterm.v1.CredentialInfo
+ (*LoginPasswordlessResponse)(nil), // 31: teleport.lib.teleterm.v1.LoginPasswordlessResponse
+ (*LoginPasswordlessRequest)(nil), // 32: teleport.lib.teleterm.v1.LoginPasswordlessRequest
+ (*FileTransferRequest)(nil), // 33: teleport.lib.teleterm.v1.FileTransferRequest
+ (*FileTransferProgress)(nil), // 34: teleport.lib.teleterm.v1.FileTransferProgress
+ (*LoginRequest)(nil), // 35: teleport.lib.teleterm.v1.LoginRequest
+ (*AddClusterRequest)(nil), // 36: teleport.lib.teleterm.v1.AddClusterRequest
+ (*ListClustersRequest)(nil), // 37: teleport.lib.teleterm.v1.ListClustersRequest
+ (*ListClustersResponse)(nil), // 38: teleport.lib.teleterm.v1.ListClustersResponse
+ (*ListLeafClustersRequest)(nil), // 39: teleport.lib.teleterm.v1.ListLeafClustersRequest
+ (*ListDatabaseUsersRequest)(nil), // 40: teleport.lib.teleterm.v1.ListDatabaseUsersRequest
+ (*ListDatabaseUsersResponse)(nil), // 41: teleport.lib.teleterm.v1.ListDatabaseUsersResponse
+ (*ListResourcesParams)(nil), // 42: teleport.lib.teleterm.v1.ListResourcesParams
+ (*ListDatabaseServersRequest)(nil), // 43: teleport.lib.teleterm.v1.ListDatabaseServersRequest
+ (*ListDatabaseServersResponse)(nil), // 44: teleport.lib.teleterm.v1.ListDatabaseServersResponse
+ (*CreateGatewayRequest)(nil), // 45: teleport.lib.teleterm.v1.CreateGatewayRequest
+ (*ListGatewaysRequest)(nil), // 46: teleport.lib.teleterm.v1.ListGatewaysRequest
+ (*ListGatewaysResponse)(nil), // 47: teleport.lib.teleterm.v1.ListGatewaysResponse
+ (*RemoveGatewayRequest)(nil), // 48: teleport.lib.teleterm.v1.RemoveGatewayRequest
+ (*SetGatewayTargetSubresourceNameRequest)(nil), // 49: teleport.lib.teleterm.v1.SetGatewayTargetSubresourceNameRequest
+ (*SetGatewayLocalPortRequest)(nil), // 50: teleport.lib.teleterm.v1.SetGatewayLocalPortRequest
+ (*GetAuthSettingsRequest)(nil), // 51: teleport.lib.teleterm.v1.GetAuthSettingsRequest
+ (*UpdateTshdEventsServerAddressRequest)(nil), // 52: teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressRequest
+ (*UpdateTshdEventsServerAddressResponse)(nil), // 53: teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressResponse
+ (*UpdateHeadlessAuthenticationStateRequest)(nil), // 54: teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateRequest
+ (*UpdateHeadlessAuthenticationStateResponse)(nil), // 55: teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateResponse
+ (*CreateConnectMyComputerRoleRequest)(nil), // 56: teleport.lib.teleterm.v1.CreateConnectMyComputerRoleRequest
+ (*CreateConnectMyComputerRoleResponse)(nil), // 57: teleport.lib.teleterm.v1.CreateConnectMyComputerRoleResponse
+ (*CreateConnectMyComputerNodeTokenRequest)(nil), // 58: teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenRequest
+ (*CreateConnectMyComputerNodeTokenResponse)(nil), // 59: teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenResponse
+ (*WaitForConnectMyComputerNodeJoinRequest)(nil), // 60: teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinRequest
+ (*WaitForConnectMyComputerNodeJoinResponse)(nil), // 61: teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinResponse
+ (*DeleteConnectMyComputerNodeRequest)(nil), // 62: teleport.lib.teleterm.v1.DeleteConnectMyComputerNodeRequest
+ (*DeleteConnectMyComputerNodeResponse)(nil), // 63: teleport.lib.teleterm.v1.DeleteConnectMyComputerNodeResponse
+ (*GetConnectMyComputerNodeNameRequest)(nil), // 64: teleport.lib.teleterm.v1.GetConnectMyComputerNodeNameRequest
+ (*GetConnectMyComputerNodeNameResponse)(nil), // 65: teleport.lib.teleterm.v1.GetConnectMyComputerNodeNameResponse
+ (*ListUnifiedResourcesRequest)(nil), // 66: teleport.lib.teleterm.v1.ListUnifiedResourcesRequest
+ (*SortBy)(nil), // 67: teleport.lib.teleterm.v1.SortBy
+ (*ListUnifiedResourcesResponse)(nil), // 68: teleport.lib.teleterm.v1.ListUnifiedResourcesResponse
+ (*PaginatedResource)(nil), // 69: teleport.lib.teleterm.v1.PaginatedResource
+ (*GetUserPreferencesRequest)(nil), // 70: teleport.lib.teleterm.v1.GetUserPreferencesRequest
+ (*GetUserPreferencesResponse)(nil), // 71: teleport.lib.teleterm.v1.GetUserPreferencesResponse
+ (*UpdateUserPreferencesRequest)(nil), // 72: teleport.lib.teleterm.v1.UpdateUserPreferencesRequest
+ (*UpdateUserPreferencesResponse)(nil), // 73: teleport.lib.teleterm.v1.UpdateUserPreferencesResponse
+ (*UserPreferences)(nil), // 74: teleport.lib.teleterm.v1.UserPreferences
+ (*AuthenticateWebDeviceRequest)(nil), // 75: teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest
+ (*AuthenticateWebDeviceResponse)(nil), // 76: teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse
+ (*GetAppRequest)(nil), // 77: teleport.lib.teleterm.v1.GetAppRequest
+ (*GetAppResponse)(nil), // 78: teleport.lib.teleterm.v1.GetAppResponse
+ (*TargetDesktop)(nil), // 79: teleport.lib.teleterm.v1.TargetDesktop
+ (*ConnectToDesktopRequest)(nil), // 80: teleport.lib.teleterm.v1.ConnectToDesktopRequest
+ (*ConnectToDesktopResponse)(nil), // 81: teleport.lib.teleterm.v1.ConnectToDesktopResponse
+ (*SetSharedDirectoryForDesktopSessionRequest)(nil), // 82: teleport.lib.teleterm.v1.SetSharedDirectoryForDesktopSessionRequest
+ (*SetSharedDirectoryForDesktopSessionResponse)(nil), // 83: teleport.lib.teleterm.v1.SetSharedDirectoryForDesktopSessionResponse
+ (*LoginPasswordlessRequest_LoginPasswordlessRequestInit)(nil), // 84: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessRequestInit
+ (*LoginPasswordlessRequest_LoginPasswordlessPINResponse)(nil), // 85: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessPINResponse
+ (*LoginPasswordlessRequest_LoginPasswordlessCredentialResponse)(nil), // 86: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessCredentialResponse
+ (*LoginRequest_LocalParams)(nil), // 87: teleport.lib.teleterm.v1.LoginRequest.LocalParams
+ (*LoginRequest_SsoParams)(nil), // 88: teleport.lib.teleterm.v1.LoginRequest.SsoParams
+ (*AccessRequest)(nil), // 89: teleport.lib.teleterm.v1.AccessRequest
+ (*ResourceID)(nil), // 90: teleport.lib.teleterm.v1.ResourceID
+ (*timestamppb.Timestamp)(nil), // 91: google.protobuf.Timestamp
+ (*v1.AccessList)(nil), // 92: teleport.accesslist.v1.AccessList
+ (*KubeResource)(nil), // 93: teleport.lib.teleterm.v1.KubeResource
+ (*KubeServer)(nil), // 94: teleport.lib.teleterm.v1.KubeServer
+ (*Cluster)(nil), // 95: teleport.lib.teleterm.v1.Cluster
+ (*DatabaseServer)(nil), // 96: teleport.lib.teleterm.v1.DatabaseServer
+ (*Gateway)(nil), // 97: teleport.lib.teleterm.v1.Gateway
+ (*Server)(nil), // 98: teleport.lib.teleterm.v1.Server
+ (*Database)(nil), // 99: teleport.lib.teleterm.v1.Database
+ (*Kube)(nil), // 100: teleport.lib.teleterm.v1.Kube
+ (*App)(nil), // 101: teleport.lib.teleterm.v1.App
+ (*WindowsDesktop)(nil), // 102: teleport.lib.teleterm.v1.WindowsDesktop
+ (*v11.ClusterUserPreferences)(nil), // 103: teleport.userpreferences.v1.ClusterUserPreferences
+ (*v11.UnifiedResourcePreferences)(nil), // 104: teleport.userpreferences.v1.UnifiedResourcePreferences
+ (*v12.DeviceWebToken)(nil), // 105: teleport.devicetrust.v1.DeviceWebToken
+ (*v12.DeviceConfirmationToken)(nil), // 106: teleport.devicetrust.v1.DeviceConfirmationToken
+ (*ReportUsageEventRequest)(nil), // 107: teleport.lib.teleterm.v1.ReportUsageEventRequest
+ (*AuthSettings)(nil), // 108: teleport.lib.teleterm.v1.AuthSettings
}
var file_teleport_lib_teleterm_v1_service_proto_depIdxs = []int32{
- 88, // 0: teleport.lib.teleterm.v1.GetAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest
- 88, // 1: teleport.lib.teleterm.v1.GetAccessRequestsResponse.requests:type_name -> teleport.lib.teleterm.v1.AccessRequest
- 89, // 2: teleport.lib.teleterm.v1.CreateAccessRequestRequest.resource_ids:type_name -> teleport.lib.teleterm.v1.ResourceID
- 90, // 3: teleport.lib.teleterm.v1.CreateAccessRequestRequest.assume_start_time:type_name -> google.protobuf.Timestamp
- 90, // 4: teleport.lib.teleterm.v1.CreateAccessRequestRequest.max_duration:type_name -> google.protobuf.Timestamp
- 90, // 5: teleport.lib.teleterm.v1.CreateAccessRequestRequest.request_ttl:type_name -> google.protobuf.Timestamp
- 88, // 6: teleport.lib.teleterm.v1.CreateAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest
- 89, // 7: teleport.lib.teleterm.v1.GetRequestableRolesRequest.resource_ids:type_name -> teleport.lib.teleterm.v1.ResourceID
- 90, // 8: teleport.lib.teleterm.v1.ReviewAccessRequestRequest.assume_start_time:type_name -> google.protobuf.Timestamp
- 88, // 9: teleport.lib.teleterm.v1.ReviewAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest
- 88, // 10: teleport.lib.teleterm.v1.PromoteAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest
- 91, // 11: teleport.lib.teleterm.v1.GetSuggestedAccessListsResponse.access_lists:type_name -> teleport.accesslist.v1.AccessList
- 92, // 12: teleport.lib.teleterm.v1.ListKubernetesResourcesResponse.resources:type_name -> teleport.lib.teleterm.v1.KubeResource
- 41, // 13: teleport.lib.teleterm.v1.ListKubernetesServersRequest.params:type_name -> teleport.lib.teleterm.v1.ListResourcesParams
- 93, // 14: teleport.lib.teleterm.v1.ListKubernetesServersResponse.resources:type_name -> teleport.lib.teleterm.v1.KubeServer
+ 89, // 0: teleport.lib.teleterm.v1.GetAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest
+ 89, // 1: teleport.lib.teleterm.v1.GetAccessRequestsResponse.requests:type_name -> teleport.lib.teleterm.v1.AccessRequest
+ 90, // 2: teleport.lib.teleterm.v1.CreateAccessRequestRequest.resource_ids:type_name -> teleport.lib.teleterm.v1.ResourceID
+ 91, // 3: teleport.lib.teleterm.v1.CreateAccessRequestRequest.assume_start_time:type_name -> google.protobuf.Timestamp
+ 91, // 4: teleport.lib.teleterm.v1.CreateAccessRequestRequest.max_duration:type_name -> google.protobuf.Timestamp
+ 91, // 5: teleport.lib.teleterm.v1.CreateAccessRequestRequest.request_ttl:type_name -> google.protobuf.Timestamp
+ 89, // 6: teleport.lib.teleterm.v1.CreateAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest
+ 90, // 7: teleport.lib.teleterm.v1.GetRequestableRolesRequest.resource_ids:type_name -> teleport.lib.teleterm.v1.ResourceID
+ 91, // 8: teleport.lib.teleterm.v1.ReviewAccessRequestRequest.assume_start_time:type_name -> google.protobuf.Timestamp
+ 89, // 9: teleport.lib.teleterm.v1.ReviewAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest
+ 89, // 10: teleport.lib.teleterm.v1.PromoteAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest
+ 92, // 11: teleport.lib.teleterm.v1.GetSuggestedAccessListsResponse.access_lists:type_name -> teleport.accesslist.v1.AccessList
+ 93, // 12: teleport.lib.teleterm.v1.ListKubernetesResourcesResponse.resources:type_name -> teleport.lib.teleterm.v1.KubeResource
+ 42, // 13: teleport.lib.teleterm.v1.ListKubernetesServersRequest.params:type_name -> teleport.lib.teleterm.v1.ListResourcesParams
+ 94, // 14: teleport.lib.teleterm.v1.ListKubernetesServersResponse.resources:type_name -> teleport.lib.teleterm.v1.KubeServer
0, // 15: teleport.lib.teleterm.v1.LoginPasswordlessResponse.prompt:type_name -> teleport.lib.teleterm.v1.PasswordlessPrompt
- 29, // 16: teleport.lib.teleterm.v1.LoginPasswordlessResponse.credentials:type_name -> teleport.lib.teleterm.v1.CredentialInfo
- 83, // 17: teleport.lib.teleterm.v1.LoginPasswordlessRequest.init:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessRequestInit
- 84, // 18: teleport.lib.teleterm.v1.LoginPasswordlessRequest.pin:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessPINResponse
- 85, // 19: teleport.lib.teleterm.v1.LoginPasswordlessRequest.credential:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessCredentialResponse
+ 30, // 16: teleport.lib.teleterm.v1.LoginPasswordlessResponse.credentials:type_name -> teleport.lib.teleterm.v1.CredentialInfo
+ 84, // 17: teleport.lib.teleterm.v1.LoginPasswordlessRequest.init:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessRequestInit
+ 85, // 18: teleport.lib.teleterm.v1.LoginPasswordlessRequest.pin:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessPINResponse
+ 86, // 19: teleport.lib.teleterm.v1.LoginPasswordlessRequest.credential:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessCredentialResponse
1, // 20: teleport.lib.teleterm.v1.FileTransferRequest.direction:type_name -> teleport.lib.teleterm.v1.FileTransferDirection
- 86, // 21: teleport.lib.teleterm.v1.LoginRequest.local:type_name -> teleport.lib.teleterm.v1.LoginRequest.LocalParams
- 87, // 22: teleport.lib.teleterm.v1.LoginRequest.sso:type_name -> teleport.lib.teleterm.v1.LoginRequest.SsoParams
- 94, // 23: teleport.lib.teleterm.v1.ListClustersResponse.clusters:type_name -> teleport.lib.teleterm.v1.Cluster
- 41, // 24: teleport.lib.teleterm.v1.ListDatabaseServersRequest.params:type_name -> teleport.lib.teleterm.v1.ListResourcesParams
- 95, // 25: teleport.lib.teleterm.v1.ListDatabaseServersResponse.resources:type_name -> teleport.lib.teleterm.v1.DatabaseServer
- 96, // 26: teleport.lib.teleterm.v1.ListGatewaysResponse.gateways:type_name -> teleport.lib.teleterm.v1.Gateway
+ 87, // 21: teleport.lib.teleterm.v1.LoginRequest.local:type_name -> teleport.lib.teleterm.v1.LoginRequest.LocalParams
+ 88, // 22: teleport.lib.teleterm.v1.LoginRequest.sso:type_name -> teleport.lib.teleterm.v1.LoginRequest.SsoParams
+ 95, // 23: teleport.lib.teleterm.v1.ListClustersResponse.clusters:type_name -> teleport.lib.teleterm.v1.Cluster
+ 42, // 24: teleport.lib.teleterm.v1.ListDatabaseServersRequest.params:type_name -> teleport.lib.teleterm.v1.ListResourcesParams
+ 96, // 25: teleport.lib.teleterm.v1.ListDatabaseServersResponse.resources:type_name -> teleport.lib.teleterm.v1.DatabaseServer
+ 97, // 26: teleport.lib.teleterm.v1.ListGatewaysResponse.gateways:type_name -> teleport.lib.teleterm.v1.Gateway
2, // 27: teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateRequest.state:type_name -> teleport.lib.teleterm.v1.HeadlessAuthenticationState
- 97, // 28: teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinResponse.server:type_name -> teleport.lib.teleterm.v1.Server
- 66, // 29: teleport.lib.teleterm.v1.ListUnifiedResourcesRequest.sort_by:type_name -> teleport.lib.teleterm.v1.SortBy
- 68, // 30: teleport.lib.teleterm.v1.ListUnifiedResourcesResponse.resources:type_name -> teleport.lib.teleterm.v1.PaginatedResource
- 98, // 31: teleport.lib.teleterm.v1.PaginatedResource.database:type_name -> teleport.lib.teleterm.v1.Database
- 97, // 32: teleport.lib.teleterm.v1.PaginatedResource.server:type_name -> teleport.lib.teleterm.v1.Server
- 99, // 33: teleport.lib.teleterm.v1.PaginatedResource.kube:type_name -> teleport.lib.teleterm.v1.Kube
- 100, // 34: teleport.lib.teleterm.v1.PaginatedResource.app:type_name -> teleport.lib.teleterm.v1.App
- 101, // 35: teleport.lib.teleterm.v1.PaginatedResource.windows_desktop:type_name -> teleport.lib.teleterm.v1.WindowsDesktop
- 73, // 36: teleport.lib.teleterm.v1.GetUserPreferencesResponse.user_preferences:type_name -> teleport.lib.teleterm.v1.UserPreferences
- 73, // 37: teleport.lib.teleterm.v1.UpdateUserPreferencesRequest.user_preferences:type_name -> teleport.lib.teleterm.v1.UserPreferences
- 73, // 38: teleport.lib.teleterm.v1.UpdateUserPreferencesResponse.user_preferences:type_name -> teleport.lib.teleterm.v1.UserPreferences
- 102, // 39: teleport.lib.teleterm.v1.UserPreferences.cluster_preferences:type_name -> teleport.userpreferences.v1.ClusterUserPreferences
- 103, // 40: teleport.lib.teleterm.v1.UserPreferences.unified_resource_preferences:type_name -> teleport.userpreferences.v1.UnifiedResourcePreferences
- 104, // 41: teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest.device_web_token:type_name -> teleport.devicetrust.v1.DeviceWebToken
- 105, // 42: teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse.confirmation_token:type_name -> teleport.devicetrust.v1.DeviceConfirmationToken
- 100, // 43: teleport.lib.teleterm.v1.GetAppResponse.app:type_name -> teleport.lib.teleterm.v1.App
- 78, // 44: teleport.lib.teleterm.v1.ConnectToDesktopRequest.target_desktop:type_name -> teleport.lib.teleterm.v1.TargetDesktop
- 51, // 45: teleport.lib.teleterm.v1.TerminalService.UpdateTshdEventsServerAddress:input_type -> teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressRequest
- 36, // 46: teleport.lib.teleterm.v1.TerminalService.ListRootClusters:input_type -> teleport.lib.teleterm.v1.ListClustersRequest
- 38, // 47: teleport.lib.teleterm.v1.TerminalService.ListLeafClusters:input_type -> teleport.lib.teleterm.v1.ListLeafClustersRequest
- 7, // 48: teleport.lib.teleterm.v1.TerminalService.StartHeadlessWatcher:input_type -> teleport.lib.teleterm.v1.StartHeadlessWatcherRequest
- 39, // 49: teleport.lib.teleterm.v1.TerminalService.ListDatabaseUsers:input_type -> teleport.lib.teleterm.v1.ListDatabaseUsersRequest
- 42, // 50: teleport.lib.teleterm.v1.TerminalService.ListDatabaseServers:input_type -> teleport.lib.teleterm.v1.ListDatabaseServersRequest
- 10, // 51: teleport.lib.teleterm.v1.TerminalService.GetAccessRequests:input_type -> teleport.lib.teleterm.v1.GetAccessRequestsRequest
- 9, // 52: teleport.lib.teleterm.v1.TerminalService.GetAccessRequest:input_type -> teleport.lib.teleterm.v1.GetAccessRequestRequest
- 13, // 53: teleport.lib.teleterm.v1.TerminalService.DeleteAccessRequest:input_type -> teleport.lib.teleterm.v1.DeleteAccessRequestRequest
- 14, // 54: teleport.lib.teleterm.v1.TerminalService.CreateAccessRequest:input_type -> teleport.lib.teleterm.v1.CreateAccessRequestRequest
- 19, // 55: teleport.lib.teleterm.v1.TerminalService.ReviewAccessRequest:input_type -> teleport.lib.teleterm.v1.ReviewAccessRequestRequest
- 17, // 56: teleport.lib.teleterm.v1.TerminalService.GetRequestableRoles:input_type -> teleport.lib.teleterm.v1.GetRequestableRolesRequest
- 16, // 57: teleport.lib.teleterm.v1.TerminalService.AssumeRole:input_type -> teleport.lib.teleterm.v1.AssumeRoleRequest
- 21, // 58: teleport.lib.teleterm.v1.TerminalService.PromoteAccessRequest:input_type -> teleport.lib.teleterm.v1.PromoteAccessRequestRequest
- 23, // 59: teleport.lib.teleterm.v1.TerminalService.GetSuggestedAccessLists:input_type -> teleport.lib.teleterm.v1.GetSuggestedAccessListsRequest
- 25, // 60: teleport.lib.teleterm.v1.TerminalService.ListKubernetesResources:input_type -> teleport.lib.teleterm.v1.ListKubernetesResourcesRequest
- 27, // 61: teleport.lib.teleterm.v1.TerminalService.ListKubernetesServers:input_type -> teleport.lib.teleterm.v1.ListKubernetesServersRequest
- 35, // 62: teleport.lib.teleterm.v1.TerminalService.AddCluster:input_type -> teleport.lib.teleterm.v1.AddClusterRequest
- 4, // 63: teleport.lib.teleterm.v1.TerminalService.RemoveCluster:input_type -> teleport.lib.teleterm.v1.RemoveClusterRequest
- 45, // 64: teleport.lib.teleterm.v1.TerminalService.ListGateways:input_type -> teleport.lib.teleterm.v1.ListGatewaysRequest
- 44, // 65: teleport.lib.teleterm.v1.TerminalService.CreateGateway:input_type -> teleport.lib.teleterm.v1.CreateGatewayRequest
- 47, // 66: teleport.lib.teleterm.v1.TerminalService.RemoveGateway:input_type -> teleport.lib.teleterm.v1.RemoveGatewayRequest
- 48, // 67: teleport.lib.teleterm.v1.TerminalService.SetGatewayTargetSubresourceName:input_type -> teleport.lib.teleterm.v1.SetGatewayTargetSubresourceNameRequest
- 49, // 68: teleport.lib.teleterm.v1.TerminalService.SetGatewayLocalPort:input_type -> teleport.lib.teleterm.v1.SetGatewayLocalPortRequest
- 50, // 69: teleport.lib.teleterm.v1.TerminalService.GetAuthSettings:input_type -> teleport.lib.teleterm.v1.GetAuthSettingsRequest
- 5, // 70: teleport.lib.teleterm.v1.TerminalService.GetCluster:input_type -> teleport.lib.teleterm.v1.GetClusterRequest
- 34, // 71: teleport.lib.teleterm.v1.TerminalService.Login:input_type -> teleport.lib.teleterm.v1.LoginRequest
- 31, // 72: teleport.lib.teleterm.v1.TerminalService.LoginPasswordless:input_type -> teleport.lib.teleterm.v1.LoginPasswordlessRequest
- 6, // 73: teleport.lib.teleterm.v1.TerminalService.Logout:input_type -> teleport.lib.teleterm.v1.LogoutRequest
- 32, // 74: teleport.lib.teleterm.v1.TerminalService.TransferFile:input_type -> teleport.lib.teleterm.v1.FileTransferRequest
- 106, // 75: teleport.lib.teleterm.v1.TerminalService.ReportUsageEvent:input_type -> teleport.lib.teleterm.v1.ReportUsageEventRequest
- 53, // 76: teleport.lib.teleterm.v1.TerminalService.UpdateHeadlessAuthenticationState:input_type -> teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateRequest
- 55, // 77: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerRole:input_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerRoleRequest
- 57, // 78: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerNodeToken:input_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenRequest
- 59, // 79: teleport.lib.teleterm.v1.TerminalService.WaitForConnectMyComputerNodeJoin:input_type -> teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinRequest
- 61, // 80: teleport.lib.teleterm.v1.TerminalService.DeleteConnectMyComputerNode:input_type -> teleport.lib.teleterm.v1.DeleteConnectMyComputerNodeRequest
- 63, // 81: teleport.lib.teleterm.v1.TerminalService.GetConnectMyComputerNodeName:input_type -> teleport.lib.teleterm.v1.GetConnectMyComputerNodeNameRequest
- 65, // 82: teleport.lib.teleterm.v1.TerminalService.ListUnifiedResources:input_type -> teleport.lib.teleterm.v1.ListUnifiedResourcesRequest
- 69, // 83: teleport.lib.teleterm.v1.TerminalService.GetUserPreferences:input_type -> teleport.lib.teleterm.v1.GetUserPreferencesRequest
- 71, // 84: teleport.lib.teleterm.v1.TerminalService.UpdateUserPreferences:input_type -> teleport.lib.teleterm.v1.UpdateUserPreferencesRequest
- 74, // 85: teleport.lib.teleterm.v1.TerminalService.AuthenticateWebDevice:input_type -> teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest
- 76, // 86: teleport.lib.teleterm.v1.TerminalService.GetApp:input_type -> teleport.lib.teleterm.v1.GetAppRequest
- 79, // 87: teleport.lib.teleterm.v1.TerminalService.ConnectToDesktop:input_type -> teleport.lib.teleterm.v1.ConnectToDesktopRequest
- 81, // 88: teleport.lib.teleterm.v1.TerminalService.SetSharedDirectoryForDesktopSession:input_type -> teleport.lib.teleterm.v1.SetSharedDirectoryForDesktopSessionRequest
- 52, // 89: teleport.lib.teleterm.v1.TerminalService.UpdateTshdEventsServerAddress:output_type -> teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressResponse
- 37, // 90: teleport.lib.teleterm.v1.TerminalService.ListRootClusters:output_type -> teleport.lib.teleterm.v1.ListClustersResponse
- 37, // 91: teleport.lib.teleterm.v1.TerminalService.ListLeafClusters:output_type -> teleport.lib.teleterm.v1.ListClustersResponse
- 8, // 92: teleport.lib.teleterm.v1.TerminalService.StartHeadlessWatcher:output_type -> teleport.lib.teleterm.v1.StartHeadlessWatcherResponse
- 40, // 93: teleport.lib.teleterm.v1.TerminalService.ListDatabaseUsers:output_type -> teleport.lib.teleterm.v1.ListDatabaseUsersResponse
- 43, // 94: teleport.lib.teleterm.v1.TerminalService.ListDatabaseServers:output_type -> teleport.lib.teleterm.v1.ListDatabaseServersResponse
- 12, // 95: teleport.lib.teleterm.v1.TerminalService.GetAccessRequests:output_type -> teleport.lib.teleterm.v1.GetAccessRequestsResponse
- 11, // 96: teleport.lib.teleterm.v1.TerminalService.GetAccessRequest:output_type -> teleport.lib.teleterm.v1.GetAccessRequestResponse
+ 98, // 28: teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinResponse.server:type_name -> teleport.lib.teleterm.v1.Server
+ 67, // 29: teleport.lib.teleterm.v1.ListUnifiedResourcesRequest.sort_by:type_name -> teleport.lib.teleterm.v1.SortBy
+ 69, // 30: teleport.lib.teleterm.v1.ListUnifiedResourcesResponse.resources:type_name -> teleport.lib.teleterm.v1.PaginatedResource
+ 99, // 31: teleport.lib.teleterm.v1.PaginatedResource.database:type_name -> teleport.lib.teleterm.v1.Database
+ 98, // 32: teleport.lib.teleterm.v1.PaginatedResource.server:type_name -> teleport.lib.teleterm.v1.Server
+ 100, // 33: teleport.lib.teleterm.v1.PaginatedResource.kube:type_name -> teleport.lib.teleterm.v1.Kube
+ 101, // 34: teleport.lib.teleterm.v1.PaginatedResource.app:type_name -> teleport.lib.teleterm.v1.App
+ 102, // 35: teleport.lib.teleterm.v1.PaginatedResource.windows_desktop:type_name -> teleport.lib.teleterm.v1.WindowsDesktop
+ 74, // 36: teleport.lib.teleterm.v1.GetUserPreferencesResponse.user_preferences:type_name -> teleport.lib.teleterm.v1.UserPreferences
+ 74, // 37: teleport.lib.teleterm.v1.UpdateUserPreferencesRequest.user_preferences:type_name -> teleport.lib.teleterm.v1.UserPreferences
+ 74, // 38: teleport.lib.teleterm.v1.UpdateUserPreferencesResponse.user_preferences:type_name -> teleport.lib.teleterm.v1.UserPreferences
+ 103, // 39: teleport.lib.teleterm.v1.UserPreferences.cluster_preferences:type_name -> teleport.userpreferences.v1.ClusterUserPreferences
+ 104, // 40: teleport.lib.teleterm.v1.UserPreferences.unified_resource_preferences:type_name -> teleport.userpreferences.v1.UnifiedResourcePreferences
+ 105, // 41: teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest.device_web_token:type_name -> teleport.devicetrust.v1.DeviceWebToken
+ 106, // 42: teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse.confirmation_token:type_name -> teleport.devicetrust.v1.DeviceConfirmationToken
+ 101, // 43: teleport.lib.teleterm.v1.GetAppResponse.app:type_name -> teleport.lib.teleterm.v1.App
+ 79, // 44: teleport.lib.teleterm.v1.ConnectToDesktopRequest.target_desktop:type_name -> teleport.lib.teleterm.v1.TargetDesktop
+ 52, // 45: teleport.lib.teleterm.v1.TerminalService.UpdateTshdEventsServerAddress:input_type -> teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressRequest
+ 37, // 46: teleport.lib.teleterm.v1.TerminalService.ListRootClusters:input_type -> teleport.lib.teleterm.v1.ListClustersRequest
+ 39, // 47: teleport.lib.teleterm.v1.TerminalService.ListLeafClusters:input_type -> teleport.lib.teleterm.v1.ListLeafClustersRequest
+ 8, // 48: teleport.lib.teleterm.v1.TerminalService.StartHeadlessWatcher:input_type -> teleport.lib.teleterm.v1.StartHeadlessWatcherRequest
+ 40, // 49: teleport.lib.teleterm.v1.TerminalService.ListDatabaseUsers:input_type -> teleport.lib.teleterm.v1.ListDatabaseUsersRequest
+ 43, // 50: teleport.lib.teleterm.v1.TerminalService.ListDatabaseServers:input_type -> teleport.lib.teleterm.v1.ListDatabaseServersRequest
+ 11, // 51: teleport.lib.teleterm.v1.TerminalService.GetAccessRequests:input_type -> teleport.lib.teleterm.v1.GetAccessRequestsRequest
+ 10, // 52: teleport.lib.teleterm.v1.TerminalService.GetAccessRequest:input_type -> teleport.lib.teleterm.v1.GetAccessRequestRequest
+ 14, // 53: teleport.lib.teleterm.v1.TerminalService.DeleteAccessRequest:input_type -> teleport.lib.teleterm.v1.DeleteAccessRequestRequest
+ 15, // 54: teleport.lib.teleterm.v1.TerminalService.CreateAccessRequest:input_type -> teleport.lib.teleterm.v1.CreateAccessRequestRequest
+ 20, // 55: teleport.lib.teleterm.v1.TerminalService.ReviewAccessRequest:input_type -> teleport.lib.teleterm.v1.ReviewAccessRequestRequest
+ 18, // 56: teleport.lib.teleterm.v1.TerminalService.GetRequestableRoles:input_type -> teleport.lib.teleterm.v1.GetRequestableRolesRequest
+ 17, // 57: teleport.lib.teleterm.v1.TerminalService.AssumeRole:input_type -> teleport.lib.teleterm.v1.AssumeRoleRequest
+ 22, // 58: teleport.lib.teleterm.v1.TerminalService.PromoteAccessRequest:input_type -> teleport.lib.teleterm.v1.PromoteAccessRequestRequest
+ 24, // 59: teleport.lib.teleterm.v1.TerminalService.GetSuggestedAccessLists:input_type -> teleport.lib.teleterm.v1.GetSuggestedAccessListsRequest
+ 26, // 60: teleport.lib.teleterm.v1.TerminalService.ListKubernetesResources:input_type -> teleport.lib.teleterm.v1.ListKubernetesResourcesRequest
+ 28, // 61: teleport.lib.teleterm.v1.TerminalService.ListKubernetesServers:input_type -> teleport.lib.teleterm.v1.ListKubernetesServersRequest
+ 36, // 62: teleport.lib.teleterm.v1.TerminalService.AddCluster:input_type -> teleport.lib.teleterm.v1.AddClusterRequest
+ 46, // 63: teleport.lib.teleterm.v1.TerminalService.ListGateways:input_type -> teleport.lib.teleterm.v1.ListGatewaysRequest
+ 45, // 64: teleport.lib.teleterm.v1.TerminalService.CreateGateway:input_type -> teleport.lib.teleterm.v1.CreateGatewayRequest
+ 48, // 65: teleport.lib.teleterm.v1.TerminalService.RemoveGateway:input_type -> teleport.lib.teleterm.v1.RemoveGatewayRequest
+ 49, // 66: teleport.lib.teleterm.v1.TerminalService.SetGatewayTargetSubresourceName:input_type -> teleport.lib.teleterm.v1.SetGatewayTargetSubresourceNameRequest
+ 50, // 67: teleport.lib.teleterm.v1.TerminalService.SetGatewayLocalPort:input_type -> teleport.lib.teleterm.v1.SetGatewayLocalPortRequest
+ 51, // 68: teleport.lib.teleterm.v1.TerminalService.GetAuthSettings:input_type -> teleport.lib.teleterm.v1.GetAuthSettingsRequest
+ 4, // 69: teleport.lib.teleterm.v1.TerminalService.GetCluster:input_type -> teleport.lib.teleterm.v1.GetClusterRequest
+ 35, // 70: teleport.lib.teleterm.v1.TerminalService.Login:input_type -> teleport.lib.teleterm.v1.LoginRequest
+ 32, // 71: teleport.lib.teleterm.v1.TerminalService.LoginPasswordless:input_type -> teleport.lib.teleterm.v1.LoginPasswordlessRequest
+ 5, // 72: teleport.lib.teleterm.v1.TerminalService.Logout:input_type -> teleport.lib.teleterm.v1.LogoutRequest
+ 6, // 73: teleport.lib.teleterm.v1.TerminalService.ClearStaleClusterClients:input_type -> teleport.lib.teleterm.v1.ClearStaleClusterClientsRequest
+ 33, // 74: teleport.lib.teleterm.v1.TerminalService.TransferFile:input_type -> teleport.lib.teleterm.v1.FileTransferRequest
+ 107, // 75: teleport.lib.teleterm.v1.TerminalService.ReportUsageEvent:input_type -> teleport.lib.teleterm.v1.ReportUsageEventRequest
+ 54, // 76: teleport.lib.teleterm.v1.TerminalService.UpdateHeadlessAuthenticationState:input_type -> teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateRequest
+ 56, // 77: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerRole:input_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerRoleRequest
+ 58, // 78: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerNodeToken:input_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenRequest
+ 60, // 79: teleport.lib.teleterm.v1.TerminalService.WaitForConnectMyComputerNodeJoin:input_type -> teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinRequest
+ 62, // 80: teleport.lib.teleterm.v1.TerminalService.DeleteConnectMyComputerNode:input_type -> teleport.lib.teleterm.v1.DeleteConnectMyComputerNodeRequest
+ 64, // 81: teleport.lib.teleterm.v1.TerminalService.GetConnectMyComputerNodeName:input_type -> teleport.lib.teleterm.v1.GetConnectMyComputerNodeNameRequest
+ 66, // 82: teleport.lib.teleterm.v1.TerminalService.ListUnifiedResources:input_type -> teleport.lib.teleterm.v1.ListUnifiedResourcesRequest
+ 70, // 83: teleport.lib.teleterm.v1.TerminalService.GetUserPreferences:input_type -> teleport.lib.teleterm.v1.GetUserPreferencesRequest
+ 72, // 84: teleport.lib.teleterm.v1.TerminalService.UpdateUserPreferences:input_type -> teleport.lib.teleterm.v1.UpdateUserPreferencesRequest
+ 75, // 85: teleport.lib.teleterm.v1.TerminalService.AuthenticateWebDevice:input_type -> teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest
+ 77, // 86: teleport.lib.teleterm.v1.TerminalService.GetApp:input_type -> teleport.lib.teleterm.v1.GetAppRequest
+ 80, // 87: teleport.lib.teleterm.v1.TerminalService.ConnectToDesktop:input_type -> teleport.lib.teleterm.v1.ConnectToDesktopRequest
+ 82, // 88: teleport.lib.teleterm.v1.TerminalService.SetSharedDirectoryForDesktopSession:input_type -> teleport.lib.teleterm.v1.SetSharedDirectoryForDesktopSessionRequest
+ 53, // 89: teleport.lib.teleterm.v1.TerminalService.UpdateTshdEventsServerAddress:output_type -> teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressResponse
+ 38, // 90: teleport.lib.teleterm.v1.TerminalService.ListRootClusters:output_type -> teleport.lib.teleterm.v1.ListClustersResponse
+ 38, // 91: teleport.lib.teleterm.v1.TerminalService.ListLeafClusters:output_type -> teleport.lib.teleterm.v1.ListClustersResponse
+ 9, // 92: teleport.lib.teleterm.v1.TerminalService.StartHeadlessWatcher:output_type -> teleport.lib.teleterm.v1.StartHeadlessWatcherResponse
+ 41, // 93: teleport.lib.teleterm.v1.TerminalService.ListDatabaseUsers:output_type -> teleport.lib.teleterm.v1.ListDatabaseUsersResponse
+ 44, // 94: teleport.lib.teleterm.v1.TerminalService.ListDatabaseServers:output_type -> teleport.lib.teleterm.v1.ListDatabaseServersResponse
+ 13, // 95: teleport.lib.teleterm.v1.TerminalService.GetAccessRequests:output_type -> teleport.lib.teleterm.v1.GetAccessRequestsResponse
+ 12, // 96: teleport.lib.teleterm.v1.TerminalService.GetAccessRequest:output_type -> teleport.lib.teleterm.v1.GetAccessRequestResponse
3, // 97: teleport.lib.teleterm.v1.TerminalService.DeleteAccessRequest:output_type -> teleport.lib.teleterm.v1.EmptyResponse
- 15, // 98: teleport.lib.teleterm.v1.TerminalService.CreateAccessRequest:output_type -> teleport.lib.teleterm.v1.CreateAccessRequestResponse
- 20, // 99: teleport.lib.teleterm.v1.TerminalService.ReviewAccessRequest:output_type -> teleport.lib.teleterm.v1.ReviewAccessRequestResponse
- 18, // 100: teleport.lib.teleterm.v1.TerminalService.GetRequestableRoles:output_type -> teleport.lib.teleterm.v1.GetRequestableRolesResponse
+ 16, // 98: teleport.lib.teleterm.v1.TerminalService.CreateAccessRequest:output_type -> teleport.lib.teleterm.v1.CreateAccessRequestResponse
+ 21, // 99: teleport.lib.teleterm.v1.TerminalService.ReviewAccessRequest:output_type -> teleport.lib.teleterm.v1.ReviewAccessRequestResponse
+ 19, // 100: teleport.lib.teleterm.v1.TerminalService.GetRequestableRoles:output_type -> teleport.lib.teleterm.v1.GetRequestableRolesResponse
3, // 101: teleport.lib.teleterm.v1.TerminalService.AssumeRole:output_type -> teleport.lib.teleterm.v1.EmptyResponse
- 22, // 102: teleport.lib.teleterm.v1.TerminalService.PromoteAccessRequest:output_type -> teleport.lib.teleterm.v1.PromoteAccessRequestResponse
- 24, // 103: teleport.lib.teleterm.v1.TerminalService.GetSuggestedAccessLists:output_type -> teleport.lib.teleterm.v1.GetSuggestedAccessListsResponse
- 26, // 104: teleport.lib.teleterm.v1.TerminalService.ListKubernetesResources:output_type -> teleport.lib.teleterm.v1.ListKubernetesResourcesResponse
- 28, // 105: teleport.lib.teleterm.v1.TerminalService.ListKubernetesServers:output_type -> teleport.lib.teleterm.v1.ListKubernetesServersResponse
- 94, // 106: teleport.lib.teleterm.v1.TerminalService.AddCluster:output_type -> teleport.lib.teleterm.v1.Cluster
- 3, // 107: teleport.lib.teleterm.v1.TerminalService.RemoveCluster:output_type -> teleport.lib.teleterm.v1.EmptyResponse
- 46, // 108: teleport.lib.teleterm.v1.TerminalService.ListGateways:output_type -> teleport.lib.teleterm.v1.ListGatewaysResponse
- 96, // 109: teleport.lib.teleterm.v1.TerminalService.CreateGateway:output_type -> teleport.lib.teleterm.v1.Gateway
- 3, // 110: teleport.lib.teleterm.v1.TerminalService.RemoveGateway:output_type -> teleport.lib.teleterm.v1.EmptyResponse
- 96, // 111: teleport.lib.teleterm.v1.TerminalService.SetGatewayTargetSubresourceName:output_type -> teleport.lib.teleterm.v1.Gateway
- 96, // 112: teleport.lib.teleterm.v1.TerminalService.SetGatewayLocalPort:output_type -> teleport.lib.teleterm.v1.Gateway
- 107, // 113: teleport.lib.teleterm.v1.TerminalService.GetAuthSettings:output_type -> teleport.lib.teleterm.v1.AuthSettings
- 94, // 114: teleport.lib.teleterm.v1.TerminalService.GetCluster:output_type -> teleport.lib.teleterm.v1.Cluster
- 3, // 115: teleport.lib.teleterm.v1.TerminalService.Login:output_type -> teleport.lib.teleterm.v1.EmptyResponse
- 30, // 116: teleport.lib.teleterm.v1.TerminalService.LoginPasswordless:output_type -> teleport.lib.teleterm.v1.LoginPasswordlessResponse
- 3, // 117: teleport.lib.teleterm.v1.TerminalService.Logout:output_type -> teleport.lib.teleterm.v1.EmptyResponse
- 33, // 118: teleport.lib.teleterm.v1.TerminalService.TransferFile:output_type -> teleport.lib.teleterm.v1.FileTransferProgress
+ 23, // 102: teleport.lib.teleterm.v1.TerminalService.PromoteAccessRequest:output_type -> teleport.lib.teleterm.v1.PromoteAccessRequestResponse
+ 25, // 103: teleport.lib.teleterm.v1.TerminalService.GetSuggestedAccessLists:output_type -> teleport.lib.teleterm.v1.GetSuggestedAccessListsResponse
+ 27, // 104: teleport.lib.teleterm.v1.TerminalService.ListKubernetesResources:output_type -> teleport.lib.teleterm.v1.ListKubernetesResourcesResponse
+ 29, // 105: teleport.lib.teleterm.v1.TerminalService.ListKubernetesServers:output_type -> teleport.lib.teleterm.v1.ListKubernetesServersResponse
+ 95, // 106: teleport.lib.teleterm.v1.TerminalService.AddCluster:output_type -> teleport.lib.teleterm.v1.Cluster
+ 47, // 107: teleport.lib.teleterm.v1.TerminalService.ListGateways:output_type -> teleport.lib.teleterm.v1.ListGatewaysResponse
+ 97, // 108: teleport.lib.teleterm.v1.TerminalService.CreateGateway:output_type -> teleport.lib.teleterm.v1.Gateway
+ 3, // 109: teleport.lib.teleterm.v1.TerminalService.RemoveGateway:output_type -> teleport.lib.teleterm.v1.EmptyResponse
+ 97, // 110: teleport.lib.teleterm.v1.TerminalService.SetGatewayTargetSubresourceName:output_type -> teleport.lib.teleterm.v1.Gateway
+ 97, // 111: teleport.lib.teleterm.v1.TerminalService.SetGatewayLocalPort:output_type -> teleport.lib.teleterm.v1.Gateway
+ 108, // 112: teleport.lib.teleterm.v1.TerminalService.GetAuthSettings:output_type -> teleport.lib.teleterm.v1.AuthSettings
+ 95, // 113: teleport.lib.teleterm.v1.TerminalService.GetCluster:output_type -> teleport.lib.teleterm.v1.Cluster
+ 3, // 114: teleport.lib.teleterm.v1.TerminalService.Login:output_type -> teleport.lib.teleterm.v1.EmptyResponse
+ 31, // 115: teleport.lib.teleterm.v1.TerminalService.LoginPasswordless:output_type -> teleport.lib.teleterm.v1.LoginPasswordlessResponse
+ 3, // 116: teleport.lib.teleterm.v1.TerminalService.Logout:output_type -> teleport.lib.teleterm.v1.EmptyResponse
+ 7, // 117: teleport.lib.teleterm.v1.TerminalService.ClearStaleClusterClients:output_type -> teleport.lib.teleterm.v1.ClearStaleClusterClientsResponse
+ 34, // 118: teleport.lib.teleterm.v1.TerminalService.TransferFile:output_type -> teleport.lib.teleterm.v1.FileTransferProgress
3, // 119: teleport.lib.teleterm.v1.TerminalService.ReportUsageEvent:output_type -> teleport.lib.teleterm.v1.EmptyResponse
- 54, // 120: teleport.lib.teleterm.v1.TerminalService.UpdateHeadlessAuthenticationState:output_type -> teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateResponse
- 56, // 121: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerRole:output_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerRoleResponse
- 58, // 122: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerNodeToken:output_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenResponse
- 60, // 123: teleport.lib.teleterm.v1.TerminalService.WaitForConnectMyComputerNodeJoin:output_type -> teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinResponse
- 62, // 124: teleport.lib.teleterm.v1.TerminalService.DeleteConnectMyComputerNode:output_type -> teleport.lib.teleterm.v1.DeleteConnectMyComputerNodeResponse
- 64, // 125: teleport.lib.teleterm.v1.TerminalService.GetConnectMyComputerNodeName:output_type -> teleport.lib.teleterm.v1.GetConnectMyComputerNodeNameResponse
- 67, // 126: teleport.lib.teleterm.v1.TerminalService.ListUnifiedResources:output_type -> teleport.lib.teleterm.v1.ListUnifiedResourcesResponse
- 70, // 127: teleport.lib.teleterm.v1.TerminalService.GetUserPreferences:output_type -> teleport.lib.teleterm.v1.GetUserPreferencesResponse
- 72, // 128: teleport.lib.teleterm.v1.TerminalService.UpdateUserPreferences:output_type -> teleport.lib.teleterm.v1.UpdateUserPreferencesResponse
- 75, // 129: teleport.lib.teleterm.v1.TerminalService.AuthenticateWebDevice:output_type -> teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse
- 77, // 130: teleport.lib.teleterm.v1.TerminalService.GetApp:output_type -> teleport.lib.teleterm.v1.GetAppResponse
- 80, // 131: teleport.lib.teleterm.v1.TerminalService.ConnectToDesktop:output_type -> teleport.lib.teleterm.v1.ConnectToDesktopResponse
- 82, // 132: teleport.lib.teleterm.v1.TerminalService.SetSharedDirectoryForDesktopSession:output_type -> teleport.lib.teleterm.v1.SetSharedDirectoryForDesktopSessionResponse
+ 55, // 120: teleport.lib.teleterm.v1.TerminalService.UpdateHeadlessAuthenticationState:output_type -> teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateResponse
+ 57, // 121: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerRole:output_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerRoleResponse
+ 59, // 122: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerNodeToken:output_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenResponse
+ 61, // 123: teleport.lib.teleterm.v1.TerminalService.WaitForConnectMyComputerNodeJoin:output_type -> teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinResponse
+ 63, // 124: teleport.lib.teleterm.v1.TerminalService.DeleteConnectMyComputerNode:output_type -> teleport.lib.teleterm.v1.DeleteConnectMyComputerNodeResponse
+ 65, // 125: teleport.lib.teleterm.v1.TerminalService.GetConnectMyComputerNodeName:output_type -> teleport.lib.teleterm.v1.GetConnectMyComputerNodeNameResponse
+ 68, // 126: teleport.lib.teleterm.v1.TerminalService.ListUnifiedResources:output_type -> teleport.lib.teleterm.v1.ListUnifiedResourcesResponse
+ 71, // 127: teleport.lib.teleterm.v1.TerminalService.GetUserPreferences:output_type -> teleport.lib.teleterm.v1.GetUserPreferencesResponse
+ 73, // 128: teleport.lib.teleterm.v1.TerminalService.UpdateUserPreferences:output_type -> teleport.lib.teleterm.v1.UpdateUserPreferencesResponse
+ 76, // 129: teleport.lib.teleterm.v1.TerminalService.AuthenticateWebDevice:output_type -> teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse
+ 78, // 130: teleport.lib.teleterm.v1.TerminalService.GetApp:output_type -> teleport.lib.teleterm.v1.GetAppResponse
+ 81, // 131: teleport.lib.teleterm.v1.TerminalService.ConnectToDesktop:output_type -> teleport.lib.teleterm.v1.ConnectToDesktopResponse
+ 83, // 132: teleport.lib.teleterm.v1.TerminalService.SetSharedDirectoryForDesktopSession:output_type -> teleport.lib.teleterm.v1.SetSharedDirectoryForDesktopSessionResponse
89, // [89:133] is the sub-list for method output_type
45, // [45:89] is the sub-list for method input_type
45, // [45:45] is the sub-list for extension type_name
@@ -5394,16 +5439,16 @@ func file_teleport_lib_teleterm_v1_service_proto_init() {
file_teleport_lib_teleterm_v1_server_proto_init()
file_teleport_lib_teleterm_v1_usage_events_proto_init()
file_teleport_lib_teleterm_v1_windows_desktop_proto_init()
- file_teleport_lib_teleterm_v1_service_proto_msgTypes[28].OneofWrappers = []any{
+ file_teleport_lib_teleterm_v1_service_proto_msgTypes[29].OneofWrappers = []any{
(*LoginPasswordlessRequest_Init)(nil),
(*LoginPasswordlessRequest_Pin)(nil),
(*LoginPasswordlessRequest_Credential)(nil),
}
- file_teleport_lib_teleterm_v1_service_proto_msgTypes[31].OneofWrappers = []any{
+ file_teleport_lib_teleterm_v1_service_proto_msgTypes[32].OneofWrappers = []any{
(*LoginRequest_Local)(nil),
(*LoginRequest_Sso)(nil),
}
- file_teleport_lib_teleterm_v1_service_proto_msgTypes[65].OneofWrappers = []any{
+ file_teleport_lib_teleterm_v1_service_proto_msgTypes[66].OneofWrappers = []any{
(*PaginatedResource_Database)(nil),
(*PaginatedResource_Server)(nil),
(*PaginatedResource_Kube)(nil),
@@ -5416,7 +5461,7 @@ func file_teleport_lib_teleterm_v1_service_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_teleport_lib_teleterm_v1_service_proto_rawDesc), len(file_teleport_lib_teleterm_v1_service_proto_rawDesc)),
NumEnums: 3,
- NumMessages: 85,
+ NumMessages: 86,
NumExtensions: 0,
NumServices: 1,
},
diff --git a/gen/proto/go/teleport/lib/teleterm/v1/service_grpc.pb.go b/gen/proto/go/teleport/lib/teleterm/v1/service_grpc.pb.go
index e4c6a07f54a73..977818c1c9a68 100644
--- a/gen/proto/go/teleport/lib/teleterm/v1/service_grpc.pb.go
+++ b/gen/proto/go/teleport/lib/teleterm/v1/service_grpc.pb.go
@@ -54,7 +54,6 @@ const (
TerminalService_ListKubernetesResources_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/ListKubernetesResources"
TerminalService_ListKubernetesServers_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/ListKubernetesServers"
TerminalService_AddCluster_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/AddCluster"
- TerminalService_RemoveCluster_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/RemoveCluster"
TerminalService_ListGateways_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/ListGateways"
TerminalService_CreateGateway_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/CreateGateway"
TerminalService_RemoveGateway_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/RemoveGateway"
@@ -65,6 +64,7 @@ const (
TerminalService_Login_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/Login"
TerminalService_LoginPasswordless_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/LoginPasswordless"
TerminalService_Logout_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/Logout"
+ TerminalService_ClearStaleClusterClients_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/ClearStaleClusterClients"
TerminalService_TransferFile_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/TransferFile"
TerminalService_ReportUsageEvent_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/ReportUsageEvent"
TerminalService_UpdateHeadlessAuthenticationState_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/UpdateHeadlessAuthenticationState"
@@ -137,8 +137,6 @@ type TerminalServiceClient interface {
ListKubernetesServers(ctx context.Context, in *ListKubernetesServersRequest, opts ...grpc.CallOption) (*ListKubernetesServersResponse, error)
// AddCluster adds a cluster to profile
AddCluster(ctx context.Context, in *AddClusterRequest, opts ...grpc.CallOption) (*Cluster, error)
- // RemoveCluster removes a cluster from profile
- RemoveCluster(ctx context.Context, in *RemoveClusterRequest, opts ...grpc.CallOption) (*EmptyResponse, error)
// ListGateways lists gateways
ListGateways(ctx context.Context, in *ListGatewaysRequest, opts ...grpc.CallOption) (*ListGatewaysResponse, error)
// CreateGateway creates a gateway
@@ -177,8 +175,12 @@ type TerminalServiceClient interface {
// -> Receive the index number associated with the selected credential in list
// <- End
LoginPasswordless(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[LoginPasswordlessRequest, LoginPasswordlessResponse], error)
- // ClusterLogin logs out a user from cluster
+ // Logs the user out of the cluster and cleans up associated resources.
+ // Optionally removes the profile.
+ // This operation is idempotent and can be safely invoked multiple times.
Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*EmptyResponse, error)
+ // Closes root and leaf cluster clients that use outdated TLS certificates.
+ ClearStaleClusterClients(ctx context.Context, in *ClearStaleClusterClientsRequest, opts ...grpc.CallOption) (*ClearStaleClusterClientsResponse, error)
// TransferFile sends a request to download/upload a file
TransferFile(ctx context.Context, in *FileTransferRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileTransferProgress], error)
// ReportUsageEvent allows to send usage events that are then anonymized and forwarded to prehog
@@ -418,16 +420,6 @@ func (c *terminalServiceClient) AddCluster(ctx context.Context, in *AddClusterRe
return out, nil
}
-func (c *terminalServiceClient) RemoveCluster(ctx context.Context, in *RemoveClusterRequest, opts ...grpc.CallOption) (*EmptyResponse, error) {
- cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
- out := new(EmptyResponse)
- err := c.cc.Invoke(ctx, TerminalService_RemoveCluster_FullMethodName, in, out, cOpts...)
- if err != nil {
- return nil, err
- }
- return out, nil
-}
-
func (c *terminalServiceClient) ListGateways(ctx context.Context, in *ListGatewaysRequest, opts ...grpc.CallOption) (*ListGatewaysResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListGatewaysResponse)
@@ -531,6 +523,16 @@ func (c *terminalServiceClient) Logout(ctx context.Context, in *LogoutRequest, o
return out, nil
}
+func (c *terminalServiceClient) ClearStaleClusterClients(ctx context.Context, in *ClearStaleClusterClientsRequest, opts ...grpc.CallOption) (*ClearStaleClusterClientsResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(ClearStaleClusterClientsResponse)
+ err := c.cc.Invoke(ctx, TerminalService_ClearStaleClusterClients_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
func (c *terminalServiceClient) TransferFile(ctx context.Context, in *FileTransferRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileTransferProgress], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &TerminalService_ServiceDesc.Streams[1], TerminalService_TransferFile_FullMethodName, cOpts...)
@@ -748,8 +750,6 @@ type TerminalServiceServer interface {
ListKubernetesServers(context.Context, *ListKubernetesServersRequest) (*ListKubernetesServersResponse, error)
// AddCluster adds a cluster to profile
AddCluster(context.Context, *AddClusterRequest) (*Cluster, error)
- // RemoveCluster removes a cluster from profile
- RemoveCluster(context.Context, *RemoveClusterRequest) (*EmptyResponse, error)
// ListGateways lists gateways
ListGateways(context.Context, *ListGatewaysRequest) (*ListGatewaysResponse, error)
// CreateGateway creates a gateway
@@ -788,8 +788,12 @@ type TerminalServiceServer interface {
// -> Receive the index number associated with the selected credential in list
// <- End
LoginPasswordless(grpc.BidiStreamingServer[LoginPasswordlessRequest, LoginPasswordlessResponse]) error
- // ClusterLogin logs out a user from cluster
+ // Logs the user out of the cluster and cleans up associated resources.
+ // Optionally removes the profile.
+ // This operation is idempotent and can be safely invoked multiple times.
Logout(context.Context, *LogoutRequest) (*EmptyResponse, error)
+ // Closes root and leaf cluster clients that use outdated TLS certificates.
+ ClearStaleClusterClients(context.Context, *ClearStaleClusterClientsRequest) (*ClearStaleClusterClientsResponse, error)
// TransferFile sends a request to download/upload a file
TransferFile(*FileTransferRequest, grpc.ServerStreamingServer[FileTransferProgress]) error
// ReportUsageEvent allows to send usage events that are then anonymized and forwarded to prehog
@@ -903,9 +907,6 @@ func (UnimplementedTerminalServiceServer) ListKubernetesServers(context.Context,
func (UnimplementedTerminalServiceServer) AddCluster(context.Context, *AddClusterRequest) (*Cluster, error) {
return nil, status.Errorf(codes.Unimplemented, "method AddCluster not implemented")
}
-func (UnimplementedTerminalServiceServer) RemoveCluster(context.Context, *RemoveClusterRequest) (*EmptyResponse, error) {
- return nil, status.Errorf(codes.Unimplemented, "method RemoveCluster not implemented")
-}
func (UnimplementedTerminalServiceServer) ListGateways(context.Context, *ListGatewaysRequest) (*ListGatewaysResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListGateways not implemented")
}
@@ -936,6 +937,9 @@ func (UnimplementedTerminalServiceServer) LoginPasswordless(grpc.BidiStreamingSe
func (UnimplementedTerminalServiceServer) Logout(context.Context, *LogoutRequest) (*EmptyResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Logout not implemented")
}
+func (UnimplementedTerminalServiceServer) ClearStaleClusterClients(context.Context, *ClearStaleClusterClientsRequest) (*ClearStaleClusterClientsResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method ClearStaleClusterClients not implemented")
+}
func (UnimplementedTerminalServiceServer) TransferFile(*FileTransferRequest, grpc.ServerStreamingServer[FileTransferProgress]) error {
return status.Errorf(codes.Unimplemented, "method TransferFile not implemented")
}
@@ -1326,24 +1330,6 @@ func _TerminalService_AddCluster_Handler(srv interface{}, ctx context.Context, d
return interceptor(ctx, in, info, handler)
}
-func _TerminalService_RemoveCluster_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
- in := new(RemoveClusterRequest)
- if err := dec(in); err != nil {
- return nil, err
- }
- if interceptor == nil {
- return srv.(TerminalServiceServer).RemoveCluster(ctx, in)
- }
- info := &grpc.UnaryServerInfo{
- Server: srv,
- FullMethod: TerminalService_RemoveCluster_FullMethodName,
- }
- handler := func(ctx context.Context, req interface{}) (interface{}, error) {
- return srv.(TerminalServiceServer).RemoveCluster(ctx, req.(*RemoveClusterRequest))
- }
- return interceptor(ctx, in, info, handler)
-}
-
func _TerminalService_ListGateways_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListGatewaysRequest)
if err := dec(in); err != nil {
@@ -1513,6 +1499,24 @@ func _TerminalService_Logout_Handler(srv interface{}, ctx context.Context, dec f
return interceptor(ctx, in, info, handler)
}
+func _TerminalService_ClearStaleClusterClients_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(ClearStaleClusterClientsRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(TerminalServiceServer).ClearStaleClusterClients(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: TerminalService_ClearStaleClusterClients_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(TerminalServiceServer).ClearStaleClusterClients(ctx, req.(*ClearStaleClusterClientsRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
func _TerminalService_TransferFile_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(FileTransferRequest)
if err := stream.RecvMsg(m); err != nil {
@@ -1844,10 +1848,6 @@ var TerminalService_ServiceDesc = grpc.ServiceDesc{
MethodName: "AddCluster",
Handler: _TerminalService_AddCluster_Handler,
},
- {
- MethodName: "RemoveCluster",
- Handler: _TerminalService_RemoveCluster_Handler,
- },
{
MethodName: "ListGateways",
Handler: _TerminalService_ListGateways_Handler,
@@ -1884,6 +1884,10 @@ var TerminalService_ServiceDesc = grpc.ServiceDesc{
MethodName: "Logout",
Handler: _TerminalService_Logout_Handler,
},
+ {
+ MethodName: "ClearStaleClusterClients",
+ Handler: _TerminalService_ClearStaleClusterClients_Handler,
+ },
{
MethodName: "ReportUsageEvent",
Handler: _TerminalService_ReportUsageEvent_Handler,
diff --git a/gen/proto/ts/teleport/lib/teleterm/v1/cluster_pb.ts b/gen/proto/ts/teleport/lib/teleterm/v1/cluster_pb.ts
index d9ddda213e5cd..e570f209d1abd 100644
--- a/gen/proto/ts/teleport/lib/teleterm/v1/cluster_pb.ts
+++ b/gen/proto/ts/teleport/lib/teleterm/v1/cluster_pb.ts
@@ -30,6 +30,7 @@ import { UnknownFieldHandler } from "@protobuf-ts/runtime";
import type { PartialMessage } from "@protobuf-ts/runtime";
import { reflectionMergePartial } from "@protobuf-ts/runtime";
import { MessageType } from "@protobuf-ts/runtime";
+import { Timestamp } from "../../../../google/protobuf/timestamp_pb";
import { TrustedDeviceRequirement } from "../../../legacy/types/trusted_device_requirement_pb";
/**
* Cluster describes cluster fields.
@@ -191,6 +192,12 @@ export interface LoggedInUser {
* @generated from protobuf field: types.TrustedDeviceRequirement trusted_device_requirement = 10;
*/
trustedDeviceRequirement: TrustedDeviceRequirement;
+ /**
+ * Expiration time of the certificate.
+ *
+ * @generated from protobuf field: google.protobuf.Timestamp valid_until = 11;
+ */
+ validUntil?: Timestamp;
}
/**
* UserType indicates whether the user was created through an SSO provider or in Teleport itself.
@@ -541,7 +548,8 @@ class LoggedInUser$Type extends MessageType {
{ no: 7, name: "requestable_roles", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ },
{ no: 8, name: "user_type", kind: "enum", T: () => ["teleport.lib.teleterm.v1.LoggedInUser.UserType", LoggedInUser_UserType, "USER_TYPE_"] },
{ no: 9, name: "is_device_trusted", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
- { no: 10, name: "trusted_device_requirement", kind: "enum", T: () => ["types.TrustedDeviceRequirement", TrustedDeviceRequirement, "TRUSTED_DEVICE_REQUIREMENT_"] }
+ { no: 10, name: "trusted_device_requirement", kind: "enum", T: () => ["types.TrustedDeviceRequirement", TrustedDeviceRequirement, "TRUSTED_DEVICE_REQUIREMENT_"] },
+ { no: 11, name: "valid_until", kind: "message", T: () => Timestamp }
]);
}
create(value?: PartialMessage): LoggedInUser {
@@ -590,6 +598,9 @@ class LoggedInUser$Type extends MessageType {
case /* types.TrustedDeviceRequirement trusted_device_requirement */ 10:
message.trustedDeviceRequirement = reader.int32();
break;
+ case /* google.protobuf.Timestamp valid_until */ 11:
+ message.validUntil = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.validUntil);
+ break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -629,6 +640,9 @@ class LoggedInUser$Type extends MessageType {
/* types.TrustedDeviceRequirement trusted_device_requirement = 10; */
if (message.trustedDeviceRequirement !== 0)
writer.tag(10, WireType.Varint).int32(message.trustedDeviceRequirement);
+ /* google.protobuf.Timestamp valid_until = 11; */
+ if (message.validUntil)
+ Timestamp.internalBinaryWrite(message.validUntil, writer.tag(11, WireType.LengthDelimited).fork(), options).join();
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
diff --git a/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.client.ts b/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.client.ts
index 77b8ca4981ac6..f31ee81bb3625 100644
--- a/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.client.ts
+++ b/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.client.ts
@@ -54,6 +54,8 @@ import type { ReportUsageEventRequest } from "./usage_events_pb";
import type { FileTransferProgress } from "./service_pb";
import type { FileTransferRequest } from "./service_pb";
import type { ServerStreamingCall } from "@protobuf-ts/runtime-rpc";
+import type { ClearStaleClusterClientsResponse } from "./service_pb";
+import type { ClearStaleClusterClientsRequest } from "./service_pb";
import type { LogoutRequest } from "./service_pb";
import type { LoginPasswordlessResponse } from "./service_pb";
import type { LoginPasswordlessRequest } from "./service_pb";
@@ -69,7 +71,6 @@ import type { Gateway } from "./gateway_pb";
import type { CreateGatewayRequest } from "./service_pb";
import type { ListGatewaysResponse } from "./service_pb";
import type { ListGatewaysRequest } from "./service_pb";
-import type { RemoveClusterRequest } from "./service_pb";
import type { Cluster } from "./cluster_pb";
import type { AddClusterRequest } from "./service_pb";
import type { ListKubernetesServersResponse } from "./service_pb";
@@ -234,12 +235,6 @@ export interface ITerminalServiceClient {
* @generated from protobuf rpc: AddCluster(teleport.lib.teleterm.v1.AddClusterRequest) returns (teleport.lib.teleterm.v1.Cluster);
*/
addCluster(input: AddClusterRequest, options?: RpcOptions): UnaryCall;
- /**
- * RemoveCluster removes a cluster from profile
- *
- * @generated from protobuf rpc: RemoveCluster(teleport.lib.teleterm.v1.RemoveClusterRequest) returns (teleport.lib.teleterm.v1.EmptyResponse);
- */
- removeCluster(input: RemoveClusterRequest, options?: RpcOptions): UnaryCall;
/**
* ListGateways lists gateways
*
@@ -315,11 +310,19 @@ export interface ITerminalServiceClient {
*/
loginPasswordless(options?: RpcOptions): DuplexStreamingCall;
/**
- * ClusterLogin logs out a user from cluster
+ * Logs the user out of the cluster and cleans up associated resources.
+ * Optionally removes the profile.
+ * This operation is idempotent and can be safely invoked multiple times.
*
* @generated from protobuf rpc: Logout(teleport.lib.teleterm.v1.LogoutRequest) returns (teleport.lib.teleterm.v1.EmptyResponse);
*/
logout(input: LogoutRequest, options?: RpcOptions): UnaryCall;
+ /**
+ * Closes root and leaf cluster clients that use outdated TLS certificates.
+ *
+ * @generated from protobuf rpc: ClearStaleClusterClients(teleport.lib.teleterm.v1.ClearStaleClusterClientsRequest) returns (teleport.lib.teleterm.v1.ClearStaleClusterClientsResponse);
+ */
+ clearStaleClusterClients(input: ClearStaleClusterClientsRequest, options?: RpcOptions): UnaryCall;
/**
* TransferFile sends a request to download/upload a file
*
@@ -616,22 +619,13 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
const method = this.methods[17], opt = this._transport.mergeOptions(options);
return stackIntercept("unary", this._transport, method, opt, input);
}
- /**
- * RemoveCluster removes a cluster from profile
- *
- * @generated from protobuf rpc: RemoveCluster(teleport.lib.teleterm.v1.RemoveClusterRequest) returns (teleport.lib.teleterm.v1.EmptyResponse);
- */
- removeCluster(input: RemoveClusterRequest, options?: RpcOptions): UnaryCall {
- const method = this.methods[18], opt = this._transport.mergeOptions(options);
- return stackIntercept("unary", this._transport, method, opt, input);
- }
/**
* ListGateways lists gateways
*
* @generated from protobuf rpc: ListGateways(teleport.lib.teleterm.v1.ListGatewaysRequest) returns (teleport.lib.teleterm.v1.ListGatewaysResponse);
*/
listGateways(input: ListGatewaysRequest, options?: RpcOptions): UnaryCall {
- const method = this.methods[19], opt = this._transport.mergeOptions(options);
+ const method = this.methods[18], opt = this._transport.mergeOptions(options);
return stackIntercept("unary", this._transport, method, opt, input);
}
/**
@@ -640,7 +634,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
* @generated from protobuf rpc: CreateGateway(teleport.lib.teleterm.v1.CreateGatewayRequest) returns (teleport.lib.teleterm.v1.Gateway);
*/
createGateway(input: CreateGatewayRequest, options?: RpcOptions): UnaryCall {
- const method = this.methods[20], opt = this._transport.mergeOptions(options);
+ const method = this.methods[19], opt = this._transport.mergeOptions(options);
return stackIntercept("unary", this._transport, method, opt, input);
}
/**
@@ -649,7 +643,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
* @generated from protobuf rpc: RemoveGateway(teleport.lib.teleterm.v1.RemoveGatewayRequest) returns (teleport.lib.teleterm.v1.EmptyResponse);
*/
removeGateway(input: RemoveGatewayRequest, options?: RpcOptions): UnaryCall {
- const method = this.methods[21], opt = this._transport.mergeOptions(options);
+ const method = this.methods[20], opt = this._transport.mergeOptions(options);
return stackIntercept("unary", this._transport, method, opt, input);
}
/**
@@ -661,7 +655,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
* @generated from protobuf rpc: SetGatewayTargetSubresourceName(teleport.lib.teleterm.v1.SetGatewayTargetSubresourceNameRequest) returns (teleport.lib.teleterm.v1.Gateway);
*/
setGatewayTargetSubresourceName(input: SetGatewayTargetSubresourceNameRequest, options?: RpcOptions): UnaryCall {
- const method = this.methods[22], opt = this._transport.mergeOptions(options);
+ const method = this.methods[21], opt = this._transport.mergeOptions(options);
return stackIntercept("unary", this._transport, method, opt, input);
}
/**
@@ -671,7 +665,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
* @generated from protobuf rpc: SetGatewayLocalPort(teleport.lib.teleterm.v1.SetGatewayLocalPortRequest) returns (teleport.lib.teleterm.v1.Gateway);
*/
setGatewayLocalPort(input: SetGatewayLocalPortRequest, options?: RpcOptions): UnaryCall {
- const method = this.methods[23], opt = this._transport.mergeOptions(options);
+ const method = this.methods[22], opt = this._transport.mergeOptions(options);
return stackIntercept("unary", this._transport, method, opt, input);
}
/**
@@ -680,7 +674,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
* @generated from protobuf rpc: GetAuthSettings(teleport.lib.teleterm.v1.GetAuthSettingsRequest) returns (teleport.lib.teleterm.v1.AuthSettings);
*/
getAuthSettings(input: GetAuthSettingsRequest, options?: RpcOptions): UnaryCall {
- const method = this.methods[24], opt = this._transport.mergeOptions(options);
+ const method = this.methods[23], opt = this._transport.mergeOptions(options);
return stackIntercept("unary", this._transport, method, opt, input);
}
/**
@@ -690,7 +684,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
* @generated from protobuf rpc: GetCluster(teleport.lib.teleterm.v1.GetClusterRequest) returns (teleport.lib.teleterm.v1.Cluster);
*/
getCluster(input: GetClusterRequest, options?: RpcOptions): UnaryCall {
- const method = this.methods[25], opt = this._transport.mergeOptions(options);
+ const method = this.methods[24], opt = this._transport.mergeOptions(options);
return stackIntercept("unary", this._transport, method, opt, input);
}
/**
@@ -699,7 +693,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
* @generated from protobuf rpc: Login(teleport.lib.teleterm.v1.LoginRequest) returns (teleport.lib.teleterm.v1.EmptyResponse);
*/
login(input: LoginRequest, options?: RpcOptions): UnaryCall {
- const method = this.methods[26], opt = this._transport.mergeOptions(options);
+ const method = this.methods[25], opt = this._transport.mergeOptions(options);
return stackIntercept("unary", this._transport, method, opt, input);
}
/**
@@ -723,18 +717,29 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
* @generated from protobuf rpc: LoginPasswordless(stream teleport.lib.teleterm.v1.LoginPasswordlessRequest) returns (stream teleport.lib.teleterm.v1.LoginPasswordlessResponse);
*/
loginPasswordless(options?: RpcOptions): DuplexStreamingCall {
- const method = this.methods[27], opt = this._transport.mergeOptions(options);
+ const method = this.methods[26], opt = this._transport.mergeOptions(options);
return stackIntercept("duplex", this._transport, method, opt);
}
/**
- * ClusterLogin logs out a user from cluster
+ * Logs the user out of the cluster and cleans up associated resources.
+ * Optionally removes the profile.
+ * This operation is idempotent and can be safely invoked multiple times.
*
* @generated from protobuf rpc: Logout(teleport.lib.teleterm.v1.LogoutRequest) returns (teleport.lib.teleterm.v1.EmptyResponse);
*/
logout(input: LogoutRequest, options?: RpcOptions): UnaryCall {
- const method = this.methods[28], opt = this._transport.mergeOptions(options);
+ const method = this.methods[27], opt = this._transport.mergeOptions(options);
return stackIntercept("unary", this._transport, method, opt, input);
}
+ /**
+ * Closes root and leaf cluster clients that use outdated TLS certificates.
+ *
+ * @generated from protobuf rpc: ClearStaleClusterClients(teleport.lib.teleterm.v1.ClearStaleClusterClientsRequest) returns (teleport.lib.teleterm.v1.ClearStaleClusterClientsResponse);
+ */
+ clearStaleClusterClients(input: ClearStaleClusterClientsRequest, options?: RpcOptions): UnaryCall {
+ const method = this.methods[28], opt = this._transport.mergeOptions(options);
+ return stackIntercept("unary", this._transport, method, opt, input);
+ }
/**
* TransferFile sends a request to download/upload a file
*
diff --git a/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.grpc-server.ts b/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.grpc-server.ts
index b2591363272fd..f2d9f31ee3bfb 100644
--- a/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.grpc-server.ts
+++ b/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.grpc-server.ts
@@ -50,6 +50,8 @@ import { UpdateHeadlessAuthenticationStateRequest } from "./service_pb";
import { ReportUsageEventRequest } from "./usage_events_pb";
import { FileTransferProgress } from "./service_pb";
import { FileTransferRequest } from "./service_pb";
+import { ClearStaleClusterClientsResponse } from "./service_pb";
+import { ClearStaleClusterClientsRequest } from "./service_pb";
import { LogoutRequest } from "./service_pb";
import { LoginPasswordlessResponse } from "./service_pb";
import { LoginPasswordlessRequest } from "./service_pb";
@@ -64,7 +66,6 @@ import { Gateway } from "./gateway_pb";
import { CreateGatewayRequest } from "./service_pb";
import { ListGatewaysResponse } from "./service_pb";
import { ListGatewaysRequest } from "./service_pb";
-import { RemoveClusterRequest } from "./service_pb";
import { Cluster } from "./cluster_pb";
import { AddClusterRequest } from "./service_pb";
import { ListKubernetesServersResponse } from "./service_pb";
@@ -227,12 +228,6 @@ export interface ITerminalService extends grpc.UntypedServiceImplementation {
* @generated from protobuf rpc: AddCluster(teleport.lib.teleterm.v1.AddClusterRequest) returns (teleport.lib.teleterm.v1.Cluster);
*/
addCluster: grpc.handleUnaryCall;
- /**
- * RemoveCluster removes a cluster from profile
- *
- * @generated from protobuf rpc: RemoveCluster(teleport.lib.teleterm.v1.RemoveClusterRequest) returns (teleport.lib.teleterm.v1.EmptyResponse);
- */
- removeCluster: grpc.handleUnaryCall;
/**
* ListGateways lists gateways
*
@@ -308,11 +303,19 @@ export interface ITerminalService extends grpc.UntypedServiceImplementation {
*/
loginPasswordless: grpc.handleBidiStreamingCall;
/**
- * ClusterLogin logs out a user from cluster
+ * Logs the user out of the cluster and cleans up associated resources.
+ * Optionally removes the profile.
+ * This operation is idempotent and can be safely invoked multiple times.
*
* @generated from protobuf rpc: Logout(teleport.lib.teleterm.v1.LogoutRequest) returns (teleport.lib.teleterm.v1.EmptyResponse);
*/
logout: grpc.handleUnaryCall;
+ /**
+ * Closes root and leaf cluster clients that use outdated TLS certificates.
+ *
+ * @generated from protobuf rpc: ClearStaleClusterClients(teleport.lib.teleterm.v1.ClearStaleClusterClientsRequest) returns (teleport.lib.teleterm.v1.ClearStaleClusterClientsResponse);
+ */
+ clearStaleClusterClients: grpc.handleUnaryCall;
/**
* TransferFile sends a request to download/upload a file
*
@@ -615,16 +618,6 @@ export const terminalServiceDefinition: grpc.ServiceDefinition
responseSerialize: value => Buffer.from(Cluster.toBinary(value)),
requestSerialize: value => Buffer.from(AddClusterRequest.toBinary(value))
},
- removeCluster: {
- path: "/teleport.lib.teleterm.v1.TerminalService/RemoveCluster",
- originalName: "RemoveCluster",
- requestStream: false,
- responseStream: false,
- responseDeserialize: bytes => EmptyResponse.fromBinary(bytes),
- requestDeserialize: bytes => RemoveClusterRequest.fromBinary(bytes),
- responseSerialize: value => Buffer.from(EmptyResponse.toBinary(value)),
- requestSerialize: value => Buffer.from(RemoveClusterRequest.toBinary(value))
- },
listGateways: {
path: "/teleport.lib.teleterm.v1.TerminalService/ListGateways",
originalName: "ListGateways",
@@ -725,6 +718,16 @@ export const terminalServiceDefinition: grpc.ServiceDefinition
responseSerialize: value => Buffer.from(EmptyResponse.toBinary(value)),
requestSerialize: value => Buffer.from(LogoutRequest.toBinary(value))
},
+ clearStaleClusterClients: {
+ path: "/teleport.lib.teleterm.v1.TerminalService/ClearStaleClusterClients",
+ originalName: "ClearStaleClusterClients",
+ requestStream: false,
+ responseStream: false,
+ responseDeserialize: bytes => ClearStaleClusterClientsResponse.fromBinary(bytes),
+ requestDeserialize: bytes => ClearStaleClusterClientsRequest.fromBinary(bytes),
+ responseSerialize: value => Buffer.from(ClearStaleClusterClientsResponse.toBinary(value)),
+ requestSerialize: value => Buffer.from(ClearStaleClusterClientsRequest.toBinary(value))
+ },
transferFile: {
path: "/teleport.lib.teleterm.v1.TerminalService/TransferFile",
originalName: "TransferFile",
diff --git a/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.ts b/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.ts
index ac602477a0994..29d50da9529e9 100644
--- a/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.ts
+++ b/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.ts
@@ -56,17 +56,6 @@ import { AccessRequest } from "./access_request_pb";
*/
export interface EmptyResponse {
}
-/**
- * RemoveClusterRequest describes RemoveClusterRequest
- *
- * @generated from protobuf message teleport.lib.teleterm.v1.RemoveClusterRequest
- */
-export interface RemoveClusterRequest {
- /**
- * @generated from protobuf field: string cluster_uri = 1;
- */
- clusterUri: string;
-}
/**
* GetClusterRequest describes GetClusterRequest
*
@@ -79,8 +68,6 @@ export interface GetClusterRequest {
clusterUri: string;
}
/**
- * LogoutRequest describes LogoutRequest
- *
* @generated from protobuf message teleport.lib.teleterm.v1.LogoutRequest
*/
export interface LogoutRequest {
@@ -88,6 +75,26 @@ export interface LogoutRequest {
* @generated from protobuf field: string cluster_uri = 1;
*/
clusterUri: string;
+ /**
+ * Whether to remove the associated YAML profile after logout.
+ *
+ * @generated from protobuf field: bool remove_profile = 2;
+ */
+ removeProfile: boolean;
+}
+/**
+ * @generated from protobuf message teleport.lib.teleterm.v1.ClearStaleClusterClientsRequest
+ */
+export interface ClearStaleClusterClientsRequest {
+ /**
+ * @generated from protobuf field: string root_cluster_uri = 1;
+ */
+ rootClusterUri: string;
+}
+/**
+ * @generated from protobuf message teleport.lib.teleterm.v1.ClearStaleClusterClientsResponse
+ */
+export interface ClearStaleClusterClientsResponse {
}
/**
* @generated from protobuf message teleport.lib.teleterm.v1.StartHeadlessWatcherRequest
@@ -1442,20 +1449,20 @@ class EmptyResponse$Type extends MessageType {
*/
export const EmptyResponse = new EmptyResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
-class RemoveClusterRequest$Type extends MessageType {
+class GetClusterRequest$Type extends MessageType {
constructor() {
- super("teleport.lib.teleterm.v1.RemoveClusterRequest", [
+ super("teleport.lib.teleterm.v1.GetClusterRequest", [
{ no: 1, name: "cluster_uri", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
- create(value?: PartialMessage): RemoveClusterRequest {
+ create(value?: PartialMessage): GetClusterRequest {
const message = globalThis.Object.create((this.messagePrototype!));
message.clusterUri = "";
if (value !== undefined)
- reflectionMergePartial(this, message, value);
+ reflectionMergePartial(this, message, value);
return message;
}
- internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: RemoveClusterRequest): RemoveClusterRequest {
+ internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetClusterRequest): GetClusterRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
@@ -1474,7 +1481,7 @@ class RemoveClusterRequest$Type extends MessageType {
}
return message;
}
- internalBinaryWrite(message: RemoveClusterRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
+ internalBinaryWrite(message: GetClusterRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string cluster_uri = 1; */
if (message.clusterUri !== "")
writer.tag(1, WireType.LengthDelimited).string(message.clusterUri);
@@ -1485,24 +1492,26 @@ class RemoveClusterRequest$Type extends MessageType {
}
}
/**
- * @generated MessageType for protobuf message teleport.lib.teleterm.v1.RemoveClusterRequest
+ * @generated MessageType for protobuf message teleport.lib.teleterm.v1.GetClusterRequest
*/
-export const RemoveClusterRequest = new RemoveClusterRequest$Type();
+export const GetClusterRequest = new GetClusterRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
-class GetClusterRequest$Type extends MessageType {
+class LogoutRequest$Type extends MessageType {
constructor() {
- super("teleport.lib.teleterm.v1.GetClusterRequest", [
- { no: 1, name: "cluster_uri", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
+ super("teleport.lib.teleterm.v1.LogoutRequest", [
+ { no: 1, name: "cluster_uri", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
+ { no: 2, name: "remove_profile", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }
]);
}
- create(value?: PartialMessage): GetClusterRequest {
+ create(value?: PartialMessage): LogoutRequest {
const message = globalThis.Object.create((this.messagePrototype!));
message.clusterUri = "";
+ message.removeProfile = false;
if (value !== undefined)
- reflectionMergePartial(this, message, value);
+ reflectionMergePartial(this, message, value);
return message;
}
- internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetClusterRequest): GetClusterRequest {
+ internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: LogoutRequest): LogoutRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
@@ -1510,6 +1519,9 @@ class GetClusterRequest$Type extends MessageType {
case /* string cluster_uri */ 1:
message.clusterUri = reader.string();
break;
+ case /* bool remove_profile */ 2:
+ message.removeProfile = reader.bool();
+ break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -1521,10 +1533,13 @@ class GetClusterRequest$Type extends MessageType {
}
return message;
}
- internalBinaryWrite(message: GetClusterRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
+ internalBinaryWrite(message: LogoutRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string cluster_uri = 1; */
if (message.clusterUri !== "")
writer.tag(1, WireType.LengthDelimited).string(message.clusterUri);
+ /* bool remove_profile = 2; */
+ if (message.removeProfile !== false)
+ writer.tag(2, WireType.Varint).bool(message.removeProfile);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -1532,30 +1547,30 @@ class GetClusterRequest$Type extends MessageType {
}
}
/**
- * @generated MessageType for protobuf message teleport.lib.teleterm.v1.GetClusterRequest
+ * @generated MessageType for protobuf message teleport.lib.teleterm.v1.LogoutRequest
*/
-export const GetClusterRequest = new GetClusterRequest$Type();
+export const LogoutRequest = new LogoutRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
-class LogoutRequest$Type extends MessageType {
+class ClearStaleClusterClientsRequest$Type extends MessageType {
constructor() {
- super("teleport.lib.teleterm.v1.LogoutRequest", [
- { no: 1, name: "cluster_uri", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
+ super("teleport.lib.teleterm.v1.ClearStaleClusterClientsRequest", [
+ { no: 1, name: "root_cluster_uri", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
- create(value?: PartialMessage): LogoutRequest {
+ create(value?: PartialMessage): ClearStaleClusterClientsRequest {
const message = globalThis.Object.create((this.messagePrototype!));
- message.clusterUri = "";
+ message.rootClusterUri = "";
if (value !== undefined)
- reflectionMergePartial(this, message, value);
+ reflectionMergePartial(this, message, value);
return message;
}
- internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: LogoutRequest): LogoutRequest {
+ internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ClearStaleClusterClientsRequest): ClearStaleClusterClientsRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
- case /* string cluster_uri */ 1:
- message.clusterUri = reader.string();
+ case /* string root_cluster_uri */ 1:
+ message.rootClusterUri = reader.string();
break;
default:
let u = options.readUnknownField;
@@ -1568,10 +1583,10 @@ class LogoutRequest$Type extends MessageType {
}
return message;
}
- internalBinaryWrite(message: LogoutRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
- /* string cluster_uri = 1; */
- if (message.clusterUri !== "")
- writer.tag(1, WireType.LengthDelimited).string(message.clusterUri);
+ internalBinaryWrite(message: ClearStaleClusterClientsRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
+ /* string root_cluster_uri = 1; */
+ if (message.rootClusterUri !== "")
+ writer.tag(1, WireType.LengthDelimited).string(message.rootClusterUri);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -1579,9 +1594,34 @@ class LogoutRequest$Type extends MessageType {
}
}
/**
- * @generated MessageType for protobuf message teleport.lib.teleterm.v1.LogoutRequest
+ * @generated MessageType for protobuf message teleport.lib.teleterm.v1.ClearStaleClusterClientsRequest
*/
-export const LogoutRequest = new LogoutRequest$Type();
+export const ClearStaleClusterClientsRequest = new ClearStaleClusterClientsRequest$Type();
+// @generated message type with reflection information, may provide speed optimized methods
+class ClearStaleClusterClientsResponse$Type extends MessageType {
+ constructor() {
+ super("teleport.lib.teleterm.v1.ClearStaleClusterClientsResponse", []);
+ }
+ create(value?: PartialMessage): ClearStaleClusterClientsResponse {
+ const message = globalThis.Object.create((this.messagePrototype!));
+ if (value !== undefined)
+ reflectionMergePartial(this, message, value);
+ return message;
+ }
+ internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ClearStaleClusterClientsResponse): ClearStaleClusterClientsResponse {
+ return target ?? this.create();
+ }
+ internalBinaryWrite(message: ClearStaleClusterClientsResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
+ let u = options.writeUnknownFields;
+ if (u !== false)
+ (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
+ return writer;
+ }
+}
+/**
+ * @generated MessageType for protobuf message teleport.lib.teleterm.v1.ClearStaleClusterClientsResponse
+ */
+export const ClearStaleClusterClientsResponse = new ClearStaleClusterClientsResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class StartHeadlessWatcherRequest$Type extends MessageType {
constructor() {
@@ -5915,7 +5955,6 @@ export const TerminalService = new ServiceType("teleport.lib.teleterm.v1.Termina
{ name: "ListKubernetesResources", options: {}, I: ListKubernetesResourcesRequest, O: ListKubernetesResourcesResponse },
{ name: "ListKubernetesServers", options: {}, I: ListKubernetesServersRequest, O: ListKubernetesServersResponse },
{ name: "AddCluster", options: {}, I: AddClusterRequest, O: Cluster },
- { name: "RemoveCluster", options: {}, I: RemoveClusterRequest, O: EmptyResponse },
{ name: "ListGateways", options: {}, I: ListGatewaysRequest, O: ListGatewaysResponse },
{ name: "CreateGateway", options: {}, I: CreateGatewayRequest, O: Gateway },
{ name: "RemoveGateway", options: {}, I: RemoveGatewayRequest, O: EmptyResponse },
@@ -5926,6 +5965,7 @@ export const TerminalService = new ServiceType("teleport.lib.teleterm.v1.Termina
{ name: "Login", options: {}, I: LoginRequest, O: EmptyResponse },
{ name: "LoginPasswordless", serverStreaming: true, clientStreaming: true, options: {}, I: LoginPasswordlessRequest, O: LoginPasswordlessResponse },
{ name: "Logout", options: {}, I: LogoutRequest, O: EmptyResponse },
+ { name: "ClearStaleClusterClients", options: {}, I: ClearStaleClusterClientsRequest, O: ClearStaleClusterClientsResponse },
{ name: "TransferFile", serverStreaming: true, options: {}, I: FileTransferRequest, O: FileTransferProgress },
{ name: "ReportUsageEvent", options: {}, I: ReportUsageEventRequest, O: EmptyResponse },
{ name: "UpdateHeadlessAuthenticationState", options: {}, I: UpdateHeadlessAuthenticationStateRequest, O: UpdateHeadlessAuthenticationStateResponse },
diff --git a/integration/teleterm_test.go b/integration/teleterm_test.go
index dcd45599ba37f..b3097cb62d683 100644
--- a/integration/teleterm_test.go
+++ b/integration/teleterm_test.go
@@ -134,6 +134,22 @@ func TestTeleterm(t *testing.T) {
testClientCache(t, pack, creds)
})
+ t.Run("clearing stale cached clients", func(t *testing.T) {
+ t.Parallel()
+
+ testClearingStaleCachedClients(t, pack, creds)
+ })
+
+ t.Run("logging out", func(t *testing.T) {
+ t.Parallel()
+ testLogout(t, pack, creds)
+ })
+
+ t.Run("setting site name", func(t *testing.T) {
+ t.Parallel()
+ testSettingSiteName(t, pack, creds)
+ })
+
t.Run("ListDatabaseUsers", func(t *testing.T) {
// ListDatabaseUsers cannot be run in parallel as it modifies the default roles of users set up
// through the test pack.
@@ -549,6 +565,172 @@ func testClientCache(t *testing.T, pack *dbhelpers.DatabasePack, creds *helpers.
require.NotEqual(t, secondCallForClient, thirdCallForClient)
}
+func testClearingStaleCachedClients(t *testing.T, pack *dbhelpers.DatabasePack, creds *helpers.UserCreds) {
+ ctx := context.Background()
+
+ tc := mustLogin(t, pack.Root.User.GetName(), pack, creds)
+
+ storageFakeClock := clockwork.NewFakeClockAt(time.Now())
+
+ storage, err := clusters.NewStorage(clusters.Config{
+ ClientStore: tc.ClientStore,
+ Clock: storageFakeClock,
+ InsecureSkipVerify: tc.InsecureSkipVerify,
+ })
+ require.NoError(t, err)
+
+ cluster, _, err := storage.Add(ctx, tc.WebProxyAddr)
+ require.NoError(t, err)
+
+ tshdEventsClient := daemon.NewTshdEventsClient(func() (grpc.DialOption, error) {
+ return grpc.WithTransportCredentials(insecure.NewCredentials()), nil
+ })
+
+ daemonService, err := daemon.New(daemon.Config{
+ Storage: storage,
+ TshdEventsClient: tshdEventsClient,
+ KubeconfigsDir: t.TempDir(),
+ AgentsDir: t.TempDir(),
+ })
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ daemonService.Stop()
+ })
+
+ firstCallForClient, err := daemonService.GetCachedClient(ctx, cluster.URI)
+ require.NoError(t, err)
+ err = daemonService.ClearStaleCachedClientsForRoot(cluster.URI)
+ require.NoError(t, err)
+ // Ensure the client wasn't closed.
+ secondCallForClient, err := daemonService.GetCachedClient(ctx, cluster.URI)
+ require.NoError(t, err)
+ require.Equal(t, firstCallForClient, secondCallForClient)
+ // Reissue user certs by assuming a role with a bogus ID in DropAccessRequests.
+ accessRequest := &api.AssumeRoleRequest{
+ RootClusterUri: cluster.URI.String(),
+ DropRequestIds: []string{"does-not-matter"},
+ }
+ err = cluster.AssumeRole(ctx, firstCallForClient, accessRequest)
+ require.NoError(t, err)
+ // The cert has changed, so after clearing stale clients,
+ // GetCachedClient should return a new client.
+ err = daemonService.ClearStaleCachedClientsForRoot(cluster.URI)
+ require.NoError(t, err)
+ thirdCallForClient, err := daemonService.GetCachedClient(ctx, cluster.URI)
+ require.NoError(t, err)
+ require.NotEqual(t, secondCallForClient, thirdCallForClient)
+}
+
+func testLogout(t *testing.T, pack *dbhelpers.DatabasePack, creds *helpers.UserCreds) {
+ ctx := context.Background()
+
+ tc := mustLogin(t, pack.Root.User.GetName(), pack, creds)
+
+ storageFakeClock := clockwork.NewFakeClockAt(time.Now())
+
+ storage, err := clusters.NewStorage(clusters.Config{
+ ClientStore: tc.ClientStore,
+ Clock: storageFakeClock,
+ InsecureSkipVerify: tc.InsecureSkipVerify,
+ })
+ require.NoError(t, err)
+
+ cluster, _, err := storage.Add(ctx, tc.WebProxyAddr)
+ require.NoError(t, err)
+
+ tshdEventsClient := daemon.NewTshdEventsClient(func() (grpc.DialOption, error) {
+ return grpc.WithTransportCredentials(insecure.NewCredentials()), nil
+ })
+
+ daemonService, err := daemon.New(daemon.Config{
+ Storage: storage,
+ TshdEventsClient: tshdEventsClient,
+ KubeconfigsDir: t.TempDir(),
+ AgentsDir: t.TempDir(),
+ })
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ daemonService.Stop()
+ })
+
+ // Ensure there is a cluster.
+ rootClusters, err := daemonService.ListRootClusters(ctx)
+ require.NoError(t, err)
+ require.Len(t, rootClusters, 1)
+
+ // Log out without removing the profile.
+ err = daemonService.ClusterLogout(ctx, cluster.URI, false)
+ require.NoError(t, err)
+ rootClusters, err = daemonService.ListRootClusters(ctx)
+ require.NoError(t, err)
+ require.Len(t, rootClusters, 1)
+ require.Empty(t, rootClusters[0].GetLoggedInUser().Name)
+
+ // Log out again, now also remove the profile.
+ err = daemonService.ClusterLogout(ctx, cluster.URI, true)
+ require.NoError(t, err)
+ rootClusters, err = daemonService.ListRootClusters(ctx)
+ require.NoError(t, err)
+ require.Empty(t, rootClusters)
+
+ // Log out again, the operation should be idempotent.
+ err = daemonService.ClusterLogout(ctx, cluster.URI, true)
+ require.NoError(t, err)
+}
+
+func testSettingSiteName(t *testing.T, pack *dbhelpers.DatabasePack, creds *helpers.UserCreds) {
+ ctx := context.Background()
+
+ tc, err := pack.Root.Cluster.NewClient(helpers.ClientConfig{
+ TeleportUser: pack.Root.User.GetName(),
+ Cluster: "root.example.com",
+ })
+ require.NoError(t, err)
+
+ storageFakeClock := clockwork.NewFakeClockAt(time.Now())
+
+ storage, err := clusters.NewStorage(clusters.Config{
+ ClientStore: tc.ClientStore,
+ Clock: storageFakeClock,
+ InsecureSkipVerify: tc.InsecureSkipVerify,
+ })
+ require.NoError(t, err)
+
+ // Add a cluster.
+ cluster, clusterClient, err := storage.Add(ctx, tc.WebProxyAddr)
+ require.NoError(t, err)
+ require.Equal(t, "root.example.com", clusterClient.SiteName)
+ require.Equal(t, "root.example.com", cluster.Name)
+ // Adding a cluster should set the site name in the profile.
+ profile, err := clusterClient.GetProfile(tc.WebProxyAddr)
+ require.NoError(t, err)
+ require.Equal(t, "root.example.com", profile.SiteName)
+
+ // Simulate logging into a leaf cluster, which changes the profile's site name.
+ clusterClient.SiteName = "leaf.example.com"
+ err = clusterClient.SaveProfile(false)
+ require.NoError(t, err)
+
+ // The URI should always resolve to the target cluster, even if the profile's site name points to a different cluster.
+ cluster, clusterClient, err = storage.ResolveCluster(cluster.URI)
+ require.NoError(t, err)
+ // These are empty because the user is not logged in, so there's no cert to retrieve the root cluster name.
+ require.Empty(t, clusterClient.SiteName)
+ require.Empty(t, cluster.Name)
+ // SiteName in the profile should still point to the leaf.
+ profile, err = clusterClient.GetProfile(tc.WebProxyAddr)
+ require.NoError(t, err)
+ require.Equal(t, "leaf.example.com", profile.SiteName)
+
+ // Saving the profile with SaveProfileAndPreserveSiteName doesn't overwrite the profile
+ // with the current clusterClient.SiteName.
+ err = clusters.SaveProfileAndPreserveSiteName(clusterClient, false)
+ require.NoError(t, err)
+ profile, err = clusterClient.GetProfile(tc.WebProxyAddr)
+ require.NoError(t, err)
+ require.Equal(t, "leaf.example.com", profile.SiteName)
+}
+
func testCreateConnectMyComputerRole(t *testing.T, pack *dbhelpers.DatabasePack) {
systemUser, err := user.Current()
require.NoError(t, err)
diff --git a/lib/client/api.go b/lib/client/api.go
index 048cf40037fef..3983eeacdcf02 100644
--- a/lib/client/api.go
+++ b/lib/client/api.go
@@ -1397,38 +1397,12 @@ func (tc *TeleportClient) LoadKeyForCluster(ctx context.Context, clusterName str
return nil
}
-// LoadKeyForClusterWithReissue fetches a cluster-specific SSH key and loads it into the
-// SSH agent. If the key is not found, it is requested to be reissued.
-func (tc *TeleportClient) LoadKeyForClusterWithReissue(ctx context.Context, clusterName string) error {
- ctx, span := tc.Tracer.Start(
- ctx,
- "teleportClient/LoadKeyForClusterWithReissue",
- oteltrace.WithSpanKind(oteltrace.SpanKindClient),
- oteltrace.WithAttributes(attribute.String("cluster", clusterName)),
- )
- defer span.End()
-
- err := tc.LoadKeyForCluster(ctx, clusterName)
- if err == nil {
- return nil
- }
- if !trace.IsNotFound(err) {
- return trace.Wrap(err)
- }
- // Reissuing also loads the new key.
- err = tc.ReissueUserCerts(ctx, CertCacheKeep, ReissueParams{RouteToCluster: clusterName})
- if err != nil {
- return trace.Wrap(err)
- }
- return nil
-}
-
// SignersForClusterWithReissue fetches cluster-specific signers from stored certificates.
// If the cluster certificates are not found, it is requested to be reissued.
func (tc *TeleportClient) SignersForClusterWithReissue(ctx context.Context, clusterName string) ([]ssh.Signer, error) {
ctx, span := tc.Tracer.Start(
ctx,
- "teleportClient/LoadKeyForClusterWithReissue",
+ "teleportClient/SignersForClusterWithReissue",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(attribute.String("cluster", clusterName)),
)
diff --git a/lib/client/client_store.go b/lib/client/client_store.go
index d45037d5a0c94..00fb18c45f832 100644
--- a/lib/client/client_store.go
+++ b/lib/client/client_store.go
@@ -238,16 +238,17 @@ func (s *Store) AddTrustedHostKeys(proxyHost string, clusterName string, hostKey
// ReadProfileStatus returns the profile status for the given profile name.
// If no profile name is provided, return the current profile.
-func (s *Store) ReadProfileStatus(profileName string) (*ProfileStatus, error) {
+func (s *Store) ReadProfileStatus(proxyAddressOrProfile string) (*ProfileStatus, error) {
var err error
- if profileName == "" {
+ var profileName string
+ if proxyAddressOrProfile == "" {
profileName, err = s.CurrentProfile()
if err != nil {
return nil, trace.Wrap(err)
}
} else {
// remove ports from proxy host, because profile name is stored by host name
- profileName, err = utils.Host(profileName)
+ profileName, err = utils.Host(proxyAddressOrProfile)
if err != nil {
return nil, trace.Wrap(err)
}
@@ -265,35 +266,38 @@ func (s *Store) ReadProfileStatus(profileName string) (*ProfileStatus, error) {
ClusterName: profile.SiteName,
Username: profile.Username,
}
+
+ // If we can't find a keyRing to match the profile, connect to the keyRing (hardware key),
+ // or read the full profile status, return a partial status.
+ // This is used for some superficial functions `tsh logout` and `tsh status`.
+ partialStatus := &ProfileStatus{
+ Name: profileName,
+ Dir: profile.Dir,
+ ProxyURL: url.URL{
+ Scheme: "https",
+ Host: profile.WebProxyAddr,
+ },
+ Username: profile.Username,
+ Cluster: profile.SiteName,
+ KubeEnabled: profile.KubeProxyAddr != "",
+ // Set ValidUntil to now and GetKeyRingError to show that the keys are not available.
+ ValidUntil: time.Now(),
+ SAMLSingleLogoutEnabled: profile.SAMLSingleLogoutEnabled,
+ SSOHost: profile.SSOHost,
+ }
+
keyRing, err := s.GetKeyRing(idx, WithAllCerts...)
if err != nil {
if trace.IsNotFound(err) || trace.IsConnectionProblem(err) {
- // If we can't find a keyRing to match the profile, or can't connect to
- // the keyRing (hardware key), return a partial status. This is used for
- // some superficial functions `tsh logout` and `tsh status`.
- return &ProfileStatus{
- Name: profileName,
- Dir: profile.Dir,
- ProxyURL: url.URL{
- Scheme: "https",
- Host: profile.WebProxyAddr,
- },
- Username: profile.Username,
- Cluster: profile.SiteName,
- KubeEnabled: profile.KubeProxyAddr != "",
- // Set ValidUntil to now and GetKeyRingError to show that the keys are not available.
- ValidUntil: time.Now(),
- GetKeyRingError: err,
- SAMLSingleLogoutEnabled: profile.SAMLSingleLogoutEnabled,
- SSOHost: profile.SSOHost,
- }, nil
+ partialStatus.GetKeyRingError = err
+ return partialStatus, nil
}
return nil, trace.Wrap(err)
}
_, onDisk := s.KeyStore.(*FSKeyStore)
- return profileStatusFromKeyRing(keyRing, profileOptions{
+ profileStatus, err := profileStatusFromKeyRing(keyRing, profileOptions{
ProfileName: profileName,
ProfileDir: profile.Dir,
WebProxyAddr: profile.WebProxyAddr,
@@ -307,19 +311,45 @@ func (s *Store) ReadProfileStatus(profileName string) (*ProfileStatus, error) {
IsVirtual: !onDisk,
TLSRoutingEnabled: profile.TLSRoutingEnabled,
})
+ if trace.IsNotFound(err) {
+ return partialStatus, nil
+ } else if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ return profileStatus, nil
}
-// FullProfileStatus returns the name of the current profile with a
-// a list of all profile statuses.
-func (s *Store) FullProfileStatus() (*ProfileStatus, []*ProfileStatus, error) {
- currentProfileName, err := s.CurrentProfile()
- if err != nil {
- return nil, nil, trace.Wrap(err)
+// FullProfileStatus returns the status of the active profile along with the
+// statuses of all profiles.
+//
+// The active profile status is determined from the provided profile if given;
+// otherwise, it is read from the current profile.
+//
+// The active profile status is nil if there is no active profile.
+func (s *Store) FullProfileStatus(proxyAddressOrProfile string) (*ProfileStatus, []*ProfileStatus, error) {
+ var currentProfileName string
+ if proxyAddressOrProfile == "" {
+ profileName, err := s.CurrentProfile()
+ if err != nil && !trace.IsNotFound(err) {
+ return nil, nil, trace.Wrap(err)
+ }
+ currentProfileName = profileName
+ } else {
+ // Remove ports from proxy host, profile name is stored by host name.
+ profileName, err := utils.Host(proxyAddressOrProfile)
+ if err != nil {
+ return nil, nil, trace.Wrap(err)
+ }
+ currentProfileName = profileName
}
- currentProfile, err := s.ReadProfileStatus(currentProfileName)
- if err != nil {
- return nil, nil, trace.Wrap(err)
+ var currentProfile *ProfileStatus
+ if currentProfileName != "" {
+ profileStatus, err := s.ReadProfileStatus(currentProfileName)
+ if err != nil {
+ return nil, nil, trace.Wrap(err)
+ }
+ currentProfile = profileStatus
}
profileNames, err := s.ListProfiles()
diff --git a/lib/client/client_store_test.go b/lib/client/client_store_test.go
index c996e2a04189f..e4c44284e571c 100644
--- a/lib/client/client_store_test.go
+++ b/lib/client/client_store_test.go
@@ -304,11 +304,18 @@ func TestClientStore(t *testing.T) {
})
require.NoError(t, err)
- currentStatus, otherStatuses, err := clientStore.FullProfileStatus()
+ currentStatus, otherStatuses, err := clientStore.FullProfileStatus("")
require.NoError(t, err)
require.Equal(t, expectStatus, currentStatus)
require.Len(t, otherStatuses, 1)
require.Equal(t, expectOtherStatus, otherStatuses[0])
+
+ // Test passing the active profile in the parameter.
+ currentStatus, otherStatuses, err = clientStore.FullProfileStatus("other.example.com")
+ require.NoError(t, err)
+ require.Equal(t, expectOtherStatus, currentStatus)
+ require.Len(t, otherStatuses, 1)
+ require.Equal(t, expectStatus, otherStatuses[0])
})
})
}
diff --git a/lib/client/clientcache/clientcache.go b/lib/client/clientcache/clientcache.go
index f5e8f44aafdf9..c8f92b9718050 100644
--- a/lib/client/clientcache/clientcache.go
+++ b/lib/client/clientcache/clientcache.go
@@ -17,6 +17,7 @@
package clientcache
import (
+ "bytes"
"context"
"log/slog"
"slices"
@@ -35,12 +36,47 @@ import (
type Cache struct {
cfg Config
mu sync.RWMutex
- // clients keeps a mapping from key (profile name and leaf cluster name) to cluster client.
- clients map[key]*client.ClusterClient
+ // clients keeps a mapping from key (profile name and leaf cluster name) to client.
+ clients map[key]*clientWithMetadata
// group prevents duplicate requests to create clients for a given cluster.
group singleflight.Group
}
+type clientWithMetadata struct {
+ client *client.ClusterClient
+ // tlsCert is the certificate that was loaded when the client was created.
+ tlsCert []byte
+ // getProfile reads the fresh profile for the client from disk.
+ getProfile func() (profile, error)
+}
+
+type profile interface {
+ // TLSCert returns the current TLS cert, stored in the agent.
+ TLSCert() ([]byte, error)
+}
+
+// isTLSCertStale checks if the cached client uses the current TLS cert
+// (read from the agent). If not, the TLS cert is considered stale.
+func (c *clientWithMetadata) isTLSCertStale() (bool, error) {
+ pr, err := c.getProfile()
+ if err != nil {
+ if trace.IsNotFound(err) {
+ return true, nil
+ }
+ return false, trace.Wrap(err)
+ }
+
+ tlsCert, err := pr.TLSCert()
+ if err != nil {
+ if trace.IsNotFound(err) {
+ return true, nil
+ }
+ return false, trace.Wrap(err)
+ }
+
+ return !bytes.Equal(c.tlsCert, tlsCert), nil
+}
+
// NewClientFunc is a function that will return a new [*client.TeleportClient] for a given profile and leaf
// cluster. [leafClusterName] may be empty, in which case implementations should return a client for the root cluster.
type NewClientFunc func(ctx context.Context, profileName, leafClusterName string) (*client.TeleportClient, error)
@@ -89,7 +125,7 @@ func New(c Config) (*Cache, error) {
return &Cache{
cfg: c,
- clients: make(map[key]*client.ClusterClient),
+ clients: make(map[key]*clientWithMetadata),
}, nil
}
@@ -100,7 +136,7 @@ func (c *Cache) Get(ctx context.Context, profileName, leafClusterName string) (*
groupClt, err, _ := c.group.Do(k.String(), func() (any, error) {
if fromCache := c.getFromCache(k); fromCache != nil {
c.cfg.Logger.DebugContext(ctx, "Retrieved client from cache", "cluster", k)
- return fromCache, nil
+ return fromCache.client, nil
}
tc, err := c.cfg.NewClientFunc(ctx, profileName, leafClusterName)
@@ -120,8 +156,19 @@ func (c *Cache) Get(ctx context.Context, profileName, leafClusterName string) (*
return nil, trace.Wrap(err)
}
+ keyRing, err := tc.LocalAgent().GetCoreKeyRing()
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
// Save the client in the cache, so we don't have to build a new connection next time.
- c.addToCache(k, newClient)
+ c.addToCache(k, &clientWithMetadata{
+ client: newClient,
+ tlsCert: keyRing.TLSCert,
+ getProfile: func() (profile, error) {
+ return tc.GetProfile(tc.WebProxyAddr)
+ },
+ })
c.cfg.Logger.InfoContext(ctx, "Added client to cache", "cluster", k)
@@ -139,8 +186,25 @@ func (c *Cache) Get(ctx context.Context, profileName, leafClusterName string) (*
return clt, nil
}
+// ClearOption configures ClearForRoot behavior.
+type ClearOption func(*clearConfig)
+
+type clearConfig struct {
+ onlyClearClientsWithStaleCert bool
+}
+
+// WithClearingOnlyClientsWithStaleCert closes only clients that use outdated certs.
+func WithClearingOnlyClientsWithStaleCert() ClearOption {
+ return func(c *clearConfig) { c.onlyClearClientsWithStaleCert = true }
+}
+
// ClearForRoot closes and removes clients from the cache for the root cluster and its leaf clusters.
-func (c *Cache) ClearForRoot(profileName string) error {
+func (c *Cache) ClearForRoot(profileName string, opts ...ClearOption) error {
+ cfg := &clearConfig{}
+ for _, o := range opts {
+ o(cfg)
+ }
+
c.mu.Lock()
defer c.mu.Unlock()
@@ -150,13 +214,23 @@ func (c *Cache) ClearForRoot(profileName string) error {
)
for k, clt := range c.clients {
- if k.profile == profileName {
- if err := clt.Close(); err != nil {
+ if k.profile != profileName {
+ continue
+ }
+ if cfg.onlyClearClientsWithStaleCert {
+ stale, err := clt.isTLSCertStale()
+ // If an error occurs, close the client as well.
+ if err != nil {
errors = append(errors, err)
+ } else if !stale {
+ continue
}
- deleted = append(deleted, k.String())
- delete(c.clients, k)
}
+ if err := clt.client.Close(); err != nil {
+ errors = append(errors, err)
+ }
+ deleted = append(deleted, k.String())
+ delete(c.clients, k)
}
c.cfg.Logger.InfoContext(context.Background(), "Invalidated cached clients for root cluster",
@@ -165,7 +239,6 @@ func (c *Cache) ClearForRoot(profileName string) error {
)
return trace.NewAggregate(errors...)
-
}
// Clear closes and removes all clients.
@@ -175,7 +248,7 @@ func (c *Cache) Clear() error {
var errors []error
for _, clt := range c.clients {
- if err := clt.Close(); err != nil {
+ if err := clt.client.Close(); err != nil {
errors = append(errors, err)
}
}
@@ -184,14 +257,14 @@ func (c *Cache) Clear() error {
return trace.NewAggregate(errors...)
}
-func (c *Cache) addToCache(k key, clusterClient *client.ClusterClient) {
+func (c *Cache) addToCache(k key, clt *clientWithMetadata) {
c.mu.Lock()
defer c.mu.Unlock()
- c.clients[k] = clusterClient
+ c.clients[k] = clt
}
-func (c *Cache) getFromCache(k key) *client.ClusterClient {
+func (c *Cache) getFromCache(k key) *clientWithMetadata {
c.mu.RLock()
defer c.mu.RUnlock()
@@ -241,7 +314,7 @@ func (c *NoCache) Get(ctx context.Context, profileName, leafClusterName string)
return newClient, nil
}
-func (c *NoCache) ClearForRoot(profileName string) error {
+func (c *NoCache) ClearForRoot(profileName string, _ ...ClearOption) error {
c.mu.Lock()
defer c.mu.Unlock()
diff --git a/lib/client/clientcache/clientcache_test.go b/lib/client/clientcache/clientcache_test.go
new file mode 100644
index 0000000000000..4e1aa962bb364
--- /dev/null
+++ b/lib/client/clientcache/clientcache_test.go
@@ -0,0 +1,180 @@
+// Teleport
+// Copyright (C) 2025 Gravitational, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package clientcache
+
+import (
+ "context"
+ "crypto/x509/pkix"
+ "log/slog"
+ "testing"
+
+ "github.com/gravitational/trace"
+ "github.com/jonboulle/clockwork"
+ "github.com/stretchr/testify/require"
+ "golang.org/x/crypto/ssh"
+
+ apiprofile "github.com/gravitational/teleport/api/profile"
+ "github.com/gravitational/teleport/api/utils/keys"
+ "github.com/gravitational/teleport/lib/auth/testauthority"
+ "github.com/gravitational/teleport/lib/client"
+ "github.com/gravitational/teleport/lib/defaults"
+ "github.com/gravitational/teleport/lib/fixtures"
+ "github.com/gravitational/teleport/lib/observability/tracing"
+ "github.com/gravitational/teleport/lib/sshca"
+ "github.com/gravitational/teleport/lib/tlsca"
+)
+
+func TestClearingClientsWithStaleCert(t *testing.T) {
+ privateKey, err := keys.ParsePrivateKey(fixtures.PEMBytes["rsa"])
+ require.NoError(t, err)
+ tlsCert, sshCert, err := makeCerts(privateKey)
+ require.NoError(t, err)
+
+ keyRing := client.NewKeyRing(privateKey, privateKey)
+ keyRing.KeyRingIndex = client.KeyRingIndex{
+ ProxyHost: "localhost",
+ Username: "testuser",
+ ClusterName: "root",
+ }
+ keyRing.Cert = sshCert
+ keyRing.TLSCert = tlsCert
+
+ profile := &apiprofile.Profile{
+ WebProxyAddr: keyRing.ProxyHost,
+ Username: keyRing.Username,
+ SiteName: keyRing.ClusterName,
+ }
+ clientStore := client.NewFSClientStore(t.TempDir())
+ err = clientStore.SaveProfile(profile, true)
+ require.NoError(t, err)
+ err = clientStore.AddKeyRing(keyRing)
+ require.NoError(t, err)
+
+ cache, err := New(Config{
+ NewClientFunc: func(ctx context.Context, profileName, leafClusterName string) (*client.TeleportClient, error) {
+ config := &client.Config{
+ ClientStore: clientStore,
+ SSHProxyAddr: "localhost:3080",
+ WebProxyAddr: "localhost:3080",
+ Username: "testuser",
+ Tracer: tracing.NoopProvider().Tracer("test"),
+ SiteName: "root",
+ }
+ if leafClusterName != "" {
+ config.SiteName = leafClusterName
+ }
+ tc, err := client.NewClient(config)
+
+ return tc, err
+ },
+ RetryWithReloginFunc: func(ctx context.Context, tc *client.TeleportClient, fn func() error, opts ...client.RetryWithReloginOption) error {
+ return fn()
+ },
+ Logger: slog.New(slog.DiscardHandler),
+ })
+ require.NoError(t, err)
+
+ // Get clients.
+ rootClient, err := cache.Get(t.Context(), "root", "")
+ require.NoError(t, err)
+ leaf1Client, err := cache.Get(t.Context(), "root", "leaf1")
+ require.NoError(t, err)
+
+ // Update the TLS cert.
+ tlsCert, _, err = makeCerts(privateKey)
+ require.NoError(t, err)
+ keyRing.TLSCert = tlsCert
+ err = clientStore.AddKeyRing(keyRing)
+ require.NoError(t, err)
+
+ // Get the client for a new leaf after the cert has been updated.
+ leaf2Client, err := cache.Get(t.Context(), "root", "leaf2")
+ require.NoError(t, err)
+
+ // Clear stale clients.
+ err = cache.ClearForRoot("root", WithClearingOnlyClientsWithStaleCert())
+ require.NoError(t, err)
+
+ newRootClient, err := cache.Get(t.Context(), "root", "")
+ require.NoError(t, err)
+ newLeaf1Client, err := cache.Get(t.Context(), "root", "leaf1")
+ require.NoError(t, err)
+ newLeaf2Client, err := cache.Get(t.Context(), "root", "leaf2")
+ require.NoError(t, err)
+ // Clients opened before updating the cert should be reopened.
+ require.NotEqual(t, newRootClient, rootClient)
+ require.NotEqual(t, newLeaf1Client, leaf1Client)
+ // The client opened after updating the cert should be untouched.
+ require.Equal(t, newLeaf2Client, leaf2Client)
+}
+
+// makeCerts makes TSL and SSH certs.
+func makeCerts(privateKey *keys.PrivateKey) ([]byte, []byte, error) {
+ cert, err := tlsca.GenerateSelfSignedCAWithSigner(privateKey, pkix.Name{
+ CommonName: "root",
+ Organization: []string{"localhost"},
+ }, nil, defaults.CATTL)
+ if err != nil {
+ return nil, nil, trace.Wrap(err)
+ }
+ ca, err := tlsca.FromCertAndSigner(cert, privateKey)
+ if err != nil {
+ return nil, nil, trace.Wrap(err)
+ }
+
+ keygen := testauthority.New()
+
+ clock := clockwork.NewRealClock()
+ identity := tlsca.Identity{
+ Username: "testuser",
+ }
+
+ subject, err := identity.Subject()
+ if err != nil {
+ return nil, nil, trace.Wrap(err)
+ }
+
+ tlsCert, err := ca.GenerateCertificate(tlsca.CertificateRequest{
+ Clock: clock,
+ PublicKey: privateKey.Public(),
+ Subject: subject,
+ NotAfter: clock.Now().UTC().Add(defaults.CATTL),
+ })
+ if err != nil {
+ return nil, nil, trace.Wrap(err)
+ }
+
+ signer, err := keys.ParsePrivateKey([]byte(fixtures.SSHCAPrivateKey))
+ if err != nil {
+ return nil, nil, trace.Wrap(err)
+ }
+ caSigner, err := ssh.NewSignerFromKey(signer)
+ if err != nil {
+ return nil, nil, trace.Wrap(err)
+ }
+
+ sshCert, err := keygen.GenerateUserCert(sshca.UserCertificateRequest{
+ CASigner: caSigner,
+ PublicUserKey: ssh.MarshalAuthorizedKey(privateKey.SSHPublicKey()),
+ Identity: sshca.Identity{
+ Username: "testuser",
+ Principals: []string{"testuser"},
+ },
+ })
+
+ return tlsCert, sshCert, trace.Wrap(err)
+}
diff --git a/lib/teleterm/apiserver/handler/handler_auth.go b/lib/teleterm/apiserver/handler/handler_auth.go
index 5193e5821dfdd..32d51d54fe9f8 100644
--- a/lib/teleterm/apiserver/handler/handler_auth.go
+++ b/lib/teleterm/apiserver/handler/handler_auth.go
@@ -26,6 +26,7 @@ import (
"github.com/gravitational/teleport"
api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1"
"github.com/gravitational/teleport/lib/client"
+ "github.com/gravitational/teleport/lib/teleterm/api/uri"
"github.com/gravitational/teleport/lib/utils"
)
@@ -106,9 +107,16 @@ func (s *Handler) LoginPasswordless(stream api.TerminalService_LoginPasswordless
return trace.Wrap(err)
}
-// Logout logs a user out from a cluster
+// Logout logs the user out of the cluster and cleans up associated resources.
+// Optionally removes the profile.
+// This operation is idempotent and can be safely invoked multiple times.
func (s *Handler) Logout(ctx context.Context, req *api.LogoutRequest) (*api.EmptyResponse, error) {
- if err := s.DaemonService.ClusterLogout(ctx, req.ClusterUri); err != nil {
+ clusterURI, err := uri.Parse(req.GetClusterUri())
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ if err = s.DaemonService.ClusterLogout(ctx, clusterURI, req.RemoveProfile); err != nil {
return nil, trace.Wrap(err)
}
diff --git a/lib/teleterm/apiserver/handler/handler_clusters.go b/lib/teleterm/apiserver/handler/handler_clusters.go
index 68769b0194473..8d25d1a5c1297 100644
--- a/lib/teleterm/apiserver/handler/handler_clusters.go
+++ b/lib/teleterm/apiserver/handler/handler_clusters.go
@@ -22,9 +22,11 @@ import (
"context"
"github.com/gravitational/trace"
+ "google.golang.org/protobuf/types/known/timestamppb"
"github.com/gravitational/teleport/api/constants"
api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1"
+ "github.com/gravitational/teleport/lib/teleterm/api/uri"
"github.com/gravitational/teleport/lib/teleterm/clusters"
)
@@ -70,15 +72,6 @@ func (s *Handler) AddCluster(ctx context.Context, req *api.AddClusterRequest) (*
return newAPIRootCluster(cluster), nil
}
-// RemoveCluster removes a cluster from local system
-func (s *Handler) RemoveCluster(ctx context.Context, req *api.RemoveClusterRequest) (*api.EmptyResponse, error) {
- if err := s.DaemonService.RemoveCluster(ctx, req.ClusterUri); err != nil {
- return nil, trace.Wrap(err)
- }
-
- return &api.EmptyResponse{}, nil
-}
-
// GetCluster returns a cluster
func (s *Handler) GetCluster(ctx context.Context, req *api.GetClusterRequest) (*api.Cluster, error) {
cluster, _, err := s.DaemonService.ResolveClusterWithDetails(ctx, req.ClusterUri)
@@ -91,6 +84,17 @@ func (s *Handler) GetCluster(ctx context.Context, req *api.GetClusterRequest) (*
return apiRootClusterWithDetails, trace.Wrap(err)
}
+// ClearStaleClusterClients closes root and leaf cluster clients that use outdated TLS certificates.
+func (s *Handler) ClearStaleClusterClients(_ context.Context, req *api.ClearStaleClusterClientsRequest) (*api.ClearStaleClusterClientsResponse, error) {
+ parsed, err := uri.Parse(req.RootClusterUri)
+ if err != nil {
+ return &api.ClearStaleClusterClientsResponse{}, trace.Wrap(err)
+ }
+
+ err = s.DaemonService.ClearStaleCachedClientsForRoot(parsed)
+ return &api.ClearStaleClusterClientsResponse{}, trace.Wrap(err)
+}
+
func newAPIRootCluster(cluster *clusters.Cluster) *api.Cluster {
loggedInUser := cluster.GetLoggedInUser()
@@ -104,6 +108,7 @@ func newAPIRootCluster(cluster *clusters.Cluster) *api.Cluster {
Roles: loggedInUser.Roles,
ActiveRequests: loggedInUser.ActiveRequests,
IsDeviceTrusted: cluster.HasDeviceTrustExtensions(),
+ ValidUntil: timestamppb.New(loggedInUser.ValidUntil),
},
SsoHost: cluster.SSOHost,
}
diff --git a/lib/teleterm/clusters/cluster.go b/lib/teleterm/clusters/cluster.go
index 326ad470fe5d2..3085bbe343536 100644
--- a/lib/teleterm/clusters/cluster.go
+++ b/lib/teleterm/clusters/cluster.go
@@ -21,6 +21,7 @@ package clusters
import (
"context"
"log/slog"
+ "time"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
@@ -312,6 +313,7 @@ func (c *Cluster) GetLoggedInUser() LoggedInUser {
SSHLogins: c.status.Logins,
Roles: c.status.Roles,
ActiveRequests: c.status.ActiveRequests,
+ ValidUntil: c.status.ValidUntil,
}
}
@@ -353,6 +355,8 @@ type LoggedInUser struct {
Roles []string
// ActiveRequests is the user active requests
ActiveRequests []string
+ // ValidUntil is expiration time of the certificate.
+ ValidUntil time.Time
}
// AddMetadataToRetryableError is Connect's equivalent of client.RetryWithRelogin. By adding the
@@ -397,3 +401,21 @@ type Server struct {
types.Server
}
+
+// SaveProfileAndPreserveSiteName wraps [client.TeleportClient.SaveProfile]. It restores the original site name before
+// saving the profile.
+//
+// It's needed because teleterm/clusters.Storage.loadProfileStatusAndClusterKey overwrites the profile's site name so that
+// the cluster client is created for the cluster specified in the URI rather than the one stored in the profile.
+// This adjustment is only relevant for Connect and should not be persisted.
+func SaveProfileAndPreserveSiteName(clusterClient *client.TeleportClient, makeCurrent bool) error {
+ profile, err := clusterClient.GetProfile(clusterClient.WebProxyAddr)
+ if err != nil && !trace.IsNotFound(err) {
+ return trace.Wrap(err)
+ }
+ if profile != nil {
+ clusterClient.SiteName = profile.SiteName
+ }
+ err = clusterClient.SaveProfile(makeCurrent)
+ return trace.Wrap(err)
+}
diff --git a/lib/teleterm/clusters/cluster_access_requests.go b/lib/teleterm/clusters/cluster_access_requests.go
index de054ebd9b59b..dc90d6e3a0ad3 100644
--- a/lib/teleterm/clusters/cluster_access_requests.go
+++ b/lib/teleterm/clusters/cluster_access_requests.go
@@ -234,7 +234,7 @@ func (c *Cluster) AssumeRole(ctx context.Context, rootClient *client.ClusterClie
return trace.Wrap(err)
}
- err = c.clusterClient.SaveProfile(true)
+ err = SaveProfileAndPreserveSiteName(c.clusterClient, true)
return trace.Wrap(err)
}
diff --git a/lib/teleterm/clusters/cluster_auth.go b/lib/teleterm/clusters/cluster_auth.go
index ebbb1e271ddbe..9f576e31b3bb8 100644
--- a/lib/teleterm/clusters/cluster_auth.go
+++ b/lib/teleterm/clusters/cluster_auth.go
@@ -53,7 +53,7 @@ func (c *Cluster) SyncAuthPreference(ctx context.Context) (*webclient.WebConfigA
c.Logger.DebugContext(ctx, "Got ping response", "response", string(pingResponseJSON))
}
- if err := c.clusterClient.SaveProfile(false); err != nil {
+ if err := SaveProfileAndPreserveSiteName(c.clusterClient, false); err != nil {
return nil, nil, trace.Wrap(err)
}
diff --git a/lib/teleterm/clusters/storage.go b/lib/teleterm/clusters/storage.go
index 02617ffc76585..af45d283d87d1 100644
--- a/lib/teleterm/clusters/storage.go
+++ b/lib/teleterm/clusters/storage.go
@@ -120,7 +120,9 @@ func (s *Storage) Remove(ctx context.Context, profileName string) error {
// https://github.com/gravitational/teleport/issues/13278
func (s *Storage) Add(ctx context.Context, webProxyAddress string) (*Cluster, *client.TeleportClient, error) {
profiles, err := s.ListProfileNames()
- if err != nil {
+ // If the tsh directory does not exist, [client.ProfileStore.SaveProfile] will
+ // create it.
+ if err != nil && !trace.IsNotFound(err) {
return nil, nil, trace.Wrap(err)
}
@@ -169,6 +171,13 @@ func (s *Storage) addCluster(ctx context.Context, webProxyAddress string) (*Clus
return nil, nil, trace.Wrap(err)
}
+ // There's an incorrect default in api/profile.profileFromFile - an empty SiteName is replaced with the profile name.
+ // A profile name is not the same thing as a site name, and they differ when the proxy hostname is different
+ // from the cluster name.
+ // Using this incorrect site name causes login failures in `tsh`, so we proactively set SiteName to the root cluster
+ // name instead.
+ clusterClient.SiteName = pingResponse.ClusterName
+
clusterLog := s.Logger.With("cluster", clusterURI)
pingResponseJSON, err := json.Marshal(pingResponse)
@@ -272,15 +281,38 @@ func (s *Storage) loadProfileStatusAndClusterKey(clusterClient *client.TeleportC
clusterClient.SiteName = rootClusterName
}
- if err == nil && clusterClient.Username != "" {
- status, err = clusterClient.ProfileStatus()
- if err != nil {
- return nil, trace.Wrap(err)
- }
+ // TODO(gzdunek): If the key doesn't exist, we should still try to read
+ // the profile status.
+ // This creates an inconsistency in how the profile is interpreted after running
+ //`tsh logout --proxy=... --user=...` by `tsh status` versus Connect.
+ //
+ // tsh will still show a profile that includes the username, while Connect
+ // receives an empty profile status and therefore has no username.
+ // Fixing this requires updating how ClusterLifecycleManager detects logouts.
+ // Right now it assumes that a logout results in an empty username.
+ // After the fix, the username would still be present, so we'll need to rely on
+ // a different field of LoggedInUser (or introduce a new one) to determine logout
+ // state reliably.
+ if err != nil || clusterClient.Username == "" {
+ return status, nil
+ }
- if err := clusterClient.LoadKeyForCluster(context.Background(), status.Cluster); err != nil {
+ status, err = clusterClient.ProfileStatus()
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ // Load SSH key for the cluster indicated in the profile.
+ // Skip if the profile is empty, the key cannot be found, or the key isn't supported as an agent key.
+ err = clusterClient.LoadKeyForCluster(context.Background(), status.Cluster)
+ if err != nil {
+ if !trace.IsNotFound(err) && !trace.IsConnectionProblem(err) && !trace.IsCompareFailed(err) {
return nil, trace.Wrap(err)
}
+ s.Logger.InfoContext(context.Background(), "Could not load key for cluster into the local agent",
+ "cluster", status.Cluster,
+ "error", err,
+ )
}
return status, nil
diff --git a/lib/teleterm/daemon/config.go b/lib/teleterm/daemon/config.go
index a8b87de4af022..ee758d72e8cff 100644
--- a/lib/teleterm/daemon/config.go
+++ b/lib/teleterm/daemon/config.go
@@ -95,7 +95,7 @@ type ClientCache interface {
Get(ctx context.Context, profileName, leafClusterName string) (*client.ClusterClient, error)
// ClearForRoot closes and removes clients from the cache
// for the root cluster and its leaf clusters.
- ClearForRoot(profileName string) error
+ ClearForRoot(profileName string, opts ...clientcache.ClearOption) error
// Clear closes and removes all clients.
Clear() error
}
diff --git a/lib/teleterm/daemon/daemon.go b/lib/teleterm/daemon/daemon.go
index f99fdcb272107..83c8757b4f711 100644
--- a/lib/teleterm/daemon/daemon.go
+++ b/lib/teleterm/daemon/daemon.go
@@ -40,6 +40,7 @@ import (
api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/client"
+ "github.com/gravitational/teleport/lib/client/clientcache"
"github.com/gravitational/teleport/lib/client/sso"
dtauthn "github.com/gravitational/teleport/lib/devicetrust/authn"
"github.com/gravitational/teleport/lib/teleterm/api/uri"
@@ -267,26 +268,6 @@ func (s *Service) newDesktopSession(desktopURI uri.ResourceURI, login string) (*
return session, cleanup, nil
}
-// RemoveCluster removes cluster
-func (s *Service) RemoveCluster(ctx context.Context, uri string) error {
- cluster, _, err := s.ResolveCluster(uri)
- if err != nil {
- return trace.Wrap(err)
- }
-
- if cluster.Connected() {
- if err := cluster.Logout(ctx); err != nil {
- return trace.Wrap(err)
- }
- }
-
- if err := s.cfg.Storage.Remove(ctx, cluster.ProfileName); err != nil {
- return trace.Wrap(err)
- }
-
- return nil
-}
-
// NewClusterClient is a wrapper on ResolveClusterURI that can be passed as an argument to
// s.cfg.CreateClientCacheFunc.
func (s *Service) NewClusterClient(ctx context.Context, profileName, leafClusterName string) (*client.TeleportClient, error) {
@@ -349,22 +330,30 @@ func (s *Service) ResolveClusterWithDetails(ctx context.Context, uri string) (*c
return withDetails, clusterClient, nil
}
-// ClusterLogout logs a user out from the cluster
-func (s *Service) ClusterLogout(ctx context.Context, uri string) error {
- cluster, _, err := s.ResolveCluster(uri)
- if err != nil {
+// ClusterLogout logs the user out of the cluster and cleans up associated resources.
+// Optionally removes the profile.
+// This operation is idempotent and can be safely invoked multiple times.
+func (s *Service) ClusterLogout(ctx context.Context, uri uri.ResourceURI, removeProfile bool) error {
+ cluster, _, err := s.ResolveClusterURI(uri)
+ if err != nil && !trace.IsNotFound(err) {
return trace.Wrap(err)
}
-
- if err := cluster.Logout(ctx); err != nil {
- return trace.Wrap(err)
+ if err == nil {
+ if err = cluster.Logout(ctx); err != nil {
+ return trace.Wrap(err)
+ }
+ if removeProfile {
+ if err = s.cfg.Storage.Remove(ctx, cluster.ProfileName); err != nil {
+ return trace.Wrap(err)
+ }
+ }
}
- if err := s.StopHeadlessWatcher(uri); err != nil && !trace.IsNotFound(err) {
+ if err = s.StopHeadlessWatcher(uri.String()); err != nil && !trace.IsNotFound(err) {
return trace.Wrap(err)
}
- return trace.Wrap(s.ClearCachedClientsForRoot(cluster.URI))
+ return trace.Wrap(s.ClearCachedClientsForRoot(uri))
}
// CreateGateway creates a gateway to given targetURI
@@ -1295,6 +1284,14 @@ func (s *Service) ClearCachedClientsForRoot(clusterURI uri.ResourceURI) error {
return trace.Wrap(s.clientCache.ClearForRoot(profileName))
}
+// ClearStaleCachedClientsForRoot closes and removes clients from the cache
+// for the root cluster and its leaf clusters, if their cert is outdated.
+func (s *Service) ClearStaleCachedClientsForRoot(clusterURI uri.ResourceURI) error {
+ profileName := clusterURI.GetProfileName()
+ err := s.clientCache.ClearForRoot(profileName, clientcache.WithClearingOnlyClientsWithStaleCert())
+ return trace.Wrap(err)
+}
+
// SetSharedDirectoryForDesktopSession opens a directory for a desktop session and enables file system operations for it.
// If there is no active desktop session associated with the specified desktop_uri and login,
// an error is returned.
diff --git a/proto/teleport/lib/teleterm/v1/cluster.proto b/proto/teleport/lib/teleterm/v1/cluster.proto
index 6a9a3bdd92615..09170fa61af0b 100644
--- a/proto/teleport/lib/teleterm/v1/cluster.proto
+++ b/proto/teleport/lib/teleterm/v1/cluster.proto
@@ -20,6 +20,7 @@ syntax = "proto3";
package teleport.lib.teleterm.v1;
+import "google/protobuf/timestamp.proto";
import "teleport/legacy/types/trusted_device_requirement.proto";
option go_package = "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1;teletermv1";
@@ -111,6 +112,8 @@ message LoggedInUser {
bool is_device_trusted = 9;
// Indicates whether access may be hindered by the lack of a trusted device.
types.TrustedDeviceRequirement trusted_device_requirement = 10;
+ // Expiration time of the certificate.
+ google.protobuf.Timestamp valid_until = 11;
}
// ACL is the access control list of the user
diff --git a/proto/teleport/lib/teleterm/v1/service.proto b/proto/teleport/lib/teleterm/v1/service.proto
index 13092ecba95ea..353433c59d6d8 100644
--- a/proto/teleport/lib/teleterm/v1/service.proto
+++ b/proto/teleport/lib/teleterm/v1/service.proto
@@ -91,8 +91,6 @@ service TerminalService {
rpc ListKubernetesServers(ListKubernetesServersRequest) returns (ListKubernetesServersResponse);
// AddCluster adds a cluster to profile
rpc AddCluster(AddClusterRequest) returns (Cluster);
- // RemoveCluster removes a cluster from profile
- rpc RemoveCluster(RemoveClusterRequest) returns (EmptyResponse);
// ListGateways lists gateways
rpc ListGateways(ListGatewaysRequest) returns (ListGatewaysResponse);
@@ -133,8 +131,12 @@ service TerminalService {
// -> Receive the index number associated with the selected credential in list
// <- End
rpc LoginPasswordless(stream LoginPasswordlessRequest) returns (stream LoginPasswordlessResponse);
- // ClusterLogin logs out a user from cluster
+ // Logs the user out of the cluster and cleans up associated resources.
+ // Optionally removes the profile.
+ // This operation is idempotent and can be safely invoked multiple times.
rpc Logout(LogoutRequest) returns (EmptyResponse);
+ // Closes root and leaf cluster clients that use outdated TLS certificates.
+ rpc ClearStaleClusterClients(ClearStaleClusterClientsRequest) returns (ClearStaleClusterClientsResponse);
// TransferFile sends a request to download/upload a file
rpc TransferFile(FileTransferRequest) returns (stream FileTransferProgress);
// ReportUsageEvent allows to send usage events that are then anonymized and forwarded to prehog
@@ -193,21 +195,23 @@ service TerminalService {
message EmptyResponse {}
-// RemoveClusterRequest describes RemoveClusterRequest
-message RemoveClusterRequest {
- string cluster_uri = 1;
-}
-
// GetClusterRequest describes GetClusterRequest
message GetClusterRequest {
string cluster_uri = 1;
}
-// LogoutRequest describes LogoutRequest
message LogoutRequest {
string cluster_uri = 1;
+ // Whether to remove the associated YAML profile after logout.
+ bool remove_profile = 2;
}
+message ClearStaleClusterClientsRequest {
+ string root_cluster_uri = 1;
+}
+
+message ClearStaleClusterClientsResponse {}
+
message StartHeadlessWatcherRequest {
string root_cluster_uri = 1;
}
diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go
index b9e44bfdb7155..ec9fabeb86aa6 100644
--- a/tool/tsh/common/tsh.go
+++ b/tool/tsh/common/tsh.go
@@ -2666,7 +2666,22 @@ func onLogout(cf *CLIConf) error {
fmt.Fprintf(cf.Stdout(), "Logged out %v from %v.\n", cf.Username, proxyHost)
// Remove all keys.
case proxyHost == "" && cf.Username == "":
- tc, err := makeClient(cf)
+ proxy, err := cf.getClientStore().CurrentProfile()
+ if err != nil && !trace.IsNotFound(err) {
+ return trace.Wrap(err)
+ }
+
+ if proxy == "" {
+ // If there's no current profile, try to use the first profile.
+ if len(profiles) > 0 {
+ proxy = profiles[0].ProxyURL.Host
+ } else {
+ fmt.Printf("All users logged out.\n")
+ return nil
+ }
+ }
+
+ tc, err := makeClientForProxy(cf, proxy)
if err != nil {
return trace.Wrap(err)
}
@@ -5112,7 +5127,7 @@ func (c *CLIConf) ProfileStatus() (*client.ProfileStatus, error) {
}
func (c *CLIConf) FullProfileStatus() (*client.ProfileStatus, []*client.ProfileStatus, error) {
- currentProfile, profiles, err := c.getClientStore().FullProfileStatus()
+ currentProfile, profiles, err := c.getClientStore().FullProfileStatus(c.Proxy)
if err != nil {
return nil, nil, trace.Wrap(err)
}
@@ -5338,7 +5353,7 @@ func humanFriendlyValidUntilDuration(validUntil time.Time, clock clockwork.Clock
}
// printStatus prints the status of the profile.
-func printStatus(debug bool, p *profileInfo, env map[string]string, isActive bool) {
+func printStatus(w io.Writer, debug bool, p *profileInfo, env map[string]string, isActive bool) {
clock := clockwork.NewRealClock()
var prefix string
proxyURL := p.getProxyURLLine(isActive, env)
@@ -5350,40 +5365,40 @@ func printStatus(debug bool, p *profileInfo, env map[string]string, isActive boo
prefix = " "
}
- fmt.Printf("%vProfile URL: %v\n", prefix, proxyURL)
+ fmt.Fprintf(w, "%vProfile URL: %v\n", prefix, proxyURL)
if debug {
switch {
case p.RelayAddr == "" && p.DefaultRelayAddr != "":
- fmt.Printf(" Relay address: %v (default)\n", p.DefaultRelayAddr)
+ fmt.Fprintf(w, " Relay address: %v (default)\n", p.DefaultRelayAddr)
case p.RelayAddr != "" && p.DefaultRelayAddr != "":
- fmt.Printf(" Relay address: %v (default: %v)\n", p.RelayAddr, p.DefaultRelayAddr)
+ fmt.Fprintf(w, " Relay address: %v (default: %v)\n", p.RelayAddr, p.DefaultRelayAddr)
case p.RelayAddr != "" && p.DefaultRelayAddr == "":
- fmt.Printf(" Relay address: %v (no default)\n", p.RelayAddr)
+ fmt.Fprintf(w, " Relay address: %v (no default)\n", p.RelayAddr)
default:
- fmt.Printf(" Relay address: (none)\n")
+ fmt.Fprintf(w, " Relay address: (none)\n")
}
} else if relayAddr := cmp.Or(p.RelayAddr, p.DefaultRelayAddr); relayAddr != "" {
- fmt.Printf(" Relay address: %v\n", relayAddr)
+ fmt.Fprintf(w, " Relay address: %v\n", relayAddr)
}
- fmt.Printf(" Logged in as: %v\n", p.Username)
+ fmt.Fprintf(w, " Logged in as: %v\n", p.Username)
if len(p.ActiveRequests) != 0 {
- fmt.Printf(" Active requests: %v\n", strings.Join(p.ActiveRequests, ", "))
+ fmt.Fprintf(w, " Active requests: %v\n", strings.Join(p.ActiveRequests, ", "))
}
if cluster != "" {
- fmt.Printf(" Cluster: %v\n", cluster)
+ fmt.Fprintf(w, " Cluster: %v\n", cluster)
}
if p.Scope != "" {
- fmt.Printf(" Scope: %v\n", p.Scope)
- fmt.Printf(" Scoped Roles:\n")
+ fmt.Fprintf(w, " Scope: %v\n", p.Scope)
+ fmt.Fprintf(w, " Scoped Roles:\n")
assignedScopes := slices.Collect(maps.Keys(p.ScopedRoles))
slices.Sort(assignedScopes)
for _, scope := range assignedScopes {
- fmt.Printf(" %s: %v\n", scope, rolesToString(debug, p.ScopedRoles[scope]))
+ fmt.Fprintf(w, " %s: %v\n", scope, rolesToString(debug, p.ScopedRoles[scope]))
}
} else {
- fmt.Printf(" Roles: %v\n", rolesToString(debug, p.Roles))
+ fmt.Fprintf(w, " Roles: %v\n", rolesToString(debug, p.Roles))
}
if debug {
var count int
@@ -5392,60 +5407,60 @@ func printStatus(debug bool, p *profileInfo, env map[string]string, isActive boo
continue
}
if count == 0 {
- fmt.Printf(" Traits: %v: %v\n", k, v)
+ fmt.Fprintf(w, " Traits: %v: %v\n", k, v)
} else {
- fmt.Printf(" %v: %v\n", k, v)
+ fmt.Fprintf(w, " %v: %v\n", k, v)
}
count = count + 1
}
}
if len(p.Logins) > 0 {
- fmt.Printf(" Logins: %v\n", strings.Join(p.Logins, ", "))
+ fmt.Fprintf(w, " Logins: %v\n", strings.Join(p.Logins, ", "))
}
if p.KubernetesEnabled {
- fmt.Printf(" Kubernetes: enabled\n")
+ fmt.Fprintf(w, " Kubernetes: enabled\n")
if kubeCluster != "" {
- fmt.Printf(" Kubernetes cluster: %q\n", kubeCluster)
+ fmt.Fprintf(w, " Kubernetes cluster: %q\n", kubeCluster)
}
if len(p.KubernetesUsers) > 0 {
- fmt.Printf(" Kubernetes users: %v\n", strings.Join(p.KubernetesUsers, ", "))
+ fmt.Fprintf(w, " Kubernetes users: %v\n", strings.Join(p.KubernetesUsers, ", "))
}
if len(p.KubernetesGroups) > 0 {
- fmt.Printf(" Kubernetes groups: %v\n", strings.Join(p.KubernetesGroups, ", "))
+ fmt.Fprintf(w, " Kubernetes groups: %v\n", strings.Join(p.KubernetesGroups, ", "))
}
} else {
- fmt.Printf(" Kubernetes: disabled\n")
+ fmt.Fprintf(w, " Kubernetes: disabled\n")
}
if len(p.Databases) != 0 {
- fmt.Printf(" Databases: %v\n", strings.Join(p.Databases, ", "))
+ fmt.Fprintf(w, " Databases: %v\n", strings.Join(p.Databases, ", "))
}
if len(p.AllowedResourceIDs) > 0 {
allowedResourcesStr, err := types.ResourceIDsToString(p.AllowedResourceIDs)
if err != nil {
logger.WarnContext(context.Background(), "failed to marshal allowed resource IDs to string", "error", err)
} else {
- fmt.Printf(" Allowed Resources: %s\n", allowedResourcesStr)
+ fmt.Fprintf(w, " Allowed Resources: %s\n", allowedResourcesStr)
}
}
if p.GitHubIdentity != nil {
- fmt.Printf(" GitHub username: %s\n", p.GitHubIdentity.Username)
+ fmt.Fprintf(w, " GitHub username: %s\n", p.GitHubIdentity.Username)
}
- fmt.Printf(" Valid until: %v [%v]\n", p.ValidUntil, humanFriendlyValidUntilDuration(p.ValidUntil, clock))
- fmt.Printf(" Extensions: %v\n", strings.Join(p.Extensions, ", "))
+ fmt.Fprintf(w, " Valid until: %v [%v]\n", p.ValidUntil, humanFriendlyValidUntilDuration(p.ValidUntil, clock))
+ fmt.Fprintf(w, " Extensions: %v\n", strings.Join(p.Extensions, ", "))
if debug {
first := true
for k, v := range p.CriticalOptions {
if first {
- fmt.Printf(" Critical options: %v: %v\n", k, v)
+ fmt.Fprintf(w, " Critical options: %v: %v\n", k, v)
} else {
- fmt.Printf(" %v: %v\n", k, v)
+ fmt.Fprintf(w, " %v: %v\n", k, v)
}
first = false
}
}
- fmt.Printf("\n")
+ fmt.Fprintf(w, "\n")
}
func isOktaRole(role string) bool {
@@ -5497,12 +5512,12 @@ func printLoginInformation(cf *CLIConf, profile *client.ProfileStatus, profiles
// Print the active profile.
if profile != nil {
- printStatus(cf.Debug, active, env, true)
+ printStatus(cf.Stdout(), cf.Debug, active, env, true)
}
// Print all other profiles.
for _, p := range others {
- printStatus(cf.Debug, p, env, false)
+ printStatus(cf.Stdout(), cf.Debug, p, env, false)
}
// Print relevant active env vars, if they are set.
@@ -5533,25 +5548,16 @@ func onStatus(cf *CLIConf) error {
}
return trace.Wrap(err)
}
-
- // make the teleport client and retrieve the certificate from the proxy:
- tc, err := makeClient(cf)
- if err != nil {
- logger.WarnContext(cf.Context, "Failed to make client for retrieving cluster alerts", "error", err)
- return trace.Wrap(err)
+ if profile == nil && len(profiles) == 0 {
+ return trace.NotFound("Not logged in.")
}
- // `tsh status` should run without requiring user interaction.
- // To achieve this, we avoid remote calls that might prompt for
- // hardware key touch or require a PIN.
- hardwareKeyInteractionRequired := tc.PrivateKeyPolicy.MFAVerified()
-
- if err := printLoginInformation(cf, profile, profiles); err != nil {
+ if err = printLoginInformation(cf, profile, profiles); err != nil {
return trace.Wrap(err)
}
if profile == nil {
- return trace.NotFound("Not logged in.")
+ return trace.NotFound("No active profile.")
}
duration := time.Until(profile.ValidUntil)
@@ -5559,6 +5565,18 @@ func onStatus(cf *CLIConf) error {
return trace.NotFound("Active profile expired.")
}
+ // make the teleport client and retrieve the certificate from the proxy:
+ tc, err := makeClient(cf)
+ if err != nil {
+ logger.WarnContext(cf.Context, "Failed to make client for retrieving cluster alerts", "error", err)
+ return trace.Wrap(err)
+ }
+
+ // `tsh status` should run without requiring user interaction.
+ // To achieve this, we avoid remote calls that might prompt for
+ // hardware key touch or require a PIN.
+ hardwareKeyInteractionRequired := tc.PrivateKeyPolicy.MFAVerified()
+
if hardwareKeyInteractionRequired {
logger.DebugContext(cf.Context, "Skipping cluster alerts due to Hardware Key PIN/Touch requirement")
} else {
diff --git a/tool/tsh/common/tsh_test.go b/tool/tsh/common/tsh_test.go
index caf01c525656b..8af5acb130107 100644
--- a/tool/tsh/common/tsh_test.go
+++ b/tool/tsh/common/tsh_test.go
@@ -87,6 +87,7 @@ import (
"github.com/gravitational/teleport/lib/cryptosuites"
"github.com/gravitational/teleport/lib/cryptosuites/cryptosuitestest"
"github.com/gravitational/teleport/lib/defaults"
+ "github.com/gravitational/teleport/lib/fixtures"
"github.com/gravitational/teleport/lib/kube/kubeconfig"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/modules/modulestest"
@@ -6087,6 +6088,17 @@ func TestLogout(t *testing.T) {
require.NoError(t, os.Remove(privKeyPath))
},
},
+ {
+ name: "TLS cert is present but SSH cert is missing",
+ modifyKeyDir: func(t *testing.T, homePath string) {
+ tlsCertPath := keypaths.TLSCertPath(homePath, clientKeyRing.ProxyHost, clientKeyRing.Username)
+ err := os.WriteFile(tlsCertPath, []byte(fixtures.TLSCACertPEM), 0o600)
+ require.NoError(t, err)
+ sshCertPath := keypaths.SSHCertPath(homePath, clientKeyRing.ProxyHost, clientKeyRing.Username, clientKeyRing.ClusterName)
+ _, err = os.ReadFile(sshCertPath)
+ require.ErrorIs(t, err, os.ErrNotExist)
+ },
+ },
{
name: "TLS private key missing",
modifyKeyDir: func(t *testing.T, homePath string) {
@@ -6107,6 +6119,13 @@ func TestLogout(t *testing.T) {
require.NoError(t, err)
},
},
+ {
+ name: "current profile missing",
+ modifyKeyDir: func(t *testing.T, homePath string) {
+ currentProfileFilePath := keypaths.CurrentProfileFilePath(homePath)
+ require.NoError(t, os.Remove(currentProfileFilePath))
+ },
+ },
} {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
@@ -6663,6 +6682,33 @@ func testListingResources[T any](t *testing.T, pack listPack[T], unmarshalFunc f
}
}
+func TestStatusPrintsProfilesIfNoActiveProfile(t *testing.T) {
+ t.Parallel()
+
+ buf := bytes.NewBuffer([]byte{})
+
+ err := Run(context.Background(), []string{
+ "status",
+ }, setHomePath(t.TempDir()), func(c *CLIConf) error {
+ c.OverrideStdout = buf
+ profile := &profile.Profile{
+ WebProxyAddr: "proxy:3080",
+ Username: "testuser",
+ }
+ // setCurrent is false, so there is no active profile.
+ err := c.getClientStore().SaveProfile(profile, false)
+ require.NoError(t, err)
+ return nil
+ })
+
+ require.Contains(t, buf.String(),
+ ` Profile URL: https://proxy:3080
+ Logged in as: testuser
+ Cluster: proxy`)
+ require.True(t, trace.IsNotFound(err))
+ require.ErrorContains(t, err, "No active profile.")
+}
+
// TestProxyTemplates verifies proxy templates apply properly to client config.
func TestProxyTemplatesMakeClient(t *testing.T) {
t.Parallel()
diff --git a/web/packages/teleterm/src/main.ts b/web/packages/teleterm/src/main.ts
index d3df7a3a37206..8fc2912cc041b 100644
--- a/web/packages/teleterm/src/main.ts
+++ b/web/packages/teleterm/src/main.ts
@@ -16,10 +16,11 @@
* along with this program. If not, see .
*/
+import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
-import { app, dialog, nativeTheme, shell } from 'electron';
+import { app, dialog, nativeTheme } from 'electron';
import { CUSTOM_PROTOCOL } from 'shared/deepLinks';
import { ensureError } from 'shared/utils/error';
@@ -27,14 +28,15 @@ import { ensureError } from 'shared/utils/error';
import { parseDeepLink } from 'teleterm/deepLinks';
import Logger from 'teleterm/logger';
import MainProcess from 'teleterm/mainProcess';
+import { registerNavigationHandlers } from 'teleterm/mainProcess/navigationHandler';
import { enableWebHandlersProtection } from 'teleterm/mainProcess/protocolHandler';
-import { manageRootClusterProxyHostAllowList } from 'teleterm/mainProcess/rootClusterProxyHostAllowList';
import { getRuntimeSettings } from 'teleterm/mainProcess/runtimeSettings';
import { WindowsManager } from 'teleterm/mainProcess/windowsManager';
import { createConfigService } from 'teleterm/services/config';
-import { createFileStorage } from 'teleterm/services/fileStorage';
+import { createFileStorage, FileStorage } from 'teleterm/services/fileStorage';
import { createFileLoggerService, LoggerColor } from 'teleterm/services/logger';
import * as types from 'teleterm/types';
+import type { StatePersistenceState } from 'teleterm/ui/services/statePersistence';
import { assertUnreachable } from 'teleterm/ui/utils';
import { setTray } from './tray';
@@ -69,7 +71,15 @@ async function initializeApp(): Promise {
updateSessionDataPath();
const settings = await getRuntimeSettings();
const logger = initMainLogger(settings);
+
+ process.on('uncaughtException', (error, origin) => {
+ logger.error('Uncaught exception', origin, error);
+ showDialogWithError(`Uncaught exception (${origin} origin)`, error);
+ app.exit(1);
+ });
+
logger.info(`Starting ${app.getName()} version ${app.getVersion()}`);
+
const {
appStateFileStorage,
configFileStorage,
@@ -89,15 +99,43 @@ async function initializeApp(): Promise {
configService
);
- process.on('uncaughtException', (error, origin) => {
- logger.error('Uncaught exception', origin, error);
- showDialogWithError(`Uncaught exception (${origin} origin)`, error);
- app.exit(1);
+ // On Windows/Linux: Re-launching the app while it's already running
+ // triggers 'second-instance' (because of app.requestSingleInstanceLock()).
+ //
+ // On macOS: Re-launching the app (from places like Finder, Spotlight, or Dock)
+ // does not trigger 'second-instance'. Instead, the system emits 'activate'.
+ // However, launching the app outside the desktop manager (e.g., from the command
+ // line) does trigger 'second-instance'.
+ app.on('second-instance', () => {
+ windowsManager.focusWindow();
+ });
+ app.on('activate', () => {
+ windowsManager.focusWindow();
});
+ // Since setUpDeepLinks adds another listener for second-instance, it's important to call it after
+ // the listener which calls windowsManager.focusWindow. This way the focus will be brought to the
+ // window before processing the listener for deep links.
+ //
+ // This must be called as early as possible, before an async code.
+ // Otherwise, if the app is launched via a macOS deep link, the 'open-url' event may be emitted
+ // before a handler is registered, causing the link to be lost.
+ setUpDeepLinks(logger, windowsManager, settings);
+
+ const tshHome = configService.get('tshHome').value;
+ // Ensure the tsh directory exist.
+ await fs.mkdir(tshHome, {
+ recursive: true,
+ });
+
+ // TODO(gzdunek): DELETE IN 20.0.0. Users should already migrate to the new location.
+ // Also remove TshHomeMigrationBanner component, relevant properties from app_state.json,
+ // and address the TODO in teleport-connect.mdx > ##Troubleshooting.
+ await migrateOldTshHomeOnce(logger, tshHome, appStateFileStorage);
+
let mainProcess: MainProcess;
try {
- mainProcess = MainProcess.create({
+ mainProcess = new MainProcess({
settings,
logger,
configService,
@@ -129,40 +167,13 @@ async function initializeApp(): Promise {
app.exit();
});
- // On Windows/Linux: Re-launching the app while it's already running
- // triggers 'second-instance' (because of app.requestSingleInstanceLock()).
- //
- // On macOS: Re-launching the app (from places like Finder, Spotlight, or Dock)
- // does not trigger 'second-instance'. Instead, the system emits 'activate'.
- // However, launching the app outside the desktop manager (e.g., from the command
- // line) does trigger 'second-instance'.
- app.on('second-instance', () => {
- windowsManager.focusWindow();
- });
- app.on('activate', () => {
- windowsManager.focusWindow();
- });
-
- // Since setUpDeepLinks adds another listener for second-instance, it's important to call it after
- // the listener which calls windowsManager.focusWindow. This way the focus will be brought to the
- // window before processing the listener for deep links.
- setUpDeepLinks(logger, windowsManager, settings);
-
- const rootClusterProxyHostAllowList = new Set();
-
- (async () => {
- const { terminalService } = await mainProcess.getTshdClients();
-
- manageRootClusterProxyHostAllowList({
- tshdClient: terminalService,
- logger,
- allowList: rootClusterProxyHostAllowList,
- });
- })().catch(error => {
- const message = 'Could not initialize the tshd client in the main process';
- logger.error(message, error);
- showDialogWithError(message, error);
- app.exit(1);
+ app.on('web-contents-created', (_, webContents) => {
+ registerNavigationHandlers(
+ webContents,
+ settings,
+ mainProcess.clusterStore,
+ logger
+ );
});
app
@@ -182,71 +193,6 @@ async function initializeApp(): Promise {
showDialogWithError(message, error);
app.exit(1);
});
-
- // Limit navigation capabilities to reduce the attack surface.
- // See TEL-Q122-19 from "Teleport Core Testing Q1 2022" security audit.
- //
- // See also points 12, 13 and 14 from the Electron's security tutorial.
- // https://github.com/electron/electron/blob/v17.2.0/docs/tutorial/security.md#12-verify-webview-options-before-creation
- app.on('web-contents-created', (_, contents) => {
- contents.on('will-navigate', (event, navigationUrl) => {
- // Allow reloading the renderer app in dev mode.
- if (settings.dev && new URL(navigationUrl).host === 'localhost:8080') {
- return;
- }
- logger.warn(`Navigation to ${navigationUrl} blocked by 'will-navigate'`);
- event.preventDefault();
- });
-
- // The usage of webview is blocked by default, but let's include the handler just in case.
- // https://github.com/electron/electron/blob/v17.2.0/docs/api/webview-tag.md#enabling
- contents.on('will-attach-webview', (event, _, params) => {
- logger.warn(
- `Opening a webview to ${params.src} blocked by 'will-attach-webview'`
- );
- event.preventDefault();
- });
-
- contents.setWindowOpenHandler(details => {
- const url = new URL(details.url);
-
- function isUrlSafe(): boolean {
- if (url.protocol !== 'https:') {
- return false;
- }
- if (url.host === 'goteleport.com') {
- return true;
- }
- if (
- url.host === 'github.com' &&
- url.pathname.startsWith('/gravitational/')
- ) {
- return true;
- }
-
- // Allow opening links to the Web UIs of root clusters currently added in the app.
- if (rootClusterProxyHostAllowList.has(url.host)) {
- return true;
- }
- }
-
- // Open links to documentation and GitHub issues in the external browser.
- // They need to have `target` set to `_blank`.
- if (isUrlSafe()) {
- shell.openExternal(url.toString());
- } else {
- logger.warn(
- `Opening a new window to ${url} blocked by 'setWindowOpenHandler'`
- );
- dialog.showErrorBox(
- 'Cannot open this link',
- 'The domain does not match any of the allowed domains. Check main.log for more details.'
- );
- }
-
- return { action: 'deny' };
- });
- });
}
/**
@@ -410,3 +356,84 @@ function showDialogWithError(title: string, unknownError: unknown) {
const content = error.stack || error.message;
dialog.showErrorBox(title, content);
}
+
+/**
+ * Migrates the old "Teleport Connect/tsh" directory to the new location
+ * ("~/.tsh" by default) by copying all files recursively.
+ * Any failure in the migration process causes an early exit and marks it as processed.
+ * Retrying on the next launch could be harmful, since the user likely already
+ * re-added their profiles.
+ */
+async function migrateOldTshHomeOnce(
+ logger: Logger,
+ tshHome: string,
+ appStorage: FileStorage
+): Promise {
+ const oldTshHome = path.resolve(app.getPath('userData'), 'tsh');
+ const tshHomeMigrationKey = 'tshHomeMigration';
+ const tshMigration: TshHomeMigration =
+ appStorage.get()?.[tshHomeMigrationKey];
+ if (tshMigration?.processed) {
+ return;
+ }
+
+ const markMigrationAsProcessed = (opts?: { noOldTshHome?: boolean }) => {
+ const migrationProcessed: TshHomeMigration = { processed: true };
+ // The properties are separated, because `tshHomeMigration` should only
+ // be updated from the main process.
+ // The renderer can only update properties in the `state` key.
+ appStorage.put(tshHomeMigrationKey, migrationProcessed);
+ // Do not promote the shared tsh directory if there was nothing to migrate.
+ if (!opts?.noOldTshHome) {
+ // TODO(gzdunek): We need a better way to manage the app state.
+ const appState = (appStorage.get('state') || {}) as StatePersistenceState;
+ appState.showTshHomeMigrationBanner = true;
+ appStorage.put('state', appState);
+ }
+ };
+
+ // Check if the old directory exists.
+ try {
+ await fs.stat(oldTshHome);
+ } catch (err) {
+ if (err.code === 'ENOENT') {
+ logger.info(
+ 'Old tsh directory does not exist, marking migration as processed'
+ );
+ markMigrationAsProcessed({ noOldTshHome: true });
+ return;
+ }
+ logger.error('Failed to read old tsh directory', err);
+ markMigrationAsProcessed();
+ return;
+ }
+
+ // Perform the migration.
+ // The dereference option allows the source and the target to be symlinks.
+ //
+ // It may happen that the user already symlinked the global tsh home to the
+ // Electron's the home.
+ // In that case, the copy will fail with ERR_FS_CP_EINVAL error.
+ try {
+ await fs.cp(oldTshHome, tshHome, {
+ recursive: true,
+ force: true,
+ dereference: true,
+ });
+ logger.info(
+ `Successfully copied tsh home directory from ${oldTshHome} to ${tshHome}`
+ );
+ } catch (err) {
+ logger.error('Failed to copy tsh directory', err);
+ } finally {
+ markMigrationAsProcessed();
+ }
+}
+
+interface TshHomeMigration {
+ /**
+ * Indicates whether the old `tsh` directory has been migrated to the new location.
+ * `true` means the migration was attempted (successfully or not) and should not be retried.
+ */
+ processed?: boolean;
+}
diff --git a/web/packages/teleterm/src/mainProcess/awaitableSender/awaitableSender.test.ts b/web/packages/teleterm/src/mainProcess/awaitableSender/awaitableSender.test.ts
new file mode 100644
index 0000000000000..5c28bc621396e
--- /dev/null
+++ b/web/packages/teleterm/src/mainProcess/awaitableSender/awaitableSender.test.ts
@@ -0,0 +1,101 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { EventEmitter } from 'events';
+
+import Logger, { NullService } from 'teleterm/logger';
+
+import {
+ AwaitableSender,
+ MessageAcknowledgementError,
+} from './awaitableSender';
+
+beforeAll(() => {
+ Logger.init(new NullService());
+});
+
+test('starts port and add event listeners in constructor', () => {
+ const port = new MockMessagePortMain();
+ new AwaitableSender(port);
+ expect(port.listeners('message')).toHaveLength(1);
+ expect(port.listeners('close')).toHaveLength(1);
+});
+
+test('send posts message and returns a promise that resolves on ack', async () => {
+ const port = new MockMessagePortMain();
+ const sender = new AwaitableSender(port);
+
+ const payload = { foo: 'bar' };
+ const promise = sender.send(payload);
+
+ expect(port.postMessage).toHaveBeenCalledWith({
+ type: 'data',
+ id: expect.any(String),
+ payload,
+ });
+
+ // The other side responds: first emit ack for other id and then an empty message.
+ port.emitMessage({ id: 'wrong-id', type: 'ack' });
+ port.emitMessage(undefined);
+ // Wait a short time and make sure the promise hasn't resolved.
+ const result = await Promise.race([
+ promise.then(() => 'resolved'),
+ new Promise(resolve => setTimeout(() => resolve('pending'), 50)),
+ ]);
+
+ expect(result).toBe('pending');
+
+ // The other side responds again: now emit ack for our send request.
+ const correctId = port.postMessage.mock.calls[0][0].id;
+ port.emitMessage({ id: correctId, type: 'ack' });
+
+ await expect(promise).resolves.toBeUndefined();
+});
+
+test('dispose removes listeners, resolves pending messages, clears map, and resolves disposeSignal', async () => {
+ const port = new MockMessagePortMain();
+ const sender = new AwaitableSender(port);
+ let sendPromise = sender.send(undefined);
+
+ port.close();
+ expect(port.listeners('message')).toHaveLength(0);
+ expect(port.listeners('close')).toHaveLength(0);
+
+ const disposedPromise = sender.whenDisposed();
+
+ // The pending send promise should resolve after dispose
+ await expect(sendPromise).rejects.toThrow(MessageAcknowledgementError);
+ await expect(disposedPromise).resolves.toBeUndefined();
+});
+
+class MockMessagePortMain extends EventEmitter {
+ public postMessage = jest.fn();
+ public start = jest.fn();
+
+ constructor() {
+ super();
+ }
+
+ emitMessage(data: unknown): void {
+ this.emit('message', { data });
+ }
+
+ close(): void {
+ this.emit('close');
+ }
+}
diff --git a/web/packages/teleterm/src/mainProcess/awaitableSender/awaitableSender.ts b/web/packages/teleterm/src/mainProcess/awaitableSender/awaitableSender.ts
new file mode 100644
index 0000000000000..089534fd162f3
--- /dev/null
+++ b/web/packages/teleterm/src/mainProcess/awaitableSender/awaitableSender.ts
@@ -0,0 +1,160 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { MessageEvent, MessagePortMain } from 'electron';
+
+export type Message = MessageData | MessageAck;
+
+export interface MessageData {
+ id: string;
+ type: 'data';
+ payload: unknown;
+}
+
+export interface MessageAck {
+ id: string;
+ type: 'ack';
+ /**
+ * Optional error.
+ * Present when the renderer received the message, but failed to process it.
+ */
+ error?: unknown;
+}
+
+function isMessageAck(v: unknown): v is MessageAck {
+ return typeof v === 'object' && 'type' in v && v.type === 'ack';
+}
+
+export interface IAwaitableSender {
+ send(payload: T, options?: { signal?: AbortSignal }): Promise;
+ whenDisposed(): Promise;
+}
+
+/**
+ * Enables sending messages from the main process to the renderer
+ * and awaiting delivery confirmation.
+ *
+ * Unlike the standard `webContents.send()` API, which is push-based,
+ * `AwaitableSender` is pull-based — the renderer must explicitly subscribe
+ * to receive messages.
+ */
+export class AwaitableSender implements IAwaitableSender {
+ private messages = new Map<
+ string,
+ { resolve(): void; reject(reason: unknown): void }
+ >();
+ private disposeSignal = Promise.withResolvers();
+
+ constructor(private port: MessagePortMain) {
+ this.port.start();
+ this.port.on('message', this.processMessage);
+ this.port.on('close', this.dispose);
+ }
+
+ /**
+ * Sends a message and awaits delivery confirmation from the receiver.
+ *
+ * This method returns a promise that resolves once the other side
+ * acknowledges receiving and processing the message.
+ * If the acknowledgment is not received within the specified timeout
+ * (default 10 seconds), the promise rejects with a `MessageAcknowledgementError`.
+ *
+ * If the renderer received the message, but failed to process it, the promise
+ * is also rejected.
+ */
+ send(
+ payload: T,
+ { signal = AbortSignal.timeout(10_000) }: { signal?: AbortSignal } = {}
+ ): Promise {
+ const id = crypto.randomUUID();
+
+ return new Promise((resolve, reject) => {
+ const cleanup = () => {
+ this.messages.delete(id);
+ signal.removeEventListener('abort', abort);
+ };
+
+ const abort = () => {
+ cleanup();
+ reject(new MessageAcknowledgementError(signal.reason));
+ };
+
+ if (signal.aborted) {
+ return abort();
+ }
+
+ signal.addEventListener('abort', abort, { once: true });
+
+ this.messages.set(id, {
+ resolve: () => {
+ cleanup();
+ resolve();
+ },
+ reject: reason => {
+ cleanup();
+ reject(reason);
+ },
+ });
+
+ const message: MessageData = { type: 'data', id, payload };
+ this.port.postMessage(message);
+ });
+ }
+
+ /** Returns a promise that resolves when the sender is disposed. */
+ whenDisposed(): Promise {
+ return this.disposeSignal.promise;
+ }
+
+ private processMessage = (event: MessageEvent): void => {
+ const message = event.data;
+ // Only to satisfy TypeScript.
+ // We don't expect non-ack messages to be received on this port.
+ if (!isMessageAck(message)) {
+ return;
+ }
+ const item = this.messages.get(message.id);
+ if (!item) {
+ return;
+ }
+ if (message.error) {
+ item.reject(message.error);
+ return;
+ }
+ item.resolve();
+ };
+
+ private dispose = (): void => {
+ this.port.off('message', this.processMessage);
+ this.port.off('close', this.dispose);
+
+ for (const { reject } of this.messages.values()) {
+ reject(new MessageAcknowledgementError(new Error('Sender was disposed')));
+ }
+ this.disposeSignal.resolve();
+ };
+}
+
+/** Error thrown when waiting for message acknowledgement confirmation was abandoned. */
+export class MessageAcknowledgementError extends Error {
+ constructor(cause?: unknown) {
+ super('Failed to receive message acknowledgement from the renderer', {
+ cause,
+ });
+ }
+}
diff --git a/web/packages/teleterm/src/mainProcess/awaitableSender/index.ts b/web/packages/teleterm/src/mainProcess/awaitableSender/index.ts
new file mode 100644
index 0000000000000..53610a590dc63
--- /dev/null
+++ b/web/packages/teleterm/src/mainProcess/awaitableSender/index.ts
@@ -0,0 +1,19 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+export * from './awaitableSender';
diff --git a/web/packages/teleterm/src/mainProcess/clusterLifecycleManager/clusterLifecycleManager.test.ts b/web/packages/teleterm/src/mainProcess/clusterLifecycleManager/clusterLifecycleManager.test.ts
new file mode 100644
index 0000000000000..fe6036c2a03f6
--- /dev/null
+++ b/web/packages/teleterm/src/mainProcess/clusterLifecycleManager/clusterLifecycleManager.test.ts
@@ -0,0 +1,352 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { enablePatches } from 'immer';
+
+import Logger, { NullService } from 'teleterm/logger';
+import { IAwaitableSender } from 'teleterm/mainProcess/awaitableSender';
+import { ClusterStore } from 'teleterm/mainProcess/clusterStore';
+import { ProfileChangeSet } from 'teleterm/mainProcess/profileWatcher';
+import { RendererIpc } from 'teleterm/mainProcess/types';
+import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient';
+import { MockTshClient } from 'teleterm/services/tshd/fixtures/mocks';
+import {
+ makeLoggedInUser,
+ makeRootCluster,
+} from 'teleterm/services/tshd/testHelpers';
+
+import {
+ ClusterLifecycleEvent,
+ ClusterLifecycleManager,
+} from './clusterLifecycleManager';
+
+beforeAll(() => {
+ Logger.init(new NullService());
+});
+
+enablePatches();
+
+const cluster = makeRootCluster();
+
+const tests: {
+ name: string;
+ setup(opts: { tshdClient: MockTshClient }): Promise<{
+ profileWatcher: () => AsyncGenerator;
+ throwInRendererHandler?: boolean;
+ }>;
+ expect(opts: {
+ clusterStore: ClusterStore;
+ tshdClient: MockTshClient;
+ rendererHandler: IAwaitableSender;
+ globalErrorHandler: jest.Mock;
+ }): void;
+}[] = [
+ {
+ name: 'when cluster is added, it updates state and notifies renderer',
+ setup: async () => {
+ return {
+ profileWatcher: makeWatcher([{ op: 'added', cluster }]),
+ };
+ },
+ expect: ({ clusterStore, rendererHandler }) => {
+ expect(clusterStore.getState().get(cluster.uri)).toBeDefined();
+ expect(rendererHandler.send).toHaveBeenCalledWith({
+ op: 'did-add-cluster',
+ uri: cluster.uri,
+ });
+ },
+ },
+ {
+ name: 'when cluster is added and renderer fails, it updates state and reports an error',
+ setup: async () => {
+ return {
+ throwInRendererHandler: true,
+ profileWatcher: makeWatcher([{ op: 'added', cluster }]),
+ };
+ },
+ expect: ({ clusterStore, globalErrorHandler }) => {
+ expect(clusterStore.getState().get(cluster.uri)).toBeDefined();
+ expect(globalErrorHandler).toHaveBeenCalledWith(
+ RendererIpc.ProfileWatcherError,
+ {
+ error: new Error('Error in renderer'),
+ reason: 'processing-error',
+ }
+ );
+ },
+ },
+ {
+ name: 'when cluster is removed, it updates state and notifies renderer',
+ setup: async ({ tshdClient }) => {
+ jest.spyOn(tshdClient, 'logout');
+ jest
+ .spyOn(tshdClient, 'listRootClusters')
+ .mockResolvedValue(new MockedUnaryCall({ clusters: [cluster] }));
+ return {
+ profileWatcher: makeWatcher([{ op: 'removed', cluster }]),
+ };
+ },
+ expect: ({ clusterStore, tshdClient, rendererHandler }) => {
+ expect(clusterStore.getState().get(cluster.uri)).toBeUndefined();
+ expect(tshdClient.logout).toHaveBeenCalledWith({
+ clusterUri: cluster.uri,
+ removeProfile: true,
+ });
+ expect(rendererHandler.send).toHaveBeenCalledWith({
+ op: 'will-logout-and-remove',
+ uri: cluster.uri,
+ });
+ },
+ },
+ {
+ name: 'when cluster is removed and renderer fails, it keeps the cluster and reports an error',
+ setup: async ({ tshdClient }) => {
+ jest.spyOn(tshdClient, 'logout');
+ jest
+ .spyOn(tshdClient, 'listRootClusters')
+ .mockResolvedValue(new MockedUnaryCall({ clusters: [cluster] }));
+ return {
+ throwInRendererHandler: true,
+ profileWatcher: makeWatcher([{ op: 'removed', cluster }]),
+ };
+ },
+ expect: ({ clusterStore, tshdClient, globalErrorHandler }) => {
+ expect(clusterStore.getState().get(cluster.uri)).toBeDefined();
+ expect(tshdClient.logout).not.toHaveBeenCalled();
+ expect(globalErrorHandler).toHaveBeenCalledWith(
+ RendererIpc.ProfileWatcherError,
+ {
+ error: new Error('Error in renderer'),
+ reason: 'processing-error',
+ }
+ );
+ },
+ },
+ {
+ name: 'when cluster becomes logged-out, it updates state and notifies renderer',
+ setup: async ({ tshdClient }) => {
+ const next = makeRootCluster({
+ connected: false,
+ loggedInUser: makeLoggedInUser({ name: '' }),
+ });
+ jest.spyOn(tshdClient, 'logout');
+ jest
+ .spyOn(tshdClient, 'listRootClusters')
+ .mockResolvedValue(new MockedUnaryCall({ clusters: [cluster] }));
+ return {
+ profileWatcher: makeWatcher([
+ { op: 'changed', next, previous: cluster },
+ ]),
+ };
+ },
+ expect: ({ clusterStore, tshdClient, rendererHandler }) => {
+ expect(clusterStore.getState().get(cluster.uri).loggedInUser.name).toBe(
+ ''
+ );
+ expect(tshdClient.logout).toHaveBeenCalledWith({
+ clusterUri: cluster.uri,
+ removeProfile: false,
+ });
+ expect(rendererHandler.send).toHaveBeenCalledWith({
+ op: 'will-logout',
+ uri: cluster.uri,
+ });
+ },
+ },
+ {
+ name: 'when cluster becomes logged-out and renderer fails, it keeps logged-in state and reports an error',
+ setup: async ({ tshdClient }) => {
+ const next = makeRootCluster({
+ connected: false,
+ loggedInUser: makeLoggedInUser({ name: '' }),
+ });
+ jest.spyOn(tshdClient, 'logout');
+ jest
+ .spyOn(tshdClient, 'listRootClusters')
+ .mockResolvedValue(new MockedUnaryCall({ clusters: [cluster] }));
+ return {
+ throwInRendererHandler: true,
+ profileWatcher: makeWatcher([
+ { op: 'changed', next, previous: cluster },
+ ]),
+ };
+ },
+ expect: ({ clusterStore, tshdClient, globalErrorHandler }) => {
+ expect(clusterStore.getState().get(cluster.uri).loggedInUser.name).toBe(
+ cluster.loggedInUser.name
+ );
+ expect(tshdClient.logout).not.toHaveBeenCalled();
+ expect(globalErrorHandler).toHaveBeenCalledWith(
+ RendererIpc.ProfileWatcherError,
+ {
+ error: new Error('Error in renderer'),
+ reason: 'processing-error',
+ }
+ );
+ },
+ },
+ {
+ name: 'when cluster changes, it updates state and clears stale clients',
+ setup: async ({ tshdClient }) => {
+ const next = makeRootCluster({
+ connected: false,
+ });
+ jest
+ .spyOn(tshdClient, 'listRootClusters')
+ .mockResolvedValue(new MockedUnaryCall({ clusters: [cluster] }));
+ jest.spyOn(tshdClient, 'clearStaleClusterClients');
+ return {
+ profileWatcher: makeWatcher([
+ { op: 'changed', next, previous: cluster },
+ ]),
+ };
+ },
+ expect: ({ clusterStore, tshdClient, rendererHandler }) => {
+ expect(clusterStore.getState().get(cluster.uri).connected).toBe(false);
+ expect(tshdClient.clearStaleClusterClients).toHaveBeenCalledWith({
+ rootClusterUri: cluster.uri,
+ });
+ expect(rendererHandler.send).not.toHaveBeenCalled();
+ },
+ },
+ {
+ name: 'when access of logged in user changes, it updates state, clears stale clients, and notifies renderer',
+ setup: async ({ tshdClient }) => {
+ const next = makeRootCluster({
+ loggedInUser: makeLoggedInUser({ activeRequests: ['abcd'] }),
+ });
+ jest
+ .spyOn(tshdClient, 'listRootClusters')
+ .mockResolvedValue(new MockedUnaryCall({ clusters: [cluster] }));
+ jest.spyOn(tshdClient, 'clearStaleClusterClients');
+ return {
+ profileWatcher: makeWatcher([
+ { op: 'changed', next, previous: cluster },
+ ]),
+ };
+ },
+ expect: ({ clusterStore, tshdClient, rendererHandler }) => {
+ expect(
+ clusterStore.getState().get(cluster.uri).loggedInUser.activeRequests
+ ).toEqual(['abcd']);
+ expect(tshdClient.clearStaleClusterClients).toHaveBeenCalledWith({
+ rootClusterUri: cluster.uri,
+ });
+ expect(rendererHandler.send).toHaveBeenCalledWith({
+ op: 'did-change-access',
+ uri: cluster.uri,
+ });
+ },
+ },
+ {
+ name: 'when access of logged in user changes and renderer fails, it updates state, clears stale clients and reports error',
+ setup: async ({ tshdClient }) => {
+ const next = makeRootCluster({
+ loggedInUser: makeLoggedInUser({ activeRequests: ['abcd'] }),
+ });
+ jest
+ .spyOn(tshdClient, 'listRootClusters')
+ .mockResolvedValue(new MockedUnaryCall({ clusters: [cluster] }));
+ jest.spyOn(tshdClient, 'clearStaleClusterClients');
+ return {
+ throwInRendererHandler: true,
+ profileWatcher: makeWatcher([
+ { op: 'changed', next, previous: cluster },
+ ]),
+ };
+ },
+ expect: ({ clusterStore, tshdClient, globalErrorHandler }) => {
+ expect(
+ clusterStore.getState().get(cluster.uri).loggedInUser.activeRequests
+ ).toEqual(['abcd']);
+ expect(tshdClient.clearStaleClusterClients).toHaveBeenCalledWith({
+ rootClusterUri: cluster.uri,
+ });
+ expect(globalErrorHandler).toHaveBeenCalledWith(
+ RendererIpc.ProfileWatcherError,
+ {
+ error: new Error('Error in renderer'),
+ reason: 'processing-error',
+ }
+ );
+ },
+ },
+];
+
+// eslint-disable-next-line jest/expect-expect
+test.each(tests)('$name', async ({ setup, expect: testExpect }) => {
+ const mockTshdClient = new MockTshClient();
+ const mockAppUpdater = {
+ maybeRemoveManagingCluster: jest.fn().mockResolvedValue(undefined),
+ };
+ const clusterStore = new ClusterStore(async () => mockTshdClient, {
+ crashWindow: async () => {},
+ });
+ const globalErrorHandler = jest.fn();
+ const windowsManager = {
+ getWindow: () => ({
+ webContents: {
+ send: globalErrorHandler,
+ },
+ }),
+ };
+ const { profileWatcher, throwInRendererHandler } = await setup({
+ tshdClient: mockTshdClient,
+ });
+
+ const done = Promise.withResolvers();
+ const consumer = (async function* () {
+ try {
+ for await (const value of profileWatcher()) {
+ yield value;
+ }
+ } finally {
+ done.resolve();
+ }
+ })();
+
+ const manager = new ClusterLifecycleManager(
+ clusterStore,
+ async () => mockTshdClient,
+ mockAppUpdater,
+ windowsManager,
+ consumer
+ );
+ const mockRendererHandler = {
+ send: throwInRendererHandler
+ ? jest.fn().mockRejectedValue(new Error('Error in renderer'))
+ : jest.fn().mockResolvedValue(undefined),
+ whenDisposed: () => new Promise(() => {}),
+ };
+ manager.setRendererEventHandler(mockRendererHandler);
+ await manager.syncRootClustersAndStartProfileWatcher();
+ await done.promise;
+
+ testExpect({
+ globalErrorHandler,
+ clusterStore,
+ tshdClient: mockTshdClient,
+ rendererHandler: mockRendererHandler,
+ });
+});
+
+function makeWatcher(...events: ProfileChangeSet[]) {
+ return async function* () {
+ yield* events;
+ };
+}
diff --git a/web/packages/teleterm/src/mainProcess/clusterLifecycleManager/clusterLifecycleManager.ts b/web/packages/teleterm/src/mainProcess/clusterLifecycleManager/clusterLifecycleManager.ts
new file mode 100644
index 0000000000000..2301dc5b96d53
--- /dev/null
+++ b/web/packages/teleterm/src/mainProcess/clusterLifecycleManager/clusterLifecycleManager.ts
@@ -0,0 +1,316 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import {
+ Cluster,
+ LoggedInUser,
+} from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
+
+import Logger from 'teleterm/logger';
+import type { IAwaitableSender } from 'teleterm/mainProcess/awaitableSender';
+import { RendererIpc } from 'teleterm/mainProcess/types';
+import { AppUpdater } from 'teleterm/services/appUpdater';
+import { isTshdRpcError, TshdClient } from 'teleterm/services/tshd';
+import { mergeClusterProfileWithDetails } from 'teleterm/services/tshd/cluster';
+import { RootClusterUri } from 'teleterm/ui/uri';
+
+import { ClusterStore } from '../clusterStore';
+import { ProfileChangeSet } from '../profileWatcher';
+
+/** Describes a lifecycle event related to a cluster. */
+export interface ClusterLifecycleEvent {
+ uri: RootClusterUri;
+ /**
+ * The lifecycle operation type.
+ *
+ * Operations prefixed with `will-` occur before the corresponding action
+ * in the main process and can interrupt it when they return an error.
+ *
+ * Operations prefixed with `did-` occur after the action has already happened
+ * in the main process, so they cannot prevent it.
+ *
+ * Operation meanings:
+ * * did-add-cluster - A cluster has been successfully added.
+ * * did-change-access - The logged-in user has changed, or their roles,
+ * or access requests have been updated.
+ * * will-logout - The user is about to be logged out.
+ * * will-logout-and-remove - The user is about to be logged out
+ * and their profile (cluster) will be removed.
+ */
+ op:
+ | 'did-add-cluster'
+ | 'did-change-access'
+ | 'will-logout'
+ | 'will-logout-and-remove';
+}
+
+export interface ProfileWatcherError {
+ error: unknown;
+ reason: 'processing-error' | 'exited';
+}
+
+interface WindowsManager {
+ getWindow(): {
+ webContents: { send(channel: string, ...args: any[]): void };
+ };
+}
+
+/**
+ * Manages the lifecycle of clusters by handling both UI actions that update them
+ * (e.g., adding a cluster, logging out) and profile watcher events.
+ *
+ * When handling an action or event requires additional work on the renderer side
+ * (for example, cleaning up before logging out), a handler registered
+ * in `rendererEventHandler` is invoked.
+ * Then, the cluster store is updated (the exact order of these steps depends
+ * on the specific case).
+ *
+ * It is important to always call a method of `ClusterLifecycleManager` rather
+ * than interacting directly with `ClusterStore` whenever the action involves
+ * additional work on the renderer side.
+ */
+export class ClusterLifecycleManager {
+ private readonly logger = new Logger('ClusterLifecycleManager');
+ private rendererEventHandler:
+ | IAwaitableSender
+ | undefined;
+ private watcherStarted = false;
+
+ constructor(
+ private readonly clusterStore: ClusterStore,
+ private readonly getTshdClient: () => Promise,
+ private readonly appUpdater: Pick,
+ private readonly windowsManager: WindowsManager,
+ private readonly profileWatcher: AsyncIterable
+ ) {}
+
+ setRendererEventHandler(
+ handler: IAwaitableSender
+ ): void {
+ if (this.rendererEventHandler) {
+ this.logger.error(
+ 'Only one renderer lifecycle event handler can be registered at a time'
+ );
+ return;
+ }
+
+ this.logger.info('Renderer lifecycle event handler registered');
+ this.rendererEventHandler = handler;
+ this.rendererEventHandler.whenDisposed().then(() => {
+ this.logger.info('Renderer lifecycle event handler unregistered');
+ this.rendererEventHandler = undefined;
+ });
+ }
+
+ async addCluster(proxyAddress: string): Promise {
+ const cluster = await this.clusterStore.add(proxyAddress);
+ await this.rendererEventHandler.send({
+ op: 'did-add-cluster',
+ uri: cluster.uri,
+ });
+ return cluster;
+ }
+
+ async logoutAndRemoveCluster(uri: RootClusterUri): Promise {
+ await this.rendererEventHandler.send({ op: 'will-logout-and-remove', uri });
+ this.onBeforeRemove(uri);
+ await this.clusterStore.logoutAndRemove(uri);
+ }
+
+ async syncCluster(uri: RootClusterUri): Promise {
+ const { previous, next } = await this.clusterStore.sync(uri);
+ if (!hasAccessChanged(previous.loggedInUser, next.loggedInUser)) {
+ return;
+ }
+
+ await this.rendererEventHandler.send({
+ op: 'did-change-access',
+ uri: next.uri,
+ });
+ }
+
+ async syncRootClustersAndStartProfileWatcher(): Promise {
+ await this.clusterStore.syncRootClusters();
+ if (!this.watcherStarted) {
+ this.watcherStarted = true;
+ void this.watchProfileChanges();
+ }
+ }
+
+ private onBeforeRemove(uri: RootClusterUri): void {
+ // Do not wait for this promise to finish as we don't want to block logout
+ // on checking app updates.
+ this.appUpdater.maybeRemoveManagingCluster(uri).catch(error => {
+ this.logger.error('Failed to remove managing cluster', error);
+ });
+ }
+
+ /**
+ * If the cluster is connected, try to sync it to get the full profile with details.
+ * Otherwise, update the cluster with the profile read from disk.
+ */
+ private async syncOrUpdateCluster(cluster: Cluster): Promise {
+ if (cluster.connected) {
+ try {
+ await this.clusterStore.sync(cluster.uri);
+ } catch (e) {
+ // Theoretically, the cert could just expire and result in an error
+ // resolvable with relogin when trying to sync the cluster.
+ // In that case, only update the store.
+ if (!(isTshdRpcError(e) && e.isResolvableWithRelogin)) {
+ throw e;
+ }
+ }
+ }
+ const existing = this.clusterStore.getState().get(cluster.uri);
+ await this.clusterStore.set(
+ mergeClusterProfileWithDetails({
+ profile: cluster,
+ details: existing || Cluster.create(),
+ })
+ );
+ }
+
+ /**
+ * Watches for changes in the `tsh` directory.
+ *
+ * Some file system events require notifying the renderer (e.g., to
+ * remove a workspace before a cluster store update is sent).
+ */
+ private async watchProfileChanges(): Promise {
+ try {
+ for await (const changes of this.profileWatcher) {
+ this.logger.info('Detected profile changes', changes);
+
+ for (const change of changes) {
+ try {
+ switch (change.op) {
+ case 'added':
+ await this.handleClusterAdded(change.cluster);
+ break;
+ case 'changed':
+ await this.handleClusterChanged(change.previous, change.next);
+ break;
+ case 'removed':
+ await this.handleClusterRemoved(change.cluster);
+ break;
+ }
+ } catch (error) {
+ this.logger.error('Error while processing cluster event', error);
+ this.handleWatcherError({ error, reason: 'processing-error' });
+ }
+ }
+ }
+ } catch (error) {
+ this.logger.error('Profile watcher exited with error', error);
+ this.handleWatcherError({ error, reason: 'exited' });
+ }
+ }
+
+ private async handleClusterAdded(cluster: Cluster): Promise {
+ await this.syncOrUpdateCluster(cluster);
+ await this.rendererEventHandler.send({
+ op: 'did-add-cluster',
+ uri: cluster.uri,
+ });
+ }
+
+ private async handleClusterChanged(
+ previous: Cluster,
+ next: Cluster
+ ): Promise {
+ const wasLoggedIn = previous.loggedInUser?.name;
+ const isLoggedIn = next.loggedInUser?.name;
+ const hasLoggedOut = wasLoggedIn && !isLoggedIn;
+
+ if (hasLoggedOut) {
+ await this.handleClusterLogout(next);
+ return;
+ }
+
+ const client = await this.getTshdClient();
+ // Only clear clients with outdated certificates.
+ // The watcher 'changed' event may be emitted right after the user logs in
+ // or assumes a role via Connect (which already closes all clients
+ // for the profile), so we avoid closing them again if they're already up to date.
+ await client.clearStaleClusterClients({ rootClusterUri: next.uri });
+ await this.syncOrUpdateCluster(next);
+
+ if (!hasAccessChanged(previous.loggedInUser, next.loggedInUser)) {
+ return;
+ }
+ await this.rendererEventHandler.send({
+ op: 'did-change-access',
+ uri: next.uri,
+ });
+ }
+
+ private async handleClusterRemoved(cluster: Cluster): Promise {
+ await this.rendererEventHandler.send({
+ op: 'will-logout-and-remove',
+ uri: cluster.uri,
+ });
+ this.onBeforeRemove(cluster.uri);
+ await this.clusterStore.logoutAndRemove(cluster.uri);
+ }
+
+ private async handleClusterLogout(cluster: Cluster): Promise {
+ await this.rendererEventHandler.send({
+ op: 'will-logout',
+ uri: cluster.uri,
+ });
+ const client = await this.getTshdClient();
+ await client.logout({ clusterUri: cluster.uri, removeProfile: false });
+ await this.syncOrUpdateCluster(cluster);
+ }
+
+ private handleWatcherError(error: ProfileWatcherError): void {
+ this.windowsManager
+ .getWindow()
+ .webContents.send(RendererIpc.ProfileWatcherError, error);
+ }
+}
+
+/**
+ * Checks if the username, roles or active requests changed.
+ * If yes, then probably the user has access to different resources.
+ */
+function hasAccessChanged(
+ previousUser: LoggedInUser | undefined,
+ nextUser: LoggedInUser | undefined
+): boolean {
+ // No user, we don't know if access changed.
+ if (!(previousUser?.name && nextUser?.name)) {
+ return false;
+ }
+
+ const hasChangedUsername = previousUser.name !== nextUser.name;
+ const hasChangedRoles = !areArraysEqual(previousUser.roles, nextUser.roles);
+ const hasChangedActiveRequests = !areArraysEqual(
+ previousUser.activeRequests,
+ nextUser.activeRequests
+ );
+
+ return hasChangedUsername || hasChangedRoles || hasChangedActiveRequests;
+}
+
+function areArraysEqual(a: string[], b: string[]): boolean {
+ const aSet = new Set(a);
+ const bSet = new Set(b);
+ return aSet.size === bSet.size && aSet.isSubsetOf(bSet);
+}
diff --git a/web/packages/teleterm/src/mainProcess/clusterLifecycleManager/index.ts b/web/packages/teleterm/src/mainProcess/clusterLifecycleManager/index.ts
new file mode 100644
index 0000000000000..a1e42e6988ce1
--- /dev/null
+++ b/web/packages/teleterm/src/mainProcess/clusterLifecycleManager/index.ts
@@ -0,0 +1,19 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+export * from './clusterLifecycleManager';
diff --git a/web/packages/teleterm/src/mainProcess/clusterStore/clusterStore.test.ts b/web/packages/teleterm/src/mainProcess/clusterStore/clusterStore.test.ts
new file mode 100644
index 0000000000000..642a3dccf2ba8
--- /dev/null
+++ b/web/packages/teleterm/src/mainProcess/clusterStore/clusterStore.test.ts
@@ -0,0 +1,118 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { enableMapSet, enablePatches } from 'immer';
+
+import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient';
+import { MockTshClient } from 'teleterm/services/tshd/fixtures/mocks';
+import {
+ makeLeafCluster,
+ makeRootCluster,
+} from 'teleterm/services/tshd/testHelpers';
+
+import { ClusterStore } from './clusterStore';
+
+enablePatches();
+enableMapSet();
+
+const cluster = makeRootCluster({ connected: false });
+const clusterWithDetails = makeRootCluster({
+ ...cluster,
+ connected: true,
+ features: {
+ advancedAccessWorkflows: true,
+ isUsageBasedBilling: true,
+ },
+});
+const leafCluster = makeLeafCluster();
+const mockWindowsManager = {
+ crashWindow: async () => undefined,
+};
+
+test('adds cluster', async () => {
+ const mockClient = new MockTshClient();
+ mockClient.addCluster = () => new MockedUnaryCall(cluster);
+ const clusterStore = new ClusterStore(
+ () => Promise.resolve(mockClient),
+ mockWindowsManager
+ );
+
+ await clusterStore.add(cluster.uri);
+
+ const state = clusterStore.getState();
+ expect(state.get(cluster.uri)).toEqual(cluster);
+});
+
+test('adding a cluster does not overwrite an existing one', async () => {
+ const mockClient = new MockTshClient();
+ mockClient.addCluster = () => new MockedUnaryCall(cluster);
+ mockClient.getCluster = () => new MockedUnaryCall(clusterWithDetails);
+ const clusterStore = new ClusterStore(
+ () => Promise.resolve(mockClient),
+ mockWindowsManager
+ );
+
+ await clusterStore.sync(cluster.uri);
+ // addCluster call returns fewer details than getCluster,
+ // so clusterStore.add shouldn't overwrite details already acquired
+ // by clusterStore.sync.
+ await clusterStore.add(cluster.uri);
+
+ const state = clusterStore.getState();
+ expect(state.get(cluster.uri)).toEqual(clusterWithDetails);
+});
+
+test('syncs cluster', async () => {
+ const mockClient = new MockTshClient();
+ mockClient.getCluster = () => new MockedUnaryCall(clusterWithDetails);
+ mockClient.listLeafClusters = () =>
+ new MockedUnaryCall({ clusters: [leafCluster] });
+ const clusterStore = new ClusterStore(
+ () => Promise.resolve(mockClient),
+ mockWindowsManager
+ );
+
+ await clusterStore.sync(cluster.uri);
+
+ const state = clusterStore.getState();
+ expect(state.get(clusterWithDetails.uri)).toStrictEqual(clusterWithDetails);
+ expect(state.get(leafCluster.uri)).toStrictEqual(leafCluster);
+});
+
+test('logs out of cluster', async () => {
+ const mockClient = new MockTshClient();
+ mockClient.getCluster = () => new MockedUnaryCall(clusterWithDetails);
+ mockClient.listLeafClusters = () =>
+ new MockedUnaryCall({ clusters: [leafCluster] });
+ const logoutMock = jest.spyOn(mockClient, 'logout');
+ const clusterStore = new ClusterStore(
+ () => Promise.resolve(mockClient),
+ mockWindowsManager
+ );
+ await clusterStore.sync(cluster.uri);
+
+ await clusterStore.logoutAndRemove(cluster.uri);
+
+ expect(logoutMock).toHaveBeenCalledWith({
+ clusterUri: cluster.uri,
+ removeProfile: true,
+ });
+ const state = clusterStore.getState();
+ expect(state.get(clusterWithDetails.uri)).toBeUndefined();
+ expect(state.get(leafCluster.uri)).toBeUndefined();
+});
diff --git a/web/packages/teleterm/src/mainProcess/clusterStore/clusterStore.ts b/web/packages/teleterm/src/mainProcess/clusterStore/clusterStore.ts
new file mode 100644
index 0000000000000..83d5ef65c5ccf
--- /dev/null
+++ b/web/packages/teleterm/src/mainProcess/clusterStore/clusterStore.ts
@@ -0,0 +1,234 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { Patch, Producer, produceWithPatches } from 'immer';
+
+import {
+ Cluster,
+ ShowResources,
+} from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
+
+import Logger from 'teleterm/logger';
+import { TshdClient } from 'teleterm/services/tshd';
+import { ClusterUri, RootClusterUri, routing } from 'teleterm/ui/uri';
+
+import { AwaitableSender } from '../awaitableSender';
+import type { WindowsManager } from '../windowsManager';
+
+export type State = ReadonlyMap;
+
+export type ClusterStoreUpdate =
+ /**
+ * Patches allow the other side to keep reference stability
+ * so that not the whole state is recreated.
+ */
+ | { kind: 'patches'; value: Patch[] }
+ /** The full state, useful to get the initial state on the other side. */
+ | { kind: 'state'; value: State };
+
+export class ClusterStore {
+ private senders = new Set>();
+ private state: State = new Map();
+ private logger = new Logger('ClusterStore');
+
+ constructor(
+ private readonly getTshdClient: () => Promise,
+ private readonly windowsManager: Pick
+ ) {}
+
+ /**
+ * Adds a cluster.
+ * Should only be called via ClusterLifecycleManager.
+ */
+ async add(proxyAddress: string): Promise {
+ const client = await this.getTshdClient();
+ const { response } = await client.addCluster({
+ name: proxyAddress,
+ });
+
+ await this.update(draft => {
+ // Do not overwrite the existing cluster;
+ // otherwise we may lose properties fetched from the auth server.
+ // Consider separating properties read from profile and those
+ // fetched from the auth server at the RPC message level.
+ if (draft.has(response.uri)) {
+ return;
+ }
+ draft.set(response.uri, response);
+ });
+
+ return response;
+ }
+
+ /**
+ * Logs out of the cluster and removes its profile.
+ * Should only be called via ClusterLifecycleManager.
+ */
+ async logoutAndRemove(uri: RootClusterUri): Promise {
+ const client = await this.getTshdClient();
+ await client.logout({ clusterUri: uri, removeProfile: true });
+ await this.update(draft => {
+ for (let d of draft.values()) {
+ if (routing.belongsToProfile(uri, d.uri)) {
+ draft.delete(d.uri);
+ }
+ }
+ });
+ }
+
+ /**
+ * Synchronizes the root clusters.
+ * Does not make a network call, only reads profiles from the disk.
+ */
+ async syncRootClusters(): Promise {
+ const client = await this.getTshdClient();
+ const { response } = await client.listRootClusters({});
+ await this.update(draft => {
+ draft.clear();
+ response.clusters.forEach(cluster => {
+ draft.set(cluster.uri, cluster);
+ });
+ });
+ }
+
+ /**
+ * Synchronizes the root cluster and returns its state before and after the update.
+ * Makes network calls to get cluster details and its leaf clusters.
+ * Should only be called via ClusterLifecycleManager.
+ */
+ async sync(
+ uri: RootClusterUri
+ ): Promise<{ previous: Cluster | undefined; next: Cluster }> {
+ let cluster: Cluster;
+ let leafs: Cluster[];
+ const client = await this.getTshdClient();
+ try {
+ const clusterAndLeafs = await getClusterAndLeafs(client, uri);
+ cluster = clusterAndLeafs.cluster;
+ leafs = clusterAndLeafs.leafs;
+ } catch (error) {
+ await this.update(draft => {
+ const cluster = draft.get(uri);
+ if (cluster) {
+ // TODO(gzdunek): We should rather store the cluster synchronization status,
+ // so the callsites could check it before reading the field.
+ // The workaround is to update the field in case of a failure,
+ // so the places that wait for showResources !== UNSPECIFIED don't get stuck indefinitely.
+ cluster.showResources = ShowResources.ACCESSIBLE_ONLY;
+ }
+ });
+ throw error;
+ }
+
+ const previous = this.state.get(uri);
+ await this.update(draft => {
+ draft.set(cluster.uri, cluster);
+ leafs.forEach(leaf => {
+ draft.set(leaf.uri, leaf);
+ });
+ });
+ return { previous, next: cluster };
+ }
+
+ async set(cluster: Cluster): Promise {
+ await this.update(draft => {
+ draft.set(cluster.uri, cluster);
+ });
+ }
+
+ getRootClusters(): Cluster[] {
+ return this.state
+ .values()
+ .toArray()
+ .filter(c => !c.leaf);
+ }
+
+ getState(): State {
+ return this.state;
+ }
+
+ /**
+ * Registers an `AwaitableSender` to send updates and automatically unregisters
+ * it when disposed.
+ *
+ * Upon registration, the current state is sent immediately as the initial
+ * message to the sender.
+ */
+ registerSender(sender: AwaitableSender): void {
+ this.logger.info('Sender registered');
+ this.senders.add(sender);
+ const send = this.withErrorHandling(update => sender.send(update));
+ void send({
+ kind: 'state',
+ value: this.state,
+ });
+ sender.whenDisposed().then(() => {
+ this.senders.delete(sender);
+ this.logger.info('Sender unregistered');
+ });
+ }
+
+ private async update(producer: Producer): Promise {
+ const [state, patches] = produceWithPatches(this.state, producer);
+ this.state = state;
+ await Promise.all(
+ this.senders.values().map(sender => {
+ const send = this.withErrorHandling(update => sender.send(update));
+ return send({
+ kind: 'patches',
+ value: patches,
+ });
+ })
+ );
+ }
+
+ /**
+ * Wraps a cluster store update sender function with error handling.
+ *
+ * Any error indicates that the renderer state may be out of sync with the cluster store.
+ * Applying further updates may fail.
+ * Prompt the user to reload the window or quit the app.
+ */
+ private withErrorHandling(
+ sender: (update: ClusterStoreUpdate) => Promise
+ ): (update: ClusterStoreUpdate) => Promise {
+ return async update => {
+ try {
+ await sender(update);
+ } catch (e) {
+ await this.windowsManager.crashWindow(e);
+ }
+ };
+ }
+}
+
+async function getClusterAndLeafs(tshdClient: TshdClient, uri: RootClusterUri) {
+ const resolved = await Promise.all([
+ tshdClient.getCluster({
+ clusterUri: uri,
+ }),
+ tshdClient.listLeafClusters({
+ clusterUri: uri,
+ }),
+ ]);
+
+ return {
+ cluster: resolved[0].response,
+ leafs: resolved[1].response.clusters,
+ };
+}
diff --git a/web/packages/teleterm/src/mainProcess/clusterStore/index.ts b/web/packages/teleterm/src/mainProcess/clusterStore/index.ts
new file mode 100644
index 0000000000000..820d317cdd8b7
--- /dev/null
+++ b/web/packages/teleterm/src/mainProcess/clusterStore/index.ts
@@ -0,0 +1,19 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+export * from './clusterStore';
diff --git a/web/packages/teleterm/src/mainProcess/fixtures/mocks.ts b/web/packages/teleterm/src/mainProcess/fixtures/mocks.ts
index a779c4f025b2e..41b7471326b72 100644
--- a/web/packages/teleterm/src/mainProcess/fixtures/mocks.ts
+++ b/web/packages/teleterm/src/mainProcess/fixtures/mocks.ts
@@ -25,6 +25,8 @@ import { AgentProcessState } from 'teleterm/mainProcess/types';
// Importing electron breaks the fixtures if that's done from within storybook.
import { createConfigService } from 'teleterm/services/config/configService';
import { createMockFileStorage } from 'teleterm/services/fileStorage/fixtures/mocks';
+import { makeRootCluster } from 'teleterm/services/tshd/testHelpers';
+import { Cluster } from 'teleterm/services/tshd/types';
import { MainProcessClient, RuntimeSettings } from 'teleterm/types';
export class MockMainProcessClient implements MainProcessClient {
@@ -105,10 +107,6 @@ export class MockMainProcessClient implements MainProcessClient {
fileStorage = createMockFileStorage();
- removeKubeConfig(): Promise {
- return Promise.resolve(undefined);
- }
-
async forceFocusWindow() {}
async symlinkTshMacOs() {
@@ -172,8 +170,6 @@ export class MockMainProcessClient implements MainProcessClient {
return this.frontendAppInit.promise;
}
- refreshClusterList() {}
-
async selectDirectoryForDesktopSession() {
return '';
}
@@ -182,7 +178,6 @@ export class MockMainProcessClient implements MainProcessClient {
return true;
}
async changeAppUpdatesManagingCluster() {}
- async maybeRemoveAppUpdatesManagingCluster() {}
async checkForAppUpdates() {}
async downloadAppUpdate() {}
async cancelAppUpdateDownload() {}
@@ -202,6 +197,30 @@ export class MockMainProcessClient implements MainProcessClient {
} {
return { cleanup: () => undefined };
}
+
+ subscribeToClusterStore(): {
+ cleanup: () => void;
+ } {
+ return { cleanup: () => undefined };
+ }
+ async logout(): Promise {}
+ async syncCluster(): Promise {}
+ async addCluster(): Promise {
+ return makeRootCluster();
+ }
+ async syncRootClusters(): Promise {
+ return [];
+ }
+ registerClusterLifecycleHandler(): {
+ cleanup: () => void;
+ } {
+ return { cleanup: () => undefined };
+ }
+ subscribeToProfileWatcherErrors(): {
+ cleanup: () => void;
+ } {
+ return { cleanup: () => undefined };
+ }
}
export const makeRuntimeSettings = (
@@ -226,7 +245,7 @@ export const makeRuntimeSettings = (
tshd: {
requestedNetworkAddress: '',
binaryPath: '',
- homeDir: '',
+ defaultHomeDir: '',
},
sharedProcess: {
requestedNetworkAddress: '',
diff --git a/web/packages/teleterm/src/mainProcess/ipcSerializer.test.ts b/web/packages/teleterm/src/mainProcess/ipcSerializer.test.ts
new file mode 100644
index 0000000000000..5b3ab156b29c5
--- /dev/null
+++ b/web/packages/teleterm/src/mainProcess/ipcSerializer.test.ts
@@ -0,0 +1,68 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { TshdRpcError } from 'teleterm/services/tshd/cloneableClient';
+
+import { deserializeError, serializeError } from './ipcSerializer';
+
+test('serializes and deserializes regular error', () => {
+ const err = new Error('Boom');
+ err.name = 'CustomError';
+
+ const serialized = serializeError(err);
+ expect(serialized instanceof Error).toBe(false);
+ expect(serialized.message).toBe('Boom');
+ expect(serialized.name).toBe('CustomError');
+ expect(serialized.stack).toBe(err.stack);
+ expect(serialized.cause).toBe(err.cause);
+
+ const cloned = structuredClone(serialized);
+ const deserialized = deserializeError(cloned);
+ expect(deserialized instanceof Error).toBe(true);
+ expect(deserialized.message).toBe('Boom');
+ expect(deserialized.name).toBe('CustomError');
+ expect(deserialized.stack).toBe(err.stack);
+ expect(deserialized.cause).toBe(err.cause);
+});
+
+test('serializes and deserializes tshd error', () => {
+ const err: TshdRpcError = {
+ name: 'TshdRpcError',
+ message: 'Could not found',
+ toString: () => 'Could not found',
+ cause: '',
+ stack: '',
+ code: 'NOT_FOUND',
+ isResolvableWithRelogin: false,
+ };
+
+ const serialized = serializeError(err);
+ expect(serialized instanceof Error).toBe(false);
+ expect(serialized.message).toBe('Could not found');
+ expect(serialized.name).toBe('TshdRpcError');
+ expect(serialized['isResolvableWithRelogin']).toBe(false);
+ expect(serialized['code']).toBe('NOT_FOUND');
+
+ const cloned = structuredClone(serialized);
+ const deserialized = deserializeError(cloned);
+ expect(deserialized instanceof Error).toBe(true);
+ expect(deserialized.message).toBe('Could not found');
+ expect(deserialized.name).toBe('TshdRpcError');
+ expect(deserialized['isResolvableWithRelogin']).toBe(false);
+ expect(deserialized['code']).toBe('NOT_FOUND');
+});
diff --git a/web/packages/teleterm/src/mainProcess/ipcSerializer.ts b/web/packages/teleterm/src/mainProcess/ipcSerializer.ts
index 42555704147c5..7011f6b5b47e8 100644
--- a/web/packages/teleterm/src/mainProcess/ipcSerializer.ts
+++ b/web/packages/teleterm/src/mainProcess/ipcSerializer.ts
@@ -21,23 +21,41 @@ export type SerializedError = {
message: string;
stack?: string;
cause?: unknown;
+ toStringResult?: string;
};
/** Serializes an Error into a plain object for transport through Electron IPC. */
export function serializeError(error: Error): SerializedError {
+ const {
+ name,
+ cause,
+ stack,
+ message,
+ // functions must be skipped, otherwise structuredClone will fail to clone the object
+ // eslint-disable-next-line unused-imports/no-unused-vars
+ toString,
+ ...enumerableFields
+ } = error;
return {
- name: error.name,
- message: error.message,
- cause: error.cause,
- stack: error.stack,
+ name,
+ message,
+ cause,
+ stack,
+ // Calling the destructured function directly could result in the following error:
+ // Method Error.prototype.toString called on incompatible receiver undefined
+ toStringResult: error.toString?.(),
+ ...enumerableFields,
};
}
/** Deserializes a plain object back into an Error instance. */
export function deserializeError(serialized: SerializedError): Error {
- const error = new Error(serialized.message);
- error.name = serialized.name;
- error.cause = serialized.cause;
- error.stack = serialized.stack;
+ const { name, cause, stack, message, toStringResult, ...rest } = serialized;
+ const error = new Error(message);
+ error.name = name;
+ error.cause = cause;
+ error.stack = stack;
+ error.toString = () => toStringResult;
+ Object.assign(error, rest);
return error;
}
diff --git a/web/packages/teleterm/src/mainProcess/mainProcess.ts b/web/packages/teleterm/src/mainProcess/mainProcess.ts
index e5f4dc173eeb0..21cd4177e1e41 100644
--- a/web/packages/teleterm/src/mainProcess/mainProcess.ts
+++ b/web/packages/teleterm/src/mainProcess/mainProcess.ts
@@ -27,15 +27,19 @@ import {
app,
dialog,
ipcMain,
+ IpcMainInvokeEvent,
Menu,
MenuItemConstructorOptions,
nativeTheme,
shell,
} from 'electron';
+import { enableMapSet, enablePatches } from 'immer';
import { AbortError } from 'shared/utils/error';
import Logger from 'teleterm/logger';
+import { ClusterLifecycleManager } from 'teleterm/mainProcess/clusterLifecycleManager';
+import { watchProfiles } from 'teleterm/mainProcess/profileWatcher';
import { getAssetPath } from 'teleterm/mainProcess/runtimeSettings';
import {
ChildProcessAddresses,
@@ -77,6 +81,8 @@ import {
} from '../services/config';
import { downloadAgent, FileDownloader, verifyAgent } from './agentDownloader';
import { AgentRunner } from './agentRunner';
+import { AwaitableSender } from './awaitableSender';
+import { ClusterStore } from './clusterStore';
import { subscribeToTabContextMenuEvent } from './contextMenus/tabContextMenu';
import { subscribeToTerminalContextMenuEvent } from './contextMenus/terminalContextMenu';
import {
@@ -111,6 +117,16 @@ export default class MainProcess {
private sharedProcessLastLogs: KeepLastChunks;
private appStateFileStorage: FileStorage;
private configFileStorage: FileStorage;
+ /**
+ * Promise holding the resolution of child process addresses.
+ *
+ * Both the internal tshd client (in the main process) and the one in
+ * the renderer depend on this promise.
+ *
+ * If the promise rejects, the error will propagate to the renderer via the IPC
+ * handler (causing the renderer to stop initialization and show the error)
+ * and also surface when attempting to access the tshdClients property.
+ */
private resolvedChildProcessAddresses: Promise;
private windowsManager: WindowsManager;
// this function can be safely called concurrently
@@ -122,13 +138,32 @@ export default class MainProcess {
)
);
private readonly agentRunner: AgentRunner;
+ /**
+ * A promise responsible for initializing clients for tshd gRPC services once the addresses of
+ * child processes are resolved. Set in the constructor.
+ *
+ * If the client setup fails, the resulting error will propagate to callsites which use
+ * tshdClients, including preload of the frontend app, causing its initialization to stop.
+ */
private tshdClients: Promise<{
terminalService: TshdClient;
autoUpdateService: AutoUpdateClient;
}>;
private readonly appUpdater: AppUpdater;
+ public readonly clusterStore: ClusterStore;
+ private clusterLifecycleManager: ClusterLifecycleManager;
+ private disposeAbortController = new AbortController();
- private constructor(opts: Options) {
+ /**
+ * Starts necessary child processes such as tsh daemon and the shared process. It also sets
+ * up IPC handlers and resolves the network addresses under which the child processes set up gRPC
+ * servers.
+ *
+ * Might throw an error if spawning a child process fails, see initTshd for more details.
+ */
+ constructor(opts: Options) {
+ enablePatches();
+ enableMapSet();
this.settings = opts.settings;
this.logger = opts.logger;
this.configService = opts.configService;
@@ -151,13 +186,20 @@ export default class MainProcess {
}
);
+ this.updateAboutPanelIfNeeded();
+ this.setAppMenu();
+ this.initTshd();
+ this.initSharedProcess();
+ this.initResolvingChildProcessAddressesAndTshdClients();
+ this.initIpc();
+
const getClusterVersions = async () => {
- const { autoUpdateService } = await this.getTshdClients();
+ const { autoUpdateService } = await this.tshdClients;
const { response } = await autoUpdateService.getClusterVersions({});
return response;
};
const getDownloadBaseUrl = async () => {
- const { autoUpdateService } = await this.getTshdClients();
+ const { autoUpdateService } = await this.tshdClients;
const {
response: { baseUrl },
} = await autoUpdateService.getDownloadBaseUrl({});
@@ -177,22 +219,33 @@ export default class MainProcess {
},
process.env[TELEPORT_TOOLS_VERSION_ENV_VAR]
);
- }
-
- /**
- * create starts necessary child processes such as tsh daemon and the shared process. It also sets
- * up IPC handlers and resolves the network addresses under which the child processes set up gRPC
- * servers.
- *
- * create might throw an error if spawning a child process fails, see initTshd for more details.
- */
- static create(opts: Options) {
- const instance = new MainProcess(opts);
- instance.init();
- return instance;
+ this.clusterStore = new ClusterStore(
+ () => this.tshdClients.then(c => c.terminalService),
+ this.windowsManager
+ );
+ const watcher = watchProfiles({
+ tshDirectory: this.configService.get('tshHome').value,
+ tshClient: {
+ listRootClusters: async () => {
+ const { terminalService } = await this.tshdClients;
+ const { response } = await terminalService.listRootClusters({});
+ return response.clusters;
+ },
+ },
+ clusterStore: this.clusterStore,
+ signal: this.disposeAbortController.signal,
+ });
+ this.clusterLifecycleManager = new ClusterLifecycleManager(
+ this.clusterStore,
+ () => this.tshdClients.then(c => c.terminalService),
+ this.appUpdater,
+ this.windowsManager,
+ watcher
+ );
}
async dispose(): Promise {
+ this.disposeAbortController.abort();
this.windowsManager.dispose();
await Promise.all([
this.appUpdater.dispose(),
@@ -209,34 +262,8 @@ export default class MainProcess {
]);
}
- private init() {
- this.updateAboutPanelIfNeeded();
- this.setAppMenu();
- this.initTshd();
- this.initSharedProcess();
- this.initResolvingChildProcessAddresses();
- this.initIpc();
- }
-
- async getTshdClients(): Promise<{
- terminalService: TshdClient;
- autoUpdateService: AutoUpdateClient;
- }> {
- if (!this.tshdClients) {
- this.tshdClients = this.resolvedChildProcessAddresses.then(
- ({ tsh: tshdAddress }) =>
- setUpTshdClients({
- runtimeSettings: this.settings,
- tshdAddress,
- })
- );
- }
-
- return this.tshdClients;
- }
-
private initTshd() {
- const { binaryPath, homeDir } = this.settings.tshd;
+ const { binaryPath } = this.settings.tshd;
this.logger.info(`Starting tsh daemon from ${binaryPath}`);
// spawn might either fail immediately by throwing an error or cause the error event to be emitted
@@ -256,7 +283,7 @@ export default class MainProcess {
windowsHide: true,
env: {
...process.env,
- TELEPORT_HOME: homeDir,
+ TELEPORT_HOME: this.configService.get('tshHome').value,
[TSH_AUTOUPDATE_ENV_VAR]: TSH_AUTOUPDATE_OFF,
},
}
@@ -334,7 +361,11 @@ export default class MainProcess {
);
}
- private initResolvingChildProcessAddresses(): void {
+ /**
+ * Initializes the resolution of child process addresses and sets up tshd clients.
+ * On Windows, the setup of tshd clients also initialized the main process cert for mTLS.
+ */
+ private initResolvingChildProcessAddressesAndTshdClients(): void {
this.resolvedChildProcessAddresses = Promise.all([
resolveNetworkAddress(
this.settings.tshd.requestedNetworkAddress,
@@ -361,6 +392,19 @@ export default class MainProcess {
)
),
]).then(([tsh, shared]) => ({ tsh, shared }));
+
+ this.tshdClients = this.resolvedChildProcessAddresses.then(
+ ({ tsh: tshdAddress }) =>
+ setUpTshdClients({
+ runtimeSettings: this.settings,
+ tshdAddress,
+ })
+ );
+ // Log the error just to avoid unhandled promise rejection if setUpTshdClients fails before
+ // anything reads tshdClients.
+ this.tshdClients.catch(error => {
+ this.logger.error('Could not initialize tshd clients', error);
+ });
}
private initIpc() {
@@ -372,44 +416,17 @@ export default class MainProcess {
event.returnValue = nativeTheme.shouldUseDarkColors;
});
- ipcMain.handle('main-process-get-resolved-child-process-addresses', () => {
+ ipcHandle('main-process-get-resolved-child-process-addresses', () => {
return this.resolvedChildProcessAddresses;
});
- // the handler can remove a single kube config file or entire directory for given cluster
- ipcMain.handle(
- 'main-process-remove-kube-config',
- (
- _,
- options: {
- relativePath: string;
- isDirectory?: boolean;
- }
- ) => {
- const { kubeConfigsDir } = this.settings;
- const filePath = path.join(kubeConfigsDir, options.relativePath);
- const isOutOfRoot = filePath.indexOf(kubeConfigsDir) !== 0;
-
- if (isOutOfRoot) {
- return Promise.reject('Invalid path');
- }
- return fs
- .rm(filePath, { recursive: !!options.isDirectory })
- .catch(error => {
- if (error.code !== 'ENOENT') {
- throw error;
- }
- });
- }
- );
-
- ipcMain.handle('main-process-show-file-save-dialog', (_, filePath) =>
+ ipcHandle('main-process-show-file-save-dialog', (_, filePath) =>
dialog.showSaveDialog({
defaultPath: path.basename(filePath),
})
);
- ipcMain.handle(
+ ipcHandle(
MainProcessIpc.SaveTextToFile,
async (
_,
@@ -448,7 +465,7 @@ export default class MainProcess {
}
);
- ipcMain.handle(
+ ipcHandle(
MainProcessIpc.ForceFocusWindow,
async (
_,
@@ -469,7 +486,7 @@ export default class MainProcess {
// Used in the `tsh install` command on macOS to make the bundled tsh available in PATH.
// Returns true if tsh got successfully installed, false if the user closed the osascript
// prompt. Throws an error when osascript fails.
- ipcMain.handle('main-process-symlink-tsh-macos', async () => {
+ ipcHandle('main-process-symlink-tsh-macos', async () => {
const source = this.settings.tshd.binaryPath;
const target = '/usr/local/bin/tsh';
const prompt =
@@ -491,7 +508,7 @@ export default class MainProcess {
}
});
- ipcMain.handle('main-process-remove-tsh-symlink-macos', async () => {
+ ipcHandle('main-process-remove-tsh-symlink-macos', async () => {
const target = '/usr/local/bin/tsh';
const prompt =
'Teleport Connect wants to remove a symlink for tsh from /usr/local/bin.';
@@ -512,21 +529,21 @@ export default class MainProcess {
}
});
- ipcMain.handle('main-process-open-config-file', async () => {
+ ipcHandle('main-process-open-config-file', async () => {
const path = this.configFileStorage.getFilePath();
await shell.openPath(path);
return path;
});
- ipcMain.handle(MainProcessIpc.DownloadConnectMyComputerAgent, () =>
+ ipcHandle(MainProcessIpc.DownloadConnectMyComputerAgent, () =>
this.downloadAgentShared()
);
- ipcMain.handle(MainProcessIpc.VerifyConnectMyComputerAgent, async () => {
+ ipcHandle(MainProcessIpc.VerifyConnectMyComputerAgent, async () => {
await verifyAgent(this.settings.agentBinaryPath);
});
- ipcMain.handle(
+ ipcHandle(
'main-process-connect-my-computer-create-agent-config-file',
(_, args: CreateAgentConfigFileArgs) =>
createAgentConfigFile(this.settings, {
@@ -537,7 +554,7 @@ export default class MainProcess {
})
);
- ipcMain.handle(
+ ipcHandle(
'main-process-connect-my-computer-is-agent-config-file-created',
async (
_,
@@ -547,7 +564,7 @@ export default class MainProcess {
) => isAgentConfigFileCreated(this.settings, args.rootClusterUri)
);
- ipcMain.handle(
+ ipcHandle(
'main-process-connect-my-computer-kill-agent',
async (
_,
@@ -559,7 +576,7 @@ export default class MainProcess {
}
);
- ipcMain.handle(
+ ipcHandle(
'main-process-connect-my-computer-remove-agent-directory',
(
_,
@@ -569,11 +586,11 @@ export default class MainProcess {
) => removeAgentDirectory(this.settings, args.rootClusterUri)
);
- ipcMain.handle(MainProcessIpc.TryRemoveConnectMyComputerAgentBinary, () =>
+ ipcHandle(MainProcessIpc.TryRemoveConnectMyComputerAgentBinary, () =>
this.agentRunner.tryRemoveAgentBinary()
);
- ipcMain.handle(
+ ipcHandle(
'main-process-connect-my-computer-run-agent',
async (
_,
@@ -609,7 +626,7 @@ export default class MainProcess {
}
);
- ipcMain.handle(
+ ipcHandle(
'main-process-open-agent-logs-directory',
async (
_,
@@ -628,7 +645,7 @@ export default class MainProcess {
}
);
- ipcMain.handle(
+ ipcHandle(
MainProcessIpc.SelectDirectoryForDesktopSession,
async (_, args: { desktopUri: string; login: string }) => {
const value = await dialog.showOpenDialog({
@@ -642,7 +659,7 @@ export default class MainProcess {
}
const [dirPath] = value.filePaths;
- const { terminalService } = await this.getTshdClients();
+ const { terminalService } = await this.tshdClients;
await terminalService.setSharedDirectoryForDesktopSession({
desktopUri: args.desktopUri,
login: args.login,
@@ -657,11 +674,11 @@ export default class MainProcess {
event.returnValue = this.appUpdater.supportsUpdates();
});
- ipcMain.handle(MainProcessIpc.CheckForAppUpdates, () =>
+ ipcHandle(MainProcessIpc.CheckForAppUpdates, () =>
this.appUpdater.checkForUpdates()
);
- ipcMain.handle(
+ ipcHandle(
MainProcessIpc.ChangeAppUpdatesManagingCluster,
(
event,
@@ -671,28 +688,48 @@ export default class MainProcess {
) => this.appUpdater.changeManagingCluster(args.clusterUri)
);
- ipcMain.handle(
- MainProcessIpc.MaybeRemoveAppUpdatesManagingCluster,
- (
- event,
- args: {
- clusterUri: RootClusterUri;
- }
- ) => this.appUpdater.maybeRemoveManagingCluster(args.clusterUri)
- );
-
- ipcMain.handle(MainProcessIpc.DownloadAppUpdate, () =>
+ ipcHandle(MainProcessIpc.DownloadAppUpdate, () =>
this.appUpdater.download()
);
- ipcMain.handle(MainProcessIpc.CancelAppUpdateDownload, () =>
+ ipcHandle(MainProcessIpc.CancelAppUpdateDownload, () =>
this.appUpdater.cancelDownload()
);
- ipcMain.handle(MainProcessIpc.QuiteAndInstallAppUpdate, () =>
+ ipcHandle(MainProcessIpc.QuiteAndInstallAppUpdate, () =>
this.appUpdater.quitAndInstall()
);
+ ipcHandle(MainProcessIpc.AddCluster, (ev, proxyAddress) =>
+ this.clusterLifecycleManager.addCluster(proxyAddress)
+ );
+
+ ipcHandle(MainProcessIpc.SyncRootClusters, () =>
+ this.clusterLifecycleManager.syncRootClustersAndStartProfileWatcher()
+ );
+
+ ipcHandle(MainProcessIpc.SyncCluster, (_, args) =>
+ this.clusterLifecycleManager.syncCluster(args.clusterUri)
+ );
+
+ ipcHandle(MainProcessIpc.Logout, async (_, args) => {
+ await this.clusterLifecycleManager.logoutAndRemoveCluster(
+ args.clusterUri
+ );
+ });
+
+ ipcMain.on(MainProcessIpc.InitClusterStoreSubscription, ev => {
+ const port = ev.ports[0];
+ this.clusterStore.registerSender(new AwaitableSender(port));
+ });
+
+ ipcMain.on(MainProcessIpc.RegisterClusterLifecycleHandler, ev => {
+ const port = ev.ports[0];
+ this.clusterLifecycleManager.setRendererEventHandler(
+ new AwaitableSender(port)
+ );
+ });
+
subscribeToTerminalContextMenuEvent(this.configService);
subscribeToTabContextMenuEvent(
this.settings.availableShells,
@@ -974,3 +1011,23 @@ function makeAppUpdaterStorage(fs: FileStorage): AppUpdaterStorage {
},
};
}
+
+/**
+ * Handles requests sent via `ipcInvoke`.
+ * The renderer must send requests using `ipcInvoke` (not `ipcRenderer.invoke`).
+ *
+ * Use this instead of `ipcMain.handle`. It ensures full error serialization
+ * and prevents Electron from adding the generic message "Error invoking remote method".
+ */
+function ipcHandle(
+ channel: string,
+ listener: (event: IpcMainInvokeEvent, ...args: any[]) => Promise | any
+): void {
+ ipcMain.handle(channel, async (...args) => {
+ try {
+ return { result: await Promise.try(listener, ...args) };
+ } catch (e) {
+ return { error: serializeError(e) };
+ }
+ });
+}
diff --git a/web/packages/teleterm/src/mainProcess/mainProcessClient.ts b/web/packages/teleterm/src/mainProcess/mainProcessClient.ts
index dc56c2a4832c1..44eb7726442ce 100644
--- a/web/packages/teleterm/src/mainProcess/mainProcessClient.ts
+++ b/web/packages/teleterm/src/mainProcess/mainProcessClient.ts
@@ -18,7 +18,10 @@
import { ipcRenderer } from 'electron';
+import Logger from 'teleterm/logger';
+import type { Message, MessageAck } from 'teleterm/mainProcess/awaitableSender';
import { CreateAgentConfigFileArgs } from 'teleterm/mainProcess/createAgentConfigFile';
+import { deserializeError } from 'teleterm/mainProcess/ipcSerializer';
import { AppUpdateEvent } from 'teleterm/services/appUpdater';
import { createFileStorageClient } from 'teleterm/services/fileStorage';
import { RootClusterUri } from 'teleterm/ui/uri';
@@ -35,6 +38,8 @@ import {
WindowsManagerIpc,
} from './types';
+const logger = new Logger('MainProcessClient');
+
export default function createMainProcessClient(): MainProcessClient {
return {
/*
@@ -97,81 +102,68 @@ export default function createMainProcessClient(): MainProcessClient {
// TODO(ravicious): Convert the rest of IPC channels to use enums defined in types.ts such as
// MainProcessIpc rather than hardcoded strings.
getResolvedChildProcessAddresses(): Promise {
- return ipcRenderer.invoke(
- 'main-process-get-resolved-child-process-addresses'
- );
+ return ipcInvoke('main-process-get-resolved-child-process-addresses');
},
showFileSaveDialog(filePath: string) {
- return ipcRenderer.invoke('main-process-show-file-save-dialog', filePath);
+ return ipcInvoke('main-process-show-file-save-dialog', filePath);
},
saveTextToFile(args) {
- return ipcRenderer.invoke(MainProcessIpc.SaveTextToFile, args);
+ return ipcInvoke(MainProcessIpc.SaveTextToFile, args);
},
openTerminalContextMenu,
openTabContextMenu,
configService: createConfigServiceClient(),
fileStorage: createFileStorageClient(),
- removeKubeConfig(options) {
- return ipcRenderer.invoke('main-process-remove-kube-config', options);
- },
forceFocusWindow(args) {
- return ipcRenderer.invoke(MainProcessIpc.ForceFocusWindow, args);
+ return ipcInvoke(MainProcessIpc.ForceFocusWindow, args);
},
symlinkTshMacOs() {
- return ipcRenderer.invoke('main-process-symlink-tsh-macos');
+ return ipcInvoke('main-process-symlink-tsh-macos');
},
removeTshSymlinkMacOs() {
- return ipcRenderer.invoke('main-process-remove-tsh-symlink-macos');
+ return ipcInvoke('main-process-remove-tsh-symlink-macos');
},
openConfigFile() {
- return ipcRenderer.invoke('main-process-open-config-file');
+ return ipcInvoke('main-process-open-config-file');
},
shouldUseDarkColors() {
return ipcRenderer.sendSync('main-process-should-use-dark-colors');
},
downloadAgent() {
- return ipcRenderer.invoke(MainProcessIpc.DownloadConnectMyComputerAgent);
+ return ipcInvoke(MainProcessIpc.DownloadConnectMyComputerAgent);
},
verifyAgent() {
- return ipcRenderer.invoke(MainProcessIpc.VerifyConnectMyComputerAgent);
+ return ipcInvoke(MainProcessIpc.VerifyConnectMyComputerAgent);
},
createAgentConfigFile(args: CreateAgentConfigFileArgs) {
- return ipcRenderer.invoke(
+ return ipcInvoke(
'main-process-connect-my-computer-create-agent-config-file',
args
);
},
isAgentConfigFileCreated(args: { rootClusterUri: RootClusterUri }) {
- return ipcRenderer.invoke(
+ return ipcInvoke(
'main-process-connect-my-computer-is-agent-config-file-created',
args
);
},
removeAgentDirectory(args: { rootClusterUri: RootClusterUri }) {
- return ipcRenderer.invoke(
+ return ipcInvoke(
'main-process-connect-my-computer-remove-agent-directory',
args
);
},
tryRemoveConnectMyComputerAgentBinary() {
- return ipcRenderer.invoke(
- MainProcessIpc.TryRemoveConnectMyComputerAgentBinary
- );
+ return ipcInvoke(MainProcessIpc.TryRemoveConnectMyComputerAgentBinary);
},
openAgentLogsDirectory(args: { rootClusterUri: RootClusterUri }) {
- return ipcRenderer.invoke('main-process-open-agent-logs-directory', args);
+ return ipcInvoke('main-process-open-agent-logs-directory', args);
},
killAgent(args: { rootClusterUri: RootClusterUri }) {
- return ipcRenderer.invoke(
- 'main-process-connect-my-computer-kill-agent',
- args
- );
+ return ipcInvoke('main-process-connect-my-computer-kill-agent', args);
},
runAgent(args: { rootClusterUri: RootClusterUri }) {
- return ipcRenderer.invoke(
- 'main-process-connect-my-computer-run-agent',
- args
- );
+ return ipcInvoke('main-process-connect-my-computer-run-agent', args);
},
getAgentState(args: { rootClusterUri: RootClusterUri }) {
return ipcRenderer.sendSync(
@@ -188,48 +180,31 @@ export default function createMainProcessClient(): MainProcessClient {
signalUserInterfaceReadiness(args: { success: boolean }) {
ipcRenderer.send(WindowsManagerIpc.SignalUserInterfaceReadiness, args);
},
- refreshClusterList() {
- ipcRenderer.send(MainProcessIpc.RefreshClusterList);
- },
selectDirectoryForDesktopSession(args: {
desktopUri: string;
login: string;
}) {
- return ipcRenderer.invoke(
- MainProcessIpc.SelectDirectoryForDesktopSession,
- args
- );
+ return ipcInvoke(MainProcessIpc.SelectDirectoryForDesktopSession, args);
},
supportsAppUpdates() {
return ipcRenderer.sendSync(MainProcessIpc.SupportsAppUpdates);
},
checkForAppUpdates() {
- return ipcRenderer.invoke(MainProcessIpc.CheckForAppUpdates);
+ return ipcInvoke(MainProcessIpc.CheckForAppUpdates);
},
downloadAppUpdate() {
- return ipcRenderer.invoke(MainProcessIpc.DownloadAppUpdate);
+ return ipcInvoke(MainProcessIpc.DownloadAppUpdate);
},
cancelAppUpdateDownload() {
- return ipcRenderer.invoke(MainProcessIpc.CancelAppUpdateDownload);
+ return ipcInvoke(MainProcessIpc.CancelAppUpdateDownload);
},
quitAndInstallAppUpdate() {
- return ipcRenderer.invoke(MainProcessIpc.QuiteAndInstallAppUpdate);
+ return ipcInvoke(MainProcessIpc.QuiteAndInstallAppUpdate);
},
changeAppUpdatesManagingCluster(clusterUri) {
- return ipcRenderer.invoke(
- MainProcessIpc.ChangeAppUpdatesManagingCluster,
- {
- clusterUri,
- }
- );
- },
- maybeRemoveAppUpdatesManagingCluster(clusterUri) {
- return ipcRenderer.invoke(
- MainProcessIpc.MaybeRemoveAppUpdatesManagingCluster,
- {
- clusterUri,
- }
- );
+ return ipcInvoke(MainProcessIpc.ChangeAppUpdatesManagingCluster, {
+ clusterUri,
+ });
},
subscribeToAppUpdateEvents: listener => {
const ipcListener = (_, updateEvent: AppUpdateEvent) => {
@@ -263,5 +238,110 @@ export default function createMainProcessClient(): MainProcessClient {
),
};
},
+ subscribeToClusterStore: listener => {
+ const { close } = startAwaitableSenderListener(
+ MainProcessIpc.InitClusterStoreSubscription,
+ listener
+ );
+
+ return {
+ cleanup: close,
+ };
+ },
+ addCluster: async (proxyAddress: string) => {
+ return await ipcInvoke(MainProcessIpc.AddCluster, proxyAddress);
+ },
+ syncRootClusters: async () => {
+ return await ipcInvoke(MainProcessIpc.SyncRootClusters);
+ },
+ syncCluster: (clusterUri: RootClusterUri) => {
+ return ipcInvoke(MainProcessIpc.SyncCluster, { clusterUri });
+ },
+ logout: (clusterUri: RootClusterUri) => {
+ return ipcInvoke(MainProcessIpc.Logout, { clusterUri });
+ },
+ registerClusterLifecycleHandler(listener): {
+ cleanup: () => void;
+ } {
+ const { close } = startAwaitableSenderListener(
+ MainProcessIpc.RegisterClusterLifecycleHandler,
+ listener
+ );
+
+ return { cleanup: close };
+ },
+ subscribeToProfileWatcherErrors: listener => {
+ const ipcListener = (_, error) => {
+ listener(error);
+ };
+
+ ipcRenderer.addListener(RendererIpc.ProfileWatcherError, ipcListener);
+ return {
+ cleanup: () =>
+ ipcRenderer.removeListener(
+ RendererIpc.ProfileWatcherError,
+ ipcListener
+ ),
+ };
+ },
+ };
+}
+
+/**
+ * Sets up a `MessagePort` listener in the renderer process and transfers
+ * the port to the main process via the specified IPC `channel`.
+ *
+ * The main process is expected to create an `AwaitableSender` using the received port,
+ * enabling it to send messages that require acknowledgments from the renderer.
+ */
+function startAwaitableSenderListener(
+ channel: string,
+ listener: (value: T) => void | Promise
+): {
+ close: () => void;
+} {
+ const { port1: localPort, port2: transferablePort } = new MessageChannel();
+
+ localPort.onmessage = async (event: MessageEvent) => {
+ const msg = event.data;
+ if (msg.type !== 'data') {
+ return;
+ }
+ const ack: MessageAck = { type: 'ack', id: msg.id };
+
+ try {
+ await listener(msg.payload as T);
+ } catch (e) {
+ ack.error = e;
+ }
+
+ localPort.postMessage(ack);
+ };
+
+ localPort.start();
+ ipcRenderer.postMessage(channel, undefined, [transferablePort]);
+
+ return {
+ close: () => {
+ localPort.onmessage = undefined;
+ localPort.close();
+ transferablePort.close();
+ },
};
}
+
+/**
+ * Resolves with the response from the main process.
+ * The main process must register the handler using `ipcHandle` (not `ipcMain.handle`).
+ *
+ * Use this instead of `ipcRenderer.invoke`.
+ */
+async function ipcInvoke(channel: string, ...args: any[]): Promise {
+ const { error, result } = await ipcRenderer.invoke(channel, ...args);
+ if (error) {
+ const deserialized = deserializeError(error);
+ logger.error(`Error invoking remote method ${channel}`, deserialized);
+ throw deserialized;
+ }
+ return result;
+}
diff --git a/web/packages/teleterm/src/mainProcess/navigationHandler.test.ts b/web/packages/teleterm/src/mainProcess/navigationHandler.test.ts
new file mode 100644
index 0000000000000..61498b898962a
--- /dev/null
+++ b/web/packages/teleterm/src/mainProcess/navigationHandler.test.ts
@@ -0,0 +1,120 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { dialog, shell, WebContents } from 'electron';
+
+import Logger, { NullService } from 'teleterm/logger';
+import { makeRuntimeSettings } from 'teleterm/mainProcess/fixtures/mocks';
+import { makeRootCluster } from 'teleterm/services/tshd/testHelpers';
+
+import { registerNavigationHandlers } from './navigationHandler';
+
+beforeAll(() => {
+ Logger.init(new NullService());
+});
+
+beforeEach(() => {
+ jest.resetAllMocks();
+});
+
+jest.mock('electron', () => ({
+ dialog: {
+ showErrorBox: jest.fn(),
+ },
+ shell: {
+ openExternal: jest.fn(),
+ },
+}));
+
+const cluster = makeRootCluster();
+
+describe('opening links to', () => {
+ test.each([
+ {
+ name: 'Teleport',
+ url: 'https://goteleport.com/',
+ allowed: true,
+ },
+ {
+ name: 'Gravitational GitHub',
+ url: 'https://github.com/gravitational/',
+ allowed: true,
+ },
+ {
+ name: 'cluster SSO host',
+ // comes from makeRootCluster
+ url: 'https://example.auth0.com/some-path',
+ allowed: true,
+ },
+ {
+ name: 'cluster proxy host',
+ // comes from makeRootCluster
+ url: 'https://teleport-local.com:3080/some-path',
+ allowed: true,
+ },
+ {
+ name: 'non-HTTPS URLs',
+ url: 'http://goteleport.com',
+ allowed: false,
+ },
+ {
+ name: 'non-Gravitational GitHub',
+ url: 'https://github.com/abc',
+ allowed: false,
+ },
+ {
+ name: 'arbitrary URLs',
+ url: 'https://google.com',
+ allowed: false,
+ },
+ ])('$name', test => {
+ let handler: Parameters[0];
+ registerNavigationHandlers(
+ {
+ setWindowOpenHandler: d => {
+ handler = d;
+ },
+ on: jest.fn(),
+ },
+ makeRuntimeSettings(),
+ { getRootClusters: () => [cluster] },
+ new Logger()
+ );
+
+ const result = handler({
+ url: test.url,
+ frameName: '',
+ features: '',
+ disposition: 'default',
+ referrer: undefined,
+ });
+
+ expect(result).toEqual({ action: 'deny' });
+ /* eslint-disable jest/no-conditional-expect */
+ if (test.allowed) {
+ expect(shell.openExternal).toHaveBeenCalledWith(test.url);
+ expect(dialog.showErrorBox).not.toHaveBeenCalled();
+ } else {
+ expect(shell.openExternal).not.toHaveBeenCalled();
+ expect(dialog.showErrorBox).toHaveBeenCalledWith(
+ 'Cannot open this link',
+ 'The domain does not match any of the allowed domains. Check main.log for more details.'
+ );
+ }
+ });
+});
diff --git a/web/packages/teleterm/src/mainProcess/navigationHandler.ts b/web/packages/teleterm/src/mainProcess/navigationHandler.ts
new file mode 100644
index 0000000000000..eb200c6328059
--- /dev/null
+++ b/web/packages/teleterm/src/mainProcess/navigationHandler.ts
@@ -0,0 +1,147 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { dialog, shell, WebContents } from 'electron';
+
+import { Cluster } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
+
+import { proxyHostToBrowserProxyHost } from 'teleterm/services/tshd/cluster';
+import { Logger, RuntimeSettings } from 'teleterm/types';
+
+import { ClusterStore } from './clusterStore';
+
+/**
+ * Limits navigation capabilities to reduce the attack surface.
+ * See TEL-Q122-19 from "Teleport Core Testing Q1 2022" security audit.
+ *
+ * See also points 12, 13 and 14 from the Electron's security tutorial.
+ * https://github.com/electron/electron/blob/v17.2.0/docs/tutorial/security.md#12-verify-webview-options-before-creation
+ */
+export function registerNavigationHandlers(
+ webContents: Pick,
+ settings: Pick,
+ clusterStore: Pick,
+ logger: Logger
+): void {
+ webContents.on('will-navigate', (event, navigationUrl) => {
+ // Allow reloading the renderer app in dev mode.
+ if (settings.dev && new URL(navigationUrl).host === 'localhost:8080') {
+ return;
+ }
+ logger.warn(`Navigation to ${navigationUrl} blocked by 'will-navigate'`);
+ event.preventDefault();
+ });
+
+ // The usage of webview is blocked by default, but let's include the handler just in case.
+ // https://github.com/electron/electron/blob/v17.2.0/docs/api/webview-tag.md#enabling
+ webContents.on('will-attach-webview', (event, _, params) => {
+ logger.warn(
+ `Opening a webview to ${params.src} blocked by 'will-attach-webview'`
+ );
+ event.preventDefault();
+ });
+
+ // Open links in the browser.
+ webContents.setWindowOpenHandler(details => {
+ const url = new URL(details.url);
+
+ if (isUrlSafe(url, clusterStore, logger)) {
+ shell.openExternal(url.toString());
+ } else {
+ logger.warn(
+ `Opening a new window to ${url} blocked by 'setWindowOpenHandler'`
+ );
+ dialog.showErrorBox(
+ 'Cannot open this link',
+ 'The domain does not match any of the allowed domains. Check main.log for more details.'
+ );
+ }
+
+ return { action: 'deny' };
+ });
+}
+
+function isUrlSafe(
+ url: URL,
+ clusterStore: Pick,
+ logger: Logger
+): boolean {
+ if (url.protocol !== 'https:') {
+ return false;
+ }
+ if (url.host === 'goteleport.com') {
+ return true;
+ }
+ if (url.host === 'github.com' && url.pathname.startsWith('/gravitational/')) {
+ return true;
+ }
+
+ const rootClusterProxyHostAllowList = makeRootClusterProxyHostAllowList(
+ clusterStore.getRootClusters(),
+ logger
+ );
+
+ // Allow opening links to the Web UIs of root clusters currently added in the app.
+ if (rootClusterProxyHostAllowList.has(url.host)) {
+ return true;
+ }
+}
+
+/**
+ * Produces a list of proxy and SSO hosts of root clusters. This enables us to
+ * open links to Web UIs of clusters from within Connect.
+ *
+ * The port part of a proxy host is dropped if the port is 443. See `proxyHostToBrowserProxyHost` for
+ * more details.
+ */
+function makeRootClusterProxyHostAllowList(
+ clusters: Cluster[],
+ logger: Logger
+): Set {
+ return new Set(
+ clusters
+ .flatMap(c => {
+ let browserProxyHost: string;
+ if (c.proxyHost) {
+ try {
+ browserProxyHost = proxyHostToBrowserProxyHost(c.proxyHost);
+ } catch (error) {
+ logger.error(
+ 'Ran into an error when converting proxy host to browser proxy host',
+ error
+ );
+ }
+ }
+
+ // Allow the SSO host for SSO login/mfa redirects.
+ let browserSsoHost: string;
+ if (c.ssoHost) {
+ try {
+ browserSsoHost = proxyHostToBrowserProxyHost(c.ssoHost);
+ } catch (error) {
+ logger.error(
+ 'Ran into an error when converting sso host to browser sso host',
+ error
+ );
+ }
+ }
+ return [browserProxyHost, browserSsoHost];
+ })
+ .filter(Boolean)
+ );
+}
diff --git a/web/packages/teleterm/src/mainProcess/profileWatcher/index.ts b/web/packages/teleterm/src/mainProcess/profileWatcher/index.ts
new file mode 100644
index 0000000000000..db5e0897a9dc7
--- /dev/null
+++ b/web/packages/teleterm/src/mainProcess/profileWatcher/index.ts
@@ -0,0 +1,19 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+export * from './profileWatcher';
diff --git a/web/packages/teleterm/src/mainProcess/profileWatcher/profileWatcher.test.ts b/web/packages/teleterm/src/mainProcess/profileWatcher/profileWatcher.test.ts
new file mode 100644
index 0000000000000..404d875fcbbc5
--- /dev/null
+++ b/web/packages/teleterm/src/mainProcess/profileWatcher/profileWatcher.test.ts
@@ -0,0 +1,369 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import fs from 'node:fs/promises';
+import os from 'node:os';
+import path from 'node:path';
+
+import { Cluster } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
+import { wait } from 'shared/utils/wait';
+
+import { makeRootCluster } from 'teleterm/services/tshd/testHelpers';
+import { RootClusterUri, routing } from 'teleterm/ui/uri';
+
+import { watchProfiles } from './profileWatcher';
+
+let tempDir: string;
+
+beforeAll(async () => {
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'profile-watcher-test'));
+});
+
+afterAll(async () => {
+ if (tempDir) {
+ await fs.rm(tempDir, { recursive: true, force: true });
+ }
+});
+
+// Ensure the watcher is stopped when a test fails.
+let abortController: AbortController;
+beforeEach(() => {
+ abortController = new AbortController();
+});
+afterEach(() => {
+ abortController.abort();
+});
+
+function makePerTestDir() {
+ return fs.mkdtemp(path.join(tempDir, 'test'));
+}
+
+const testDebounceMs = 10;
+
+async function mockTshClient(tshDir: string, initial: { clusters: Cluster[] }) {
+ const listRootClusters = async () => {
+ let paths: string[] = [];
+ try {
+ paths = await fs.readdir(tshDir);
+ } catch (err) {
+ if (err.code === 'ENOENT') {
+ throw {
+ name: 'TshdRpcError',
+ code: 'NOT_FOUND',
+ };
+ }
+ throw err;
+ }
+ return Promise.all(
+ paths.map(async singlePath => {
+ const file = await fs.readFile(
+ path.join(tshDir, singlePath, 'cluster.json'),
+ {
+ encoding: 'utf-8',
+ }
+ );
+ return Cluster.fromJsonString(file);
+ })
+ );
+ };
+
+ const insertOrUpdateCluster = async (cluster: Cluster) => {
+ const profileDir = path.join(tshDir, routing.parseClusterName(cluster.uri));
+
+ await fs.mkdir(profileDir, { recursive: true });
+ await fs.writeFile(
+ path.join(profileDir, 'cluster.json'),
+ Cluster.toJsonString(cluster),
+ { encoding: 'utf-8' }
+ );
+ };
+
+ const removeCluster = async (uri: RootClusterUri) => {
+ const profileDir = path.join(tshDir, routing.parseClusterName(uri));
+
+ await fs.rm(profileDir, {
+ recursive: true,
+ });
+ };
+
+ // Set initial state.
+ await Promise.all(
+ initial.clusters.map(cluster => insertOrUpdateCluster(cluster))
+ );
+
+ return {
+ listRootClusters,
+ insertOrUpdateCluster,
+ removeCluster,
+ };
+}
+
+function mockClusterStore(initial: { clusters: Cluster[] }) {
+ return {
+ getRootClusters: () => initial.clusters,
+ clearAll: () => {
+ initial.clusters = [];
+ },
+ };
+}
+
+test('yields an "added" change when new cluster appears', async () => {
+ const tshDir = await makePerTestDir();
+ const tshClientMock = await mockTshClient(tshDir, { clusters: [] });
+ const clusterStoreMock = mockClusterStore({ clusters: [] });
+ const watcher = watchProfiles({
+ tshDirectory: tshDir,
+ tshClient: tshClientMock,
+ clusterStore: clusterStoreMock,
+ signal: abortController.signal,
+ debounceMs: testDebounceMs,
+ });
+
+ const cluster = makeRootCluster();
+ await tshClientMock.insertOrUpdateCluster(cluster);
+
+ const { value } = await watcher.next();
+ expect(value).toEqual([{ op: 'added', cluster }]);
+
+ await watcher.return(undefined);
+});
+
+test('yields a "removed" change when cluster disappears', async () => {
+ const tshDir = await makePerTestDir();
+ const cluster = makeRootCluster();
+ const tshClientMock = await mockTshClient(tshDir, { clusters: [cluster] });
+ const clusterStoreMock = mockClusterStore({ clusters: [cluster] });
+ const watcher = watchProfiles({
+ tshDirectory: tshDir,
+ tshClient: tshClientMock,
+ clusterStore: clusterStoreMock,
+ signal: abortController.signal,
+ debounceMs: testDebounceMs,
+ });
+
+ void tshClientMock.removeCluster(cluster.uri);
+
+ const { value } = await watcher.next();
+ expect(value).toEqual([{ op: 'removed', cluster }]);
+
+ await watcher.return(undefined);
+});
+
+test('yields a "changed" change when cluster properties differ', async () => {
+ const tshDir = await makePerTestDir();
+ const oldCluster = makeRootCluster();
+ const tshClientMock = await mockTshClient(tshDir, { clusters: [oldCluster] });
+ const clusterStoreMock = mockClusterStore({ clusters: [oldCluster] });
+ const watcher = watchProfiles({
+ tshDirectory: tshDir,
+ tshClient: tshClientMock,
+ clusterStore: clusterStoreMock,
+ signal: abortController.signal,
+ debounceMs: testDebounceMs,
+ });
+
+ const newCluster: Cluster = { ...oldCluster, connected: false };
+ void tshClientMock.insertOrUpdateCluster(newCluster);
+
+ const { value } = await watcher.next();
+ expect(value).toEqual([
+ { op: 'changed', previous: oldCluster, next: newCluster },
+ ]);
+
+ await watcher.return(undefined);
+});
+
+test('does not yield when no cluster changes detected', async () => {
+ const tshDir = await makePerTestDir();
+ const cluster = makeRootCluster();
+ const tshClientMock = await mockTshClient(tshDir, {
+ clusters: [
+ // Extend the cluster with properties loaded from the proxy to verify
+ // if they are properly ignored when detecting changes.
+ { ...cluster, authClusterId: 'some-id' },
+ ],
+ });
+ const clusterStoreMock = mockClusterStore({ clusters: [cluster] });
+ const watcher = watchProfiles({
+ tshDirectory: tshDir,
+ tshClient: tshClientMock,
+ clusterStore: clusterStoreMock,
+ signal: abortController.signal,
+ debounceMs: testDebounceMs,
+ });
+
+ // Overwrite the cluster (profile properties are unchanged).
+ void tshClientMock.insertOrUpdateCluster(cluster);
+
+ const race = Promise.race([
+ watcher.next(),
+ // Wait a little longer than the debounce value.
+ wait(testDebounceMs + testDebounceMs / 2).then(() => 'timeout'),
+ ]);
+
+ expect(await race).toBe('timeout');
+ // Cancel the watcher with the abort signal, it's blocked on `watcher.next()`.
+ abortController.abort();
+});
+
+test('file system events are debounced', async () => {
+ const tshDir = await makePerTestDir();
+ const tshClientMock = await mockTshClient(tshDir, { clusters: [] });
+ const clusterStoreMock = mockClusterStore({ clusters: [] });
+ const handler = jest.fn().mockImplementation(() => Promise.resolve());
+ const testDebounceMs = 100;
+ const watcher = watchProfiles({
+ tshDirectory: tshDir,
+ tshClient: tshClientMock,
+ clusterStore: clusterStoreMock,
+ signal: abortController.signal,
+ debounceMs: testDebounceMs,
+ });
+
+ void (async () => {
+ for await (let e of watcher) {
+ await handler(e);
+ }
+ })();
+
+ const cluster = makeRootCluster();
+
+ // Insert two rapid events within debounce interval.
+ await tshClientMock.insertOrUpdateCluster(cluster);
+ await tshClientMock.insertOrUpdateCluster(cluster);
+ // Wait slightly longer than debounce interval to ensure a single handler is called.
+ await wait(2 * testDebounceMs);
+ expect(handler).toHaveBeenCalledTimes(1);
+});
+
+test('no events are lost when handler is slow', async () => {
+ const tshDir = await makePerTestDir();
+ const tshClientMock = await mockTshClient(tshDir, { clusters: [] });
+ const clusterStoreMock = mockClusterStore({ clusters: [] });
+ const handler = jest.fn().mockImplementation(() => wait(2 * testDebounceMs));
+ const watcher = watchProfiles({
+ tshDirectory: tshDir,
+ tshClient: tshClientMock,
+ clusterStore: clusterStoreMock,
+ signal: abortController.signal,
+ debounceMs: testDebounceMs,
+ });
+
+ const cluster = makeRootCluster();
+
+ void (async () => {
+ for await (let e of watcher) {
+ const handle = handler(e);
+ // Insert the second event while the first event is still processed.
+ void tshClientMock.insertOrUpdateCluster(cluster);
+ await handle;
+ }
+ })();
+
+ await tshClientMock.insertOrUpdateCluster(cluster);
+ await expect(() => {
+ return handler.mock.calls.length === 2;
+ }).toEventuallyBeTrue({ waitFor: 1000, tick: 10 });
+});
+
+test('watcher stops when consumer throws', async () => {
+ const tshDir = await makePerTestDir();
+ const tshClientMock = await mockTshClient(tshDir, { clusters: [] });
+ const clusterStoreMock = mockClusterStore({ clusters: [] });
+ const watcher = watchProfiles({
+ tshDirectory: tshDir,
+ tshClient: tshClientMock,
+ clusterStore: clusterStoreMock,
+ signal: abortController.signal,
+ debounceMs: testDebounceMs,
+ });
+
+ await expect(watcher.throw(new Error('Consumer failure'))).rejects.toThrow(
+ 'Consumer failure'
+ );
+
+ await tshClientMock.insertOrUpdateCluster(makeRootCluster());
+
+ const race = await Promise.race([
+ watcher.next(),
+ wait(300).then(() => 'timeout'),
+ ]);
+
+ expect(race).toStrictEqual({ done: true, value: undefined });
+});
+
+test('removing tsh directory does not break watcher', async () => {
+ const tshDir = await makePerTestDir();
+ const cluster = makeRootCluster();
+ const tshClientMock = await mockTshClient(tshDir, { clusters: [] });
+ const clusterStoreMock = mockClusterStore({ clusters: [cluster] });
+ const watcher = watchProfiles({
+ tshDirectory: tshDir,
+ tshClient: tshClientMock,
+ clusterStore: clusterStoreMock,
+ signal: abortController.signal,
+ debounceMs: testDebounceMs,
+ });
+
+ // Start the watcher by pulling the first value.
+ const firstEvent = watcher.next();
+ await fs.rm(tshDir, { recursive: true });
+ expect((await firstEvent).value).toEqual([{ op: 'removed', cluster }]);
+ // Clean up the store, so that we can detect a change.
+ clusterStoreMock.clearAll();
+
+ await fs.mkdir(tshDir);
+ await tshClientMock.insertOrUpdateCluster(cluster);
+ // The second event needs to wait for the dir to be detected.
+ jest.useFakeTimers();
+ const secondEvent = watcher.next();
+ // Polling uses 1 second interval.
+ jest.advanceTimersByTime(1000);
+ jest.useRealTimers();
+ expect((await secondEvent).value).toEqual([{ op: 'added', cluster }]);
+});
+
+test('max file system events count is restricted', async () => {
+ const tshDir = await makePerTestDir();
+ const cluster = makeRootCluster();
+ const tshClientMock = await mockTshClient(tshDir, { clusters: [] });
+ const clusterStoreMock = mockClusterStore({ clusters: [] });
+ const watcher = watchProfiles({
+ tshDirectory: tshDir,
+ tshClient: tshClientMock,
+ clusterStore: clusterStoreMock,
+ signal: abortController.signal,
+ debounceMs: 100,
+ maxFileSystemEvents: 1,
+ });
+
+ const promises = await Promise.allSettled([
+ // Start the watcher by pulling the first value.
+ watcher.next(),
+ // Makes two updates.
+ tshClientMock.insertOrUpdateCluster(cluster),
+ ]);
+
+ expect(promises[0]).toEqual({
+ status: 'rejected',
+ reason: expect.objectContaining({
+ message:
+ 'Exceeded file system event limit: more than 1 events detected within 100 ms',
+ }),
+ });
+});
diff --git a/web/packages/teleterm/src/mainProcess/profileWatcher/profileWatcher.ts b/web/packages/teleterm/src/mainProcess/profileWatcher/profileWatcher.ts
new file mode 100644
index 0000000000000..8b09588521c7a
--- /dev/null
+++ b/web/packages/teleterm/src/mainProcess/profileWatcher/profileWatcher.ts
@@ -0,0 +1,282 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { watch } from 'node:fs';
+import { access } from 'node:fs/promises';
+
+import { Cluster } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
+import { debounce } from 'shared/utils/highbar';
+import { wait } from 'shared/utils/wait';
+
+import { isTshdRpcError } from 'teleterm/services/tshd';
+import { mergeClusterProfileWithDetails } from 'teleterm/services/tshd/cluster';
+import { RootClusterUri } from 'teleterm/ui/uri';
+
+interface TshClient {
+ listRootClusters(): Promise;
+}
+
+interface ClusterStore {
+ getRootClusters(): Cluster[];
+}
+
+/**
+ * Watches the specified `tshDirectory` for profile changes.
+ * File system events are debounced with a default 200 ms delay.
+ * The watcher should be started only after the initial cluster store sync completes,
+ * to prevent unnecessary profile change events.
+ *
+ * When the watched directory is removed, the watcher emits a profile change
+ * and enters polling mode (with 1 second interval) until the directory reappears.
+ */
+export async function* watchProfiles({
+ tshDirectory,
+ tshClient,
+ clusterStore,
+ debounceMs = 200,
+ maxFileSystemEvents = 4096,
+ signal,
+}: {
+ tshDirectory: string;
+ tshClient: TshClient;
+ clusterStore: ClusterStore;
+ debounceMs?: number;
+ /**
+ * Maximum number of file system events that can accumulate while debouncing.
+ *
+ * Note: On Windows, removing a watched directory can trigger an infinite stream
+ * of events. Setting this limit helps mitigate that issue.
+ *
+ * Default 4096.
+ * */
+ maxFileSystemEvents?: number;
+ signal?: AbortSignal;
+}): AsyncGenerator {
+ while (!signal?.aborted) {
+ try {
+ // eslint-disable-next-line unused-imports/no-unused-vars
+ for await (const _ of debounceWatch(
+ tshDirectory,
+ debounceMs,
+ maxFileSystemEvents,
+ signal
+ )) {
+ const clusters = await tshClient.listRootClusters();
+ const newClusters = new Map(clusters.map(c => [c.uri, c]));
+ const oldClusters = new Map(
+ clusterStore.getRootClusters().map(c => [c.uri, c])
+ );
+
+ const changes = detectChanges(oldClusters, newClusters);
+ if (changes.length > 0) {
+ yield changes;
+ }
+ }
+ } catch (error) {
+ // Check if the error is caused by removing the watched directory.
+ // Removing that directory emits different events, depending on a platform:
+ // - On macOS/Linux, it emits a 'rename' event.
+ // - On Windows, it may throw an EPERM error, or emit thousands of events
+ // (so that we check FileSystemEventsOverflowError).
+ // To reliably detect removal on macOS/Linux, we expect tshClient.listRootClusters()
+ // to fail with a filesystem-related error, allowing us to catch all relevant cases here.
+ if (
+ isTshdRpcError(error, 'NOT_FOUND') ||
+ error instanceof FileSystemEventsOverflowError ||
+ error?.code === 'EPERM'
+ ) {
+ const ok = await pathExists(tshDirectory);
+ if (!ok) {
+ yield clusterStore
+ .getRootClusters()
+ .map(cluster => ({ op: 'removed', cluster }));
+ await waitForPath(tshDirectory, signal);
+ continue;
+ }
+ }
+ throw error;
+ }
+ }
+}
+
+class FileSystemEventsOverflowError extends Error {
+ constructor(maxCount: number, debounceMs: number) {
+ super(
+ `Exceeded file system event limit: more than ${maxCount} events detected within ${debounceMs} ms`
+ );
+ }
+}
+
+async function pathExists(dirPath: string): Promise {
+ try {
+ await access(dirPath);
+ return true;
+ } catch (error) {
+ if (error.code === 'ENOENT') {
+ return false;
+ }
+ throw error;
+ }
+}
+
+/** Waits for path to exists, polling at intervals (1 second). */
+async function waitForPath(
+ dirPath: string,
+ signal?: AbortSignal
+): Promise {
+ if (signal?.aborted) {
+ return;
+ }
+
+ while (!signal?.aborted) {
+ // Start from waiting, pathExists() was invoked earlier to check the path.
+ await wait(1000, signal);
+ const exist = await pathExists(dirPath);
+ if (exist) {
+ return;
+ }
+ }
+}
+
+export type ProfileChange =
+ | {
+ /** A cluster has been added. */
+ op: 'added';
+ cluster: Cluster;
+ }
+ | {
+ /** A cluster has been removed. */
+ op: 'removed';
+ cluster: Cluster;
+ }
+ | {
+ /**
+ * A cluster's properties have changed.
+ * Only the properties present locally in the profile are compared.
+ * (`listRootClusters` doesn't return cluster details from the proxy).
+ */
+ op: 'changed';
+ previous: Cluster;
+ next: Cluster;
+ };
+
+export type ProfileChangeSet = ProfileChange[];
+
+async function* debounceWatch(
+ path: string,
+ debounceMs: number,
+ maxFileSystemEvents: number,
+ abortSignal: AbortSignal | undefined
+): AsyncGenerator {
+ let signal = Promise.withResolvers();
+ let closed = false;
+ let eventsToDebounce = 0;
+ const scheduleYield = debounce(() => {
+ eventsToDebounce = 0;
+ signal.resolve();
+ }, debounceMs);
+ const onEvent = () => {
+ ++eventsToDebounce;
+ if (eventsToDebounce > maxFileSystemEvents) {
+ signal.reject(
+ new FileSystemEventsOverflowError(maxFileSystemEvents, debounceMs)
+ );
+ return;
+ }
+ scheduleYield();
+ };
+
+ const watcher = watch(
+ path,
+ { signal: abortSignal, recursive: true },
+ onEvent
+ );
+
+ const closeHandler = () => {
+ closed = true;
+ signal.resolve();
+ };
+ const errorHandler = (e: Error) => signal.reject(e);
+ watcher.on('close', closeHandler);
+ watcher.on('error', errorHandler);
+
+ // The watcher might be restarted if the path disappears and then reappears.
+ // Begin by checking for any changes immediately.
+ onEvent();
+
+ try {
+ while (true) {
+ await signal.promise;
+ if (closed) {
+ break;
+ }
+
+ // Recreate the signal so any events occurring while yielding will resolve the next promise.
+ signal = Promise.withResolvers();
+
+ yield;
+ }
+ } finally {
+ scheduleYield.cancel();
+ watcher.close();
+ watcher.off('close', closeHandler);
+ watcher.off('error', errorHandler);
+ }
+}
+
+function detectChanges(
+ previousClusters: Map,
+ nextClusters: Map
+): ProfileChange[] {
+ const changes: ProfileChange[] = [];
+ const allUris = new Set([...previousClusters.keys(), ...nextClusters.keys()]);
+
+ for (const uri of allUris) {
+ const previous = previousClusters.get(uri);
+ const next = nextClusters.get(uri);
+
+ if (!nextClusters.has(uri)) {
+ changes.push({
+ op: 'removed',
+ cluster: previous,
+ });
+ } else if (!previousClusters.has(uri)) {
+ changes.push({ op: 'added', cluster: next });
+ } else if (
+ // Ensure we are comparing only profile properties.
+ !Cluster.equals(
+ mergeClusterProfileWithDetails({
+ profile: previous,
+ details: Cluster.create(),
+ }),
+ mergeClusterProfileWithDetails({
+ profile: next,
+ details: Cluster.create(),
+ })
+ )
+ ) {
+ changes.push({
+ op: 'changed',
+ previous: previous,
+ next: next,
+ });
+ }
+ }
+
+ return changes;
+}
diff --git a/web/packages/teleterm/src/mainProcess/rootClusterProxyHostAllowList.ts b/web/packages/teleterm/src/mainProcess/rootClusterProxyHostAllowList.ts
deleted file mode 100644
index 9551237e0bbdb..0000000000000
--- a/web/packages/teleterm/src/mainProcess/rootClusterProxyHostAllowList.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-/**
- * Teleport
- * Copyright (C) 2024 Gravitational, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-
-import { ipcMain } from 'electron';
-
-import { isAbortError } from 'shared/utils/abortError';
-
-import { MainProcessIpc } from 'teleterm/mainProcess/types';
-import { TshdClient } from 'teleterm/services/tshd';
-import { cloneAbortSignal } from 'teleterm/services/tshd/cloneableClient';
-import { proxyHostToBrowserProxyHost } from 'teleterm/services/tshd/cluster';
-import * as tshd from 'teleterm/services/tshd/types';
-import { Logger } from 'teleterm/types';
-
-export type RootClusterProxyHostAllowList = Set;
-
-/**
- * Refreshes the allow list whenever the renderer process notifies the main process that the list of
- * clusters in ClustersService got updated.
- *
- * The allow list includes proxy hosts of root clusters. This enables us to open links to Web UIs of
- * clusters from within Connect.
- *
- * The port part of a proxy host is dropped if the port is 443. See proxyHostToBrowserProxyHost for
- * more details.
- */
-export function manageRootClusterProxyHostAllowList({
- tshdClient,
- logger,
- allowList,
-}: {
- tshdClient: TshdClient;
- logger: Logger;
- allowList: RootClusterProxyHostAllowList;
-}) {
- let abortController: AbortController;
-
- const refreshAllowList = async () => {
- // Allow only one call to be in progress. This ensures that on subsequent calls to
- // refreshAllowList, we always store only the most recent version of the list.
- abortController?.abort();
- abortController = new AbortController();
-
- let rootClusters: tshd.Cluster[];
- try {
- const { response } = await tshdClient.listRootClusters(
- {},
- { abort: cloneAbortSignal(abortController.signal) }
- );
- rootClusters = response.clusters;
- } catch (error) {
- if (isAbortError(error)) {
- // Ignore abort errors. They will be logged by the gRPC client middleware.
- return;
- }
-
- logger.error('Could not fetch root clusters', error);
- // Return instead of throwing as there's nothing else we can do with the error at this place
- // in the program.
- return;
- }
-
- allowList.clear();
- for (const rootCluster of rootClusters) {
- if (rootCluster.proxyHost) {
- let browserProxyHost: string;
- try {
- browserProxyHost = proxyHostToBrowserProxyHost(rootCluster.proxyHost);
- allowList.add(browserProxyHost);
- } catch (error) {
- logger.error(
- 'Ran into an error when converting proxy host to browser proxy host',
- error
- );
- }
- }
-
- // Allow the SSO host for SSO login/mfa redirects.
- if (rootCluster.ssoHost) {
- let browserSsoHost: string;
- try {
- browserSsoHost = proxyHostToBrowserProxyHost(rootCluster.ssoHost);
- allowList.add(browserSsoHost);
- } catch (error) {
- logger.error(
- 'Ran into an error when converting sso host to browser sso host',
- error
- );
- }
- }
- }
- };
-
- refreshAllowList();
-
- ipcMain.on(MainProcessIpc.RefreshClusterList, () => {
- refreshAllowList();
- });
-}
diff --git a/web/packages/teleterm/src/mainProcess/runtimeSettings.ts b/web/packages/teleterm/src/mainProcess/runtimeSettings.ts
index e159d551e2edc..843d6c76ebe15 100644
--- a/web/packages/teleterm/src/mainProcess/runtimeSettings.ts
+++ b/web/packages/teleterm/src/mainProcess/runtimeSettings.ts
@@ -79,7 +79,7 @@ export async function getRuntimeSettings(): Promise {
const tshd = {
binaryPath: tshBinPath,
- homeDir: getTshHomeDir(),
+ defaultHomeDir: path.resolve(app.getPath('home'), '.tsh'),
requestedNetworkAddress: tshAddress,
};
const sharedProcess = {
@@ -147,14 +147,6 @@ function getKubeConfigsDir(): string {
return kubeConfigsPath;
}
-function getTshHomeDir() {
- const tshPath = path.resolve(app.getPath('userData'), 'tsh');
- if (!fs.existsSync(tshPath)) {
- fs.mkdirSync(tshPath);
- }
- return tshPath;
-}
-
// binDir is used in the packaged version to add tsh to PATH.
// tshBinPath is used by Connect to call tsh directly.
function getBinaryPaths(): { binDir?: string; tshBinPath: string } {
diff --git a/web/packages/teleterm/src/mainProcess/types.ts b/web/packages/teleterm/src/mainProcess/types.ts
index e5749c8183335..439ea54ab21bd 100644
--- a/web/packages/teleterm/src/mainProcess/types.ts
+++ b/web/packages/teleterm/src/mainProcess/types.ts
@@ -16,7 +16,14 @@
* along with this program. If not, see .
*/
+import { Cluster } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
+
import { DeepLinkParseResult } from 'teleterm/deepLinks';
+import type {
+ ClusterLifecycleEvent,
+ ProfileWatcherError,
+} from 'teleterm/mainProcess/clusterLifecycleManager';
+import type { ClusterStoreUpdate } from 'teleterm/mainProcess/clusterStore';
import { CreateAgentConfigFileArgs } from 'teleterm/mainProcess/createAgentConfigFile';
import { AppUpdateEvent } from 'teleterm/services/appUpdater';
import { FileStorage } from 'teleterm/services/fileStorage';
@@ -69,7 +76,7 @@ export type RuntimeSettings = {
tshd: {
requestedNetworkAddress: string;
binaryPath: string;
- homeDir: string;
+ defaultHomeDir: string;
};
sharedProcess: {
requestedNetworkAddress: string;
@@ -139,10 +146,6 @@ export type MainProcessClient = {
}>;
configService: ConfigService;
fileStorage: FileStorage;
- removeKubeConfig(options: {
- relativePath: string;
- isDirectory?: boolean;
- }): Promise;
/**
* Tells the OS to focus the window. If wait is true, polls periodically for window status and
* resolves when it's focused or after a short timeout.
@@ -195,7 +198,6 @@ export type MainProcessClient = {
* interacted with the relevant modals during startup and is free to use the app.
*/
signalUserInterfaceReadiness(args: { success: boolean }): void;
- refreshClusterList(): void;
/**
* Opens the Electron directory picker and sends the selected path to tshd through SetSharedDirectoryForDesktopSession.
* tshd then verifies whether there is an active session for the specified desktop user and attempts to open the directory.
@@ -210,9 +212,6 @@ export type MainProcessClient = {
changeAppUpdatesManagingCluster(
clusterUri: RootClusterUri | undefined
): Promise;
- maybeRemoveAppUpdatesManagingCluster(
- clusterUri: RootClusterUri
- ): Promise;
supportsAppUpdates(): boolean;
checkForAppUpdates(): Promise;
downloadAppUpdate(): Promise;
@@ -229,6 +228,23 @@ export type MainProcessClient = {
): {
cleanup: () => void;
};
+ addCluster(proxyAddress: string): Promise;
+ syncCluster(clusterUri: RootClusterUri): Promise;
+ syncRootClusters(): Promise;
+ logout(clusterUri: RootClusterUri): Promise;
+ subscribeToClusterStore(listener: (value: ClusterStoreUpdate) => void): {
+ cleanup: () => void;
+ };
+ registerClusterLifecycleHandler(
+ listener: (event: ClusterLifecycleEvent) => Promise
+ ): {
+ cleanup: () => void;
+ };
+ subscribeToProfileWatcherErrors(
+ listener: (args: ProfileWatcherError) => void
+ ): {
+ cleanup: () => void;
+ };
};
export type ChildProcessAddresses = {
@@ -337,12 +353,12 @@ export enum RendererIpc {
OpenAppUpdateDialog = 'renderer-open-app-update-dialog',
AppUpdateEvent = 'renderer-app-update-event',
IsInBackgroundMode = 'renderer-is-in-background-mode',
+ ProfileWatcherError = 'renderer-profile-watcher-error',
}
export enum MainProcessIpc {
GetRuntimeSettings = 'main-process-get-runtime-settings',
TryRemoveConnectMyComputerAgentBinary = 'main-process-try-remove-connect-my-computer-agent-binary',
- RefreshClusterList = 'main-process-refresh-cluster-list',
DownloadConnectMyComputerAgent = 'main-process-connect-my-computer-download-agent',
VerifyConnectMyComputerAgent = 'main-process-connect-my-computer-verify-agent',
SaveTextToFile = 'main-process-save-text-to-file',
@@ -353,8 +369,13 @@ export enum MainProcessIpc {
CancelAppUpdateDownload = 'main-process-cancel-app-update-download',
QuiteAndInstallAppUpdate = 'main-process-quit-and-install-app-update',
ChangeAppUpdatesManagingCluster = 'main-process-change-app-updates-managing-cluster',
- MaybeRemoveAppUpdatesManagingCluster = 'main-process-maybe-remove-app-updates-managing-cluster',
SupportsAppUpdates = 'main-process-supports-app-updates',
+ InitClusterStoreSubscription = 'main-process-init-cluster-store-subscription',
+ SyncCluster = 'main-process-sync-cluster',
+ AddCluster = 'main-process-add-cluster',
+ SyncRootClusters = 'main-process-sync-root-clusters',
+ Logout = 'main-process-logout',
+ RegisterClusterLifecycleHandler = 'main-process-register-cluster-lifecycle-handler',
}
export enum WindowsManagerIpc {
diff --git a/web/packages/teleterm/src/mainProcess/windowsManager.ts b/web/packages/teleterm/src/mainProcess/windowsManager.ts
index c68d44d24ecc5..9d5d335e0cb4b 100644
--- a/web/packages/teleterm/src/mainProcess/windowsManager.ts
+++ b/web/packages/teleterm/src/mainProcess/windowsManager.ts
@@ -31,6 +31,8 @@ import {
screen,
} from 'electron';
+import { ensureError } from 'shared/utils/error';
+
import { DeepLinkParseResult } from 'teleterm/deepLinks';
import Logger from 'teleterm/logger';
import {
@@ -69,6 +71,7 @@ export class WindowsManager {
* by the OS (e.g. Command+H).
*/
private isInBackgroundMode: boolean;
+ private crashWindowPromise: Promise;
constructor(
private fileStorage: FileStorage,
@@ -516,6 +519,39 @@ export class WindowsManager {
return keepRunning;
}
+ /**
+ * Displays an error in a system dialog and offers reloading the window
+ * or quitting the app.
+ */
+ async crashWindow(error: unknown): Promise {
+ if (this.crashWindowPromise) {
+ return this.crashWindowPromise;
+ }
+ this.crashWindowPromise = this.doCrashWindow(error);
+ try {
+ await this.crashWindowPromise;
+ } finally {
+ this.crashWindowPromise = undefined;
+ }
+ }
+
+ private async doCrashWindow(error: unknown): Promise {
+ this.logger.error('Window crashed', error);
+ const { response } = await dialog.showMessageBox(this.window, {
+ type: 'error',
+ message: 'Teleport Connect has crashed',
+ detail: ensureError(error).message,
+ buttons: ['Reload Window', 'Quit'],
+ defaultId: 0,
+ noLink: true,
+ });
+ if (response === 0) {
+ this.window.reload();
+ } else {
+ app.quit();
+ }
+ }
+
private isWindowUsable(): boolean {
return this.window && !this.window.isDestroyed();
}
diff --git a/web/packages/teleterm/src/services/config/appConfigSchema.ts b/web/packages/teleterm/src/services/config/appConfigSchema.ts
index d453dbdd8422f..e7600ae054886 100644
--- a/web/packages/teleterm/src/services/config/appConfigSchema.ts
+++ b/web/packages/teleterm/src/services/config/appConfigSchema.ts
@@ -70,6 +70,10 @@ export const createAppConfigSchema = (settings: RuntimeSettings) => {
.describe(
'Keeps the app running in the menu bar/system tray even when the main window is closed. On Linux, displaying the system tray icon may require installing shell extensions.'
),
+ tshHome: z
+ .string()
+ .default(settings.tshd.defaultHomeDir)
+ .describe('Home location for tsh configuration and data.'),
/**
* This value can be provided by the user and is unsanitized. This means that it cannot be directly interpolated
* in a styled component or used in CSS, as it may inject malicious CSS code.
diff --git a/web/packages/teleterm/src/services/fileStorage/fileStorageClient.ts b/web/packages/teleterm/src/services/fileStorage/fileStorageClient.ts
index c9820a6cb4bbc..c39f15808c75e 100644
--- a/web/packages/teleterm/src/services/fileStorage/fileStorageClient.ts
+++ b/web/packages/teleterm/src/services/fileStorage/fileStorageClient.ts
@@ -24,6 +24,8 @@ import {
} from '../../mainProcess/types';
import { FileStorage } from './fileStorage';
+const APP_STATE_FIELDS_MODIFIABLE_FROM_RENDERER = ['state'];
+
// TODO(ravicious): The main process should not expose the whole interface of FileStorage to the
// renderer, only what's absolutely needed by the renderer. FileStorage at the moment includes a
// bunch of functions that are used only in the main process (and should be used only there).
@@ -36,11 +38,18 @@ export function subscribeToFileStorageEvents(configService: FileStorage): void {
case FileStorageEventType.Get:
return (event.returnValue = configService.get(item.key));
case FileStorageEventType.Put:
+ if (!APP_STATE_FIELDS_MODIFIABLE_FROM_RENDERER.includes(item.key)) {
+ throw new Error(
+ `Could not update "${item.key}". This field is readonly in the renderer process.`
+ );
+ }
return configService.put(item.key, item.json);
case FileStorageEventType.Write:
return configService.write();
case FileStorageEventType.Replace:
- return configService.replace(item.json);
+ throw new Error(
+ 'Replacing state is not allowed in the renderer process.'
+ );
case FileStorageEventType.GetFilePath:
return configService.getFilePath();
case FileStorageEventType.GetFileName:
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 4dbab055667d2..a58d1b37c9af9 100644
--- a/web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.test.ts
+++ b/web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.test.ts
@@ -67,6 +67,7 @@ describe('getPtyProcessOptions', () => {
customShellPath: '',
ssh: makeSshOptions(),
windowsPty: { useConpty: true },
+ tshHome: '',
},
cmd: cmd,
env: processEnv,
@@ -102,6 +103,7 @@ describe('getPtyProcessOptions', () => {
customShellPath: '',
ssh: makeSshOptions(),
windowsPty: { useConpty: true },
+ tshHome: '',
},
cmd: cmd,
env: processEnv,
@@ -134,6 +136,7 @@ describe('getPtyProcessOptions', () => {
customShellPath: '',
ssh: makeSshOptions({ noResume: true }),
windowsPty: { useConpty: true },
+ tshHome: '',
},
cmd: cmd,
env: processEnv,
@@ -164,6 +167,7 @@ describe('getPtyProcessOptions', () => {
customShellPath: '',
ssh: makeSshOptions({ forwardAgent: true }),
windowsPty: { useConpty: true },
+ tshHome: '',
},
cmd: cmd,
env: processEnv,
@@ -194,6 +198,7 @@ describe('getPtyProcessOptions', () => {
customShellPath: '',
ssh: makeSshOptions({ forwardAgent: false }),
windowsPty: { useConpty: true },
+ tshHome: '',
},
cmd: cmd,
env: processEnv,
@@ -229,6 +234,7 @@ describe('buildPtyOptions', () => {
customShellPath: '',
ssh: makeSshOptions(),
windowsPty: { useConpty: true },
+ tshHome: '',
},
cmd,
});
@@ -256,6 +262,7 @@ describe('buildPtyOptions', () => {
customShellPath: '/custom/shell/path/better-shell',
ssh: makeSshOptions(),
windowsPty: { useConpty: true },
+ tshHome: '',
},
cmd,
});
@@ -283,6 +290,7 @@ describe('buildPtyOptions', () => {
customShellPath: '',
ssh: makeSshOptions(),
windowsPty: { useConpty: true },
+ tshHome: '',
},
cmd,
});
@@ -320,6 +328,7 @@ describe('buildPtyOptions', () => {
customShellPath: '',
ssh: makeSshOptions(),
windowsPty: { useConpty: true },
+ tshHome: '',
},
cmd,
processEnv: {
diff --git a/web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.ts b/web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.ts
index 90077b8359727..a8d71a5444b7d 100644
--- a/web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.ts
+++ b/web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.ts
@@ -44,6 +44,7 @@ type PtyOptions = {
ssh: SshOptions;
windowsPty: Pick;
customShellPath: string;
+ tshHome: string;
};
const WSLENV_VAR = 'WSLENV';
@@ -101,7 +102,7 @@ export async function buildPtyOptions({
...shellEnv,
TERM_PROGRAM: 'Teleport_Connect',
TERM_PROGRAM_VERSION: settings.appVersion,
- TELEPORT_HOME: settings.tshd.homeDir,
+ TELEPORT_HOME: options.tshHome,
TELEPORT_CLUSTER: cmd.clusterName,
TELEPORT_PROXY: cmd.proxyHost,
[TSH_AUTOUPDATE_ENV_VAR]: TSH_AUTOUPDATE_OFF,
diff --git a/web/packages/teleterm/src/services/pty/ptyService.ts b/web/packages/teleterm/src/services/pty/ptyService.ts
index 6fadcfe1fe305..1e25c31d584d2 100644
--- a/web/packages/teleterm/src/services/pty/ptyService.ts
+++ b/web/packages/teleterm/src/services/pty/ptyService.ts
@@ -48,6 +48,7 @@ export function createPtyService(
forwardAgent: configService.get('ssh.forwardAgent').value,
},
customShellPath: configService.get('terminal.customShell').value,
+ tshHome: configService.get('tshHome').value,
windowsPty,
},
cmd: command,
diff --git a/web/packages/teleterm/src/services/tshd/cluster.ts b/web/packages/teleterm/src/services/tshd/cluster.ts
index 4ca67b44565e6..0a78c04011603 100644
--- a/web/packages/teleterm/src/services/tshd/cluster.ts
+++ b/web/packages/teleterm/src/services/tshd/cluster.ts
@@ -18,6 +18,12 @@
import * as whatwg from 'whatwg-url';
+import { TrustedDeviceRequirement } from 'gen-proto-ts/teleport/legacy/types/trusted_device_requirement_pb';
+import {
+ Cluster,
+ LoggedInUser_UserType,
+} from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
+
/**
* Accepts a proxy host in the form of "cluster-address.example.com:3090" and returns the host as
* understood by browsers.
@@ -65,3 +71,47 @@ export function proxyHostname(proxyHost: string) {
return whatwgURL.hostname;
}
+
+/**
+ * Creates a cluster by merging properties that can be read from
+ * the profile from disk with properties from details.
+ *
+ * Listing all fields ensures TypeScript will force us to handle new fields
+ * by specifying whether they come from the profile or the details.
+ */
+export function mergeClusterProfileWithDetails({
+ profile,
+ details,
+}: {
+ profile: Cluster;
+ details: Cluster;
+}): Cluster {
+ return {
+ uri: profile.uri,
+ connected: profile.connected,
+ leaf: profile.leaf,
+ profileStatusError: profile.profileStatusError,
+ proxyHost: profile.proxyHost,
+ ssoHost: profile.ssoHost,
+ name: details.name,
+ showResources: details.showResources,
+ features: details.features,
+ authClusterId: details.authClusterId,
+ proxyVersion: details.proxyVersion,
+ loggedInUser: profile.loggedInUser && {
+ name: profile.loggedInUser.name,
+ activeRequests: profile.loggedInUser.activeRequests,
+ roles: profile.loggedInUser.roles,
+ isDeviceTrusted: profile.loggedInUser.isDeviceTrusted,
+ validUntil: profile.loggedInUser.validUntil,
+ userType:
+ details.loggedInUser?.userType || LoggedInUser_UserType.UNSPECIFIED,
+ trustedDeviceRequirement:
+ details.loggedInUser?.trustedDeviceRequirement ||
+ TrustedDeviceRequirement.UNSPECIFIED,
+ requestableRoles: details.loggedInUser?.requestableRoles || [],
+ suggestedReviewers: details.loggedInUser?.suggestedReviewers || [],
+ acl: details.loggedInUser?.acl || undefined,
+ },
+ };
+}
diff --git a/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts b/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts
index 76be4dbf51c57..382f7bb109d2a 100644
--- a/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts
+++ b/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts
@@ -71,10 +71,10 @@ export class MockTshClient implements TshdClient {
localConnectorName: '',
clientVersionStatus: ClientVersionStatus.OK,
});
- removeCluster = () => new MockedUnaryCall({});
login = () => new MockedUnaryCall({});
loginPasswordless = undefined;
logout = () => new MockedUnaryCall({});
+ clearStaleClusterClients = () => new MockedUnaryCall({});
transferFile = undefined;
reportUsageEvent = () => new MockedUnaryCall({});
createConnectMyComputerRole = () =>
diff --git a/web/packages/teleterm/src/services/tshd/testHelpers.ts b/web/packages/teleterm/src/services/tshd/testHelpers.ts
index 476c992ee5f35..9cfb2fe025e2e 100644
--- a/web/packages/teleterm/src/services/tshd/testHelpers.ts
+++ b/web/packages/teleterm/src/services/tshd/testHelpers.ts
@@ -16,6 +16,7 @@
* along with this program. If not, see .
*/
+import { Timestamp } from 'gen-proto-ts/google/protobuf/timestamp_pb';
import { TrustedDeviceRequirement } from 'gen-proto-ts/teleport/legacy/types/trusted_device_requirement_pb';
import { App } from 'gen-proto-ts/teleport/lib/teleterm/v1/app_pb';
import {
@@ -24,6 +25,7 @@ import {
} from 'gen-proto-ts/teleport/lib/teleterm/v1/auth_settings_pb';
import {
ACL,
+ LoggedInUser_UserType,
ShowResources,
} from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
import { WindowsDesktop } from 'gen-proto-ts/teleport/lib/teleterm/v1/windows_desktop_pb';
@@ -259,7 +261,8 @@ export const makeLoggedInUser = (
roles: [],
requestableRoles: [],
suggestedReviewers: [],
- userType: tsh.LoggedInUser_UserType.LOCAL,
+ userType: LoggedInUser_UserType.LOCAL,
+ validUntil: Timestamp.fromDate(new Date()),
...props,
});
diff --git a/web/packages/teleterm/src/ui/AccessRequests/SelectorMenu.test.tsx b/web/packages/teleterm/src/ui/AccessRequests/SelectorMenu.test.tsx
index 9193a52a10b9e..1af297ba33312 100644
--- a/web/packages/teleterm/src/ui/AccessRequests/SelectorMenu.test.tsx
+++ b/web/packages/teleterm/src/ui/AccessRequests/SelectorMenu.test.tsx
@@ -18,7 +18,6 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import { useEffect } from 'react';
import { render } from 'design/utils/testing';
@@ -29,27 +28,35 @@ import {
makeRootCluster,
} from 'teleterm/services/tshd/testHelpers';
import { SelectorMenu } from 'teleterm/ui/AccessRequests/SelectorMenu';
-import {
- ResourcesContextProvider,
- useResourcesContext,
-} from 'teleterm/ui/DocumentCluster/resourcesContext';
+import { ResourcesContextProvider } from 'teleterm/ui/DocumentCluster/resourcesContext';
import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider';
import { MockAppContext } from 'teleterm/ui/fixtures/mocks';
import { MockWorkspaceContextProvider } from 'teleterm/ui/fixtures/MockWorkspaceContextProvider';
-import { RootClusterUri } from 'teleterm/ui/uri';
import { AccessRequestsContextProvider } from './AccessRequestsContext';
-test('assuming or dropping a request refreshes resources', async () => {
+test('assuming or dropping a request calls API', async () => {
const appContext = new MockAppContext();
const accessRequest = makeAccessRequest();
const cluster = makeRootCluster({
features: { advancedAccessWorkflows: true, isUsageBasedBilling: false },
- loggedInUser: makeLoggedInUser({ activeRequests: [accessRequest.id] }),
});
appContext.addRootCluster(cluster);
jest.spyOn(appContext.clustersService, 'dropRoles');
- const refreshListener = jest.fn();
+ jest
+ .spyOn(appContext.clustersService, 'assumeRoles')
+ .mockImplementation(async () => {
+ appContext.clustersService.setState(state => {
+ state.clusters.get(cluster.uri).loggedInUser.activeRequests = [
+ accessRequest.id,
+ ];
+ });
+ });
+ appContext.tshd.getAccessRequests = () => {
+ return new MockedUnaryCall({
+ requests: [accessRequest],
+ });
+ };
appContext.tshd.getAccessRequest = () => {
return new MockedUnaryCall({
request: accessRequest,
@@ -61,10 +68,6 @@ test('assuming or dropping a request refreshes resources', async () => {
-
@@ -77,12 +80,18 @@ test('assuming or dropping a request refreshes resources', async () => {
const item = await screen.findByText(accessRequest.resources.at(0).id.name);
await userEvent.click(item);
+ expect(appContext.clustersService.assumeRoles).toHaveBeenCalledTimes(1);
+ expect(appContext.clustersService.assumeRoles).toHaveBeenCalledWith(
+ cluster.uri,
+ [accessRequest.id]
+ );
+ expect(await screen.findByText(/access assumed/i)).toBeInTheDocument();
+ await userEvent.click(item);
expect(appContext.clustersService.dropRoles).toHaveBeenCalledTimes(1);
expect(appContext.clustersService.dropRoles).toHaveBeenCalledWith(
cluster.uri,
[accessRequest.id]
);
- expect(refreshListener).toHaveBeenCalledTimes(1);
});
test('assumed request are always visible, even if getAccessRequests no longer returns them', async () => {
@@ -139,17 +148,3 @@ test('assumed request are always visible, even if getAccessRequests no longer re
await screen.findByText('request-with-details-not-available')
).toBeInTheDocument();
});
-
-function RequestRefreshSubscriber(props: {
- rootClusterUri: RootClusterUri;
- onResourcesRefreshRequest: () => void;
-}) {
- const { onResourcesRefreshRequest } = useResourcesContext(
- props.rootClusterUri
- );
- useEffect(() => {
- onResourcesRefreshRequest(props.onResourcesRefreshRequest);
- }, [onResourcesRefreshRequest, props.onResourcesRefreshRequest]);
-
- return null;
-}
diff --git a/web/packages/teleterm/src/ui/AccessRequests/SelectorMenu.tsx b/web/packages/teleterm/src/ui/AccessRequests/SelectorMenu.tsx
index ffd9c1a72d5af..96d4a2b55246e 100644
--- a/web/packages/teleterm/src/ui/AccessRequests/SelectorMenu.tsx
+++ b/web/packages/teleterm/src/ui/AccessRequests/SelectorMenu.tsx
@@ -44,7 +44,6 @@ import {
MenuListItem,
Separator,
} from 'teleterm/ui/components/Menu';
-import { useResourcesContext } from 'teleterm/ui/DocumentCluster/resourcesContext';
import { useWorkspaceContext } from 'teleterm/ui/Documents';
import { useLoggedInUser } from 'teleterm/ui/hooks/useLoggedInUser';
import { TopBarButton } from 'teleterm/ui/TopBar/TopBarButton';
@@ -83,7 +82,6 @@ export function SelectorMenu() {
const ctx = useAppContext();
const { clustersService } = ctx;
const selectorRef = useRef(null);
- const { requestResourcesRefresh } = useResourcesContext(rootClusterUri);
const loggedInUser = useLoggedInUser();
const username = loggedInUser?.name;
@@ -210,7 +208,6 @@ export function SelectorMenu() {
await clustersService.assumeRoles(rootClusterUri, [requestId]);
}
});
- requestResourcesRefresh();
}
const isResourceRequestAssumed = sortedRequests
diff --git a/web/packages/teleterm/src/ui/App.test.tsx b/web/packages/teleterm/src/ui/App.test.tsx
index d15b30d1b5904..e97512b8f6984 100644
--- a/web/packages/teleterm/src/ui/App.test.tsx
+++ b/web/packages/teleterm/src/ui/App.test.tsx
@@ -25,7 +25,6 @@ import { mockIntersectionObserver } from 'jsdom-testing-mocks';
import { render } from 'design/utils/testing';
import Logger, { NullService } from 'teleterm/logger';
-import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient';
import { makeRootCluster } from 'teleterm/services/tshd/testHelpers';
import { App } from 'teleterm/ui/App';
import { MockAppContext } from 'teleterm/ui/fixtures/mocks';
@@ -67,6 +66,9 @@ test('activating a workspace via deep link overrides the previously active works
connected: false,
});
const appContext = new MockAppContext();
+ appContext.addRootCluster(deepLinkCluster);
+ appContext.addRootCluster(previouslyActiveCluster);
+
jest
.spyOn(appContext.statePersistenceService, 'getWorkspacesState')
.mockReturnValue({
@@ -88,11 +90,6 @@ test('activating a workspace via deep link overrides the previously active works
'usageReporting.enabled',
false
);
- jest.spyOn(appContext.tshd, 'listRootClusters').mockReturnValue(
- new MockedUnaryCall({
- clusters: [deepLinkCluster, previouslyActiveCluster],
- })
- );
render();
@@ -198,11 +195,7 @@ test.each<{
'usageReporting.enabled',
false
);
- jest.spyOn(appContext.tshd, 'listRootClusters').mockReturnValue(
- new MockedUnaryCall({
- clusters: [rootCluster],
- })
- );
+ appContext.addRootCluster(rootCluster);
render();
diff --git a/web/packages/teleterm/src/ui/ClusterConnect/ClusterAdd/ClusterAdd.story.tsx b/web/packages/teleterm/src/ui/ClusterConnect/ClusterAdd/ClusterAdd.story.tsx
index d1b0d6a5d379f..a4b1563b20eca 100644
--- a/web/packages/teleterm/src/ui/ClusterConnect/ClusterAdd/ClusterAdd.story.tsx
+++ b/web/packages/teleterm/src/ui/ClusterConnect/ClusterAdd/ClusterAdd.story.tsx
@@ -85,7 +85,7 @@ function getMockAppContext(
} = {}
) {
const appContext = new MockAppContext();
- appContext.clustersService.addRootCluster =
+ appContext.mockMainProcessClient.addCluster =
args.addRootCluster || (() => Promise.resolve(makeRootCluster()));
return appContext;
}
diff --git a/web/packages/teleterm/src/ui/ClusterConnect/ClusterAdd/ClusterAdd.tsx b/web/packages/teleterm/src/ui/ClusterConnect/ClusterAdd/ClusterAdd.tsx
index e2d6303bb7313..7c62b012a0b52 100644
--- a/web/packages/teleterm/src/ui/ClusterConnect/ClusterAdd/ClusterAdd.tsx
+++ b/web/packages/teleterm/src/ui/ClusterConnect/ClusterAdd/ClusterAdd.tsx
@@ -35,12 +35,11 @@ export function ClusterAdd(props: {
onSuccess(clusterUri: string): void;
prefill: { clusterAddress: string };
}) {
- const { clustersService, workspacesService } = useAppContext();
+ const { mainProcessClient } = useAppContext();
const [{ status, statusText }, addCluster] = useAsync(
async (addr: string) => {
const proxyAddr = parseClusterProxyWebAddr(addr);
- const cluster = await clustersService.addRootCluster(proxyAddr);
- workspacesService.addWorkspace(cluster.uri);
+ const cluster = await mainProcessClient.addCluster(proxyAddr);
return props.onSuccess(cluster.uri);
}
);
diff --git a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.test.tsx b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.test.tsx
index 3d5cd873e40fc..1d773d5706f95 100644
--- a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.test.tsx
+++ b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.test.tsx
@@ -172,11 +172,11 @@ it('shows two separate prompt texts during SSO login', async () => {
.spyOn(appContext.tshd, 'login')
.mockImplementation(async () => loginPromise);
- const { resolve: resolveGetClusterPromise, promise: getClusterPromise } =
- Promise.withResolvers>();
+ const { resolve: resolveSyncClusterPromise, promise: syncClusterPromise } =
+ Promise.withResolvers();
jest
- .spyOn(appContext.tshd, 'getCluster')
- .mockImplementation(async () => getClusterPromise);
+ .spyOn(appContext.mainProcessClient, 'syncCluster')
+ .mockImplementation(async () => syncClusterPromise);
render(
@@ -205,6 +205,6 @@ it('shows two separate prompt texts during SSO login', async () => {
await act(async () => {
// Resolve the promise to avoid leaving a hanging promise around.
- resolveGetClusterPromise(new MockedUnaryCall(cluster));
+ resolveSyncClusterPromise();
});
});
diff --git a/web/packages/teleterm/src/ui/ClusterLogout/ClusterLogout.tsx b/web/packages/teleterm/src/ui/ClusterLogout/ClusterLogout.tsx
index f4936f9dafd1f..9326f1276dbb3 100644
--- a/web/packages/teleterm/src/ui/ClusterLogout/ClusterLogout.tsx
+++ b/web/packages/teleterm/src/ui/ClusterLogout/ClusterLogout.tsx
@@ -25,11 +25,11 @@ import DialogConfirmation, {
} from 'design/DialogConfirmation';
import { Cross } from 'design/Icon';
import { P } from 'design/Text/Text';
+import { useAsync } from 'shared/hooks/useAsync';
+import { useAppContext } from 'teleterm/ui/appContextProvider';
import { RootClusterUri, routing } from 'teleterm/ui/uri';
-import { useClusterLogout } from './useClusterLogout';
-
export function ClusterLogout({
clusterUri,
onClose,
@@ -39,9 +39,10 @@ export function ClusterLogout({
hidden?: boolean;
onClose(): void;
}) {
- const { removeCluster, status, statusText } = useClusterLogout({
- clusterUri,
- });
+ const ctx = useAppContext();
+ const [{ status, statusText }, removeCluster] = useAsync(() =>
+ ctx.mainProcessClient.logout(clusterUri)
+ );
async function removeClusterAndClose(): Promise {
const [, err] = await removeCluster();
diff --git a/web/packages/teleterm/src/ui/ClusterLogout/cleanUpBeforeLogout.ts b/web/packages/teleterm/src/ui/ClusterLogout/cleanUpBeforeLogout.ts
new file mode 100644
index 0000000000000..b84c196d7b914
--- /dev/null
+++ b/web/packages/teleterm/src/ui/ClusterLogout/cleanUpBeforeLogout.ts
@@ -0,0 +1,54 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { IAppContext } from 'teleterm/ui/types';
+import { RootClusterUri } from 'teleterm/ui/uri';
+
+/** Disposes cluster-related resources. */
+export async function cleanUpBeforeLogout(
+ ctx: IAppContext,
+ clusterUri: RootClusterUri,
+ opts: { removeWorkspace: boolean }
+): Promise {
+ if (ctx.workspacesService.getRootClusterUri() === clusterUri) {
+ const [firstConnectedWorkspace] = ctx.workspacesService
+ .getConnectedWorkspacesClustersUri()
+ .filter(c => c !== clusterUri);
+ if (firstConnectedWorkspace) {
+ await ctx.workspacesService.setActiveWorkspace(firstConnectedWorkspace);
+ } else {
+ await ctx.workspacesService.setActiveWorkspace(null);
+ }
+ }
+
+ // Remove connections first, they depend both on the cluster and the workspace.
+ ctx.connectionTracker.removeItemsBelongingToRootCluster(clusterUri);
+ // Remove the workspace next, because it depends on the cluster.
+ if (opts.removeWorkspace) {
+ ctx.workspacesService.removeWorkspace(clusterUri);
+ } else {
+ ctx.workspacesService.clearWorkspace(clusterUri);
+ }
+
+ // If there are active ssh connections to the agent, killing it will take a few seconds. To work
+ // around this, kill the agent only after removing the workspace. Removing the workspace closes
+ // ssh tabs, so it should terminate connections to the cluster from the app.
+ await ctx.connectMyComputerService.killAgentAndRemoveData(clusterUri);
+
+ await ctx.clustersService.removeClusterGateways(clusterUri);
+}
diff --git a/web/packages/teleterm/src/ui/ClusterLogout/useClusterLogout.ts b/web/packages/teleterm/src/ui/ClusterLogout/useClusterLogout.ts
deleted file mode 100644
index 90683da79f324..0000000000000
--- a/web/packages/teleterm/src/ui/ClusterLogout/useClusterLogout.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * Teleport
- * Copyright (C) 2023 Gravitational, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-
-import { useAsync } from 'shared/hooks/useAsync';
-
-import { useLogger } from 'teleterm/ui/hooks/useLogger';
-import { RootClusterUri } from 'teleterm/ui/uri';
-
-import { useAppContext } from '../appContextProvider';
-
-export function useClusterLogout({
- clusterUri,
-}: {
- clusterUri: RootClusterUri;
-}) {
- const ctx = useAppContext();
- const logger = useLogger('useClusterLogout');
- const [{ status, statusText }, removeCluster] = useAsync(async () => {
- await ctx.clustersService.logout(clusterUri);
- // This function checks for updates, do not wait for it.
- ctx.mainProcessClient
- .maybeRemoveAppUpdatesManagingCluster(clusterUri)
- .catch(err => {
- logger.error('Failed to remove managing cluster', err);
- });
-
- if (ctx.workspacesService.getRootClusterUri() === clusterUri) {
- const [firstConnectedWorkspace] =
- ctx.workspacesService.getConnectedWorkspacesClustersUri();
- if (firstConnectedWorkspace) {
- await ctx.workspacesService.setActiveWorkspace(firstConnectedWorkspace);
- } else {
- await ctx.workspacesService.setActiveWorkspace(null);
- }
- }
-
- // Remove connections first, they depend both on the cluster and the workspace.
- ctx.connectionTracker.removeItemsBelongingToRootCluster(clusterUri);
-
- // Remove the workspace next, because it depends on the cluster.
- ctx.workspacesService.removeWorkspace(clusterUri);
-
- // If there are active ssh connections to the agent, killing it will take a few seconds. To work
- // around this, kill the agent only after removing the workspace. Removing the workspace closes
- // ssh tabs, so it should terminate connections to the cluster from the app.
- //
- // If ClustersService.logout above fails, the user should still be able to manage the agent.
- await ctx.connectMyComputerService.killAgentAndRemoveData(clusterUri);
-
- // Remove the cluster, it does not depend on anything.
- await ctx.clustersService.removeClusterAndResources(clusterUri);
- });
-
- return {
- status,
- statusText,
- removeCluster,
- };
-}
diff --git a/web/packages/teleterm/src/ui/DocumentAccessRequests/useAssumeAccess.ts b/web/packages/teleterm/src/ui/DocumentAccessRequests/useAssumeAccess.ts
index ed4742889ada8..63639d475b3b7 100644
--- a/web/packages/teleterm/src/ui/DocumentAccessRequests/useAssumeAccess.ts
+++ b/web/packages/teleterm/src/ui/DocumentAccessRequests/useAssumeAccess.ts
@@ -35,8 +35,6 @@ export function useAssumeAccess() {
const [assumeRoleAttempt, runAssumeRole] = useAsync((requestId: string) =>
retryWithRelogin(ctx, clusterUri, async () => {
await ctx.clustersService.assumeRoles(rootClusterUri, [requestId]);
- // Refresh the current resource tabs in the workspace.
- requestResourcesRefresh();
})
);
diff --git a/web/packages/teleterm/src/ui/DocumentCluster/resourcesContext.test.tsx b/web/packages/teleterm/src/ui/DocumentCluster/resourcesContext.test.tsx
index 12c220e43632b..3c653eb549f15 100644
--- a/web/packages/teleterm/src/ui/DocumentCluster/resourcesContext.test.tsx
+++ b/web/packages/teleterm/src/ui/DocumentCluster/resourcesContext.test.tsx
@@ -19,6 +19,7 @@
import { renderHook } from '@testing-library/react';
import { rootClusterUri } from 'teleterm/services/tshd/testHelpers';
+import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider';
import {
ResourcesContextProvider,
@@ -28,7 +29,9 @@ import {
describe('requestResourcesRefresh', () => {
it('calls listener registered with onResourcesRefreshRequest', () => {
const wrapper = ({ children }) => (
- {children}
+
+ {children}
+
);
const { result } = renderHook(
() => ({
@@ -58,7 +61,9 @@ describe('requestResourcesRefresh', () => {
describe('onResourcesRefreshRequest cleanup function', () => {
it('removes the listener', () => {
const wrapper = ({ children }) => (
- {children}
+
+ {children}
+
);
const { result } = renderHook(() => useResourcesContext(rootClusterUri), {
wrapper,
diff --git a/web/packages/teleterm/src/ui/DocumentCluster/resourcesContext.tsx b/web/packages/teleterm/src/ui/DocumentCluster/resourcesContext.tsx
index 13c8964127ea8..e5bb664dc8ac5 100644
--- a/web/packages/teleterm/src/ui/DocumentCluster/resourcesContext.tsx
+++ b/web/packages/teleterm/src/ui/DocumentCluster/resourcesContext.tsx
@@ -24,9 +24,11 @@ import {
PropsWithChildren,
useCallback,
useContext,
+ useEffect,
useRef,
} from 'react';
+import { useAppContext } from 'teleterm/ui/appContextProvider';
import { RootClusterUri } from 'teleterm/ui/uri';
export interface ResourcesContext {
@@ -54,6 +56,7 @@ export interface ResourcesContext {
const ResourcesContext = createContext(null);
export const ResourcesContextProvider: FC = props => {
+ const appCtx = useAppContext();
const emitterRef = useRef(undefined);
if (!emitterRef.current) {
emitterRef.current = new EventEmitter();
@@ -86,6 +89,12 @@ export const ResourcesContextProvider: FC = props => {
[]
);
+ useEffect(() => {
+ return appCtx.addResourceRefreshListener(uri => {
+ requestResourcesRefresh(uri);
+ });
+ }, [appCtx, requestResourcesRefresh]);
+
return (
{
const ctx = new MockAppContext();
ctx.addRootCluster(clusterOrange);
ctx.addRootCluster(clusterViolet);
+ ctx.statePersistenceService.putState({
+ ...ctx.statePersistenceService.getState(),
+ showTshHomeMigrationBanner: true,
+ });
return (
- ;
+
);
};
@@ -74,9 +78,13 @@ export const WithErrors = () => {
})
);
ctx.addRootCluster(clusterViolet);
+ ctx.statePersistenceService.putState({
+ ...ctx.statePersistenceService.getState(),
+ showTshHomeMigrationBanner: true,
+ });
return (
- ;
+
);
};
diff --git a/web/packages/teleterm/src/ui/TabHost/ClusterConnectPanel/ClusterConnectPanel.tsx b/web/packages/teleterm/src/ui/TabHost/ClusterConnectPanel/ClusterConnectPanel.tsx
index 9890f52be0355..3654ee2715efb 100644
--- a/web/packages/teleterm/src/ui/TabHost/ClusterConnectPanel/ClusterConnectPanel.tsx
+++ b/web/packages/teleterm/src/ui/TabHost/ClusterConnectPanel/ClusterConnectPanel.tsx
@@ -33,6 +33,7 @@ import {
import { useAppContext } from 'teleterm/ui/appContextProvider';
import { NullKeyboardArrowsNavigation } from 'teleterm/ui/components/KeyboardArrowsNavigation/KeyboardArrowsNavigation';
import { useStoreSelector } from 'teleterm/ui/hooks/useStoreSelector';
+import { TshHomeMigrationBanner } from 'teleterm/ui/TopBar/Identity';
import { ClusterList } from 'teleterm/ui/TopBar/Identity/IdentityList/IdentityList';
import { RootClusterUri } from 'teleterm/ui/uri';
@@ -77,6 +78,14 @@ export function ClusterConnectPanel() {
Log in to a cluster to use Teleport Connect.
+ {/* Apply the same styling as used for the cluster items below. */}
+ p.theme.space[1]}px;
+ border-radius: ${p => p.theme.radii[2]}px;
+ padding: ${p => p.theme.space[2]}px;
+ `}
+ />
{/*Disable arrows navigation, it doesn't work well here,*/}
{/*since it requires the container to be focused.*/}
{/*The user can navigate with Tab.*/}
diff --git a/web/packages/teleterm/src/ui/TopBar/Identity/Identity.story.tsx b/web/packages/teleterm/src/ui/TopBar/Identity/Identity.story.tsx
index 4a4bcca868a7c..e2d88090d4d10 100644
--- a/web/packages/teleterm/src/ui/TopBar/Identity/Identity.story.tsx
+++ b/web/packages/teleterm/src/ui/TopBar/Identity/Identity.story.tsx
@@ -16,9 +16,11 @@
* along with this program. If not, see .
*/
+import { Meta, StoryObj } from '@storybook/react-vite';
import { useLayoutEffect } from 'react';
import Flex from 'design/Flex';
+import { Timestamp } from 'gen-proto-ts/google/protobuf/timestamp_pb';
import { TrustedDeviceRequirement } from 'gen-proto-ts/teleport/legacy/types/trusted_device_requirement_pb';
import { Cluster } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
@@ -32,21 +34,122 @@ import { RootClusterUri } from 'teleterm/ui/uri';
import { IdentityContainer } from './Identity';
-export default {
+interface StoryProps {
+ clusters: ('violet' | 'orange' | 'green')[];
+ activeCluster: boolean;
+ activeClusterExpired: boolean;
+ deviceTrust: 'enrolled' | 'required-not-enrolled' | 'not-enrolled';
+ showProfileErrors: boolean;
+}
+
+const meta: Meta = {
title: 'Teleterm/Identity',
+ component: props => {
+ const hasOrange = props.clusters.includes('orange');
+ const hasViolet = props.clusters.includes('violet');
+ const hasGreen = props.clusters.includes('green');
+ const clusters = [
+ hasOrange &&
+ makeRootCluster({
+ ...clusterOrange,
+ profileStatusError: props.showProfileErrors ? profileStatusError : '',
+ }),
+ hasViolet &&
+ makeRootCluster({
+ ...clusterViolet,
+ profileStatusError: props.showProfileErrors ? profileStatusError : '',
+ }),
+ hasGreen &&
+ makeRootCluster({
+ ...clusterGreen,
+ profileStatusError: props.showProfileErrors ? profileStatusError : '',
+ }),
+ ].filter(Boolean);
+
+ const hasClusterWithLoggedInUser =
+ props.activeCluster && (hasOrange || hasViolet);
+ if (hasClusterWithLoggedInUser) {
+ clusters[0].loggedInUser = makeLoggedInUser({
+ ...clusters[0].loggedInUser,
+ validUntil: Timestamp.fromDate(
+ props.activeClusterExpired
+ ? new Date()
+ : new Date(Date.now() + 24 * 60 * 60 * 1000)
+ ),
+ isDeviceTrusted: props.deviceTrust === 'enrolled',
+ trustedDeviceRequirement:
+ props.deviceTrust === 'required-not-enrolled'
+ ? TrustedDeviceRequirement.REQUIRED
+ : TrustedDeviceRequirement.NOT_REQUIRED,
+ });
+ }
+
+ return (
+
+ );
+ },
+ argTypes: {
+ clusters: {
+ control: { type: 'check' },
+ options: ['violet', 'orange', 'green'],
+ description: 'List of clusters to show.',
+ },
+ activeCluster: {
+ control: { type: 'boolean' },
+ description: 'Makes "violet" or "orange" an active cluster.',
+ },
+ deviceTrust: {
+ control: { type: 'radio' },
+ options: ['enrolled', 'required-not-enrolled', 'not-enrolled'],
+ description: 'Controls device trust requirement.',
+ },
+ activeClusterExpired: {
+ control: { type: 'boolean' },
+ description: 'Whether the active cluster has expired cert.',
+ },
+ showProfileErrors: {
+ control: { type: 'boolean' },
+ description: 'Shows profile errors for all clusters.',
+ },
+ },
+ args: {
+ clusters: ['violet', 'orange', 'green'],
+ activeCluster: true,
+ deviceTrust: 'not-enrolled',
+ activeClusterExpired: false,
+ showProfileErrors: false,
+ },
};
+export default meta;
+
const clusterOrange = makeRootCluster({
- name: 'orange',
+ name: 'orange-psv-eindhoven-eredivisie-production-lorem-ipsum',
loggedInUser: makeLoggedInUser({
- name: 'bob',
- roles: ['access', 'editor'],
+ name: 'ruud-van-nistelrooy-van-der-sar',
+ roles: [
+ 'circle-mark-app-access',
+ 'grafana-lite-app-access',
+ 'grafana-gold-app-access',
+ 'release-lion-app-access',
+ 'release-fox-app-access',
+ 'sales-center-lorem-app-access',
+ 'sales-center-ipsum-db-access',
+ 'sales-center-shop-app-access',
+ 'sales-center-floor-db-access',
+ ],
}),
uri: '/clusters/orange',
});
const clusterViolet = makeRootCluster({
name: 'violet',
- loggedInUser: makeLoggedInUser({ name: 'sammy' }),
+ loggedInUser: makeLoggedInUser({
+ name: 'sammy',
+ roles: ['access', 'editor'],
+ }),
uri: '/clusters/violet',
});
const clusterGreen = makeRootCluster({
@@ -63,11 +166,21 @@ const OpenIdentityPopover = (props: {
activeClusterUri: RootClusterUri | undefined;
}) => {
const ctx = new MockAppContext();
+ ctx.statePersistenceService.putState({
+ ...ctx.statePersistenceService.getState(),
+ showTshHomeMigrationBanner: true,
+ });
props.clusters.forEach(c => {
ctx.addRootCluster(c);
});
+ ctx.workspacesService.addWorkspace(clusterGreen.uri);
+ ctx.workspacesService.addWorkspace(clusterViolet.uri);
+ ctx.workspacesService.addWorkspace(clusterOrange.uri);
ctx.workspacesService.setState(draftState => {
draftState.rootClusterUri = props.activeClusterUri;
+ draftState.workspaces[clusterGreen.uri].color = 'green';
+ draftState.workspaces[clusterViolet.uri].color = 'purple';
+ draftState.workspaces[clusterOrange.uri].color = 'yellow';
});
useOpenPopover();
@@ -98,125 +211,60 @@ const useOpenPopover = () => {
}, []);
};
-export function NoRootClusters() {
- return ;
-}
-
-export function OneClusterWithNoActiveCluster() {
- return (
-
- );
-}
+export const NoRootClusters: StoryObj = {
+ args: {
+ clusters: [],
+ },
+};
-export function OneClusterWithActiveCluster() {
- const cluster = makeRootCluster({
- loggedInUser: makeLoggedInUser({
- name: 'alice',
- roles: ['access', 'editor'],
- }),
- });
+export const OneClusterWithNoActiveCluster: StoryObj = {
+ args: {
+ clusters: ['orange'],
+ activeCluster: false,
+ },
+};
- return (
-
- );
-}
+export const OneClusterWithActiveCluster: StoryObj = {
+ args: {
+ clusters: ['violet'],
+ },
+};
-export function ManyClustersWithNoActiveCluster() {
- return (
-
- );
-}
+export const ManyClustersWithNoActiveCluster: StoryObj = {
+ args: {
+ clusters: ['orange', 'green', 'violet'],
+ activeCluster: false,
+ },
+};
-export function ManyClustersWithActiveCluster() {
- return (
-
- );
-}
+export const ManyClustersWithActiveCluster: StoryObj = {
+ args: {
+ clusters: ['orange', 'green', 'violet'],
+ },
+};
-export function ManyClustersWithProfileErrorsAndActiveCluster() {
- return (
-
- );
-}
+export const ManyClustersWithProfileErrorsAndActiveCluster: StoryObj =
+ {
+ args: {
+ clusters: ['orange', 'green', 'violet'],
+ showProfileErrors: true,
+ },
+ };
-export function LongNamesWithManyRoles() {
- return (
-
- );
-}
+export const TrustedDeviceEnrolled: StoryObj = {
+ args: {
+ deviceTrust: 'enrolled',
+ },
+};
-export function TrustedDeviceEnrolled() {
- return (
-
- );
-}
+export const TrustedDeviceRequiredButNotEnrolled: StoryObj = {
+ args: {
+ deviceTrust: 'required-not-enrolled',
+ },
+};
-export function TrustedDeviceRequiredButNotEnrolled() {
- return (
-
- );
-}
+export const ActiveClusterExpired: StoryObj = {
+ args: {
+ activeClusterExpired: true,
+ },
+};
diff --git a/web/packages/teleterm/src/ui/TopBar/Identity/Identity.tsx b/web/packages/teleterm/src/ui/TopBar/Identity/Identity.tsx
index 2cd6d874bd43a..2a49f42af6dc5 100644
--- a/web/packages/teleterm/src/ui/TopBar/Identity/Identity.tsx
+++ b/web/packages/teleterm/src/ui/TopBar/Identity/Identity.tsx
@@ -19,12 +19,14 @@
import { useCallback, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
-import { Box } from 'design';
+import { Box, ButtonText, Flex, Link, P3 } from 'design';
+import { Cross, Stars } from 'design/Icon';
import Popover from 'design/Popover';
import { TrustedDeviceRequirement } from 'gen-proto-ts/teleport/legacy/types/trusted_device_requirement_pb';
import * as tshd from 'teleterm/services/tshd/types';
import { KeyboardArrowsNavigation } from 'teleterm/ui/components/KeyboardArrowsNavigation';
+import { usePersistedState } from 'teleterm/ui/hooks/usePersistedState';
import { useStoreSelector } from 'teleterm/ui/hooks/useStoreSelector';
import {
useKeyboardShortcutFormatters,
@@ -119,6 +121,7 @@ export function IdentityContainer() {
deviceTrustStatus={deviceTrustStatus}
/>
)}
+
{focusGrabber}
);
+
+export function TshHomeMigrationBanner(props: { className?: string }) {
+ const [state, setState] = usePersistedState(
+ 'showTshHomeMigrationBanner',
+ false
+ );
+ if (!state) {
+ return;
+ }
+
+ return (
+
+
+
+ {/*This max width value prevents the banner from being too wide in ClusterConnectPanel. */}
+
+ {/*Matches the look of a subtitle in TitleAndSubtitle. */}
+
+ Profiles are now{' '}
+
+ automatically synced
+ {' '}
+ between Teleport Connect and the tsh command-line tool.
+
+
+ setState(false)}
+ >
+
+
+
+
+ );
+}
diff --git a/web/packages/teleterm/src/ui/TopBar/Identity/IdentityList/IdentityList.tsx b/web/packages/teleterm/src/ui/TopBar/Identity/IdentityList/IdentityList.tsx
index de66e52681542..5e52acd2965b1 100644
--- a/web/packages/teleterm/src/ui/TopBar/Identity/IdentityList/IdentityList.tsx
+++ b/web/packages/teleterm/src/ui/TopBar/Identity/IdentityList/IdentityList.tsx
@@ -16,12 +16,20 @@
* along with this program. If not, see .
*/
+import { formatDistanceToNowStrict, isPast } from 'date-fns';
import { JSX } from 'react';
import styled from 'styled-components';
-import { ButtonText, Flex, Label, P3 } from 'design';
-import { Logout, Refresh, ShieldCheck, ShieldWarning } from 'design/Icon';
+import { ButtonText, Flex, Label, P3, Stack } from 'design';
+import {
+ Clock,
+ Logout,
+ Refresh,
+ ShieldCheck,
+ ShieldWarning,
+} from 'design/Icon';
import Link from 'design/Link';
+import { Timestamp } from 'gen-proto-ts/google/protobuf/timestamp_pb';
import { Cluster } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
import { ProfileStatusError } from 'teleterm/ui/components/ProfileStatusError';
@@ -46,6 +54,10 @@ export function ActiveCluster(props: {
onLogout(): void;
}) {
const clusterName = routing.parseClusterName(props.activeCluster.uri);
+ const validUntil =
+ props.activeCluster.loggedInUser?.validUntil &&
+ Timestamp.toDate(props.activeCluster.loggedInUser.validUntil);
+
return (
<>
@@ -103,7 +115,35 @@ export function ActiveCluster(props: {
))}
-
+
+ {validUntil && (
+
+
+
+ {isPast(validUntil) ? (
+ 'Session expired.'
+ ) : (
+ <>
+ Session expires{' '}
+
+ {formatDistanceToNowStrict(validUntil, {
+ addSuffix: true,
+ })}
+ .
+
+ >
+ )}
+
+
+ )}
+
+
>
diff --git a/web/packages/teleterm/src/ui/TopBar/Identity/index.ts b/web/packages/teleterm/src/ui/TopBar/Identity/index.ts
index 3297d80580132..0814cfe2f9f32 100644
--- a/web/packages/teleterm/src/ui/TopBar/Identity/index.ts
+++ b/web/packages/teleterm/src/ui/TopBar/Identity/index.ts
@@ -16,4 +16,7 @@
* along with this program. If not, see .
*/
-export { IdentityContainer as Identity } from './Identity';
+export {
+ IdentityContainer as Identity,
+ TshHomeMigrationBanner,
+} from './Identity';
diff --git a/web/packages/teleterm/src/ui/Vnet/integration.test.tsx b/web/packages/teleterm/src/ui/Vnet/integration.test.tsx
index 9a731ebf32836..054bcd675fd75 100644
--- a/web/packages/teleterm/src/ui/Vnet/integration.test.tsx
+++ b/web/packages/teleterm/src/ui/Vnet/integration.test.tsx
@@ -71,6 +71,7 @@ test.each(tests)(
const user = userEvent.setup();
const ctx = new MockAppContext();
const rootCluster = makeRootCluster();
+ ctx.addRootCluster(rootCluster, { noActivate: true });
ctx.configService.set('usageReporting.enabled', false);
jest.spyOn(ctx.tshd, 'listUnifiedResources').mockReturnValue(
@@ -84,11 +85,6 @@ test.each(tests)(
],
})
);
- jest.spyOn(ctx.tshd, 'listRootClusters').mockReturnValue(
- new MockedUnaryCall({
- clusters: [rootCluster],
- })
- );
jest.spyOn(ctx.vnet, 'getServiceInfo').mockReturnValue(
new MockedUnaryCall({
appDnsZones: [proxyHostname(rootCluster.proxyHost)],
@@ -167,6 +163,7 @@ test.each(tests)(
const user = userEvent.setup();
const ctx = new MockAppContext();
const rootCluster = makeRootCluster();
+ ctx.addRootCluster(rootCluster, { noActivate: true });
ctx.configService.set('usageReporting.enabled', false);
ctx.statePersistenceService.putState({
...ctx.statePersistenceService.getState(),
@@ -184,11 +181,6 @@ test.each(tests)(
],
})
);
- jest.spyOn(ctx.tshd, 'listRootClusters').mockReturnValue(
- new MockedUnaryCall({
- clusters: [rootCluster],
- })
- );
jest.spyOn(ctx.vnet, 'getServiceInfo').mockReturnValue(
new MockedUnaryCall({
appDnsZones: [proxyHostname(rootCluster.proxyHost)],
@@ -247,13 +239,9 @@ test('launching VNet for the first time from the connections panel does not open
const user = userEvent.setup();
const ctx = new MockAppContext();
const rootCluster = makeRootCluster();
+ ctx.addRootCluster(rootCluster, { noActivate: true });
ctx.configService.set('usageReporting.enabled', false);
- jest.spyOn(ctx.tshd, 'listRootClusters').mockReturnValue(
- new MockedUnaryCall({
- clusters: [rootCluster],
- })
- );
jest.spyOn(ctx.vnet, 'getServiceInfo').mockReturnValue(
new MockedUnaryCall({
appDnsZones: [proxyHostname(rootCluster.proxyHost)],
diff --git a/web/packages/teleterm/src/ui/appContext.ts b/web/packages/teleterm/src/ui/appContext.ts
index f766cdec331ef..240925877ff93 100644
--- a/web/packages/teleterm/src/ui/appContext.ts
+++ b/web/packages/teleterm/src/ui/appContext.ts
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import { debounce } from 'shared/utils/highbar';
+import { getErrorMessage } from 'shared/utils/error';
import { ConfigService } from 'teleterm/services/config';
import { TshdClient, VnetClient } from 'teleterm/services/tshd/createClient';
@@ -25,6 +25,7 @@ import {
MainProcessClient,
TshdEventContextBridgeService,
} from 'teleterm/types';
+import { cleanUpBeforeLogout } from 'teleterm/ui/ClusterLogout/cleanUpBeforeLogout';
import { ClustersService } from 'teleterm/ui/services/clusters';
import { ConnectionTrackerService } from 'teleterm/ui/services/connectionTracker';
import { ConnectMyComputerService } from 'teleterm/ui/services/connectMyComputer';
@@ -40,7 +41,11 @@ import { TerminalsService } from 'teleterm/ui/services/terminals';
import { TshdNotificationsService } from 'teleterm/ui/services/tshdNotifications/tshdNotificationService';
import { UsageService } from 'teleterm/ui/services/usage';
import { WorkspacesService } from 'teleterm/ui/services/workspacesService/workspacesService';
-import { IAppContext, UnexpectedVnetShutdownListener } from 'teleterm/ui/types';
+import {
+ IAppContext,
+ ResourceRefreshListener,
+ UnexpectedVnetShutdownListener,
+} from 'teleterm/ui/types';
import { CommandLauncher } from './commandLauncher';
import { createTshdEventsContextBridgeService } from './tshdEvents';
@@ -81,6 +86,7 @@ export default class AppContext implements IAppContext {
private _unexpectedVnetShutdownListener:
| UnexpectedVnetShutdownListener
| undefined;
+ private _resourceRefreshListener: ResourceRefreshListener | undefined;
constructor(config: ElectronGlobals) {
const { tshClient, ptyServiceClient, mainProcessClient } = config;
@@ -154,6 +160,9 @@ export default class AppContext implements IAppContext {
tshClient,
this.configService
);
+
+ this.registerClusterLifecycleHandler();
+ this.subscribeToProfileWatcherErrors();
}
async pullInitialState(): Promise {
@@ -161,7 +170,6 @@ export default class AppContext implements IAppContext {
createTshdEventsContextBridgeService(this)
);
- this.notifyMainProcessAboutClusterListChanges();
void this.clustersService.syncGatewaysAndCatchErrors();
await this.clustersService.syncRootClustersAndCatchErrors();
this.workspacesService.restorePersistedState();
@@ -198,22 +206,77 @@ export default class AppContext implements IAppContext {
return this._unexpectedVnetShutdownListener;
}
- private notifyMainProcessAboutClusterListChanges() {
- // Debounce the notifications sent to the main process so that we don't unnecessarily send more
- // than one notification per frame. The main process doesn't need to be notified absolutely
- // immediately after a change in the cluster list.
- //
- // The clusters map in ClustersService gets updated a bunch of times during the start of the
- // app. After each update, the renderer tells the main process to refresh the list. The main
- // process sends a request to list root clusters and cancels any pending ones. Debouncing here
- // helps to minimize those cancellations.
- const refreshClusterList = debounce(
- this.mainProcessClient.refreshClusterList,
- 16
- );
- this.clustersService.subscribeWithSelector(
- state => state.clusters,
- refreshClusterList
+ /** Sets the listener and returns a cleanup function which removes the listener. */
+ addResourceRefreshListener(listener: ResourceRefreshListener): () => void {
+ this._resourceRefreshListener = listener;
+
+ return () => {
+ this._resourceRefreshListener = undefined;
+ };
+ }
+
+ /** Gets called when `ClusterLifecycleManager` requests resource refresh. */
+ get resourceRefreshListener(): ResourceRefreshListener {
+ return this._resourceRefreshListener;
+ }
+
+ private registerClusterLifecycleHandler(): void {
+ // Queue chain ensures sequential processing.
+ let processingQueue = Promise.resolve();
+
+ this.mainProcessClient.registerClusterLifecycleHandler(({ uri, op }) => {
+ // Chain onto the queue and catch errors so it keeps processing
+ const task = processingQueue.then(async () => {
+ switch (op) {
+ case 'did-add-cluster':
+ return this.workspacesService.addWorkspace(uri);
+ case 'did-change-access':
+ if (!this.clustersService.findCluster(uri).connected) {
+ // Only refresh resources when the cluster is connected.
+ return;
+ }
+ return this.resourceRefreshListener(uri);
+ case 'will-logout':
+ return cleanUpBeforeLogout(this, uri, { removeWorkspace: false });
+ case 'will-logout-and-remove':
+ return cleanUpBeforeLogout(this, uri, { removeWorkspace: true });
+ default:
+ op satisfies never;
+ }
+ });
+
+ // Update the queue so the next event waits for this one.
+ // Catch errors, they will be returned below.
+ processingQueue = task.catch(() => {});
+
+ return task;
+ });
+ }
+
+ private subscribeToProfileWatcherErrors(): void {
+ let notificationId: string | undefined;
+ this.mainProcessClient.subscribeToProfileWatcherErrors(
+ ({ error, reason }) => {
+ let title: string;
+ switch (reason) {
+ case 'processing-error':
+ title =
+ 'Failed to process the detected profile update. Changes made through tsh may not be reflected in the app.';
+ break;
+ case 'exited':
+ title =
+ "Stopped monitoring profiles. Changes made through tsh won't be reflected in the app.";
+ break;
+ }
+
+ if (notificationId) {
+ this.notificationsService.removeNotification(notificationId);
+ }
+ notificationId = this.notificationsService.notifyError({
+ title,
+ description: getErrorMessage(error),
+ });
+ }
);
}
}
diff --git a/web/packages/teleterm/src/ui/fixtures/mocks.ts b/web/packages/teleterm/src/ui/fixtures/mocks.ts
index 87075e10862f9..c7a7f69e7c16d 100644
--- a/web/packages/teleterm/src/ui/fixtures/mocks.ts
+++ b/web/packages/teleterm/src/ui/fixtures/mocks.ts
@@ -55,7 +55,8 @@ export class MockAppContext extends AppContext {
addRootClusterWithDoc(
cluster: Cluster,
- doc: Document[] | Document | undefined
+ doc: Document[] | Document | undefined,
+ options?: AddRootClusterOptions
) {
this.clustersService.setState(draftState => {
draftState.clusters.set(cluster.uri, cluster);
@@ -63,13 +64,20 @@ export class MockAppContext extends AppContext {
const docs = Array.isArray(doc) ? doc : [doc];
this.workspacesService.addWorkspace(cluster.uri);
this.workspacesService.setState(draftState => {
- draftState.rootClusterUri = cluster.uri;
+ if (!options?.noActivate) {
+ draftState.rootClusterUri = cluster.uri;
+ }
draftState.workspaces[cluster.uri].documents = docs.filter(Boolean);
draftState.workspaces[cluster.uri].location = docs[0]?.uri;
});
}
- addRootCluster(cluster: Cluster) {
- this.addRootClusterWithDoc(cluster, undefined);
+ addRootCluster(cluster: Cluster, options?: AddRootClusterOptions) {
+ this.addRootClusterWithDoc(cluster, undefined, options);
}
}
+
+interface AddRootClusterOptions {
+ /** Does not set the cluster as active in workspaces service. */
+ noActivate?: boolean;
+}
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 465b3d85e9041..1ff01a388b48b 100644
--- a/web/packages/teleterm/src/ui/services/clusters/clustersService.test.ts
+++ b/web/packages/teleterm/src/ui/services/clusters/clustersService.test.ts
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import { MainProcessClient } from 'teleterm/mainProcess/types';
+import { MockMainProcessClient } from 'teleterm/mainProcess/fixtures/mocks';
import type { TshdClient } from 'teleterm/services/tshd';
import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient';
import {
@@ -60,9 +60,7 @@ const UsageServiceMock = UsageService as jest.MockedClass;
function createService(client: Partial): ClustersService {
return new ClustersService(
client as TshdClient,
- {
- removeKubeConfig: jest.fn().mockResolvedValueOnce(undefined),
- } as unknown as MainProcessClient,
+ new MockMainProcessClient(),
new NotificationsServiceMock(),
new UsageServiceMock(undefined, undefined, undefined, undefined, undefined)
);
@@ -73,7 +71,6 @@ function getClientMocks(): Partial {
login: jest.fn().mockReturnValueOnce(new MockedUnaryCall({})),
logout: jest.fn().mockReturnValueOnce(new MockedUnaryCall({})),
addCluster: jest.fn().mockReturnValueOnce(new MockedUnaryCall(clusterMock)),
- removeCluster: jest.fn().mockReturnValueOnce(new MockedUnaryCall({})),
getCluster: jest.fn().mockReturnValueOnce(new MockedUnaryCall(clusterMock)),
listLeafClusters: jest
.fn()
@@ -93,50 +90,7 @@ function getClientMocks(): Partial {
};
}
-test('add cluster', async () => {
- const { addCluster } = getClientMocks();
- const service = createService({
- addCluster,
- });
-
- await service.addRootCluster(clusterUri);
-
- expect(addCluster).toHaveBeenCalledWith({ name: clusterUri });
- expect(service.state.clusters).toStrictEqual(
- new Map([[clusterUri, clusterMock]])
- );
-});
-
-test('add cluster does not overwrite the existing cluster', async () => {
- const { addCluster } = getClientMocks();
- const service = createService({
- addCluster,
- });
- service.state.clusters.set(clusterUri, {
- ...clusterMock,
- features: { advancedAccessWorkflows: true, isUsageBasedBilling: true },
- });
-
- await service.addRootCluster(clusterUri);
-
- expect(addCluster).toHaveBeenCalledWith({ name: clusterUri });
- expect(service.state.clusters).toStrictEqual(
- new Map([
- [
- clusterUri,
- {
- ...clusterMock,
- features: {
- advancedAccessWorkflows: true,
- isUsageBasedBilling: true,
- },
- },
- ],
- ])
- );
-});
-
-test('remove cluster', async () => {
+test('remove gateways', async () => {
const { removeGateway } = getClientMocks();
const service = createService({ removeGateway });
const gatewayFromRootCluster = makeDatabaseGateway({
@@ -153,10 +107,6 @@ test('remove cluster', async () => {
});
service.setState(draftState => {
- draftState.clusters = new Map([
- [clusterMock.uri, clusterMock],
- [leafClusterMock.uri, leafClusterMock],
- ]);
draftState.gateways = new Map([
[gatewayFromRootCluster.uri, gatewayFromRootCluster],
[gatewayFromLeafCluster.uri, gatewayFromLeafCluster],
@@ -164,14 +114,11 @@ test('remove cluster', async () => {
]);
});
- await service.removeClusterAndResources(clusterUri);
+ await service.removeClusterGateways(clusterUri);
- expect(service.findCluster(clusterUri)).toBeUndefined();
- expect(service.findCluster(leafClusterMock.uri)).toBeUndefined();
expect(service.state.gateways).toEqual(
new Map([[gatewayFromOtherCluster.uri, gatewayFromOtherCluster]])
);
-
expect(removeGateway).toHaveBeenCalledWith({
gatewayUri: gatewayFromRootCluster.uri,
});
@@ -183,49 +130,6 @@ test('remove cluster', async () => {
});
});
-test('sync root cluster', async () => {
- const { getCluster, listLeafClusters, startHeadlessWatcher } =
- getClientMocks();
- const service = createService({
- getCluster,
- listLeafClusters,
- startHeadlessWatcher,
- });
-
- await service.syncAndWatchRootClusterWithErrorHandling(clusterUri);
-
- expect(service.findCluster(clusterUri)).toStrictEqual(clusterMock);
- expect(service.findCluster(leafClusterMock.uri)).toStrictEqual(
- leafClusterMock
- );
- expect(listLeafClusters).toHaveBeenCalledWith({ clusterUri });
- expect(startHeadlessWatcher).toHaveBeenCalledWith({
- rootClusterUri: clusterUri,
- });
-});
-
-test('logout from cluster', async () => {
- const { logout, removeCluster } = getClientMocks();
- const service = createService({
- logout,
- removeCluster,
- getCluster: () => new MockedUnaryCall({ ...clusterMock, connected: false }),
- });
- service.setState(draftState => {
- draftState.clusters = new Map([
- [clusterMock.uri, clusterMock],
- [leafClusterMock.uri, leafClusterMock],
- ]);
- });
-
- await service.logout(clusterUri);
-
- expect(logout).toHaveBeenCalledWith({ clusterUri });
- expect(removeCluster).toHaveBeenCalledWith({ clusterUri });
- expect(service.findCluster(clusterMock.uri).connected).toBe(false);
- expect(service.findCluster(leafClusterMock.uri).connected).toBe(false);
-});
-
test('create a gateway', async () => {
const { createGateway } = 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 eb10ddde510ac..19806324102a3 100644
--- a/web/packages/teleterm/src/ui/services/clusters/clustersService.ts
+++ b/web/packages/teleterm/src/ui/services/clusters/clustersService.ts
@@ -16,10 +16,8 @@
* along with this program. If not, see .
*/
-import {
- Cluster,
- ShowResources,
-} from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
+import { applyPatches, castDraft, enablePatches } from 'immer';
+
import { Gateway } from 'gen-proto-ts/teleport/lib/teleterm/v1/gateway_pb';
import {
CreateAccessRequestRequest,
@@ -28,10 +26,11 @@ import {
ReviewAccessRequestRequest,
} from 'gen-proto-ts/teleport/lib/teleterm/v1/service_pb';
import { useStore } from 'shared/libs/stores';
-import { isAbortError } from 'shared/utils/error';
+import { AbortError, isAbortError } from 'shared/utils/error';
+import type { State as ClustersState } from 'teleterm/mainProcess/clusterStore';
import { MainProcessClient } from 'teleterm/mainProcess/types';
-import { cloneAbortSignal, TshdClient } from 'teleterm/services/tshd';
+import { TshdClient } from 'teleterm/services/tshd';
import { getGatewayTargetUriKind } from 'teleterm/services/tshd/gateway';
import { NotificationsService } from 'teleterm/ui/services/notifications';
import { UsageService } from 'teleterm/ui/services/usage';
@@ -42,10 +41,17 @@ import { ImmutableStore } from '../immutableStore';
const { routing } = uri;
type ClustersServiceState = {
- clusters: Map;
+ /**
+ * `clusters` is a local mirror of the `ClusterStore` state from the main process.
+ * This state is read-only and must not be updated manually — it is managed exclusively
+ * through `subscribeToClusterStore`.
+ */
+ clusters: ClustersState;
gateways: Map;
};
+enablePatches();
+
export function createClusterServiceState(): ClustersServiceState {
return {
clusters: new Map(),
@@ -63,44 +69,7 @@ export class ClustersService extends ImmutableStore {
private usageService: UsageService
) {
super();
- }
-
- async addRootCluster(addr: string) {
- const { response: cluster } = await this.client.addCluster({ name: addr });
- // Do not overwrite the existing cluster;
- // otherwise we may lose properties fetched from the auth server.
- // Consider separating properties read from profile and those
- // fetched from the auth server at the RPC message level.
- if (!this.state.clusters.has(cluster.uri)) {
- this.setState(draft => {
- draft.clusters.set(cluster.uri, cluster);
- });
- }
-
- return cluster;
- }
-
- /**
- * Logs out of the cluster and removes the profile.
- * Does not remove the cluster from the state, but sets the cluster and its leafs as disconnected.
- * It needs to be done, because some code can operate on the cluster the intermediate period between logout
- * and actually removing it from the state.
- * A code that operates on that intermediate state is in `useClusterLogout.tsx`.
- * After invoking `logout()`, it looks for the next workspace to switch to. If we hadn't marked the cluster as disconnected,
- * the method might have returned us the same cluster we wanted to log out of.
- */
- async logout(clusterUri: uri.RootClusterUri) {
- // TODO(gzdunek): logout and removeCluster should be combined into a single acton in tshd
- await this.client.logout({ clusterUri });
- await this.client.removeCluster({ clusterUri });
-
- this.setState(draft => {
- draft.clusters.forEach(cluster => {
- if (routing.belongsToProfile(clusterUri, cluster.uri)) {
- cluster.connected = false;
- }
- });
- });
+ this.subscribeToClusterStore();
}
async authenticateWebDevice(
@@ -136,7 +105,7 @@ export class ClustersService extends ImmutableStore {
clusterUri: uri.RootClusterUri
) {
try {
- await this.syncRootCluster(clusterUri);
+ await this.mainProcessClient.syncCluster(clusterUri);
} catch (e) {
const cluster = this.findCluster(clusterUri);
const clusterName =
@@ -186,21 +155,35 @@ export class ClustersService extends ImmutableStore {
* errors up.
*/
async syncRootCluster(clusterUri: uri.RootClusterUri) {
- await Promise.all([
- this.syncClusterInfo(clusterUri),
- this.syncLeafClustersList(clusterUri),
- ]);
+ await this.mainProcessClient.syncCluster(clusterUri);
}
+ /**
+ * Synchronizes root clusters.
+ *
+ * This should only be called before creating workspaces.
+ * If called afterward, a cluster might be removed without first removing
+ * its associated workspace, resulting in an invalid state.
+ */
async syncRootClustersAndCatchErrors(abortSignal?: AbortSignal) {
- let clusters: Cluster[];
-
+ //TODO(gzdunek): Implement passing abort signals over IPC.
+ // In this particular case it's fine to discard waiting for the result.
+ const abortPromise =
+ abortSignal &&
+ new Promise((_, reject) => {
+ if (abortSignal.aborted) {
+ reject(new AbortError());
+ return;
+ }
+ abortSignal.addEventListener('abort', () => reject(new AbortError()), {
+ once: true,
+ });
+ });
try {
- const { response } = await this.client.listRootClusters(
- {},
- { abortSignal: abortSignal && cloneAbortSignal(abortSignal) }
- );
- clusters = response.clusters;
+ await Promise.race([
+ abortPromise,
+ await this.mainProcessClient.syncRootClusters(),
+ ]);
} catch (error) {
if (isAbortError(error)) {
this.logger.info('Listing root clusters aborted');
@@ -220,12 +203,8 @@ export class ClustersService extends ImmutableStore {
return;
}
- this.setState(draft => {
- draft.clusters = new Map(clusters.map(c => [c.uri, c]));
- });
-
// Sync root clusters and resume headless watchers for any active login sessions.
- clusters
+ this.getRootClusters()
.filter(c => c.connected)
.forEach(c => this.syncAndWatchRootClusterWithErrorHandling(c.uri));
}
@@ -251,21 +230,11 @@ export class ClustersService extends ImmutableStore {
}
}
- private async syncLeafClustersList(clusterUri: uri.RootClusterUri) {
- const { response } = await this.client.listLeafClusters({
- clusterUri,
- });
-
- this.setState(draft => {
- for (const leaf of response.clusters) {
- draft.clusters.set(leaf.uri, leaf);
- }
- });
-
- return response.clusters;
- }
-
- /** Assumes roles for the given requests. */
+ /**
+ * Assumes roles for the given requests.
+ * After it's done, resources refresh is requested in
+ * ClusterLifecycleManager.syncCluster.
+ */
async assumeRoles(
rootClusterUri: uri.RootClusterUri,
requestIds: string[]
@@ -279,7 +248,11 @@ export class ClustersService extends ImmutableStore {
await this.syncRootCluster(rootClusterUri);
}
- /** Drops roles for the given requests. */
+ /**
+ * Drops roles for the given requests.
+ * After it's done, resources refresh is requested in
+ * ClusterLifecycleManager.syncCluster.
+ */
async dropRoles(
rootClusterUri: uri.RootClusterUri,
requestIds: string[]
@@ -315,22 +288,9 @@ export class ClustersService extends ImmutableStore {
return response;
}
- /** Removes cluster, its leafs and other resources. */
- async removeClusterAndResources(clusterUri: uri.RootClusterUri) {
- this.setState(draft => {
- draft.clusters.forEach(cluster => {
- if (routing.belongsToProfile(clusterUri, cluster.uri)) {
- draft.clusters.delete(cluster.uri);
- }
- });
- });
- await this.removeClusterKubeConfigs(clusterUri);
- await this.removeClusterGateways(clusterUri);
- }
-
// TODO(ravicious): Create a single RPC for this rather than sending a separate request for each
// gateway.
- private async removeClusterGateways(clusterUri: uri.RootClusterUri) {
+ async removeClusterGateways(clusterUri: uri.RootClusterUri) {
for (const [, gateway] of this.state.gateways) {
if (routing.belongsToProfile(clusterUri, gateway.targetUri)) {
try {
@@ -512,41 +472,19 @@ export class ClustersService extends ImmutableStore {
return this.getClusters().filter(c => !c.leaf);
}
- async removeClusterKubeConfigs(clusterUri: string): Promise {
- const {
- params: { rootClusterId },
- } = routing.parseClusterUri(clusterUri);
- return this.mainProcessClient.removeKubeConfig({
- relativePath: rootClusterId,
- isDirectory: true,
- });
- }
-
useState() {
return useStore(this).state;
}
- private async syncClusterInfo(clusterUri: uri.RootClusterUri) {
- try {
- const { response: cluster } = await this.client.getCluster({
- clusterUri,
- });
- this.setState(draft => {
- draft.clusters.set(clusterUri, cluster);
- });
- } catch (error) {
- this.setState(draft => {
- const cluster = draft.clusters.get(clusterUri);
- if (cluster) {
- // TODO(gzdunek): We should rather store the cluster synchronization status,
- // so the callsites could check it before reading the field.
- // The workaround is to update the field in case of a failure,
- // so the places that wait for showResources !== UNSPECIFIED don't get stuck indefinitely.
- cluster.showResources = ShowResources.ACCESSIBLE_ONLY;
+ private subscribeToClusterStore(): void {
+ this.mainProcessClient.subscribeToClusterStore(e => {
+ this.setState(c => {
+ if (e.kind === 'state') {
+ c.clusters = castDraft(e.value);
+ return;
}
+ applyPatches(c.clusters, e.value);
});
-
- throw error;
- }
+ });
}
}
diff --git a/web/packages/teleterm/src/ui/services/immutableStore/immutableStore.ts b/web/packages/teleterm/src/ui/services/immutableStore/immutableStore.ts
index 65979a1c04712..ae466e0fefc09 100644
--- a/web/packages/teleterm/src/ui/services/immutableStore/immutableStore.ts
+++ b/web/packages/teleterm/src/ui/services/immutableStore/immutableStore.ts
@@ -18,7 +18,7 @@
/* eslint-disable @typescript-eslint/ban-ts-comment*/
-import { enableMapSet, produce } from 'immer';
+import { enableMapSet, produce, Producer } from 'immer';
import stateLogger from 'shared/libs/stores/logger';
import Store from 'shared/libs/stores/store';
@@ -31,7 +31,7 @@ export class ImmutableStore extends Store {
protected logger = new Logger(this.constructor.name);
// @ts-ignore
- setState(nextState: (draftState: T) => T | void): void {
+ setState(nextState: Producer): void {
const prevState = this.state;
this.state = produce(this.state, nextState);
stateLogger.logState(this.constructor.name, prevState, 'with', this.state);
diff --git a/web/packages/teleterm/src/ui/services/statePersistence/statePersistenceService.ts b/web/packages/teleterm/src/ui/services/statePersistence/statePersistenceService.ts
index 2828ffc4c93b9..6c59b2c387946 100644
--- a/web/packages/teleterm/src/ui/services/statePersistence/statePersistenceService.ts
+++ b/web/packages/teleterm/src/ui/services/statePersistence/statePersistenceService.ts
@@ -64,6 +64,8 @@ export interface StatePersistenceState {
*/
hasEverStarted: boolean;
};
+ /** Shows a banner above the cluster list to notify that a new tsh home dir is used. */
+ showTshHomeMigrationBanner: boolean;
}
// Before adding new methods to this service, consider using usePersistedState instead.
diff --git a/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts b/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts
index 3f1c67beaf7c4..3f80718dd3a7f 100644
--- a/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts
+++ b/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts
@@ -154,52 +154,25 @@ describe('restoring workspace', () => {
};
const clusterBar = makeRootCluster({ uri: '/clusters/bar' });
const workspaceBar: PersistedWorkspace = {
+ color: 'purple',
localClusterUri: clusterBar.uri,
documents: [],
location: undefined,
};
const clusterBaz = makeRootCluster({ uri: '/clusters/baz' });
- const workspaceBaz: PersistedWorkspace = {
- localClusterUri: clusterBaz.uri,
- documents: [],
- location: undefined,
- };
const clusterQux = makeRootCluster({ uri: '/clusters/qux' });
- const workspaceQux: PersistedWorkspace = {
- localClusterUri: clusterQux.uri,
- documents: [],
- location: undefined,
- };
const clusterWaldo = makeRootCluster({ uri: '/clusters/waldo' });
- const workspaceWaldo: PersistedWorkspace = {
- localClusterUri: clusterWaldo.uri,
- documents: [],
- location: undefined,
- };
const clusterFred = makeRootCluster({ uri: '/clusters/fred' });
- const workspaceFred: PersistedWorkspace = {
- localClusterUri: clusterFred.uri,
- documents: [],
- location: undefined,
- };
const clusterGrault = makeRootCluster({ uri: '/clusters/grault' });
- const workspaceGrault: PersistedWorkspace = {
- localClusterUri: clusterGrault.uri,
- documents: [],
- location: undefined,
- };
const clusterPlugh = makeRootCluster({ uri: '/clusters/plugh' });
- const workspacePlugh: PersistedWorkspace = {
- localClusterUri: clusterPlugh.uri,
- documents: [],
- location: undefined,
- };
const { workspacesService } = getTestSetup({
cluster: [
clusterFoo,
- clusterBar,
+ // The workspace for clusterBaz has no assigned color, but clusterBar's workspace does.
+ // Return clusterBaz first to verify that it receives a new, unused color.
clusterBaz,
+ clusterBar,
clusterQux,
clusterWaldo,
clusterFred,
@@ -209,20 +182,14 @@ describe('restoring workspace', () => {
persistedWorkspaces: {
[clusterFoo.uri]: workspaceFoo,
[clusterBar.uri]: workspaceBar,
- [clusterBaz.uri]: workspaceBaz,
- [clusterQux.uri]: workspaceQux,
- [clusterWaldo.uri]: workspaceWaldo,
- [clusterFred.uri]: workspaceFred,
- [clusterGrault.uri]: workspaceGrault,
- [clusterPlugh.uri]: workspacePlugh,
},
});
workspacesService.restorePersistedState();
expect(workspacesService.getWorkspace(clusterFoo.uri).color).toBe('blue'); // read from disk
- expect(workspacesService.getWorkspace(clusterBar.uri).color).toBe('purple'); // the first unused color
- expect(workspacesService.getWorkspace(clusterBaz.uri).color).toBe('green');
+ expect(workspacesService.getWorkspace(clusterBar.uri).color).toBe('purple'); // read from disk
+ expect(workspacesService.getWorkspace(clusterBaz.uri).color).toBe('green'); // the first unused color
expect(workspacesService.getWorkspace(clusterQux.uri).color).toBe('yellow');
expect(workspacesService.getWorkspace(clusterWaldo.uri).color).toBe('red');
expect(workspacesService.getWorkspace(clusterFred.uri).color).toBe('cyan');
diff --git a/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts b/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts
index cba7fab5f5407..636dc7f69aaa9 100644
--- a/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts
+++ b/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts
@@ -352,6 +352,7 @@ export class WorkspacesService extends ImmutableStore {
}
if (cluster.profileStatusError) {
+ // TODO(gzdunek): We should only sync the target cluster, not all of them.
await this.clustersService.syncRootClustersAndCatchErrors(abortSignal);
// Update the cluster.
cluster = this.clustersService.findCluster(clusterUri);
@@ -465,6 +466,18 @@ export class WorkspacesService extends ImmutableStore {
});
}
+ clearWorkspace(clusterUri: RootClusterUri): void {
+ this.setState(draftState => {
+ draftState.workspaces[clusterUri] = getWorkspaceDefaultState(
+ clusterUri,
+ draftState.workspaces
+ );
+ });
+ this.restoredState = produce(this.restoredState, draftState => {
+ delete draftState.workspaces[clusterUri];
+ });
+ }
+
getConnectedWorkspacesClustersUri() {
return (Object.keys(this.state.workspaces) as RootClusterUri[]).filter(
clusterUri => this.clustersService.findCluster(clusterUri)?.connected
@@ -513,6 +526,14 @@ export class WorkspacesService extends ImmutableStore {
this.restoredState = produce(restoredState, () => {});
const restoredWorkspaces = this.clustersService
.getRootClusters()
+ // Start restoring clusters from the ones that already have a workspace.
+ // The algorithm that assigns a color in getWorkspaceDefaultState needs
+ // to know all used colors.
+ .toSorted((a, b) => {
+ const hasA = !!this.restoredState.workspaces[a.uri];
+ const hasB = !!this.restoredState.workspaces[b.uri];
+ return hasB === hasA ? 0 : hasA ? -1 : 1;
+ })
.reduce((workspaces, cluster) => {
const restoredWorkspace = this.restoredState.workspaces[cluster.uri];
workspaces[cluster.uri] = getWorkspaceDefaultState(
diff --git a/web/packages/teleterm/src/ui/types.ts b/web/packages/teleterm/src/ui/types.ts
index ed00bfb246c14..09ec13cfc14f5 100644
--- a/web/packages/teleterm/src/ui/types.ts
+++ b/web/packages/teleterm/src/ui/types.ts
@@ -39,6 +39,7 @@ import { TerminalsService } from 'teleterm/ui/services/terminals';
import { TshdNotificationsService } from 'teleterm/ui/services/tshdNotifications/tshdNotificationService';
import { UsageService } from 'teleterm/ui/services/usage';
import { WorkspacesService } from 'teleterm/ui/services/workspacesService';
+import { RootClusterUri } from 'teleterm/ui/uri';
export interface IAppContext {
clustersService: ClustersService;
@@ -83,8 +84,15 @@ export interface IAppContext {
* process (renderer).
*/
unexpectedVnetShutdownListener: UnexpectedVnetShutdownListener | undefined;
+
+ /** Sets the listener and returns a cleanup function which removes the listener. */
+ addResourceRefreshListener: (listener: ResourceRefreshListener) => () => void;
+ /** Gets called when `ClusterLifecycleManager` requests resource refresh. */
+ resourceRefreshListener: ResourceRefreshListener | undefined;
}
export type UnexpectedVnetShutdownListener = (
request: tshdEventsApi.ReportUnexpectedVnetShutdownRequest
) => void;
+
+export type ResourceRefreshListener = (uri: RootClusterUri) => void;