From 8441d121659718265982c791106f940a1c6eda1e Mon Sep 17 00:00:00 2001 From: Jacob Hull Date: Wed, 11 Jan 2023 11:15:18 -0800 Subject: [PATCH] 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 --- logdna/common_test.go | 14 +- logdna/data_source_alert.go | 7 + logdna/data_source_alert_test.go | 16 ++ logdna/meta.go | 89 ++++++++++ logdna/provider.go | 43 +++-- logdna/provider_test.go | 1 + logdna/request.go | 51 ++++-- logdna/request_test.go | 28 ++- logdna/request_types.go | 15 ++ logdna/resource_alert.go | 13 +- logdna/resource_alert_test.go | 13 ++ logdna/resource_archive.go | 11 ++ logdna/resource_category.go | 11 ++ logdna/resource_category_test.go | 18 ++ logdna/resource_child_organization.go | 193 +++++++++++++++++++++ logdna/resource_child_organization_test.go | 51 ++++++ logdna/resource_index_rate_alert.go | 95 +++++----- logdna/resource_index_rate_alert_test.go | 45 ++++- logdna/resource_ingestion_exclusion.go | 12 +- logdna/resource_key.go | 11 ++ logdna/resource_key_test.go | 18 ++ logdna/resource_member.go | 12 +- logdna/resource_member_test.go | 18 ++ logdna/resource_stream_config.go | 11 ++ logdna/resource_stream_exclusion.go | 13 +- logdna/resource_view.go | 11 ++ logdna/resource_view_test.go | 14 ++ logdna/response_types.go | 7 + 28 files changed, 745 insertions(+), 96 deletions(-) create mode 100644 logdna/meta.go create mode 100644 logdna/resource_child_organization.go create mode 100644 logdna/resource_child_organization_test.go diff --git a/logdna/common_test.go b/logdna/common_test.go index 3b91ac3..4b1cd8f 100644 --- a/logdna/common_test.go +++ b/logdna/common_test.go @@ -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) diff --git a/logdna/data_source_alert.go b/logdna/data_source_alert.go index c22a740..0ff7635 100644 --- a/logdna/data_source_alert.go +++ b/logdna/data_source_alert.go @@ -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 diff --git a/logdna/data_source_alert_test.go b/logdna/data_source_alert_test.go index d54cc61..13238d1 100644 --- a/logdna/data_source_alert_test.go +++ b/logdna/data_source_alert_test.go @@ -2,6 +2,7 @@ package logdna import ( "fmt" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -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"]), diff --git a/logdna/meta.go b/logdna/meta.go new file mode 100644 index 0000000..d9adc5f --- /dev/null +++ b/logdna/meta.go @@ -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) + } +} diff --git a/logdna/provider.go b/logdna/provider.go index 3d08946..0dd71cb 100644 --- a/logdna/provider.go +++ b/logdna/provider.go @@ -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 } @@ -18,8 +20,15 @@ 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, @@ -27,31 +36,29 @@ func Provider() *schema.Provider { 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 diff --git a/logdna/provider_test.go b/logdna/provider_test.go index a1a1300..c02c950 100644 --- a/logdna/provider_test.go +++ b/logdna/provider_test.go @@ -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 diff --git a/logdna/request.go b/logdna/request.go index 54288ee..a5d9e4e 100644 --- a/logdna/request.go +++ b/logdna/request.go @@ -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 @@ -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) diff --git a/logdna/request_test.go b/logdna/request_test.go index 142965c..54b99aa 100644 --- a/logdna/request_test.go +++ b/logdna/request_test.go @@ -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) { @@ -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"}) diff --git a/logdna/request_types.go b/logdna/request_types.go index 8d9de7e..754cb84 100644 --- a/logdna/request_types.go +++ b/logdna/request_types.go @@ -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 @@ -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, diff --git a/logdna/resource_alert.go b/logdna/resource_alert.go index 836ac12..3a495f5 100644 --- a/logdna/resource_alert.go +++ b/logdna/resource_alert.go @@ -11,12 +11,21 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) +/* + This resource needs to initialize before Terraform initializes so we can correctly populate the Provider schema. + We can't use the init() function because Terraform initializes before that. +*/ +var _ = registerTerraform(TerraformInfo{ + name: "logdna_alert", + orgType: OrgTypeRegular, + terraformType: TerraformTypeResource, + schema: resourceAlert(), +}) + func resourceAlertCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { var diags diag.Diagnostics pc := m.(*providerConfig) - alert := alertRequest{} - if diags = alert.CreateRequestBody(d); diags.HasError() { return diags } diff --git a/logdna/resource_alert_test.go b/logdna/resource_alert_test.go index eade13b..1649469 100644 --- a/logdna/resource_alert_test.go +++ b/logdna/resource_alert_test.go @@ -38,6 +38,19 @@ func TestAlert_ErrorResourceName(t *testing.T) { }) } +func TestAlert_ErrorOrgType(t *testing.T) { + pcArgs := []string{enterpriseServiceKey, apiHostUrl, "enterprise"} + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmtTestConfigResource("alert", "new", pcArgs, alertDefaults, nilOpt, nilLst), + ExpectError: regexp.MustCompile("Error: Only regular organizations can instantiate a \"logdna_alert\" resource"), + }, + }, + }) +} + func TestAlert_ErrorsChannel(t *testing.T) { imArgs := map[string]map[string]string{"email_channel": cloneDefaults(chnlDefaults["email_channel"])} imArgs["email_channel"]["immediate"] = `"not a bool"` diff --git a/logdna/resource_archive.go b/logdna/resource_archive.go index 2b22bf9..2640174 100644 --- a/logdna/resource_archive.go +++ b/logdna/resource_archive.go @@ -11,6 +11,17 @@ import ( const archiveConfigID = "archive" +/* + This resource needs to initialize before Terraform initializes so we can correctly populate the Provider schema. + We can't use the init() function because Terraform initializes before that. +*/ +var _ = registerTerraform(TerraformInfo{ + name: "logdna_archive", + orgType: OrgTypeRegular, + terraformType: TerraformTypeResource, + schema: resourceArchiveConfig(), +}) + type ibmConfig struct { Bucket string `json:"bucket"` Endpoint string `json:"endpoint"` diff --git a/logdna/resource_category.go b/logdna/resource_category.go index f3be24f..c90508b 100644 --- a/logdna/resource_category.go +++ b/logdna/resource_category.go @@ -11,6 +11,17 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) +/* + This resource needs to initialize before Terraform initializes so we can correctly populate the Provider schema. + We can't use the init() function because Terraform initializes before that. +*/ +var _ = registerTerraform(TerraformInfo{ + name: "logdna_category", + orgType: OrgTypeRegular, + terraformType: TerraformTypeResource, + schema: resourceCategory(), +}) + func resourceCategoryCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { var diags diag.Diagnostics pc := m.(*providerConfig) diff --git a/logdna/resource_category_test.go b/logdna/resource_category_test.go index 9b6d15c..0d822bc 100644 --- a/logdna/resource_category_test.go +++ b/logdna/resource_category_test.go @@ -26,6 +26,24 @@ func TestCategory_ErrorProviderUrl(t *testing.T) { }) } +func TestCategory_ErrorOrgType(t *testing.T) { + pcArgs := []string{enterpriseServiceKey, apiHostUrl, "enterprise"} + catArgs := map[string]string{ + "name": `"test-category"`, + "type": `"views"`, + } + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmtTestConfigResource("category", "new", pcArgs, catArgs, nilOpt, nilLst), + ExpectError: regexp.MustCompile("Error: Only regular organizations can instantiate a \"logdna_category\" resource"), + }, + }, + }) +} + func TestCategory_ErrorResourceName(t *testing.T) { catArgs := map[string]string{ "type": `"views"`, diff --git a/logdna/resource_child_organization.go b/logdna/resource_child_organization.go new file mode 100644 index 0000000..dc23887 --- /dev/null +++ b/logdna/resource_child_organization.go @@ -0,0 +1,193 @@ +package logdna + +import ( + "context" + "encoding/json" + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +/* + This resource needs to initialize before Terraform initializes so we can correctly populate the Provider schema. + We can't use the init() function because Terraform initializes before that. +*/ +var _ = registerTerraform(TerraformInfo{ + name: "logdna_child_organization", + orgType: OrgTypeEnterprise, + terraformType: TerraformTypeResource, + schema: resourceChildOrg(), +}) + +func resourceChildOrgCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + pc := m.(*providerConfig) + + req := newRequestConfig( + pc, + "POST", + "/v1/enterprise/account", + nil, + ) + req.serviceKey = d.Get("servicekey").(string) + + body, err := req.MakeRequest() + log.Printf("[DEBUG] %s %s, payload is: %s", req.method, req.apiURL, body) + + if err != nil { + return diag.FromErr(err) + } + + createdChildOrg := childOrgResponse{} + err = json.Unmarshal(body, &createdChildOrg) + if err != nil { + return diag.FromErr(err) + } + log.Printf("[DEBUG] After %s method, the created child org is %+v", req.method, createdChildOrg) + + d.SetId(createdChildOrg.Account) + return resourceChildOrgRead(ctx, d, m) +} + +func resourceChildOrgRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var diags diag.Diagnostics + pc := m.(*providerConfig) + childOrgID := d.Id() + + req := newRequestConfig( + pc, + "GET", + fmt.Sprintf("/v1/enterprise/account/%s", childOrgID), + nil, + ) + + body, err := req.MakeRequest() + + log.Printf("[DEBUG] GET child org raw response body %s\n", body) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Cannot read the remote child org resource", + Detail: err.Error(), + }) + return diags + } + + childOrg := childOrgResponse{} + err = json.Unmarshal(body, &childOrg) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Cannot unmarshal response from the remote childOrg resource", + Detail: err.Error(), + }) + return diags + } + log.Printf("[DEBUG] The GET child org structure is as follows: %+v\n", childOrg) + + // Top level keys can be set directly + appendError(d.Set("retention", childOrg.Retention), &diags) + appendError(d.Set("retention_tiers", childOrg.RetentionTiers), &diags) + appendError(d.Set("owner", childOrg.Owner), &diags) + + return diags +} + +func resourceChildOrgUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var diags diag.Diagnostics + pc := m.(*providerConfig) + childOrgID := d.Id() + + childOrg := childOrgPutRequest{} + if diags = childOrg.CreateRequestBody(d); diags.HasError() { + return diags + } + + req := newRequestConfig( + pc, + "PUT", + fmt.Sprintf("/v1/enterprise/account/%s", childOrgID), + childOrg, + ) + + 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 resourceChildOrgRead(ctx, d, m) +} + +func resourceChildOrgDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + pc := m.(*providerConfig) + childOrgID := d.Id() + + req := newRequestConfig( + pc, + "DELETE", + fmt.Sprintf("/v1/enterprise/account/%s", childOrgID), + nil, + ) + + body, err := req.MakeRequest() + log.Printf("[DEBUG] DELETE request body : %s", body) + + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + return nil +} + +func resourceChildOrg() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceChildOrgCreate, + ReadContext: resourceChildOrgRead, + UpdateContext: resourceChildOrgUpdate, + DeleteContext: resourceChildOrgDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + ForceNew: true, + Computed: true, + }, + "retention": { + Type: schema.TypeInt, + Optional: true, + }, + "retention_tiers": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + Computed: true, + }, + "servicekey": { + Type: schema.TypeString, + ForceNew: true, + Required: true, + Sensitive: true, + DiffSuppressFunc: func(_, _, _ string, _ *schema.ResourceData) bool { + return false + }, + }, + "owner": { + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + return new == "" + }, + }, + }, + } +} diff --git a/logdna/resource_child_organization_test.go b/logdna/resource_child_organization_test.go new file mode 100644 index 0000000..108085a --- /dev/null +++ b/logdna/resource_child_organization_test.go @@ -0,0 +1,51 @@ +package logdna + +import ( + "fmt" + "regexp" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestChildOrg_ErrorOrgType(t *testing.T) { + pcArgs := []string{serviceKey, apiHostUrl} + orgArgs := map[string]string{ + "servicekey": fmt.Sprintf(`"%s"`, serviceKey), + } + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmtTestConfigResource("child_organization", "new", pcArgs, orgArgs, nilOpt, nilLst), + ExpectError: regexp.MustCompile("Error: Only enterprise organizations can instantiate a \"logdna_child_organization\" resource"), + }, + }, + }) +} + +func TestChildOrg_Basic(t *testing.T) { + orgArgs := map[string]string{ + "servicekey": fmt.Sprintf(`"%s"`, serviceKey), + } + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + // NOTE: This tests detach childOrg operation + Config: fmtTestConfigResource("child_organization", "delete", []string{enterpriseServiceKey, apiHostUrl, "enterprise"}, orgArgs, nilOpt, nilLst), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("logdna_child_organization.delete", "servicekey", strings.Replace(orgArgs["servicekey"], "\"", "", 2)), + ), + }, + { + ResourceName: "logdna_child_organization.delete", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} diff --git a/logdna/resource_index_rate_alert.go b/logdna/resource_index_rate_alert.go index 585022f..663a7d1 100644 --- a/logdna/resource_index_rate_alert.go +++ b/logdna/resource_index_rate_alert.go @@ -13,6 +13,17 @@ import ( const indexRateAlertConfigID = "config" +/* + This resource needs to initialize before Terraform initializes so we can correctly populate the Provider schema. + We can't use the init() function because Terraform initializes before that. +*/ +var _ = registerTerraform(TerraformInfo{ + name: "logdna_index_rate_alert", + orgType: OrgTypeRegular, + terraformType: TerraformTypeResource, + schema: resourceIndexRateAlert(), +}) + /** * Create/Update index rate alert resource * As API does not allow the POST method, this method calls PUT to be used for both create and update. @@ -218,52 +229,52 @@ func resourceIndexRateAlert() *schema.Resource { }, }, }, - "webhook_channel":{ - Type: schema.TypeList, + "webhook_channel": { + Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "url": { - Type: schema.TypeString, - Required: true, - }, - "method": { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice([]string{"GET", "POST","PUT","DELETE"}, false), - }, - "headers": &schema.Schema{ - Type: schema.TypeMap, - Optional:true, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - Computed: true, - }, - "bodytemplate": { - Type: schema.TypeString, - Optional: true, - // This function compares JSON, ignoring whitespace that can occur in a .tf config. - // Without this, `terraform apply` will think values are different from remote to state. - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - var jsonOld, jsonNew interface{} - var err error - err = json.Unmarshal([]byte(old), &jsonOld) - if err != nil { - return false - } - err = json.Unmarshal([]byte(new), &jsonNew) - if err != nil { - return false - } - shouldSuppress := reflect.DeepEqual(jsonNew, jsonOld) - log.Println("[DEBUG] Does view 'bodytemplate' value in state appear the same as remote?", shouldSuppress) - return shouldSuppress - }, + Schema: map[string]*schema.Schema{ + "url": { + Type: schema.TypeString, + Required: true, + }, + "method": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"GET", "POST", "PUT", "DELETE"}, false), + }, + "headers": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + }, + "bodytemplate": { + Type: schema.TypeString, + Optional: true, + // This function compares JSON, ignoring whitespace that can occur in a .tf config. + // Without this, `terraform apply` will think values are different from remote to state. + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + var jsonOld, jsonNew interface{} + var err error + err = json.Unmarshal([]byte(old), &jsonOld) + if err != nil { + return false + } + err = json.Unmarshal([]byte(new), &jsonNew) + if err != nil { + return false + } + shouldSuppress := reflect.DeepEqual(jsonNew, jsonOld) + log.Println("[DEBUG] Does view 'bodytemplate' value in state appear the same as remote?", shouldSuppress) + return shouldSuppress + }, + }, }, - }, }, - }, + }, "enabled": { Type: schema.TypeBool, Required: true, diff --git a/logdna/resource_index_rate_alert_test.go b/logdna/resource_index_rate_alert_test.go index d15e2ad..5019a57 100644 --- a/logdna/resource_index_rate_alert_test.go +++ b/logdna/resource_index_rate_alert_test.go @@ -38,6 +38,35 @@ func TestIndexRateAlert_ErrorProviderUrl(t *testing.T) { }) } +func TestIndexRateAlert_ErrorOrgType(t *testing.T) { + pcArgs := []string{enterpriseServiceKey, apiHostUrl, "enterprise"} + iraArgs := map[string]string{ + "max_lines": `3`, + "max_z_score": `3`, + "threshold_alert": `"separate"`, + "frequency": `"hourly"`, + "enabled": `false`, + } + + chArgs := map[string]map[string]string{ + "channels": { + "email": `["test@logdna.com", "test2@logdna.com"]`, + "slack": `["https://hooks.slack.com/KEY"]`, + "pagerduty": `["ndt3k75rsw520d8t55dv35decdyt3mkcb3r"]`, + }, + } + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmtTestConfigResource("index_rate_alert", "new", pcArgs, iraArgs, chArgs, nilLst), + ExpectError: regexp.MustCompile("Error: Only regular organizations can instantiate a \"logdna_index_rate_alert\" resource"), + }, + }, + }) +} + func TestIndexRateAlert_ErrorResourceThresholdAlertInvalid(t *testing.T) { iraArgs := map[string]string{ "max_lines": `3`, @@ -216,12 +245,12 @@ func TestIndexRateAlert_Basic(t *testing.T) { "pagerduty": `["ndt3k75rsw520d8t55dv35decdyt3mkcb3r"]`, }, "webhook_channel": { - "url" : `"https://something.com"`, - "method": `"POST"`, - "headers" : `{ + "url": `"https://something.com"`, + "method": `"POST"`, + "headers": `{ field2 = "value2" }`, - "bodytemplate" :`jsonencode({ + "bodytemplate": `jsonencode({ something = "something" })`, }, @@ -242,15 +271,15 @@ func TestIndexRateAlert_Basic(t *testing.T) { "pagerduty": `["new3k75rsw520d8t55dv35decdyt3mkcnew"]`, }, "webhook_channel": { - "url" : `"https://something.com"`, + "url": `"https://something.com"`, "method": `"PUT"`, - "headers" : `{ + "headers": `{ field2 = "value2" }`, - "bodytemplate" :`jsonencode({ + "bodytemplate": `jsonencode({ something = "!something" })`, - }, + }, } createdEmails := strings.Split( diff --git a/logdna/resource_ingestion_exclusion.go b/logdna/resource_ingestion_exclusion.go index c9c2fe3..321b414 100644 --- a/logdna/resource_ingestion_exclusion.go +++ b/logdna/resource_ingestion_exclusion.go @@ -11,9 +11,19 @@ import ( const baseIngestionExclusionUrl = "/v1/config/ingestion/exclusions" +/* + This resource needs to initialize before Terraform initializes so we can correctly populate the Provider schema. + We can't use the init() function because Terraform initializes before that. +*/ +var _ = registerTerraform(TerraformInfo{ + name: "logdna_ingestion_exclusion", + orgType: OrgTypeRegular, + terraformType: TerraformTypeResource, + schema: resourceIngestionExclusion(), +}) + func resourceIngestionExclusionCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { var diags diag.Diagnostics - pc := m.(*providerConfig) ex := ingestionExclusionRule{ exclusionRule: exclusionRule{ diff --git a/logdna/resource_key.go b/logdna/resource_key.go index bbd5088..185b1fd 100644 --- a/logdna/resource_key.go +++ b/logdna/resource_key.go @@ -11,6 +11,17 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) +/* + This resource needs to initialize before Terraform initializes so we can correctly populate the Provider schema. + We can't use the init() function because Terraform initializes before that. +*/ +var _ = registerTerraform(TerraformInfo{ + name: "logdna_key", + orgType: OrgTypeRegular, + terraformType: TerraformTypeResource, + schema: resourceKey(), +}) + func resourceKeyCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { var diags diag.Diagnostics pc := m.(*providerConfig) diff --git a/logdna/resource_key_test.go b/logdna/resource_key_test.go index 8887a03..cbdd0ab 100644 --- a/logdna/resource_key_test.go +++ b/logdna/resource_key_test.go @@ -22,6 +22,24 @@ func TestKey_ErrorResourceTypeUndefined(t *testing.T) { }) } +func TestKey_ErrorOrgType(t *testing.T) { + pcArgs := []string{enterpriseServiceKey, apiHostUrl, "enterprise"} + keyArgs := map[string]string{ + "type": `"service"`, + "name": `"my first name"`, + } + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmtTestConfigResource("key", "new", pcArgs, keyArgs, nilOpt, nilLst), + ExpectError: regexp.MustCompile("Error: Only regular organizations can instantiate a \"logdna_key\" resource"), + }, + }, + }) +} + func TestKey_ErrorResourceTypeInvalid(t *testing.T) { args := map[string]string{ "type": `"incorrect"`, diff --git a/logdna/resource_member.go b/logdna/resource_member.go index 22967d0..075a89b 100644 --- a/logdna/resource_member.go +++ b/logdna/resource_member.go @@ -12,12 +12,22 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) +/* + This resource needs to initialize before Terraform initializes so we can correctly populate the Provider schema. + We can't use the init() function because Terraform initializes before that. +*/ +var _ = registerTerraform(TerraformInfo{ + name: "logdna_member", + orgType: OrgTypeRegular, + terraformType: TerraformTypeResource, + schema: resourceMember(), +}) + 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 } diff --git a/logdna/resource_member_test.go b/logdna/resource_member_test.go index 255d52f..b3ef113 100644 --- a/logdna/resource_member_test.go +++ b/logdna/resource_member_test.go @@ -24,6 +24,24 @@ func TestMember_ErrorRoleEmpty(t *testing.T) { }) } +func TestMember_ErrorOrgType(t *testing.T) { + pcArgs := []string{enterpriseServiceKey, apiHostUrl, "enterprise"} + memberArgs := map[string]string{ + "email": `"member@example.org"`, + "role": `"member"`, + } + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmtTestConfigResource("member", "new", pcArgs, memberArgs, nilOpt, nilLst), + ExpectError: regexp.MustCompile("Error: Only regular organizations can instantiate a \"logdna_member\" resource"), + }, + }, + }) +} + func TestMember_Basic(t *testing.T) { memberArgs := map[string]string{ "email": `"member@example.org"`, diff --git a/logdna/resource_stream_config.go b/logdna/resource_stream_config.go index 5af44d2..c35c295 100644 --- a/logdna/resource_stream_config.go +++ b/logdna/resource_stream_config.go @@ -10,6 +10,17 @@ import ( const streamConfigID = "stream" +/* + This resource needs to initialize before Terraform initializes so we can correctly populate the Provider schema. + We can't use the init() function because Terraform initializes before that. +*/ +var _ = registerTerraform(TerraformInfo{ + name: "logdna_stream_config", + orgType: OrgTypeRegular, + terraformType: TerraformTypeResource, + schema: resourceStreamConfig(), +}) + type streamConfig struct { Status string `json:"status,omitempty"` Brokers []string `json:"brokers"` diff --git a/logdna/resource_stream_exclusion.go b/logdna/resource_stream_exclusion.go index fdd72b6..af02497 100644 --- a/logdna/resource_stream_exclusion.go +++ b/logdna/resource_stream_exclusion.go @@ -9,10 +9,21 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) +/* + This resource needs to initialize before Terraform initializes so we can correctly populate the Provider schema. + We can't use the init() function because Terraform initializes before that. +*/ +var _ = registerTerraform(TerraformInfo{ + name: "logdna_stream_exclusion", + orgType: OrgTypeRegular, + terraformType: TerraformTypeResource, + schema: resourceStreamExclusion(), +}) + func resourceStreamExclusionCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { var diags diag.Diagnostics - pc := m.(*providerConfig) + ex := exclusionRule{ Title: d.Get("title").(string), Active: d.Get("active").(bool), diff --git a/logdna/resource_view.go b/logdna/resource_view.go index 2b997c2..8964828 100644 --- a/logdna/resource_view.go +++ b/logdna/resource_view.go @@ -20,6 +20,17 @@ const ( WEBHOOK = "webhook" ) +/* + This resource needs to initialize before Terraform initializes so we can correctly populate the Provider schema. + We can't use the init() function because Terraform initializes before that. +*/ +var _ = registerTerraform(TerraformInfo{ + name: "logdna_view", + orgType: OrgTypeRegular, + terraformType: TerraformTypeResource, + schema: resourceView(), +}) + func resourceViewCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { var diags diag.Diagnostics pc := m.(*providerConfig) diff --git a/logdna/resource_view_test.go b/logdna/resource_view_test.go index 0b6e73f..4e7de64 100644 --- a/logdna/resource_view_test.go +++ b/logdna/resource_view_test.go @@ -26,6 +26,20 @@ func TestView_ErrorProviderUrl(t *testing.T) { }) } +func TestView_ErrorOrgType(t *testing.T) { + pcArgs := []string{enterpriseServiceKey, apiHostUrl, "enterprise"} + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmtTestConfigResource("view", "new", pcArgs, viewDefaults, nilOpt, nilLst), + ExpectError: regexp.MustCompile("Error: Only regular organizations can instantiate a \"logdna_view\" resource"), + }, + }, + }) +} + func TestView_ErrorsResourceFields(t *testing.T) { nme := cloneDefaults(rsDefaults["view"]) nme["name"] = "" diff --git a/logdna/response_types.go b/logdna/response_types.go index af0019b..769a520 100644 --- a/logdna/response_types.go +++ b/logdna/response_types.go @@ -45,6 +45,13 @@ type memberResponse struct { Groups []string `json:"groups,omitempty"` } +type childOrgResponse struct { + Account string `json:"account"` + Retention int `json:"retention"` + RetentionTiers []int `json:"retentionTiers"` + Owner string `json:"owner"` +} + // 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)