Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for enterprise and child organizations #77

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions logdna/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,17 @@ 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]
serviceKey, url, orgType := opts[0], opts[1], opts[2]

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

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

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{
jakedipity marked this conversation as resolved.
Show resolved Hide resolved
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]
jakedipity marked this conversation as resolved.
Show resolved Hide resolved

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
}
Comment on lines +64 to +72
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 buildSchemaMap(tfInfoList []TerraformInfo) map[string]*schema.Resource {
schemaMap := make(map[string]*schema.Resource)
for _, info := range tfInfoList {
schemaMap[info.name] = info.schema
}
return schemaMap
}

Mainly, the a doesn't impart any information to the reader. Renaming the m and e variables would also really help the next person to look at this code.


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
51 changes: 34 additions & 17 deletions logdna/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,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: io.ReadAll,
jsonMarshal: json.Marshal,
}

// Used during testing only; Allow mutations passed in by tests
Expand All @@ -64,7 +74,14 @@ func (c *requestConfig) MakeRequest() ([]byte, error) {
if payloadBuf.Len() > 0 {
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 @@ -42,7 +42,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 for request with a body", func(t *testing.T) {
Expand Down Expand Up @@ -95,6 +95,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 @@ -89,6 +89,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 @@ -210,6 +215,16 @@ func aggregateIndexRateAlertWebhookFromSchema(
return &allWebhookEntries
}

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