Add tctl support for Linux desktop (#63273)

* Add LinuxDesktop gRPC and backend

* Add tctl support for Linux desktop

* Add LinuxDesktop gRPC and backend

* Remove CloneResource

* Review comments

* Fix logins

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

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

* Fix role

* Remove duplicate code

* Cleanup

* Fix test

---------

Co-authored-by: Zac Bergquist <[email protected]>
This commit is contained in:
Przemko Robakowski
2026-04-22 09:37:12 +00:00
committed by GitHub
co-authored by Zac Bergquist
parent a8cc3174a8
commit d62d4aec25
3 changed files with 247 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
/*
* Teleport
* Copyright (C) 2026 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package resources
import (
"context"
"fmt"
"io"
"github.com/gravitational/trace"
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/asciitable"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/itertools/stream"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/tool/common"
)
type linuxDesktopCollection struct {
desktops []*linuxdesktopv1.LinuxDesktop
}
func linuxDesktopHandler() Handler {
return Handler{
getHandler: getLinuxDesktops,
createHandler: createLinuxDesktop,
updateHandler: updateLinuxDesktop,
deleteHandler: deleteLinuxDesktop,
singleton: false,
mfaRequired: false,
description: "A Linux remote desktop protected by Teleport",
}
}
func (c *linuxDesktopCollection) Resources() (r []types.Resource) {
for _, resource := range c.desktops {
r = append(r, types.ProtoResource153ToLegacy(resource))
}
return r
}
func (c *linuxDesktopCollection) WriteText(w io.Writer, verbose bool) error {
var rows [][]string
for _, d := range c.desktops {
labels := common.FormatLabels(d.Metadata.Labels, verbose)
rows = append(rows, []string{d.Metadata.Name, d.Spec.Addr, d.Spec.Hostname, labels})
}
headers := []string{"Name", "Address", "Hostname", "Labels"}
var t asciitable.Table
if verbose {
t = asciitable.MakeTable(headers, rows...)
} else {
t = asciitable.MakeTableWithTruncatedColumn(headers, rows, "Labels")
}
_, err := t.AsBuffer().WriteTo(w)
return trace.Wrap(err)
}
func getLinuxDesktops(ctx context.Context, client *authclient.Client, ref services.Ref, opts GetOpts) (Collection, error) {
linuxDesktopClient := client.LinuxDesktopClient()
if ref.Name != "" {
desktop, err := linuxDesktopClient.GetLinuxDesktop(ctx, ref.Name)
if err != nil {
if trace.IsNotFound(err) {
return nil, trace.NotFound("Linux desktop %q not found", ref.Name)
}
return nil, trace.Wrap(err)
}
return &linuxDesktopCollection{desktops: []*linuxdesktopv1.LinuxDesktop{desktop}}, nil
}
desktops, err := stream.Collect(clientutils.Resources(ctx, linuxDesktopClient.ListLinuxDesktops))
if err != nil {
return nil, trace.Wrap(err)
}
return &linuxDesktopCollection{desktops: desktops}, nil
}
func deleteLinuxDesktop(ctx context.Context, client *authclient.Client, ref services.Ref) error {
linuxDesktopClient := client.LinuxDesktopClient()
if err := linuxDesktopClient.DeleteLinuxDesktop(ctx, ref.Name); err != nil {
return trace.Wrap(err)
}
fmt.Printf("linux desktop %q has been deleted\n", ref.Name)
return nil
}
func createLinuxDesktop(ctx context.Context, client *authclient.Client, raw services.UnknownResource, opts CreateOpts) error {
desktop, err := services.UnmarshalLinuxDesktop(raw.Raw, services.DisallowUnknown())
if err != nil {
return trace.Wrap(err)
}
linuxDesktopClient := client.LinuxDesktopClient()
if _, err := linuxDesktopClient.CreateLinuxDesktop(ctx, desktop); err != nil {
if trace.IsAlreadyExists(err) {
if !opts.Force {
return trace.AlreadyExists("linux desktop %q already exists", desktop.Metadata.Name)
}
if _, err := linuxDesktopClient.UpsertLinuxDesktop(ctx, desktop); err != nil {
return trace.Wrap(err)
}
fmt.Printf("linux desktop %q has been updated\n", desktop.Metadata.Name)
return nil
}
return trace.Wrap(err)
}
fmt.Printf("linux desktop %q has been created\n", desktop.Metadata.Name)
return nil
}
func updateLinuxDesktop(ctx context.Context, client *authclient.Client, raw services.UnknownResource, opts CreateOpts) error {
desktop, err := services.UnmarshalLinuxDesktop(raw.Raw, services.DisallowUnknown())
if err != nil {
return trace.Wrap(err)
}
linuxDesktopClient := client.LinuxDesktopClient()
if _, err := linuxDesktopClient.UpdateLinuxDesktop(ctx, desktop); err != nil {
return trace.Wrap(err)
}
fmt.Printf("linux desktop %q has been updated\n", desktop.Metadata.Name)
return nil
}
@@ -0,0 +1,99 @@
/*
* Teleport
* Copyright (C) 2026 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package resources
import (
"testing"
"github.com/stretchr/testify/require"
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/asciitable"
)
func makeLinuxDesktop(name, addr, hostname string, labels map[string]string) *linuxdesktopv1.LinuxDesktop {
return &linuxdesktopv1.LinuxDesktop{
Kind: types.KindLinuxDesktop,
Version: types.V1,
Metadata: &headerv1.Metadata{
Name: name,
Labels: labels,
},
Spec: &linuxdesktopv1.LinuxDesktopSpec{
Addr: addr,
Hostname: hostname,
},
}
}
func TestLinuxDesktopCollection_WriteText(t *testing.T) {
desktops := []*linuxdesktopv1.LinuxDesktop{
makeLinuxDesktop("desktop-1", "192.168.1.100:22", "prod-host-1", map[string]string{
"env": "prod",
}),
makeLinuxDesktop("desktop-2", "192.168.1.101:22", "dev-host-1", map[string]string{
"env": "dev",
}),
makeLinuxDesktop("desktop-3", "192.168.1.102:22", "test-host-1", map[string]string{
"env": "test",
}),
}
table := asciitable.MakeTable(
[]string{"Name", "Address", "Hostname", "Labels"},
[]string{"desktop-1", "192.168.1.100:22", "prod-host-1", "env=prod"},
[]string{"desktop-2", "192.168.1.101:22", "dev-host-1", "env=dev"},
[]string{"desktop-3", "192.168.1.102:22", "test-host-1", "env=test"},
)
formatted := table.AsBuffer().String()
collectionFormatTest(t, &linuxDesktopCollection{desktops: desktops}, formatted, formatted)
}
func TestLinuxDesktopCollection_Resources(t *testing.T) {
t.Parallel()
desktop := makeLinuxDesktop("test-desktop", "127.0.0.1:22", "test-host", map[string]string{
"env": "test",
})
collection := &linuxDesktopCollection{desktops: []*linuxdesktopv1.LinuxDesktop{desktop}}
resources := collection.Resources()
require.Len(t, resources, 1)
require.Equal(t, types.KindLinuxDesktop, resources[0].GetKind())
require.Equal(t, "test-desktop", resources[0].GetName())
}
func TestLinuxDesktopHandler(t *testing.T) {
t.Parallel()
handler := linuxDesktopHandler()
require.NotNil(t, handler.getHandler)
require.NotNil(t, handler.createHandler)
require.NotNil(t, handler.updateHandler)
require.NotNil(t, handler.deleteHandler)
require.False(t, handler.singleton)
require.False(t, handler.mfaRequired)
require.Equal(t, "A Linux remote desktop protected by Teleport", handler.description)
}
+1
View File
@@ -70,6 +70,7 @@ func Handlers() map[string]Handler {
types.KindInstaller: installerHandler(),
types.KindKubeServer: kubeServerHandler(),
types.KindKubernetesCluster: kubeClusterHandler(),
types.KindLinuxDesktop: linuxDesktopHandler(),
types.KindLock: lockHandler(),
types.KindNode: serverHandler(),
types.KindOIDCConnector: oidcConnectorHandler(),