diff --git a/CHANGELOG.md b/CHANGELOG.md index 62dc1deeb..2a27de706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## [Unreleased] +- Support multiple group by fields in SLOs ([#870](https://github.com/elastic/terraform-provider-elasticstack/pull/878)) - Use the auto-generated OAS schema from elastic/kibana for the Fleet API. ([#834](https://github.com/elastic/terraform-provider-elasticstack/issues/834)) ## [0.11.11] - 2024-10-25 diff --git a/docs/resources/kibana_slo.md b/docs/resources/kibana_slo.md index 4dee6be5d..9845b31ee 100644 --- a/docs/resources/kibana_slo.md +++ b/docs/resources/kibana_slo.md @@ -208,7 +208,7 @@ resource "elasticstack_kibana_slo" "custom_metric" { - `apm_availability_indicator` (Block List, Max: 1) (see [below for nested schema](#nestedblock--apm_availability_indicator)) - `apm_latency_indicator` (Block List, Max: 1) (see [below for nested schema](#nestedblock--apm_latency_indicator)) -- `group_by` (String) Optional group by field to use to generate an SLO per distinct value. +- `group_by` (List of String) Optional group by fields to use to generate an SLO per distinct value. - `histogram_custom_indicator` (Block List, Max: 1) (see [below for nested schema](#nestedblock--histogram_custom_indicator)) - `kql_custom_indicator` (Block List, Max: 1) (see [below for nested schema](#nestedblock--kql_custom_indicator)) - `metric_custom_indicator` (Block List, Max: 1) (see [below for nested schema](#nestedblock--metric_custom_indicator)) diff --git a/generated/slo-spec.yml b/generated/slo-spec.yml index 5529502e2..a1922fa03 100644 --- a/generated/slo-spec.yml +++ b/generated/slo-spec.yml @@ -1389,6 +1389,14 @@ components: $ref: '#/components/schemas/objective' settings: $ref: '#/components/schemas/settings' + groupBy: + description: optional group by field to use to generate an SLO per distinct value + oneOf: + - type: string + - type: array + items: + type: string + example: some.field tags: description: List of tags type: array diff --git a/generated/slo/api/openapi.yaml b/generated/slo/api/openapi.yaml index 33bc46518..6533b9907 100644 --- a/generated/slo/api/openapi.yaml +++ b/generated/slo/api/openapi.yaml @@ -1349,6 +1349,8 @@ components: $ref: '#/components/schemas/objective' settings: $ref: '#/components/schemas/settings' + groupBy: + $ref: '#/components/schemas/slo_response_groupBy' tags: description: List of tags items: diff --git a/generated/slo/docs/UpdateSloRequest.md b/generated/slo/docs/UpdateSloRequest.md index 31065689a..2a4cdb393 100644 --- a/generated/slo/docs/UpdateSloRequest.md +++ b/generated/slo/docs/UpdateSloRequest.md @@ -11,6 +11,7 @@ Name | Type | Description | Notes **BudgetingMethod** | Pointer to [**BudgetingMethod**](BudgetingMethod.md) | | [optional] **Objective** | Pointer to [**Objective**](Objective.md) | | [optional] **Settings** | Pointer to [**Settings**](Settings.md) | | [optional] +**GroupBy** | Pointer to [**SloResponseGroupBy**](SloResponseGroupBy.md) | | [optional] **Tags** | Pointer to **[]string** | List of tags | [optional] ## Methods @@ -207,6 +208,31 @@ SetSettings sets Settings field to given value. HasSettings returns a boolean if a field has been set. +### GetGroupBy + +`func (o *UpdateSloRequest) GetGroupBy() SloResponseGroupBy` + +GetGroupBy returns the GroupBy field if non-nil, zero value otherwise. + +### GetGroupByOk + +`func (o *UpdateSloRequest) GetGroupByOk() (*SloResponseGroupBy, bool)` + +GetGroupByOk returns a tuple with the GroupBy field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetGroupBy + +`func (o *UpdateSloRequest) SetGroupBy(v SloResponseGroupBy)` + +SetGroupBy sets GroupBy field to given value. + +### HasGroupBy + +`func (o *UpdateSloRequest) HasGroupBy() bool` + +HasGroupBy returns a boolean if a field has been set. + ### GetTags `func (o *UpdateSloRequest) GetTags() []string` diff --git a/generated/slo/model_slo_response_group_by.go b/generated/slo/model_slo_response_group_by.go index 6e8723595..1c7fd3f9c 100644 --- a/generated/slo/model_slo_response_group_by.go +++ b/generated/slo/model_slo_response_group_by.go @@ -42,8 +42,8 @@ func (dst *SloResponseGroupBy) UnmarshalJSON(data []byte) error { // try to unmarshal data into ArrayOfString err = json.Unmarshal(data, &dst.ArrayOfString) if err == nil { - jsonstring, _ := json.Marshal(dst.ArrayOfString) - if string(jsonstring) == "{}" { // empty struct + jsonArraystring, _ := json.Marshal(dst.ArrayOfString) + if string(jsonArraystring) == "{}" { // empty struct dst.ArrayOfString = nil } else { match++ diff --git a/generated/slo/model_update_slo_request.go b/generated/slo/model_update_slo_request.go index eb975d0a6..242972bca 100644 --- a/generated/slo/model_update_slo_request.go +++ b/generated/slo/model_update_slo_request.go @@ -28,6 +28,7 @@ type UpdateSloRequest struct { BudgetingMethod *BudgetingMethod `json:"budgetingMethod,omitempty"` Objective *Objective `json:"objective,omitempty"` Settings *Settings `json:"settings,omitempty"` + GroupBy *SloResponseGroupBy `json:"groupBy,omitempty"` // List of tags Tags []string `json:"tags,omitempty"` } @@ -273,6 +274,38 @@ func (o *UpdateSloRequest) SetSettings(v Settings) { o.Settings = &v } +// GetGroupBy returns the GroupBy field value if set, zero value otherwise. +func (o *UpdateSloRequest) GetGroupBy() SloResponseGroupBy { + if o == nil || IsNil(o.GroupBy) { + var ret SloResponseGroupBy + return ret + } + return *o.GroupBy +} + +// GetGroupByOk returns a tuple with the GroupBy field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSloRequest) GetGroupByOk() (*SloResponseGroupBy, bool) { + if o == nil || IsNil(o.GroupBy) { + return nil, false + } + return o.GroupBy, true +} + +// HasGroupBy returns a boolean if a field has been set. +func (o *UpdateSloRequest) HasGroupBy() bool { + if o != nil && !IsNil(o.GroupBy) { + return true + } + + return false +} + +// SetGroupBy gets a reference to the given SloResponseGroupBy and assigns it to the GroupBy field. +func (o *UpdateSloRequest) SetGroupBy(v SloResponseGroupBy) { + o.GroupBy = &v +} + // GetTags returns the Tags field value if set, zero value otherwise. func (o *UpdateSloRequest) GetTags() []string { if o == nil || IsNil(o.Tags) { @@ -336,6 +369,9 @@ func (o UpdateSloRequest) ToMap() (map[string]interface{}, error) { if !IsNil(o.Settings) { toSerialize["settings"] = o.Settings } + if !IsNil(o.GroupBy) { + toSerialize["groupBy"] = o.GroupBy + } if !IsNil(o.Tags) { toSerialize["tags"] = o.Tags } diff --git a/internal/clients/kibana/slo.go b/internal/clients/kibana/slo.go index 1c0c519c6..e2c40733f 100644 --- a/internal/clients/kibana/slo.go +++ b/internal/clients/kibana/slo.go @@ -53,7 +53,7 @@ func DeleteSlo(ctx context.Context, apiClient *clients.ApiClient, sloId string, return utils.CheckHttpError(res, "Unabled to delete slo with ID "+string(sloId)) } -func UpdateSlo(ctx context.Context, apiClient *clients.ApiClient, s models.Slo) (*models.Slo, diag.Diagnostics) { +func UpdateSlo(ctx context.Context, apiClient *clients.ApiClient, s models.Slo, supportsGroupByList bool) (*models.Slo, diag.Diagnostics) { client, err := apiClient.GetSloClient() if err != nil { return nil, diag.FromErr(err) @@ -72,6 +72,7 @@ func UpdateSlo(ctx context.Context, apiClient *clients.ApiClient, s models.Slo) BudgetingMethod: (*slo.BudgetingMethod)(&s.BudgetingMethod), Objective: &s.Objective, Settings: s.Settings, + GroupBy: transformGroupBy(s.GroupBy, supportsGroupByList), Tags: s.Tags, } @@ -90,7 +91,7 @@ func UpdateSlo(ctx context.Context, apiClient *clients.ApiClient, s models.Slo) return sloResponseToModel(s.SpaceID, slo), diag.Diagnostics{} } -func CreateSlo(ctx context.Context, apiClient *clients.ApiClient, s models.Slo) (*models.Slo, diag.Diagnostics) { +func CreateSlo(ctx context.Context, apiClient *clients.ApiClient, s models.Slo, supportsGroupByList bool) (*models.Slo, diag.Diagnostics) { client, err := apiClient.GetSloClient() if err != nil { return nil, diag.FromErr(err) @@ -109,7 +110,7 @@ func CreateSlo(ctx context.Context, apiClient *clients.ApiClient, s models.Slo) BudgetingMethod: slo.BudgetingMethod(s.BudgetingMethod), Objective: s.Objective, Settings: s.Settings, - GroupBy: transformGroupBy(s.GroupBy), + GroupBy: transformGroupBy(s.GroupBy, supportsGroupByList), Tags: s.Tags, } @@ -177,14 +178,33 @@ func sloResponseToModel(spaceID string, res *slo.SloResponse) *models.Slo { TimeWindow: res.TimeWindow, Objective: res.Objective, Settings: &res.Settings, + GroupBy: transformGroupByFromResponse(res.GroupBy), Tags: res.Tags, } } -func transformGroupBy(groupBy *string) *slo.SloResponseGroupBy { +func transformGroupBy(groupBy []string, supportsGroupByList bool) *slo.SloResponseGroupBy { if groupBy == nil { return nil } - return &slo.SloResponseGroupBy{String: groupBy} + if !supportsGroupByList && len(groupBy) > 0 { + return &slo.SloResponseGroupBy{ + String: &groupBy[0], + } + } + + return &slo.SloResponseGroupBy{ArrayOfString: &groupBy} +} + +func transformGroupByFromResponse(groupBy slo.SloResponseGroupBy) []string { + if groupBy.String != nil { + return []string{*groupBy.String} + } + + if groupBy.ArrayOfString == nil { + return nil + } + + return *groupBy.ArrayOfString } diff --git a/internal/kibana/slo.go b/internal/kibana/slo.go index 727075e79..ce8c6bd8c 100644 --- a/internal/kibana/slo.go +++ b/internal/kibana/slo.go @@ -9,18 +9,77 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana" "github.com/elastic/terraform-provider-elasticstack/internal/models" "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) +var SLOSupportsMultipleGroupByMinVersion = version.Must(version.NewVersion("8.14.0")) + func ResourceSlo() *schema.Resource { + return &schema.Resource{ + Description: "Creates an SLO.", + + CreateContext: resourceSloCreate, + UpdateContext: resourceSloUpdate, + ReadContext: resourceSloRead, + DeleteContext: resourceSloDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: getSchema(), + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Version: 0, + Type: getResourceSchemaV0().CoreConfigSchema().ImpliedType(), + Upgrade: func(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + groupBy, ok := rawState["group_by"] + if !ok { + return rawState, nil + } + + groupByStr, ok := groupBy.(string) + if !ok { + return rawState, nil + } + + if len(groupByStr) == 0 { + return rawState, nil + } + + rawState["group_by"] = []string{groupByStr} + return rawState, nil + }, + }, + }, + } +} + +func getResourceSchemaV0() *schema.Resource { + s := getSchema() + s["group_by"] = &schema.Schema{ + Description: "Optional group by field to use to generate an SLO per distinct value.", + Type: schema.TypeString, + Optional: true, + ForceNew: false, + } + + return &schema.Resource{ + Schema: s, + } +} + +func getSchema() map[string]*schema.Schema { var indicatorAddresses []string for i := range indicatorAddressToType { indicatorAddresses = append(indicatorAddresses, i) } - sloSchema := map[string]*schema.Schema{ + return map[string]*schema.Schema{ "slo_id": { Description: "An ID (8 and 36 characters). If omitted, a UUIDv1 will be generated server-side.", Type: schema.TypeString, @@ -409,10 +468,17 @@ func ResourceSlo() *schema.Resource { ForceNew: true, }, "group_by": { - Description: "Optional group by field to use to generate an SLO per distinct value.", - Type: schema.TypeString, + Description: "Optional group by fields to use to generate an SLO per distinct value.", + Type: schema.TypeList, Optional: true, ForceNew: false, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + DefaultFunc: func() (interface{}, error) { + return []string{"*"}, nil + }, }, "tags": { Description: "The tags for the SLO.", @@ -424,21 +490,6 @@ func ResourceSlo() *schema.Resource { }, }, } - - return &schema.Resource{ - Description: "Creates an SLO.", - - CreateContext: resourceSloCreate, - UpdateContext: resourceSloUpdate, - ReadContext: resourceSloRead, - DeleteContext: resourceSloDelete, - - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - - Schema: sloSchema, - } } func getOrNilString(path string, d *schema.ResourceData) *string { @@ -614,7 +665,6 @@ func getSloFromResourceData(d *schema.ResourceData) (models.Slo, diag.Diagnostic Objective: objective, Settings: &settings, SpaceID: d.Get("space_id").(string), - GroupBy: getOrNilString("group_by", d), } // Explicitly set SLO object id if provided, otherwise we'll use the autogenerated ID from the Kibana API response @@ -622,6 +672,12 @@ func getSloFromResourceData(d *schema.ResourceData) (models.Slo, diag.Diagnostic slo.SloID = *sloID } + if groupBy, ok := d.GetOk("group_by"); ok { + for _, g := range groupBy.([]interface{}) { + slo.GroupBy = append(slo.GroupBy, g.(string)) + } + } + if tags, ok := d.GetOk("tags"); ok { for _, t := range tags.([]interface{}) { slo.Tags = append(slo.Tags, t.(string)) @@ -642,8 +698,17 @@ func resourceSloCreate(ctx context.Context, d *schema.ResourceData, meta interfa return diags } - res, diags := kibana.CreateSlo(ctx, client, slo) + serverVersion, diags := client.ServerVersion(ctx) + if diags.HasError() { + return diags + } + + supportsMultipleGroupBy := serverVersion.GreaterThanOrEqual(SLOSupportsMultipleGroupByMinVersion) + if len(slo.GroupBy) > 1 && !supportsMultipleGroupBy { + return diag.Errorf("multiple group_by fields are not supported in this version of the Elastic Stack. Multiple group_by fields requires %s", SLOSupportsMultipleGroupByMinVersion) + } + res, diags := kibana.CreateSlo(ctx, client, slo, supportsMultipleGroupBy) if diags.HasError() { return diags } @@ -665,8 +730,17 @@ func resourceSloUpdate(ctx context.Context, d *schema.ResourceData, meta interfa return diags } - res, diags := kibana.UpdateSlo(ctx, client, slo) + serverVersion, diags := client.ServerVersion(ctx) + if diags.HasError() { + return diags + } + supportsMultipleGroupBy := serverVersion.GreaterThanOrEqual(SLOSupportsMultipleGroupByMinVersion) + if len(slo.GroupBy) > 1 && !supportsMultipleGroupBy { + return diag.Errorf("multiple group_by fields are not supported in this version of the Elastic Stack. Multiple group_by fields requires %s", SLOSupportsMultipleGroupByMinVersion) + } + + res, diags := kibana.UpdateSlo(ctx, client, slo, supportsMultipleGroupBy) if diags.HasError() { return diags } @@ -837,10 +911,8 @@ func resourceSloRead(ctx context.Context, d *schema.ResourceData, meta interface return diag.FromErr(err) } - if s.GroupBy != nil { - if err := d.Set("group_by", s.GroupBy); err != nil { - return diag.FromErr(err) - } + if err := d.Set("group_by", s.GroupBy); err != nil { + return diag.FromErr(err) } if err := d.Set("slo_id", s.SloID); err != nil { diff --git a/internal/kibana/slo_test.go b/internal/kibana/slo_test.go index 80f84d458..a7d6f8b3d 100644 --- a/internal/kibana/slo_test.go +++ b/internal/kibana/slo_test.go @@ -11,6 +11,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/acctest" "github.com/elastic/terraform-provider-elasticstack/internal/clients" "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana" + kibanaresource "github.com/elastic/terraform-provider-elasticstack/internal/kibana" "github.com/elastic/terraform-provider-elasticstack/internal/versionutils" "github.com/hashicorp/go-version" "github.com/stretchr/testify/require" @@ -36,16 +37,16 @@ func TestAccResourceSlo(t *testing.T) { Steps: []resource.TestStep{ { SkipFunc: versionutils.CheckIfVersionMeetsConstraints(slo8_9Constraints), - Config: getSLOConfig(sloName, "apm_latency_indicator", false, []string{}, ""), + Config: getSLOConfig(sloVars{name: sloName, indicatorType: "apm_latency_indicator"}), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "name", sloName), - resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "slo_id", "fully-sick-slo"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "slo_id", "id-"+sloName), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "description", "fully sick SLO"), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "apm_latency_indicator.0.environment", "production"), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "apm_latency_indicator.0.service", "my-service"), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "apm_latency_indicator.0.transaction_type", "request"), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "apm_latency_indicator.0.transaction_name", "GET /sup/dawg"), - resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "apm_latency_indicator.0.index", "my-index"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "apm_latency_indicator.0.index", "my-index-"+sloName), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "apm_latency_indicator.0.threshold", "500"), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "time_window.0.duration", "7d"), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "time_window.0.type", "rolling"), @@ -60,14 +61,17 @@ func TestAccResourceSlo(t *testing.T) { }, { //check that name can be updated SkipFunc: versionutils.CheckIfVersionMeetsConstraints(slo8_9Constraints), - Config: getSLOConfig(fmt.Sprintf("Updated %s", sloName), "apm_latency_indicator", false, []string{}, ""), + Config: getSLOConfig(sloVars{ + name: fmt.Sprintf("updated-%s", sloName), + indicatorType: "apm_latency_indicator", + }), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "name", fmt.Sprintf("Updated %s", sloName)), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "name", fmt.Sprintf("updated-%s", sloName)), ), }, { //check that settings can be updated from api-computed defaults SkipFunc: versionutils.CheckIfVersionMeetsConstraints(slo8_9Constraints), - Config: getSLOConfig(sloName, "apm_latency_indicator", true, []string{}, ""), + Config: getSLOConfig(sloVars{name: sloName, indicatorType: "apm_latency_indicator", settingsEnabled: true}), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "settings.0.sync_delay", "5m"), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "settings.0.frequency", "5m"), @@ -75,20 +79,20 @@ func TestAccResourceSlo(t *testing.T) { }, { SkipFunc: versionutils.CheckIfVersionMeetsConstraints(slo8_9Constraints), - Config: getSLOConfig(sloName, "apm_availability_indicator", true, []string{}, ""), + Config: getSLOConfig(sloVars{name: sloName, indicatorType: "apm_availability_indicator", settingsEnabled: true}), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "apm_availability_indicator.0.environment", "production"), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "apm_availability_indicator.0.service", "my-service"), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "apm_availability_indicator.0.transaction_type", "request"), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "apm_availability_indicator.0.transaction_name", "GET /sup/dawg"), - resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "apm_availability_indicator.0.index", "my-index"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "apm_availability_indicator.0.index", "my-index-"+sloName), ), }, { SkipFunc: versionutils.CheckIfVersionMeetsConstraints(slo8_9Constraints), - Config: getSLOConfig(sloName, "kql_custom_indicator", true, []string{}, ""), + Config: getSLOConfig(sloVars{name: sloName, indicatorType: "kql_custom_indicator", settingsEnabled: true}), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "kql_custom_indicator.0.index", "my-index"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "kql_custom_indicator.0.index", "my-index-"+sloName), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "kql_custom_indicator.0.good", "latency < 300"), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "kql_custom_indicator.0.total", "*"), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "kql_custom_indicator.0.filter", "labels.groupId: group-0"), @@ -97,9 +101,9 @@ func TestAccResourceSlo(t *testing.T) { }, { SkipFunc: versionutils.CheckIfVersionMeetsConstraints(slo8_10Constraints), - Config: getSLOConfig(sloName, "histogram_custom_indicator", true, []string{}, ""), + Config: getSLOConfig(sloVars{name: sloName, indicatorType: "histogram_custom_indicator", settingsEnabled: true}), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "histogram_custom_indicator.0.index", "my-index"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "histogram_custom_indicator.0.index", "my-index-"+sloName), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "histogram_custom_indicator.0.good.0.field", "test"), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "histogram_custom_indicator.0.good.0.aggregation", "value_count"), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "histogram_custom_indicator.0.good.0.filter", "latency < 300"), @@ -111,9 +115,14 @@ func TestAccResourceSlo(t *testing.T) { }, { SkipFunc: versionutils.CheckIfVersionMeetsConstraints(slo8_10Constraints), - Config: getSLOConfig(sloName, "metric_custom_indicator", true, []string{}, "some.field"), + Config: getSLOConfig(sloVars{ + name: sloName, + indicatorType: "metric_custom_indicator", + settingsEnabled: true, + groupBy: []string{"some.field"}, + }), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.index", "my-index"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.index", "my-index-"+sloName), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.good.0.metrics.0.name", "A"), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.good.0.metrics.0.aggregation", "sum"), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.good.0.metrics.0.field", "processor.processed"), @@ -129,12 +138,18 @@ func TestAccResourceSlo(t *testing.T) { resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.total.0.metrics.1.aggregation", "sum"), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.total.0.metrics.1.field", "processor.accepted"), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.total.0.equation", "A + B"), - resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "group_by", "some.field"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "group_by.#", "1"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "group_by.0", "some.field"), ), }, { SkipFunc: versionutils.CheckIfVersionMeetsConstraints(slo8_10Constraints), - Config: getSLOConfig(sloName, "metric_custom_indicator", true, []string{"tag-1", "another_tag"}, ""), + Config: getSLOConfig(sloVars{ + name: sloName, + indicatorType: "metric_custom_indicator", + settingsEnabled: true, + tags: []string{"tag-1", "another_tag"}, + }), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "tags.0", "tag-1"), resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "tags.1", "another_tag"), @@ -144,6 +159,84 @@ func TestAccResourceSlo(t *testing.T) { }) } +func TestAccResourceSloGroupBy(t *testing.T) { + sloName := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlphaNum) + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: checkResourceSloDestroy, + Steps: []resource.TestStep{ + { + // Create the SLO with the last provider version enforcing single element group_by + ExternalProviders: map[string]resource.ExternalProvider{ + "elasticstack": { + Source: "elastic/elasticstack", + VersionConstraint: "0.11.11", + }, + }, + ProtoV6ProviderFactories: acctest.Providers, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(kibanaresource.SLOSupportsMultipleGroupByMinVersion), + Config: getSLOConfig(sloVars{ + name: sloName, + indicatorType: "metric_custom_indicator", + settingsEnabled: true, + groupBy: []string{"some.field"}, + useSingleElementGroupBy: true, + }), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.index", "my-index-"+sloName), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.good.0.metrics.0.name", "A"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.good.0.metrics.0.aggregation", "sum"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.good.0.metrics.0.field", "processor.processed"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.good.0.metrics.1.name", "B"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.good.0.metrics.1.aggregation", "sum"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.good.0.metrics.1.field", "processor.processed"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.good.0.equation", "A + B"), + + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.total.0.metrics.0.name", "A"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.total.0.metrics.0.aggregation", "sum"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.total.0.metrics.0.field", "processor.accepted"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.total.0.metrics.1.name", "B"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.total.0.metrics.1.aggregation", "sum"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.total.0.metrics.1.field", "processor.accepted"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.total.0.equation", "A + B"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "group_by", "some.field"), + ), + }, + { + ProtoV6ProviderFactories: acctest.Providers, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(kibanaresource.SLOSupportsMultipleGroupByMinVersion), + Config: getSLOConfig(sloVars{ + name: sloName, + indicatorType: "metric_custom_indicator", + settingsEnabled: true, + groupBy: []string{"some.field", "some.other.field"}, + }), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.index", "my-index-"+sloName), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.good.0.metrics.0.name", "A"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.good.0.metrics.0.aggregation", "sum"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.good.0.metrics.0.field", "processor.processed"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.good.0.metrics.1.name", "B"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.good.0.metrics.1.aggregation", "sum"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.good.0.metrics.1.field", "processor.processed"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.good.0.equation", "A + B"), + + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.total.0.metrics.0.name", "A"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.total.0.metrics.0.aggregation", "sum"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.total.0.metrics.0.field", "processor.accepted"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.total.0.metrics.1.name", "B"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.total.0.metrics.1.aggregation", "sum"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.total.0.metrics.1.field", "processor.accepted"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "metric_custom_indicator.0.total.0.equation", "A + B"), + // resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "group_by.#", "2"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "group_by.0", "some.field"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "group_by.1", "some.other.field"), + ), + }, + }, + }) +} + func TestAccResourceSloErrors(t *testing.T) { multipleIndicatorsConfig := ` provider "elasticstack" { @@ -152,7 +245,7 @@ func TestAccResourceSloErrors(t *testing.T) { } resource "elasticstack_elasticsearch_index" "my_index" { - name = "my-index" + name = "my-index-fail" deletion_protection = false } @@ -161,7 +254,7 @@ func TestAccResourceSloErrors(t *testing.T) { description = "multiple indicator fail" histogram_custom_indicator { - index = "my-index" + index = "my-index-fail" good { field = "test" aggregation = "value_count" @@ -176,7 +269,7 @@ func TestAccResourceSloErrors(t *testing.T) { } kql_custom_indicator { - index = "my-index" + index = "my-index-fail" good = "latency < 300" total = "*" filter = "labels.groupId: group-0" @@ -200,7 +293,7 @@ func TestAccResourceSloErrors(t *testing.T) { }` - budgetingMethodFailConfig := getSLOConfig("budgetingMethodFail", "apm_latency_indicator", false, []string{}, "") + budgetingMethodFailConfig := getSLOConfig(sloVars{name: "budgetingmethodfail", indicatorType: "apm_latency_indicator"}) budgetingMethodFailConfig = strings.Replace(budgetingMethodFailConfig, "budgeting_method = \"timeslices\"", "budgeting_method = \"supdawg\"", -1) resource.Test(t, resource.TestCase{ @@ -214,7 +307,7 @@ func TestAccResourceSloErrors(t *testing.T) { }, { SkipFunc: versionutils.CheckIfVersionIsUnsupported(version.Must(version.NewSemver("8.10.0-SNAPSHOT"))), - Config: getSLOConfig("failwhale", "histogram_custom_indicator_agg_fail", false, []string{}, ""), + Config: getSLOConfig(sloVars{name: "failwhale", indicatorType: "histogram_custom_indicator_agg_fail"}), ExpectError: regexp.MustCompile(`expected histogram_custom_indicator.0.good.0.aggregation to be one of \["?value_count"? "?range"?\], got supdawg`), }, { @@ -252,9 +345,18 @@ func checkResourceSloDestroy(s *terraform.State) error { return nil } -func getSLOConfig(name string, indicatorType string, settingsEnabled bool, tags []string, group_by string) string { +type sloVars struct { + name string + indicatorType string + settingsEnabled bool + tags []string + groupBy []string + useSingleElementGroupBy bool +} + +func getSLOConfig(vars sloVars) string { var settings string - if settingsEnabled { + if vars.settingsEnabled { settings = ` settings { sync_delay = "5m" @@ -266,16 +368,23 @@ func getSLOConfig(name string, indicatorType string, settingsEnabled bool, tags } var tagsOption string - if len(tags) != 0 { - tagsJson, _ := json.Marshal(tags) + if len(vars.tags) != 0 { + tagsJson, _ := json.Marshal(vars.tags) tagsOption = "tags = " + string(tagsJson) } else { tagsOption = "" } var groupByOption string - if len(group_by) != 0 { - groupByOption = "group_by = \"" + group_by + "\"" + if len(vars.groupBy) != 0 { + var groupByVal string + if vars.useSingleElementGroupBy { + groupByVal = fmt.Sprintf(`"%s"`, vars.groupBy[0]) + } else { + groupByBytes, _ := json.Marshal(vars.groupBy) + groupByVal = string(groupByBytes) + } + groupByOption = "group_by = " + groupByVal } else { groupByOption = "" } @@ -287,13 +396,13 @@ func getSLOConfig(name string, indicatorType string, settingsEnabled bool, tags } resource "elasticstack_elasticsearch_index" "my_index" { - name = "my-index" + name = "my-index-%s" deletion_protection = false } resource "elasticstack_kibana_slo" "test_slo" { name = "%s" - slo_id = "fully-sick-slo" + slo_id = "id-%s" description = "fully sick SLO" %s @@ -326,43 +435,43 @@ func getSLOConfig(name string, indicatorType string, settingsEnabled bool, tags switch indicatorType { case "apm_latency_indicator": - indicator = ` + indicator = fmt.Sprintf(` apm_latency_indicator { environment = "production" service = "my-service" transaction_type = "request" transaction_name = "GET /sup/dawg" - index = "my-index" + index = "my-index-%s" threshold = 500 } - ` + `, vars.name) case "apm_availability_indicator": - indicator = ` + indicator = fmt.Sprintf(` apm_availability_indicator { environment = "production" service = "my-service" transaction_type = "request" transaction_name = "GET /sup/dawg" - index = "my-index" + index = "my-index-%s" } - ` + `, vars.name) case "kql_custom_indicator": - indicator = ` + indicator = fmt.Sprintf(` kql_custom_indicator { - index = "my-index" + index = "my-index-%s" good = "latency < 300" total = "*" filter = "labels.groupId: group-0" timestamp_field = "custom_timestamp" } - ` + `, vars.name) case "histogram_custom_indicator": - indicator = ` + indicator = fmt.Sprintf(` histogram_custom_indicator { - index = "my-index" + index = "my-index-%s" good { field = "test" aggregation = "value_count" @@ -375,12 +484,12 @@ func getSLOConfig(name string, indicatorType string, settingsEnabled bool, tags filter = "labels.groupId: group-0" timestamp_field = "custom_timestamp" } - ` + `, vars.name) case "histogram_custom_indicator_agg_fail": - indicator = ` + indicator = fmt.Sprintf(` histogram_custom_indicator { - index = "my-index" + index = "my-index-%s" good { field = "test" aggregation = "supdawg" @@ -395,12 +504,12 @@ func getSLOConfig(name string, indicatorType string, settingsEnabled bool, tags filter = "labels.groupId: group-0" timestamp_field = "custom_timestamp" } - ` + `, vars.name) case "metric_custom_indicator": - indicator = ` + indicator = fmt.Sprintf(` metric_custom_indicator { - index = "my-index" + index = "my-index-%s" good { metrics { name = "A" @@ -429,11 +538,12 @@ func getSLOConfig(name string, indicatorType string, settingsEnabled bool, tags equation = "A + B" } } - ` + `, vars.name) } return indicator } - config := fmt.Sprintf(configTemplate, name, getIndicator(indicatorType), settings, groupByOption, tagsOption) + config := fmt.Sprintf(configTemplate, vars.name, vars.name, vars.name, getIndicator(vars.indicatorType), settings, groupByOption, tagsOption) + return config } diff --git a/internal/models/slo.go b/internal/models/slo.go index 295c659bf..2ab58e61e 100644 --- a/internal/models/slo.go +++ b/internal/models/slo.go @@ -14,6 +14,6 @@ type Slo struct { Objective slo.Objective Settings *slo.Settings SpaceID string - GroupBy *string + GroupBy []string Tags []string }