diff --git a/CHANGELOG.md b/CHANGELOG.md index 722be015..e092bd54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.0.4 (Unreleased) + +### New and Improved + +* Adds managed groups resource + ([PR](https://github.com/hashicorp/terraform-provider-boundary/pull/118)). + ## 1.0.3 (June 30, 2021) ### New and Improved diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 9b1cf27e..9e360a5f 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -62,6 +62,7 @@ func New() *schema.Provider { "boundary_auth_method_oidc": resourceAuthMethodOidc(), "boundary_credential_library_vault": resourceCredentialLibraryVault(), "boundary_credential_store_vault": resourceCredentialStoreVault(), + "boundary_managed_group": resourceManagedGroup(), "boundary_group": resourceGroup(), "boundary_host": resourceHost(), "boundary_host_catalog": resourceHostCatalog(), diff --git a/internal/provider/resource_managed_group.go b/internal/provider/resource_managed_group.go new file mode 100644 index 00000000..8452e1c7 --- /dev/null +++ b/internal/provider/resource_managed_group.go @@ -0,0 +1,227 @@ +package provider + +import ( + "context" + "net/http" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/managedgroups" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ( + managedGroupFilterKey = "filter" +) + +func resourceManagedGroup() *schema.Resource { + return &schema.Resource{ + Description: "The managed group resource allows you to configure a Boundary group.", + + CreateContext: resourceManagedGroupCreate, + ReadContext: resourceManagedGroupRead, + UpdateContext: resourceManagedGroupUpdate, + DeleteContext: resourceManagedGroupDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + IDKey: { + Description: "The ID of the group.", + Type: schema.TypeString, + Computed: true, + }, + NameKey: { + Description: "The managed group name. Defaults to the resource name.", + Type: schema.TypeString, + Optional: true, + }, + DescriptionKey: { + Description: "The managed group description.", + Type: schema.TypeString, + Optional: true, + }, + AuthMethodIdKey: { + Description: "The resource ID for the auth method.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + managedGroupFilterKey: { + Description: "Boolean expression to filter the workers for this managed group.", + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func setFromManagedGroupResponseMap(d *schema.ResourceData, raw map[string]interface{}) error { + if err := d.Set(NameKey, raw[NameKey]); err != nil { + return err + } + if err := d.Set(DescriptionKey, raw[DescriptionKey]); err != nil { + return err + } + if err := d.Set(AuthMethodIdKey, raw[AuthMethodIdKey]); err != nil { + return err + } + + if attrsVal, ok := raw["attributes"]; ok { + attrs := attrsVal.(map[string]interface{}) + if v, ok := attrs[managedGroupFilterKey]; ok { + if err := d.Set(managedGroupFilterKey, v); err != nil { + return err + } + } + } + + d.SetId(raw[IDKey].(string)) + + return nil +} + +func resourceManagedGroupCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + grpClient := managedgroups.NewClient(md.client) + + var authMethodId string + authMethodVal, ok := d.GetOk(AuthMethodIdKey) + if !ok { + return diag.Errorf("no auth method ID provided") + } + authMethodId = authMethodVal.(string) + + var opts []managedgroups.Option + nameVal, ok := d.GetOk(NameKey) + if ok { + nameStr := nameVal.(string) + opts = append(opts, managedgroups.WithName(nameStr)) + } + + descVal, ok := d.GetOk(DescriptionKey) + if ok { + descStr := descVal.(string) + opts = append(opts, managedgroups.WithDescription(descStr)) + } + + v, ok := d.GetOk(managedGroupFilterKey) + if ok { + str := v.(string) + opts = append(opts, managedgroups.WithOidcManagedGroupFilter(str)) + } + + grp, err := grpClient.Create(ctx, authMethodId, opts...) + if err != nil { + return diag.Errorf("error creating managed group: %v", err) + } + if grp == nil { + return diag.Errorf("managed group nil after create") + } + raw := grp.GetResponse().Map + + if err := setFromManagedGroupResponseMap(d, raw); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceManagedGroupRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + grpClient := managedgroups.NewClient(md.client) + + grp, err := grpClient.Read(ctx, d.Id()) + if err != nil { + if apiErr := api.AsServerError(err); apiErr.Response().StatusCode() == http.StatusNotFound { + d.SetId("") + return nil + } + return diag.Errorf("error reading managed group: %v", err) + } + if grp == nil { + return diag.Errorf("managed group nil after read") + } + + if err := setFromManagedGroupResponseMap(d, grp.GetResponse().Map); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceManagedGroupUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + grpClient := managedgroups.NewClient(md.client) + + var opts []managedgroups.Option + var name *string + if d.HasChange(NameKey) { + opts = append(opts, managedgroups.DefaultName()) + nameVal, ok := d.GetOk(NameKey) + if ok { + nameStr := nameVal.(string) + name = &nameStr + opts = append(opts, managedgroups.WithName(nameStr)) + } + } + + var desc *string + if d.HasChange(DescriptionKey) { + opts = append(opts, managedgroups.DefaultDescription()) + descVal, ok := d.GetOk(DescriptionKey) + if ok { + descStr := descVal.(string) + desc = &descStr + opts = append(opts, managedgroups.WithDescription(descStr)) + } + } + + var filter *string + if d.HasChange(managedGroupFilterKey) { + if f, ok := d.GetOk(managedGroupFilterKey); ok { + filterStr := f.(string) + filter = &filterStr + opts = append(opts, managedgroups.WithOidcManagedGroupFilter(filterStr)) + } + } + + if len(opts) > 0 { + opts = append(opts, managedgroups.WithAutomaticVersioning(true)) + _, err := grpClient.Update(ctx, d.Id(), 0, opts...) + if err != nil { + return diag.Errorf("error updating managed group: %v", err) + } + } + + if d.HasChange(NameKey) { + if err := d.Set(NameKey, name); err != nil { + return diag.FromErr(err) + } + } + if d.HasChange(DescriptionKey) { + if err := d.Set(DescriptionKey, desc); err != nil { + return diag.FromErr(err) + } + } + if d.HasChange(managedGroupFilterKey) { + if err := d.Set(managedGroupFilterKey, filter); err != nil { + return diag.FromErr(err) + } + } + + return nil +} + +func resourceManagedGroupDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + grpClient := managedgroups.NewClient(md.client) + + _, err := grpClient.Delete(ctx, d.Id()) + if err != nil { + return diag.Errorf("error calling delete managed group: %s", err.Error()) + } + + return nil +} diff --git a/internal/provider/resource_managed_group_test.go b/internal/provider/resource_managed_group_test.go new file mode 100644 index 00000000..5f0dc103 --- /dev/null +++ b/internal/provider/resource_managed_group_test.go @@ -0,0 +1,133 @@ +package provider + +import ( + "context" + "fmt" + "net/http" + "strings" + "testing" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/managedgroups" + "github.com/hashicorp/boundary/testing/controller" + "github.com/hashicorp/cap/oidc" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +const ( + managedGroupName = "test" + managedGroupDescription = "test managed group" + managedGroupUpdate = "_update" +) + +var ( + fooManagedGroup = fmt.Sprintf(` +resource "boundary_managed_group" "foo" { + name = "%s" + description = "%s" + auth_method_id = boundary_auth_method_oidc.foo.id + filter = "name == \"foo\"" +}`, managedGroupName, managedGroupDescription) + + fooManagedGroupUpdate = fmt.Sprintf(` +resource "boundary_managed_group" "foo" { + name = "%s" + description = "%s" + auth_method_id = boundary_auth_method_oidc.foo.id + filter = "name == \"bar\"" +}`, managedGroupName+managedGroupUpdate, managedGroupDescription+managedGroupUpdate) +) + +func TestAccManagedGroup(t *testing.T) { + wrapper := testWrapper(t, tcRecoveryKey) + tp := oidc.StartTestProvider(t) + tc := controller.NewTestController(t, append(tcConfig, controller.WithRecoveryKms(wrapper))...) + + tpCert := strings.TrimSpace(tp.CACert()) + createConfig := fmt.Sprintf(fooAuthMethodOidc, fooAuthMethodOidcDesc, tp.Addr(), tpCert) + + defer tc.Shutdown() + url := tc.ApiAddrs()[0] + + var provider *schema.Provider + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories(&provider), + CheckDestroy: testAccCheckManagedGroupResourceDestroy(t, provider), + Steps: []resource.TestStep{ + { + // test create + Config: testConfig(url, fooOrg, createConfig, fooManagedGroup), + Check: resource.ComposeTestCheckFunc( + testAccCheckManagedGroupResourceExists(provider, "boundary_managed_group.foo"), + resource.TestCheckResourceAttr("boundary_managed_group.foo", DescriptionKey, managedGroupDescription), + resource.TestCheckResourceAttr("boundary_managed_group.foo", NameKey, managedGroupName), + resource.TestCheckResourceAttr("boundary_managed_group.foo", managedGroupFilterKey, `name == "foo"`), + ), + }, + importStep("boundary_managed_group.foo"), + { + // test update + Config: testConfig(url, fooOrg, createConfig, fooManagedGroupUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckManagedGroupResourceExists(provider, "boundary_managed_group.foo"), + resource.TestCheckResourceAttr("boundary_managed_group.foo", DescriptionKey, managedGroupDescription+managedGroupUpdate), + resource.TestCheckResourceAttr("boundary_managed_group.foo", NameKey, managedGroupName+managedGroupUpdate), + resource.TestCheckResourceAttr("boundary_managed_group.foo", managedGroupFilterKey, `name == "bar"`), + ), + }, + }, + }) +} + +func testAccCheckManagedGroupResourceExists(testProvider *schema.Provider, name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + id := rs.Primary.ID + if id == "" { + return fmt.Errorf("No ID is set") + } + + md := testProvider.Meta().(*metaData) + grpClient := managedgroups.NewClient(md.client) + + _, err := grpClient.Read(context.Background(), id) + if err != nil { + return fmt.Errorf("Got an error when reading group %q: %v", id, err) + } + + return nil + } +} + +func testAccCheckManagedGroupResourceDestroy(t *testing.T, testProvider *schema.Provider) resource.TestCheckFunc { + return func(s *terraform.State) error { + if testProvider.Meta() == nil { + t.Fatal("got nil provider metadata") + } + + md := testProvider.Meta().(*metaData) + + for _, rs := range s.RootModule().Resources { + switch rs.Type { + case "boundary_managed_group": + grpClient := managedgroups.NewClient(md.client) + id := rs.Primary.ID + + _, err := grpClient.Read(context.Background(), id) + if apiErr := api.AsServerError(err); apiErr == nil || apiErr.Response().StatusCode() != http.StatusNotFound { + return fmt.Errorf("didn't get a 404 when reading destroyed resource %q: %v", id, apiErr) + } + + default: + continue + } + } + return nil + } +}