From dfec34e669b695522202063c2487df2ff6f703e6 Mon Sep 17 00:00:00 2001 From: Jeff Malnick Date: Thu, 27 Aug 2020 16:46:01 -0700 Subject: [PATCH] feat: add organization resource --- internal/provider/resource_organization.go | 167 ++++++++++++++++++ .../provider/resource_organization_test.go | 114 ++++++++++++ 2 files changed, 281 insertions(+) create mode 100644 internal/provider/resource_organization.go create mode 100644 internal/provider/resource_organization_test.go diff --git a/internal/provider/resource_organization.go b/internal/provider/resource_organization.go new file mode 100644 index 00000000..748eac4c --- /dev/null +++ b/internal/provider/resource_organization.go @@ -0,0 +1,167 @@ +package provider + +import ( + "fmt" + + "github.com/hashicorp/boundary/api/scopes" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +const ( + organizationDescriptionKey = "description" + organizationNameKey = "name" +) + +func resourceOrganization() *schema.Resource { + return &schema.Resource{ + Create: resourceOrganizationCreate, + Read: resourceOrganizationRead, + Update: resourceOrganizationUpdate, + Delete: resourceOrganizationDelete, + + Schema: map[string]*schema.Schema{ + organizationNameKey: { + Type: schema.TypeString, + Optional: true, + }, + organizationDescriptionKey: { + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +// convertOrganizationToResourceData populates the provided ResourceData with the appropriate values from the provided Project. +// The organization passed into thie function should be one read from the boundary API with all fields populated. +func convertOrganizationToResourceData(o *scopes.Scope, d *schema.ResourceData) error { + if o.Name != "" { + if err := d.Set(organizationNameKey, o.Name); err != nil { + return err + } + } + + if o.Description != "" { + if err := d.Set(organizationDescriptionKey, o.Description); err != nil { + return err + } + } + + d.SetId(o.Id) + return nil +} + +// convertResourceDataToOrganization returns a localy built Project using the values provided in the ResourceData. +func convertResourceDataToOrganization(d *schema.ResourceData) (*scopes.Scope, error) { + o := &scopes.Scope{Scope: &scopes.ScopeInfo{}} + + if descVal, ok := d.GetOk(organizationDescriptionKey); ok { + o.Description = descVal.(string) + } + + if nameVal, ok := d.GetOk(organizationNameKey); ok { + o.Name = nameVal.(string) + } + + if d.Id() != "" { + o.Id = d.Id() + } + + return o, nil +} + +func resourceOrganizationCreate(d *schema.ResourceData, meta interface{}) error { + md := meta.(*metaData) + client := md.client + ctx := md.ctx + + scp := scopes.NewScopesClient(client) + o, err := convertResourceDataToOrganization(d) + if err != nil { + return err + } + + o, _, err = scp.Create( + ctx, + client.ScopeId(), + scopes.WithName(o.Name), + scopes.WithDescription(o.Description)) + if err != nil { + return err + } + d.SetId(o.Id) + + return nil +} + +func resourceOrganizationRead(d *schema.ResourceData, meta interface{}) error { + md := meta.(*metaData) + client := md.client + ctx := md.ctx + + scp := scopes.NewScopesClient(client) + + o, _, err := scp.Read(ctx, d.Id()) + if err != nil { + return err + } + return convertOrganizationToResourceData(o, d) +} + +func resourceOrganizationUpdate(d *schema.ResourceData, meta interface{}) error { + md := meta.(*metaData) + client := md.client + ctx := md.ctx + + projClient := client.Clone() + projClient.SetScopeId(d.Id()) + scp := scopes.NewScopesClient(projClient) + o, err := convertResourceDataToOrganization(d) + if err != nil { + return err + } + + if d.HasChange(organizationDescriptionKey) { + desc := d.Get(organizationDescriptionKey).(string) + o.Description = desc + } + + if d.HasChange(organizationNameKey) { + name := d.Get(organizationNameKey).(string) + o.Name = name + } + + o, _, err = scp.Update( + ctx, + d.Id(), + 0, + scopes.WithAutomaticVersioning(), + scopes.WithDescription(o.Description), + scopes.WithName(o.Name), + ) + if err != nil { + return err + } + + return convertOrganizationToResourceData(o, d) +} + +func resourceOrganizationDelete(d *schema.ResourceData, meta interface{}) error { + md := meta.(*metaData) + client := md.client + ctx := md.ctx + + projClient := client.Clone() + projClient.SetScopeId(d.Id()) + scp := scopes.NewScopesClient(projClient) + o, err := convertResourceDataToOrganization(d) + if err != nil { + return err + } + + _, _, err = scp.Delete(ctx, o.Id) + if err != nil { + return fmt.Errorf("failed deleting organization: %w", err) + } + return nil +} diff --git a/internal/provider/resource_organization_test.go b/internal/provider/resource_organization_test.go new file mode 100644 index 00000000..fee53285 --- /dev/null +++ b/internal/provider/resource_organization_test.go @@ -0,0 +1,114 @@ +package provider + +import ( + "fmt" + "net/http" + "strings" + "testing" + + "github.com/hashicorp/boundary/api/scopes" + "github.com/hashicorp/boundary/testing/controller" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +const ( + orgFooName = "foo" + orgFooDescription = "foo description" + orgFooDescriptionUpdate = "foo bar description" +) + +var ( + fooOrg = ` +resource "boundary_organization" "foo" {}` + + orgFoo = fmt.Sprintf(` +resource "boundary_organization" "foo" { + name = "%s" + description = "%s" +}`, orgFooName, orgFooDescription) + + orgFooUpdate = fmt.Sprintf(` +resource "boundary_organization" "foo" { + name = "%s" + description = "%s" +}`, orgFooName, orgFooDescriptionUpdate) +) + +func TestAccOrganizationCreation(t *testing.T) { + tc := controller.NewTestController(t, tcConfig...) + defer tc.Shutdown() + url := tc.ApiAddrs()[0] + + resource.Test(t, resource.TestCase{ + Providers: testProviders, + CheckDestroy: testAccCheckOrganizationResourceDestroy(t), + Steps: []resource.TestStep{ + // test create + { + Config: testConfig(url, orgFoo), + Check: resource.ComposeTestCheckFunc( + testAccCheckOrganizationResourceExists("boundary_organization.foo"), + resource.TestCheckResourceAttr("boundary_organization.foo", organizationNameKey, orgFooName), + resource.TestCheckResourceAttr("boundary_organization.foo", organizationDescriptionKey, orgFooDescription), + ), + }, + // test update + { + Config: testConfig(url, orgFooUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckOrganizationResourceExists("boundary_organization.foo"), + resource.TestCheckResourceAttr("boundary_organization.foo", organizationNameKey, orgFooName), + resource.TestCheckResourceAttr("boundary_organization.foo", organizationDescriptionKey, orgFooDescriptionUpdate), + ), + }, + }, + }) +} + +func testAccCheckOrganizationResourceExists(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") + } + + if !strings.HasPrefix(id, "o_") { + return fmt.Errorf("ID not formatted as expected, expected prefix 'o_', got %s", id) + } + + md := testProvider.Meta().(*metaData) + scp := scopes.NewScopesClient(md.client) + + if _, _, err := scp.Read(md.ctx, id); err != nil { + return fmt.Errorf("Got an error when reading organization %q: %v", id, err) + } + + return nil + } +} + +func testAccCheckOrganizationResourceDestroy(t *testing.T) resource.TestCheckFunc { + return func(s *terraform.State) error { + // retrieve the connection established in Provider configuration + md := testProvider.Meta().(*metaData) + scp := scopes.NewScopesClient(md.client) + + for _, rs := range s.RootModule().Resources { + id := rs.Primary.ID + switch rs.Type { + case "boundary_organization": + if _, apiErr, _ := scp.Read(md.ctx, id); apiErr == nil || apiErr.Status != http.StatusNotFound && apiErr.Status != http.StatusForbidden { + return fmt.Errorf("Didn't get a 404 or 403 when reading destroyed organization %q: %v", id, apiErr) + } + default: + t.Logf("Got unknown resource type %q", rs.Type) + } + } + return nil + } +}