Skip to content

Commit

Permalink
Merge pull request #41 from hashicorp/f-group-updates
Browse files Browse the repository at this point in the history
Add update group command
  • Loading branch information
dadgar authored Mar 23, 2024
2 parents 556af8c + 9f15960 commit 85e39b0
Show file tree
Hide file tree
Showing 9 changed files with 423 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .changelog/41.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
iam groups: Add update group command.
```
3 changes: 3 additions & 0 deletions .github/scripts/changelog_checker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ if [ -z "$changelog_files" ]; then
exit 1
fi

# Install the changelog-check command
go install github.com/hashicorp/go-changelog/cmd/changelog-check@latest

# Validate format with make changelog-check, exit with error if any note has an
# invalid format
for file in $changelog_files; do
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/changelog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0 # by default the checkout action doesn't checkout all branches
- uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5
with:
go-version: 'stable'
- name: Check for changelog entry in diff
run: ./.github/scripts/changelog_checker.sh
env:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ require (
github.com/hashicorp/go-hclog v1.5.0
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/hcl/v2 v2.19.1
github.com/hashicorp/hcp-sdk-go v0.87.1-0.20240319165551-1cfc2eaf58a7
github.com/hashicorp/hcp-sdk-go v0.89.0
github.com/lithammer/dedent v1.1.0
github.com/manifoldco/promptui v0.9.0
github.com/mitchellh/cli v1.1.5
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI=
github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
github.com/hashicorp/hcp-sdk-go v0.87.1-0.20240319165551-1cfc2eaf58a7 h1:+QcV5/qp7qO/L+xaaX06ZHpX0YzM1A0Ex80yW0hklxw=
github.com/hashicorp/hcp-sdk-go v0.87.1-0.20240319165551-1cfc2eaf58a7/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk=
github.com/hashicorp/hcp-sdk-go v0.89.0 h1:OpN2Yvr0YL/9kEMXpa1fvq5v7d66A4Vc4tilL/fwcwc=
github.com/hashicorp/hcp-sdk-go v0.89.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk=
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
Expand Down
1 change: 1 addition & 0 deletions internal/commands/iam/groups/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func NewCmdGroups(ctx *cmd.Context) *cmd.Command {
cmd.AddChild(NewCmdRead(ctx, nil))
cmd.AddChild(NewCmdCreate(ctx, nil))
cmd.AddChild(NewCmdDelete(ctx, nil))
cmd.AddChild(NewCmdUpdate(ctx, nil))
cmd.AddChild(members.NewCmdMembers(ctx))
return cmd
}
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)
})
}
}
Loading

0 comments on commit 85e39b0

Please sign in to comment.