Skip to content

Commit

Permalink
Add groups update command
Browse files Browse the repository at this point in the history
  • Loading branch information
dadgar committed Mar 20, 2024
1 parent 238addc commit 0a65e0a
Show file tree
Hide file tree
Showing 2 changed files with 336 additions and 0 deletions.
119 changes: 119 additions & 0 deletions internal/commands/iam/groups/update.go
Original file line number Diff line number Diff line change
@@ -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
}
217 changes: 217 additions & 0 deletions internal/commands/iam/groups/update_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}

0 comments on commit 0a65e0a

Please sign in to comment.