From 2a0369c433ba3c0d2e43d7912cbab5cb3aedd3e9 Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Thu, 5 Dec 2024 16:14:07 +0000 Subject: [PATCH 1/5] Add basic tctl commands for WorkloadIdentity resource kind --- api/client/client.go | 7 +++ lib/services/resource.go | 2 + tool/tctl/common/collection.go | 32 ++++++++++++++ tool/tctl/common/resource_command.go | 66 ++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+) diff --git a/api/client/client.go b/api/client/client.go index eb405a343f567..10aa123b166a2 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -97,6 +97,7 @@ import ( userspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/users/v1" usertaskv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/usertasks/v1" "github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1" + workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1" userpreferencespb "github.com/gravitational/teleport/api/gen/proto/go/userpreferences/v1" "github.com/gravitational/teleport/api/internalutils/stream" "github.com/gravitational/teleport/api/metadata" @@ -879,6 +880,12 @@ func (c *Client) SPIFFEFederationServiceClient() machineidv1pb.SPIFFEFederationS return machineidv1pb.NewSPIFFEFederationServiceClient(c.conn) } +// WorkloadIdentityResourceServiceClient returns an unadorned client for the +// workload identity resource service. +func (c *Client) WorkloadIdentityResourceServiceClient() workloadidentityv1pb.WorkloadIdentityResourceServiceClient { + return workloadidentityv1pb.NewWorkloadIdentityResourceServiceClient(c.conn) +} + // PresenceServiceClient returns an unadorned client for the presence service. func (c *Client) PresenceServiceClient() presencepb.PresenceServiceClient { return presencepb.NewPresenceServiceClient(c.conn) diff --git a/lib/services/resource.go b/lib/services/resource.go index d2fba666fa6fd..4ba89f2527078 100644 --- a/lib/services/resource.go +++ b/lib/services/resource.go @@ -245,6 +245,8 @@ func ParseShortcut(in string) (string, error) { return types.KindAccessGraphSettings, nil case types.KindSPIFFEFederation, types.KindSPIFFEFederation + "s": return types.KindSPIFFEFederation, nil + case types.KindWorkloadIdentity, types.KindWorkloadIdentity + "s", "workload_identities", "workloadidentity", "workloadidentities", "workloadidentitys": + return types.KindWorkloadIdentity, nil case types.KindStaticHostUser, types.KindStaticHostUser + "s", "host_user", "host_users": return types.KindStaticHostUser, nil case types.KindUserTask, types.KindUserTask + "s": diff --git a/tool/tctl/common/collection.go b/tool/tctl/common/collection.go index c5134bb82f617..3c844a28637a7 100644 --- a/tool/tctl/common/collection.go +++ b/tool/tctl/common/collection.go @@ -42,6 +42,7 @@ import ( userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v2" usertasksv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/usertasks/v1" "github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1" + workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/accesslist" "github.com/gravitational/teleport/api/types/discoveryconfig" @@ -1784,6 +1785,37 @@ func (c *spiffeFederationCollection) writeText(w io.Writer, verbose bool) error return trace.Wrap(err) } +type workloadIdentityCollection struct { + items []*workloadidentityv1pb.WorkloadIdentity +} + +func (c *workloadIdentityCollection) resources() []types.Resource { + r := make([]types.Resource, 0, len(c.items)) + for _, resource := range c.items { + r = append(r, types.Resource153ToLegacy(resource)) + } + return r +} + +func (c *workloadIdentityCollection) writeText(w io.Writer, verbose bool) error { + headers := []string{"Name", "SPIFFE ID"} + + var rows [][]string + for _, item := range c.items { + rows = append(rows, []string{ + item.Metadata.Name, + item.GetSpec().GetSpiffe().GetId(), + }) + } + + t := asciitable.MakeTable(headers, rows...) + + // stable sort by name. + t.SortRowsBy([]int{0}, true) + _, err := t.AsBuffer().WriteTo(w) + return trace.Wrap(err) +} + type staticHostUserCollection struct { items []*userprovisioningpb.StaticHostUser } diff --git a/tool/tctl/common/resource_command.go b/tool/tctl/common/resource_command.go index a702992f68ea0..36d9f5302046e 100644 --- a/tool/tctl/common/resource_command.go +++ b/tool/tctl/common/resource_command.go @@ -55,6 +55,7 @@ import ( userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v2" usertasksv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/usertasks/v1" "github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1" + workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1" "github.com/gravitational/teleport/api/internalutils/stream" "github.com/gravitational/teleport/api/mfa" "github.com/gravitational/teleport/api/types" @@ -171,6 +172,7 @@ func (rc *ResourceCommand) Initialize(app *kingpin.Application, config *servicec types.KindAccessGraphSettings: rc.upsertAccessGraphSettings, types.KindPlugin: rc.createPlugin, types.KindSPIFFEFederation: rc.createSPIFFEFederation, + types.KindWorkloadIdentity: rc.createWorkloadIdentity, types.KindStaticHostUser: rc.createStaticHostUser, types.KindUserTask: rc.createUserTask, types.KindAutoUpdateConfig: rc.createAutoUpdateConfig, @@ -1076,6 +1078,32 @@ func (rc *ResourceCommand) createSPIFFEFederation(ctx context.Context, client *a return nil } +func (rc *ResourceCommand) createWorkloadIdentity(ctx context.Context, client *authclient.Client, raw services.UnknownResource) error { + in, err := services.UnmarshalWorkloadIdentity(raw.Raw) + if err != nil { + return trace.Wrap(err) + } + + c := client.WorkloadIdentityResourceServiceClient() + if rc.force { + if _, err := c.UpsertWorkloadIdentity(ctx, &workloadidentityv1pb.UpsertWorkloadIdentityRequest{ + WorkloadIdentity: in, + }); err != nil { + return trace.Wrap(err) + } + } else { + if _, err := c.CreateWorkloadIdentity(ctx, &workloadidentityv1pb.CreateWorkloadIdentityRequest{ + WorkloadIdentity: in, + }); err != nil { + return trace.Wrap(err) + } + } + + fmt.Printf("Workload Identity %q has been created\n", in.GetMetadata().GetName()) + + return nil +} + func (rc *ResourceCommand) updateCrownJewel(ctx context.Context, client *authclient.Client, resource services.UnknownResource) error { in, err := services.UnmarshalCrownJewel(resource.Raw) if err != nil { @@ -1969,6 +1997,14 @@ func (rc *ResourceCommand) Delete(ctx context.Context, client *authclient.Client return trace.Wrap(err) } fmt.Printf("SPIFFE federation %q has been deleted\n", rc.ref.Name) + case types.KindWorkloadIdentity: + if _, err := client.WorkloadIdentityResourceServiceClient().DeleteWorkloadIdentity( + ctx, &workloadidentityv1pb.DeleteWorkloadIdentityRequest{ + Name: rc.ref.Name, + }); err != nil { + return trace.Wrap(err) + } + fmt.Printf("Workload Identity %q has been deleted\n", rc.ref.Name) case types.KindStaticHostUser: if err := client.StaticHostUserClient().DeleteStaticHostUser(ctx, rc.ref.Name); err != nil { return trace.Wrap(err) @@ -3122,6 +3158,36 @@ func (rc *ResourceCommand) getCollection(ctx context.Context, client *authclient } return &spiffeFederationCollection{items: resources}, nil + case types.KindWorkloadIdentity: + if rc.ref.Name != "" { + resource, err := client.WorkloadIdentityResourceServiceClient().GetWorkloadIdentity(ctx, &workloadidentityv1pb.GetWorkloadIdentityRequest{ + Name: rc.ref.Name, + }) + if err != nil { + return nil, trace.Wrap(err) + } + return &workloadIdentityCollection{items: []*workloadidentityv1pb.WorkloadIdentity{resource}}, nil + } + + var resources []*workloadidentityv1pb.WorkloadIdentity + pageToken := "" + for { + resp, err := client.WorkloadIdentityResourceServiceClient().ListWorkloadIdentities(ctx, &workloadidentityv1pb.ListWorkloadIdentitiesRequest{ + PageToken: pageToken, + }) + if err != nil { + return nil, trace.Wrap(err) + } + + resources = append(resources, resp.WorkloadIdentities...) + + if resp.NextPageToken == "" { + break + } + pageToken = resp.NextPageToken + } + + return &workloadIdentityCollection{items: resources}, nil case types.KindBotInstance: if rc.ref.Name != "" && rc.ref.SubKind != "" { // Gets a specific bot instance, e.g. bot_instance// From bb53c081ab213b8b8111a8efe82bf90104abb115 Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Fri, 6 Dec 2024 07:32:18 +0000 Subject: [PATCH 2/5] Add `tctl workload-identity ls` --- tool/tctl/common/cmds.go | 1 + tool/tctl/common/workload_identity_command.go | 120 ++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 tool/tctl/common/workload_identity_command.go diff --git a/tool/tctl/common/cmds.go b/tool/tctl/common/cmds.go index 1c727eba01e25..c233761e044b9 100644 --- a/tool/tctl/common/cmds.go +++ b/tool/tctl/common/cmds.go @@ -42,6 +42,7 @@ func Commands() []CLICommand { &DesktopCommand{}, &LockCommand{}, &BotsCommand{}, + &WorkloadIdentityCommand{}, &InventoryCommand{}, &RecordingsCommand{}, &AlertCommand{}, diff --git a/tool/tctl/common/workload_identity_command.go b/tool/tctl/common/workload_identity_command.go new file mode 100644 index 0000000000000..d9deec66dd28e --- /dev/null +++ b/tool/tctl/common/workload_identity_command.go @@ -0,0 +1,120 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package common + +import ( + "context" + "fmt" + "io" + "os" + + "github.com/alecthomas/kingpin/v2" + "github.com/gravitational/trace" + + "github.com/gravitational/teleport" + workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1" + "github.com/gravitational/teleport/lib/auth/authclient" + "github.com/gravitational/teleport/lib/service/servicecfg" +) + +// WorkloadIdentityCommand is a group of commands pertaining to Teleport +// Workload Identity. +type WorkloadIdentityCommand struct { + format string + workloadIdentityName string + + listCmd *kingpin.CmdClause + rmCmd *kingpin.CmdClause + + stdout io.Writer +} + +// Initialize sets up the "tctl workload-identity" command. +func (c *WorkloadIdentityCommand) Initialize( + app *kingpin.Application, config *servicecfg.Config, +) { + // TODO(noah): Remove the hidden flag once base functionality is released. + cmd := app.Command( + "workload-identity", + "Manage Teleport Workload Identity.", + ).Hidden() + + c.listCmd = cmd.Command( + "ls", + "List workload identity configurations.", + ) + c.listCmd. + Flag( + "format", + "Output format, 'text' or 'json'", + ). + Hidden(). + Default(teleport.Text). + EnumVar(&c.format, teleport.Text, teleport.JSON) + + c.rmCmd = cmd.Command( + "rm", + "Delete a workload identity configuration.", + ) + c.rmCmd. + Arg("name", "Name of the workload identity configuration to delete."). + Required(). + StringVar(&c.workloadIdentityName) + + if c.stdout == nil { + c.stdout = os.Stdout + } +} + +// TryRun attempts to run subcommands. +func (c *WorkloadIdentityCommand) TryRun( + ctx context.Context, cmd string, client *authclient.Client, +) (match bool, err error) { + switch cmd { + case c.listCmd.FullCommand(): + panic("unimplemented") + //err = c.ListWorkloadIdentities(ctx, client) + case c.rmCmd.FullCommand(): + err = c.DeleteWorkloadIdentity(ctx, client) + default: + return false, nil + } + + return true, trace.Wrap(err) +} + +func (c *WorkloadIdentityCommand) DeleteWorkloadIdentity( + ctx context.Context, + client *authclient.Client, +) error { + workloadIdentityClient := client.WorkloadIdentityResourceServiceClient() + _, err := workloadIdentityClient.DeleteWorkloadIdentity( + ctx, &workloadidentityv1pb.DeleteWorkloadIdentityRequest{ + Name: c.workloadIdentityName, + }) + if err != nil { + return trace.Wrap(err) + } + + fmt.Fprintf( + c.stdout, + "Workload Identity %q deleted successfully.\n", + c.workloadIdentityName, + ) + + return nil +} From 9e6ff1e0983a8c88f3a6436916a07bb0fc2e13d8 Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Fri, 6 Dec 2024 07:37:49 +0000 Subject: [PATCH 3/5] Add `tctl workload-identity ls` --- tool/tctl/common/workload_identity_command.go | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/tool/tctl/common/workload_identity_command.go b/tool/tctl/common/workload_identity_command.go index d9deec66dd28e..54ceff23dfdaa 100644 --- a/tool/tctl/common/workload_identity_command.go +++ b/tool/tctl/common/workload_identity_command.go @@ -27,8 +27,10 @@ import ( "github.com/gravitational/teleport" workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1" + "github.com/gravitational/teleport/lib/asciitable" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/service/servicecfg" + "github.com/gravitational/teleport/lib/utils" ) // WorkloadIdentityCommand is a group of commands pertaining to Teleport @@ -86,8 +88,7 @@ func (c *WorkloadIdentityCommand) TryRun( ) (match bool, err error) { switch cmd { case c.listCmd.FullCommand(): - panic("unimplemented") - //err = c.ListWorkloadIdentities(ctx, client) + err = c.ListWorkloadIdentities(ctx, client) case c.rmCmd.FullCommand(): err = c.DeleteWorkloadIdentity(ctx, client) default: @@ -118,3 +119,46 @@ func (c *WorkloadIdentityCommand) DeleteWorkloadIdentity( return nil } + +// ListWorkloadIdentities writes a listing of the WorkloadIdentity resources +func (c *WorkloadIdentityCommand) ListWorkloadIdentities( + ctx context.Context, client *authclient.Client, +) error { + workloadIdentityClient := client.WorkloadIdentityResourceServiceClient() + var workloadIdentities []*workloadidentityv1pb.WorkloadIdentity + req := &workloadidentityv1pb.ListWorkloadIdentitiesRequest{} + for { + resp, err := workloadIdentityClient.ListWorkloadIdentities(ctx, req) + if err != nil { + return trace.Wrap(err) + } + + workloadIdentities = append( + workloadIdentities, resp.WorkloadIdentities..., + ) + if resp.NextPageToken == "" { + break + } + req.PageToken = resp.NextPageToken + } + + if c.format == teleport.Text { + if len(workloadIdentities) == 0 { + fmt.Fprintln(c.stdout, "No workload identities configured") + return nil + } + t := asciitable.MakeTable([]string{"Name", "SPIFFE ID"}) + for _, u := range workloadIdentities { + t.AddRow([]string{ + u.GetMetadata().GetName(), u.GetSpec().GetSpiffe().GetId(), + }) + } + fmt.Fprintln(c.stdout, t.AsBuffer().String()) + } else { + err := utils.WriteJSONArray(c.stdout, workloadIdentities) + if err != nil { + return trace.Wrap(err, "failed to marshal workload identities") + } + } + return nil +} From 63f8bbe61edc353568d5fe9c1439e8fc2cf29d72 Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Mon, 9 Dec 2024 12:01:33 +0000 Subject: [PATCH 4/5] Fix message strings to be consistent --- tool/tctl/common/resource_command.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tool/tctl/common/resource_command.go b/tool/tctl/common/resource_command.go index 36d9f5302046e..3faa1d656fa02 100644 --- a/tool/tctl/common/resource_command.go +++ b/tool/tctl/common/resource_command.go @@ -1099,7 +1099,7 @@ func (rc *ResourceCommand) createWorkloadIdentity(ctx context.Context, client *a } } - fmt.Printf("Workload Identity %q has been created\n", in.GetMetadata().GetName()) + fmt.Printf("Workload identity %q has been created\n", in.GetMetadata().GetName()) return nil } @@ -2004,7 +2004,7 @@ func (rc *ResourceCommand) Delete(ctx context.Context, client *authclient.Client }); err != nil { return trace.Wrap(err) } - fmt.Printf("Workload Identity %q has been deleted\n", rc.ref.Name) + fmt.Printf("Workload identity %q has been deleted\n", rc.ref.Name) case types.KindStaticHostUser: if err := client.StaticHostUserClient().DeleteStaticHostUser(ctx, rc.ref.Name); err != nil { return trace.Wrap(err) From 2d3b284b9096aee5bb503f59a924a2ba84111c88 Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Mon, 9 Dec 2024 12:36:17 +0000 Subject: [PATCH 5/5] Add tests for `tctl workload-identity` functionality --- .../workload-identity_ls.golden | 4 + .../workload-identity_ls_empty.golden | 1 + .../workload-identity_rm.golden | 1 + tool/tctl/common/workload_identity_test.go | 163 ++++++++++++++++++ 4 files changed, 169 insertions(+) create mode 100644 tool/tctl/common/testdata/TestWorkloadIdentity/workload-identity_ls.golden create mode 100644 tool/tctl/common/testdata/TestWorkloadIdentity/workload-identity_ls_empty.golden create mode 100644 tool/tctl/common/testdata/TestWorkloadIdentity/workload-identity_rm.golden create mode 100644 tool/tctl/common/workload_identity_test.go diff --git a/tool/tctl/common/testdata/TestWorkloadIdentity/workload-identity_ls.golden b/tool/tctl/common/testdata/TestWorkloadIdentity/workload-identity_ls.golden new file mode 100644 index 0000000000000..80f6254aa91dc --- /dev/null +++ b/tool/tctl/common/testdata/TestWorkloadIdentity/workload-identity_ls.golden @@ -0,0 +1,4 @@ +Name SPIFFE ID +---- --------- +test /test + diff --git a/tool/tctl/common/testdata/TestWorkloadIdentity/workload-identity_ls_empty.golden b/tool/tctl/common/testdata/TestWorkloadIdentity/workload-identity_ls_empty.golden new file mode 100644 index 0000000000000..1e9ddb9aaa645 --- /dev/null +++ b/tool/tctl/common/testdata/TestWorkloadIdentity/workload-identity_ls_empty.golden @@ -0,0 +1 @@ +No workload identities configured diff --git a/tool/tctl/common/testdata/TestWorkloadIdentity/workload-identity_rm.golden b/tool/tctl/common/testdata/TestWorkloadIdentity/workload-identity_rm.golden new file mode 100644 index 0000000000000..3195b79b30d87 --- /dev/null +++ b/tool/tctl/common/testdata/TestWorkloadIdentity/workload-identity_rm.golden @@ -0,0 +1 @@ +Workload Identity "test" deleted successfully. diff --git a/tool/tctl/common/workload_identity_test.go b/tool/tctl/common/workload_identity_test.go new file mode 100644 index 0000000000000..818ec2cb1d8f4 --- /dev/null +++ b/tool/tctl/common/workload_identity_test.go @@ -0,0 +1,163 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package common + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" + "gopkg.in/yaml.v3" + + headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" + workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/auth/authclient" + "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/teleport/lib/utils/golden" + "github.com/gravitational/teleport/tool/teleport/testenv" +) + +func runWorkloadIdentityCommand( + t *testing.T, clt *authclient.Client, args []string, +) (*bytes.Buffer, error) { + var stdoutBuf bytes.Buffer + cmd := &WorkloadIdentityCommand{ + stdout: &stdoutBuf, + } + return &stdoutBuf, runCommand(t, clt, cmd, args) +} + +func TestWorkloadIdentity(t *testing.T) { + t.Parallel() + + process := testenv.MakeTestServer(t, testenv.WithLogger(utils.NewSlogLoggerForTests())) + rootClient := testenv.MakeDefaultAuthClient(t, process) + + yamlData := `kind: workload_identity +version: v1 +metadata: + name: test +spec: + spiffe: + id: /test +` + var expected workloadidentityv1pb.WorkloadIdentity + require.NoError(t, yaml.Unmarshal([]byte(yamlData), &expected)) + + t.Run("workload-identity ls empty", func(t *testing.T) { + buf, err := runWorkloadIdentityCommand( + t, rootClient, []string{ + "workload-identity", "ls", + }, + ) + require.NoError(t, err) + if golden.ShouldSet() { + golden.Set(t, buf.Bytes()) + } + require.Equal(t, string(golden.Get(t)), buf.String()) + }) + + t.Run("resource list empty", func(t *testing.T) { + buf, err := runResourceCommand( + t, rootClient, []string{ + "get", + types.KindWorkloadIdentity, + "--format=json", + }, + ) + require.NoError(t, err) + + resources := mustDecodeJSON[[]*workloadidentityv1pb.WorkloadIdentity](t, buf) + require.Empty(t, resources) + }) + + t.Run("create", func(t *testing.T) { + + yamlPath := filepath.Join(t.TempDir(), "workload_identity.yaml") + require.NoError(t, os.WriteFile(yamlPath, []byte(yamlData), 0644)) + _, err := runResourceCommand(t, rootClient, []string{"create", yamlPath}) + require.NoError(t, err) + }) + + t.Run("workload-identity ls", func(t *testing.T) { + buf, err := runWorkloadIdentityCommand( + t, rootClient, []string{ + "workload-identity", "ls", + }, + ) + require.NoError(t, err) + if golden.ShouldSet() { + golden.Set(t, buf.Bytes()) + } + require.Equal(t, string(golden.Get(t)), buf.String()) + }) + + t.Run("resource list", func(t *testing.T) { + buf, err := runResourceCommand( + t, rootClient, []string{ + "get", + types.KindWorkloadIdentity, + "--format=json", + }, + ) + require.NoError(t, err) + + resources := mustDecodeJSON[[]*workloadidentityv1pb.WorkloadIdentity](t, buf) + require.NotEmpty(t, resources) + require.Empty(t, cmp.Diff( + []*workloadidentityv1pb.WorkloadIdentity{&expected}, + resources, + protocmp.Transform(), + protocmp.IgnoreFields(&headerv1.Metadata{}, "revision"), + )) + }) + + t.Run("workload-identity rm", func(t *testing.T) { + buf, err := runWorkloadIdentityCommand( + t, rootClient, []string{ + "workload-identity", "rm", + expected.GetMetadata().GetName(), + }, + ) + require.NoError(t, err) + if golden.ShouldSet() { + golden.Set(t, buf.Bytes()) + } + require.Equal(t, string(golden.Get(t)), buf.String()) + }) + + t.Run("resource list empty after delete", func(t *testing.T) { + buf, err := runResourceCommand( + t, rootClient, []string{ + "get", + types.KindWorkloadIdentity, + "--format=json", + }, + ) + require.NoError(t, err) + + resources := mustDecodeJSON[[]*workloadidentityv1pb.WorkloadIdentity](t, buf) + require.Empty(t, resources) + }) +}