From 3aebde16dd669906721e80dceb9f8bb4d96bff6a Mon Sep 17 00:00:00 2001 From: Ezequiel Lopes Date: Wed, 11 Dec 2024 10:16:00 -0300 Subject: [PATCH] Feat/auto scale behavior (#63) * feat: add scale down behavior to autoscaling configuration * chore: update go-tsuruclient dependency to latest version * feat: enhance scale down functionality in autoscaling configuration * feat: implement flatten scale down functionality for autoscaling * test: add unit tests for flatten scale down functionality * test: add acceptance tests for scale down functionality in autoscaling * chore: add VSCode settings for Go test environment variables * refactor: rename flattenScaleDown --- .vscode/settings.json | 6 + go.mod | 2 +- go.sum | 4 +- .../provider/resource_tsuru_app_autoscale.go | 63 ++++++++- .../resource_tsuru_app_autoscale_flatten.go | 132 ++++++++++++++++++ ...rce_tsuru_app_autoscale_scale_down_test.go | 96 +++++++++++++ .../resource_tsuru_app_autoscale_test.go | 104 ++++++++++++++ 7 files changed, 403 insertions(+), 4 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 internal/provider/resource_tsuru_app_autoscale_flatten.go create mode 100644 internal/provider/resource_tsuru_app_autoscale_scale_down_test.go diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6236a86 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "go.testEnvVars": { + "TF_ACC": "1", + "TF_ACC_TERRAFORM_VERSION": "1.4.4" + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index f7d10fb..8a28a52 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/labstack/echo/v4 v4.9.1 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.9.0 - github.com/tsuru/go-tsuruclient v0.0.0-20240409125509-22a1e08326f4 + github.com/tsuru/go-tsuruclient v0.0.0-20241029183502-e219a905d873 github.com/tsuru/tsuru-client v0.0.0-20240325204824-8c0dc602a5be k8s.io/apimachinery v0.26.2 ) diff --git a/go.sum b/go.sum index b116de8..319b2dd 100644 --- a/go.sum +++ b/go.sum @@ -206,8 +206,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tsuru/gnuflag v0.0.0-20151217162021-86b8c1b864aa h1:JlLQP1xa13a994p/Aau2e3K9xXYaHNoNvTDVIMHSUa4= github.com/tsuru/gnuflag v0.0.0-20151217162021-86b8c1b864aa/go.mod h1:UibOSvkMFKRe/eiwktAPAvQG8L+p8nYsECJvu3Dgw7I= -github.com/tsuru/go-tsuruclient v0.0.0-20240409125509-22a1e08326f4 h1:MGmG6AxKP8XRe7nQqIQR+Tsb5tCzHnYpYk0tiuXVgxY= -github.com/tsuru/go-tsuruclient v0.0.0-20240409125509-22a1e08326f4/go.mod h1:qwh/KJ6ypa2GISRI79XFOHhnSjGOe1cZVPHF3nfrf18= +github.com/tsuru/go-tsuruclient v0.0.0-20241029183502-e219a905d873 h1:Rs3urDCvqLpmGpUKOJNRiOCij/A+EcemdeOaGmGcs/E= +github.com/tsuru/go-tsuruclient v0.0.0-20241029183502-e219a905d873/go.mod h1:qwh/KJ6ypa2GISRI79XFOHhnSjGOe1cZVPHF3nfrf18= github.com/tsuru/tablecli v0.0.0-20190131152944-7ded8a3383c6 h1:1XDdWFAjIbCSG1OjN9v9KdWhuM8UtYlFcfHe/Ldkchk= github.com/tsuru/tablecli v0.0.0-20190131152944-7ded8a3383c6/go.mod h1:ztYpOhW+u1k21FEqp7nZNgpWbr0dUKok5lgGCZi+1AQ= github.com/tsuru/tsuru v0.0.0-20240325190920-410c71393b77 h1:cuWFjNLaemdQZhojqJbb/rOXO97tlcPeLAHg/x+EQGk= diff --git a/internal/provider/resource_tsuru_app_autoscale.go b/internal/provider/resource_tsuru_app_autoscale.go index afb86d2..56259fb 100644 --- a/internal/provider/resource_tsuru_app_autoscale.go +++ b/internal/provider/resource_tsuru_app_autoscale.go @@ -128,6 +128,31 @@ func resourceTsuruApplicationAutoscale() *schema.Resource { Optional: true, AtLeastOneOf: []string{"cpu_average", "schedule", "prometheus"}, }, + "scale_down": { + Type: schema.TypeList, + Description: "Behavior of the auto scale down", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "units": { + Type: schema.TypeInt, + Optional: true, + Description: "Number of units to scale down", + }, + "percentage": { + Type: schema.TypeInt, + Optional: true, + Description: "Percentage of units to scale down", + }, + "stabilization_window": { + Type: schema.TypeInt, + Optional: true, + Description: "Stabilization window in seconds", + }, + }, + }, + Optional: true, + AtLeastOneOf: []string{"cpu_average", "schedule", "prometheus"}, + }, }, } } @@ -167,6 +192,9 @@ func resourceTsuruApplicationAutoscaleSet(ctx context.Context, d *schema.Resourc Process: process, MinUnits: int32(minUnits), MaxUnits: int32(maxUnits), + Behavior: tsuru_client.AutoScaleSpecBehavior{ + ScaleDown: tsuru_client.AutoScaleSpecBehaviorScaleDown{}, + }, } if cpu, ok := d.GetOk("cpu_average"); ok { @@ -186,6 +214,10 @@ func resourceTsuruApplicationAutoscaleSet(ctx context.Context, d *schema.Resourc autoscale.Prometheus = prometheus } } + if m, ok := d.GetOk("scale_down"); ok { + scaleDown := scaleDownFromResourceData(m) + autoscale.Behavior.ScaleDown = scaleDown + } err = resource.RetryContext(ctx, d.Timeout(schema.TimeoutCreate), func() *resource.RetryError { _, err = provider.TsuruClient.AppApi.AutoScaleAdd(ctx, app, autoscale) @@ -225,6 +257,8 @@ func resourceTsuruApplicationAutoscaleRead(ctx context.Context, d *schema.Resour retryCount := 0 maxRetries := 5 + + _, proposed := d.GetChange("scale_down") // autoscale info reflects near realtime err = resource.RetryContext(ctx, d.Timeout(schema.TimeoutCreate), func() *resource.RetryError { retryCount++ @@ -254,7 +288,7 @@ func resourceTsuruApplicationAutoscaleRead(ctx context.Context, d *schema.Resour d.Set("schedule", flattenSchedules(autoscale.Schedules)) d.Set("prometheus", flattenPrometheus(autoscale.Prometheus, d)) - + d.Set("scale_down", flattenScaleDown(autoscale.Behavior.ScaleDown, proposed)) return nil } @@ -394,6 +428,33 @@ func prometheusFromResourceData(meta interface{}) []tsuru_client.AutoScalePromet return prometheus } +func scaleDownFromResourceData(meta interface{}) tsuru_client.AutoScaleSpecBehaviorScaleDown { + scaleDownMeta := meta.([]interface{}) + if len(scaleDownMeta) == 0 { + return tsuru_client.AutoScaleSpecBehaviorScaleDown{} + } + scaleDown := tsuru_client.AutoScaleSpecBehaviorScaleDown{} + for _, iFace := range scaleDownMeta { + sd := iFace.(map[string]interface{}) + if v, ok := sd["percentage"]; ok { + if val, ok := v.(int); ok { + scaleDown.PercentagePolicyValue = int32(val) + } + } + if v, ok := sd["units"]; ok { + if val, ok := v.(int); ok { + scaleDown.UnitsPolicyValue = int32(val) + } + } + if v, ok := sd["stabilization_window"]; ok { + if val, ok := v.(int); ok { + scaleDown.StabilizationWindow = int32(val) + } + } + } + return scaleDown +} + func flattenSchedules(schedules []tsuru_client.AutoScaleSchedule) []interface{} { result := []interface{}{} diff --git a/internal/provider/resource_tsuru_app_autoscale_flatten.go b/internal/provider/resource_tsuru_app_autoscale_flatten.go new file mode 100644 index 0000000..9db2f30 --- /dev/null +++ b/internal/provider/resource_tsuru_app_autoscale_flatten.go @@ -0,0 +1,132 @@ +// Copyright 2024 tsuru authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package provider + +import ( + "fmt" + "reflect" + + tsuru_client "github.com/tsuru/go-tsuruclient/pkg/tsuru" +) + +type flattenScaleDownBehavior struct { + PERCENTAGE_VALUE int32 + PERCENTAGE_LABEL string + STABILIZATION_WINDOW_VALUE int32 + STABILIZATION_WINDOW_LABEL string + UNITS_VALUE int32 + UNITS_LABEL string + ScaleDownRead tsuru_client.AutoScaleSpecBehaviorScaleDown + Proposed interface{} +} + +func flattenScaleDown(scaleDownRead tsuru_client.AutoScaleSpecBehaviorScaleDown, proposed interface{}) interface{} { + fsd := &flattenScaleDownBehavior{ + PERCENTAGE_VALUE: 10, + PERCENTAGE_LABEL: "percentage", + STABILIZATION_WINDOW_VALUE: 300, + STABILIZATION_WINDOW_LABEL: "stabilization_window", + UNITS_VALUE: 3, + UNITS_LABEL: "units", + ScaleDownRead: scaleDownRead, + Proposed: proposed, + } + return fsd.execute() +} + +func (fsd *flattenScaleDownBehavior) execute() interface{} { + if fsd.ScaleDownRead == (tsuru_client.AutoScaleSpecBehaviorScaleDown{}) { + return nil + } + proposedList, err := fsd.convertToMapSlice(fsd.Proposed) + if err != nil { + return []map[string]interface{}{{ + "percentage": fsd.ScaleDownRead.PercentagePolicyValue, + "stabilization_window": fsd.ScaleDownRead.StabilizationWindow, + "units": fsd.ScaleDownRead.UnitsPolicyValue, + }} + } + if value, ok := fsd.noInputParameters(proposedList); ok { + return value + } + return fsd.withInputParameters(proposedList) +} + +func (fsd *flattenScaleDownBehavior) withInputParameters(proposedList []map[string]interface{}) (value []map[string]interface{}) { + scaleDownCurrent := []map[string]interface{}{{}} + percentage, ok := fsd.findScaleDownInProposedList(proposedList, fsd.PERCENTAGE_LABEL) + if ok && percentage != 0 || fsd.ScaleDownRead.PercentagePolicyValue != int32(fsd.PERCENTAGE_VALUE) { + scaleDownCurrent[0][fsd.PERCENTAGE_LABEL] = fsd.ScaleDownRead.PercentagePolicyValue + } + stabilizationWindow, ok := fsd.findScaleDownInProposedList(proposedList, fsd.STABILIZATION_WINDOW_LABEL) + if ok && stabilizationWindow != 0 || fsd.ScaleDownRead.StabilizationWindow != int32(fsd.STABILIZATION_WINDOW_VALUE) { + scaleDownCurrent[0][fsd.STABILIZATION_WINDOW_LABEL] = fsd.ScaleDownRead.StabilizationWindow + } + units, ok := fsd.findScaleDownInProposedList(proposedList, fsd.UNITS_LABEL) + if ok && units != 0 || fsd.ScaleDownRead.UnitsPolicyValue != int32(fsd.UNITS_VALUE) { + scaleDownCurrent[0][fsd.UNITS_LABEL] = fsd.ScaleDownRead.UnitsPolicyValue + } + return scaleDownCurrent +} + +func (fsd *flattenScaleDownBehavior) noInputParameters(proposedList []map[string]interface{}) (value interface{}, ok bool) { + if len(proposedList) != 0 { + return nil, false + } + scaleDownCurrent := []map[string]interface{}{{}} + if fsd.ScaleDownRead.PercentagePolicyValue != fsd.PERCENTAGE_VALUE { + scaleDownCurrent[0][fsd.PERCENTAGE_LABEL] = fsd.ScaleDownRead.PercentagePolicyValue + } + if fsd.ScaleDownRead.StabilizationWindow != fsd.STABILIZATION_WINDOW_VALUE { + scaleDownCurrent[0][fsd.STABILIZATION_WINDOW_LABEL] = fsd.ScaleDownRead.StabilizationWindow + } + if fsd.ScaleDownRead.UnitsPolicyValue != fsd.UNITS_VALUE { + scaleDownCurrent[0][fsd.UNITS_LABEL] = fsd.ScaleDownRead.UnitsPolicyValue + } + if fsd.isScaleDownEmpty(scaleDownCurrent) { + return nil, true + } + return scaleDownCurrent, true +} + +func (fsd *flattenScaleDownBehavior) findScaleDownInProposedList(proposedList []map[string]interface{}, key string) (value int, ok bool) { + for _, item := range proposedList { + if v, ok := item[key]; ok { + return v.(int), true + } + } + return 0, false +} + +func (fsd *flattenScaleDownBehavior) convertToMapSlice(input interface{}) ([]map[string]interface{}, error) { + var result []map[string]interface{} + if reflect.TypeOf(input).Kind() != reflect.Slice { + return nil, fmt.Errorf("scale down: invalid input type, slice expected") + } + for _, item := range input.([]interface{}) { + if mapItem, ok := item.(map[string]interface{}); ok { + result = append(result, mapItem) + } else { + return []map[string]interface{}{}, nil + } + } + return result, nil +} + +func (fsd *flattenScaleDownBehavior) isScaleDownEmpty(param []map[string]interface{}) bool { + if len(param) != 1 { + return false + } + if _, ok := param[0][fsd.PERCENTAGE_LABEL]; ok { + return false + } + if _, ok := param[0][fsd.STABILIZATION_WINDOW_LABEL]; ok { + return false + } + if _, ok := param[0][fsd.UNITS_LABEL]; ok { + return false + } + return true +} diff --git a/internal/provider/resource_tsuru_app_autoscale_scale_down_test.go b/internal/provider/resource_tsuru_app_autoscale_scale_down_test.go new file mode 100644 index 0000000..dcfce02 --- /dev/null +++ b/internal/provider/resource_tsuru_app_autoscale_scale_down_test.go @@ -0,0 +1,96 @@ +// Copyright 2024 tsuru authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package provider + +import ( + "testing" + + "github.com/stretchr/testify/assert" + tsuru_client "github.com/tsuru/go-tsuruclient/pkg/tsuru" +) + +func TestFluentDown(t *testing.T) { + assert := assert.New(t) + tests := []struct { + scaleDownRead tsuru_client.AutoScaleSpecBehaviorScaleDown + scaleDownInput interface{} + expected interface{} + }{ + { + scaleDownRead: tsuru_client.AutoScaleSpecBehaviorScaleDown{ + UnitsPolicyValue: 3, + PercentagePolicyValue: 10, + StabilizationWindow: 300, + }, + scaleDownInput: []interface{}{}, + expected: nil, + }, + + { + scaleDownRead: tsuru_client.AutoScaleSpecBehaviorScaleDown{ + UnitsPolicyValue: 3, + PercentagePolicyValue: 10, + StabilizationWindow: 300, + }, + scaleDownInput: []interface{}{ + map[string]interface{}{"units": 3}, + }, + expected: []map[string]interface{}{{ + "units": int32(3), + }}, + }, + { + scaleDownRead: tsuru_client.AutoScaleSpecBehaviorScaleDown{ + UnitsPolicyValue: 3, + PercentagePolicyValue: 10, + StabilizationWindow: 300, + }, + scaleDownInput: []interface{}{ + map[string]interface{}{"units": 3}, + map[string]interface{}{"stabilization_window": 300}, + map[string]interface{}{"percentage": 10}, + }, + expected: []map[string]interface{}{{ + "units": int32(3), + "stabilization_window": int32(300), + "percentage": int32(10), + }}, + }, + { + scaleDownRead: tsuru_client.AutoScaleSpecBehaviorScaleDown{ + UnitsPolicyValue: 21, + PercentagePolicyValue: 21, + StabilizationWindow: 21, + }, + scaleDownInput: []interface{}{ + map[string]interface{}{"units": 3}, + map[string]interface{}{"stabilization_window": 300}, + map[string]interface{}{"percentage": 10}, + }, + expected: []map[string]interface{}{{ + "units": int32(21), + "stabilization_window": int32(21), + "percentage": int32(21), + }}, + }, + { + scaleDownRead: tsuru_client.AutoScaleSpecBehaviorScaleDown{ + UnitsPolicyValue: 21, + PercentagePolicyValue: 21, + StabilizationWindow: 21, + }, + scaleDownInput: []interface{}{}, + expected: []map[string]interface{}{{ + "units": int32(21), + "stabilization_window": int32(21), + "percentage": int32(21), + }}, + }, + } + for _, test := range tests { + readToDiff := flattenScaleDown(test.scaleDownRead, test.scaleDownInput) + assert.Equal(test.expected, readToDiff) + } +} diff --git a/internal/provider/resource_tsuru_app_autoscale_test.go b/internal/provider/resource_tsuru_app_autoscale_test.go index 8b206ff..cc04b2b 100644 --- a/internal/provider/resource_tsuru_app_autoscale_test.go +++ b/internal/provider/resource_tsuru_app_autoscale_test.go @@ -51,6 +51,9 @@ func TestAccResourceTsuruAppAutoscalePercentage(t *testing.T) { MinUnits: 3, MaxUnits: 10, AverageCPU: "800m", + Behavior: tsuru.AutoScaleSpecBehavior{ + ScaleDown: tsuru.AutoScaleSpecBehaviorScaleDown{}, + }, }}) } return c.JSON(http.StatusOK, nil) @@ -715,3 +718,104 @@ func TestAccTsuruAutoscaleSetShouldErrorWithoutCPUOrScheduleOrPrometheus(t *test }, }) } + +func TestAccResourceTsuruAppAutoscaleScaleDown(t *testing.T) { + fakeServer := echo.New() + iterationCount := 0 + fakeServer.GET("/1.0/apps/:name", func(c echo.Context) error { + name := c.Param("name") + if name != "app01" { + return nil + } + + return c.JSON(http.StatusOK, &tsuru.App{ + Name: name, + Description: "my beautiful application", + TeamOwner: "myteam", + Teams: []string{ + "mysupport-team", + "mysponsors", + }, + Cluster: "my-cluster-01", + Pool: "my-pool", + Provisioner: "kubernetes", + Deploys: 2, + }) + + }) + + fakeServer.GET("/1.9/apps/:app/units/autoscale", func(c echo.Context) error { + if iterationCount == 1 { + return c.JSON(http.StatusOK, []tsuru.AutoScaleSpec{{ + Process: "web", + MinUnits: 3, + MaxUnits: 10, + AverageCPU: "800m", + Behavior: tsuru.AutoScaleSpecBehavior{ + ScaleDown: tsuru.AutoScaleSpecBehaviorScaleDown{ + StabilizationWindow: 80, + UnitsPolicyValue: 5, + PercentagePolicyValue: 15, + }, + }, + }}) + } + return c.JSON(http.StatusOK, nil) + }) + + fakeServer.POST("/1.9/apps/:app/units/autoscale", func(c echo.Context) error { + autoscale := tsuru.AutoScaleSpec{} + c.Bind(&autoscale) + assert.Equal(t, "web", autoscale.Process) + iterationCount++ + return c.JSON(http.StatusOK, map[string]interface{}{"ok": "true"}) + }) + + fakeServer.DELETE("/1.9/apps/:app/units/autoscale", func(c echo.Context) error { + p := c.QueryParam("process") + assert.Equal(t, "web", p) + return c.NoContent(http.StatusNoContent) + }) + + fakeServer.HTTPErrorHandler = func(err error, c echo.Context) { + t.Errorf("methods=%s, path=%s, err=%s", c.Request().Method, c.Path(), err.Error()) + } + server := httptest.NewServer(fakeServer) + os.Setenv("TSURU_TARGET", server.URL) + + resourceName := "tsuru_app_autoscale.autoscale" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccResourceTsuruAppAutoscale_scaleDown(), + Check: resource.ComposeAggregateTestCheckFunc( + testAccResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "app", "app01"), + resource.TestCheckResourceAttr(resourceName, "scale_down.0.percentage", "15"), + resource.TestCheckResourceAttr(resourceName, "scale_down.0.stabilization_window", "80"), + resource.TestCheckResourceAttr(resourceName, "scale_down.0.units", "5"), + ), + }, + }, + }) +} + +func testAccResourceTsuruAppAutoscale_scaleDown() string { + return ` + resource "tsuru_app_autoscale" "autoscale" { + app = "app01" + process = "web" + min_units = 3 + max_units = 10 + cpu_average = "800m" + scale_down { + units = 5 + percentage = 15 + stabilization_window = 80 + } + } +` +}