[vnet] feat: show SSH status in VNet slider (#55755)

* [vnet] feat: show SSH status in VNet slider

* rename rpc

* simplify when app and ssh zones match

* Avoid making DNS zones bold

* Fix the check for equal sets

* Add presets to VnetSliderStep story

* Fix case and punctuation

* Fix positioning of inline warning icon

* fix test

---------

Co-authored-by: Rafał Cieślak <rafal.cieslak@goteleport.com>
This commit is contained in:
Nic Klaassen
2025-06-19 15:49:11 +00:00
committed by GitHub
co-authored by Rafał Cieślak
parent 5029a83e6b
commit ff49ab0efd
14 changed files with 456 additions and 270 deletions
@@ -248,27 +248,27 @@ func (*StopResponse) Descriptor() ([]byte, []int) {
return file_teleport_lib_teleterm_vnet_v1_vnet_service_proto_rawDescGZIP(), []int{3}
}
// Request for ListDNSZones.
type ListDNSZonesRequest struct {
// Request for GetServiceInfo.
type GetServiceInfoRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListDNSZonesRequest) Reset() {
*x = ListDNSZonesRequest{}
func (x *GetServiceInfoRequest) Reset() {
*x = GetServiceInfoRequest{}
mi := &file_teleport_lib_teleterm_vnet_v1_vnet_service_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListDNSZonesRequest) String() string {
func (x *GetServiceInfoRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListDNSZonesRequest) ProtoMessage() {}
func (*GetServiceInfoRequest) ProtoMessage() {}
func (x *ListDNSZonesRequest) ProtoReflect() protoreflect.Message {
func (x *GetServiceInfoRequest) ProtoReflect() protoreflect.Message {
mi := &file_teleport_lib_teleterm_vnet_v1_vnet_service_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -280,34 +280,40 @@ func (x *ListDNSZonesRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use ListDNSZonesRequest.ProtoReflect.Descriptor instead.
func (*ListDNSZonesRequest) Descriptor() ([]byte, []int) {
// Deprecated: Use GetServiceInfoRequest.ProtoReflect.Descriptor instead.
func (*GetServiceInfoRequest) Descriptor() ([]byte, []int) {
return file_teleport_lib_teleterm_vnet_v1_vnet_service_proto_rawDescGZIP(), []int{4}
}
// Response for ListDNSZones.
type ListDNSZonesResponse struct {
// GetServiceInfoResponse contains the status of the running VNet service.
type GetServiceInfoResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
// dns_zones is a deduplicated list of DNS zones.
DnsZones []string `protobuf:"bytes,1,rep,name=dns_zones,json=dnsZones,proto3" json:"dns_zones,omitempty"`
// app_dns_zones is a deduplicated list of all DNS zones valid as DNS
// suffixes for connections to TCP apps.
AppDnsZones []string `protobuf:"bytes,1,rep,name=app_dns_zones,json=appDnsZones,proto3" json:"app_dns_zones,omitempty"`
// clusters is a list of cluster names valid as DNS suffixes for SSH hosts.
Clusters []string `protobuf:"bytes,2,rep,name=clusters,proto3" json:"clusters,omitempty"`
// ssh_configured is true if the user's SSH config file includes VNet's
// generated SSH config necessary for SSH access.
SshConfigured bool `protobuf:"varint,3,opt,name=ssh_configured,json=sshConfigured,proto3" json:"ssh_configured,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListDNSZonesResponse) Reset() {
*x = ListDNSZonesResponse{}
func (x *GetServiceInfoResponse) Reset() {
*x = GetServiceInfoResponse{}
mi := &file_teleport_lib_teleterm_vnet_v1_vnet_service_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListDNSZonesResponse) String() string {
func (x *GetServiceInfoResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListDNSZonesResponse) ProtoMessage() {}
func (*GetServiceInfoResponse) ProtoMessage() {}
func (x *ListDNSZonesResponse) ProtoReflect() protoreflect.Message {
func (x *GetServiceInfoResponse) ProtoReflect() protoreflect.Message {
mi := &file_teleport_lib_teleterm_vnet_v1_vnet_service_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -319,18 +325,32 @@ func (x *ListDNSZonesResponse) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use ListDNSZonesResponse.ProtoReflect.Descriptor instead.
func (*ListDNSZonesResponse) Descriptor() ([]byte, []int) {
// Deprecated: Use GetServiceInfoResponse.ProtoReflect.Descriptor instead.
func (*GetServiceInfoResponse) Descriptor() ([]byte, []int) {
return file_teleport_lib_teleterm_vnet_v1_vnet_service_proto_rawDescGZIP(), []int{5}
}
func (x *ListDNSZonesResponse) GetDnsZones() []string {
func (x *GetServiceInfoResponse) GetAppDnsZones() []string {
if x != nil {
return x.DnsZones
return x.AppDnsZones
}
return nil
}
func (x *GetServiceInfoResponse) GetClusters() []string {
if x != nil {
return x.Clusters
}
return nil
}
func (x *GetServiceInfoResponse) GetSshConfigured() bool {
if x != nil {
return x.SshConfigured
}
return false
}
// Request for GetBackgroundItemStatus.
type GetBackgroundItemStatusRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
@@ -503,10 +523,12 @@ const file_teleport_lib_teleterm_vnet_v1_vnet_service_proto_rawDesc = "" +
"\fStartRequest\"\x0f\n" +
"\rStartResponse\"\r\n" +
"\vStopRequest\"\x0e\n" +
"\fStopResponse\"\x15\n" +
"\x13ListDNSZonesRequest\"3\n" +
"\x14ListDNSZonesResponse\x12\x1b\n" +
"\tdns_zones\x18\x01 \x03(\tR\bdnsZones\" \n" +
"\fStopResponse\"\x17\n" +
"\x15GetServiceInfoRequest\"\x7f\n" +
"\x16GetServiceInfoResponse\x12\"\n" +
"\rapp_dns_zones\x18\x01 \x03(\tR\vappDnsZones\x12\x1a\n" +
"\bclusters\x18\x02 \x03(\tR\bclusters\x12%\n" +
"\x0essh_configured\x18\x03 \x01(\bR\rsshConfigured\" \n" +
"\x1eGetBackgroundItemStatusRequest\"n\n" +
"\x1fGetBackgroundItemStatusResponse\x12K\n" +
"\x06status\x18\x01 \x01(\x0e23.teleport.lib.teleterm.vnet.v1.BackgroundItemStatusR\x06status\"\x17\n" +
@@ -519,11 +541,11 @@ const file_teleport_lib_teleterm_vnet_v1_vnet_service_proto_rawDesc = "" +
"\x1eBACKGROUND_ITEM_STATUS_ENABLED\x10\x02\x12,\n" +
"(BACKGROUND_ITEM_STATUS_REQUIRES_APPROVAL\x10\x03\x12$\n" +
" BACKGROUND_ITEM_STATUS_NOT_FOUND\x10\x04\x12(\n" +
"$BACKGROUND_ITEM_STATUS_NOT_SUPPORTED\x10\x052\xe5\x04\n" +
"$BACKGROUND_ITEM_STATUS_NOT_SUPPORTED\x10\x052\xeb\x04\n" +
"\vVnetService\x12b\n" +
"\x05Start\x12+.teleport.lib.teleterm.vnet.v1.StartRequest\x1a,.teleport.lib.teleterm.vnet.v1.StartResponse\x12_\n" +
"\x04Stop\x12*.teleport.lib.teleterm.vnet.v1.StopRequest\x1a+.teleport.lib.teleterm.vnet.v1.StopResponse\x12w\n" +
"\fListDNSZones\x122.teleport.lib.teleterm.vnet.v1.ListDNSZonesRequest\x1a3.teleport.lib.teleterm.vnet.v1.ListDNSZonesResponse\x12\x98\x01\n" +
"\x04Stop\x12*.teleport.lib.teleterm.vnet.v1.StopRequest\x1a+.teleport.lib.teleterm.vnet.v1.StopResponse\x12}\n" +
"\x0eGetServiceInfo\x124.teleport.lib.teleterm.vnet.v1.GetServiceInfoRequest\x1a5.teleport.lib.teleterm.vnet.v1.GetServiceInfoResponse\x12\x98\x01\n" +
"\x17GetBackgroundItemStatus\x12=.teleport.lib.teleterm.vnet.v1.GetBackgroundItemStatusRequest\x1a>.teleport.lib.teleterm.vnet.v1.GetBackgroundItemStatusResponse\x12}\n" +
"\x0eRunDiagnostics\x124.teleport.lib.teleterm.vnet.v1.RunDiagnosticsRequest\x1a5.teleport.lib.teleterm.vnet.v1.RunDiagnosticsResponseBUZSgithub.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/vnet/v1;vnetv1b\x06proto3"
@@ -547,8 +569,8 @@ var file_teleport_lib_teleterm_vnet_v1_vnet_service_proto_goTypes = []any{
(*StartResponse)(nil), // 2: teleport.lib.teleterm.vnet.v1.StartResponse
(*StopRequest)(nil), // 3: teleport.lib.teleterm.vnet.v1.StopRequest
(*StopResponse)(nil), // 4: teleport.lib.teleterm.vnet.v1.StopResponse
(*ListDNSZonesRequest)(nil), // 5: teleport.lib.teleterm.vnet.v1.ListDNSZonesRequest
(*ListDNSZonesResponse)(nil), // 6: teleport.lib.teleterm.vnet.v1.ListDNSZonesResponse
(*GetServiceInfoRequest)(nil), // 5: teleport.lib.teleterm.vnet.v1.GetServiceInfoRequest
(*GetServiceInfoResponse)(nil), // 6: teleport.lib.teleterm.vnet.v1.GetServiceInfoResponse
(*GetBackgroundItemStatusRequest)(nil), // 7: teleport.lib.teleterm.vnet.v1.GetBackgroundItemStatusRequest
(*GetBackgroundItemStatusResponse)(nil), // 8: teleport.lib.teleterm.vnet.v1.GetBackgroundItemStatusResponse
(*RunDiagnosticsRequest)(nil), // 9: teleport.lib.teleterm.vnet.v1.RunDiagnosticsRequest
@@ -560,12 +582,12 @@ var file_teleport_lib_teleterm_vnet_v1_vnet_service_proto_depIdxs = []int32{
11, // 1: teleport.lib.teleterm.vnet.v1.RunDiagnosticsResponse.report:type_name -> teleport.lib.vnet.diag.v1.Report
1, // 2: teleport.lib.teleterm.vnet.v1.VnetService.Start:input_type -> teleport.lib.teleterm.vnet.v1.StartRequest
3, // 3: teleport.lib.teleterm.vnet.v1.VnetService.Stop:input_type -> teleport.lib.teleterm.vnet.v1.StopRequest
5, // 4: teleport.lib.teleterm.vnet.v1.VnetService.ListDNSZones:input_type -> teleport.lib.teleterm.vnet.v1.ListDNSZonesRequest
5, // 4: teleport.lib.teleterm.vnet.v1.VnetService.GetServiceInfo:input_type -> teleport.lib.teleterm.vnet.v1.GetServiceInfoRequest
7, // 5: teleport.lib.teleterm.vnet.v1.VnetService.GetBackgroundItemStatus:input_type -> teleport.lib.teleterm.vnet.v1.GetBackgroundItemStatusRequest
9, // 6: teleport.lib.teleterm.vnet.v1.VnetService.RunDiagnostics:input_type -> teleport.lib.teleterm.vnet.v1.RunDiagnosticsRequest
2, // 7: teleport.lib.teleterm.vnet.v1.VnetService.Start:output_type -> teleport.lib.teleterm.vnet.v1.StartResponse
4, // 8: teleport.lib.teleterm.vnet.v1.VnetService.Stop:output_type -> teleport.lib.teleterm.vnet.v1.StopResponse
6, // 9: teleport.lib.teleterm.vnet.v1.VnetService.ListDNSZones:output_type -> teleport.lib.teleterm.vnet.v1.ListDNSZonesResponse
6, // 9: teleport.lib.teleterm.vnet.v1.VnetService.GetServiceInfo:output_type -> teleport.lib.teleterm.vnet.v1.GetServiceInfoResponse
8, // 10: teleport.lib.teleterm.vnet.v1.VnetService.GetBackgroundItemStatus:output_type -> teleport.lib.teleterm.vnet.v1.GetBackgroundItemStatusResponse
10, // 11: teleport.lib.teleterm.vnet.v1.VnetService.RunDiagnostics:output_type -> teleport.lib.teleterm.vnet.v1.RunDiagnosticsResponse
7, // [7:12] is the sub-list for method output_type
@@ -37,7 +37,7 @@ const _ = grpc.SupportPackageIsVersion9
const (
VnetService_Start_FullMethodName = "/teleport.lib.teleterm.vnet.v1.VnetService/Start"
VnetService_Stop_FullMethodName = "/teleport.lib.teleterm.vnet.v1.VnetService/Stop"
VnetService_ListDNSZones_FullMethodName = "/teleport.lib.teleterm.vnet.v1.VnetService/ListDNSZones"
VnetService_GetServiceInfo_FullMethodName = "/teleport.lib.teleterm.vnet.v1.VnetService/GetServiceInfo"
VnetService_GetBackgroundItemStatus_FullMethodName = "/teleport.lib.teleterm.vnet.v1.VnetService/GetBackgroundItemStatus"
VnetService_RunDiagnostics_FullMethodName = "/teleport.lib.teleterm.vnet.v1.VnetService/RunDiagnostics"
)
@@ -52,16 +52,8 @@ type VnetServiceClient interface {
Start(ctx context.Context, in *StartRequest, opts ...grpc.CallOption) (*StartResponse, error)
// Stop stops VNet.
Stop(ctx context.Context, in *StopRequest, opts ...grpc.CallOption) (*StopResponse, error)
// ListDNSZones returns DNS zones of all root and leaf clusters with non-expired user certs. This
// includes the proxy service hostnames and custom DNS zones configured in vnet_config.
//
// This is fetched independently of what the Electron app thinks the current state of the cluster
// looks like, since the VNet admin process also fetches this data independently of the Electron
// app.
//
// Just like the admin process, it skips root and leaf clusters for which the vnet_config couldn't
// be fetched (due to e.g., a network error or an expired cert).
ListDNSZones(ctx context.Context, in *ListDNSZonesRequest, opts ...grpc.CallOption) (*ListDNSZonesResponse, error)
// GetServiceInfo returns info about the running VNet service.
GetServiceInfo(ctx context.Context, in *GetServiceInfoRequest, opts ...grpc.CallOption) (*GetServiceInfoResponse, error)
// GetBackgroundItemStatus returns the status of the background item responsible for launching
// VNet daemon. macOS only. tsh must be compiled with the vnetdaemon build tag.
GetBackgroundItemStatus(ctx context.Context, in *GetBackgroundItemStatusRequest, opts ...grpc.CallOption) (*GetBackgroundItemStatusResponse, error)
@@ -98,10 +90,10 @@ func (c *vnetServiceClient) Stop(ctx context.Context, in *StopRequest, opts ...g
return out, nil
}
func (c *vnetServiceClient) ListDNSZones(ctx context.Context, in *ListDNSZonesRequest, opts ...grpc.CallOption) (*ListDNSZonesResponse, error) {
func (c *vnetServiceClient) GetServiceInfo(ctx context.Context, in *GetServiceInfoRequest, opts ...grpc.CallOption) (*GetServiceInfoResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListDNSZonesResponse)
err := c.cc.Invoke(ctx, VnetService_ListDNSZones_FullMethodName, in, out, cOpts...)
out := new(GetServiceInfoResponse)
err := c.cc.Invoke(ctx, VnetService_GetServiceInfo_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -138,16 +130,8 @@ type VnetServiceServer interface {
Start(context.Context, *StartRequest) (*StartResponse, error)
// Stop stops VNet.
Stop(context.Context, *StopRequest) (*StopResponse, error)
// ListDNSZones returns DNS zones of all root and leaf clusters with non-expired user certs. This
// includes the proxy service hostnames and custom DNS zones configured in vnet_config.
//
// This is fetched independently of what the Electron app thinks the current state of the cluster
// looks like, since the VNet admin process also fetches this data independently of the Electron
// app.
//
// Just like the admin process, it skips root and leaf clusters for which the vnet_config couldn't
// be fetched (due to e.g., a network error or an expired cert).
ListDNSZones(context.Context, *ListDNSZonesRequest) (*ListDNSZonesResponse, error)
// GetServiceInfo returns info about the running VNet service.
GetServiceInfo(context.Context, *GetServiceInfoRequest) (*GetServiceInfoResponse, error)
// GetBackgroundItemStatus returns the status of the background item responsible for launching
// VNet daemon. macOS only. tsh must be compiled with the vnetdaemon build tag.
GetBackgroundItemStatus(context.Context, *GetBackgroundItemStatusRequest) (*GetBackgroundItemStatusResponse, error)
@@ -170,8 +154,8 @@ func (UnimplementedVnetServiceServer) Start(context.Context, *StartRequest) (*St
func (UnimplementedVnetServiceServer) Stop(context.Context, *StopRequest) (*StopResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Stop not implemented")
}
func (UnimplementedVnetServiceServer) ListDNSZones(context.Context, *ListDNSZonesRequest) (*ListDNSZonesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListDNSZones not implemented")
func (UnimplementedVnetServiceServer) GetServiceInfo(context.Context, *GetServiceInfoRequest) (*GetServiceInfoResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetServiceInfo not implemented")
}
func (UnimplementedVnetServiceServer) GetBackgroundItemStatus(context.Context, *GetBackgroundItemStatusRequest) (*GetBackgroundItemStatusResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetBackgroundItemStatus not implemented")
@@ -236,20 +220,20 @@ func _VnetService_Stop_Handler(srv interface{}, ctx context.Context, dec func(in
return interceptor(ctx, in, info, handler)
}
func _VnetService_ListDNSZones_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListDNSZonesRequest)
func _VnetService_GetServiceInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetServiceInfoRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(VnetServiceServer).ListDNSZones(ctx, in)
return srv.(VnetServiceServer).GetServiceInfo(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: VnetService_ListDNSZones_FullMethodName,
FullMethod: VnetService_GetServiceInfo_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(VnetServiceServer).ListDNSZones(ctx, req.(*ListDNSZonesRequest))
return srv.(VnetServiceServer).GetServiceInfo(ctx, req.(*GetServiceInfoRequest))
}
return interceptor(ctx, in, info, handler)
}
@@ -306,8 +290,8 @@ var VnetService_ServiceDesc = grpc.ServiceDesc{
Handler: _VnetService_Stop_Handler,
},
{
MethodName: "ListDNSZones",
Handler: _VnetService_ListDNSZones_Handler,
MethodName: "GetServiceInfo",
Handler: _VnetService_GetServiceInfo_Handler,
},
{
MethodName: "GetBackgroundItemStatus",
@@ -27,8 +27,8 @@ import type { RunDiagnosticsResponse } from "./vnet_service_pb";
import type { RunDiagnosticsRequest } from "./vnet_service_pb";
import type { GetBackgroundItemStatusResponse } from "./vnet_service_pb";
import type { GetBackgroundItemStatusRequest } from "./vnet_service_pb";
import type { ListDNSZonesResponse } from "./vnet_service_pb";
import type { ListDNSZonesRequest } from "./vnet_service_pb";
import type { GetServiceInfoResponse } from "./vnet_service_pb";
import type { GetServiceInfoRequest } from "./vnet_service_pb";
import type { StopResponse } from "./vnet_service_pb";
import type { StopRequest } from "./vnet_service_pb";
import { stackIntercept } from "@protobuf-ts/runtime-rpc";
@@ -55,19 +55,11 @@ export interface IVnetServiceClient {
*/
stop(input: StopRequest, options?: RpcOptions): UnaryCall<StopRequest, StopResponse>;
/**
* ListDNSZones returns DNS zones of all root and leaf clusters with non-expired user certs. This
* includes the proxy service hostnames and custom DNS zones configured in vnet_config.
* GetServiceInfo returns info about the running VNet service.
*
* This is fetched independently of what the Electron app thinks the current state of the cluster
* looks like, since the VNet admin process also fetches this data independently of the Electron
* app.
*
* Just like the admin process, it skips root and leaf clusters for which the vnet_config couldn't
* be fetched (due to e.g., a network error or an expired cert).
*
* @generated from protobuf rpc: ListDNSZones(teleport.lib.teleterm.vnet.v1.ListDNSZonesRequest) returns (teleport.lib.teleterm.vnet.v1.ListDNSZonesResponse);
* @generated from protobuf rpc: GetServiceInfo(teleport.lib.teleterm.vnet.v1.GetServiceInfoRequest) returns (teleport.lib.teleterm.vnet.v1.GetServiceInfoResponse);
*/
listDNSZones(input: ListDNSZonesRequest, options?: RpcOptions): UnaryCall<ListDNSZonesRequest, ListDNSZonesResponse>;
getServiceInfo(input: GetServiceInfoRequest, options?: RpcOptions): UnaryCall<GetServiceInfoRequest, GetServiceInfoResponse>;
/**
* GetBackgroundItemStatus returns the status of the background item responsible for launching
* VNet daemon. macOS only. tsh must be compiled with the vnetdaemon build tag.
@@ -113,21 +105,13 @@ export class VnetServiceClient implements IVnetServiceClient, ServiceInfo {
return stackIntercept<StopRequest, StopResponse>("unary", this._transport, method, opt, input);
}
/**
* ListDNSZones returns DNS zones of all root and leaf clusters with non-expired user certs. This
* includes the proxy service hostnames and custom DNS zones configured in vnet_config.
* GetServiceInfo returns info about the running VNet service.
*
* This is fetched independently of what the Electron app thinks the current state of the cluster
* looks like, since the VNet admin process also fetches this data independently of the Electron
* app.
*
* Just like the admin process, it skips root and leaf clusters for which the vnet_config couldn't
* be fetched (due to e.g., a network error or an expired cert).
*
* @generated from protobuf rpc: ListDNSZones(teleport.lib.teleterm.vnet.v1.ListDNSZonesRequest) returns (teleport.lib.teleterm.vnet.v1.ListDNSZonesResponse);
* @generated from protobuf rpc: GetServiceInfo(teleport.lib.teleterm.vnet.v1.GetServiceInfoRequest) returns (teleport.lib.teleterm.vnet.v1.GetServiceInfoResponse);
*/
listDNSZones(input: ListDNSZonesRequest, options?: RpcOptions): UnaryCall<ListDNSZonesRequest, ListDNSZonesResponse> {
getServiceInfo(input: GetServiceInfoRequest, options?: RpcOptions): UnaryCall<GetServiceInfoRequest, GetServiceInfoResponse> {
const method = this.methods[2], opt = this._transport.mergeOptions(options);
return stackIntercept<ListDNSZonesRequest, ListDNSZonesResponse>("unary", this._transport, method, opt, input);
return stackIntercept<GetServiceInfoRequest, GetServiceInfoResponse>("unary", this._transport, method, opt, input);
}
/**
* GetBackgroundItemStatus returns the status of the background item responsible for launching
@@ -24,8 +24,8 @@ import { RunDiagnosticsResponse } from "./vnet_service_pb";
import { RunDiagnosticsRequest } from "./vnet_service_pb";
import { GetBackgroundItemStatusResponse } from "./vnet_service_pb";
import { GetBackgroundItemStatusRequest } from "./vnet_service_pb";
import { ListDNSZonesResponse } from "./vnet_service_pb";
import { ListDNSZonesRequest } from "./vnet_service_pb";
import { GetServiceInfoResponse } from "./vnet_service_pb";
import { GetServiceInfoRequest } from "./vnet_service_pb";
import { StopResponse } from "./vnet_service_pb";
import { StopRequest } from "./vnet_service_pb";
import { StartResponse } from "./vnet_service_pb";
@@ -50,19 +50,11 @@ export interface IVnetService extends grpc.UntypedServiceImplementation {
*/
stop: grpc.handleUnaryCall<StopRequest, StopResponse>;
/**
* ListDNSZones returns DNS zones of all root and leaf clusters with non-expired user certs. This
* includes the proxy service hostnames and custom DNS zones configured in vnet_config.
* GetServiceInfo returns info about the running VNet service.
*
* This is fetched independently of what the Electron app thinks the current state of the cluster
* looks like, since the VNet admin process also fetches this data independently of the Electron
* app.
*
* Just like the admin process, it skips root and leaf clusters for which the vnet_config couldn't
* be fetched (due to e.g., a network error or an expired cert).
*
* @generated from protobuf rpc: ListDNSZones(teleport.lib.teleterm.vnet.v1.ListDNSZonesRequest) returns (teleport.lib.teleterm.vnet.v1.ListDNSZonesResponse);
* @generated from protobuf rpc: GetServiceInfo(teleport.lib.teleterm.vnet.v1.GetServiceInfoRequest) returns (teleport.lib.teleterm.vnet.v1.GetServiceInfoResponse);
*/
listDNSZones: grpc.handleUnaryCall<ListDNSZonesRequest, ListDNSZonesResponse>;
getServiceInfo: grpc.handleUnaryCall<GetServiceInfoRequest, GetServiceInfoResponse>;
/**
* GetBackgroundItemStatus returns the status of the background item responsible for launching
* VNet daemon. macOS only. tsh must be compiled with the vnetdaemon build tag.
@@ -110,15 +102,15 @@ export const vnetServiceDefinition: grpc.ServiceDefinition<IVnetService> = {
responseSerialize: value => Buffer.from(StopResponse.toBinary(value)),
requestSerialize: value => Buffer.from(StopRequest.toBinary(value))
},
listDNSZones: {
path: "/teleport.lib.teleterm.vnet.v1.VnetService/ListDNSZones",
originalName: "ListDNSZones",
getServiceInfo: {
path: "/teleport.lib.teleterm.vnet.v1.VnetService/GetServiceInfo",
originalName: "GetServiceInfo",
requestStream: false,
responseStream: false,
responseDeserialize: bytes => ListDNSZonesResponse.fromBinary(bytes),
requestDeserialize: bytes => ListDNSZonesRequest.fromBinary(bytes),
responseSerialize: value => Buffer.from(ListDNSZonesResponse.toBinary(value)),
requestSerialize: value => Buffer.from(ListDNSZonesRequest.toBinary(value))
responseDeserialize: bytes => GetServiceInfoResponse.fromBinary(bytes),
requestDeserialize: bytes => GetServiceInfoRequest.fromBinary(bytes),
responseSerialize: value => Buffer.from(GetServiceInfoResponse.toBinary(value)),
requestSerialize: value => Buffer.from(GetServiceInfoRequest.toBinary(value))
},
getBackgroundItemStatus: {
path: "/teleport.lib.teleterm.vnet.v1.VnetService/GetBackgroundItemStatus",
+63 -33
View File
@@ -60,24 +60,38 @@ export interface StopRequest {
export interface StopResponse {
}
/**
* Request for ListDNSZones.
* Request for GetServiceInfo.
*
* @generated from protobuf message teleport.lib.teleterm.vnet.v1.ListDNSZonesRequest
* @generated from protobuf message teleport.lib.teleterm.vnet.v1.GetServiceInfoRequest
*/
export interface ListDNSZonesRequest {
export interface GetServiceInfoRequest {
}
/**
* Response for ListDNSZones.
* GetServiceInfoResponse contains the status of the running VNet service.
*
* @generated from protobuf message teleport.lib.teleterm.vnet.v1.ListDNSZonesResponse
* @generated from protobuf message teleport.lib.teleterm.vnet.v1.GetServiceInfoResponse
*/
export interface ListDNSZonesResponse {
export interface GetServiceInfoResponse {
/**
* dns_zones is a deduplicated list of DNS zones.
* app_dns_zones is a deduplicated list of all DNS zones valid as DNS
* suffixes for connections to TCP apps.
*
* @generated from protobuf field: repeated string dns_zones = 1;
* @generated from protobuf field: repeated string app_dns_zones = 1;
*/
dnsZones: string[];
appDnsZones: string[];
/**
* clusters is a list of cluster names valid as DNS suffixes for SSH hosts.
*
* @generated from protobuf field: repeated string clusters = 2;
*/
clusters: string[];
/**
* ssh_configured is true if the user's SSH config file includes VNet's
* generated SSH config necessary for SSH access.
*
* @generated from protobuf field: bool ssh_configured = 3;
*/
sshConfigured: boolean;
}
/**
* Request for GetBackgroundItemStatus.
@@ -251,20 +265,20 @@ class StopResponse$Type extends MessageType<StopResponse> {
*/
export const StopResponse = new StopResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class ListDNSZonesRequest$Type extends MessageType<ListDNSZonesRequest> {
class GetServiceInfoRequest$Type extends MessageType<GetServiceInfoRequest> {
constructor() {
super("teleport.lib.teleterm.vnet.v1.ListDNSZonesRequest", []);
super("teleport.lib.teleterm.vnet.v1.GetServiceInfoRequest", []);
}
create(value?: PartialMessage<ListDNSZonesRequest>): ListDNSZonesRequest {
create(value?: PartialMessage<GetServiceInfoRequest>): GetServiceInfoRequest {
const message = globalThis.Object.create((this.messagePrototype!));
if (value !== undefined)
reflectionMergePartial<ListDNSZonesRequest>(this, message, value);
reflectionMergePartial<GetServiceInfoRequest>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ListDNSZonesRequest): ListDNSZonesRequest {
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetServiceInfoRequest): GetServiceInfoRequest {
return target ?? this.create();
}
internalBinaryWrite(message: ListDNSZonesRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
internalBinaryWrite(message: GetServiceInfoRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -272,30 +286,40 @@ class ListDNSZonesRequest$Type extends MessageType<ListDNSZonesRequest> {
}
}
/**
* @generated MessageType for protobuf message teleport.lib.teleterm.vnet.v1.ListDNSZonesRequest
* @generated MessageType for protobuf message teleport.lib.teleterm.vnet.v1.GetServiceInfoRequest
*/
export const ListDNSZonesRequest = new ListDNSZonesRequest$Type();
export const GetServiceInfoRequest = new GetServiceInfoRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class ListDNSZonesResponse$Type extends MessageType<ListDNSZonesResponse> {
class GetServiceInfoResponse$Type extends MessageType<GetServiceInfoResponse> {
constructor() {
super("teleport.lib.teleterm.vnet.v1.ListDNSZonesResponse", [
{ no: 1, name: "dns_zones", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }
super("teleport.lib.teleterm.vnet.v1.GetServiceInfoResponse", [
{ no: 1, name: "app_dns_zones", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "clusters", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ },
{ no: 3, name: "ssh_configured", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }
]);
}
create(value?: PartialMessage<ListDNSZonesResponse>): ListDNSZonesResponse {
create(value?: PartialMessage<GetServiceInfoResponse>): GetServiceInfoResponse {
const message = globalThis.Object.create((this.messagePrototype!));
message.dnsZones = [];
message.appDnsZones = [];
message.clusters = [];
message.sshConfigured = false;
if (value !== undefined)
reflectionMergePartial<ListDNSZonesResponse>(this, message, value);
reflectionMergePartial<GetServiceInfoResponse>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ListDNSZonesResponse): ListDNSZonesResponse {
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetServiceInfoResponse): GetServiceInfoResponse {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* repeated string dns_zones */ 1:
message.dnsZones.push(reader.string());
case /* repeated string app_dns_zones */ 1:
message.appDnsZones.push(reader.string());
break;
case /* repeated string clusters */ 2:
message.clusters.push(reader.string());
break;
case /* bool ssh_configured */ 3:
message.sshConfigured = reader.bool();
break;
default:
let u = options.readUnknownField;
@@ -308,10 +332,16 @@ class ListDNSZonesResponse$Type extends MessageType<ListDNSZonesResponse> {
}
return message;
}
internalBinaryWrite(message: ListDNSZonesResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* repeated string dns_zones = 1; */
for (let i = 0; i < message.dnsZones.length; i++)
writer.tag(1, WireType.LengthDelimited).string(message.dnsZones[i]);
internalBinaryWrite(message: GetServiceInfoResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* repeated string app_dns_zones = 1; */
for (let i = 0; i < message.appDnsZones.length; i++)
writer.tag(1, WireType.LengthDelimited).string(message.appDnsZones[i]);
/* repeated string clusters = 2; */
for (let i = 0; i < message.clusters.length; i++)
writer.tag(2, WireType.LengthDelimited).string(message.clusters[i]);
/* bool ssh_configured = 3; */
if (message.sshConfigured !== false)
writer.tag(3, WireType.Varint).bool(message.sshConfigured);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -319,9 +349,9 @@ class ListDNSZonesResponse$Type extends MessageType<ListDNSZonesResponse> {
}
}
/**
* @generated MessageType for protobuf message teleport.lib.teleterm.vnet.v1.ListDNSZonesResponse
* @generated MessageType for protobuf message teleport.lib.teleterm.vnet.v1.GetServiceInfoResponse
*/
export const ListDNSZonesResponse = new ListDNSZonesResponse$Type();
export const GetServiceInfoResponse = new GetServiceInfoResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class GetBackgroundItemStatusRequest$Type extends MessageType<GetBackgroundItemStatusRequest> {
constructor() {
@@ -471,7 +501,7 @@ export const RunDiagnosticsResponse = new RunDiagnosticsResponse$Type();
export const VnetService = new ServiceType("teleport.lib.teleterm.vnet.v1.VnetService", [
{ name: "Start", options: {}, I: StartRequest, O: StartResponse },
{ name: "Stop", options: {}, I: StopRequest, O: StopResponse },
{ name: "ListDNSZones", options: {}, I: ListDNSZonesRequest, O: ListDNSZonesResponse },
{ name: "GetServiceInfo", options: {}, I: GetServiceInfoRequest, O: GetServiceInfoResponse },
{ name: "GetBackgroundItemStatus", options: {}, I: GetBackgroundItemStatusRequest, O: GetBackgroundItemStatusResponse },
{ name: "RunDiagnostics", options: {}, I: RunDiagnosticsRequest, O: RunDiagnosticsResponse }
]);
+22 -9
View File
@@ -35,6 +35,7 @@ import (
prehogv1alpha "github.com/gravitational/teleport/gen/proto/go/prehog/v1alpha"
apiteleterm "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1"
api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/vnet/v1"
diagv1 "github.com/gravitational/teleport/gen/proto/go/teleport/lib/vnet/diag/v1"
vnetv1 "github.com/gravitational/teleport/gen/proto/go/teleport/lib/vnet/v1"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/teleterm/api/uri"
@@ -43,6 +44,7 @@ import (
"github.com/gravitational/teleport/lib/teleterm/daemon"
logutils "github.com/gravitational/teleport/lib/utils/log"
"github.com/gravitational/teleport/lib/vnet"
"github.com/gravitational/teleport/lib/vnet/diag"
)
var log = logutils.NewPackageLogger(teleport.ComponentKey, "term:vnet")
@@ -219,13 +221,8 @@ func (s *Service) Stop(ctx context.Context, req *api.StopRequest) (*api.StopResp
return &api.StopResponse{}, nil
}
// ListDNSZones returns DNS zones of all root and leaf clusters with non-expired user certs. This
// includes the proxy service hostnames and custom DNS zones configured in vnet_config.
//
// This is fetched exactly the same way the VNet process fetches the DNS zones
// but may be slightly out of sync with the OS configuration if the admin
// process hasn't configured a recent change yet.
func (s *Service) ListDNSZones(ctx context.Context, req *api.ListDNSZonesRequest) (*api.ListDNSZonesResponse, error) {
// GetServiceInfo returns info about the running VNet service.
func (s *Service) GetServiceInfo(ctx context.Context, _ *api.GetServiceInfoRequest) (*api.GetServiceInfoResponse, error) {
// Acquire the lock just to check the status of the service. We don't want the actual process of
// listing DNS zones to block the user from performing other operations.
s.mu.Lock()
@@ -240,8 +237,24 @@ func (s *Service) ListDNSZones(ctx context.Context, req *api.ListDNSZonesRequest
if err != nil {
return nil, trace.Wrap(err)
}
return &api.ListDNSZonesResponse{
DnsZones: unifiedClusterConfig.AppDNSZones(),
sshDiag, err := diag.NewSSHDiag(&diag.SSHConfig{
ProfilePath: s.cfg.profilePath,
})
if err != nil {
return nil, trace.Wrap(err, "building SSH diagnostic")
}
sshReport, err := sshDiag.Run(ctx)
if err != nil {
return nil, trace.Wrap(err, "running SSH diagnostic")
}
sshConfigured := sshReport.Status == diagv1.CheckReportStatus_CHECK_REPORT_STATUS_OK &&
sshReport.GetSshConfigurationReport().UserOpensshConfigIncludesVnetSshConfig
return &api.GetServiceInfoResponse{
AppDnsZones: unifiedClusterConfig.AppDNSZones(),
Clusters: unifiedClusterConfig.ClusterNames,
SshConfigured: sshConfigured,
}, nil
}
@@ -30,16 +30,8 @@ service VnetService {
// Stop stops VNet.
rpc Stop(StopRequest) returns (StopResponse);
// ListDNSZones returns DNS zones of all root and leaf clusters with non-expired user certs. This
// includes the proxy service hostnames and custom DNS zones configured in vnet_config.
//
// This is fetched independently of what the Electron app thinks the current state of the cluster
// looks like, since the VNet admin process also fetches this data independently of the Electron
// app.
//
// Just like the admin process, it skips root and leaf clusters for which the vnet_config couldn't
// be fetched (due to e.g., a network error or an expired cert).
rpc ListDNSZones(ListDNSZonesRequest) returns (ListDNSZonesResponse);
// GetServiceInfo returns info about the running VNet service.
rpc GetServiceInfo(GetServiceInfoRequest) returns (GetServiceInfoResponse);
// GetBackgroundItemStatus returns the status of the background item responsible for launching
// VNet daemon. macOS only. tsh must be compiled with the vnetdaemon build tag.
@@ -62,13 +54,19 @@ message StopRequest {}
// Response for Stop.
message StopResponse {}
// Request for ListDNSZones.
message ListDNSZonesRequest {}
// Request for GetServiceInfo.
message GetServiceInfoRequest {}
// Response for ListDNSZones.
message ListDNSZonesResponse {
// dns_zones is a deduplicated list of DNS zones.
repeated string dns_zones = 1;
// GetServiceInfoResponse contains the status of the running VNet service.
message GetServiceInfoResponse {
// app_dns_zones is a deduplicated list of all DNS zones valid as DNS
// suffixes for connections to TCP apps.
repeated string app_dns_zones = 1;
// clusters is a list of cluster names valid as DNS suffixes for SSH hosts.
repeated string clusters = 2;
// ssh_configured is true if the user's SSH config file includes VNet's
// generated SSH config necessary for SSH access.
bool ssh_configured = 3;
}
// Request for GetBackgroundItemStatus.
@@ -118,7 +118,12 @@ export class MockTshClient implements TshdClient {
export class MockVnetClient implements VnetClient {
start = () => new MockedUnaryCall({});
stop = () => new MockedUnaryCall({});
listDNSZones = () => new MockedUnaryCall({ dnsZones: [] });
getServiceInfo = () =>
new MockedUnaryCall({
appDnsZones: [],
clusters: [],
sshConfigured: false,
});
getBackgroundItemStatus = () => new MockedUnaryCall({ status: 0 });
runDiagnostics() {
@@ -21,6 +21,7 @@ import styled from 'styled-components';
import { ButtonIcon, Flex, Text } from 'design';
import { Trash, Unlink } from 'design/Icon';
import { typography, TypographyProps } from 'design/system';
import { useKeyboardArrowsNavigation } from 'teleterm/ui/components/KeyboardArrowsNavigation';
import { ListItem } from 'teleterm/ui/components/ListItem';
@@ -98,18 +99,9 @@ export function ConnectionItem(props: {
line-height: 16px;
`}
>
<span
css={`
font-size: 10px;
background: ${props => props.theme.colors.spotBackground[2]};
opacity: 0.85;
padding: 1px 2px;
margin-right: 4px;
border-radius: 4px;
`}
>
<ConnectionKindIndicator>
{getKindName(props.item)}
</span>
</ConnectionKindIndicator>
<span
css={`
vertical-align: middle;
@@ -152,6 +144,16 @@ const ConnectionListItem = styled(ListItem)<{ $showClusterName?: boolean }>`
height: unset;
`;
export const ConnectionKindIndicator = styled.span<TypographyProps>`
font-size: 10px;
background: ${props => props.theme.colors.spotBackground[2]};
opacity: 0.85;
padding: 1px 2px;
margin-right: 4px;
border-radius: 4px;
${typography}
`;
function getKindName(connection: ExtendedTrackedConnection): string {
switch (connection.kind) {
case 'connection.gateway':
@@ -109,6 +109,15 @@ const StyledStatus = styled(Box)<{
&:after {
content: '⚠';
font-size: 12px;
${props.$inline &&
`
// This cuts out a little portion of the icon on the left. This is most clearly visible
// on Windows. But at least it better aligns with the other statuses.
//
// TODO(ravicious): Rewrite this to not use weird characters to represent different
// statuses so that all statuses properly align together.
margin: -1px;
`}
${!props.$inline &&
`
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Meta } from '@storybook/react';
import { Meta, StoryObj } from '@storybook/react';
import { useEffect } from 'react';
import { Box } from 'design';
@@ -38,8 +38,10 @@ import { VnetSliderStep as Component } from './VnetSliderStep';
type StoryProps = {
startVnet: 'success' | 'error' | 'processing';
autoStart: boolean;
dnsZones: string[];
listDnsZones:
appDnsZones: string[];
clusters: string[];
sshConfigured: boolean;
fetchStatus:
| 'success'
| 'error'
| 'processing'
@@ -51,6 +53,20 @@ type StoryProps = {
unexpectedShutdown: boolean;
};
const defaultArgs: StoryProps = {
startVnet: 'success',
autoStart: true,
appDnsZones: ['teleport.example.com', 'company.test'],
clusters: ['teleport.example.com'],
sshConfigured: false,
fetchStatus: 'success',
vnetDiag: true,
runDiagnostics: 'success',
diagReport: 'ok',
isWorkspacePresent: true,
unexpectedShutdown: false,
};
const meta: Meta<StoryProps> = {
title: 'Teleterm/Vnet/VnetSliderStep',
component: VnetSliderStep,
@@ -68,10 +84,13 @@ const meta: Meta<StoryProps> = {
control: { type: 'inline-radio' },
options: ['success', 'error', 'processing'],
},
dnsZones: {
appDnsZones: {
control: { type: 'object' },
},
listDnsZones: {
clusters: {
control: { type: 'object' },
},
fetchStatus: {
control: { type: 'inline-radio' },
options: [
'success',
@@ -93,21 +112,11 @@ const meta: Meta<StoryProps> = {
"If there's no workspace, the button to open the diag report is disabled.",
},
},
args: {
startVnet: 'success',
autoStart: true,
dnsZones: ['teleport.example.com', 'company.test'],
listDnsZones: 'success',
vnetDiag: true,
runDiagnostics: 'success',
diagReport: 'ok',
isWorkspacePresent: true,
unexpectedShutdown: false,
},
render: props => <VnetSliderStep {...props} />,
};
export default meta;
export function VnetSliderStep(props: StoryProps) {
function VnetSliderStep(props: StoryProps) {
const appContext = new MockAppContext({
platform: props.vnetDiag ? 'darwin' : 'win32',
});
@@ -148,22 +157,30 @@ export function VnetSliderStep(props: StoryProps) {
};
}
if (props.listDnsZones === 'processing') {
appContext.vnet.listDNSZones = () => pendingPromise;
if (props.fetchStatus === 'processing') {
appContext.vnet.getServiceInfo = () => pendingPromise;
} else {
let firstCall = true;
appContext.vnet.listDNSZones = () => {
if (props.listDnsZones === 'processing-with-previous-results') {
appContext.vnet.getServiceInfo = () => {
if (props.fetchStatus === 'processing-with-previous-results') {
if (firstCall) {
firstCall = false;
return new MockedUnaryCall({ dnsZones: props.dnsZones });
return new MockedUnaryCall({
appDnsZones: props.appDnsZones,
clusters: props.clusters,
sshConfigured: props.sshConfigured,
});
}
return pendingPromise;
}
return new MockedUnaryCall(
{ dnsZones: props.dnsZones },
props.listDnsZones === 'error'
{
appDnsZones: props.appDnsZones,
clusters: props.clusters,
sshConfigured: props.sshConfigured,
},
props.fetchStatus === 'error'
? new Error('something went wrong')
: undefined
);
@@ -204,8 +221,8 @@ export function VnetSliderStep(props: StoryProps) {
>
<ConnectionsContextProvider>
<VnetContextProvider>
{props.listDnsZones === 'processing-with-previous-results' && (
<RerequestDNSZones />
{props.fetchStatus === 'processing-with-previous-results' && (
<RerequestServiceInfo />
)}
<Component
refCallback={noop}
@@ -221,16 +238,73 @@ export function VnetSliderStep(props: StoryProps) {
);
}
const RerequestDNSZones = () => {
const { listDNSZones, listDNSZonesAttempt } = useVnetContext();
const RerequestServiceInfo = () => {
const { getServiceInfo, serviceInfoAttempt } = useVnetContext();
useEffect(() => {
if (listDNSZonesAttempt.status === 'success') {
listDNSZones();
if (serviceInfoAttempt.status === 'success') {
getServiceInfo();
}
}, [listDNSZonesAttempt, listDNSZones]);
}, [serviceInfoAttempt, getServiceInfo]);
return null;
};
const noop = () => {};
export const CloudCustomer: StoryObj<StoryProps> = {
args: {
...defaultArgs,
appDnsZones: ['example.teleport.sh'],
clusters: ['example.teleport.sh'],
},
};
export const SelfHostedWithDifferentClusterName: StoryObj<StoryProps> = {
args: {
...defaultArgs,
appDnsZones: ['teleport.example.com'],
clusters: ['teleport-example'],
},
};
export const SelfHostedWithEqualNameAndLeaf: StoryObj<StoryProps> = {
args: {
...defaultArgs,
appDnsZones: ['teleport.example.com', 'leaf.example.com'],
clusters: ['teleport.example.com', 'leaf.example.com'],
},
};
export const SelfHostedWithEqualNameAndDifferentLeaf: StoryObj<StoryProps> = {
args: {
...defaultArgs,
appDnsZones: ['teleport.example.com', 'leaf.example.com'],
clusters: ['teleport.example.com', 'teleport-leaf'],
},
};
export const SelfHostedWithEqualNameAndCustomDNSZones: StoryObj<StoryProps> = {
args: {
...defaultArgs,
appDnsZones: ['teleport.example.com', 'company.com', 'apps.company'],
clusters: ['teleport.example.com'],
},
};
export const SelfHostedWithManyLeavesAndZones: StoryObj<StoryProps> = {
args: {
...defaultArgs,
appDnsZones: [
'teleport.example.com',
'leaf.example.com',
'second-leaf.example.com',
'company.com',
],
clusters: [
'teleport.example.com',
'teleport-leaf',
'second-leaf.example.com',
],
},
};
@@ -24,6 +24,7 @@ import { useRefAutoFocus } from 'shared/hooks';
import { useDelayedRepeatedAttempt } from 'shared/hooks/useAsync';
import { mergeRefs } from 'shared/libs/mergeRefs';
import { ConnectionKindIndicator } from 'teleterm/ui/TopBar/Connections/ConnectionsFilterableList/ConnectionItem';
import { ConnectionStatusIndicator } from 'teleterm/ui/TopBar/Connections/ConnectionsFilterableList/ConnectionStatusIndicator';
import { DiagnosticsAlert } from './DiagnosticsAlert';
@@ -104,18 +105,19 @@ export const VnetSliderStep = (props: StepComponentProps) => {
) : (
<Flex flexDirection="column" gap={1}>
<Text>
VNet enables any program to connect to TCP apps protected by
Teleport.
VNet enables any program to connect to TCP apps or SSH servers
protected by Teleport.
</Text>
<Text>
Start VNet and connect to any TCP app over its public address –
VNet authenticates the connection for you under the hood.
Start VNet and connect to any TCP app or SSH server at its own
DNS address – VNet authenticates the connection for you under
the hood.
</Text>
</Flex>
))}
</Flex>
{status.value === 'running' && <DnsZones />}
{status.value === 'running' && <VnetStatus />}
<DiagnosticsAlert
runDiagnosticsFromVnetPanel={runDiagnosticsFromVnetPanel}
@@ -132,41 +134,39 @@ const ErrorText = (props: PropsWithChildren) => (
);
/**
* DnsZones displays the list of currently proxied DNS zones, as understood by the VNet admin
* process. The list is cached in the context and updated when the VNet panel gets opened.
* VnetStatus displays the status of the running VNet service. The list is cached in the context and
* updated when the VNet panel gets opened.
*
* As for 95% of users the list will never change during the lifespan of VNet, the VNet panel always
* optimistically displays previously fetched results while fetching new list.
*/
const DnsZones = () => {
const { listDNSZones, listDNSZonesAttempt: eagerListDNSZonesAttempt } =
const VnetStatus = () => {
const { getServiceInfo, serviceInfoAttempt: eagerServiceInfoAttempt } =
useVnetContext();
const listDNSZonesAttempt = useDelayedRepeatedAttempt(
eagerListDNSZonesAttempt
);
const dnsZonesRefreshRequestedRef = useRef(false);
const serviceInfoAttempt = useDelayedRepeatedAttempt(eagerServiceInfoAttempt);
const serviceInfoRefreshRequestedRef = useRef(false);
useEffect(
function refreshListOnOpen() {
if (!dnsZonesRefreshRequestedRef.current) {
dnsZonesRefreshRequestedRef.current = true;
listDNSZones();
if (!serviceInfoRefreshRequestedRef.current) {
serviceInfoRefreshRequestedRef.current = true;
getServiceInfo();
}
},
[listDNSZones]
[getServiceInfo]
);
if (listDNSZonesAttempt.status === 'error') {
if (serviceInfoAttempt.status === 'error') {
return (
<Text p={textSpacing}>
<ConnectionStatusIndicator status="warning" inline mr={2} />
VNet is working, but Teleport Connect could not fetch DNS zones:{' '}
{listDNSZonesAttempt.statusText}
VNet is running, but Teleport Connect could not fetch its status:{' '}
{serviceInfoAttempt.statusText}
<ButtonSecondary
ml={2}
size="small"
type="button"
onClick={listDNSZones}
onClick={getServiceInfo}
>
Retry
</ButtonSecondary>
@@ -175,36 +175,100 @@ const DnsZones = () => {
}
if (
listDNSZonesAttempt.status === '' ||
(listDNSZonesAttempt.status === 'processing' && !listDNSZonesAttempt.data)
serviceInfoAttempt.status === '' ||
(serviceInfoAttempt.status === 'processing' && !serviceInfoAttempt.data)
) {
return (
<Text p={textSpacing}>
<ConnectionStatusIndicator status="processing" inline mr={2} />
Updating the list of DNS zones…
Updating VNet status…
</Text>
);
}
const dnsZones = listDNSZonesAttempt.data;
const statusIndicator = (
<ConnectionStatusIndicator
status={serviceInfoAttempt.status === 'success' ? 'on' : 'processing'}
title={
serviceInfoAttempt.status === 'processing'
? 'Updating VNet status…'
: undefined
}
inline
mr={2}
mt={2}
/>
);
const serviceInfo = serviceInfoAttempt.data;
const sshConfiguredIndicator = serviceInfo.sshConfigured ? null : (
<Flex>
<ConnectionStatusIndicator status={'warning'} inline mr={2} />
SSH clients are not configured to use VNet (see diag report).
</Flex>
);
if (serviceInfo.appDnsZones.length == 0 && serviceInfo.clusters.length == 0) {
return (
<Flex p={textSpacing}>
{statusIndicator}
No clusters connected yet, VNet is not proxying any connections.
</Flex>
);
}
const appDNSZones = new Set(serviceInfo.appDnsZones);
const sshClusters = new Set(serviceInfo.clusters);
const appAndSshAreEqual =
appDNSZones.size == sshClusters.size && appDNSZones.isSubsetOf(sshClusters);
if (appAndSshAreEqual) {
return (
<Text p={textSpacing}>
<Flex>
{statusIndicator}
Proxying TCP and SSH connections to {[...appDNSZones].join(', ')}
</Flex>
{sshConfiguredIndicator}
</Text>
);
}
const both = [...appDNSZones.intersection(sshClusters)].sort();
const justTCP = [...appDNSZones.difference(sshClusters)].sort();
const justSSH = [...sshClusters.difference(appDNSZones)].sort();
return (
<Text p={textSpacing}>
<ConnectionStatusIndicator
status={listDNSZonesAttempt.status === 'success' ? 'on' : 'processing'}
title={
listDNSZonesAttempt.status === 'processing'
? 'Updating the list of DNS zones…'
: undefined
}
inline
mr={2}
/>
{dnsZones.length === 0 ? (
<>No clusters connected yet, VNet is not proxying any connections.</>
) : (
<>Proxying TCP connections to {dnsZones.join(', ')}</>
)}
</Text>
<Box p={textSpacing}>
<Flex>
{statusIndicator}
<Box>
Proxying TCP and SSH connections to:
<Text typography="body2">
{both.length ? (
<Box>
<ConnectionKindIndicator bold>TCP</ConnectionKindIndicator>
<ConnectionKindIndicator bold>SSH</ConnectionKindIndicator>
{both.join(', ')}
</Box>
) : null}
{justTCP.length ? (
<Box>
<ConnectionKindIndicator bold>TCP</ConnectionKindIndicator>
{justTCP.join(', ')}
</Box>
) : null}
{justSSH.length ? (
<Box>
<ConnectionKindIndicator bold>SSH</ConnectionKindIndicator>
{justSSH.join(', ')}
</Box>
) : null}
</Text>
</Box>
</Flex>
{sshConfiguredIndicator}
</Box>
);
};
@@ -88,9 +88,11 @@ test.each(tests)(
clusters: [rootCluster],
})
);
jest.spyOn(ctx.vnet, 'listDNSZones').mockReturnValue(
jest.spyOn(ctx.vnet, 'getServiceInfo').mockReturnValue(
new MockedUnaryCall({
dnsZones: [proxyHostname(rootCluster.proxyHost)],
appDnsZones: [proxyHostname(rootCluster.proxyHost)],
clusters: [rootCluster.name],
sshConfigured: true,
})
);
@@ -120,7 +122,7 @@ test.each(tests)(
).toBeInTheDocument();
await user.click(within(docVnetInfo).getByText('Start VNet'));
expect(
await screen.findByText(/Proxying TCP connections/)
await screen.findByText(/Proxying TCP and SSH connections/)
).toBeInTheDocument();
// Verify that a notification is shown and that the address is in the clipboard.
@@ -141,7 +143,7 @@ test.each(tests)(
await user.click(within(docVnetInfo).getByText('Stop VNet'));
await user.click(await within(docVnetInfo).findByText('Start VNet'));
expect(
await screen.findByText(/Proxying TCP connections/)
await screen.findByText(/Proxying TCP and SSH connections/)
).toBeInTheDocument();
// Verify that the address was not copied to the clipboard after the second start from the "Start
@@ -182,9 +184,11 @@ test.each(tests)(
clusters: [rootCluster],
})
);
jest.spyOn(ctx.vnet, 'listDNSZones').mockReturnValue(
jest.spyOn(ctx.vnet, 'getServiceInfo').mockReturnValue(
new MockedUnaryCall({
dnsZones: [proxyHostname(rootCluster.proxyHost)],
appDnsZones: [proxyHostname(rootCluster.proxyHost)],
clusters: [rootCluster.name],
sshConfigured: true,
})
);
@@ -209,7 +213,7 @@ test.each(tests)(
// Verify that VNet is running and that the public address was copied to the clipboard.
expect(
await screen.findByText(/Proxying TCP connections/)
await screen.findByText(/Proxying TCP and SSH connections/)
).toBeInTheDocument();
expect(
await screen.findByText(
@@ -241,9 +245,11 @@ test('launching VNet for the first time from the connections panel does not open
clusters: [rootCluster],
})
);
jest.spyOn(ctx.vnet, 'listDNSZones').mockReturnValue(
jest.spyOn(ctx.vnet, 'getServiceInfo').mockReturnValue(
new MockedUnaryCall({
dnsZones: [proxyHostname(rootCluster.proxyHost)],
appDnsZones: [proxyHostname(rootCluster.proxyHost)],
clusters: [rootCluster.name],
sshConfigured: true,
})
);
@@ -28,7 +28,10 @@ import {
useState,
} from 'react';
import { BackgroundItemStatus } from 'gen-proto-ts/teleport/lib/teleterm/vnet/v1/vnet_service_pb';
import {
BackgroundItemStatus,
GetServiceInfoResponse,
} from 'gen-proto-ts/teleport/lib/teleterm/vnet/v1/vnet_service_pb';
import { Report } from 'gen-proto-ts/teleport/lib/vnet/diag/v1/diag_pb';
import { useStateRef } from 'shared/hooks';
import { Attempt, makeEmptyAttempt, useAsync } from 'shared/hooks/useAsync';
@@ -60,8 +63,8 @@ export type VnetContext = {
startAttempt: Attempt<void>;
stop: () => Promise<[void, Error]>;
stopAttempt: Attempt<void>;
listDNSZones: () => Promise<[string[], Error]>;
listDNSZonesAttempt: Attempt<string[]>;
getServiceInfo: () => Promise<[GetServiceInfoResponse, Error]>;
serviceInfoAttempt: Attempt<GetServiceInfoResponse>;
runDiagnostics: () => Promise<[Report, Error]>;
diagnosticsAttempt: Attempt<Report>;
/**
@@ -234,9 +237,9 @@ export const VnetContextProvider: FC<
])
);
const [listDNSZonesAttempt, listDNSZones] = useAsync(
const [serviceInfoAttempt, getServiceInfo] = useAsync(
useCallback(
() => vnet.listDNSZones({}).then(({ response }) => response.dnsZones),
() => vnet.getServiceInfo({}).then(({ response }) => response),
[vnet]
)
);
@@ -469,8 +472,8 @@ export const VnetContextProvider: FC<
startAttempt,
stop,
stopAttempt,
listDNSZones,
listDNSZonesAttempt,
getServiceInfo,
serviceInfoAttempt,
runDiagnostics,
diagnosticsAttempt,
getDisabledDiagnosticsReason,