From 4372592f0cb58d223e2ea11cb78bac6bcd5e59c8 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 27 Feb 2017 05:36:37 -0500 Subject: [PATCH] provider/datadog: Add datadog_user resource (#12268) * provider/datadog: Add datadog_user resource * provider/datadog: Fix datadog_user import test, simplify datadog_user ACC tests, more consise documentation --- .../datadog/import_datadog_user_test.go | 35 ++++ builtin/providers/datadog/provider.go | 2 + .../datadog/resource_datadog_user.go | 160 ++++++++++++++++++ .../datadog/resource_datadog_user_test.go | 117 +++++++++++++ .../providers/datadog/r/user.html.markdown | 49 ++++++ website/source/layouts/datadog.erb | 3 + 6 files changed, 366 insertions(+) create mode 100644 builtin/providers/datadog/import_datadog_user_test.go create mode 100644 builtin/providers/datadog/resource_datadog_user.go create mode 100644 builtin/providers/datadog/resource_datadog_user_test.go create mode 100644 website/source/docs/providers/datadog/r/user.html.markdown diff --git a/builtin/providers/datadog/import_datadog_user_test.go b/builtin/providers/datadog/import_datadog_user_test.go new file mode 100644 index 0000000000..0adec86b4e --- /dev/null +++ b/builtin/providers/datadog/import_datadog_user_test.go @@ -0,0 +1,35 @@ +package datadog + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/resource" +) + +func TestDatadogUser_import(t *testing.T) { + resourceName := "datadog_user.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDatadogUserDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckDatadogUserConfigImported, + }, + resource.TestStep{ + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +const testAccCheckDatadogUserConfigImported = ` +resource "datadog_user" "foo" { + email = "test@example.com" + handle = "test@example.com" + name = "Test User" +} +` diff --git a/builtin/providers/datadog/provider.go b/builtin/providers/datadog/provider.go index ec7d91e518..60b4cef277 100644 --- a/builtin/providers/datadog/provider.go +++ b/builtin/providers/datadog/provider.go @@ -4,6 +4,7 @@ import ( "log" "errors" + "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" ) @@ -26,6 +27,7 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "datadog_monitor": resourceDatadogMonitor(), "datadog_timeboard": resourceDatadogTimeboard(), + "datadog_user": resourceDatadogUser(), }, ConfigureFunc: providerConfigure, diff --git a/builtin/providers/datadog/resource_datadog_user.go b/builtin/providers/datadog/resource_datadog_user.go new file mode 100644 index 0000000000..17c80ef3e1 --- /dev/null +++ b/builtin/providers/datadog/resource_datadog_user.go @@ -0,0 +1,160 @@ +package datadog + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "gopkg.in/zorkian/go-datadog-api.v2" +) + +func resourceDatadogUser() *schema.Resource { + return &schema.Resource{ + Create: resourceDatadogUserCreate, + Read: resourceDatadogUserRead, + Update: resourceDatadogUserUpdate, + Delete: resourceDatadogUserDelete, + Exists: resourceDatadogUserExists, + Importer: &schema.ResourceImporter{ + State: resourceDatadogUserImport, + }, + + Schema: map[string]*schema.Schema{ + "disabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "email": { + Type: schema.TypeString, + Required: true, + }, + "handle": { + Type: schema.TypeString, + Required: true, + }, + "is_admin": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "role": { + Type: schema.TypeString, + Optional: true, + }, + "verified": { + Type: schema.TypeBool, + Optional: true, + }, + }, + } +} + +func resourceDatadogUserExists(d *schema.ResourceData, meta interface{}) (b bool, e error) { + // Exists - This is called to verify a resource still exists. It is called prior to Read, + // and lowers the burden of Read to be able to assume the resource exists. + client := meta.(*datadog.Client) + + if _, err := client.GetUser(d.Id()); err != nil { + if strings.Contains(err.Error(), "404 Not Found") { + return false, nil + } + return false, err + } + + return true, nil +} + +func resourceDatadogUserCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*datadog.Client) + + var u datadog.User + u.SetDisabled(d.Get("disabled").(bool)) + u.SetEmail(d.Get("email").(string)) + u.SetHandle(d.Get("handle").(string)) + u.SetIsAdmin(d.Get("is_admin").(bool)) + u.SetName(d.Get("name").(string)) + u.SetRole(d.Get("role").(string)) + + // Datadog does not actually delete users, so CreateUser might return a 409. + // We ignore that case and proceed, likely re-enabling the user. + if _, err := client.CreateUser(u.Handle, u.Name); err != nil { + if !strings.Contains(err.Error(), "API error 409 Conflict") { + return fmt.Errorf("error creating user: %s", err.Error()) + } + log.Printf("[INFO] Updating existing Datadog user %q", u.Handle) + } + + if err := client.UpdateUser(u); err != nil { + return fmt.Errorf("error creating user: %s", err.Error()) + } + + d.SetId(u.GetHandle()) + + return nil +} + +func resourceDatadogUserRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*datadog.Client) + + u, err := client.GetUser(d.Id()) + if err != nil { + return err + } + + log.Printf("[DEBUG] user: %v", u) + d.Set("disabled", u.GetDisabled()) + d.Set("email", u.GetEmail()) + d.Set("handle", u.GetHandle()) + d.Set("is_admin", u.GetIsAdmin()) + d.Set("name", u.GetName()) + d.Set("role", u.GetRole()) + d.Set("verified", u.GetVerified()) + + return nil +} + +func resourceDatadogUserUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*datadog.Client) + var u datadog.User + u.SetDisabled(d.Get("disabled").(bool)) + u.SetEmail(d.Get("email").(string)) + u.SetHandle(d.Id()) + u.SetIsAdmin(d.Get("is_admin").(bool)) + u.SetName(d.Get("name").(string)) + u.SetRole(d.Get("role").(string)) + + if err := client.UpdateUser(u); err != nil { + return fmt.Errorf("error updating user: %s", err.Error()) + } + + return resourceDatadogUserRead(d, meta) +} + +func resourceDatadogUserDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*datadog.Client) + + // Datadog does not actually delete users, but instead marks them as disabled. + // Bypass DeleteUser if GetUser returns User.Disabled == true, otherwise it will 400. + if u, err := client.GetUser(d.Id()); err == nil && u.GetDisabled() { + return nil + } + + if err := client.DeleteUser(d.Id()); err != nil { + return err + } + + return nil +} + +func resourceDatadogUserImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + if err := resourceDatadogUserRead(d, meta); err != nil { + return nil, err + } + return []*schema.ResourceData{d}, nil +} diff --git a/builtin/providers/datadog/resource_datadog_user_test.go b/builtin/providers/datadog/resource_datadog_user_test.go new file mode 100644 index 0000000000..22f77ce094 --- /dev/null +++ b/builtin/providers/datadog/resource_datadog_user_test.go @@ -0,0 +1,117 @@ +package datadog + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "gopkg.in/zorkian/go-datadog-api.v2" +) + +func TestAccDatadogUser_Updated(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDatadogUserDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckDatadogUserConfigRequired, + Check: resource.ComposeTestCheckFunc( + testAccCheckDatadogUserExists("datadog_user.foo"), + resource.TestCheckResourceAttr( + "datadog_user.foo", "email", "test@example.com"), + resource.TestCheckResourceAttr( + "datadog_user.foo", "handle", "test@example.com"), + resource.TestCheckResourceAttr( + "datadog_user.foo", "name", "Test User"), + ), + }, + resource.TestStep{ + Config: testAccCheckDatadogUserConfigUpdated, + Check: resource.ComposeTestCheckFunc( + testAccCheckDatadogUserExists("datadog_user.foo"), + resource.TestCheckResourceAttr( + "datadog_user.foo", "disabled", "true"), + resource.TestCheckResourceAttr( + "datadog_user.foo", "email", "updated@example.com"), + resource.TestCheckResourceAttr( + "datadog_user.foo", "handle", "test@example.com"), + resource.TestCheckResourceAttr( + "datadog_user.foo", "is_admin", "true"), + resource.TestCheckResourceAttr( + "datadog_user.foo", "name", "Updated User"), + ), + }, + }, + }) +} + +func testAccCheckDatadogUserDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*datadog.Client) + + if err := datadogUserDestroyHelper(s, client); err != nil { + return err + } + return nil +} + +func testAccCheckDatadogUserExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*datadog.Client) + if err := datadogUserExistsHelper(s, client); err != nil { + return err + } + return nil + } +} + +const testAccCheckDatadogUserConfigRequired = ` +resource "datadog_user" "foo" { + email = "test@example.com" + handle = "test@example.com" + name = "Test User" +} +` + +const testAccCheckDatadogUserConfigUpdated = ` +resource "datadog_user" "foo" { + disabled = true + email = "updated@example.com" + handle = "test@example.com" + is_admin = true + name = "Updated User" +} +` + +func datadogUserDestroyHelper(s *terraform.State, client *datadog.Client) error { + for _, r := range s.RootModule().Resources { + id := r.Primary.ID + u, err := client.GetUser(id) + + if err != nil { + if strings.Contains(err.Error(), "404 Not Found") { + continue + } + return fmt.Errorf("Received an error retrieving user %s", err) + } + + // Datadog only disables user on DELETE + if u.GetDisabled() { + continue + } + return fmt.Errorf("User still exists") + } + return nil +} + +func datadogUserExistsHelper(s *terraform.State, client *datadog.Client) error { + for _, r := range s.RootModule().Resources { + id := r.Primary.ID + if _, err := client.GetUser(id); err != nil { + return fmt.Errorf("Received an error retrieving user %s", err) + } + } + return nil +} diff --git a/website/source/docs/providers/datadog/r/user.html.markdown b/website/source/docs/providers/datadog/r/user.html.markdown new file mode 100644 index 0000000000..25404dd32d --- /dev/null +++ b/website/source/docs/providers/datadog/r/user.html.markdown @@ -0,0 +1,49 @@ +--- +layout: "datadog" +page_title: "Datadog: datadog_user" +sidebar_current: "docs-datadog-resource-user" +description: |- + Provides a Datadog user resource. This can be used to create and manage users. +--- + +# datadog\_user + +Provides a Datadog user resource. This can be used to create and manage Datadog users. + +## Example Usage + +``` +# Create a new Datadog user +resource "datadog_user" "foo" { + email = "new@example.com" + handle = "new@example.com" + name = "New User" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `disabled` - (Optional) Whether the user is disabled +* `email` - (Required) Email address for user +* `handle` - (Required) The user handle, must be a valid email. +* `is_admin` - (Optional) Whether the user is an administrator +* `name` - (Required) Name for user +* `role` - (Optional) Role description for user (NOTE: can only be applied on user creation) + +## Attributes Reference + +The following attributes are exported: + +* `disabled` - Returns true if Datadog user is disabled (NOTE: Datadog does not actually delete users so this will be true for those as well) +* `id` - ID of the Datadog user +* `verified` - Returns true if Datadog user is verified + +## Import + +users can be imported using their handle, e.g. + +``` +$ terraform import datadog_user.example_user existing@example.com +``` diff --git a/website/source/layouts/datadog.erb b/website/source/layouts/datadog.erb index ac17834abb..7c68835428 100644 --- a/website/source/layouts/datadog.erb +++ b/website/source/layouts/datadog.erb @@ -19,6 +19,9 @@ > datadog_timeboard + > + datadog_user +