From 68ade3dd44ffd08dc6fc59a6705cfe7616e9235f Mon Sep 17 00:00:00 2001 From: Yuting Liu Date: Mon, 6 Jan 2025 15:09:42 -0800 Subject: [PATCH] SUMO-245309 Add the terraform support for Azure metric sources --- ...rce_sumologic_azure_metrics_source_test.go | 194 ++++++++++++++++++ ...source_sumologic_generic_polling_source.go | 71 ++++++- .../resource_sumologic_polling_source.go | 40 +++- sumologic/sumologic_polling_source.go | 16 +- 4 files changed, 308 insertions(+), 13 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..f51bc5f1 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,11 +487,23 @@ 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 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{}) @@ -577,7 +612,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 +709,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 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"`