diff --git a/tool/tctl/common/collection.go b/tool/tctl/common/collection.go index 5719344fe4bd3..1afe9f0c7bca9 100644 --- a/tool/tctl/common/collection.go +++ b/tool/tctl/common/collection.go @@ -866,6 +866,35 @@ func (c *windowsDesktopCollection) writeJSON(w io.Writer) error { return utils.WriteJSONArray(w, c.desktops) } +type dynamicWindowsDesktopCollection struct { + desktops []types.DynamicWindowsDesktop +} + +func (c *dynamicWindowsDesktopCollection) resources() (r []types.Resource) { + r = make([]types.Resource, 0, len(c.desktops)) + for _, resource := range c.desktops { + r = append(r, resource) + } + return r +} + +func (c *dynamicWindowsDesktopCollection) writeText(w io.Writer, verbose bool) error { + var rows [][]string + for _, d := range c.desktops { + labels := common.FormatLabels(d.GetAllLabels(), verbose) + rows = append(rows, []string{d.GetName(), d.GetAddr(), d.GetDomain(), labels}) + } + headers := []string{"Name", "Address", "AD Domain", "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) +} + type tokenCollection struct { tokens []types.ProvisionToken } diff --git a/tool/tctl/common/edit_command_test.go b/tool/tctl/common/edit_command_test.go index 9ce3a89d7bf85..c0ffbf342a485 100644 --- a/tool/tctl/common/edit_command_test.go +++ b/tool/tctl/common/edit_command_test.go @@ -91,6 +91,10 @@ func TestEditResources(t *testing.T) { kind: types.KindAutoUpdateVersion, edit: testEditAutoUpdateVersion, }, + { + kind: types.KindDynamicWindowsDesktop, + edit: testEditDynamicWindowsDesktop, + }, } for _, test := range tests { @@ -635,3 +639,35 @@ func testEditAutoUpdateVersion(t *testing.T, clt *authclient.Client) { "tools_autoupdate should have been modified by edit") assert.Equal(t, expected.GetSpec().GetTools().GetTargetVersion(), actual.GetSpec().GetTools().GetTargetVersion()) } + +func testEditDynamicWindowsDesktop(t *testing.T, clt *authclient.Client) { + ctx := context.Background() + + expected, err := types.NewDynamicWindowsDesktopV1("test", nil, types.DynamicWindowsDesktopSpecV1{ + Addr: "test", + }) + require.NoError(t, err) + created, err := clt.DynamicDesktopClient().CreateDynamicWindowsDesktop(ctx, expected) + require.NoError(t, err) + + editor := func(name string) error { + f, err := os.Create(name) + if err != nil { + return trace.Wrap(err, "opening file to edit") + } + + expected.SetRevision(created.GetRevision()) + expected.Spec.Addr = "test2" + + collection := &dynamicWindowsDesktopCollection{desktops: []types.DynamicWindowsDesktop{expected}} + return trace.NewAggregate(writeYAML(collection, f), f.Close()) + } + + _, err = runEditCommand(t, clt, []string{"edit", "dynamic_windows_desktop/test"}, withEditor(editor)) + require.NoError(t, err) + + actual, err := clt.DynamicDesktopClient().GetDynamicWindowsDesktop(ctx, expected.GetName()) + require.NoError(t, err) + expected.SetRevision(actual.GetRevision()) + require.Empty(t, cmp.Diff(expected, actual, protocmp.Transform())) +} diff --git a/tool/tctl/common/resource_command.go b/tool/tctl/common/resource_command.go index 7975a80a7298f..dd558e01f7319 100644 --- a/tool/tctl/common/resource_command.go +++ b/tool/tctl/common/resource_command.go @@ -156,6 +156,7 @@ func (rc *ResourceCommand) Initialize(app *kingpin.Application, config *servicec types.KindOktaImportRule: rc.createOktaImportRule, types.KindIntegration: rc.createIntegration, types.KindWindowsDesktop: rc.createWindowsDesktop, + types.KindDynamicWindowsDesktop: rc.createDynamicWindowsDesktop, types.KindAccessList: rc.createAccessList, types.KindDiscoveryConfig: rc.createDiscoveryConfig, types.KindAuditQuery: rc.createAuditQuery, @@ -193,6 +194,7 @@ func (rc *ResourceCommand) Initialize(app *kingpin.Application, config *servicec types.KindUserTask: rc.updateUserTask, types.KindAutoUpdateConfig: rc.updateAutoUpdateConfig, types.KindAutoUpdateVersion: rc.updateAutoUpdateVersion, + types.KindDynamicWindowsDesktop: rc.updateDynamicWindowsDesktop, } rc.config = config @@ -891,6 +893,45 @@ func (rc *ResourceCommand) createWindowsDesktop(ctx context.Context, client *aut return nil } +func (rc *ResourceCommand) createDynamicWindowsDesktop(ctx context.Context, client *authclient.Client, raw services.UnknownResource) error { + wd, err := services.UnmarshalDynamicWindowsDesktop(raw.Raw) + if err != nil { + return trace.Wrap(err) + } + dynamicDesktopClient := client.DynamicDesktopClient() + if _, err := dynamicDesktopClient.CreateDynamicWindowsDesktop(ctx, wd); err != nil { + if trace.IsAlreadyExists(err) { + if !rc.force { + return trace.AlreadyExists("application %q already exists", wd.GetName()) + } + if _, err := dynamicDesktopClient.UpdateDynamicWindowsDesktop(ctx, wd); err != nil { + return trace.Wrap(err) + } + fmt.Printf("dynamic windows desktop %q has been updated\n", wd.GetName()) + return nil + } + return trace.Wrap(err) + } + + fmt.Printf("dynamic windows desktop %q has been updated\n", wd.GetName()) + return nil +} + +func (rc *ResourceCommand) updateDynamicWindowsDesktop(ctx context.Context, client *authclient.Client, raw services.UnknownResource) error { + wd, err := services.UnmarshalDynamicWindowsDesktop(raw.Raw) + if err != nil { + return trace.Wrap(err) + } + + dynamicDesktopClient := client.DynamicDesktopClient() + if _, err := dynamicDesktopClient.UpdateDynamicWindowsDesktop(ctx, wd); err != nil { + return trace.Wrap(err) + } + + fmt.Printf("dynamic windows desktop %q has been updated\n", wd.GetName()) + return nil +} + func (rc *ResourceCommand) createAppServer(ctx context.Context, client *authclient.Client, raw services.UnknownResource) error { appServer, err := services.UnmarshalAppServer(raw.Raw) if err != nil { @@ -1688,6 +1729,11 @@ func (rc *ResourceCommand) Delete(ctx context.Context, client *authclient.Client return trace.Wrap(err) } fmt.Printf("windows desktop service %q has been deleted\n", rc.ref.Name) + case types.KindDynamicWindowsDesktop: + if err = client.DynamicDesktopClient().DeleteDynamicWindowsDesktop(ctx, rc.ref.Name); err != nil { + return trace.Wrap(err) + } + fmt.Printf("dynamic windows desktop %q has been deleted\n", rc.ref.Name) case types.KindWindowsDesktop: desktops, err := client.GetWindowsDesktops(ctx, types.WindowsDesktopFilter{Name: rc.ref.Name}) @@ -2461,6 +2507,41 @@ func (rc *ResourceCommand) getCollection(ctx context.Context, client *authclient return nil, trace.NotFound("Windows desktop %q not found", rc.ref.Name) } return &windowsDesktopCollection{desktops: out}, nil + case types.KindDynamicWindowsDesktop: + dynamicDesktopClient := client.DynamicDesktopClient() + if rc.ref.Name != "" { + desktop, err := dynamicDesktopClient.GetDynamicWindowsDesktop(ctx, rc.ref.Name) + if err != nil { + return nil, trace.Wrap(err) + } + return &dynamicWindowsDesktopCollection{ + desktops: []types.DynamicWindowsDesktop{desktop}, + }, nil + } + + pageToken := "" + desktops := make([]types.DynamicWindowsDesktop, 0, 100) + for { + d, next, err := dynamicDesktopClient.ListDynamicWindowsDesktop(ctx, 100, pageToken) + if err != nil { + return nil, trace.Wrap(err) + } + if rc.ref.Name == "" { + desktops = append(desktops, d...) + } else { + for _, desktop := range desktops { + if desktop.GetName() == rc.ref.Name { + desktops = append(desktops, desktop) + } + } + } + pageToken = next + if next == "" { + break + } + } + + return &dynamicWindowsDesktopCollection{desktops}, nil case types.KindToken: if rc.ref.Name == "" { tokens, err := client.GetTokens(ctx) diff --git a/tool/tctl/common/resource_command_test.go b/tool/tctl/common/resource_command_test.go index dca834a2e6e7f..cd7b8f7fc5de4 100644 --- a/tool/tctl/common/resource_command_test.go +++ b/tool/tctl/common/resource_command_test.go @@ -1419,6 +1419,10 @@ func TestCreateResources(t *testing.T) { kind: types.KindAutoUpdateVersion, create: testCreateAutoUpdateVersion, }, + { + kind: types.KindDynamicWindowsDesktop, + create: testCreateDynamicWindowsDesktop, + }, } for _, test := range tests { @@ -2359,6 +2363,35 @@ version: v1 )) } +func testCreateDynamicWindowsDesktop(t *testing.T, clt *authclient.Client) { + const resourceYAML = `kind: dynamic_windows_desktop +metadata: + name: test + revision: 3a43b44a-201e-4d7f-aef1-ae2f6d9811ed +spec: + addr: test +version: v1 +` + + // Create the resource. + resourceYAMLPath := filepath.Join(t.TempDir(), "resource.yaml") + require.NoError(t, os.WriteFile(resourceYAMLPath, []byte(resourceYAML), 0644)) + _, err := runResourceCommand(t, clt, []string{"create", resourceYAMLPath}) + require.NoError(t, err) + + // Get the resource + buf, err := runResourceCommand(t, clt, []string{"get", types.KindDynamicWindowsDesktop, "--format=json"}) + require.NoError(t, err) + resources := mustDecodeJSON[[]types.DynamicWindowsDesktopV1](t, buf) + require.Len(t, resources, 1) + + var expected types.DynamicWindowsDesktopV1 + require.NoError(t, yaml.Unmarshal([]byte(resourceYAML), &expected)) + expected.SetRevision(resources[0].GetRevision()) + + require.Empty(t, cmp.Diff([]types.DynamicWindowsDesktopV1{expected}, resources, protocmp.Transform())) +} + func TestPluginResourceWrapper(t *testing.T) { tests := []struct { name string