Skip to content

Commit

Permalink
Feat/schedule autoscale (#220)
Browse files Browse the repository at this point in the history
* feat: scheduled autoscale

* test: scheduled autoscale

* fix: autoscale cpu trigger info

* feat: add Timezone info to Autoscale

* chore: update go-tsuruclient package

* fix: lint
  • Loading branch information
crgisch authored Dec 20, 2023
1 parent 65cd8f7 commit 60688b1
Show file tree
Hide file tree
Showing 6 changed files with 295 additions and 23 deletions.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ require (
github.com/antihax/optional v1.0.0
github.com/ghodss/yaml v1.0.0
github.com/iancoleman/orderedmap v0.2.0
github.com/lnquy/cron v1.1.1
github.com/mattn/go-shellwords v1.0.12
github.com/mitchellh/go-wordwrap v1.0.1
github.com/pkg/errors v0.9.1
github.com/pmorie/go-open-service-broker-client v0.0.0-20180330214919-dca737037ce6
github.com/sabhiram/go-gitignore v0.0.0-20171017070213-362f9845770f
github.com/tsuru/gnuflag v0.0.0-20151217162021-86b8c1b864aa
github.com/tsuru/go-tsuruclient v0.0.0-20231031185823-b0abac462f59
github.com/tsuru/go-tsuruclient v0.0.0-20231124151049-7b8d8ea2ee30
github.com/tsuru/tablecli v0.0.0-20190131152944-7ded8a3383c6
github.com/tsuru/tsuru v0.0.0-20231009130140-65592312e508
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=
github.com/lnquy/cron v1.1.1 h1:iaDX1ublgQ9LBhA8l9BVU+FrTE1PPSPAuvAdhgdnXgA=
github.com/lnquy/cron v1.1.1/go.mod h1:hu2Y7H68/8oKk6T4+K4qdbopbnaP4rGltK3ylWiiDss=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
Expand Down Expand Up @@ -733,8 +735,8 @@ github.com/tsuru/config v0.0.0-20201023175036-375aaee8b560 h1:fniQ/BmYAHdnNmY333
github.com/tsuru/config v0.0.0-20201023175036-375aaee8b560/go.mod h1:mj6t8JKWU51GScTT50XRmDj65T5XhTyNvO5FUNV5zS4=
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-20231031185823-b0abac462f59 h1:RgFlupHEAJvIXH6FgtzGQqG8pS6ck3miqEXZN+a4dLs=
github.com/tsuru/go-tsuruclient v0.0.0-20231031185823-b0abac462f59/go.mod h1:BmePxHey9hxrxk0kzTMHFFr7aJWXSxtlrUx6FIeV0Ic=
github.com/tsuru/go-tsuruclient v0.0.0-20231124151049-7b8d8ea2ee30 h1:lPzNgSgTz+27YI5vCwEAGENgzNYb0gNtYcke0WuQsbk=
github.com/tsuru/go-tsuruclient v0.0.0-20231124151049-7b8d8ea2ee30/go.mod h1:BmePxHey9hxrxk0kzTMHFFr7aJWXSxtlrUx6FIeV0Ic=
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-20231009130140-65592312e508 h1:eaMg/uBeTv6B7O+AMTq3OKqNsiA0/kB5Zkgy7ipXgcI=
Expand Down
65 changes: 54 additions & 11 deletions tsuru/client/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"time"

"github.com/ajg/form"
"github.com/lnquy/cron"
"github.com/tsuru/gnuflag"
"github.com/tsuru/go-tsuruclient/pkg/client"
"github.com/tsuru/go-tsuruclient/pkg/tsuru"
Expand Down Expand Up @@ -744,22 +745,52 @@ func (a *app) String(simplified bool) string {
renderServiceInstanceBinds(&buf, a.ServiceInstanceBinds)
}

autoScaleTable := tablecli.NewTable()
autoScaleTable.Headers = tablecli.Row([]string{"Process", "Min", "Max", "Target CPU"})
var autoScaleTables []*tablecli.Table
processes := []string{}

for _, as := range a.AutoScale {
cpu := cpuValue(as.AverageCPU)
autoScaleTable.AddRow(tablecli.Row([]string{
fmt.Sprintf("%s (v%d)", as.Process, as.Version),
strconv.Itoa(int(as.MinUnits)),
strconv.Itoa(int(as.MaxUnits)),
cpu,
}))
autoScaleTable := tablecli.NewTable()
autoScaleTable.LineSeparator = true

processString := fmt.Sprintf(
"Process: %s (v%d), Min Units: %d, Max Units: %d",
as.Process, as.Version, int(as.MinUnits), int(as.MaxUnits),
)
processes = append(processes, processString)

autoScaleTable.Headers = tablecli.Row([]string{
"Triggers",
"Trigger details",
})

if as.AverageCPU != "" {
cpu := cpuValue(as.AverageCPU)
autoScaleTable.AddRow(tablecli.Row([]string{
"CPU",
fmt.Sprintf("Target: %s", cpu),
}))
}

for _, schedule := range as.Schedules {
scheduleInfo := buildScheduleInfo(schedule)
autoScaleTable.AddRow(tablecli.Row([]string{
"Schedule",
scheduleInfo,
}))
}

autoScaleTables = append(autoScaleTables, autoScaleTable)
}

if autoScaleTable.Rows() > 0 {
if len(processes) > 0 {
buf.WriteString("\n")
buf.WriteString("Auto Scale:\n")
buf.WriteString(autoScaleTable.String())
for i, asTable := range autoScaleTables {
buf.WriteString("\n")
buf.WriteString(processes[i])
buf.WriteString("\n")
buf.WriteString(asTable.String())
}
}

if !simplified && (a.Plan.Memory != 0 || a.Plan.CPUMilli != 0) {
Expand Down Expand Up @@ -790,6 +821,18 @@ func (a *app) String(simplified bool) string {
return tplBuffer.String() + buf.String()
}

func buildScheduleInfo(schedule tsuru.AutoScaleSchedule) string {
// Init with default EN locale
exprDesc, _ := cron.NewDescriptor()

startTimeHuman, _ := exprDesc.ToDescription(schedule.Start, cron.Locale_en)
endTimeHuman, _ := exprDesc.ToDescription(schedule.End, cron.Locale_en)

return fmt.Sprintf("Start: %s (%s)\nEnd: %s (%s)\nUnits: %d\nTimezone: %s",
startTimeHuman, schedule.Start, endTimeHuman, schedule.End, schedule.MinReplicas, schedule.Timezone,
)
}

func (a *app) SimpleServicesView() string {
sibs := make([]tsuru.AppServiceInstanceBinds, len(a.ServiceInstanceBinds))
copy(sibs, a.ServiceInstanceBinds)
Expand Down
157 changes: 151 additions & 6 deletions tsuru/client/apps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1606,12 +1606,157 @@ Units [process worker]: 1
+--------+---------+------+------+
Auto Scale:
+--------------+-----+-----+------------+
| Process | Min | Max | Target CPU |
+--------------+-----+-----+------------+
| web (v10) | 1 | 10 | 50% |
| worker (v10) | 2 | 5 | 200% |
+--------------+-----+-----+------------+
Process: web (v10), Min Units: 1, Max Units: 10
+----------+-----------------+
| Triggers | Trigger details |
+----------+-----------------+
| CPU | Target: 50% |
+----------+-----------------+
Process: worker (v10), Min Units: 2, Max Units: 5
+----------+-----------------+
| Triggers | Trigger details |
+----------+-----------------+
| CPU | Target: 200% |
+----------+-----------------+
`
context := cmd.Context{
Stdout: &stdout,
Stderr: &stderr,
}
client := cmd.NewClient(&http.Client{Transport: &cmdtest.Transport{Message: result, Status: http.StatusOK}}, nil, manager)
command := AppInfo{}
command.Flags().Parse(true, []string{"--app", "app1"})
err := command.Run(&context, client)
c.Assert(err, check.IsNil)
c.Assert(stdout.String(), check.Equals, expected)
}

func (s *S) TestAppInfoWithKEDAAutoScale(c *check.C) {
var stdout, stderr bytes.Buffer
result := `{
"name": "app1",
"teamowner": "myteam",
"cname": [
""
],
"ip": "myapp.tsuru.io",
"platform": "php",
"repository": "git@git.com:php.git",
"state": "dead",
"units": [
{
"ID": "app1/0",
"Status": "started",
"ProcessName": "web"
},
{
"ID": "app1/1",
"Status": "started",
"ProcessName": "worker"
}
],
"teams": [
"tsuruteam",
"crane"
],
"owner": "myapp_owner",
"deploys": 7,
"router": "planb",
"autoscale": [
{
"process":"web",
"minUnits":1,
"maxUnits":10,
"averageCPU":"500m",
"version":10,
"schedules": [
{
"minReplicas":2,
"start":"0 6 * * *",
"end":"0 18 * * *",
"timezone":"UTC"
},{
"minReplicas":3,
"start":"0 12 * * *",
"end":"0 15 * * *",
"timezone":"UTC"
}
]
},
{
"process":"worker",
"minUnits":2,
"maxUnits":5,
"averageCPU":"2",
"version":10,
"schedules": [
{
"minReplicas":1,
"start":"0 0 * * *",
"end":"0 6 * * *",
"timezone":"America/Sao_Paulo"
}
]
}
]
}`
expected := `Application: app1
Platform: php
Router: planb
Teams: myteam (owner), tsuruteam, crane
External Addresses: myapp.tsuru.io
Created by: myapp_owner
Deploys: 7
Pool:
Quota: 0/0 units
Units [process web]: 1
+--------+---------+------+------+
| Name | Status | Host | Port |
+--------+---------+------+------+
| app1/0 | started | | |
+--------+---------+------+------+
Units [process worker]: 1
+--------+---------+------+------+
| Name | Status | Host | Port |
+--------+---------+------+------+
| app1/1 | started | | |
+--------+---------+------+------+
Auto Scale:
Process: web (v10), Min Units: 1, Max Units: 10
+----------+---------------------------------+
| Triggers | Trigger details |
+----------+---------------------------------+
| CPU | Target: 50% |
+----------+---------------------------------+
| Schedule | Start: At 06:00 AM (0 6 * * *) |
| | End: At 06:00 PM (0 18 * * *) |
| | Units: 2 |
| | Timezone: UTC |
+----------+---------------------------------+
| Schedule | Start: At 12:00 PM (0 12 * * *) |
| | End: At 03:00 PM (0 15 * * *) |
| | Units: 3 |
| | Timezone: UTC |
+----------+---------------------------------+
Process: worker (v10), Min Units: 2, Max Units: 5
+----------+--------------------------------+
| Triggers | Trigger details |
+----------+--------------------------------+
| CPU | Target: 200% |
+----------+--------------------------------+
| Schedule | Start: At 12:00 AM (0 0 * * *) |
| | End: At 06:00 AM (0 6 * * *) |
| | Units: 1 |
| | Timezone: America/Sao_Paulo |
+----------+--------------------------------+
`
context := cmd.Context{
Expand Down
34 changes: 31 additions & 3 deletions tsuru/client/autoscale.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package client

import (
"context"
"encoding/json"
"fmt"
"strconv"

Expand All @@ -29,13 +30,26 @@ type AutoScaleSet struct {
cmd.AppNameMixIn
fs *gnuflag.FlagSet
autoscale tsuru.AutoScaleSpec
schedules cmd.StringSliceFlag
}

func (c *AutoScaleSet) Info() *cmd.Info {
return &cmd.Info{
Name: "unit-autoscale-set",
Usage: "unit autoscale set [-a/--app appname] [-p/--process processname] [--cpu targetCPU] [--min minUnits] [--max maxUnits]",
Desc: `Sets a unit auto scale configuration.`,
Name: "unit-autoscale-set",
Usage: "unit autoscale set [-a/--app appname] [-p/--process processname] [--cpu targetCPU] [--min minUnits] [--max maxUnits] [--schedule scheduleWindow]",
Desc: `
# Sets an autoscale configuration:
# Based on 50% of CPU utilization with min units 1 and max units 3
unit autoscale set -a my-app --cpu 50% --min 1 --max 3
# Based on a schedule window everyday from 6AM to 6PM UTC
unit autoscale set -a my-app --min 1 --max 3 --schedule '{"minReplicas": 2, "start": "0 6 * * *", "end": "0 18 * * *"}'
# Combining both
unit autoscale set -a my-app --cpu 50% --min 1 --max 3 --schedule '{"minReplicas": 2, "start": "0 6 * * *", "end": "0 18 * * *"}'
# When using more than one trigger (CPU + Schedule as an example), the number of units will be determined by the highest value
`,
MinArgs: 0,
MaxArgs: 0,
}
Expand All @@ -54,6 +68,8 @@ func (c *AutoScaleSet) Flags() *gnuflag.FlagSet {
c.fs.Var((*int32Value)(&c.autoscale.MinUnits), "min", "Minimum Units")

c.fs.Var((*int32Value)(&c.autoscale.MaxUnits), "max", "Maximum Units")

c.fs.Var(&c.schedules, "schedule", "Schedule window to up/down scale. Example: {\"minReplicas\": 2, \"start\": \"0 6 * * *\", \"end\": \"0 18 * * *\"}")
}
return c.fs
}
Expand All @@ -70,6 +86,18 @@ func (c *AutoScaleSet) Run(ctx *cmd.Context, cli *cmd.Client) error {
return err
}

schedules := []tsuru.AutoScaleSchedule{}
for _, scheduleString := range c.schedules {
var autoScaleSchedule tsuru.AutoScaleSchedule
if err = json.Unmarshal([]byte(scheduleString), &autoScaleSchedule); err != nil {
return err
}

schedules = append(schedules, autoScaleSchedule)
}

c.autoscale.Schedules = schedules

_, err = apiClient.AppApi.AutoScaleAdd(context.TODO(), appName, c.autoscale)
if err != nil {
return err
Expand Down
Loading

0 comments on commit 60688b1

Please sign in to comment.