Skip to content

Commit 70f58e7

Browse files
committed
feat: add support for enterprise and child organizations
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]>
1 parent d6e5602 commit 70f58e7

28 files changed

+1402
-796
lines changed

logdna/common_test.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import (
55
"regexp"
66
"strings"
77

8-
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
98
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
1010
)
1111

1212
const tmplPc = `provider "logdna" {
@@ -106,15 +106,19 @@ func fmtTestConfigResource(objTyp, rsName string, pcArgs []string, rsArgs map[st
106106
}
107107

108108
func fmtProviderBlock(args ...string) string {
109-
opts := []string{serviceKey, ""}
109+
opts := []string{serviceKey, "", "regular"}
110110
copy(opts, args)
111-
sk, ul := opts[0], opts[1]
111+
sk, ul, tp := opts[0], opts[1], opts[2]
112112

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

118+
if tp != "" {
119+
pcCfg = pcCfg + fmt.Sprintf("\n\ttype = %q", tp)
120+
}
121+
118122
return fmt.Sprintf(tmplPc, pcCfg)
119123
}
120124

logdna/data_source_alert.go

+7
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ var alertProps = map[string]*schema.Schema{
2626
"triggerlimit": intSchema,
2727
}
2828

29+
var _ = registerTerraform(TerraformInfo{
30+
name: "logdna_alert",
31+
orgType: OrgTypeRegular,
32+
terraformType: TerraformTypeDataSource,
33+
schema: dataSourceAlert(),
34+
})
35+
2936
func dataSourceAlertRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
3037
var diags diag.Diagnostics
3138

logdna/data_source_alert_test.go

+16
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package logdna
22

33
import (
44
"fmt"
5+
"regexp"
56
"testing"
67

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

18+
func TestDataAlert_ErrorOrgType(t *testing.T) {
19+
pcArgs := []string{enterpriseServiceKey, apiHostUrl, "enterprise"}
20+
alertConfig := fmtTestConfigResource("alert", "test", pcArgs, alertDefaults, nilOpt, nilLst)
21+
22+
resource.Test(t, resource.TestCase{
23+
Providers: testAccProviders,
24+
Steps: []resource.TestStep{
25+
{
26+
Config: fmt.Sprintf("%s\n%s", alertConfig, ds),
27+
ExpectError: regexp.MustCompile("Error: Only regular organizations can instantiate a \"logdna_alert\" resource"),
28+
},
29+
},
30+
})
31+
}
32+
1733
func TestDataAlert_BulkChannels(t *testing.T) {
1834
emArgs := map[string]map[string]string{
1935
"email_channel": cloneDefaults(chnlDefaults["email_channel"]),

logdna/meta.go

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package logdna
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9+
)
10+
11+
type OrgType string
12+
type TerraformType string
13+
type TerraformInfo struct {
14+
name string
15+
orgType OrgType
16+
terraformType TerraformType
17+
schema *schema.Resource
18+
}
19+
20+
const (
21+
OrgTypeRegular OrgType = "regular"
22+
OrgTypeEnterprise OrgType = "enterprise"
23+
)
24+
25+
const (
26+
TerraformTypeResource TerraformType = "resource"
27+
TerraformTypeDataSource TerraformType = "data source"
28+
)
29+
30+
var terraformRegistry []TerraformInfo
31+
32+
func registerTerraform(info TerraformInfo) *TerraformInfo {
33+
terraformRegistry = append(terraformRegistry, info)
34+
infoPt := &terraformRegistry[len(terraformRegistry)-1]
35+
36+
if infoPt.schema.CreateContext != nil {
37+
infoPt.schema.CreateContext = buildTerraformFunc(infoPt.schema.CreateContext, infoPt)
38+
}
39+
if infoPt.schema.ReadContext != nil {
40+
infoPt.schema.ReadContext = buildTerraformFunc(infoPt.schema.ReadContext, infoPt)
41+
}
42+
if infoPt.schema.UpdateContext != nil {
43+
infoPt.schema.UpdateContext = buildTerraformFunc(infoPt.schema.UpdateContext, infoPt)
44+
}
45+
if infoPt.schema.DeleteContext != nil {
46+
infoPt.schema.DeleteContext = buildTerraformFunc(infoPt.schema.DeleteContext, infoPt)
47+
}
48+
49+
return infoPt
50+
}
51+
52+
func filterRegistry(terraformType TerraformType) []TerraformInfo {
53+
newSlice := []TerraformInfo{}
54+
55+
for _, info := range terraformRegistry {
56+
if info.terraformType == terraformType {
57+
newSlice = append(newSlice, info)
58+
}
59+
}
60+
61+
return newSlice
62+
}
63+
64+
func buildSchemaMap(a []TerraformInfo) map[string]*schema.Resource {
65+
m := make(map[string]*schema.Resource)
66+
67+
for _, e := range a {
68+
m[e.name] = e.schema
69+
}
70+
71+
return m
72+
}
73+
74+
func buildTerraformFunc(contextFunc func(context.Context, *schema.ResourceData, interface{}) diag.Diagnostics, info *TerraformInfo) func(context.Context, *schema.ResourceData, interface{}) diag.Diagnostics {
75+
return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
76+
var diags diag.Diagnostics
77+
pc := m.(*providerConfig)
78+
79+
if pc.orgType != info.orgType {
80+
diags = append(diags, diag.Diagnostic{
81+
Severity: diag.Error,
82+
Summary: fmt.Sprintf("Only %s organizations can instantiate a \"%s\" %s", info.orgType, info.name, info.terraformType),
83+
})
84+
return diags
85+
}
86+
87+
return contextFunc(ctx, d, m)
88+
}
89+
}

logdna/provider.go

+25-18
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import (
55
"time"
66

77
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
89
)
910

1011
type providerConfig struct {
1112
serviceKey string
13+
orgType OrgType
1214
baseURL string
1315
httpClient *http.Client
1416
}
@@ -18,40 +20,45 @@ func Provider() *schema.Provider {
1820
return &schema.Provider{
1921
Schema: map[string]*schema.Schema{
2022
"servicekey": {
21-
Type: schema.TypeString,
22-
Required: true,
23+
Type: schema.TypeString,
24+
Sensitive: true,
25+
Optional: true,
26+
},
27+
"type": {
28+
Type: schema.TypeString,
29+
Optional: true,
30+
Default: "regular",
31+
ValidateFunc: validation.StringInSlice([]string{"regular", "enterprise"}, false),
2332
},
2433
"url": {
2534
Type: schema.TypeString,
2635
Optional: true,
2736
Default: "https://api.logdna.com",
2837
},
2938
},
30-
DataSourcesMap: map[string]*schema.Resource{
31-
"logdna_alert": dataSourceAlert(),
32-
},
33-
ResourcesMap: map[string]*schema.Resource{
34-
"logdna_alert": resourceAlert(),
35-
"logdna_view": resourceView(),
36-
"logdna_category": resourceCategory(),
37-
"logdna_stream_config": resourceStreamConfig(),
38-
"logdna_stream_exclusion": resourceStreamExclusion(),
39-
"logdna_ingestion_exclusion": resourceIngestionExclusion(),
40-
"logdna_archive": resourceArchiveConfig(),
41-
"logdna_key": resourceKey(),
42-
"logdna_index_rate_alert": resourceIndexRateAlert(),
43-
"logdna_member": resourceMember(),
44-
},
45-
ConfigureFunc: providerConfigure,
39+
DataSourcesMap: buildSchemaMap(filterRegistry(TerraformTypeDataSource)),
40+
ResourcesMap: buildSchemaMap(filterRegistry(TerraformTypeResource)),
41+
ConfigureFunc: providerConfigure,
4642
}
4743
}
4844

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

50+
orgType := OrgTypeRegular
51+
52+
switch orgTypeRaw {
53+
case "regular":
54+
orgType = OrgTypeRegular
55+
case "enterprise":
56+
orgType = OrgTypeEnterprise
57+
}
58+
5359
return &providerConfig{
5460
serviceKey: serviceKey,
61+
orgType: orgType,
5562
baseURL: url,
5663
httpClient: &http.Client{Timeout: 15 * time.Second},
5764
}, nil

logdna/provider_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import (
88
)
99

1010
var serviceKey = os.Getenv("SERVICE_KEY")
11+
var enterpriseServiceKey = os.Getenv("ENTERPRISE_SERVICE_KEY")
1112
var apiHostUrl = os.Getenv("API_URL")
12-
var globalPcArgs = []string {serviceKey, apiHostUrl}
13+
var globalPcArgs = []string{serviceKey, apiHostUrl}
1314
var testAccProviders map[string]*schema.Provider
1415
var testAccProvider *schema.Provider
1516

logdna/request.go

+35-17
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"io"
8+
"io/ioutil"
89
"net/http"
910
)
1011

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

1819
// Configuration for the HTTP client used to make requests to remote resources
1920
type requestConfig struct {
20-
serviceKey string
21-
httpClient httpClientInterface
22-
apiURL string
23-
method string
24-
body interface{}
25-
httpRequest httpRequest
26-
bodyReader bodyReader
27-
jsonMarshal jsonMarshal
21+
serviceKey string
22+
enterpriseKey string
23+
httpClient httpClientInterface
24+
apiURL string
25+
method string
26+
body interface{}
27+
httpRequest httpRequest
28+
bodyReader bodyReader
29+
jsonMarshal jsonMarshal
2830
}
2931

3032
// newRequestConfig abstracts the struct creation to allow for mocking
3133
func newRequestConfig(pc *providerConfig, method string, uri string, body interface{}, mutators ...func(*requestConfig)) *requestConfig {
34+
serviceKey := ""
35+
enterpriseKey := ""
36+
switch pc.orgType {
37+
case OrgTypeRegular:
38+
serviceKey = pc.serviceKey
39+
case OrgTypeEnterprise:
40+
enterpriseKey = pc.serviceKey
41+
}
3242
rc := &requestConfig{
33-
serviceKey: pc.serviceKey,
34-
httpClient: pc.httpClient,
35-
apiURL: fmt.Sprintf("%s%s", pc.baseURL, uri), // uri should have a preceding slash (/)
36-
method: method,
37-
body: body,
38-
httpRequest: http.NewRequest,
39-
bodyReader: io.ReadAll,
40-
jsonMarshal: json.Marshal,
43+
serviceKey: serviceKey,
44+
enterpriseKey: enterpriseKey,
45+
httpClient: pc.httpClient,
46+
apiURL: fmt.Sprintf("%s%s", pc.baseURL, uri), // uri should have a preceding slash (/)
47+
method: method,
48+
body: body,
49+
httpRequest: http.NewRequest,
50+
bodyReader: ioutil.ReadAll,
51+
jsonMarshal: json.Marshal,
4152
}
4253

4354
// Used during testing only; Allow mutations passed in by tests
@@ -62,7 +73,14 @@ func (c *requestConfig) MakeRequest() ([]byte, error) {
6273
return nil, err
6374
}
6475
req.Header.Set("Content-Type", "application/json")
65-
req.Header.Set("servicekey", c.serviceKey)
76+
77+
if c.serviceKey != "" {
78+
req.Header.Set("servicekey", c.serviceKey)
79+
}
80+
if c.enterpriseKey != "" {
81+
req.Header.Set("enterprise-servicekey", c.enterpriseKey)
82+
}
83+
6684
res, err := c.httpClient.Do(req)
6785
if err != nil {
6886
return nil, fmt.Errorf("error during HTTP request: %s", err)

logdna/request_test.go

+27-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func setJSONMarshal(customMarshaller jsonMarshal) func(*requestConfig) {
4040

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

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

71+
t.Run("Server receives proper method, URL, and headers for enterprise org", func(t *testing.T) {
72+
enterprisePC := providerConfig{serviceKey: "abc123", orgType: OrgTypeEnterprise, httpClient: &http.Client{Timeout: 15 * time.Second}}
73+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
74+
assert.Equal("GET", r.Method, "method is correct")
75+
assert.Equal(fmt.Sprintf("/someapi/%s", resourceID), r.URL.String(), "URL is correct")
76+
key, ok := r.Header["Enterprise-Servicekey"]
77+
assert.Equal(true, ok, "enterprise-servicekey header exists")
78+
assert.Equal(1, len(key), "enterprise-servicekey header is correct")
79+
key = r.Header["Content-Type"]
80+
assert.Equal("application/json", key[0], "content-type header is correct")
81+
}))
82+
defer ts.Close()
83+
84+
enterprisePC.baseURL = ts.URL
85+
86+
req := newRequestConfig(
87+
&enterprisePC,
88+
"GET",
89+
fmt.Sprintf("/someapi/%s", resourceID),
90+
nil,
91+
)
92+
93+
_, err := req.MakeRequest()
94+
assert.Nil(err, "No errors")
95+
})
96+
7197
t.Run("Reads and decodes response from the server", func(t *testing.T) {
7298
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
7399
err := json.NewEncoder(w).Encode(viewResponse{ViewID: "test123456"})

0 commit comments

Comments
 (0)