Add LinuxDesktop resource to unified resources (#63101)

* Add LinuxDesktop gRPC and backend

* Add LinuxDesktop resource to unified resources

* Remove CloneResource

* Review comments

* Fix logins

* Review comment

* Add ClonableResourceWithLabels

* Fix missing CloneResource

* Update lib/auth/linuxdesktop/linuxdesktopv1/service.go

Co-authored-by: Zac Bergquist <[email protected]>

* Fix role

* Update lib/auth/auth_with_roles.go

Co-authored-by: Zac Bergquist <[email protected]>

* Update lib/web/ui/server.go

Co-authored-by: Zac Bergquist <[email protected]>

* Fix compilation

* e

* skip gogoproto

* e

* fix test

* Update api/client/proto/linuxdesktop.go

Co-authored-by: Zac Bergquist <[email protected]>

---------

Co-authored-by: Zac Bergquist <[email protected]>
This commit is contained in:
Przemko Robakowski
2026-04-28 20:57:34 +00:00
committed by GitHub
co-authored by Zac Bergquist
parent a13ff87916
commit 1c0a636206
26 changed files with 2459 additions and 1131 deletions
+3
View File
@@ -4521,6 +4521,9 @@ func convertEnrichedResource(resource *proto.PaginatedResource) (*types.Enriched
return &types.EnrichedResource{ResourceWithLabels: r, RequiresRequest: resource.RequiresRequest}, nil
} else if r := resource.GetGitServer(); r != nil {
return &types.EnrichedResource{ResourceWithLabels: r, RequiresRequest: resource.RequiresRequest}, nil
} else if r := resource.GetLinuxDesktop(); r != nil {
desktop := proto.UnpackLinuxDesktop(r)
return &types.EnrichedResource{ResourceWithLabels: desktop, Logins: resource.Logins, RequiresRequest: resource.RequiresRequest}, nil
} else {
return nil, trace.BadParameter("received unsupported resource %T", resource.Resource)
}
File diff suppressed because it is too large Load Diff
+51
View File
@@ -0,0 +1,51 @@
// Copyright 2026 Gravitational, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proto
import (
linuxdesktopv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/linuxdesktop/v1"
"github.com/gravitational/teleport/api/types"
)
// PackLinuxDesktop packs Linux desktop into its wire format.
func PackLinuxDesktop(desktop *linuxdesktopv1.LinuxDesktop) isPaginatedResource_Resource {
return &PaginatedResource_LinuxDesktop{
LinuxDesktop: &LinuxDesktop{
Kind: desktop.Kind,
SubKind: desktop.SubKind,
Version: desktop.Version,
Metadata: types.Metadata153ToLegacy(desktop.Metadata),
Addr: desktop.Spec.Addr,
Hostname: desktop.Spec.Hostname,
ProxyIds: desktop.Spec.ProxyIds,
},
}
}
// UnpackLinuxDesktop converts a wire-format LinuxDesktop resource back into an linuxdesktopv1.LinuxDesktop instance.
func UnpackLinuxDesktop(src *LinuxDesktop) types.ResourceWithLabels {
dst := &linuxdesktopv1.LinuxDesktop{
Kind: src.Kind,
SubKind: src.SubKind,
Version: src.Version,
Metadata: types.LegacyTo153Metadata(src.Metadata),
Spec: &linuxdesktopv1.LinuxDesktopSpec{
Addr: src.Addr,
Hostname: src.Hostname,
ProxyIds: src.ProxyIds,
},
}
return types.ProtoResource153ToLegacy(dst)
}
+130
View File
@@ -0,0 +1,130 @@
// Copyright 2026 Gravitational, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proto
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/testing/protocmp"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
linuxdesktopv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/linuxdesktop/v1"
"github.com/gravitational/teleport/api/types"
)
func TestPackUnpackLinuxDesktop(t *testing.T) {
t.Parallel()
tests := []struct {
name string
desktop *linuxdesktopv1.LinuxDesktop
}{
{
name: "basic linux desktop",
desktop: &linuxdesktopv1.LinuxDesktop{
Kind: types.KindLinuxDesktop,
SubKind: "",
Version: types.V3,
Metadata: &headerv1.Metadata{
Name: "test-linux-desktop",
Labels: map[string]string{
"env": "production",
"region": "us-east-1",
},
},
Spec: &linuxdesktopv1.LinuxDesktopSpec{
Addr: "192.168.1.100:22",
Hostname: "linux-host-1",
ProxyIds: []string{"proxy-1", "proxy-2"},
},
},
},
{
name: "linux desktop with no labels",
desktop: &linuxdesktopv1.LinuxDesktop{
Kind: types.KindLinuxDesktop,
Version: types.V3,
Metadata: &headerv1.Metadata{
Name: "minimal-desktop",
},
Spec: &linuxdesktopv1.LinuxDesktopSpec{
Addr: "example.com:22",
Hostname: "minimal-host",
},
},
},
{
name: "linux desktop with empty proxy IDs",
desktop: &linuxdesktopv1.LinuxDesktop{
Kind: types.KindLinuxDesktop,
Version: types.V3,
Metadata: &headerv1.Metadata{
Name: "no-proxy-desktop",
Labels: map[string]string{
"test": "label",
},
},
Spec: &linuxdesktopv1.LinuxDesktopSpec{
Addr: "172.16.0.10:22",
Hostname: "isolated-host",
ProxyIds: []string{},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Pack the desktop
packed := PackLinuxDesktop(tt.desktop)
require.NotNil(t, packed)
// Verify packed structure
paginatedResource, ok := packed.(*PaginatedResource_LinuxDesktop)
require.True(t, ok, "packed resource should be *PaginatedResource_LinuxDesktop")
require.NotNil(t, paginatedResource.LinuxDesktop)
// Verify wire format fields
wireDesktop := paginatedResource.LinuxDesktop
require.Equal(t, tt.desktop.Kind, wireDesktop.Kind)
require.Equal(t, tt.desktop.SubKind, wireDesktop.SubKind)
require.Equal(t, tt.desktop.Version, wireDesktop.Version)
require.Equal(t, tt.desktop.Metadata.Name, wireDesktop.Metadata.Name)
require.Equal(t, tt.desktop.Metadata.Labels, wireDesktop.Metadata.Labels)
require.Equal(t, tt.desktop.Spec.Addr, wireDesktop.Addr)
require.Equal(t, tt.desktop.Spec.Hostname, wireDesktop.Hostname)
require.Equal(t, tt.desktop.Spec.ProxyIds, wireDesktop.ProxyIds)
// Unpack the desktop
unpacked := UnpackLinuxDesktop(wireDesktop)
require.NotNil(t, unpacked)
// Unwrap to get the original proto type
unwrapper, ok := unpacked.(types.Resource153UnwrapperT[*linuxdesktopv1.LinuxDesktop])
require.True(t, ok, "unpacked resource should be unwrappable")
unpackedDesktop := unwrapper.UnwrapT()
require.NotNil(t, unpackedDesktop)
// Verify round-trip equality using protocmp
// Ignore the Expires field as it's added during metadata conversion
if diff := cmp.Diff(tt.desktop, unpackedDesktop, protocmp.Transform(), protocmp.IgnoreFields(&headerv1.Metadata{}, "expires")); diff != "" {
t.Errorf("round-trip mismatch (-want +got):\n%s", diff)
}
})
}
}
@@ -2193,6 +2193,23 @@ message IdentityCenterAccountAssignment {
IdentityCenterPermissionSet PermissionSet = 7;
}
// LinuxDesktop represents Linux desktop. This is strictly a wire-format object
// for use with the unified resource cache, and the types defined in the
// `linuxdesktop` package should be used for actual processing.
message LinuxDesktop {
// Kind is the Linux desktop resource kind.
string kind = 1;
// SubKind is an optional resource subkind.
string sub_kind = 2;
// Version is the resource version.
string version = 3;
// Metadata is the account metadata.
types.Metadata metadata = 4 [(gogoproto.nullable) = false];
string addr = 5;
string hostname = 6;
repeated string proxy_ids = 7;
}
message ListInstallersRequest {
// The maximum number of items to return.
// The server may impose a different page size at its discretion.
@@ -2239,6 +2256,8 @@ message PaginatedResource {
// IdentityCenterAccountAssignment represents a requestable Identity Center
// Account Assignment
IdentityCenterAccountAssignment IdentityCenterAccountAssignment = 16 [(gogoproto.jsontag) = "identity_center_account_assignment,omitempty"];
// LinuxDesktop represents LinuxDesktop resource
LinuxDesktop linux_desktop = 17;
}
// Logins allowed for the included resource. Only to be populated for SSH and Desktops.
+11 -1
View File
@@ -329,6 +329,11 @@ type protoResource153ToLegacyAdapter[T ProtoResource153] struct {
resource153ToResourceWithLabelsAdapter[T]
}
func (r *protoResource153ToLegacyAdapter[T]) CloneResource() ResourceWithLabels {
clone := proto.CloneOf(r.inner)
return ProtoResource153ToLegacy(clone)
}
// UnwrapT is an escape hatch for Resource153 instances that are piped down into
// the codebase as a legacy Resource.
//
@@ -352,7 +357,7 @@ func (r *protoResource153ToLegacyAdapter[T]) MarshalJSON() ([]byte, error) {
//
// Note that CheckAndSetDefaults is a noop for the returned resource and
// SetSubKind is not implemented and panics on use.
func ProtoResource153ToLegacy[T ProtoResource153](r T) ResourceWithLabels {
func ProtoResource153ToLegacy[T ProtoResource153](r T) ClonableResourceWithLabels {
return &protoResource153ToLegacyAdapter[T]{
r,
resource153ToResourceWithLabelsAdapter[T]{
@@ -416,3 +421,8 @@ func ConvertResource[T any](resource Resource) (T, error) {
var zero T
return zero, trace.BadParameter("expected resource type %T, got %T", zero, resource)
}
type ClonableResourceWithLabels interface {
ResourceWithLabels
CloneResource() ResourceWithLabels
}
+18
View File
@@ -46,6 +46,7 @@ import (
auditlogpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/auditlog/v1"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
identitycenterv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/identitycenter/v1"
linuxdesktopv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/linuxdesktop/v1"
mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
"github.com/gravitational/teleport/api/internalutils/stream"
"github.com/gravitational/teleport/api/metadata"
@@ -1445,6 +1446,7 @@ var (
types.KindNode: {},
types.KindSAMLIdPServiceProvider: {},
types.KindWindowsDesktop: {},
types.KindLinuxDesktop: {},
types.KindMCP: {},
}
@@ -1622,6 +1624,17 @@ func (a *ServerWithRoles) ListUnifiedResources(ctx context.Context, req *proto.L
continue
}
r.Logins = logins
} else if d := r.GetLinuxDesktop(); d != nil {
desktop := proto.UnpackLinuxDesktop(d)
logins, err := resourceLister.getAllowedLogins(desktop)
if err != nil {
a.authServer.logger.WarnContext(ctx, "Unable to determine logins for Linux desktop",
"error", err,
"resource", desktop.GetName(),
)
continue
}
r.Logins = logins
} else if d := r.GetAppServer(); d != nil {
// Apps representing an Identity Center Account have a collection of Permission Sets
// that can be thought of as individually-addressable sub-resources. To present a consitent
@@ -2359,6 +2372,11 @@ func (r *resourceChecker) CanAccess(resource types.ResourceWithLabels) error {
if isCheckable {
return r.CheckAccess(checkable, state, services.NewIdentityCenterAccountAssignmentMatcher(rr.UnwrapT()))
}
case types.Resource153UnwrapperT[*linuxdesktopv1.LinuxDesktop]:
checkable, isCheckable := rr.(services.AccessCheckable)
if isCheckable {
return r.CheckAccess(checkable, state)
}
}
return trace.BadParameter("could not check access to resource type %T", resource)
@@ -240,14 +240,9 @@ func (s *Service) UpsertLinuxDesktop(ctx context.Context, req *linuxdesktopv1.Up
if err := checkAccess(authCtx, existing); err != nil {
return nil, trace.Wrap(err)
}
if err := checkAccess(authCtx, desktop); err != nil {
return nil, trace.Wrap(err)
}
} else {
if err := checkAccess(authCtx, desktop); err != nil {
return nil, trace.Wrap(err)
}
}
if err := checkAccess(authCtx, desktop); err != nil {
return nil, trace.Wrap(err)
}
rsp, err := s.backend.UpsertLinuxDesktop(ctx, desktop)
+30
View File
@@ -35,7 +35,9 @@ import (
apidefaults "github.com/gravitational/teleport/api/defaults"
componentfeaturesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/componentfeatures/v1"
identitycenterv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/identitycenter/v1"
linuxdesktopv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/linuxdesktop/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils/clientutils"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/componentfeatures"
"github.com/gravitational/teleport/lib/utils"
@@ -396,6 +398,7 @@ func (c *UnifiedResourceCache) itemKindMatches(r resource, kinds map[string]stru
switch r.GetKind() {
case types.KindNode,
types.KindWindowsDesktop,
types.KindLinuxDesktop,
types.KindGitServer,
types.KindDatabase,
types.KindKubernetesCluster:
@@ -527,6 +530,7 @@ type ResourceGetter interface {
DatabaseServersGetter
AppServersGetter
WindowsDesktopGetter
LinuxDesktopGetter
KubernetesServerGetter
SAMLIdpServiceProviderGetter
IdentityCenterAccountGetter
@@ -641,6 +645,11 @@ func (c *UnifiedResourceCache) getResourcesAndUpdateCurrent(ctx context.Context)
return trace.Wrap(err)
}
newLinuxDesktops, err := c.getLinuxDesktops(ctx)
if err != nil {
return trace.Wrap(err)
}
newICAccounts, err := c.getIdentityCenterAccounts(ctx)
if err != nil {
return trace.Wrap(err)
@@ -666,6 +675,7 @@ func (c *UnifiedResourceCache) getResourcesAndUpdateCurrent(ctx context.Context)
putResources(c, newKubes)
putResources(c, newSAMLApps)
putResources(c, newDesktops)
putResources(c, newLinuxDesktops)
putResources(c, newICAccounts)
putResources(c, newGitServers)
c.stale = false
@@ -720,6 +730,18 @@ func (c *UnifiedResourceCache) getDesktops(ctx context.Context) ([]types.Windows
return newDesktops, nil
}
// getLinuxDesktops will get all Linux desktops
func (c *UnifiedResourceCache) getLinuxDesktops(ctx context.Context) ([]resource, error) {
var linuxDesktops []resource
for linuxDesktop, err := range clientutils.Resources(ctx, c.ListLinuxDesktops) {
if err != nil {
return nil, trace.Wrap(err)
}
linuxDesktops = append(linuxDesktops, types.ProtoResource153ToLegacy(linuxDesktop))
}
return linuxDesktops, nil
}
// getSAMLApps will get all SAML Idp Service Providers
func (c *UnifiedResourceCache) getSAMLApps(ctx context.Context) ([]types.SAMLIdPServiceProvider, error) {
var newSAMLApps []types.SAMLIdPServiceProvider
@@ -905,6 +927,7 @@ func (c *UnifiedResourceCache) resourceKinds() []types.WatchKind {
{Kind: types.KindDatabaseServer},
{Kind: types.KindAppServer},
{Kind: types.KindWindowsDesktop},
{Kind: types.KindLinuxDesktop},
{Kind: types.KindSAMLIdPServiceProvider},
{Kind: types.KindIdentityCenterAccount},
{Kind: types.KindGitServer},
@@ -1234,6 +1257,13 @@ func MakePaginatedResource(requestType string, r types.ResourceWithLabels, requi
}
protoResource = &proto.PaginatedResource{Resource: &proto.PaginatedResource_KubernetesServer{KubernetesServer: srv}, RequiresRequest: requiresRequest}
case types.KindLinuxDesktop:
unwrapper, ok := resource.(types.Resource153UnwrapperT[*linuxdesktopv1.LinuxDesktop])
if !ok {
return nil, trace.BadParameter("%s has invalid type %T", resourceKind, resource)
}
protoResource = &proto.PaginatedResource{Resource: proto.PackLinuxDesktop(unwrapper.UnwrapT()), Logins: logins, RequiresRequest: requiresRequest}
case types.KindWindowsDesktop:
desktop, ok := resource.(*types.WindowsDesktopV3)
if !ok {
+319 -22
View File
@@ -40,6 +40,7 @@ import (
componentfeaturesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/componentfeatures/v1"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
identitycenterv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/identitycenter/v1"
linuxdesktopv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/linuxdesktop/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/common"
"github.com/gravitational/teleport/api/types/header"
@@ -54,6 +55,7 @@ type client struct {
bk backend.Backend
services.Presence
services.WindowsDesktops
services.LinuxDesktops
services.SAMLIdPServiceProviders
services.GitServers
services.IdentityCenterAccounts
@@ -74,11 +76,14 @@ func newClient(t *testing.T) *client {
Backend: bk,
})
require.NoError(t, err)
linuxDesktopService, err := local.NewLinuxDesktopService(bk)
require.NoError(t, err)
return &client{
bk: bk,
Presence: local.NewPresenceService(bk),
WindowsDesktops: local.NewWindowsDesktopService(bk),
LinuxDesktops: linuxDesktopService,
SAMLIdPServiceProviders: samlService,
Events: local.NewEventsService(bk),
GitServers: gitService,
@@ -89,7 +94,7 @@ func newClient(t *testing.T) *client {
func TestUnifiedResourceWatcher(t *testing.T) {
t.Parallel()
ctx := context.Background()
ctx := t.Context()
clt := newClient(t)
// Add node to the backend.
@@ -185,9 +190,10 @@ func TestUnifiedResourceWatcher(t *testing.T) {
gitServer, gitServer2,
services.IdentityCenterAccountToAppServer(icAcct),
}
assert.Eventually(t, func() bool {
assert.EventuallyWithT(t, func(t *assert.CollectT) {
res, err = w.GetUnifiedResources(ctx)
return len(res) == len(expectedRes)
require.NoError(t, err)
require.Len(t, res, len(expectedRes))
}, 5*time.Second, 10*time.Millisecond, "Timed out waiting for unified resources to be added")
assert.Empty(t, cmp.Diff(
expectedRes,
@@ -213,14 +219,15 @@ func TestUnifiedResourceWatcher(t *testing.T) {
services.IdentityCenterAccountToAppServer(icAcct),
}
assert.Eventually(t, func() bool {
assert.EventuallyWithT(t, func(t *assert.CollectT) {
res, err = w.GetUnifiedResources(ctx)
require.NoError(t, err)
serverUpdated := slices.ContainsFunc(res, func(r types.ResourceWithLabels) bool {
node, ok := r.(types.Server)
return ok && node.GetAddr() == "192.168.0.1:22"
})
return len(res) == len(expectedRes) && serverUpdated
require.True(t, serverUpdated)
require.Len(t, res, len(expectedRes))
}, 5*time.Second, 10*time.Millisecond, "Timed out waiting for unified resources to be updated")
assert.Empty(t, cmp.Diff(
expectedRes,
@@ -238,7 +245,7 @@ func TestUnifiedResourceWatcher(t *testing.T) {
func TestUnifiedResourceCacheIterateResources(t *testing.T) {
t.Parallel()
ctx := context.Background()
ctx := t.Context()
clt := newClient(t)
node := newNodeServer(t, "node1", "hostname1", "127.0.0.1:22", false /*tunnel*/)
@@ -425,7 +432,7 @@ func TestUnifiedResourceCacheIterateResources(t *testing.T) {
func TestUnifiedResourceCacheIteration(t *testing.T) {
t.Parallel()
ctx := context.Background()
ctx := t.Context()
const resourceCount = 1234
ids := make([]string, 0, resourceCount)
@@ -820,7 +827,7 @@ func TestUnifiedResourceCacheIteration(t *testing.T) {
func TestUnifiedResourceWatcher_PreventDuplicates(t *testing.T) {
t.Parallel()
ctx := context.Background()
ctx := t.Context()
clt := newClient(t)
w, err := services.NewUnifiedResourceCache(ctx, services.UnifiedResourceCacheConfig{
ResourceWatcherConfig: services.ResourceWatcherConfig{
@@ -836,9 +843,10 @@ func TestUnifiedResourceWatcher_PreventDuplicates(t *testing.T) {
_, err = clt.UpsertNode(ctx, node)
require.NoError(t, err)
assert.Eventually(t, func() bool {
res, _ := w.GetUnifiedResources(ctx)
return len(res) == 1
assert.EventuallyWithT(t, func(t *assert.CollectT) {
res, err := w.GetUnifiedResources(ctx)
require.NoError(t, err)
require.Len(t, res, 1)
}, 5*time.Second, 10*time.Millisecond, "Timed out waiting for unified resources to be added")
// update a node
@@ -846,10 +854,11 @@ func TestUnifiedResourceWatcher_PreventDuplicates(t *testing.T) {
_, err = clt.UpsertNode(ctx, updatedNode)
require.NoError(t, err)
// only one resource should still exists with the name "node1" (with hostname updated)
assert.Eventually(t, func() bool {
res, _ := w.GetUnifiedResources(ctx)
return len(res) == 1
// only one resource should still exist with the name "node1" (with hostname updated)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
res, err := w.GetUnifiedResources(ctx)
require.NoError(t, err)
require.Len(t, res, 1)
}, 5*time.Second, 10*time.Millisecond, "Timed out waiting for unified resources to be added")
}
@@ -975,7 +984,7 @@ func TestUnifiedResourceCache_AppServerComponentFeaturesIntersection(t *testing.
func TestUnifiedResourceWatcher_DeleteEvent(t *testing.T) {
t.Parallel()
ctx := context.Background()
ctx := t.Context()
clt := newClient(t)
w, err := services.NewUnifiedResourceCache(ctx, services.UnifiedResourceCacheConfig{
ResourceWatcherConfig: services.ResourceWatcherConfig{
@@ -1094,9 +1103,10 @@ func TestUnifiedResourceWatcher_DeleteEvent(t *testing.T) {
_, err = clt.CreateGitServer(ctx, gitServer)
require.NoError(t, err)
assert.Eventually(t, func() bool {
res, _ := w.GetUnifiedResources(ctx)
return len(res) == 8
assert.EventuallyWithT(t, func(t *assert.CollectT) {
res, err := w.GetUnifiedResources(ctx)
require.NoError(t, err)
require.Len(t, res, 8)
}, 5*time.Second, 10*time.Millisecond, "Timed out waiting for unified resources to be added")
// delete just one of each of the HA servers
@@ -1222,7 +1232,7 @@ func newICAccount(t *testing.T, ctx context.Context, svc services.IdentityCenter
func TestOktaAppServers(t *testing.T) {
clt := newClient(t)
ctx := context.Background()
ctx := t.Context()
appsServer := []*types.AppServerV3{
mustCreateOktaAppServer(t, uuid.NewString(), "App 1"),
@@ -1234,7 +1244,7 @@ func TestOktaAppServers(t *testing.T) {
require.NoError(t, err)
}
w, err := services.NewUnifiedResourceCache(context.Background(), services.UnifiedResourceCacheConfig{
w, err := services.NewUnifiedResourceCache(t.Context(), services.UnifiedResourceCacheConfig{
ResourceWatcherConfig: services.ResourceWatcherConfig{
Component: teleport.ComponentUnifiedResource,
Client: clt,
@@ -1309,7 +1319,7 @@ func newMCPServerApp(t *testing.T, name string) *types.AppV3 {
func TestUnifiedResourceCacheIterateMCPServers(t *testing.T) {
t.Parallel()
ctx := context.Background()
ctx := t.Context()
clt := newClient(t)
app, err := types.NewAppServerV3(
@@ -1414,3 +1424,290 @@ func TestUnifiedResourceCacheIterateMCPServers(t *testing.T) {
})
}
}
func TestUnifiedResourceLinuxDesktop(t *testing.T) {
t.Parallel()
ctx := t.Context()
clt := newClient(t)
// Create a unified resource cache
w, err := services.NewUnifiedResourceCache(ctx, services.UnifiedResourceCacheConfig{
ResourceWatcherConfig: services.ResourceWatcherConfig{
Client: clt,
Component: teleport.ComponentUnifiedResource,
},
ResourceGetter: clt,
})
require.NoError(t, err)
// Create and upsert a Linux desktop
linuxDesktop1 := &linuxdesktopv1.LinuxDesktop{
Kind: types.KindLinuxDesktop,
Version: types.V3,
Metadata: &headerv1.Metadata{
Name: "linux-desktop-1",
Labels: map[string]string{
"env": "production",
"region": "us-west-1",
},
},
Spec: &linuxdesktopv1.LinuxDesktopSpec{
Addr: "10.0.0.1:22",
Hostname: "linux-host-1",
ProxyIds: []string{"proxy-1"},
},
}
_, err = clt.UpsertLinuxDesktop(ctx, linuxDesktop1)
require.NoError(t, err)
// Wait for the resource to appear in the cache
var resources []types.ResourceWithLabels
assert.EventuallyWithT(t, func(t *assert.CollectT) {
resources, err = w.GetUnifiedResources(ctx)
require.NoError(t, err)
require.Len(t, resources, 1)
}, 5*time.Second, 10*time.Millisecond, "Timed out waiting for Linux desktop to be added")
// Verify the resource is a Linux desktop with correct properties
require.Len(t, resources, 1)
resource := resources[0]
require.Equal(t, types.KindLinuxDesktop, resource.GetKind())
require.Equal(t, "linux-desktop-1", resource.GetName())
require.Contains(t, resource.GetAllLabels(), "env")
require.Equal(t, "production", resource.GetAllLabels()["env"])
// Verify it can be unwrapped
unwrapper, ok := resource.(types.Resource153UnwrapperT[*linuxdesktopv1.LinuxDesktop])
require.True(t, ok, "resource should be unwrappable to LinuxDesktop")
unwrapped := unwrapper.UnwrapT()
require.Equal(t, "linux-desktop-1", unwrapped.Metadata.Name)
require.Equal(t, "10.0.0.1:22", unwrapped.Spec.Addr)
require.Equal(t, "linux-host-1", unwrapped.Spec.Hostname)
// Add a second Linux desktop
linuxDesktop2 := &linuxdesktopv1.LinuxDesktop{
Kind: types.KindLinuxDesktop,
Version: types.V3,
Metadata: &headerv1.Metadata{
Name: "linux-desktop-2",
Labels: map[string]string{
"env": "staging",
},
},
Spec: &linuxdesktopv1.LinuxDesktopSpec{
Addr: "10.0.0.2:22",
Hostname: "linux-host-2",
ProxyIds: []string{"proxy-2"},
},
}
_, err = clt.UpsertLinuxDesktop(ctx, linuxDesktop2)
require.NoError(t, err)
// Wait for both resources to appear
assert.EventuallyWithT(t, func(t *assert.CollectT) {
resources, err = w.GetUnifiedResources(ctx)
require.NoError(t, err)
require.Len(t, resources, 2)
}, 5*time.Second, 10*time.Millisecond, "Timed out waiting for second Linux desktop")
// Verify both desktops are present
require.Len(t, resources, 2)
names := []string{resources[0].GetName(), resources[1].GetName()}
require.ElementsMatch(t, []string{"linux-desktop-1", "linux-desktop-2"}, names)
// Delete the first desktop
err = clt.DeleteLinuxDesktop(ctx, "linux-desktop-1")
require.NoError(t, err)
// Wait for deletion to be reflected
assert.EventuallyWithT(t, func(t *assert.CollectT) {
resources, err = w.GetUnifiedResources(ctx)
require.NoError(t, err)
require.Len(t, resources, 1)
require.Equal(t, "linux-desktop-2", resources[0].GetName())
}, 5*time.Second, 10*time.Millisecond, "Timed out waiting for Linux desktop deletion")
}
func TestUnifiedResourceLinuxDesktopFiltering(t *testing.T) {
t.Parallel()
ctx := t.Context()
clt := newClient(t)
// Create a unified resource cache
w, err := services.NewUnifiedResourceCache(ctx, services.UnifiedResourceCacheConfig{
ResourceWatcherConfig: services.ResourceWatcherConfig{
Client: clt,
Component: teleport.ComponentUnifiedResource,
},
ResourceGetter: clt,
})
require.NoError(t, err)
// Wait for cache initialization to avoid test flakiness
assert.Eventually(t, w.IsInitialized, 5*time.Second, 10*time.Millisecond, "Timed out waiting for all resources cache initialization")
// Add a Linux desktop
linuxDesktop := &linuxdesktopv1.LinuxDesktop{
Kind: types.KindLinuxDesktop,
Version: types.V3,
Metadata: &headerv1.Metadata{
Name: "linux-desktop",
Labels: map[string]string{
"env": "test",
},
},
Spec: &linuxdesktopv1.LinuxDesktopSpec{
Addr: "10.0.0.10:22",
Hostname: "test-host",
},
}
_, err = clt.UpsertLinuxDesktop(ctx, linuxDesktop)
require.NoError(t, err)
// Add a Windows desktop for comparison
windowsDesktop, err := types.NewWindowsDesktopV3(
"windows-desktop",
map[string]string{"env": "test"},
types.WindowsDesktopSpecV3{
Addr: "10.0.0.20:3389",
HostID: "windows-host",
},
)
require.NoError(t, err)
err = clt.UpsertWindowsDesktop(ctx, windowsDesktop)
require.NoError(t, err)
// Add a node for comparison
node := newNodeServer(t, "node", "node-host", "10.0.0.30:22", false)
_, err = clt.UpsertNode(ctx, node)
require.NoError(t, err)
// Wait for all resources
var allResources []types.ResourceWithLabels
assert.EventuallyWithT(t, func(t *assert.CollectT) {
allResources, err = w.GetUnifiedResources(ctx)
require.NoError(t, err)
require.Len(t, allResources, 3)
}, 5*time.Second, 10*time.Millisecond, "Timed out waiting for all resources")
// Helper to collect resources from iterator
collect := func(t *testing.T, iter iter.Seq2[types.ResourceWithLabels, error]) (collected []types.ResourceWithLabels) {
for r, err := range iter {
require.NoError(t, err)
collected = append(collected, r)
}
return collected
}
// Test filtering by Linux desktop kind only
linuxOnly := collect(t, w.Resources(ctx, "", types.SortBy{}, types.KindLinuxDesktop))
require.Len(t, linuxOnly, 1)
require.Equal(t, types.KindLinuxDesktop, linuxOnly[0].GetKind())
require.Equal(t, "linux-desktop", linuxOnly[0].GetName())
// Test filtering by Windows desktop kind only
windowsOnly := collect(t, w.Resources(ctx, "", types.SortBy{}, types.KindWindowsDesktop))
require.Len(t, windowsOnly, 1)
require.Equal(t, types.KindWindowsDesktop, windowsOnly[0].GetKind())
require.Equal(t, "windows-desktop", windowsOnly[0].GetName())
// Test filtering by both desktop kinds
bothDesktops := collect(t, w.Resources(ctx, "", types.SortBy{}, types.KindLinuxDesktop, types.KindWindowsDesktop))
require.Len(t, bothDesktops, 2)
kinds := []string{bothDesktops[0].GetKind(), bothDesktops[1].GetKind()}
require.ElementsMatch(t, []string{types.KindLinuxDesktop, types.KindWindowsDesktop}, kinds)
// Test filtering by node kind (should not include desktops)
nodesOnly := collect(t, w.Resources(ctx, "", types.SortBy{}, types.KindNode))
require.Len(t, nodesOnly, 1)
require.Equal(t, types.KindNode, nodesOnly[0].GetKind())
}
func TestMakePaginatedResourceLinuxDesktop(t *testing.T) {
t.Parallel()
tests := []struct {
name string
desktop *linuxdesktopv1.LinuxDesktop
requiresRequest bool
logins []string
}{
{
name: "basic linux desktop",
desktop: &linuxdesktopv1.LinuxDesktop{
Kind: types.KindLinuxDesktop,
Version: types.V3,
Metadata: &headerv1.Metadata{
Name: "test-desktop",
Labels: map[string]string{
"env": "production",
},
},
Spec: &linuxdesktopv1.LinuxDesktopSpec{
Addr: "192.168.1.100:22",
Hostname: "desktop-host",
ProxyIds: []string{"proxy-1"},
},
},
requiresRequest: false,
logins: []string{"ubuntu", "root"},
},
{
name: "linux desktop requiring request",
desktop: &linuxdesktopv1.LinuxDesktop{
Kind: types.KindLinuxDesktop,
Version: types.V3,
Metadata: &headerv1.Metadata{
Name: "protected-desktop",
},
Spec: &linuxdesktopv1.LinuxDesktopSpec{
Addr: "10.0.0.50:22",
Hostname: "protected-host",
},
},
requiresRequest: true,
logins: []string{"admin"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Convert to ResourceWithLabels
wrapped := types.ProtoResource153ToLegacy(tt.desktop)
// Create paginated resource
paginated, err := services.MakePaginatedResource(types.KindLinuxDesktop, wrapped, tt.requiresRequest)
require.NoError(t, err)
require.NotNil(t, paginated)
// Verify the paginated resource contains a LinuxDesktop
wireDesktop := paginated.GetLinuxDesktop()
require.NotNil(t, wireDesktop, "paginated resource should contain LinuxDesktop")
// Verify fields match
require.Equal(t, tt.desktop.Kind, wireDesktop.Kind)
require.Equal(t, tt.desktop.Version, wireDesktop.Version)
require.Equal(t, tt.desktop.Metadata.Name, wireDesktop.Metadata.Name)
require.Equal(t, tt.desktop.Spec.Addr, wireDesktop.Addr)
require.Equal(t, tt.desktop.Spec.Hostname, wireDesktop.Hostname)
require.Equal(t, tt.desktop.Spec.ProxyIds, wireDesktop.ProxyIds)
require.Equal(t, tt.requiresRequest, paginated.RequiresRequest)
// Test unpacking
unpacked := proto.UnpackLinuxDesktop(wireDesktop)
require.NotNil(t, unpacked)
require.Equal(t, types.KindLinuxDesktop, unpacked.GetKind())
require.Equal(t, tt.desktop.Metadata.Name, unpacked.GetName())
// Verify it can be unwrapped back to the original type
unwrapper, ok := unpacked.(types.Resource153UnwrapperT[*linuxdesktopv1.LinuxDesktop])
require.True(t, ok, "unpacked resource should be unwrappable")
unpackedDesktop := unwrapper.UnwrapT()
require.Equal(t, tt.desktop.Metadata.Name, unpackedDesktop.Metadata.Name)
require.Equal(t, tt.desktop.Spec.Addr, unpackedDesktop.Spec.Addr)
require.Equal(t, tt.desktop.Spec.Hostname, unpackedDesktop.Spec.Hostname)
})
}
}
+5 -1
View File
@@ -65,6 +65,7 @@ import (
"github.com/gravitational/teleport/api/client/webclient"
"github.com/gravitational/teleport/api/constants"
apidefaults "github.com/gravitational/teleport/api/defaults"
linuxdesktopv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/linuxdesktop/v1"
mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
notificationsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/notifications/v1"
summarizerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/summarizer/v1"
@@ -3428,6 +3429,7 @@ func makeUnifiedResourceRequest(r *http.Request) (*proto.ListUnifiedResourcesReq
types.KindDatabase,
types.KindNode,
types.KindWindowsDesktop,
types.KindLinuxDesktop,
types.KindKubernetesCluster,
types.KindSAMLIdPServiceProvider,
types.KindGitServer,
@@ -3587,7 +3589,9 @@ func (h *Handler) clusterUnifiedResourcesGet(w http.ResponseWriter, request *htt
})
unifiedResources = append(unifiedResources, app)
case types.WindowsDesktop:
unifiedResources = append(unifiedResources, ui.MakeDesktop(r, enriched.Logins, enriched.RequiresRequest))
unifiedResources = append(unifiedResources, ui.MakeWindowsDesktop(r, enriched.Logins, enriched.RequiresRequest))
case types.Resource153UnwrapperT[*linuxdesktopv1.LinuxDesktop]:
unifiedResources = append(unifiedResources, ui.MakeLinuxDesktop(r.UnwrapT(), enriched.Logins, enriched.RequiresRequest))
case types.KubeCluster:
kube := ui.MakeKubeCluster(r, accessChecker, enriched.RequiresRequest)
unifiedResources = append(unifiedResources, kube)
+7 -6
View File
@@ -27,6 +27,7 @@ import (
"github.com/julienschmidt/httprouter"
"github.com/gravitational/teleport/api/client"
linuxdesktopv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/linuxdesktop/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/httplib"
"github.com/gravitational/teleport/lib/reversetunnelclient"
@@ -260,12 +261,12 @@ func (h *Handler) clusterDesktopsGet(w http.ResponseWriter, r *http.Request, p h
uiDesktops := make([]webui.Desktop, 0, len(page.Resources))
for _, r := range page.Resources {
desktop, ok := r.ResourceWithLabels.(types.WindowsDesktop)
if !ok {
continue
switch desktop := r.ResourceWithLabels.(type) {
case types.WindowsDesktop:
uiDesktops = append(uiDesktops, webui.MakeWindowsDesktop(desktop, r.Logins, false /* requiresRequest */))
case types.Resource153UnwrapperT[*linuxdesktopv1.LinuxDesktop]:
uiDesktops = append(uiDesktops, webui.MakeLinuxDesktop(desktop.UnwrapT(), r.Logins, false /* requiresRequest */))
}
uiDesktops = append(uiDesktops, webui.MakeDesktop(desktop, r.Logins, false /* requiresRequest */))
}
return listResourcesGetResponse{
@@ -333,7 +334,7 @@ func (h *Handler) getDesktopHandle(w http.ResponseWriter, r *http.Request, p htt
return nil, trace.Wrap(err)
}
return webui.MakeDesktop(desktop, logins, false /* requiresRequest */), nil
return webui.MakeWindowsDesktop(desktop, logins, false /* requiresRequest */), nil
}
// desktopIsActive checks if a desktop has an active session and returns a desktopIsActive.
+21 -5
View File
@@ -25,6 +25,7 @@ import (
"github.com/gravitational/teleport/api/constants"
componentfeaturesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/componentfeatures/v1"
integrationv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1"
linuxdesktopv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/linuxdesktop/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/services"
@@ -524,10 +525,25 @@ type Desktop struct {
RequiresRequest bool `json:"requiresRequest,omitempty"`
}
// MakeDesktop converts a desktop from its API form to a type the UI can display.
func MakeDesktop(windowsDesktop types.WindowsDesktop, logins []string, requiresRequest bool) Desktop {
// stripRdpPort strips the default rdp port from an ip address since it is unimportant to display
stripRdpPort := func(addr string) string {
func MakeLinuxDesktop(linuxDesktop *linuxdesktopv1.LinuxDesktop, logins []string, requiresRequest bool) Desktop {
uiLabels := ui.MakeLabelsWithoutInternalPrefixes(linuxDesktop.Metadata.Labels)
return Desktop{
Kind: linuxDesktop.GetKind(),
OS: constants.LinuxOS,
Name: linuxDesktop.Spec.Hostname,
Addr: linuxDesktop.Spec.Addr,
Labels: uiLabels,
HostID: linuxDesktop.Metadata.Name,
Logins: logins,
RequiresRequest: requiresRequest,
}
}
// MakeWindowsDesktop converts a desktop from its API form to a type the UI can display.
func MakeWindowsDesktop(windowsDesktop types.WindowsDesktop, logins []string, requiresRequest bool) Desktop {
// stripRDPPort strips the default rdp port from an ip address since it is unimportant to display
stripRDPPort := func(addr string) string {
splitAddr := strings.Split(addr, ":")
if len(splitAddr) > 1 && splitAddr[1] == strconv.Itoa(defaults.RDPListenPort) {
return splitAddr[0]
@@ -541,7 +557,7 @@ func MakeDesktop(windowsDesktop types.WindowsDesktop, logins []string, requiresR
Kind: windowsDesktop.GetKind(),
OS: constants.WindowsOS,
Name: windowsDesktop.GetName(),
Addr: stripRdpPort(windowsDesktop.GetAddr()),
Addr: stripRDPPort(windowsDesktop.GetAddr()),
Labels: uiLabels,
HostID: windowsDesktop.GetHostID(),
Logins: logins,
+130 -1
View File
@@ -25,6 +25,8 @@ import (
"github.com/stretchr/testify/require"
apidefaults "github.com/gravitational/teleport/api/defaults"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
linuxdesktopv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/linuxdesktop/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/componentfeatures"
"github.com/gravitational/teleport/lib/defaults"
@@ -456,7 +458,7 @@ func TestMakeDesktopHiddenLabel(t *testing.T) {
)
require.NoError(t, err)
desktop := MakeDesktop(windowsDesktop, nil, false)
desktop := MakeWindowsDesktop(windowsDesktop, nil, false)
labels := []ui.Label{
{
Name: "label3",
@@ -467,6 +469,133 @@ func TestMakeDesktopHiddenLabel(t *testing.T) {
require.Equal(t, labels, desktop.Labels)
}
func TestMakeLinuxDesktop(t *testing.T) {
t.Parallel()
tests := []struct {
name string
desktop func(t *testing.T) *linuxdesktopv1.LinuxDesktop
logins []string
requiresRequest bool
expected Desktop
}{
{
name: "basic linux desktop",
desktop: func(t *testing.T) *linuxdesktopv1.LinuxDesktop {
return &linuxdesktopv1.LinuxDesktop{
Kind: types.KindLinuxDesktop,
Version: types.V3,
Metadata: &headerv1.Metadata{
Name: "linux-desktop-1",
Labels: map[string]string{
"env": "prod",
"region": "us-west",
},
},
Spec: &linuxdesktopv1.LinuxDesktopSpec{
Addr: "10.0.0.1:22",
Hostname: "linux-host-1",
ProxyIds: []string{"proxy-1"},
},
}
},
logins: []string{"ubuntu", "root"},
requiresRequest: false,
expected: Desktop{
Kind: types.KindLinuxDesktop,
OS: "linux",
Name: "linux-host-1",
Addr: "10.0.0.1:22",
HostID: "linux-desktop-1",
Labels: []ui.Label{
{Name: "env", Value: "prod"},
{Name: "region", Value: "us-west"},
},
Logins: []string{"ubuntu", "root"},
RequiresRequest: false,
},
},
{
name: "linux desktop with internal labels filtered",
desktop: func(t *testing.T) *linuxdesktopv1.LinuxDesktop {
return &linuxdesktopv1.LinuxDesktop{
Kind: types.KindLinuxDesktop,
Version: types.V3,
Metadata: &headerv1.Metadata{
Name: "linux-desktop-2",
Labels: map[string]string{
"teleport.internal/resource-id": "12345",
"teleport.hidden/secret": "value",
"visible": "label",
},
},
Spec: &linuxdesktopv1.LinuxDesktopSpec{
Addr: "192.168.1.100:22",
Hostname: "linux-host-2",
},
}
},
logins: []string{"admin"},
requiresRequest: true,
expected: Desktop{
Kind: types.KindLinuxDesktop,
OS: "linux",
Name: "linux-host-2",
Addr: "192.168.1.100:22",
HostID: "linux-desktop-2",
Labels: []ui.Label{
{Name: "visible", Value: "label"},
},
Logins: []string{"admin"},
RequiresRequest: true,
},
},
{
name: "linux desktop with no labels",
desktop: func(t *testing.T) *linuxdesktopv1.LinuxDesktop {
return &linuxdesktopv1.LinuxDesktop{
Kind: types.KindLinuxDesktop,
Version: types.V3,
Metadata: &headerv1.Metadata{
Name: "linux-desktop-3",
},
Spec: &linuxdesktopv1.LinuxDesktopSpec{
Addr: "example.com:2222",
Hostname: "linux-host-3",
},
}
},
logins: nil,
requiresRequest: false,
expected: Desktop{
Kind: types.KindLinuxDesktop,
OS: "linux",
Name: "linux-host-3",
Addr: "example.com:2222",
HostID: "linux-desktop-3",
Labels: []ui.Label{},
Logins: nil,
RequiresRequest: false,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
desktop := MakeLinuxDesktop(tt.desktop(t), tt.logins, tt.requiresRequest)
require.Equal(t, tt.expected.Kind, desktop.Kind)
require.Equal(t, tt.expected.OS, desktop.OS)
require.Equal(t, tt.expected.Name, desktop.Name)
require.Equal(t, tt.expected.Addr, desktop.Addr)
require.Equal(t, tt.expected.HostID, desktop.HostID)
require.Equal(t, tt.expected.Logins, desktop.Logins)
require.Equal(t, tt.expected.RequiresRequest, desktop.RequiresRequest)
require.ElementsMatch(t, tt.expected.Labels, desktop.Labels)
})
}
}
func TestMakeDesktopServiceHiddenLabel(t *testing.T) {
windowsDesktopService := &types.WindowsDesktopServiceV3{
ResourceHeader: types.ResourceHeader{
@@ -1150,6 +1150,8 @@ function getPrettyResourceKind(kind: RequestableResourceKind): string {
return 'User Group';
case 'windows_desktop':
return 'Desktop';
case 'linux_desktop':
return 'Desktop';
case 'saml_idp_service_provider':
return 'SAML Application';
case 'namespace':
@@ -44,6 +44,7 @@ export function getEmptyResourceState(): ResourceMap {
kube_cluster: {},
user_group: {},
windows_desktop: {},
linux_desktop: {},
role: {},
saml_idp_service_provider: {},
namespace: {},
@@ -184,6 +184,8 @@ function getIcon(item: RequestableResourceKind): ComponentType<IconProps> {
return Icon.GitHub;
case 'windows_desktop':
return Icon.Desktop;
case 'linux_desktop':
return Icon.Linux;
case 'resource': // This probably never shows in the UI.
return Icon.Server;
default:
@@ -150,6 +150,7 @@ const filterKindNameMap: Record<ResourceFilterKind, string> = {
app: 'Applications',
db: 'Databases',
windows_desktop: 'Desktops',
linux_desktop: 'Desktops',
kube_cluster: 'Kubernetes Clusters',
node: 'SSH Resources',
user_group: 'User Groups',
@@ -175,6 +175,28 @@ export function makeUnifiedResourceViewItemDesktop(
};
}
export function makeUnifiedResourceViewItemLinuxDesktop(
resource: UnifiedResourceDesktop,
ui: UnifiedResourceUi
): UnifiedResourceViewItem {
return {
name: resource.name,
SecondaryIcon: DesktopIcon,
primaryIconName: 'linux',
ActionButton: ui.ActionButton,
labels: resource.labels,
cardViewProps: {
primaryDesc: 'Linux',
secondaryDesc: resource.addr,
},
listViewProps: {
resourceType: 'Linux',
addr: resource.addr,
},
requiresRequest: resource.requiresRequest,
};
}
export function makeUnifiedResourceViewItemUserGroup(
resource: UnifiedResourceUserGroup,
ui: UnifiedResourceUi
@@ -255,6 +277,8 @@ export function mapResourceToViewItem({ resource, ui }: SharedUnifiedResource) {
return makeUnifiedResourceViewItemApp(resource, ui);
case 'windows_desktop':
return makeUnifiedResourceViewItemDesktop(resource, ui);
case 'linux_desktop':
return makeUnifiedResourceViewItemLinuxDesktop(resource, ui);
case 'user_group':
return makeUnifiedResourceViewItemUserGroup(resource, ui);
case 'git_server':
@@ -125,7 +125,7 @@ export interface UnifiedResourceKube {
}
export type UnifiedResourceDesktop = {
kind: 'windows_desktop';
kind: 'windows_desktop' | 'linux_desktop';
os: 'windows' | 'linux' | 'darwin';
name: string;
addr: string;
@@ -232,7 +232,8 @@ function getResourcesSubsections({
searchableTags: ['resources', 'desktops', 'rdp', 'windows'],
category: NavigationCategory.Resources,
exact: false,
customRouteMatchFn: () => isKindActive('windows_desktop'),
customRouteMatchFn: () =>
isKindActive('windows_desktop') || isKindActive('linux_desktop'),
onClick: () => setPinnedUserPreference(false),
subCategory: CustomNavigationSubcategory.FilteredViews,
},
@@ -93,6 +93,7 @@ export type ResourceIdKind =
| 'kube_cluster'
| 'user_group'
| 'windows_desktop'
| 'linux_desktop'
| 'saml_idp_service_provider'
| 'aws_ic_account_assignment'
| 'git_server';
@@ -34,6 +34,7 @@ test('correct formatting of desktops fetch response', async () => {
addr: '10.0.0.10',
labels: [{ name: 'env', value: 'test' }],
logins: ['Administrator'],
host_id: undefined,
},
],
startKey: mockResponse.startKey,
@@ -67,6 +68,7 @@ test('null labels field in desktops fetch response', async () => {
const mockResponse = {
items: [
{
kind: 'windows_desktop',
addr: '10.0.0.10',
labels: [{ name: 'env', value: 'test' }],
name: 'DC1-teleport-demo',
@@ -19,13 +19,13 @@
import type { Desktop } from './types';
export function makeDesktop(json): Desktop {
const { os, name, addr, host_id, requiresRequest } = json;
const { os, name, addr, host_id, requiresRequest, kind } = json;
const labels = json.labels || [];
const logins = json.logins || [];
return {
kind: 'windows_desktop',
kind,
os,
name,
addr,
@@ -20,7 +20,7 @@ import { ResourceLabel } from 'teleport/services/agents';
// Desktop is a remote desktop.
export type Desktop = {
kind: 'windows_desktop';
kind: 'windows_desktop' | 'linux_desktop';
// OS is the os of this desktop.
os: 'windows' | 'linux' | 'darwin';
// Name is name (uuid) of the windows desktop.
@@ -39,6 +39,8 @@ export function makeUnifiedResource(json: any): UnifiedResource {
return makeNode(json);
case 'windows_desktop':
return makeDesktop(json);
case 'linux_desktop':
return makeDesktop(json);
case 'git_server':
return makeGitServer(json);
default: