Skip to content

Commit

Permalink
feat(member): implement new members resources
Browse files Browse the repository at this point in the history
Creates a new members resource to allow terraform to manage the members
of their organizations. This new resource allows configuring the roles
and groups as well!

Ref: LOG-12857
Signed-off-by: Jacob Hull <[email protected]>
Co-authored-by: Keerthika Damodaraswamy <[email protected]>
  • Loading branch information
jakedipity and Keerthika Damodaraswamy committed Sep 9, 2022
1 parent d5d6074 commit 224b3f2
Show file tree
Hide file tree
Showing 7 changed files with 333 additions and 9 deletions.
40 changes: 40 additions & 0 deletions docs/resources/logdna_member.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Resource: `logdna_member`

This resource allows you to manage the team members of an organization.

## Example

```hcl
provider "logdna" {
servicekey = "xxxxxxxxxxxxxxxxxxxxxxxx"
}
resource "logdna_member" "admin_user" {
email = "[email protected]"
role = "admin"
}
```

## Argument Reference

The following arguments are supported:

- `email`: **string** _(Required)_ The email of the user. If a user with that email does not exist, they will be invited to join Mezmo.
- `role`: **string** _(Required)_ The role of this user. Can be one of `admin`, `member`, and `readonly`. `owner` roles can only be changed through the UI.
- `groups`: **string[]** _(Optional)_ The id of the groups the user belongs to. Defaults to an empty list.

## Attributes Reference

In addition to all the arguments above, the following attributes are exported:

- `email`: **string** The email of the member.
- `role`: **string** The role of the member.
- `groups`: **string[]** The groups the member belongs to.

## Import

A member can be imported using their `email`, e.g.,

```sh
$ terraform import logdna_member.user1 <email>
```
1 change: 1 addition & 0 deletions logdna/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func Provider() *schema.Provider {
"logdna_archive": resourceArchiveConfig(),
"logdna_key": resourceKey(),
"logdna_index_rate_alert": resourceIndexRateAlert(),
"logdna_member": resourceMember(),
},
ConfigureFunc: providerConfigure,
}
Expand Down
4 changes: 2 additions & 2 deletions logdna/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ type requestConfig struct {
// newRequestConfig abstracts the struct creation to allow for mocking
func newRequestConfig(pc *providerConfig, method string, uri string, body interface{}, mutators ...func(*requestConfig)) *requestConfig {
rc := &requestConfig{
serviceKey: pc.serviceKey,
httpClient: pc.httpClient,
serviceKey: pc.serviceKey,
httpClient: pc.httpClient,
apiURL: fmt.Sprintf("%s%s", pc.baseURL, uri), // uri should have a preceding slash (/)
method: method,
body: body,
Expand Down
46 changes: 39 additions & 7 deletions logdna/request_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ package logdna

import (
"encoding/json"
"fmt"
"errors"
"fmt"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
Expand Down Expand Up @@ -70,6 +70,17 @@ type indexRateAlertRequest struct {
Enabled bool `json:"enabled,omitempty"`
}

type memberRequest struct {
Email string `json:"email,omitempty"`
Role string `json:"role,omitempty"`
Groups []string `json:"groups,omitempty"`
}

type memberPutRequest struct {
Role string `json:"role,omitempty"`
Groups []string `json:"groups"`
}

func (view *viewRequest) CreateRequestBody(d *schema.ResourceData) diag.Diagnostics {
// This function pulls from the schema in preparation to JSON marshal
var diags diag.Diagnostics
Expand Down Expand Up @@ -135,24 +146,45 @@ func (doc *indexRateAlertRequest) CreateRequestBody(d *schema.ResourceData) diag
)
}

doc.MaxLines = d.Get("max_lines").(int)
doc.MaxZScore = d.Get("max_z_score").(int)
doc.Enabled = d.Get("enabled").(bool)
doc.MaxLines = d.Get("max_lines").(int)
doc.MaxZScore = d.Get("max_z_score").(int)
doc.Enabled = d.Get("enabled").(bool)
doc.ThresholdAlert = d.Get("threshold_alert").(string)
doc.Frequency = d.Get("frequency").(string)
doc.Frequency = d.Get("frequency").(string)

var indexRateAlertChannel indexRateAlertChannelRequest
var channel = channels[0].(map[string]interface{})

indexRateAlertChannel.Email = listToStrings(channel["email"].([]interface{}))
indexRateAlertChannel.Email = listToStrings(channel["email"].([]interface{}))
indexRateAlertChannel.Pagerduty = listToStrings(channel["pagerduty"].([]interface{}))
indexRateAlertChannel.Slack = listToStrings(channel["slack"].([]interface{}))
indexRateAlertChannel.Slack = listToStrings(channel["slack"].([]interface{}))

doc.Channels = indexRateAlertChannel

return diags
}

func (member *memberRequest) CreateRequestBody(d *schema.ResourceData) diag.Diagnostics {
var diags diag.Diagnostics

// Scalars
member.Email = d.Get("email").(string)
member.Role = d.Get("role").(string)
member.Groups = listToStrings(d.Get("groups").([]interface{}))

return diags
}

func (member *memberPutRequest) CreateRequestBody(d *schema.ResourceData) diag.Diagnostics {
var diags diag.Diagnostics

// Scalars
member.Role = d.Get("role").(string)
member.Groups = listToStrings(d.Get("groups").([]interface{}))

return diags
}

func aggregateAllChannelsFromSchema(
d *schema.ResourceData,
diags *diag.Diagnostics,
Expand Down
177 changes: 177 additions & 0 deletions logdna/resource_member.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package logdna

import (
"context"
"encoding/json"
"fmt"
"log"
"strings"

"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 resourceMemberCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
var diags diag.Diagnostics
pc := m.(*providerConfig)

member := memberRequest{}

if diags = member.CreateRequestBody(d); diags.HasError() {
return diags
}

req := newRequestConfig(
pc,
"POST",
"/v1/config/members",
member,
)
body, err := req.MakeRequest()
log.Printf("[DEBUG] %s %s, payload is: %s", req.method, req.apiURL, body)

if err != nil {
return diag.FromErr(err)
}

createdMember := memberResponse{}
err = json.Unmarshal(body, &createdMember)
if err != nil {
return diag.FromErr(err)
}
log.Printf("[DEBUG] After %s member, the created member is %+v", req.method, createdMember)

d.SetId(createdMember.Email)

return resourceMemberRead(ctx, d, m)
}

func resourceMemberRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
var diags diag.Diagnostics

pc := m.(*providerConfig)
memberID := d.Id()

req := newRequestConfig(
pc,
"GET",
fmt.Sprintf("/v1/config/members/%s", memberID),
nil,
)

body, err := req.MakeRequest()

log.Printf("[DEBUG] GET member raw response body %s\n", body)
if err != nil {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: "Cannot read the remote member resource",
Detail: err.Error(),
})
return diags
}

member := memberResponse{}
err = json.Unmarshal(body, &member)
if err != nil {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: "Cannot unmarshal response from the remote member resource",
Detail: err.Error(),
})
return diags
}
log.Printf("[DEBUG] The GET member structure is as follows: %+v\n", member)

// Top level keys can be set directly
appendError(d.Set("email", member.Email), &diags)
appendError(d.Set("role", member.Role), &diags)
appendError(d.Set("groups", member.Groups), &diags)

return diags
}

func resourceMemberUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
var diags diag.Diagnostics
pc := m.(*providerConfig)
memberID := d.Id()

member := memberPutRequest{}
if diags = member.CreateRequestBody(d); diags.HasError() {
return diags
}

req := newRequestConfig(
pc,
"PUT",
fmt.Sprintf("/v1/config/members/%s", memberID),
member,
)

body, err := req.MakeRequest()
log.Printf("[DEBUG] %s %s, payload is: %s", req.method, req.apiURL, body)

if err != nil {
return diag.FromErr(err)
}

log.Printf("[DEBUG] %s %s SUCCESS. Remote resource updated.", req.method, req.apiURL)

return resourceMemberRead(ctx, d, m)
}

func resourceMemberDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
pc := m.(*providerConfig)
memberID := d.Id()

req := newRequestConfig(
pc,
"DELETE",
fmt.Sprintf("/v1/config/members/%s", memberID),
nil,
)

body, err := req.MakeRequest()
log.Printf("[DEBUG] %s %s key %s", req.method, req.apiURL, body)

if err != nil {
return diag.FromErr(err)
}

d.SetId("")
return nil
}

func resourceMember() *schema.Resource {
return &schema.Resource{
CreateContext: resourceMemberCreate,
UpdateContext: resourceMemberUpdate,
ReadContext: resourceMemberRead,
DeleteContext: resourceMemberDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},

Schema: map[string]*schema.Schema{
"email": {
Type: schema.TypeString,
ForceNew: true,
Required: true,
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
return strings.EqualFold(old, new)
},
},
"role": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{"owner", "admin", "member", "readonly"}, false),
},
"groups": {
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
},
},
}
}
68 changes: 68 additions & 0 deletions logdna/resource_member_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package logdna

import (
"regexp"
"strings"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func TestMember_ErrorRoleEmpty(t *testing.T) {
args := map[string]string{
"email": `"[email protected]"`,
}

resource.Test(t, resource.TestCase{
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: fmtTestConfigResource("member", "new", []string{serviceKey, apiHostUrl}, args, nilOpt, nilLst),
ExpectError: regexp.MustCompile("The argument \"role\" is required, but no definition was found."),
},
},
})
}

func TestMember_Basic(t *testing.T) {
memberArgs := map[string]string{
"email": `"[email protected]"`,
"role": `"member"`,
}

adminArgs := map[string]string{
"email": `"[email protected]"`,
"role": `"admin"`,
}
resource.Test(t, resource.TestCase{
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: fmtTestConfigResource("member", "member", []string{serviceKey, apiHostUrl}, memberArgs, nilOpt, nilLst),
Check: resource.ComposeTestCheckFunc(
testResourceExists("member", "member"),
resource.TestCheckResourceAttr("logdna_member.member", "email", strings.Replace(memberArgs["email"], "\"", "", 2)),
resource.TestCheckResourceAttr("logdna_member.member", "role", strings.Replace(memberArgs["role"], "\"", "", 2)),
),
},
{
ResourceName: "logdna_member.member",
ImportState: true,
ImportStateVerify: true,
},
{
Config: fmtTestConfigResource("member", "admin", []string{serviceKey, apiHostUrl}, adminArgs, nilOpt, nilLst),
Check: resource.ComposeTestCheckFunc(
testResourceExists("member", "admin"),
resource.TestCheckResourceAttr("logdna_member.admin", "email", strings.Replace(adminArgs["email"], "\"", "", 2)),
resource.TestCheckResourceAttr("logdna_member.admin", "role", strings.Replace(adminArgs["role"], "\"", "", 2)),
),
},
{
ResourceName: "logdna_member.admin",
ImportState: true,
ImportStateVerify: true,
},
},
})
}
6 changes: 6 additions & 0 deletions logdna/response_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ type keyResponse struct {
Created int `json:"created,omitempty"`
}

type memberResponse struct {
Email string `json:"email"`
Role string `json:"role"`
Groups []string `json:"groups,omitempty"`
}

// channelResponse contains channel data returned from the logdna APIs
// NOTE - Properties with `interface` are due to the APIs returning
// some things as strings (PUT/emails) and other times arrays (GET/emails)
Expand Down

0 comments on commit 224b3f2

Please sign in to comment.