Skip to content

Commit

Permalink
SUMO-245309 Add the terraform support for Azure metric sources
Browse files Browse the repository at this point in the history
  • Loading branch information
yuting-liu committed Jan 7, 2025
1 parent e3162c7 commit 68ade3d
Show file tree
Hide file tree
Showing 4 changed files with 308 additions and 13 deletions.
194 changes: 194 additions & 0 deletions sumologic/resource_sumologic_azure_metrics_source_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
71 changes: 60 additions & 11 deletions sumologic/resource_sumologic_generic_polling_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
},
},
},
}
Expand All @@ -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,
Expand Down Expand Up @@ -243,10 +251,13 @@ func resourceSumologicGenericPollingSource() *schema.Resource {
Type: schema.TypeString,
Optional: true,
},
"environment": {
Type: schema.TypeString,
Optional: true,
},
},
},
}

return pollingSource
}

Expand Down Expand Up @@ -450,25 +461,49 @@ 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)
}

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{})
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
40 changes: 39 additions & 1 deletion sumologic/resource_sumologic_polling_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
},
Expand All @@ -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)
Expand Down
Loading

0 comments on commit 68ade3d

Please sign in to comment.