From 0a65e0a9a40559f163c85d2f42bce5a1543eec67 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Wed, 20 Mar 2024 16:48:50 -0700 Subject: [PATCH] Add groups update command --- internal/commands/iam/groups/update.go | 119 +++++++++++ internal/commands/iam/groups/update_test.go | 217 ++++++++++++++++++++ 2 files changed, 336 insertions(+) create mode 100644 internal/commands/iam/groups/update.go create mode 100644 internal/commands/iam/groups/update_test.go diff --git a/internal/commands/iam/groups/update.go b/internal/commands/iam/groups/update.go new file mode 100644 index 00000000..f538a59e --- /dev/null +++ b/internal/commands/iam/groups/update.go @@ -0,0 +1,119 @@ +package groups + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcp-sdk-go/clients/cloud-iam/stable/2019-12-10/client/groups_service" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-iam/stable/2019-12-10/models" + "github.com/hashicorp/hcp/internal/commands/iam/groups/helper" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/flagvalue" + "github.com/hashicorp/hcp/internal/pkg/heredoc" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" +) + +func NewCmdUpdate(ctx *cmd.Context, runF func(*UpdateOpts) error) *cmd.Command { + opts := &UpdateOpts{ + Ctx: ctx.ShutdownCtx, + Profile: ctx.Profile, + IO: ctx.IO, + Client: groups_service.New(ctx.HCP, nil), + } + + cmd := &cmd.Command{ + Name: "update", + ShortHelp: "Update an existing group.", + LongHelp: heredoc.New(ctx.IO).Must(` + The {{ template "mdCodeOrBold" "hcp iam groups update" }} command updates a group. + + Update can be used to update the display name or description of an existing group. + `), + Examples: []cmd.Example{ + { + Preamble: "Update a group's description.", + Command: heredoc.New(ctx.IO, heredoc.WithPreserveNewlines()).Must(` + $ hcp iam groups update example-group \ + --description="updated description" \ + --display-name="new display name" + `), + }, + }, + Args: cmd.PositionalArguments{ + Args: []cmd.PositionalArgument{ + { + Name: "GROUP_NAME", + Documentation: heredoc.New(ctx.IO).Mustf(helper.GroupNameArgDoc, "update"), + }, + }, + Autocomplete: helper.PredictGroupResourceNameSuffix(opts.Ctx, opts.Profile.OrganizationID, opts.Client), + }, + Flags: cmd.Flags{ + Local: []*cmd.Flag{ + { + Name: "description", + DisplayValue: "NEW_DESCRIPTION", + Description: "New description for the group.", + Value: flagvalue.Simple((*string)(nil), &opts.Description), + }, + { + Name: "display-name", + DisplayValue: "NEW_DISPLAY_NAME", + Description: "New display name for the group.", + Value: flagvalue.Simple((*string)(nil), &opts.DisplayName), + }, + }, + }, + RunF: func(c *cmd.Command, args []string) error { + opts.Name = args[0] + if runF != nil { + return runF(opts) + } + + return updateRun(opts) + }, + PersistentPreRun: func(c *cmd.Command, args []string) error { + return cmd.RequireOrganization(ctx) + }, + } + + return cmd +} + +type UpdateOpts struct { + Ctx context.Context + Profile *profile.Profile + IO iostreams.IOStreams + + Name string + DisplayName *string + Description *string + Client groups_service.ClientService +} + +func updateRun(opts *UpdateOpts) error { + if opts.DisplayName == nil && opts.Description == nil { + return fmt.Errorf("either display name or description must be specified") + } + + rn := helper.ResourceName(opts.Name, opts.Profile.OrganizationID) + req := groups_service.NewGroupsServiceUpdateGroup2ParamsWithContext(opts.Ctx) + req.ResourceName = rn + req.Group = &models.HashicorpCloudIamGroup{} + + if opts.DisplayName != nil { + req.Group.DisplayName = *opts.DisplayName + } + if opts.Description != nil { + req.Group.Description = *opts.Description + } + + if _, err := opts.Client.GroupsServiceUpdateGroup2(req, nil); err != nil { + return fmt.Errorf("failed to update group: %w", err) + } + + fmt.Fprintf(opts.IO.Err(), "%s Group %q updated\n", + opts.IO.ColorScheme().SuccessIcon(), rn) + return nil +} diff --git a/internal/commands/iam/groups/update_test.go b/internal/commands/iam/groups/update_test.go new file mode 100644 index 00000000..a21dd5e2 --- /dev/null +++ b/internal/commands/iam/groups/update_test.go @@ -0,0 +1,217 @@ +package groups + +import ( + "context" + "net/http" + "testing" + + "github.com/go-openapi/runtime/client" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-iam/stable/2019-12-10/client/groups_service" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-iam/stable/2019-12-10/models" + mock_groups_service "github.com/hashicorp/hcp/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-iam/stable/2019-12-10/client/groups_service" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestNewCmdUpdate(t *testing.T) { + t.Parallel() + + bar, baz := "bar", "baz" + cases := []struct { + Name string + Args []string + Profile func(t *testing.T) *profile.Profile + Error string + Expect *UpdateOpts + }{ + { + Name: "No Org", + Profile: profile.TestProfile, + Args: []string{}, + Error: "Organization ID must be configured before running the command.", + }, + { + Name: "Too many args", + Profile: func(t *testing.T) *profile.Profile { + return profile.TestProfile(t).SetOrgID("123") + }, + Args: []string{"foo", "bar"}, + Error: "accepts 1 arg(s), received 2", + }, + { + Name: "Good", + Profile: func(t *testing.T) *profile.Profile { + return profile.TestProfile(t).SetOrgID("123") + }, + Args: []string{"foo", "--display-name", "bar", "--description", "baz"}, + Expect: &UpdateOpts{ + Name: "foo", + DisplayName: &bar, + Description: &baz, + }, + }, + } + + for _, c := range cases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + r := require.New(t) + + // Create a context. + io := iostreams.Test() + ctx := &cmd.Context{ + IO: io, + Profile: c.Profile(t), + Output: format.New(io), + HCP: &client.Runtime{}, + ShutdownCtx: context.Background(), + } + + var gotOpts *UpdateOpts + createCmd := NewCmdUpdate(ctx, func(o *UpdateOpts) error { + gotOpts = o + return nil + }) + createCmd.SetIO(io) + + code := createCmd.Run(c.Args) + if c.Error != "" { + r.NotZero(code) + r.Contains(io.Error.String(), c.Error) + return + } + + r.Zero(code, io.Error.String()) + r.NotNil(gotOpts) + r.Equal(c.Expect.Name, gotOpts.Name) + r.Equal(c.Expect.DisplayName, gotOpts.DisplayName) + r.EqualValues(c.Expect.Description, gotOpts.Description) + }) + } +} + +func TestCreateUpdateNoFields(t *testing.T) { + t.Parallel() + r := require.New(t) + + io := iostreams.Test() + iam := mock_groups_service.NewMockClientService(t) + opts := &UpdateOpts{ + Ctx: context.Background(), + IO: io, + Profile: profile.TestProfile(t).SetOrgID("123"), + Client: iam, + Name: "test", + } + + err := updateRun(opts) + r.ErrorContains(err, "either display name or description must be specified") + +} + +func TestCreateUpdate(t *testing.T) { + t.Parallel() + + cases := []struct { + Name string + RespErr bool + GivenName string + ExpectedName string + DisplayName string + Description string + Error string + }{ + { + Name: "Server error", + GivenName: "test-group", + ExpectedName: "iam/organization/123/group/test-group", + Description: "This is a test", + RespErr: true, + Error: "failed to update group: [PATCH /iam/2019-12-10/{resource_name}][403]", + }, + { + Name: "Good suffix", + GivenName: "test-group", + ExpectedName: "iam/organization/123/group/test-group", + DisplayName: "new display name", + Description: "This is a test", + }, + { + Name: "Good resource name", + GivenName: "iam/organization/456/group/test-group", + ExpectedName: "iam/organization/456/group/test-group", + DisplayName: "new display name", + Description: "This is a test", + }, + } + + for _, c := range cases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + r := require.New(t) + + io := iostreams.Test() + iam := mock_groups_service.NewMockClientService(t) + opts := &UpdateOpts{ + Ctx: context.Background(), + IO: io, + Profile: profile.TestProfile(t).SetOrgID("123"), + Client: iam, + Name: c.GivenName, + } + if c.DisplayName != "" { + opts.DisplayName = &c.DisplayName + } + if c.Description != "" { + opts.Description = &c.Description + } + + // Expect a request to get the user. + call := iam.EXPECT().GroupsServiceUpdateGroup2(mock.MatchedBy(func(req *groups_service.GroupsServiceUpdateGroup2Params) bool { + if req.ResourceName != c.ExpectedName { + return false + } + + if c.DisplayName != "" && req.Group.DisplayName != c.DisplayName { + return false + } + + if c.Description != "" && req.Group.Description != c.Description { + return false + } + + return true + }), nil).Once() + + if c.RespErr { + call.Return(nil, groups_service.NewGroupsServiceUpdateGroup2Default(http.StatusForbidden)) + } else { + ok := groups_service.NewGroupsServiceUpdateGroup2OK() + ok.Payload = &models.HashicorpCloudIamCreateGroupResponse{ + Group: &models.HashicorpCloudIamGroup{ + ResourceID: "iam.group:123456", + ResourceName: c.ExpectedName, + }, + } + + call.Return(ok, nil) + } + + // Run the command + err := updateRun(opts) + if c.Error != "" { + r.ErrorContains(err, c.Error) + return + } + + r.NoError(err) + r.Contains(io.Error.String(), c.ExpectedName) + }) + } +}