diff --git a/docs/data-sources/group.md b/docs/data-sources/group.md new file mode 100644 index 00000000..41957e52 --- /dev/null +++ b/docs/data-sources/group.md @@ -0,0 +1,60 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "boundary_group Data Source - terraform-provider-boundary" +subcategory: "" +description: |- + The boundary_group data source allows you to find a Boundary group. +--- + +# boundary_group (Data Source) + +The boundary_group data source allows you to find a Boundary group. + +## Example Usage + +```terraform +# Retrieve a user from the global scope +data "boundary_group" "global_group" { + name = "admin" +} + +# User from an org scope +data "boundary_scope" "org" { + name = "org" + scope_id = "global" +} + +data "boundary_group" "org_group" { + name = "username" + scope_id = data.boundary_scope.org.id +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the group to retrieve. + +### Optional + +- `scope_id` (String) The scope ID in which the resource is created. Defaults `global` if unset. + +### Read-Only + +- `description` (String) The description of the retrieved group. +- `id` (String) The ID of the retrieved group. +- `member_ids` (Set of String) Resource IDs for group members, these are most likely boundary users. +- `scope` (List of Object) (see [below for nested schema](#nestedatt--scope)) + + +### Nested Schema for `scope` + +Read-Only: + +- `description` (String) +- `id` (String) +- `name` (String) +- `parent_scope_id` (String) +- `type` (String) diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md index 146f1e7a..8dc5330a 100644 --- a/docs/data-sources/user.md +++ b/docs/data-sources/user.md @@ -13,13 +13,11 @@ The user data source allows you to find a Boundary user. ```terraform # Retrieve a user from the global scope - data "boundary_user" "global_scope_admin" { name = "admin" } # User from a org scope - data "boundary_user" "org_user" { name = "username" scope_id = data.boundary_scope.org.id diff --git a/examples/data-sources/boundary_group/data-source.tf b/examples/data-sources/boundary_group/data-source.tf new file mode 100644 index 00000000..f039e0ed --- /dev/null +++ b/examples/data-sources/boundary_group/data-source.tf @@ -0,0 +1,15 @@ +# Retrieve a user from the global scope +data "boundary_group" "global_group" { + name = "admin" +} + +# User from an org scope +data "boundary_scope" "org" { + name = "org" + scope_id = "global" +} + +data "boundary_group" "org_group" { + name = "username" + scope_id = data.boundary_scope.org.id +} diff --git a/examples/data-sources/boundary_user/data-source.tf b/examples/data-sources/boundary_user/data-source.tf index b6685579..bf2fe63b 100644 --- a/examples/data-sources/boundary_user/data-source.tf +++ b/examples/data-sources/boundary_user/data-source.tf @@ -1,11 +1,9 @@ # Retrieve a user from the global scope - data "boundary_user" "global_scope_admin" { name = "admin" } # User from a org scope - data "boundary_user" "org_user" { name = "username" scope_id = data.boundary_scope.org.id diff --git a/internal/provider/const.go b/internal/provider/const.go index 8823b0f7..8945b944 100644 --- a/internal/provider/const.go +++ b/internal/provider/const.go @@ -46,9 +46,14 @@ const ( internalForceUpdateKey = "internal_force_update" // workerFilter is used for common "worker_filter" resource attribute WorkerFilterKey = "worker_filter" - // common User arguments - LoginNameKey = "login_name" + // LoginNameKey is used for common "login_name" resource attribute + LoginNameKey = "login_name" + // PrimaryAccountIdKey is used for common "primary_account_id" resource attribute PrimaryAccountIdKey = "primary_account_id" - ScopeKey = "scope" - ParentScopeIdKey = "parent_scope_id" + // ScopeKey is used for common "scope" resource attribute + ScopeKey = "scope" + // ParentScopeKey is used for common "parent_scope_id" resource attribute + ParentScopeIdKey = "parent_scope_id" + // GroupMemberIdsKey is used for common "member_ids" resource attribute + GroupMemberIdsKey = "member_ids" ) diff --git a/internal/provider/data_source_group.go b/internal/provider/data_source_group.go new file mode 100644 index 00000000..0d74bf49 --- /dev/null +++ b/internal/provider/data_source_group.go @@ -0,0 +1,144 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "net/http" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/groups" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func dataSourceGroup() *schema.Resource { + return &schema.Resource{ + Description: "The boundary_group data source allows you to find a Boundary group.", + ReadContext: dataSourceGroupRead, + + Schema: map[string]*schema.Schema{ + NameKey: { + Description: "The name of the group to retrieve.", + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + ScopeIdKey: { + Description: "The scope ID in which the resource is created. Defaults `global` if unset.", + Type: schema.TypeString, + Optional: true, + Default: "global", + ValidateFunc: validation.StringIsNotEmpty, + }, + IDKey: { + Description: "The ID of the retrieved group.", + Type: schema.TypeString, + Computed: true, + }, + DescriptionKey: { + Description: "The description of the retrieved group.", + Type: schema.TypeString, + Computed: true, + }, + ScopeKey: { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + IDKey: { + Type: schema.TypeString, + Computed: true, + }, + NameKey: { + Type: schema.TypeString, + Computed: true, + }, + TypeKey: { + Type: schema.TypeString, + Computed: true, + }, + DescriptionKey: { + Type: schema.TypeString, + Computed: true, + }, + ParentScopeIdKey: { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + GroupMemberIdsKey: { + Description: "Resource IDs for group members, these are most likely boundary users.", + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + }, + }, + } +} + +func dataSourceGroupRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + + name := d.Get(NameKey).(string) + scopeId := d.Get(ScopeIdKey).(string) + + gcl := groups.NewClient(md.client) + groupsList, err := gcl.List(ctx, scopeId, + groups.WithFilter(FilterWithItemNameMatches(name)), + ) + if err != nil { + return diag.Errorf("error calling list group: %v", err) + } + groups := groupsList.GetItems() + if groups == nil { + return diag.Errorf("no groups found") + } + if len(groups) == 0 { + return diag.Errorf("no matching group found") + } + if len(groups) > 1 { + return diag.Errorf("error found more than 1 group") + } + + grr, err := gcl.Read(ctx, groups[0].Id) + if err != nil { + if apiErr := api.AsServerError(err); apiErr != nil && apiErr.Response().StatusCode() == http.StatusNotFound { + d.SetId("") + return nil + } + return diag.Errorf("error calling read group: %v", err) + } + if grr == nil { + return diag.Errorf("group nil after read") + } + + if err := setFromGroupRead(d, *grr.Item); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func setFromGroupRead(d *schema.ResourceData, group groups.Group) error { + if err := d.Set(NameKey, group.Name); err != nil { + return err + } + if err := d.Set(DescriptionKey, group.Description); err != nil { + return err + } + if err := d.Set(ScopeIdKey, group.ScopeId); err != nil { + return err + } + if err := d.Set(GroupMemberIdsKey, group.MemberIds); err != nil { + return err + } + + d.Set(ScopeKey, flattenScopeInfo(group.Scope)) + d.SetId(group.Id) + return nil +} diff --git a/internal/provider/data_source_group_test.go b/internal/provider/data_source_group_test.go new file mode 100644 index 00000000..82da9140 --- /dev/null +++ b/internal/provider/data_source_group_test.go @@ -0,0 +1,113 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/boundary/testing/controller" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ( + testGroupName = "test_group" +) + +var groupReadGlobal = fmt.Sprintf(` + +resource "boundary_user" "user" { + description = "user" + scope_id = "global" + depends_on = [boundary_role.org1_admin] +} + +resource "boundary_group" "group" { + name = "%s" + description = "test" + scope_id = "global" + member_ids = [boundary_user.user.id] + depends_on = [boundary_user.user] +} + +data "boundary_group" "group" { + depends_on = [ boundary_group.group ] + name = "%s" +}`, testGroupName, testGroupName) + +var groupReadOrg = fmt.Sprintf(` +resource "boundary_user" "user" { + description = "user" + scope_id = boundary_scope.org1.id + depends_on = [boundary_role.org1_admin] +} + +resource "boundary_group" "group" { + name = "%s" + description = "test" + scope_id = boundary_scope.org1.id + member_ids = [boundary_user.user.id] + depends_on = [boundary_user.user] +} + +data "boundary_group" "group" { + depends_on = [ boundary_group.group ] + name = "%s" + scope_id = boundary_scope.org1.id +}`, testGroupName, testGroupName) + +func TestAccGroupReadGlobal(t *testing.T) { + tc := controller.NewTestController(t, tcConfig...) + defer tc.Shutdown() + url := tc.ApiAddrs()[0] + + var provider *schema.Provider + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories(&provider), + Steps: []resource.TestStep{ + { + Config: testConfig(url, fooOrg, groupReadGlobal), + Check: resource.ComposeTestCheckFunc( + testAccCheckGroupResourceExists(provider, "boundary_group.group"), + resource.TestCheckResourceAttrSet("data.boundary_group.group", IDKey), + resource.TestCheckResourceAttrSet("data.boundary_group.group", ScopeIdKey), + resource.TestCheckResourceAttr("data.boundary_group.group", NameKey, testGroupName), + resource.TestCheckResourceAttrSet("data.boundary_group.group", DescriptionKey), + resource.TestCheckResourceAttrSet("data.boundary_group.group", fmt.Sprintf("%s.#", GroupMemberIdsKey)), + resource.TestCheckResourceAttrSet("data.boundary_group.group", "scope.0.id"), + resource.TestCheckResourceAttr("data.boundary_group.group", "scope.0.name", "global"), + resource.TestCheckResourceAttr("data.boundary_group.group", "scope.0.type", "global"), + ), + }, + }, + }) +} + +func TestAccGroupReadOrg(t *testing.T) { + tc := controller.NewTestController(t, tcConfig...) + defer tc.Shutdown() + url := tc.ApiAddrs()[0] + + var provider *schema.Provider + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories(&provider), + Steps: []resource.TestStep{ + { + Config: testConfig(url, fooOrg, groupReadOrg), + Check: resource.ComposeTestCheckFunc( + testAccCheckGroupResourceExists(provider, "boundary_group.group"), + resource.TestCheckResourceAttrSet("data.boundary_group.group", IDKey), + resource.TestCheckResourceAttrSet("data.boundary_group.group", ScopeIdKey), + resource.TestCheckResourceAttr("data.boundary_group.group", NameKey, testGroupName), + resource.TestCheckResourceAttrSet("data.boundary_group.group", DescriptionKey), + resource.TestCheckResourceAttrSet("data.boundary_group.group", fmt.Sprintf("%s.#", GroupMemberIdsKey)), + resource.TestCheckResourceAttrSet("data.boundary_group.group", "scope.0.id"), + resource.TestCheckResourceAttr("data.boundary_group.group", "scope.0.name", "org1"), + resource.TestCheckResourceAttr("data.boundary_group.group", "scope.0.type", "org"), + ), + }, + }, + }) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 83cf8551..1d4ba4ad 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -129,6 +129,7 @@ func New() *schema.Provider { }, DataSourcesMap: map[string]*schema.Resource{ "boundary_account": dataSourceAccount(), + "boundary_group": dataSourceGroup(), "boundary_scope": dataSourceScope(), "boundary_user": dataSourceUser(), }, diff --git a/internal/provider/resource_group.go b/internal/provider/resource_group.go index 68db6b08..b136cb97 100644 --- a/internal/provider/resource_group.go +++ b/internal/provider/resource_group.go @@ -13,10 +13,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -const ( - groupMemberIdsKey = "member_ids" -) - func resourceGroup() *schema.Resource { return &schema.Resource{ Description: "The group resource allows you to configure a Boundary group.", @@ -51,7 +47,7 @@ func resourceGroup() *schema.Resource { Required: true, ForceNew: true, }, - groupMemberIdsKey: { + GroupMemberIdsKey: { Description: "Resource IDs for group members, these are most likely boundary users.", Type: schema.TypeSet, Optional: true, @@ -71,7 +67,7 @@ func setFromGroupResponseMap(d *schema.ResourceData, raw map[string]interface{}) if err := d.Set(ScopeIdKey, raw["scope_id"]); err != nil { return err } - if err := d.Set(groupMemberIdsKey, raw["member_ids"]); err != nil { + if err := d.Set(GroupMemberIdsKey, raw["member_ids"]); err != nil { return err } d.SetId(raw["id"].(string)) @@ -118,7 +114,7 @@ func resourceGroupCreate(ctx context.Context, d *schema.ResourceData, meta inter } }() - if val, ok := d.GetOk(groupMemberIdsKey); ok { + if val, ok := d.GetOk(GroupMemberIdsKey); ok { list := val.(*schema.Set).List() memberIds := make([]string, 0, len(list)) for _, i := range list { @@ -209,9 +205,9 @@ func resourceGroupUpdate(ctx context.Context, d *schema.ResourceData, meta inter // The above call may not actually happen, so we use d.Id() and automatic // versioning here - if d.HasChange(groupMemberIdsKey) { + if d.HasChange(GroupMemberIdsKey) { var memberIds []string - if membersVal, ok := d.GetOk(groupMemberIdsKey); ok { + if membersVal, ok := d.GetOk(GroupMemberIdsKey); ok { members := membersVal.(*schema.Set).List() for _, member := range members { memberIds = append(memberIds, member.(string)) @@ -222,7 +218,7 @@ func resourceGroupUpdate(ctx context.Context, d *schema.ResourceData, meta inter if err != nil { return diag.Errorf("error updating members in group: %v", err) } - if err := d.Set(groupMemberIdsKey, memberIds); err != nil { + if err := d.Set(GroupMemberIdsKey, memberIds); err != nil { return diag.FromErr(err) }