Skip to content

Commit

Permalink
feat: add support for enterprise and child organizations
Browse files Browse the repository at this point in the history
This begins support for terraform management of enterprise and child
organizations:
* Allows the provider to accept an enterprise key to indicate an
  enterprise organization.
* Enterprise organizations can attach and detach child organizations.

Ref: LOG-13116
Signed-off-by: Jacob Hull <[email protected]>
  • Loading branch information
jakedipity committed Jan 11, 2023
1 parent 7c95206 commit c4f2fa0
Show file tree
Hide file tree
Showing 28 changed files with 649 additions and 43 deletions.
8 changes: 6 additions & 2 deletions logdna/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,19 @@ func fmtTestConfigResource(objTyp, rsName string, pcArgs []string, rsArgs map[st
}

func fmtProviderBlock(args ...string) string {
opts := []string{serviceKey, ""}
opts := []string{serviceKey, "", "regular"}
copy(opts, args)
sk, ul := opts[0], opts[1]
sk, ul, tp := opts[0], opts[1], opts[2]

pcCfg := fmt.Sprintf(`servicekey = %q`, sk)
if ul != "" {
pcCfg = pcCfg + fmt.Sprintf("\n\turl = %q", ul)
}

if tp != "" {
pcCfg = pcCfg + fmt.Sprintf("\n\ttype = %q", tp)
}

return fmt.Sprintf(tmplPc, pcCfg)
}

Expand Down
7 changes: 7 additions & 0 deletions logdna/data_source_alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ var alertProps = map[string]*schema.Schema{
"triggerlimit": intSchema,
}

var _ = registerTerraform(TerraformInfo{
name: "logdna_alert",
orgType: OrgTypeRegular,
terraformType: TerraformTypeDataSource,
schema: dataSourceAlert(),
})

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

Expand Down
16 changes: 16 additions & 0 deletions logdna/data_source_alert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package logdna

import (
"fmt"
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
Expand All @@ -14,6 +15,21 @@ data "logdna_alert" "remote" {
}
`

func TestDataAlert_ErrorOrgType(t *testing.T) {
pcArgs := []string{enterpriseServiceKey, apiHostUrl, "enterprise"}
alertConfig := fmtTestConfigResource("alert", "test", pcArgs, alertDefaults, nilOpt, nilLst)

resource.Test(t, resource.TestCase{
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf("%s\n%s", alertConfig, ds),
ExpectError: regexp.MustCompile("Error: Only regular organizations can instantiate a \"logdna_alert\" resource"),
},
},
})
}

func TestDataAlert_BulkChannels(t *testing.T) {
emArgs := map[string]map[string]string{
"email_channel": cloneDefaults(chnlDefaults["email_channel"]),
Expand Down
89 changes: 89 additions & 0 deletions logdna/meta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package logdna

import (
"context"
"fmt"

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

type OrgType string
type TerraformType string
type TerraformInfo struct {
name string
orgType OrgType
terraformType TerraformType
schema *schema.Resource
}

const (
OrgTypeRegular OrgType = "regular"
OrgTypeEnterprise OrgType = "enterprise"
)

const (
TerraformTypeResource TerraformType = "resource"
TerraformTypeDataSource TerraformType = "data source"
)

var terraformRegistry []TerraformInfo

func registerTerraform(info TerraformInfo) *TerraformInfo {
terraformRegistry = append(terraformRegistry, info)
infoPt := &terraformRegistry[len(terraformRegistry)-1]

if infoPt.schema.CreateContext != nil {
infoPt.schema.CreateContext = buildTerraformFunc(infoPt.schema.CreateContext, infoPt)
}
if infoPt.schema.ReadContext != nil {
infoPt.schema.ReadContext = buildTerraformFunc(infoPt.schema.ReadContext, infoPt)
}
if infoPt.schema.UpdateContext != nil {
infoPt.schema.UpdateContext = buildTerraformFunc(infoPt.schema.UpdateContext, infoPt)
}
if infoPt.schema.DeleteContext != nil {
infoPt.schema.DeleteContext = buildTerraformFunc(infoPt.schema.DeleteContext, infoPt)
}

return infoPt
}

func filterRegistry(terraformType TerraformType) []TerraformInfo {
newSlice := []TerraformInfo{}

for _, info := range terraformRegistry {
if info.terraformType == terraformType {
newSlice = append(newSlice, info)
}
}

return newSlice
}

func buildSchemaMap(a []TerraformInfo) map[string]*schema.Resource {
m := make(map[string]*schema.Resource)

for _, e := range a {
m[e.name] = e.schema
}

return m
}

func buildTerraformFunc(contextFunc func(context.Context, *schema.ResourceData, interface{}) diag.Diagnostics, info *TerraformInfo) func(context.Context, *schema.ResourceData, interface{}) diag.Diagnostics {
return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
var diags diag.Diagnostics
pc := m.(*providerConfig)

if pc.orgType != info.orgType {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: fmt.Sprintf("Only %s organizations can instantiate a \"%s\" %s", info.orgType, info.name, info.terraformType),
})
return diags
}

return contextFunc(ctx, d, m)
}
}
43 changes: 25 additions & 18 deletions logdna/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import (
"time"

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

type providerConfig struct {
serviceKey string
orgType OrgType
baseURL string
httpClient *http.Client
}
Expand All @@ -18,40 +20,45 @@ func Provider() *schema.Provider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
"servicekey": {
Type: schema.TypeString,
Required: true,
Type: schema.TypeString,
Sensitive: true,
Optional: true,
},
"type": {
Type: schema.TypeString,
Optional: true,
Default: "regular",
ValidateFunc: validation.StringInSlice([]string{"regular", "enterprise"}, false),
},
"url": {
Type: schema.TypeString,
Optional: true,
Default: "https://api.logdna.com",
},
},
DataSourcesMap: map[string]*schema.Resource{
"logdna_alert": dataSourceAlert(),
},
ResourcesMap: map[string]*schema.Resource{
"logdna_alert": resourceAlert(),
"logdna_view": resourceView(),
"logdna_category": resourceCategory(),
"logdna_stream_config": resourceStreamConfig(),
"logdna_stream_exclusion": resourceStreamExclusion(),
"logdna_ingestion_exclusion": resourceIngestionExclusion(),
"logdna_archive": resourceArchiveConfig(),
"logdna_key": resourceKey(),
"logdna_index_rate_alert": resourceIndexRateAlert(),
"logdna_member": resourceMember(),
},
ConfigureFunc: providerConfigure,
DataSourcesMap: buildSchemaMap(filterRegistry(TerraformTypeDataSource)),
ResourcesMap: buildSchemaMap(filterRegistry(TerraformTypeResource)),
ConfigureFunc: providerConfigure,
}
}

func providerConfigure(d *schema.ResourceData) (interface{}, error) {
serviceKey := d.Get("servicekey").(string)
orgTypeRaw := d.Get("type").(string)
url := d.Get("url").(string)

orgType := OrgTypeRegular

switch orgTypeRaw {
case "regular":
orgType = OrgTypeRegular
case "enterprise":
orgType = OrgTypeEnterprise
}

return &providerConfig{
serviceKey: serviceKey,
orgType: orgType,
baseURL: url,
httpClient: &http.Client{Timeout: 15 * time.Second},
}, nil
Expand Down
1 change: 1 addition & 0 deletions logdna/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
)

var serviceKey = os.Getenv("SERVICE_KEY")
var enterpriseServiceKey = os.Getenv("ENTERPRISE_SERVICE_KEY")
var apiHostUrl = os.Getenv("API_URL")
var globalPcArgs = []string{serviceKey, apiHostUrl}
var testAccProviders map[string]*schema.Provider
Expand Down
52 changes: 35 additions & 17 deletions logdna/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
)

Expand All @@ -17,27 +18,37 @@ type httpClientInterface interface {

// Configuration for the HTTP client used to make requests to remote resources
type requestConfig struct {
serviceKey string
httpClient httpClientInterface
apiURL string
method string
body interface{}
httpRequest httpRequest
bodyReader bodyReader
jsonMarshal jsonMarshal
serviceKey string
enterpriseKey string
httpClient httpClientInterface
apiURL string
method string
body interface{}
httpRequest httpRequest
bodyReader bodyReader
jsonMarshal jsonMarshal
}

// newRequestConfig abstracts the struct creation to allow for mocking
func newRequestConfig(pc *providerConfig, method string, uri string, body interface{}, mutators ...func(*requestConfig)) *requestConfig {
serviceKey := ""
enterpriseKey := ""
switch pc.orgType {
case OrgTypeRegular:
serviceKey = pc.serviceKey
case OrgTypeEnterprise:
enterpriseKey = pc.serviceKey
}
rc := &requestConfig{
serviceKey: pc.serviceKey,
httpClient: pc.httpClient,
apiURL: fmt.Sprintf("%s%s", pc.baseURL, uri), // uri should have a preceding slash (/)
method: method,
body: body,
httpRequest: http.NewRequest,
bodyReader: io.ReadAll,
jsonMarshal: json.Marshal,
serviceKey: serviceKey,
enterpriseKey: enterpriseKey,
httpClient: pc.httpClient,
apiURL: fmt.Sprintf("%s%s", pc.baseURL, uri), // uri should have a preceding slash (/)
method: method,
body: body,
httpRequest: http.NewRequest,
bodyReader: ioutil.ReadAll,
jsonMarshal: json.Marshal,
}

// Used during testing only; Allow mutations passed in by tests
Expand All @@ -64,7 +75,14 @@ func (c *requestConfig) MakeRequest() ([]byte, error) {
if payloadBuf != nil {
req.Header.Set("Content-Type", "application/json")
}
req.Header.Set("servicekey", c.serviceKey)

if c.serviceKey != "" {
req.Header.Set("servicekey", c.serviceKey)
}
if c.enterpriseKey != "" {
req.Header.Set("enterprise-servicekey", c.enterpriseKey)
}

res, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error during HTTP request: %s", err)
Expand Down
28 changes: 27 additions & 1 deletion logdna/request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func setJSONMarshal(customMarshaller jsonMarshal) func(*requestConfig) {

func TestRequest_MakeRequest(t *testing.T) {
assert := assert.New(t)
pc := providerConfig{serviceKey: "abc123", httpClient: &http.Client{Timeout: 15 * time.Second}}
pc := providerConfig{serviceKey: "abc123", orgType: OrgTypeRegular, httpClient: &http.Client{Timeout: 15 * time.Second}}
resourceID := "test123456"

t.Run("Server receives proper method, URL, and headers", func(t *testing.T) {
Expand Down Expand Up @@ -68,6 +68,32 @@ func TestRequest_MakeRequest(t *testing.T) {
assert.Nil(err, "No errors")
})

t.Run("Server receives proper method, URL, and headers for enterprise org", func(t *testing.T) {
enterprisePC := providerConfig{serviceKey: "abc123", orgType: OrgTypeEnterprise, httpClient: &http.Client{Timeout: 15 * time.Second}}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal("GET", r.Method, "method is correct")
assert.Equal(fmt.Sprintf("/someapi/%s", resourceID), r.URL.String(), "URL is correct")
key, ok := r.Header["Enterprise-Servicekey"]
assert.Equal(true, ok, "enterprise-servicekey header exists")
assert.Equal(1, len(key), "enterprise-servicekey header is correct")
key = r.Header["Content-Type"]
assert.Equal("application/json", key[0], "content-type header is correct")
}))
defer ts.Close()

enterprisePC.baseURL = ts.URL

req := newRequestConfig(
&enterprisePC,
"GET",
fmt.Sprintf("/someapi/%s", resourceID),
nil,
)

_, err := req.MakeRequest()
assert.Nil(err, "No errors")
})

t.Run("Reads and decodes response from the server", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := json.NewEncoder(w).Encode(viewResponse{ViewID: "test123456"})
Expand Down
15 changes: 15 additions & 0 deletions logdna/request_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ type memberPutRequest struct {
Groups []string `json:"groups"`
}

type childOrgPutRequest struct {
Retention int `json:"retention"`
Owner string `json:"owner"`
}

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 @@ -185,6 +190,16 @@ func (member *memberPutRequest) CreateRequestBody(d *schema.ResourceData) diag.D
return diags
}

func (childOrg *childOrgPutRequest) CreateRequestBody(d *schema.ResourceData) diag.Diagnostics {
var diags diag.Diagnostics

// Scalars
childOrg.Retention = d.Get("retention").(int)
childOrg.Owner = d.Get("owner").(string)

return diags
}

func aggregateAllChannelsFromSchema(
d *schema.ResourceData,
diags *diag.Diagnostics,
Expand Down
Loading

0 comments on commit c4f2fa0

Please sign in to comment.