From 539770033eebcc5473e85b130338d83332fe79c2 Mon Sep 17 00:00:00 2001 From: Yuting Liu Date: Mon, 6 Jan 2025 15:09:42 -0800 Subject: [PATCH 1/7] SUMO-245309 Add the terraform support for Azure metric sources --- ...rce_sumologic_azure_metrics_source_test.go | 194 ++++++++++++++++++ ...source_sumologic_generic_polling_source.go | 139 +++++++++++-- .../resource_sumologic_polling_source.go | 40 +++- sumologic/sumologic_polling_source.go | 16 +- 4 files changed, 365 insertions(+), 24 deletions(-) create mode 100644 sumologic/resource_sumologic_azure_metrics_source_test.go diff --git a/sumologic/resource_sumologic_azure_metrics_source_test.go b/sumologic/resource_sumologic_azure_metrics_source_test.go new file mode 100644 index 00000000..447d0eef --- /dev/null +++ b/sumologic/resource_sumologic_azure_metrics_source_test.go @@ -0,0 +1,194 @@ +package sumologic + +import ( + "fmt" + "os" + "strconv" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func TestAccSumologicAzureMetricsSource_create(t *testing.T) { + var azureMetricsSource PollingSource + var collector Collector + cName, cDescription, cCategory := getRandomizedParams() + sName, sDescription, sCategory := getRandomizedParams() + azureMetricsResourceName := "sumologic_azure_metrics_source.azure" + testTenantId := os.GetEnv("SUMOLOGIC_TEST_AZURE_TENANT_ID") + testClientId := os.GetEnv("SUMOLOGIC_TEST_AZURE_CLIENT_ID") + testClientSecret := os.GETENV("SUMOLOGIC_TEST_AZURE_CLIENT_SECRET") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: TestAccSumologicAzureMetricsSourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSumologicAzureMetricsSourceConfig(cName, cDescription, cCategory, sName, sDescription, sCategory, testTenantId, testClientId, testClientSecret), + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureMetricsSourceExists(azureMetricsResourceName, &azureMetricsSource), + testAccCheckAzureMetricsSourceDestroy(&azureMetricsSource, sName, sDescription, sCategory), + resource.TestCheckResourceAttrSet(azureMetricsResourceName, "id"), + resource.TestCheckResourceAttr(azureMetricsResourceName, "name", sName), + resource.TestCheckResourceAttr(azureMetricsResourceName, "description", sDescription), + resource.TestCheckResourceAttr(azureMetricsResourceName, "category", sCategory), + resource.TestCheckResourceAttr(azureMetricsResourceName, "content_type", "AzureMetrics"), + resource.TestCheckResourceAttr(azureMetricsResourceName, "path.0.type", "AzureMetricsPath"), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccSumologicAzureMetricsSource_update(t *testing.T) { + + var azureMetricsSource PollingSource + var collector Collector + cName, cDescription, cCategory := getRandomizedParams() + sName, sDescription, sCategory := getRandomizedParams() + sNameUpdated, sDescriptionUpdated, sCategoryUpdated := getRandomizedParams() + azureMetricsResourceName := "sumologic_azure_metrics_source.azure" + testTenantId := os.GetEnv("SUMOLOGIC_TEST_AZURE_TENANT_ID") + testClientId := os.GetEnv("SUMOLOGIC_TEST_AZURE_CLIENT_ID") + testClientSecret := os.GETENV("SUMOLOGIC_TEST_AZURE_CLIENT_SECRET") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: TestAccSumologicAzureMetricsSourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSumologicAzureMetricsSourceConfig(cName, cDescription, cCategory, sName, sDescription, sCategory, testSASKeyName, testSASKey, testNamespace, testEventHub, testConsumerGroup, testRegion), + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureMetricsSourceExists(azureMetricsResourceName, &azureMetricsSource), + testAccCheckAzureMetricsSourceDestroy(&azureMetricsSource, sName, sDescription, sCategory), + resource.TestCheckResourceAttrSet(azureMetricsResourceName, "id"), + resource.TestCheckResourceAttr(azureMetricsResourceName, "name", sName), + resource.TestCheckResourceAttr(azureMetricsResourceName, "description", sDescription), + resource.TestCheckResourceAttr(azureMetricsResourceName, "category", sCategory), + resource.TestCheckResourceAttr(azureMetricsResourceName, "content_type", "AzureMetrics"), + resource.TestCheckResourceAttr(azureMetricsResourceName, "path.0.type", "AzureMetricsPath"), + ), + ExpectNonEmptyPlan: true, + }, + { + Config: testAccSumologicAzureMetricsSourceConfig(cName, cDescription, cCategory, sNameUpdated, sDescriptionUpdated, sCategoryUpdated, testTenantId, testClientId, testClientSecret), + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureMetricsSourceExists(azureMetricsResourceName, &azureMetricsSource), + testAccCheckAzureMetricsSourceDestroy(&azureMetricsSource, sNameUpdated, sDescriptionUpdated, sCategoryUpdated), + resource.TestCheckResourceAttrSet(azureMetricsResourceName, "id"), + resource.TestCheckResourceAttr(azureMetricsResourceName, "name", sNameUpdated), + resource.TestCheckResourceAttr(azureMetricsResourceName, "description", sDescriptionUpdated), + resource.TestCheckResourceAttr(azureMetricsResourceName, "category", sCategoryUpdated), + resource.TestCheckResourceAttr(azureMetricsResourceName, "content_type", "AzureMetrics"), + resource.TestCheckResourceAttr(azureMetricsResourceName, "path.0.type", "AzureMetricsPath"), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) + +} + +func testAccCheckAzureMetricsSourceDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*Client) + for _, rs := range s.RootModule().Resources { + if rs.Type != "sumologic_azure_event_hub_log_source" { + continue + } + if rs.Primary.ID == "" { + return fmt.Errorf("Azure Event Hub Log Source destruction check: Azure Event Hub Log Source ID is not set") + } + id, err := strconv.Atoi(rs.Primary.ID) + if err != nil { + return fmt.Errorf("Encountered an error: " + err.Error()) + } + collectorID, err := strconv.Atoi(rs.Primary.Attributes["collector_id"]) + if err != nil { + return fmt.Errorf("Encountered an error: " + err.Error()) + } + s, err := client.GetPollingSource(collectorID, id) + if err != nil { + return fmt.Errorf("Encountered an error: " + err.Error()) + } + if s != nil { + return fmt.Errorf("Polling Source still exists") + } + } + return nil +} + +func testAccCheckAzureMetricsSourceExists(n string, pollingSource *PollingSource) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + if rs.Primary.ID == "" { + return fmt.Errorf("Polling Source ID is not set") + } + id, err := strconv.Atoi(rs.Primary.ID) + if err != nil { + return fmt.Errorf("Polling Source id should be int; got %s", rs.Primary.ID) + } + collectorID, err := strconv.Atoi(rs.Primary.Attributes["collector_id"]) + if err != nil { + return fmt.Errorf("Encountered an error: " + err.Error()) + } + c := testAccProvider.Meta().(*Client) + pollingSourceResp, err := c.GetPollingSource(collectorID, id) + if err != nil { + return err + } + *pollingSource = *pollingSourceResp + return nil + } +} + +func testAccSumologicAzureMetricsSourceConfig(cName, cDescription, cCategory, sName, sDescription, sCategory, testTenantId, testClientId, testClientSecret) { + return fmt.Sprintf(` +resource "sumologic_collector" "test" { + name = "%s" + description = "%s" + category = "%s" +} +resource "sumologic_azure_event_metrics_source" "azure-metrics" { + name = "%s" + description = "%s" + category = "%s" + content_type = "AzureMetrics" + collector_id = "${sumologic_collector.test.id}" + + authentication { + type = "AzureClientSecretAuthentication" + tenant_id = "%s" + client_id = "%s" + client_secret = "%s" + } + + path { + type = "AzureMetricsPath" + environment = "Azure" + limit_to_namespaces = ["Microsoft.ClassicStorage/storageAccounts"] + tag_filters { + type = "AzureTagFilters" + namespace = "Microsoft.ClassicStorage/storageAccounts" + tags { + name = "test-name-1" + values = ["value1"] + } + tags { + name = "test-name-2" + values = ["value2"] + } + } + } + + lifecycle { + ignore_changes = [authentication.0.client_secret] + } +}`, cName, cDescription, cCategory, sName, sDescription, sCategory, testTenantId, testClientId, testClientSecret) +} diff --git a/sumologic/resource_sumologic_generic_polling_source.go b/sumologic/resource_sumologic_generic_polling_source.go index ed6aa9c5..f8b332d8 100644 --- a/sumologic/resource_sumologic_generic_polling_source.go +++ b/sumologic/resource_sumologic_generic_polling_source.go @@ -56,7 +56,7 @@ func resourceSumologicGenericPollingSource() *schema.Resource { "type": { Type: schema.TypeString, Required: true, - ValidateFunc: validation.StringInSlice([]string{"S3BucketAuthentication", "AWSRoleBasedAuthentication", "service_account", "AzureEventHubAuthentication"}, false), + ValidateFunc: validation.StringInSlice([]string{"S3BucketAuthentication", "AWSRoleBasedAuthentication", "service_account", "AzureEventHubAuthentication", "AzureClientSecretAuthentication"}, false), }, "access_key": { Type: schema.TypeString, @@ -118,6 +118,14 @@ func resourceSumologicGenericPollingSource() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "tenant_id": { + Type: schema.TypeString, + Optional: true, + }, + "client_secret": { + Type: schema.TypeString, + Optional: true, + }, }, }, } @@ -133,7 +141,7 @@ func resourceSumologicGenericPollingSource() *schema.Resource { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringInSlice([]string{"S3BucketPathExpression", "CloudWatchPath", - "AwsInventoryPath", "AwsXRayPath", "GcpMetricsPath", "AzureEventHubPath"}, false), + "AwsInventoryPath", "AwsXRayPath", "GcpMetricsPath", "AzureEventHubPath", "AzureMetricsPath"}, false), }, "bucket_name": { Type: schema.TypeString, @@ -243,10 +251,13 @@ func resourceSumologicGenericPollingSource() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "environment": { + Type: schema.TypeString, + Optional: true, + }, }, }, } - return pollingSource } @@ -450,13 +461,25 @@ func getCustomServices(path map[string]interface{}) []string { return customServices } -func flattenPollingTagFilters(v []TagFilter) []map[string]interface{} { +func flattenPollingTagFilters(v []interface{}) []map[string]interface{} { var filters []map[string]interface{} for _, d := range v { - filter := map[string]interface{}{ - "type": d.Type, - "namespace": d.Namespace, - "tags": d.Tags, + filter := make(map[string]interface{}) + switch t := d.(type) { + case TagFilter: + filter = map[string]interface{}{ + "type": t.Type, + "namespace": t.Namespace, + "Tags": t.Tags, + } + case AzureTagFilter: + filter = map[string]interface{}{ + "type": t.Type, + "namespace": t.Namespace, + "Tags": flattenAzureTagKeyValuePair(t.Tags), + } + default: + continue } filters = append(filters, filter) } @@ -464,27 +487,85 @@ func flattenPollingTagFilters(v []TagFilter) []map[string]interface{} { return filters } -func getPollingTagFilters(d *schema.ResourceData) []TagFilter { +func flattenAzureTagKeyValuePair(v []AzureTagKeyValuePair) []map[string]interface{} { + var tags []map[string]interface{} + for _, d := range v { + tag := map[string]interface{}{ + "name": d.Name, + "values": d.Values, + } + tags = append(tags, tag) + } + return tags +} + +func getTagFilter(config map[string]interface{}) TagFilter { + filter := TagFilter{} + filter.Type = "TagFilters" + filter.Namespace = config["namespace"].(string) + rawTags := config["tags"].([]interface{}) + Tags := make([]string, len(rawTags)) + for i, v := range rawTags { + Tags[i] = v.(string) + } + filter.Tags = Tags + return filter +} + +func getAzureTagFilter(config map[string]interface{}) AzureTagFilter { + filter := AzureTagFilter{} + filter.Type = "AzureTagFilters" + filter.namespace = config["namespace"].(string) + RawTags := config["tags"].([]interface{}) + Tags := make([]AzureTagKeyValuePair, len(rawTags)) + for i, tagPair := range rawTags { + pair := AzureTagKeyValuePair{} + pair.Name = tagPair["name"] + // get the values from the azure key value pair + rawValues = tagPair["values"].([]interface{}) + valueList := make([]string, len(rawValues)) + for j, v := range rawValues { + valueList[j] = v.(string) + } + pair.Values = valueList + Tags[i] = pair + } + return filter +} + + +func getPollingTagFilters(d *schema.ResourceData) []interface{} { paths := d.Get("path").([]interface{}) path := paths[0].(map[string]interface{}) rawTagFilterConfig := path["tag_filters"].([]interface{}) - var filters []TagFilter + var filters []interface{} for _, rawConfig := range rawTagFilterConfig { config := rawConfig.(map[string]interface{}) - filter := TagFilter{} - filter.Type = config["type"].(string) - filter.Namespace = config["namespace"].(string) - - rawTags := config["tags"].([]interface{}) - Tags := make([]string, len(rawTags)) - for i, v := range rawTags { - Tags[i] = v.(string) + filterType := config["type"].(string) + filterNamespace := config["namespace"].(string) + + switch filterType := config["type"].(string); filterType { + case "TagFilters": + filter := getPollingTagFilters(config) + filters = append(filters, filter) + case "AzureTagFilters": + filter := getAzureTagFilter(config) + filters = append(filter, filter) + // type is optional + default: + if len(tags) > 0 { + switch tags[0].(type) { + case map[string]interface{}: + filter := getAzureTagFilter(config) + filters = append(filter, filter) + case string: + filter := getPollingTagFilters(config) + filters = append(filters, filter) + } + } } - filter.Tags = Tags - filters = append(filters, filter) } - return filters } @@ -577,7 +658,10 @@ func getPollingAuthentication(d *schema.ResourceData) (PollingAuthentication, er authSettings.Type = "AzureEventHubAuthentication" authSettings.SharedAccessPolicyName = auth["shared_access_policy_name"].(string) authSettings.SharedAccessPolicyKey = auth["shared_access_policy_key"].(string) - + case "AzureClientSecretAuthentication": + authSettings.TenantId = auth["tenant_id"].(string) + authSettings.ClientId = auth["client_id"].(string) + authSettings.ClientSecret = auth["client_secret"].(string) default: errorMessage := fmt.Sprintf("[ERROR] Unknown authType: %v", authType) log.Print(errorMessage) @@ -671,6 +755,17 @@ func getPollingPathSettings(d *schema.ResourceData) (PollingPath, error) { if path["region"] != nil { pathSettings.Region = path["region"].(string) } + case "AzureMetricsPath": + pathSettings.Type = "AzureMetricsPath" + pathSettings.Environment = path["environment"].(string) + rawLimitToNamespaces := path["limit_to_namespaces"].([]interface{}) + LimitToNamespaces := make([]string, 0, len(rawLimitToNamespaces)) + for _, v := range rawLimitToNamespaces { + if v != nil { + LimitToNamespaces = append(LimitToNamespaces, v.(string)) + } + } + pathSettings.TagFilters = getPollingTagFilters(d) default: errorMessage := fmt.Sprintf("[ERROR] Unknown resourceType in path: %v", pathType) log.Print(errorMessage) diff --git a/sumologic/resource_sumologic_polling_source.go b/sumologic/resource_sumologic_polling_source.go index 06dc7eba..8641b8aa 100644 --- a/sumologic/resource_sumologic_polling_source.go +++ b/sumologic/resource_sumologic_polling_source.go @@ -119,8 +119,9 @@ func resourceSumologicPollingSource() *schema.Resource { Type: schema.TypeList, Optional: true, Elem: &schema.Schema{ - Type: schema.TypeString, + Type: schema.TypeMap, // Accept both maps (for objects) and strings }, + ValidateFunc: validateTags, }, }, }, @@ -132,6 +133,43 @@ func resourceSumologicPollingSource() *schema.Resource { return pollingSource } +func validateTags(val interface{}, key string) ([]string, []error) { + v := val.(map[string]interface{}) + var errs []error + + if tags, ok := v.([]interface{}); ok { + for i, tag := range tags { + switch t := tag.(type) { + case map[string]interface{}: + // Validate object structure + // Validate "name" to be a string + if _, ok := t["name"]; !ok { + errors = append(errors, fmt.Errorf("%s[%d]: missing required field 'name'", key, i)) + } else if _, ok := t["name"].(string); !ok { + errors = append(errors, fmt.Errorf("%s[%d]: 'name' must be a string", key, i)) + } + + // Validate "values" to be a list of strings + if _, ok := t["values"]; !ok { + errors = append(errors, fmt.Errorf("%s[%d]: missing required field 'values'", key, i)) + } else if values, ok := t["values"].([]interface{}); !ok { + errors = append(errors, fmt.Errorf("%s[%d]: 'values' must be a list of strings", key, i)) + } else { + for j, value := range values { + if _, ok := value.(string); !ok { + errors = append(errors, fmt.Errorf("%s[%d].values[%d]: must be a string", key, i, j)) + } + } + } + case string: + continue + default: + errors = append(errors, fmt.Errorf("%s[%d]: must be either a string or an object with 'name' and 'values'", key, i)) + } + } + } +} + func resourceSumologicPollingSourceCreate(d *schema.ResourceData, meta interface{}) error { c := meta.(*Client) diff --git a/sumologic/sumologic_polling_source.go b/sumologic/sumologic_polling_source.go index cf367d4e..9d583a7e 100644 --- a/sumologic/sumologic_polling_source.go +++ b/sumologic/sumologic_polling_source.go @@ -41,6 +41,8 @@ type PollingAuthentication struct { ClientX509CertUrl string `json:"client_x509_cert_url"` SharedAccessPolicyName string `json:"sharedAccessPolicyName"` SharedAccessPolicyKey string `json:"sharedAccessPolicyKey"` + TenantId string `json:"tenantId"` + ClientSecret string `json:"clientSecret"` } type PollingPath struct { @@ -51,13 +53,14 @@ type PollingPath struct { LimitToNamespaces []string `json:"limitToNamespaces,omitempty"` LimitToServices []string `json:"limitToServices,omitempty"` CustomServices []string `json:"customServices,omitempty"` - TagFilters []TagFilter `json:"tagFilters,omitempty"` + TagFilters []interface{} `json:"tagFilters,omitempty"` SnsTopicOrSubscriptionArn PollingSnsTopicOrSubscriptionArn `json:"snsTopicOrSubscriptionArn,omitempty"` UseVersionedApi *bool `json:"useVersionedApi,omitempty"` Namespace string `json:"namespace,omitempty"` EventHubName string `json:"eventHubName,omitempty"` ConsumerGroup string `json:"consumerGroup,omitempty"` Region string `json:"region,omitempty"` + Environment string `json:"environment,omitempty"` } type TagFilter struct { @@ -66,6 +69,17 @@ type TagFilter struct { Tags []string `json:"tags"` } +type AzureTagFilter struct { + Type string `json:"type"` + Namespace string `json:"namespace"` + Tags []AzureTagKeyValuePair `json:"tags"` +} + +type AzureTagKeyValuePair struct { + Name string `json:"name"` + Values []string `json:"values"` +} + type PollingSnsTopicOrSubscriptionArn struct { IsSuccess bool `json:"isSuccess"` Arn string `json:"arn"` From 6c1b4a010771dcf2c3f5cc59165ca0a26e73ec77 Mon Sep 17 00:00:00 2001 From: Yuting Liu Date: Tue, 7 Jan 2025 16:32:46 -0800 Subject: [PATCH 2/7] SUMO-245309 Fix formatting --- sumologic/resource_sumologic_generic_polling_source.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sumologic/resource_sumologic_generic_polling_source.go b/sumologic/resource_sumologic_generic_polling_source.go index f8b332d8..71696feb 100644 --- a/sumologic/resource_sumologic_generic_polling_source.go +++ b/sumologic/resource_sumologic_generic_polling_source.go @@ -521,7 +521,7 @@ func getAzureTagFilter(config map[string]interface{}) AzureTagFilter { for i, tagPair := range rawTags { pair := AzureTagKeyValuePair{} pair.Name = tagPair["name"] - // get the values from the azure key value pair + // get the values from the azure key value pair rawValues = tagPair["values"].([]interface{}) valueList := make([]string, len(rawValues)) for j, v := range rawValues { @@ -533,7 +533,6 @@ func getAzureTagFilter(config map[string]interface{}) AzureTagFilter { return filter } - func getPollingTagFilters(d *schema.ResourceData) []interface{} { paths := d.Get("path").([]interface{}) path := paths[0].(map[string]interface{}) @@ -562,7 +561,7 @@ func getPollingTagFilters(d *schema.ResourceData) []interface{} { case string: filter := getPollingTagFilters(config) filters = append(filters, filter) - } + } } } } From 5d6282bb37dfa35d8c35b21677835c25c29364e5 Mon Sep 17 00:00:00 2001 From: Yuting Liu Date: Wed, 8 Jan 2025 10:43:25 -0800 Subject: [PATCH 3/7] SUMO-245309 Fix the compile error --- ...rce_sumologic_azure_metrics_source_test.go | 45 +++++++---- ...source_sumologic_generic_polling_source.go | 40 +++++----- .../resource_sumologic_polling_source.go | 75 +++++++++++++------ 3 files changed, 103 insertions(+), 57 deletions(-) diff --git a/sumologic/resource_sumologic_azure_metrics_source_test.go b/sumologic/resource_sumologic_azure_metrics_source_test.go index 447d0eef..e42d8b16 100644 --- a/sumologic/resource_sumologic_azure_metrics_source_test.go +++ b/sumologic/resource_sumologic_azure_metrics_source_test.go @@ -16,20 +16,22 @@ func TestAccSumologicAzureMetricsSource_create(t *testing.T) { cName, cDescription, cCategory := getRandomizedParams() sName, sDescription, sCategory := getRandomizedParams() azureMetricsResourceName := "sumologic_azure_metrics_source.azure" - testTenantId := os.GetEnv("SUMOLOGIC_TEST_AZURE_TENANT_ID") - testClientId := os.GetEnv("SUMOLOGIC_TEST_AZURE_CLIENT_ID") - testClientSecret := os.GETENV("SUMOLOGIC_TEST_AZURE_CLIENT_SECRET") + testTenantId := os.Getenv("SUMOLOGIC_TEST_AZURE_TENANT_ID") + testClientId := os.Getenv("SUMOLOGIC_TEST_AZURE_CLIENT_ID") + testClientSecret := os.Getenv("SUMOLOGIC_TEST_AZURE_CLIENT_SECRET") resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, - CheckDestroy: TestAccSumologicAzureMetricsSourceDestroy, + CheckDestroy: testAccCheckAzureMetricsSourceDestroy, Steps: []resource.TestStep{ { Config: testAccSumologicAzureMetricsSourceConfig(cName, cDescription, cCategory, sName, sDescription, sCategory, testTenantId, testClientId, testClientSecret), Check: resource.ComposeTestCheckFunc( testAccCheckAzureMetricsSourceExists(azureMetricsResourceName, &azureMetricsSource), - testAccCheckAzureMetricsSourceDestroy(&azureMetricsSource, sName, sDescription, sCategory), + testAccCheckAzureMetricsSourceValues(&azureMetricsSource, sName, sDescription, sCategory), + testAccCheckCollectorExists("sumologic_collector.test", &collector), + testAccCheckCollectorValues(&collector, cName, cDescription, cCategory, "Etc/UTC", ""), resource.TestCheckResourceAttrSet(azureMetricsResourceName, "id"), resource.TestCheckResourceAttr(azureMetricsResourceName, "name", sName), resource.TestCheckResourceAttr(azureMetricsResourceName, "description", sDescription), @@ -44,27 +46,25 @@ func TestAccSumologicAzureMetricsSource_create(t *testing.T) { } func TestAccSumologicAzureMetricsSource_update(t *testing.T) { - var azureMetricsSource PollingSource - var collector Collector cName, cDescription, cCategory := getRandomizedParams() sName, sDescription, sCategory := getRandomizedParams() sNameUpdated, sDescriptionUpdated, sCategoryUpdated := getRandomizedParams() azureMetricsResourceName := "sumologic_azure_metrics_source.azure" - testTenantId := os.GetEnv("SUMOLOGIC_TEST_AZURE_TENANT_ID") - testClientId := os.GetEnv("SUMOLOGIC_TEST_AZURE_CLIENT_ID") - testClientSecret := os.GETENV("SUMOLOGIC_TEST_AZURE_CLIENT_SECRET") + testTenantId := os.Getenv("SUMOLOGIC_TEST_AZURE_TENANT_ID") + testClientId := os.Getenv("SUMOLOGIC_TEST_AZURE_CLIENT_ID") + testClientSecret := os.Getenv("SUMOLOGIC_TEST_AZURE_CLIENT_SECRET") resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, - CheckDestroy: TestAccSumologicAzureMetricsSourceDestroy, + CheckDestroy: testAccCheckAzureMetricsSourceDestroy, Steps: []resource.TestStep{ { - Config: testAccSumologicAzureMetricsSourceConfig(cName, cDescription, cCategory, sName, sDescription, sCategory, testSASKeyName, testSASKey, testNamespace, testEventHub, testConsumerGroup, testRegion), + Config: testAccSumologicAzureMetricsSourceConfig(cName, cDescription, cCategory, sName, sDescription, sCategory, testTenantId, testClientId, testClientSecret), Check: resource.ComposeTestCheckFunc( testAccCheckAzureMetricsSourceExists(azureMetricsResourceName, &azureMetricsSource), - testAccCheckAzureMetricsSourceDestroy(&azureMetricsSource, sName, sDescription, sCategory), + testAccCheckAzureMetricsSourceValues(&azureMetricsSource, sName, sDescription, sCategory), resource.TestCheckResourceAttrSet(azureMetricsResourceName, "id"), resource.TestCheckResourceAttr(azureMetricsResourceName, "name", sName), resource.TestCheckResourceAttr(azureMetricsResourceName, "description", sDescription), @@ -78,7 +78,7 @@ func TestAccSumologicAzureMetricsSource_update(t *testing.T) { Config: testAccSumologicAzureMetricsSourceConfig(cName, cDescription, cCategory, sNameUpdated, sDescriptionUpdated, sCategoryUpdated, testTenantId, testClientId, testClientSecret), Check: resource.ComposeTestCheckFunc( testAccCheckAzureMetricsSourceExists(azureMetricsResourceName, &azureMetricsSource), - testAccCheckAzureMetricsSourceDestroy(&azureMetricsSource, sNameUpdated, sDescriptionUpdated, sCategoryUpdated), + testAccCheckAzureMetricsSourceValues(&azureMetricsSource, sNameUpdated, sDescriptionUpdated, sCategoryUpdated), resource.TestCheckResourceAttrSet(azureMetricsResourceName, "id"), resource.TestCheckResourceAttr(azureMetricsResourceName, "name", sNameUpdated), resource.TestCheckResourceAttr(azureMetricsResourceName, "description", sDescriptionUpdated), @@ -148,7 +148,22 @@ func testAccCheckAzureMetricsSourceExists(n string, pollingSource *PollingSource } } -func testAccSumologicAzureMetricsSourceConfig(cName, cDescription, cCategory, sName, sDescription, sCategory, testTenantId, testClientId, testClientSecret) { +func testAccCheckAzureMetricsSourceValues(pollingSource *PollingSource, name, description, category string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if pollingSource.Name != name { + return fmt.Errorf("bad name, expected \"%s\", got: %#v", name, pollingSource.Name) + } + if pollingSource.Description != description { + return fmt.Errorf("bad description, expected \"%s\", got: %#v", description, pollingSource.Description) + } + if pollingSource.Category != category { + return fmt.Errorf("bad category, expected \"%s\", got: %#v", category, pollingSource.Category) + } + return nil + } +} + +func testAccSumologicAzureMetricsSourceConfig(cName, cDescription, cCategory, sName, sDescription, sCategory, testTenantId, testClientId, testClientSecret string) string { return fmt.Sprintf(` resource "sumologic_collector" "test" { name = "%s" diff --git a/sumologic/resource_sumologic_generic_polling_source.go b/sumologic/resource_sumologic_generic_polling_source.go index 71696feb..5f6088e5 100644 --- a/sumologic/resource_sumologic_generic_polling_source.go +++ b/sumologic/resource_sumologic_generic_polling_source.go @@ -499,11 +499,10 @@ func flattenAzureTagKeyValuePair(v []AzureTagKeyValuePair) []map[string]interfac return tags } -func getTagFilter(config map[string]interface{}) TagFilter { +func getTagFilter(rawTags []interface{}, filterType string, filterNamespace string) TagFilter { filter := TagFilter{} - filter.Type = "TagFilters" - filter.Namespace = config["namespace"].(string) - rawTags := config["tags"].([]interface{}) + filter.Type = filterType + filter.Namespace = filterNamespace Tags := make([]string, len(rawTags)) for i, v := range rawTags { Tags[i] = v.(string) @@ -512,17 +511,17 @@ func getTagFilter(config map[string]interface{}) TagFilter { return filter } -func getAzureTagFilter(config map[string]interface{}) AzureTagFilter { +func getAzureTagFilter(rawTags []interface{}, filterType string, filterNamespace string) AzureTagFilter { filter := AzureTagFilter{} - filter.Type = "AzureTagFilters" - filter.namespace = config["namespace"].(string) - RawTags := config["tags"].([]interface{}) + filter.Type = filterType + filter.Namespace = filterNamespace Tags := make([]AzureTagKeyValuePair, len(rawTags)) - for i, tagPair := range rawTags { + for i, rawTagPair := range rawTags { + tagPair := rawTagPair.(map[string]interface{}) pair := AzureTagKeyValuePair{} - pair.Name = tagPair["name"] + pair.Name = tagPair["name"].(string) // get the values from the azure key value pair - rawValues = tagPair["values"].([]interface{}) + rawValues := tagPair["values"].([]interface{}) valueList := make([]string, len(rawValues)) for j, v := range rawValues { valueList[j] = v.(string) @@ -543,23 +542,24 @@ func getPollingTagFilters(d *schema.ResourceData) []interface{} { config := rawConfig.(map[string]interface{}) filterType := config["type"].(string) filterNamespace := config["namespace"].(string) + rawTags := config["tags"].([]interface{}) - switch filterType := config["type"].(string); filterType { + switch filterType { case "TagFilters": - filter := getPollingTagFilters(config) + filter := getTagFilter(rawTags, filterType, filterNamespace) filters = append(filters, filter) case "AzureTagFilters": - filter := getAzureTagFilter(config) - filters = append(filter, filter) + filter := getAzureTagFilter(rawTags, filterType, filterNamespace) + filters = append(filters, filter) // type is optional default: - if len(tags) > 0 { - switch tags[0].(type) { + if len(rawTags) > 0 { + switch rawTags[0].(type) { case map[string]interface{}: - filter := getAzureTagFilter(config) - filters = append(filter, filter) + filter := getAzureTagFilter(rawTags, filterType, filterNamespace) + filters = append(filters, filter) case string: - filter := getPollingTagFilters(config) + filter := getTagFilter(rawTags, filterType, filterNamespace) filters = append(filters, filter) } } diff --git a/sumologic/resource_sumologic_polling_source.go b/sumologic/resource_sumologic_polling_source.go index 8641b8aa..e3481ea6 100644 --- a/sumologic/resource_sumologic_polling_source.go +++ b/sumologic/resource_sumologic_polling_source.go @@ -134,10 +134,9 @@ func resourceSumologicPollingSource() *schema.Resource { } func validateTags(val interface{}, key string) ([]string, []error) { - v := val.(map[string]interface{}) - var errs []error + var errors []error - if tags, ok := v.([]interface{}); ok { + if tags, ok := val.([]interface{}); ok { for i, tag := range tags { switch t := tag.(type) { case map[string]interface{}: @@ -168,6 +167,7 @@ func validateTags(val interface{}, key string) ([]string, []error) { } } } + return nil, errors } func resourceSumologicPollingSourceCreate(d *schema.ResourceData, meta interface{}) error { @@ -280,41 +280,72 @@ func getThirdPartyPathAttributes(pollingResource []PollingResource) []map[string return s } -func flattenTagFilters(v []TagFilter) []map[string]interface{} { +func flattenTagFilters(v []interface{}) []map[string]interface{} { var filters []map[string]interface{} for _, d := range v { - filter := map[string]interface{}{ - "type": d.Type, - "namespace": d.Namespace, - "tags": d.Tags, + log.Printf("[DEBUG] Entering myTerraformFunction" + d.(string)) + switch t := d.(type) { + case TagFilter: + filter := map[string]interface{}{ + "type": t.Type, + "namespace": t.Namespace, + "tags": t.Tags, + } + filters = append(filters, filter) + case AzureTagFilter: + var tags []map[string]interface{} + for _, rawTag := range t.Tags { + tag := map[string]interface{}{ + "name": rawTag.Name, + "values": rawTag.Values, + } + tags = append(tags, tag) + } + + filter := map[string]interface{}{ + "type": t.Type, + "namespace": t.Namespace, + "tags": tags, + } + filters = append(filters, filter) } - filters = append(filters, filter) } - return filters } -func getTagFilters(d *schema.ResourceData) []TagFilter { +func getTagFilters(d *schema.ResourceData) []interface{} { paths := d.Get("path").([]interface{}) path := paths[0].(map[string]interface{}) rawTagFilterConfig := path["tag_filters"].([]interface{}) - var filters []TagFilter + var filters []interface{} for _, rawConfig := range rawTagFilterConfig { config := rawConfig.(map[string]interface{}) - filter := TagFilter{} - filter.Type = config["type"].(string) - filter.Namespace = config["namespace"].(string) - + filterType := config["type"].(string) + filterNamespace := config["namespace"].(string) rawTags := config["tags"].([]interface{}) - Tags := make([]string, len(rawTags)) - for i, v := range rawTags { - Tags[i] = v.(string) + + switch filterType { + case "TagFilters": + filter := getTagFilter(rawTags, filterType, filterNamespace) + filters = append(filters, filter) + case "AzureTagFilters": + filter := getAzureTagFilter(rawTags, filterType, filterNamespace) + filters = append(filters, filter) + // type is optional + default: + if len(rawTags) > 0 { + switch rawTags[0].(type) { + case map[string]interface{}: + filter := getAzureTagFilter(rawTags, filterType, filterNamespace) + filters = append(filters, filter) + case string: + filter := getTagFilter(rawTags, filterType, filterNamespace) + filters = append(filters, filter) + } + } } - filter.Tags = Tags - filters = append(filters, filter) } - return filters } From d7473108e48feb3e1085781e34f3408dab48ff77 Mon Sep 17 00:00:00 2001 From: Yuting Liu Date: Wed, 8 Jan 2025 17:00:42 -0800 Subject: [PATCH 4/7] SUMO-245309 Add azure_tag_filters in terraform resource --- sumologic/provider.go | 1 + ...rce_sumologic_azure_metrics_source_test.go | 6 +- ...source_sumologic_generic_polling_source.go | 71 +++++++++++++++---- .../resource_sumologic_polling_source.go | 42 +++++++++-- 4 files changed, 99 insertions(+), 21 deletions(-) diff --git a/sumologic/provider.go b/sumologic/provider.go index 9320777b..29e84634 100644 --- a/sumologic/provider.go +++ b/sumologic/provider.go @@ -119,6 +119,7 @@ func Provider() terraform.ResourceProvider { "sumologic_rum_source": resourceSumologicRumSource(), "sumologic_role_v2": resourceSumologicRoleV2(), "sumologic_azure_event_hub_log_source": resourceSumologicGenericPollingSource(), + "sumologic_azure_metric_source": resourceSumologicGenericPollingSource(), }, DataSourcesMap: map[string]*schema.Resource{ "sumologic_cse_log_mapping_vendor_product": dataSourceCSELogMappingVendorAndProduct(), diff --git a/sumologic/resource_sumologic_azure_metrics_source_test.go b/sumologic/resource_sumologic_azure_metrics_source_test.go index e42d8b16..5c9b8720 100644 --- a/sumologic/resource_sumologic_azure_metrics_source_test.go +++ b/sumologic/resource_sumologic_azure_metrics_source_test.go @@ -96,7 +96,7 @@ func TestAccSumologicAzureMetricsSource_update(t *testing.T) { func testAccCheckAzureMetricsSourceDestroy(s *terraform.State) error { client := testAccProvider.Meta().(*Client) for _, rs := range s.RootModule().Resources { - if rs.Type != "sumologic_azure_event_hub_log_source" { + if rs.Type != "sumologic_azure_metrics_source" { continue } if rs.Primary.ID == "" { @@ -170,7 +170,7 @@ resource "sumologic_collector" "test" { description = "%s" category = "%s" } -resource "sumologic_azure_event_metrics_source" "azure-metrics" { +resource "sumologic_azure_metrics_source" "azure-metrics" { name = "%s" description = "%s" category = "%s" @@ -188,7 +188,7 @@ resource "sumologic_azure_event_metrics_source" "azure-metrics" { type = "AzureMetricsPath" environment = "Azure" limit_to_namespaces = ["Microsoft.ClassicStorage/storageAccounts"] - tag_filters { + azure_tag_filters { type = "AzureTagFilters" namespace = "Microsoft.ClassicStorage/storageAccounts" tags { diff --git a/sumologic/resource_sumologic_generic_polling_source.go b/sumologic/resource_sumologic_generic_polling_source.go index 5f6088e5..b73b4d41 100644 --- a/sumologic/resource_sumologic_generic_polling_source.go +++ b/sumologic/resource_sumologic_generic_polling_source.go @@ -219,6 +219,41 @@ func resourceSumologicGenericPollingSource() *schema.Resource { }, }, }, + "azure_tag_filters": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Optional: true, + }, + "namespace": { + Type: schema.TypeString, + Optional: true, + }, + "tags": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "values": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + }, + }, + }, "sns_topic_or_subscription_arn": { Type: schema.TypeList, Computed: true, @@ -389,6 +424,7 @@ func getPollingThirdPartyPathAttributes(pollingResource []PollingResource) []map "use_versioned_api": t.Path.UseVersionedApi, "custom_services": flattenCustomServices(t.Path.CustomServices), "tag_filters": flattenPollingTagFilters(t.Path.TagFilters), + "azure_tag_filters": flattenPollingAzureTagFilters(t.Path.TagFilters), "sns_topic_or_subscription_arn": flattenPollingSnsTopicOrSubscriptionArn(t.Path.SnsTopicOrSubscriptionArn), "namespace": t.Path.Namespace, "event_hub_name": t.Path.EventHubName, @@ -464,26 +500,32 @@ func getCustomServices(path map[string]interface{}) []string { func flattenPollingTagFilters(v []interface{}) []map[string]interface{} { var filters []map[string]interface{} for _, d := range v { - filter := make(map[string]interface{}) switch t := d.(type) { case TagFilter: - filter = map[string]interface{}{ + filter := map[string]interface{}{ "type": t.Type, "namespace": t.Namespace, - "Tags": t.Tags, + "tags": t.Tags, } + filters = append(filters, filter) + } + } + return filters +} + +func flattenPollingAzureTagFilters(v []interface{}) []map[string]interface{} { + var filters []map[string]interface{} + for _, d := range v { + switch t := d.(type) { case AzureTagFilter: - filter = map[string]interface{}{ + filter := map[string]interface{}{ "type": t.Type, "namespace": t.Namespace, - "Tags": flattenAzureTagKeyValuePair(t.Tags), + "tags": flattenAzureTagKeyValuePair(t.Tags), } - default: - continue + filters = append(filters, filter) } - filters = append(filters, filter) } - return filters } @@ -499,10 +541,11 @@ func flattenAzureTagKeyValuePair(v []AzureTagKeyValuePair) []map[string]interfac return tags } -func getTagFilter(rawTags []interface{}, filterType string, filterNamespace string) TagFilter { +func getTagFilter(config map[string]interface{}) TagFilter { filter := TagFilter{} - filter.Type = filterType - filter.Namespace = filterNamespace + filter.Type = config["type"].(string) + filter.Namespace = config["namespace"].(string) + rawTags := config["tags"].([]interface{}) Tags := make([]string, len(rawTags)) for i, v := range rawTags { Tags[i] = v.(string) @@ -546,7 +589,7 @@ func getPollingTagFilters(d *schema.ResourceData) []interface{} { switch filterType { case "TagFilters": - filter := getTagFilter(rawTags, filterType, filterNamespace) + filter := getTagFilter(config) filters = append(filters, filter) case "AzureTagFilters": filter := getAzureTagFilter(rawTags, filterType, filterNamespace) @@ -559,7 +602,7 @@ func getPollingTagFilters(d *schema.ResourceData) []interface{} { filter := getAzureTagFilter(rawTags, filterType, filterNamespace) filters = append(filters, filter) case string: - filter := getTagFilter(rawTags, filterType, filterNamespace) + filter := getTagFilter(config) filters = append(filters, filter) } } diff --git a/sumologic/resource_sumologic_polling_source.go b/sumologic/resource_sumologic_polling_source.go index e3481ea6..b82c66e6 100644 --- a/sumologic/resource_sumologic_polling_source.go +++ b/sumologic/resource_sumologic_polling_source.go @@ -119,9 +119,43 @@ func resourceSumologicPollingSource() *schema.Resource { Type: schema.TypeList, Optional: true, Elem: &schema.Schema{ - Type: schema.TypeMap, // Accept both maps (for objects) and strings + Type: schema.TypeString, + }, + }, + }, + }, + }, + "azure_tag_filters": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Optional: true, + }, + "namespace": { + Type: schema.TypeString, + Optional: true, + }, + "tags": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "values": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, }, - ValidateFunc: validateTags, }, }, }, @@ -327,7 +361,7 @@ func getTagFilters(d *schema.ResourceData) []interface{} { switch filterType { case "TagFilters": - filter := getTagFilter(rawTags, filterType, filterNamespace) + filter := getTagFilter(config) filters = append(filters, filter) case "AzureTagFilters": filter := getAzureTagFilter(rawTags, filterType, filterNamespace) @@ -340,7 +374,7 @@ func getTagFilters(d *schema.ResourceData) []interface{} { filter := getAzureTagFilter(rawTags, filterType, filterNamespace) filters = append(filters, filter) case string: - filter := getTagFilter(rawTags, filterType, filterNamespace) + filter := getTagFilter(config) filters = append(filters, filter) } } From df9b4b0ea94a0491c818fe46751b9b5e38fd4163 Mon Sep 17 00:00:00 2001 From: Yuting Liu Date: Sun, 12 Jan 2025 21:27:14 -0800 Subject: [PATCH 5/7] SUMO-245309 Fix UT related with tag filters --- sumologic/provider.go | 2 +- ...rce_sumologic_azure_metrics_source_test.go | 2 +- ...source_sumologic_generic_polling_source.go | 149 +++++++++--------- .../resource_sumologic_polling_source.go | 33 ++-- sumologic/sumologic_polling_source.go | 1 + 5 files changed, 87 insertions(+), 100 deletions(-) diff --git a/sumologic/provider.go b/sumologic/provider.go index 29e84634..90b4e5ee 100644 --- a/sumologic/provider.go +++ b/sumologic/provider.go @@ -119,7 +119,7 @@ func Provider() terraform.ResourceProvider { "sumologic_rum_source": resourceSumologicRumSource(), "sumologic_role_v2": resourceSumologicRoleV2(), "sumologic_azure_event_hub_log_source": resourceSumologicGenericPollingSource(), - "sumologic_azure_metric_source": resourceSumologicGenericPollingSource(), + "sumologic_azure_metrics_source": resourceSumologicGenericPollingSource(), }, DataSourcesMap: map[string]*schema.Resource{ "sumologic_cse_log_mapping_vendor_product": dataSourceCSELogMappingVendorAndProduct(), diff --git a/sumologic/resource_sumologic_azure_metrics_source_test.go b/sumologic/resource_sumologic_azure_metrics_source_test.go index 5c9b8720..b86ca49f 100644 --- a/sumologic/resource_sumologic_azure_metrics_source_test.go +++ b/sumologic/resource_sumologic_azure_metrics_source_test.go @@ -170,7 +170,7 @@ resource "sumologic_collector" "test" { description = "%s" category = "%s" } -resource "sumologic_azure_metrics_source" "azure-metrics" { +resource "sumologic_azure_metrics_source" "azure" { name = "%s" description = "%s" category = "%s" diff --git a/sumologic/resource_sumologic_generic_polling_source.go b/sumologic/resource_sumologic_generic_polling_source.go index b73b4d41..a3da5025 100644 --- a/sumologic/resource_sumologic_generic_polling_source.go +++ b/sumologic/resource_sumologic_generic_polling_source.go @@ -25,7 +25,7 @@ func resourceSumologicGenericPollingSource() *schema.Resource { Required: true, ForceNew: true, ValidateFunc: validation.StringInSlice([]string{"AwsS3Bucket", "AwsElbBucket", "AwsCloudFrontBucket", - "AwsCloudTrailBucket", "AwsS3AuditBucket", "AwsCloudWatch", "AwsInventory", "AwsXRay", "GcpMetrics", "AwsS3ArchiveBucket", "AzureEventHubLog"}, false), + "AwsCloudTrailBucket", "AwsS3AuditBucket", "AwsCloudWatch", "AwsInventory", "AwsXRay", "GcpMetrics", "AwsS3ArchiveBucket", "AzureEventHubLog", "AzureMetrics"}, false), } pollingSource.Schema["scan_interval"] = &schema.Schema{ Type: schema.TypeInt, @@ -226,7 +226,7 @@ func resourceSumologicGenericPollingSource() *schema.Resource { Schema: map[string]*schema.Schema{ "type": { Type: schema.TypeString, - Optional: true, + Required: true, }, "namespace": { Type: schema.TypeString, @@ -499,15 +499,20 @@ func getCustomServices(path map[string]interface{}) []string { func flattenPollingTagFilters(v []interface{}) []map[string]interface{} { var filters []map[string]interface{} - for _, d := range v { - switch t := d.(type) { - case TagFilter: - filter := map[string]interface{}{ - "type": t.Type, - "namespace": t.Namespace, - "tags": t.Tags, + if len(v) > 0 { + filter := v[0].(map[string]interface{}) + switch filterType, ok := filter["type"].(string); { + case !ok || filterType == "AzureTagFilters": // do nothing + default: + for _, d := range v { + rawFilter := d.(map[string]interface{}) + filter := map[string]interface{}{ + "type": rawFilter["type"].(string), + "namespace": rawFilter["namespace"].(string), + "tags": rawFilter["tags"].([]interface{}), + } + filters = append(filters, filter) } - filters = append(filters, filter) } } return filters @@ -515,98 +520,90 @@ func flattenPollingTagFilters(v []interface{}) []map[string]interface{} { func flattenPollingAzureTagFilters(v []interface{}) []map[string]interface{} { var filters []map[string]interface{} - for _, d := range v { - switch t := d.(type) { - case AzureTagFilter: - filter := map[string]interface{}{ - "type": t.Type, - "namespace": t.Namespace, - "tags": flattenAzureTagKeyValuePair(t.Tags), + if len(v) > 0 { + filter := v[0].(map[string]interface{}) + switch filterType, ok := filter["type"].(string); { + case !ok || filterType == "AzureTagFilters": + for _, d := range v { + rawFilter := d.(map[string]interface{}) + filter := map[string]interface{}{ + "type": "AzureTagFilters", + "namespace": rawFilter["namespace"].(string), + "tags": flattenAzureTagKeyValuePair(rawFilter["tags"].([]interface{})), + } + filters = append(filters, filter) } - filters = append(filters, filter) + default: // do nothing } } return filters } -func flattenAzureTagKeyValuePair(v []AzureTagKeyValuePair) []map[string]interface{} { +func flattenAzureTagKeyValuePair(v []interface{}) []map[string]interface{} { var tags []map[string]interface{} for _, d := range v { + rawTag := d.(map[string]interface{}) tag := map[string]interface{}{ - "name": d.Name, - "values": d.Values, + "name": rawTag["name"].(string), + "values": rawTag["values"].([]interface{}), } tags = append(tags, tag) } return tags } -func getTagFilter(config map[string]interface{}) TagFilter { - filter := TagFilter{} - filter.Type = config["type"].(string) - filter.Namespace = config["namespace"].(string) - rawTags := config["tags"].([]interface{}) - Tags := make([]string, len(rawTags)) - for i, v := range rawTags { - Tags[i] = v.(string) - } - filter.Tags = Tags - return filter -} +func getPollingTagFilters(d *schema.ResourceData) []interface{} { + paths := d.Get("path").([]interface{}) + path := paths[0].(map[string]interface{}) + rawTagFilterConfig := path["tag_filters"].([]interface{}) + var filters []interface{} -func getAzureTagFilter(rawTags []interface{}, filterType string, filterNamespace string) AzureTagFilter { - filter := AzureTagFilter{} - filter.Type = filterType - filter.Namespace = filterNamespace - Tags := make([]AzureTagKeyValuePair, len(rawTags)) - for i, rawTagPair := range rawTags { - tagPair := rawTagPair.(map[string]interface{}) - pair := AzureTagKeyValuePair{} - pair.Name = tagPair["name"].(string) - // get the values from the azure key value pair - rawValues := tagPair["values"].([]interface{}) - valueList := make([]string, len(rawValues)) - for j, v := range rawValues { - valueList[j] = v.(string) + for _, rawConfig := range rawTagFilterConfig { + config := rawConfig.(map[string]interface{}) + filter := TagFilter{} + filter.Type = config["type"].(string) + filter.Namespace = config["namespace"].(string) + + rawTags := config["tags"].([]interface{}) + Tags := make([]string, len(rawTags)) + for i, v := range rawTags { + Tags[i] = v.(string) } - pair.Values = valueList - Tags[i] = pair + filter.Tags = Tags + filters = append(filters, filter) } - return filter + return filters } -func getPollingTagFilters(d *schema.ResourceData) []interface{} { +func getPollingAzureTagFilters(d *schema.ResourceData) []interface{} { paths := d.Get("path").([]interface{}) path := paths[0].(map[string]interface{}) - rawTagFilterConfig := path["tag_filters"].([]interface{}) + rawTagFilterConfig := path["azure_tag_filters"].([]interface{}) var filters []interface{} for _, rawConfig := range rawTagFilterConfig { config := rawConfig.(map[string]interface{}) - filterType := config["type"].(string) - filterNamespace := config["namespace"].(string) - rawTags := config["tags"].([]interface{}) + filter := AzureTagFilter{} + filter.Type = config["type"].(string) + filter.Namespace = config["namespace"].(string) - switch filterType { - case "TagFilters": - filter := getTagFilter(config) - filters = append(filters, filter) - case "AzureTagFilters": - filter := getAzureTagFilter(rawTags, filterType, filterNamespace) - filters = append(filters, filter) - // type is optional - default: - if len(rawTags) > 0 { - switch rawTags[0].(type) { - case map[string]interface{}: - filter := getAzureTagFilter(rawTags, filterType, filterNamespace) - filters = append(filters, filter) - case string: - filter := getTagFilter(config) - filters = append(filters, filter) - } + rawTags := config["tags"].([]interface{}) + Tags := make([]AzureTagKeyValuePair, len(rawTags)) + for i, rawTagPair := range rawTags { + tagPair := rawTagPair.(map[string]interface{}) + pair := AzureTagKeyValuePair{} + pair.Name = tagPair["name"].(string) + // get the values from the azure key value pair + rawValues := tagPair["values"].([]interface{}) + valueList := make([]string, len(rawValues)) + for j, v := range rawValues { + valueList[j] = v.(string) } + pair.Values = valueList + Tags[i] = pair } + filter.Tags = Tags + filters = append(filters, filter) } return filters } @@ -701,8 +698,9 @@ func getPollingAuthentication(d *schema.ResourceData) (PollingAuthentication, er authSettings.SharedAccessPolicyName = auth["shared_access_policy_name"].(string) authSettings.SharedAccessPolicyKey = auth["shared_access_policy_key"].(string) case "AzureClientSecretAuthentication": + authSettings.Type = "AzureClientSecretAuthentication" authSettings.TenantId = auth["tenant_id"].(string) - authSettings.ClientId = auth["client_id"].(string) + authSettings.AzureClientId = auth["client_id"].(string) authSettings.ClientSecret = auth["client_secret"].(string) default: errorMessage := fmt.Sprintf("[ERROR] Unknown authType: %v", authType) @@ -807,7 +805,8 @@ func getPollingPathSettings(d *schema.ResourceData) (PollingPath, error) { LimitToNamespaces = append(LimitToNamespaces, v.(string)) } } - pathSettings.TagFilters = getPollingTagFilters(d) + pathSettings.LimitToNamespaces = LimitToNamespaces + pathSettings.TagFilters = getPollingAzureTagFilters(d) default: errorMessage := fmt.Sprintf("[ERROR] Unknown resourceType in path: %v", pathType) log.Print(errorMessage) diff --git a/sumologic/resource_sumologic_polling_source.go b/sumologic/resource_sumologic_polling_source.go index b82c66e6..bfa28693 100644 --- a/sumologic/resource_sumologic_polling_source.go +++ b/sumologic/resource_sumologic_polling_source.go @@ -132,7 +132,7 @@ func resourceSumologicPollingSource() *schema.Resource { Schema: map[string]*schema.Schema{ "type": { Type: schema.TypeString, - Optional: true, + Required: true, }, "namespace": { Type: schema.TypeString, @@ -355,30 +355,17 @@ func getTagFilters(d *schema.ResourceData) []interface{} { for _, rawConfig := range rawTagFilterConfig { config := rawConfig.(map[string]interface{}) - filterType := config["type"].(string) - filterNamespace := config["namespace"].(string) - rawTags := config["tags"].([]interface{}) + filter := TagFilter{} + filter.Type = config["type"].(string) + filter.Namespace = config["namespace"].(string) - switch filterType { - case "TagFilters": - filter := getTagFilter(config) - filters = append(filters, filter) - case "AzureTagFilters": - filter := getAzureTagFilter(rawTags, filterType, filterNamespace) - filters = append(filters, filter) - // type is optional - default: - if len(rawTags) > 0 { - switch rawTags[0].(type) { - case map[string]interface{}: - filter := getAzureTagFilter(rawTags, filterType, filterNamespace) - filters = append(filters, filter) - case string: - filter := getTagFilter(config) - filters = append(filters, filter) - } - } + rawTags := config["tags"].([]interface{}) + Tags := make([]string, len(rawTags)) + for i, v := range rawTags { + Tags[i] = v.(string) } + filter.Tags = Tags + filters = append(filters, filter) } return filters } diff --git a/sumologic/sumologic_polling_source.go b/sumologic/sumologic_polling_source.go index 9d583a7e..25edd523 100644 --- a/sumologic/sumologic_polling_source.go +++ b/sumologic/sumologic_polling_source.go @@ -41,6 +41,7 @@ type PollingAuthentication struct { ClientX509CertUrl string `json:"client_x509_cert_url"` SharedAccessPolicyName string `json:"sharedAccessPolicyName"` SharedAccessPolicyKey string `json:"sharedAccessPolicyKey"` + AzureClientId string `json:"clientId"` TenantId string `json:"tenantId"` ClientSecret string `json:"clientSecret"` } From 9d5d99d930a107637b0500d2b4315958a8b8ade1 Mon Sep 17 00:00:00 2001 From: Yuting Liu Date: Mon, 13 Jan 2025 15:17:30 -0800 Subject: [PATCH 6/7] SUMO-245309 Add the doc for the new resource sumologic_azure_metrics_source --- .github/workflows/test.yml | 3 + CHANGELOG.md | 3 +- .../resource_sumologic_polling_source.go | 89 ------------------- .../docs/r/azure_metrics_source.html.markdown | 67 ++++++++++++++ 4 files changed, 72 insertions(+), 90 deletions(-) create mode 100644 website/docs/r/azure_metrics_source.html.markdown diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a35e2580..61366564 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -98,6 +98,9 @@ jobs: SUMOLOGIC_TEST_ROLE_ARN: ${{ secrets.SUMOLOGIC_TEST_ROLE_ARN }} SUMOLOGIC_TEST_SAS_KEY: ${{ secrets.SUMOLOGIC_TEST_SAS_KEY }} SUMOLOGIC_TEST_SAS_KEY_NAME: ${{ secrets.SUMOLOGIC_TEST_SAS_KEY_NAME }} + SUMOLOGIC_TEST_AZURE_TENANT_ID: ${{ secrets.SUMOLOGIC_TEST_AZURE_TENANT_ID }} + SUMOLOGIC_TEST_AZURE_CLIENT_ID: ${{ secrets.SUMOLOGIC_TEST_AZURE_CLIENT_ID }} + SUMOLOGIC_TEST_AZURE_CLIENT_SECRET: ${{ secrets.SUMOLOGIC_TEST_AZURE_CLIENT_SECRET }} # disable go test timeout. We rely on GitHub action timeout. run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 01e28566..0f78153c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## X.Y.Z (Unreleased) -* Add new change notes here +FEATURES: +* **New Resource:** sumologic_azure_metrics_source (GH-710) ## 3.0.0 (December 09, 2024) **REMOVALS:** diff --git a/sumologic/resource_sumologic_polling_source.go b/sumologic/resource_sumologic_polling_source.go index bfa28693..ad537a9d 100644 --- a/sumologic/resource_sumologic_polling_source.go +++ b/sumologic/resource_sumologic_polling_source.go @@ -125,41 +125,6 @@ func resourceSumologicPollingSource() *schema.Resource { }, }, }, - "azure_tag_filters": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "type": { - Type: schema.TypeString, - Required: true, - }, - "namespace": { - Type: schema.TypeString, - Optional: true, - }, - "tags": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - }, - "values": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - }, - }, - }, - }, - }, - }, }, }, } @@ -167,43 +132,6 @@ func resourceSumologicPollingSource() *schema.Resource { return pollingSource } -func validateTags(val interface{}, key string) ([]string, []error) { - var errors []error - - if tags, ok := val.([]interface{}); ok { - for i, tag := range tags { - switch t := tag.(type) { - case map[string]interface{}: - // Validate object structure - // Validate "name" to be a string - if _, ok := t["name"]; !ok { - errors = append(errors, fmt.Errorf("%s[%d]: missing required field 'name'", key, i)) - } else if _, ok := t["name"].(string); !ok { - errors = append(errors, fmt.Errorf("%s[%d]: 'name' must be a string", key, i)) - } - - // Validate "values" to be a list of strings - if _, ok := t["values"]; !ok { - errors = append(errors, fmt.Errorf("%s[%d]: missing required field 'values'", key, i)) - } else if values, ok := t["values"].([]interface{}); !ok { - errors = append(errors, fmt.Errorf("%s[%d]: 'values' must be a list of strings", key, i)) - } else { - for j, value := range values { - if _, ok := value.(string); !ok { - errors = append(errors, fmt.Errorf("%s[%d].values[%d]: must be a string", key, i, j)) - } - } - } - case string: - continue - default: - errors = append(errors, fmt.Errorf("%s[%d]: must be either a string or an object with 'name' and 'values'", key, i)) - } - } - } - return nil, errors -} - func resourceSumologicPollingSourceCreate(d *schema.ResourceData, meta interface{}) error { c := meta.(*Client) @@ -317,7 +245,6 @@ func getThirdPartyPathAttributes(pollingResource []PollingResource) []map[string func flattenTagFilters(v []interface{}) []map[string]interface{} { var filters []map[string]interface{} for _, d := range v { - log.Printf("[DEBUG] Entering myTerraformFunction" + d.(string)) switch t := d.(type) { case TagFilter: filter := map[string]interface{}{ @@ -326,22 +253,6 @@ func flattenTagFilters(v []interface{}) []map[string]interface{} { "tags": t.Tags, } filters = append(filters, filter) - case AzureTagFilter: - var tags []map[string]interface{} - for _, rawTag := range t.Tags { - tag := map[string]interface{}{ - "name": rawTag.Name, - "values": rawTag.Values, - } - tags = append(tags, tag) - } - - filter := map[string]interface{}{ - "type": t.Type, - "namespace": t.Namespace, - "tags": tags, - } - filters = append(filters, filter) } } return filters diff --git a/website/docs/r/azure_metrics_source.html.markdown b/website/docs/r/azure_metrics_source.html.markdown new file mode 100644 index 00000000..e6d10d32 --- /dev/null +++ b/website/docs/r/azure_metrics_source.html.markdown @@ -0,0 +1,67 @@ +--- +layout: "sumologic" +page_title: "SumoLogic: sumologic_azure_metrics_source" +description: |- + Provides a Sumologic Azure Metrics Source. +--- + +# sumologic_azure_metrics_source +Provides a [Sumologic Azure Metrics Source](https://help.sumologic.com/docs/send-data/hosted-collectors/microsoft-source/azure-metrics-source/) + +__IMPORTANT:__ The Azure Event Hub credentials are stored in plain-text in the state. This is a potential security issue. + +## Example Usage +```hcl +resource "sumologic_azure_metrics_source" "terraform_azure_metrics_source" { + name = "Azure Metrics Source" + description = "My description" + category = "azure/metrics" + content_type = "AzureMetrics" + collector_id = "${sumologic_collector.collector.id}" + + authentication { + type = "AzureClientSecretAuthentication" + tenant_id = "azure_tenant_id" + client_id = "azure_client_id" + client_secret = "azure_client_secret" + } + + path { + type = "AzureMetricsPath" + environment = "Azure" + limit_to_namespaces = ["Microsoft.ClassicStorage/storageAccounts"] + azure_tag_filters { + type = "AzureTagFilters" + namespace = "Microsoft.ClassicStorage/storageAccounts" + tags { + name = "test-name-1" + values = ["value1", "value2"] + } + } + } +} + +resource "sumologic_collector" "collector" { + name = "my-collector" + description = "Just testing this" +} +``` + +## Argument reference +In addition to the [Common Source Properties](https://registry.terraform.io/providers/SumoLogic/sumologic/latest/docs#common-source-properties), the following arguments are supported: + - `content_type` - (Required) Must be `AzureMetrics`. +- `authentication` - (Required) Authentication details for connecting to ingest metrics from Azure. + + `type` - (Required) Must be `AzureClientSecretAuthentication`. + + `tenant_id` - (Required) Your tenant id collected from [Azure platform](https://help.sumologic.com/docs/send-data/hosted-collectors/microsoft-source/azure-metrics-source/#vendor-configuration). + + `client_id` - (Required) Your client id collected from [Azure platform](https://help.sumologic.com/docs/send-data/hosted-collectors/microsoft-source/azure-metrics-source/#vendor-configuration). + + `client_secret` - (Required) Your client secret collected from [Azure platform](https://help.sumologic.com/docs/send-data/hosted-collectors/microsoft-source/azure-metrics-source/#vendor-configuration). + - `path` - (Required) The location to scan for new data. + + `type` - (Required) Must be `AzureMetricsPath`. + + `environment` - (Required) The environment to collect Azure metrics. + + `limit_to_namespaces` - (Opitonal) The list of namespaces to collect metrics. By default all namespaces are selected. + + `azure_tag_filters` - (Optional) Tag filters allow you to filter the Azure metrics by the tags you have assigned to your Azure resources. You can define tag filters for each supported namespace. If you do not define any tag filters, all metrics will be collected for namespaces you configured for the source above. + + `type` - (Required) This value has to be set to `AzureTagFilters` + + `namespace` - Namespace for which you want to define the tag filters. + + `tags` - List of key and value list of tag filters. + + `name`: The name of tag. + + `values`: The list of accepted values for the tag name. From 6ad54319ecd078cc7e48a1a64436b4433fbde51b Mon Sep 17 00:00:00 2001 From: Yuting Liu Date: Mon, 13 Jan 2025 17:13:22 -0800 Subject: [PATCH 7/7] SUMO-245309 Fix the azure metrcis update ut --- sumologic/resource_sumologic_generic_polling_source.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sumologic/resource_sumologic_generic_polling_source.go b/sumologic/resource_sumologic_generic_polling_source.go index a3da5025..c9515800 100644 --- a/sumologic/resource_sumologic_generic_polling_source.go +++ b/sumologic/resource_sumologic_generic_polling_source.go @@ -459,6 +459,8 @@ func getPollingThirdPartyAuthenticationAttributes(pollingResource []PollingResou "client_x509_cert_url": t.Authentication.ClientX509CertUrl, "shared_access_policy_name": t.Authentication.SharedAccessPolicyName, "shared_access_policy_key": t.Authentication.SharedAccessPolicyKey, + "tenant_id": t.Authentication.TenantId, + "client_secret": t.Authentication.ClientSecret, } s = append(s, mapping) }