diff --git a/internal/provider/resource_group.go b/internal/provider/resource_group.go index 1de810a5..8e847739 100644 --- a/internal/provider/resource_group.go +++ b/internal/provider/resource_group.go @@ -12,6 +12,7 @@ const ( groupNameKey = "name" groupDescriptionKey = "description" groupScopeIDKey = "scope_id" + groupMemberIDsKey = "member_ids" ) func resourceGroup() *schema.Resource { @@ -35,6 +36,11 @@ func resourceGroup() *schema.Resource { ForceNew: true, Computed: true, }, + groupMemberIDsKey: { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, }, } } @@ -53,12 +59,23 @@ func convertGroupToResourceData(g *groups.Group, d *schema.ResourceData) error { } } - if g.Scope.Id != "" { + // TODO when calling SetMembers() on a group the API returns + // a nil ScopeInfo so we are checking here to ensure + // we catch it. This work around can possibly ignore + // an updated scope ID when updating members and scope + // of a group simultaniously. + if g.Scope != nil && g.Scope.Id != "" { if err := d.Set(groupScopeIDKey, g.Scope.Id); err != nil { return err } } + if g.MemberIds != nil { + if err := d.Set(groupMemberIDsKey, g.MemberIds); err != nil { + return err + } + } + d.SetId(g.Id) return nil @@ -80,6 +97,13 @@ func convertResourceDataToGroup(d *schema.ResourceData, meta *metaData) *groups. g.Scope.Id = scopeIDVal.(string) } + if val, ok := d.GetOk(groupMemberIDsKey); ok { + memberIds := val.(*schema.Set).List() + for _, i := range memberIds { + g.MemberIds = append(g.MemberIds, i.(string)) + } + } + if d.Id() != "" { g.Id = d.Id() } @@ -95,6 +119,8 @@ func resourceGroupCreate(d *schema.ResourceData, meta interface{}) error { g := convertResourceDataToGroup(d, md) grps := groups.NewGroupsClient(client) + memIDs := g.MemberIds + g, apiErr, err := grps.Create( ctx, groups.WithScopeId(g.Scope.Id), @@ -107,6 +133,21 @@ func resourceGroupCreate(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error creating group: %s", apiErr.Message) } + if len(memIDs) > 0 { + g, apiErr, err = grps.SetMembers( + ctx, + g.Id, + g.Version, + memIDs, + groups.WithScopeId(g.Scope.Id)) + if apiErr != nil { + return fmt.Errorf("error setting principals on role: %s\n", apiErr.Message) + } + if err != nil { + return fmt.Errorf("error setting principals on role: %s\n", err) + } + } + return convertGroupToResourceData(g, d) } @@ -162,6 +203,24 @@ func resourceGroupUpdate(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("%+v\n", apiErr.Message) } + if d.HasChange(groupMemberIDsKey) { + memberIds := []string{} + members := d.Get(groupMemberIDsKey).(*schema.Set).List() + for _, member := range members { + memberIds = append(memberIds, member.(string)) + } + + g, apiErr, err = grps.SetMembers( + ctx, + g.Id, + g.Version, + memberIds, + groups.WithScopeId(g.Scope.Id)) + if apiErr != nil || err != nil { + return fmt.Errorf("error updating members on group:\n API Err: %+v\n Err: %+v\n", *apiErr, err) + } + } + return convertGroupToResourceData(g, d) } diff --git a/internal/provider/resource_group_test.go b/internal/provider/resource_group_test.go index 2fcbbe98..aeca0ad6 100644 --- a/internal/provider/resource_group_test.go +++ b/internal/provider/resource_group_test.go @@ -29,6 +29,30 @@ resource "boundary_group" "foo" { description = "%s" }`, fooGroupDescriptionUpdate) + orgGroupWithMembers = ` +resource "boundary_user" "foo" { + description = "foo" +} + +resource "boundary_group" "with_members" { + description = "with members" + member_ids = [boundary_user.foo.id] +}` + + orgGroupWithMembersUpdate = ` +resource "boundary_user" "foo" { + description = "foo" +} + +resource "boundary_user" "bar" { + description = "bar" +} + +resource "boundary_group" "with_members" { + description = "with members" + member_ids = [boundary_user.foo.id, boundary_user.bar.id] +}` + orgToProjectGroupUpdate = fmt.Sprintf(` resource "boundary_group" "foo" { name = "test" @@ -131,6 +155,105 @@ func TestAccGroup(t *testing.T) { }) } +func TestAccGroupWithMembers(t *testing.T) { + tc := controller.NewTestController(t, tcConfig...) + + defer tc.Shutdown() + url := tc.ApiAddrs()[0] + + resource.Test(t, resource.TestCase{ + Providers: testProviders, + CheckDestroy: testAccCheckGroupResourceDestroy(t), + Steps: []resource.TestStep{ + { + // test create + Config: testConfig(url, orgGroupWithMembers), + Check: resource.ComposeTestCheckFunc( + testAccCheckGroupResourceExists("boundary_group.with_members"), + testAccCheckGroupResourceExists("boundary_user.foo"), + resource.TestCheckResourceAttr("boundary_group.with_members", groupDescriptionKey, "with members"), + testAccCheckGroupResourceMembersSet("boundary_group.with_members", []string{"boundary_user.foo"}), + ), + }, + { + // test update + Config: testConfig(url, orgGroupWithMembersUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckGroupResourceExists("boundary_group.with_members"), + testAccCheckGroupResourceExists("boundary_user.foo"), + resource.TestCheckResourceAttr("boundary_group.with_members", groupDescriptionKey, "with members"), + testAccCheckGroupResourceMembersSet("boundary_group.with_members", []string{"boundary_user.foo", "boundary_user.bar"}), + ), + }, + }, + }) +} + +func testAccCheckGroupResourceMembersSet(name string, members []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("role resource not found: %s", name) + } + + id := rs.Primary.ID + if id == "" { + return fmt.Errorf("role resource ID is not set") + } + + // ensure users are declared in state + memberIDs := []string{} + for _, userResourceName := range members { + ur, ok := s.RootModule().Resources[userResourceName] + if !ok { + return fmt.Errorf("user resource not found: %s", userResourceName) + } + + memberID := ur.Primary.ID + if id == "" { + return fmt.Errorf("principal resource ID not set") + } + + memberIDs = append(memberIDs, memberID) + } + + // check boundary to ensure it matches + md := testProvider.Meta().(*metaData) + client := md.client.Clone() + + projID, ok := rs.Primary.Attributes["scope_id"] + if ok { + client.SetScopeId(projID) + } + grpsClient := groups.NewGroupsClient(client) + + g, _, err := grpsClient.Read(md.ctx, id) + if err != nil { + return fmt.Errorf("Got an error when reading role %q: %v", id, err) + } + + // for every member set as a member on the group in the state, ensure + // each group in boundary has the same setings + if len(g.MemberIds) == 0 { + return fmt.Errorf("no members found on group") + } + + for _, stateMember := range g.MemberIds { + ok := false + for _, gotMember := range memberIDs { + if gotMember == stateMember { + ok = true + } + } + if !ok { + return fmt.Errorf("member in state not set in boundary: %s", stateMember) + } + } + + return nil + } +} + func testAccCheckGroupProjectScope(name string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[name] @@ -206,6 +329,8 @@ func testAccCheckGroupResourceDestroy(t *testing.T) resource.TestCheckFunc { switch rs.Type { case "boundary_project": continue + case "boundary_user": + continue case "boundary_group": projClient := md.client.Clone() projID, ok := rs.Primary.Attributes["scope_id"] diff --git a/internal/provider/resource_role.go b/internal/provider/resource_role.go index b2dc6abf..46e3ad83 100644 --- a/internal/provider/resource_role.go +++ b/internal/provider/resource_role.go @@ -189,8 +189,6 @@ func resourceRoleCreate(d *schema.ResourceData, meta interface{}) error { } } - d.SetId(r.Id) - return convertRoleToResourceData(r, d) } diff --git a/website/docs/r/resource_group.html.markdown b/website/docs/r/resource_group.html.markdown index f1f19afd..301c6e14 100644 --- a/website/docs/r/resource_group.html.markdown +++ b/website/docs/r/resource_group.html.markdown @@ -12,9 +12,14 @@ The group resource allows you to configure a Boundary group. ## Example Usage ```hcl +resource "boundary_user" "foo" { + description = "foo user" +} + resource "boundary_group" "example" { name = "My group" description = "My first group!" + member_ids = [boundary_user.foo.id] } ``` @@ -25,9 +30,14 @@ resource "boundary_project" "foo" { name = "foo_project" } +resource "boundary_user" "foo" { + description = "foo user" +} + resource "boundary_group" "example" { name = "My group" description = "My first group!" + member_ids = [boundary_user.foo.id] scope_id = boundary_project.foo.id } ``` @@ -38,3 +48,4 @@ The following arguments are optional: * `description` - The group description. * `name` - The group name. Defaults to the resource name. * `scope_id` - The scope ID in which the resource is created. Defaults to the provider's `default_scope` if unset. +* `member_ids` - Resource IDs for group members, these are most likely boundary users.