From 59f8abba89860e5e0399dddab6a481fa3dcfba59 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 26 Jan 2022 16:19:39 +0200 Subject: [PATCH] Add PredictKube scaler (#2418) Signed-off-by: Daniel Yavorovych Signed-off-by: alex60217101990 --- CHANGELOG.md | 1 + go.mod | 21 +- go.sum | 70 ++- .../authentication/authentication_helpers.go | 158 ++++++ .../authentication/authentication_types.go | 33 ++ pkg/scalers/predictkube_scaler.go | 483 ++++++++++++++++++ pkg/scalers/predictkube_scaler_test.go | 222 ++++++++ pkg/scalers/prometheus_scaler.go | 117 +---- pkg/scalers/prometheus_scaler_test.go | 6 +- pkg/scaling/scale_handler.go | 2 + tests/scalers/predictkube.test.ts | 235 +++++++++ 11 files changed, 1247 insertions(+), 101 deletions(-) create mode 100644 pkg/scalers/authentication/authentication_helpers.go create mode 100644 pkg/scalers/predictkube_scaler.go create mode 100644 pkg/scalers/predictkube_scaler_test.go create mode 100644 tests/scalers/predictkube.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 70fa6513062..a9f361b301b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - Add New Relic Scaler ([#2387](https://github.com/kedacore/keda/pull/2387)) - Add ActiveMQ Scaler ([#2305](https://github.com/kedacore/keda/pull/2305)) - Add New Datadog Scaler ([#2354](https://github.com/kedacore/keda/pull/2354)) +- Add PredictKube Scaler ([#2418](https://github.com/kedacore/keda/pull/2418)) ### Improvements diff --git a/go.mod b/go.mod index 4f1fbc8d19d..2e34211d1fa 100644 --- a/go.mod +++ b/go.mod @@ -18,9 +18,12 @@ require ( github.com/Shopify/sarama v1.31.0 github.com/aws/aws-sdk-go v1.42.40 github.com/denisenkom/go-mssqldb v0.12.0 + github.com/dysnix/predictkube-libs v0.0.0-20220125103715-5502104557b3 + github.com/dysnix/predictkube-proto v0.0.0-20211223141524-d309509b6b5f github.com/elastic/go-elasticsearch/v7 v7.16.0 github.com/go-logr/logr v1.2.2 github.com/go-playground/assert/v2 v2.0.1 + github.com/go-playground/validator/v10 v10.9.0 github.com/go-redis/redis/v8 v8.11.4 github.com/go-sql-driver/mysql v1.6.0 github.com/gocql/gocql v0.0.0-20211222173705-d73e6b1002a7 @@ -35,14 +38,17 @@ require ( github.com/newrelic/newrelic-client-go v0.70.0 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.18.0 + github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.12.0 + github.com/prometheus/common v0.32.1 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 github.com/robfig/cron/v3 v3.0.1 github.com/streadway/amqp v1.0.0 github.com/stretchr/testify v1.7.0 github.com/tidwall/gjson v1.13.0 github.com/xdg/scram v1.0.5 + github.com/xhit/go-str2duration/v2 v2.0.0 go.mongodb.org/mongo-driver v1.8.2 google.golang.org/api v0.65.0 google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 @@ -87,6 +93,7 @@ require ( github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/andybalholm/brotli v1.0.3 // indirect github.com/armon/go-metrics v0.3.9 // indirect github.com/armon/go-radix v1.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -107,13 +114,15 @@ require ( github.com/emicklei/go-restful-swagger12 v0.0.0-20201014110547-68ccff494617 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect - github.com/fatih/color v1.7.0 // indirect + github.com/fatih/color v1.9.0 // indirect github.com/felixge/httpsnoop v1.0.1 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/go-logr/zapr v1.2.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-stack/stack v1.8.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.2.0 // indirect @@ -126,6 +135,7 @@ require ( github.com/google/uuid v1.3.0 // indirect github.com/googleapis/gax-go/v2 v2.1.1 // indirect github.com/googleapis/gnostic v0.5.5 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect @@ -159,10 +169,12 @@ require ( github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.13.6 // indirect + github.com/leodido/go-urn v1.2.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-ieproxy v0.0.1 // indirect github.com/mattn/go-isatty v0.0.12 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect @@ -172,12 +184,13 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/nxadm/tail v1.4.8 // indirect github.com/oklog/run v1.0.0 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect @@ -186,6 +199,10 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect + github.com/ulikunitz/unixtime v0.1.2 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.31.0 // indirect + github.com/wagslane/go-password-validator v0.3.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.0.2 // indirect github.com/xdg-go/stringprep v1.0.2 // indirect diff --git a/go.sum b/go.sum index 48a8c57a5ad..b3182fdc25e 100644 --- a/go.sum +++ b/go.sum @@ -153,6 +153,9 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/andybalholm/brotli v1.0.3 h1:fpcw+r1N1h0Poc1F/pHbW40cUm/lMEQslZtCkBQ0UnM= +github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -184,6 +187,8 @@ github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q= +github.com/brianvoe/gofakeit/v6 v6.9.0/go.mod h1:palrJUk4Fyw38zIFB/uBZqsgzW5VsNllhHKKwAebzew= +github.com/bxcodec/faker/v3 v3.6.0/go.mod h1:gF31YgnMSMKgkvl+fyEo1xuSMbEuieyqfeslGYFjneM= github.com/c2h5oh/datasize v0.0.0-20171227191756-4eba002a5eae/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M= github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= @@ -250,6 +255,10 @@ github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5O github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dysnix/predictkube-libs v0.0.0-20220125103715-5502104557b3 h1:xn8a2rMA67PqQdvSzKt0KQn7hLZc2UVvyH15rKFg2f0= +github.com/dysnix/predictkube-libs v0.0.0-20220125103715-5502104557b3/go.mod h1:WrLfDUxV7bb1OiF6LFeXPO45FlPcHdG7LIQov/JPR2E= +github.com/dysnix/predictkube-proto v0.0.0-20211223141524-d309509b6b5f h1:56GoyLUD9Z3+Ko0iC8hGPq2RPvjceQEdbio78i5mhvQ= +github.com/dysnix/predictkube-proto v0.0.0-20211223141524-d309509b6b5f/go.mod h1:zTsQdEyzxs3OHHtrjf8WpmexujIMTYyCVz/38VCt0uo= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-resiliency v1.2.0 h1:v7g92e/KSN71Rq7vSThKaWIq68fL4YHvWyiUKorFR1Q= github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= @@ -286,10 +295,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQL github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fasthttp/router v1.4.4/go.mod h1:TiyF2kc+mogKcTxqkhUbiXpwklouv5dN58A0ZUo8J6s= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/favadi/protoc-go-inject-tag v1.3.0/go.mod h1:SSkUBgfqw2IJ2p7NPNKWk0Idwxt/qIt2LQgFPUgRGtc= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= @@ -332,6 +344,7 @@ github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTg github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/zapr v0.4.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= github.com/go-logr/zapr v1.2.0 h1:n4JnPI1T3Qq1SFEi/F8rwLrZERp2bso19PJZDB9dayk= github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -347,12 +360,15 @@ github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyr github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A= +github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= @@ -606,6 +622,7 @@ github.com/influxdata/influxdb-client-go/v2 v2.7.0/go.mod h1:Y/0W1+TZir7ypoQZYd2 github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= github.com/influxdata/tdigest v0.0.0-20180711151920-a7d76c6f093a/go.mod h1:9GkyshztGufsdPQWjH+ifgnIr3xNUL5syI70g2dzU1o= +github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= @@ -621,6 +638,8 @@ github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJk github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.3/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -652,6 +671,7 @@ github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQL github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -669,8 +689,9 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= -github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= @@ -696,8 +717,11 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -740,6 +764,7 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/newrelic/newrelic-client-go v0.70.0 h1:QNt5OP5h78OUvfo6ewdb2JfzL6XwNR+GcmTORZcQ7bE= @@ -751,6 +776,8 @@ github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+ github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -765,6 +792,7 @@ github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGV github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.18.0 h1:ngbYoRctxjl8SiF7XgP0NxBFbfHcg3wfHMMaFHWwMTM= @@ -778,11 +806,14 @@ github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144T github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -835,13 +866,15 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -908,10 +941,18 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ulikunitz/unixtime v0.1.2 h1:X28zmTs0BODKZs7tgEC+WCwyV53fqgmRwwFpVKGDmso= +github.com/ulikunitz/unixtime v0.1.2/go.mod h1:saexy7bPPO+LTD3J5HtEFSCxeDuHb0TJ3Dx8PKXOa6c= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.31.0 h1:lrauRLII19afgCs2fnWRJ4M5IkV0lo2FqA61uGkNBfE= +github.com/valyala/fasthttp v1.31.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I= +github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2 h1:akYIkZ28e6A96dkWNJQu3nmCzH3YfwMPQExUYDaRv7w= @@ -922,6 +963,8 @@ github.com/xdg/scram v1.0.5 h1:TuS0RFmt5Is5qm9Tm2SoD89OPqe4IRiFtyFY4iwWXsw= github.com/xdg/scram v1.0.5/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v1.0.3 h1:cmL5Enob4W83ti/ZHuZLuKD/xqJfus4fVPwE+/BDm+4= github.com/xdg/stringprep v1.0.3/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +github.com/xhit/go-str2duration/v2 v2.0.0 h1:uFtk6FWB375bP7ewQl+/1wBcn840GPhnySOdcz/okPE= +github.com/xhit/go-str2duration/v2 v2.0.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= @@ -1018,6 +1061,7 @@ golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -1110,6 +1154,7 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -1117,6 +1162,7 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211104170005-ce137452f963/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -1226,12 +1272,14 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1434,6 +1482,7 @@ google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEc google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211016002631-37fc39342514/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211104193956-4c6863e31247/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= @@ -1526,6 +1575,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.22.4/go.mod h1:1aeVC+pe9ZmvKZban/gW4QPra7PRoTEssyc922qCAkk= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1536,34 +1586,41 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.22.0/go.mod h1:0AoXXqst47OI/L0oGKq9DG61dvGRPXs7X4/B7KyjBCU= +k8s.io/api v0.22.2/go.mod h1:y3ydYpLJAaDI+BbSe2xmGcqxiWHmWjkEeIbiwHvnPR8= k8s.io/api v0.22.5/go.mod h1:mEhXyLaSD1qTOf40rRiKXkc+2iCem09rWLlFwhCEiAs= k8s.io/api v0.23.0/go.mod h1:8wmDdLBHBNxtOIytwLstXt5E9PddnZb0GaMcqsvDBpg= k8s.io/api v0.23.2 h1:62cpzreV3dCuj0hqPi8r4dyWh48ogMcyh+ga9jEGij4= k8s.io/api v0.23.2/go.mod h1:sYuDb3flCtRPI8ghn6qFrcK5ZBu2mhbElxRE95qpwlI= +k8s.io/apiextensions-apiserver v0.22.2/go.mod h1:2E0Ve/isxNl7tWLSUDgi6+cmwHi5fQRdwGVCxbC+KFA= k8s.io/apiextensions-apiserver v0.22.5/go.mod h1:tIXeZ0BrDxUb1PoAz+tgOz43Zi1Bp4BEEqVtUccMJbE= k8s.io/apiextensions-apiserver v0.23.0 h1:uii8BYmHYiT2ZTAJxmvc3X8UhNYMxl2A0z0Xq3Pm+WY= k8s.io/apiextensions-apiserver v0.23.0/go.mod h1:xIFAEEDlAZgpVBl/1VSjGDmLoXAWRG40+GsWhKhAxY4= k8s.io/apimachinery v0.22.0/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= +k8s.io/apimachinery v0.22.2/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= k8s.io/apimachinery v0.22.5/go.mod h1:xziclGKwuuJ2RM5/rSFQSYAj0zdbci3DH8kj+WvyN0U= k8s.io/apimachinery v0.23.0/go.mod h1:fFCTTBKvKcwTPFzjlcxp91uPFZr+JA0FubU4fLzzFYc= k8s.io/apimachinery v0.23.2 h1:dBmjCOeYBdg2ibcQxMuUq+OopZ9fjfLIR5taP/XKeTs= k8s.io/apimachinery v0.23.2/go.mod h1:zDqeV0AK62LbCI0CI7KbWCAYdLg+E+8UXJ0rIz5gmS8= k8s.io/apiserver v0.22.0/go.mod h1:04kaIEzIQrTGJ5syLppQWvpkLJXQtJECHmae+ZGc/nc= +k8s.io/apiserver v0.22.2/go.mod h1:vrpMmbyjWrgdyOvZTSpsusQq5iigKNWv9o9KlDAbBHI= k8s.io/apiserver v0.22.5/go.mod h1:s2WbtgZAkTKt679sYtSudEQrTGWUSQAPe6MupLnlmaQ= k8s.io/apiserver v0.23.0/go.mod h1:Cec35u/9zAepDPPFyT+UMrgqOCjgJ5qtfVJDxjZYmt4= k8s.io/apiserver v0.23.2 h1:vGFCojjwSLyunapA7FWuzyekml/s0nAsoh4iBpzWzOs= k8s.io/apiserver v0.23.2/go.mod h1:Kdt8gafkPev9Gfh+H6lCPbmRu42f7BfhOfHKKa3dtyU= k8s.io/client-go v0.22.0/go.mod h1:GUjIuXR5PiEv/RVK5OODUsm6eZk7wtSWZSaSJbpFdGg= +k8s.io/client-go v0.22.2/go.mod h1:sAlhrkVDf50ZHx6z4K0S40wISNTarf1r800F+RlCF6U= k8s.io/client-go v0.22.5/go.mod h1:cs6yf/61q2T1SdQL5Rdcjg9J1ElXSwbjSrW2vFImM4Y= k8s.io/client-go v0.23.0/go.mod h1:hrDnpnK1mSr65lHHcUuIZIXDgEbzc7/683c6hyG4jTA= k8s.io/client-go v0.23.2 h1:BNbOcxa99jxHH8mM1cPKGIrrKRnCSAfAtyonYGsbFtE= k8s.io/client-go v0.23.2/go.mod h1:k3YbsWg6GWdHF1THHTQP88X9RhB1DWPo3Dq7KfU/D1c= k8s.io/code-generator v0.22.0/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o= +k8s.io/code-generator v0.22.2/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o= k8s.io/code-generator v0.22.5/go.mod h1:sbdWCOVob+KaQ5O7xs8PNNaCTpbWVqNgA6EPwLOmRNk= k8s.io/code-generator v0.23.0/go.mod h1:vQvOhDXhuzqiVfM/YHp+dmg10WDZCchJVObc9MvowsE= k8s.io/code-generator v0.23.2 h1:KqIwRDwEZpjA0x7gr4S8LQgyT6iFnR57lksnxBha0g0= k8s.io/code-generator v0.23.2/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= k8s.io/component-base v0.22.0/go.mod h1:SXj6Z+V6P6GsBhHZVbWCw9hFjUdUYnJerlhhPnYCBCg= +k8s.io/component-base v0.22.2/go.mod h1:5Br2QhI9OTe79p+TzPe9JKNQYvEKbq9rTJDWllunGug= k8s.io/component-base v0.22.5/go.mod h1:VK3I+TjuF9eaa+Ln67dKxhGar5ynVbwnGrUiNF4MqCI= k8s.io/component-base v0.23.0/go.mod h1:DHH5uiFvLC1edCpvcTDV++NKULdYYU6pR9Tt3HIKMKI= k8s.io/component-base v0.23.2 h1:dAYmUhWIBWO762etTjBEEKtYYHi5CoQInSLtK6LM1Zs= @@ -1605,6 +1662,7 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyz sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.25/go.mod h1:Mlj9PNLmG9bZ6BHFwFKDo5afkpWyUISkb9Me0GnK66I= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.27 h1:KQOkVzXrLNb0EP6W0FD6u3CCPAwgXFYwZitbj7K0P0Y= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.27/go.mod h1:tq2nT0Kx7W+/f2JVE+zxYtUhdjuELJkVpNz+x/QN5R4= +sigs.k8s.io/controller-runtime v0.10.2/go.mod h1:CQp8eyUQZ/Q7PJvnIrB6/hgfTC1kBkGylwsLgOQi1WY= sigs.k8s.io/controller-runtime v0.11.0 h1:DqO+c8mywcZLFJWILq4iktoECTyn30Bkj0CwgqMpZWQ= sigs.k8s.io/controller-runtime v0.11.0/go.mod h1:KKwLiTooNGu+JmLZGn9Sl3Gjmfj66eMbCQznLP5zcqA= sigs.k8s.io/custom-metrics-apiserver v1.22.0 h1:nRrRRCq46m3y6lCp/6rfptPjX0eGsF88s66vt9TWgac= diff --git a/pkg/scalers/authentication/authentication_helpers.go b/pkg/scalers/authentication/authentication_helpers.go new file mode 100644 index 00000000000..fd8b260172f --- /dev/null +++ b/pkg/scalers/authentication/authentication_helpers.go @@ -0,0 +1,158 @@ +package authentication + +import ( + "crypto/tls" + "errors" + "fmt" + "net" + "net/http" + "strings" + "time" + + pConfig "github.com/prometheus/common/config" + + libs "github.com/dysnix/predictkube-libs/external/configs" + "github.com/dysnix/predictkube-libs/external/http_transport" + + kedautil "github.com/kedacore/keda/v2/pkg/util" +) + +const ( + authModesKey = "authModes" +) + +func GetAuthConfigs(triggerMetadata, authParams map[string]string) (out *AuthMeta, err error) { + out = &AuthMeta{} + + authModes, ok := triggerMetadata[authModesKey] + // no authMode specified + if !ok { + return nil, nil + } + + authTypes := strings.Split(authModes, ",") + for _, t := range authTypes { + authType := Type(strings.TrimSpace(t)) + + switch authType { + case BearerAuthType: + if len(authParams["bearerToken"]) == 0 { + return nil, errors.New("no bearer token provided") + } + if out.EnableBasicAuth { + return nil, errors.New("beare and basic authentication can not be set both") + } + + out.BearerToken = authParams["bearerToken"] + out.EnableBearerAuth = true + case BasicAuthType: + if len(authParams["username"]) == 0 { + return nil, errors.New("no username given") + } + if out.EnableBearerAuth { + return nil, errors.New("beare and basic authentication can not be set both") + } + + out.Username = authParams["username"] + // password is optional. For convenience, many application implement basic auth with + // username as apikey and password as empty + out.Password = authParams["password"] + out.EnableBasicAuth = true + case TLSAuthType: + if len(authParams["cert"]) == 0 { + return nil, errors.New("no cert given") + } + out.Cert = authParams["cert"] + + if len(authParams["key"]) == 0 { + return nil, errors.New("no key given") + } + + out.Key = authParams["key"] + out.EnableTLS = true + default: + return nil, fmt.Errorf("err incorrect value for authMode is given: %s", t) + } + } + + if len(authParams["ca"]) > 0 { + out.CA = authParams["ca"] + } + + return out, err +} + +func CreateHTTPRoundTripper(roundTripperType TransportType, auth *AuthMeta, conf ...*HTTPTransport) (rt http.RoundTripper, err error) { + tlsConfig := &tls.Config{InsecureSkipVerify: false} + if auth != nil && (auth.CA != "" || auth.EnableTLS) { + tlsConfig, err = kedautil.NewTLSConfig( + auth.Cert, + auth.Key, + auth.CA, + ) + if err != nil || tlsConfig == nil { + return nil, fmt.Errorf("error creating the TLS config: %s", err) + } + } + + switch roundTripperType { + case NetHTTP: + // from official github.com/prometheus/client_golang/api package + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 10 * time.Second, + TLSClientConfig: tlsConfig, + }, nil + case FastHTTP: + // default configs + httpConf := &libs.HTTPTransport{ + MaxIdleConnDuration: 10, + ReadTimeout: time.Second * 15, + WriteTimeout: time.Second * 15, + } + + if len(conf) > 0 { + httpConf = &libs.HTTPTransport{ + MaxIdleConnDuration: conf[0].MaxIdleConnDuration, + ReadTimeout: conf[0].ReadTimeout, + WriteTimeout: conf[0].WriteTimeout, + } + } + + var roundTripper http.RoundTripper + if roundTripper, err = http_transport.NewHttpTransport( + libs.SetTransportConfigs(httpConf), + libs.SetTLS(tlsConfig), + ); err != nil { + return nil, fmt.Errorf("error creating fast http round tripper: %s", err) + } + + if auth != nil { + if auth.EnableBasicAuth { + rt = pConfig.NewBasicAuthRoundTripper( + auth.Username, + pConfig.Secret(auth.Password), + "", roundTripper, + ) + } + + if auth.EnableBearerAuth { + rt = pConfig.NewAuthorizationCredentialsRoundTripper( + "Bearer", + pConfig.Secret(auth.BearerToken), + roundTripper, + ) + } + } else { + rt = roundTripper + } + + return rt, nil + } + + return rt, nil +} diff --git a/pkg/scalers/authentication/authentication_types.go b/pkg/scalers/authentication/authentication_types.go index 6764af42a9a..13f49167e13 100644 --- a/pkg/scalers/authentication/authentication_types.go +++ b/pkg/scalers/authentication/authentication_types.go @@ -1,5 +1,7 @@ package authentication +import "time" + // Type describes the authentication type used in a scaler type Type string @@ -13,3 +15,34 @@ const ( // BearerAuthType is a auth type using a bearer token BearerAuthType Type = "bearer" ) + +// TransportType is type of http transport +type TransportType int + +const ( + NetHTTP TransportType = iota // NetHTTP standard Go net/http client. + FastHTTP // FastHTTP Fast http client. +) + +type AuthMeta struct { + // bearer auth + EnableBearerAuth bool + BearerToken string + + // basic auth + EnableBasicAuth bool + Username string + Password string // +optional + + // client certification + EnableTLS bool + Cert string + Key string + CA string +} + +type HTTPTransport struct { + MaxIdleConnDuration time.Duration + ReadTimeout time.Duration + WriteTimeout time.Duration +} diff --git a/pkg/scalers/predictkube_scaler.go b/pkg/scalers/predictkube_scaler.go new file mode 100644 index 00000000000..983d3e7b002 --- /dev/null +++ b/pkg/scalers/predictkube_scaler.go @@ -0,0 +1,483 @@ +package scalers + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "math" + "net/http" + "strconv" + "time" + + "github.com/go-playground/validator/v10" + "github.com/prometheus/client_golang/api" + v1 "github.com/prometheus/client_golang/api/prometheus/v1" + "github.com/prometheus/common/model" + "github.com/xhit/go-str2duration/v2" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + health "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/status" + "k8s.io/api/autoscaling/v2beta2" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/metrics/pkg/apis/external_metrics" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + libs "github.com/dysnix/predictkube-libs/external/configs" + pc "github.com/dysnix/predictkube-libs/external/grpc/client" + tc "github.com/dysnix/predictkube-libs/external/types_convertation" + "github.com/dysnix/predictkube-proto/external/proto/commonproto" + pb "github.com/dysnix/predictkube-proto/external/proto/services" + + "github.com/kedacore/keda/v2/pkg/scalers/authentication" + kedautil "github.com/kedacore/keda/v2/pkg/util" +) + +const ( + predictKubeMetricType = "External" + predictKubeMetricPrefix = "predictkube_metric" + + invalidMetricTypeErr = "metric type is invalid" +) + +var ( + mlEngineHost = "api.predictkube.com" + mlEnginePort = 443 + + defaultStep = time.Minute * 5 + + grpcConf = &libs.GRPC{ + Enabled: true, + UseReflection: true, + Compression: libs.Compression{ + Enabled: false, + }, + Conn: &libs.Connection{ + Host: mlEngineHost, + Port: uint16(mlEnginePort), + ReadBufferSize: 50 << 20, + WriteBufferSize: 50 << 20, + MaxMessageSize: 50 << 20, + Insecure: false, + Timeout: time.Second * 15, + }, + Keepalive: &libs.Keepalive{ + Time: time.Minute * 5, + Timeout: time.Minute * 5, + EnforcementPolicy: &libs.EnforcementPolicy{ + MinTime: time.Minute * 20, + PermitWithoutStream: false, + }, + }, + } +) + +type PredictKubeScaler struct { + metadata *predictKubeMetadata + prometheusClient api.Client + grpcConn *grpc.ClientConn + grpcClient pb.MlEngineServiceClient + healthClient health.HealthClient + api v1.API +} + +type predictKubeMetadata struct { + predictHorizon time.Duration + historyTimeWindow time.Duration + stepDuration time.Duration + apiKey string + prometheusAddress string + prometheusAuth *authentication.AuthMeta + query string + threshold int64 + scalerIndex int +} + +var predictKubeLog = logf.Log.WithName("predictkube_scaler") + +func (s *PredictKubeScaler) setupClientConn() error { + clientOpt, err := pc.SetGrpcClientOptions(grpcConf, + &libs.Base{ + Monitoring: libs.Monitoring{ + Enabled: false, + }, + Profiling: libs.Profiling{ + Enabled: false, + }, + Single: &libs.Single{ + Enabled: false, + }, + }, + pc.InjectPublicClientMetadataInterceptor(s.metadata.apiKey), + ) + + if !grpcConf.Conn.Insecure { + clientOpt = append(clientOpt, grpc.WithTransportCredentials( + credentials.NewTLS(&tls.Config{ + ServerName: mlEngineHost, + }), + )) + } + + if err != nil { + return err + } + + s.grpcConn, err = grpc.Dial(fmt.Sprintf("%s:%d", mlEngineHost, mlEnginePort), clientOpt...) + if err != nil { + return err + } + + s.grpcClient = pb.NewMlEngineServiceClient(s.grpcConn) + s.healthClient = health.NewHealthClient(s.grpcConn) + + return err +} + +// NewPredictKubeScaler creates a new PredictKube scaler +func NewPredictKubeScaler(ctx context.Context, config *ScalerConfig) (*PredictKubeScaler, error) { + s := &PredictKubeScaler{} + + meta, err := parsePredictKubeMetadata(config) + if err != nil { + predictKubeLog.Error(err, "error parsing PredictKube metadata") + return nil, fmt.Errorf("error parsing PredictKube metadata: %3s", err) + } + + s.metadata = meta + + err = s.initPredictKubePrometheusConn(ctx) + if err != nil { + predictKubeLog.Error(err, "error create Prometheus client and API objects") + return nil, fmt.Errorf("error create Prometheus client and API objects: %3s", err) + } + + err = s.setupClientConn() + if err != nil { + predictKubeLog.Error(err, "error init GRPC client") + return nil, fmt.Errorf("error init GRPC client: %3s", err) + } + + return s, nil +} + +// IsActive returns true if we are able to get metrics from PredictKube +func (s *PredictKubeScaler) IsActive(ctx context.Context) (bool, error) { + results, err := s.doQuery(ctx) + if err != nil { + return false, err + } + + resp, err := s.healthClient.Check(ctx, &health.HealthCheckRequest{}) + + if resp == nil { + return len(results) > 0, fmt.Errorf("can't connect grpc server: empty server response, code: %v", codes.Unknown) + } + + if err != nil { + return len(results) > 0, fmt.Errorf("can't connect grpc server: %v, code: %v", err, status.Code(err)) + } + + var y int64 + if len(results) > 0 { + y = int64(results[len(results)-1].Value) + } + + return y > 0, nil +} + +func (s *PredictKubeScaler) Close(_ context.Context) error { + return s.grpcConn.Close() +} + +func (s *PredictKubeScaler) GetMetricSpecForScaling(context.Context) []v2beta2.MetricSpec { + targetMetricValue := resource.NewQuantity(s.metadata.threshold, resource.DecimalSI) + metricName := kedautil.NormalizeString(fmt.Sprintf("predictkube-%s", predictKubeMetricPrefix)) + externalMetric := &v2beta2.ExternalMetricSource{ + Metric: v2beta2.MetricIdentifier{ + Name: GenerateMetricNameWithIndex(s.metadata.scalerIndex, metricName), + }, + Target: v2beta2.MetricTarget{ + Type: v2beta2.AverageValueMetricType, + AverageValue: targetMetricValue, + }, + } + + metricSpec := v2beta2.MetricSpec{ + External: externalMetric, Type: predictKubeMetricType, + } + return []v2beta2.MetricSpec{metricSpec} +} + +func (s *PredictKubeScaler) GetMetrics(ctx context.Context, metricName string, _ labels.Selector) ([]external_metrics.ExternalMetricValue, error) { + value, err := s.doPredictRequest(ctx) + if err != nil { + predictKubeLog.Error(err, "error executing query to predict controller service") + return []external_metrics.ExternalMetricValue{}, err + } + + if value == 0 { + err = errors.New("empty response after predict request") + predictKubeLog.Error(err, "") + return nil, err + } + + predictKubeLog.V(1).Info(fmt.Sprintf("predict value is: %d", value)) + + val := *resource.NewQuantity(value, resource.DecimalSI) + + metric := external_metrics.ExternalMetricValue{ + MetricName: metricName, + Value: val, + Timestamp: metav1.Now(), + } + + return append([]external_metrics.ExternalMetricValue{}, metric), nil +} + +func (s *PredictKubeScaler) doPredictRequest(ctx context.Context) (int64, error) { + results, err := s.doQuery(ctx) + if err != nil { + return 0, err + } + + resp, err := s.grpcClient.GetPredictMetric(ctx, &pb.ReqGetPredictMetric{ + ForecastHorizon: uint64(math.Round(float64(s.metadata.predictHorizon / s.metadata.stepDuration))), + Observations: results, + }) + + if err != nil { + return 0, err + } + + var y int64 + if len(results) > 0 { + y = int64(results[len(results)-1].Value) + } + + x := resp.GetResultMetric() + + return func(x, y int64) int64 { + if x < y { + return y + } + return x + }(x, y), nil +} + +func (s *PredictKubeScaler) doQuery(ctx context.Context) ([]*commonproto.Item, error) { + currentTime := time.Now().UTC() + + if s.metadata.stepDuration == 0 { + s.metadata.stepDuration = defaultStep + } + + r := v1.Range{ + Start: currentTime.Add(-s.metadata.historyTimeWindow), + End: currentTime, + Step: s.metadata.stepDuration, + } + + val, warns, err := s.api.QueryRange(ctx, s.metadata.query, r) + + if len(warns) > 0 { + predictKubeLog.V(1).Info("warnings", warns) + } + + if err != nil { + return nil, err + } + + return s.parsePrometheusResult(val) +} + +// parsePrometheusResult parsing response from prometheus server. +func (s *PredictKubeScaler) parsePrometheusResult(result model.Value) (out []*commonproto.Item, err error) { + metricName := GenerateMetricNameWithIndex(s.metadata.scalerIndex, kedautil.NormalizeString(fmt.Sprintf("predictkube-%s", predictKubeMetricPrefix))) + switch result.Type() { + case model.ValVector: + if res, ok := result.(model.Vector); ok { + for _, val := range res { + t, err := tc.AdaptTimeToPbTimestamp(tc.TimeToTimePtr(val.Timestamp.Time())) + if err != nil { + return nil, err + } + + out = append(out, &commonproto.Item{ + Timestamp: t, + Value: float64(val.Value), + MetricName: metricName, + }) + } + } + case model.ValMatrix: + if res, ok := result.(model.Matrix); ok { + for _, val := range res { + for _, v := range val.Values { + t, err := tc.AdaptTimeToPbTimestamp(tc.TimeToTimePtr(v.Timestamp.Time())) + if err != nil { + return nil, err + } + + out = append(out, &commonproto.Item{ + Timestamp: t, + Value: float64(v.Value), + MetricName: metricName, + }) + } + } + } + case model.ValScalar: + if res, ok := result.(*model.Scalar); ok { + t, err := tc.AdaptTimeToPbTimestamp(tc.TimeToTimePtr(res.Timestamp.Time())) + if err != nil { + return nil, err + } + + out = append(out, &commonproto.Item{ + Timestamp: t, + Value: float64(res.Value), + MetricName: metricName, + }) + } + case model.ValString: + if res, ok := result.(*model.String); ok { + t, err := tc.AdaptTimeToPbTimestamp(tc.TimeToTimePtr(res.Timestamp.Time())) + if err != nil { + return nil, err + } + + s, err := strconv.ParseFloat(res.Value, 64) + if err != nil { + return nil, err + } + + out = append(out, &commonproto.Item{ + Timestamp: t, + Value: s, + MetricName: metricName, + }) + } + default: + return nil, errors.New(invalidMetricTypeErr) + } + + return out, nil +} + +func parsePredictKubeMetadata(config *ScalerConfig) (result *predictKubeMetadata, err error) { + validate := validator.New() + meta := predictKubeMetadata{} + + if val, ok := config.TriggerMetadata["query"]; ok { + if len(val) == 0 { + return nil, fmt.Errorf("no query given") + } + + meta.query = val + } else { + return nil, fmt.Errorf("no query given") + } + + if val, ok := config.TriggerMetadata["prometheusAddress"]; ok { + err = validate.Var(val, "url") + if err != nil { + return nil, fmt.Errorf("invalid prometheusAddress") + } + + meta.prometheusAddress = val + } else { + return nil, fmt.Errorf("no prometheusAddress given") + } + + if val, ok := config.TriggerMetadata["predictHorizon"]; ok { + meta.predictHorizon, err = str2duration.ParseDuration(val) + if err != nil { + return nil, fmt.Errorf("predictHorizon parsing error %s", err.Error()) + } + } else { + return nil, fmt.Errorf("no predictHorizon given") + } + + if val, ok := config.TriggerMetadata["queryStep"]; ok { + meta.stepDuration, err = str2duration.ParseDuration(val) + if err != nil { + return nil, fmt.Errorf("queryStep parsing error %s", err.Error()) + } + } else { + return nil, fmt.Errorf("no queryStep given") + } + + if val, ok := config.TriggerMetadata["historyTimeWindow"]; ok { + meta.historyTimeWindow, err = str2duration.ParseDuration(val) + if err != nil { + return nil, fmt.Errorf("historyTimeWindow parsing error %s", err.Error()) + } + } else { + return nil, fmt.Errorf("no historyTimeWindow given") + } + + if val, ok := config.TriggerMetadata["threshold"]; ok { + meta.threshold, err = strconv.ParseInt(val, 10, 64) + if err != nil { + return nil, fmt.Errorf("threshold parsing error %s", err.Error()) + } + } else { + return nil, fmt.Errorf("no threshold given") + } + + meta.scalerIndex = config.ScalerIndex + + if val, ok := config.AuthParams["apiKey"]; ok { + err = validate.Var(val, "jwt") + if err != nil { + return nil, fmt.Errorf("invalid apiKey") + } + + meta.apiKey = val + } else { + return nil, fmt.Errorf("no api key given") + } + + // parse auth configs from ScalerConfig + meta.prometheusAuth, err = authentication.GetAuthConfigs(config.TriggerMetadata, config.AuthParams) + if err != nil { + return nil, err + } + + return &meta, nil +} + +func (s *PredictKubeScaler) ping(ctx context.Context) (err error) { + _, err = s.api.Runtimeinfo(ctx) + return err +} + +// initPredictKubePrometheusConn init prometheus client and setup connection to API +func (s *PredictKubeScaler) initPredictKubePrometheusConn(ctx context.Context) (err error) { + var roundTripper http.RoundTripper + // create http.RoundTripper with auth settings from ScalerConfig + if roundTripper, err = authentication.CreateHTTPRoundTripper( + authentication.FastHTTP, + s.metadata.prometheusAuth, + ); err != nil { + predictKubeLog.V(1).Error(err, "init Prometheus client http transport") + return err + } + + if s.prometheusClient, err = api.NewClient(api.Config{ + Address: s.metadata.prometheusAddress, + RoundTripper: roundTripper, + }); err != nil { + predictKubeLog.V(1).Error(err, "init Prometheus client") + return err + } + + s.api = v1.NewAPI(s.prometheusClient) + + return s.ping(ctx) +} diff --git a/pkg/scalers/predictkube_scaler_test.go b/pkg/scalers/predictkube_scaler_test.go new file mode 100644 index 00000000000..aff3ded74cc --- /dev/null +++ b/pkg/scalers/predictkube_scaler_test.go @@ -0,0 +1,222 @@ +package scalers + +import ( + "context" + "fmt" + "log" + "math/rand" + "net" + "testing" + "time" + + "github.com/phayes/freeport" + "github.com/stretchr/testify/assert" + "google.golang.org/grpc" + "k8s.io/apimachinery/pkg/api/resource" + + libsSrv "github.com/dysnix/predictkube-libs/external/grpc/server" + pb "github.com/dysnix/predictkube-proto/external/proto/services" +) + +type server struct { + grpcSrv *grpc.Server + listener net.Listener + port int + val int64 +} + +func (s *server) GetPredictMetric(_ context.Context, _ *pb.ReqGetPredictMetric) (res *pb.ResGetPredictMetric, err error) { + s.val = int64(rand.Intn(30000-10000) + 10000) + return &pb.ResGetPredictMetric{ + ResultMetric: s.val, + }, nil +} + +func (s *server) start() <-chan error { + errCh := make(chan error, 1) + + go func() { + defer close(errCh) + + var ( + err error + ) + + s.port, err = freeport.GetFreePort() + if err != nil { + log.Fatalf("Could not get free port for init mock grpc server: %s", err) + } + + serverURL := fmt.Sprintf("0.0.0.0:%d", s.port) + if s.listener == nil { + var err error + s.listener, err = net.Listen("tcp4", serverURL) + + if err != nil { + log.Println("starting grpc server with error") + + errCh <- err + return + } + } + + log.Printf("🚀 starting mock grpc server. On host 0.0.0.0, with port: %d", s.port) + + if err := s.grpcSrv.Serve(s.listener); err != nil { + log.Println(err, "serving grpc server with error") + + errCh <- err + return + } + }() + + return errCh +} + +func (s *server) stop() error { + s.grpcSrv.GracefulStop() + return libsSrv.CheckNetErrClosing(s.listener.Close()) +} + +func runMockGrpcPredictServer() (*server, *grpc.Server) { + grpcServer := grpc.NewServer() + + mockGrpcServer := &server{grpcSrv: grpcServer} + + defer func() { + if r := recover(); r != nil { + _ = mockGrpcServer.stop() + panic(r) + } + }() + + go func() { + for errCh := range mockGrpcServer.start() { + if errCh != nil { + log.Printf("GRPC server listen error: %3v", errCh) + } + } + }() + + pb.RegisterMlEngineServiceServer(grpcServer, mockGrpcServer) + + return mockGrpcServer, grpcServer +} + +const testAPIKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ0ZXN0IENyZWF0ZUNsaWVudCIsImV4cCI6MTY0NjkxNzI3Nywic3ViIjoiODM4NjY5ODAtM2UzNS0xMWVjLTlmMjQtYWNkZTQ4MDAxMTIyIn0.5QEuO6_ysdk2abGvk3Xp7Q25M4H4pIFXeqP2E7n9rKI" + +type predictKubeMetadataTestData struct { + metadata map[string]string + authParams map[string]string + isError bool +} + +var testPredictKubeMetadata = []predictKubeMetadataTestData{ + // all properly formed + { + map[string]string{"predictHorizon": "2h", "historyTimeWindow": "7d", "prometheusAddress": "http://demo.robustperception.io:9090", "queryStep": "2m", "threshold": "2000", "query": "up"}, + map[string]string{"apiKey": testAPIKey}, false, + }, + // missing prometheusAddress + { + map[string]string{"predictHorizon": "2h", "historyTimeWindow": "7d", "prometheusAddress": "", "queryStep": "2m", "threshold": "2000", "query": "up"}, + map[string]string{"apiKey": testAPIKey}, true, + }, + // malformed threshold + { + map[string]string{"predictHorizon": "2h", "historyTimeWindow": "7d", "prometheusAddress": "http://localhost:9090", "queryStep": "2m", "threshold": "one", "query": "up"}, + + map[string]string{"apiKey": testAPIKey}, true, + }, + // missing query + { + map[string]string{"predictHorizon": "2h", "historyTimeWindow": "7d", "prometheusAddress": "http://localhost:9090", "queryStep": "2m", "threshold": "one", "query": ""}, + map[string]string{"apiKey": testAPIKey}, true, + }, +} + +func TestPredictKubeParseMetadata(t *testing.T) { + for _, testData := range testPredictKubeMetadata { + _, err := parsePredictKubeMetadata(&ScalerConfig{TriggerMetadata: testData.metadata, AuthParams: testData.authParams}) + if err != nil && !testData.isError { + t.Error("Expected success but got error", err) + } + if testData.isError && err == nil { + t.Error("Expected error but got success") + } + } +} + +type predictKubeMetricIdentifier struct { + metadataTestData *predictKubeMetadataTestData + scalerIndex int + name string +} + +var predictKubeMetricIdentifiers = []predictKubeMetricIdentifier{ + {&testPredictKubeMetadata[0], 0, fmt.Sprintf("s0-predictkube-%s", predictKubeMetricPrefix)}, + {&testPredictKubeMetadata[0], 1, fmt.Sprintf("s1-predictkube-%s", predictKubeMetricPrefix)}, +} + +func TestPredictKubeGetMetricSpecForScaling(t *testing.T) { + mockPredictServer, grpcServer := runMockGrpcPredictServer() + defer func() { + _ = mockPredictServer.stop() + grpcServer.GracefulStop() + }() + + mlEngineHost = "0.0.0.0" + mlEnginePort = mockPredictServer.port + + for _, testData := range predictKubeMetricIdentifiers { + mockPredictKubeScaler, err := NewPredictKubeScaler( + context.Background(), &ScalerConfig{ + TriggerMetadata: testData.metadataTestData.metadata, + AuthParams: testData.metadataTestData.authParams, + ScalerIndex: testData.scalerIndex, + }, + ) + assert.NoError(t, err) + + metricSpec := mockPredictKubeScaler.GetMetricSpecForScaling(context.Background()) + metricName := metricSpec[0].External.Metric.Name + if metricName != testData.name { + t.Error("Wrong External metric source name:", metricName) + return + } + + t.Log(metricSpec) + } +} + +func TestPredictKubeGetMetrics(t *testing.T) { + grpcConf.Conn.Insecure = true + + mockPredictServer, grpcServer := runMockGrpcPredictServer() + <-time.After(time.Second * 3) + defer func() { + _ = mockPredictServer.stop() + grpcServer.GracefulStop() + }() + + mlEngineHost = "0.0.0.0" + mlEnginePort = mockPredictServer.port + + for _, testData := range predictKubeMetricIdentifiers { + mockPredictKubeScaler, err := NewPredictKubeScaler( + context.Background(), &ScalerConfig{ + TriggerMetadata: testData.metadataTestData.metadata, + AuthParams: testData.metadataTestData.authParams, + ScalerIndex: testData.scalerIndex, + }, + ) + assert.NoError(t, err) + + result, err := mockPredictKubeScaler.GetMetrics(context.Background(), predictKubeMetricPrefix, nil) + assert.NoError(t, err) + assert.Equal(t, len(result), 1) + assert.Equal(t, result[0].Value, *resource.NewQuantity(mockPredictServer.val, resource.DecimalSI)) + + t.Logf("get: %v, want: %v, predictMetric: %d", result[0].Value, *resource.NewQuantity(mockPredictServer.val, resource.DecimalSI), mockPredictServer.val) + } +} diff --git a/pkg/scalers/prometheus_scaler.go b/pkg/scalers/prometheus_scaler.go index fadad7baa8a..9a311dc038e 100644 --- a/pkg/scalers/prometheus_scaler.go +++ b/pkg/scalers/prometheus_scaler.go @@ -3,13 +3,11 @@ package scalers import ( "context" "encoding/json" - "errors" "fmt" "io/ioutil" "net/http" url_pkg "net/url" "strconv" - "strings" "time" v2beta2 "k8s.io/api/autoscaling/v2beta2" @@ -36,27 +34,12 @@ type prometheusScaler struct { } type prometheusMetadata struct { - serverAddress string - metricName string - query string - threshold int - - // bearer auth - enableBearerAuth bool - bearerToken string - - // basic auth - enableBasicAuth bool - username string - password string // +optional - - // client certification - enableTLS bool - cert string - key string - ca string - - scalerIndex int + serverAddress string + metricName string + query string + threshold int + prometheusAuth *authentication.AuthMeta + scalerIndex int } type promQueryResult struct { @@ -82,13 +65,15 @@ func NewPrometheusScaler(config *ScalerConfig) (Scaler, error) { httpClient := kedautil.CreateHTTPClient(config.GlobalHTTPTimeout, false) - if meta.ca != "" || meta.enableTLS { - config, err := kedautil.NewTLSConfig(meta.cert, meta.key, meta.ca) - if err != nil || config == nil { - return nil, fmt.Errorf("error creating the TLS config: %s", err) + if meta.prometheusAuth != nil && (meta.prometheusAuth.CA != "" || meta.prometheusAuth.EnableTLS) { + // create http.RoundTripper with auth settings from ScalerConfig + if httpClient.Transport, err = authentication.CreateHTTPRoundTripper( + authentication.NetHTTP, + meta.prometheusAuth, + ); err != nil { + predictKubeLog.V(1).Error(err, "init Prometheus client http transport") + return nil, err } - - httpClient.Transport = &http.Transport{TLSClientConfig: config} } return &prometheusScaler{ @@ -97,8 +82,8 @@ func NewPrometheusScaler(config *ScalerConfig) (Scaler, error) { }, nil } -func parsePrometheusMetadata(config *ScalerConfig) (*prometheusMetadata, error) { - meta := prometheusMetadata{} +func parsePrometheusMetadata(config *ScalerConfig) (meta *prometheusMetadata, err error) { + meta = &prometheusMetadata{} if val, ok := config.TriggerMetadata[promServerAddress]; ok && val != "" { meta.serverAddress = val @@ -129,61 +114,13 @@ func parsePrometheusMetadata(config *ScalerConfig) (*prometheusMetadata, error) meta.scalerIndex = config.ScalerIndex - authModes, ok := config.TriggerMetadata["authModes"] - // no authMode specified - if !ok { - return &meta, nil - } - - authTypes := strings.Split(authModes, ",") - for _, t := range authTypes { - authType := authentication.Type(strings.TrimSpace(t)) - switch authType { - case authentication.BearerAuthType: - if len(config.AuthParams["bearerToken"]) == 0 { - return nil, errors.New("no bearer token provided") - } - if meta.enableBasicAuth { - return nil, errors.New("beare and basic authentication can not be set both") - } - - meta.bearerToken = config.AuthParams["bearerToken"] - meta.enableBearerAuth = true - case authentication.BasicAuthType: - if len(config.AuthParams["username"]) == 0 { - return nil, errors.New("no username given") - } - if meta.enableBearerAuth { - return nil, errors.New("beare and basic authentication can not be set both") - } - - meta.username = config.AuthParams["username"] - // password is optional. For convenience, many application implement basic auth with - // username as apikey and password as empty - meta.password = config.AuthParams["password"] - meta.enableBasicAuth = true - case authentication.TLSAuthType: - if len(config.AuthParams["cert"]) == 0 { - return nil, errors.New("no cert given") - } - meta.cert = config.AuthParams["cert"] - - if len(config.AuthParams["key"]) == 0 { - return nil, errors.New("no key given") - } - - meta.key = config.AuthParams["key"] - meta.enableTLS = true - default: - return nil, fmt.Errorf("err incorrect value for authMode is given: %s", t) - } - } - - if len(config.AuthParams["ca"]) > 0 { - meta.ca = config.AuthParams["ca"] + // parse auth configs from ScalerConfig + meta.prometheusAuth, err = authentication.GetAuthConfigs(config.TriggerMetadata, config.AuthParams) + if err != nil { + return nil, err } - return &meta, nil + return meta, nil } func (s *prometheusScaler) IsActive(ctx context.Context) (bool, error) { @@ -227,10 +164,10 @@ func (s *prometheusScaler) ExecutePromQuery(ctx context.Context) (float64, error return -1, err } - if s.metadata.enableBearerAuth { - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", s.metadata.bearerToken)) - } else if s.metadata.enableBasicAuth { - req.SetBasicAuth(s.metadata.username, s.metadata.password) + if s.metadata.prometheusAuth != nil && s.metadata.prometheusAuth.EnableBearerAuth { + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", s.metadata.prometheusAuth.BearerToken)) + } else if s.metadata.prometheusAuth != nil && s.metadata.prometheusAuth.EnableBasicAuth { + req.SetBasicAuth(s.metadata.prometheusAuth.Username, s.metadata.prometheusAuth.Password) } r, err := s.httpClient.Do(req) @@ -242,7 +179,7 @@ func (s *prometheusScaler) ExecutePromQuery(ctx context.Context) (float64, error if err != nil { return -1, err } - r.Body.Close() + _ = r.Body.Close() if !(r.StatusCode >= 200 && r.StatusCode <= 299) { return -1, fmt.Errorf("prometheus query api returned error. status: %d response: %s", r.StatusCode, string(b)) @@ -283,7 +220,7 @@ func (s *prometheusScaler) ExecutePromQuery(ctx context.Context) (float64, error return v, nil } -func (s *prometheusScaler) GetMetrics(ctx context.Context, metricName string, metricSelector labels.Selector) ([]external_metrics.ExternalMetricValue, error) { +func (s *prometheusScaler) GetMetrics(ctx context.Context, metricName string, _ labels.Selector) ([]external_metrics.ExternalMetricValue, error) { val, err := s.ExecutePromQuery(ctx) if err != nil { prometheusLog.Error(err, "error executing prometheus query") diff --git a/pkg/scalers/prometheus_scaler_test.go b/pkg/scalers/prometheus_scaler_test.go index 3f4169502c0..3a73a7d10d4 100644 --- a/pkg/scalers/prometheus_scaler_test.go +++ b/pkg/scalers/prometheus_scaler_test.go @@ -114,9 +114,9 @@ func TestPrometheusScalerAuthParams(t *testing.T) { } if err == nil { - if (meta.enableBearerAuth && !strings.Contains(testData.metadata["authModes"], "bearer")) || - (meta.enableBasicAuth && !strings.Contains(testData.metadata["authModes"], "basic")) || - (meta.enableTLS && !strings.Contains(testData.metadata["authModes"], "tls")) { + if (meta.prometheusAuth.EnableBearerAuth && !strings.Contains(testData.metadata["authModes"], "bearer")) || + (meta.prometheusAuth.EnableBasicAuth && !strings.Contains(testData.metadata["authModes"], "basic")) || + (meta.prometheusAuth.EnableTLS && !strings.Contains(testData.metadata["authModes"], "tls")) { t.Error("wrong auth mode detected") } } diff --git a/pkg/scaling/scale_handler.go b/pkg/scaling/scale_handler.go index cab410b16f2..81ba0e83a9a 100644 --- a/pkg/scaling/scale_handler.go +++ b/pkg/scaling/scale_handler.go @@ -406,6 +406,8 @@ func buildScaler(ctx context.Context, client client.Client, triggerType string, return scalers.NewOpenstackSwiftScaler(ctx, config) case "postgresql": return scalers.NewPostgreSQLScaler(config) + case "predictkube": + return scalers.NewPredictKubeScaler(ctx, config) case "prometheus": return scalers.NewPrometheusScaler(config) case "rabbitmq": diff --git a/tests/scalers/predictkube.test.ts b/tests/scalers/predictkube.test.ts new file mode 100644 index 00000000000..5db9437fb63 --- /dev/null +++ b/tests/scalers/predictkube.test.ts @@ -0,0 +1,235 @@ +import * as fs from 'fs' +import * as sh from 'shelljs' +import * as tmp from 'tmp' +import test from 'ava' +import {waitForRollout} from "./helpers"; + +const predictkubeApiKey = process.env['PREDICTKUBE_API_KEY'] +const testNamespace = 'predictkube-test' +const prometheusNamespace = 'monitoring' +const prometheusDeploymentFile = 'scalers/prometheus-deployment.yaml' + +test.before(t => { + // install prometheus + sh.exec(`kubectl create namespace ${prometheusNamespace}`) + t.is(0, sh.exec(`kubectl apply --namespace ${prometheusNamespace} -f ${prometheusDeploymentFile}`).code, 'creating a Prometheus deployment should work.') + // wait for prometheus to load + t.is(0, waitForRollout('deployment', "prometheus-server", prometheusNamespace)) + + sh.config.silent = true + // create deployments - there are two deployments - both using the same image but one deployment + // is directly tied to the KEDA HPA while the other is isolated that can be used for metrics + // even when the KEDA deployment is at zero - the service points to both deployments + const tmpFile = tmp.fileSync() + fs.writeFileSync(tmpFile.name, deployYaml + .replace('{{PREDICTKUBE_API_KEY}}', Buffer.from(predictkubeApiKey).toString('base64')) + .replace('{{PROMETHEUS_NAMESPACE}}', prometheusNamespace) + ) + sh.exec(`kubectl create namespace ${testNamespace}`) + t.is( + 0, + sh.exec(`kubectl apply -f ${tmpFile.name} --namespace ${testNamespace}`).code, + 'creating a deployment should work.' + ) + for (let i = 0; i < 10; i++) { + const readyReplicaCount = sh.exec(`kubectl get deployment.apps/test-app --namespace ${testNamespace} -o jsonpath="{.status.readyReplicas}`).stdout + if (readyReplicaCount != '1') { + sh.exec('sleep 2s') + } + } +}) + +test.serial('Deployment should have 0 replicas on start', t => { + const replicaCount = sh.exec( + `kubectl get deployment.apps/keda-test-app --namespace ${testNamespace} -o jsonpath="{.spec.replicas}"` + ).stdout + t.is(replicaCount, '0', 'replica count should start out as 0') +}) + +test.serial(`Deployment should scale to 5 (the max) with HTTP Requests exceeding in the rate then back to 0`, t => { + // generate a large number of HTTP requests (using Apache Bench) that will take some time + // so prometheus has some time to scrape it + const tmpFile = tmp.fileSync() + fs.writeFileSync(tmpFile.name, generateRequestsYaml.replace('{{NAMESPACE}}', testNamespace)) + t.is( + 0, + sh.exec(`kubectl apply -f ${tmpFile.name} --namespace ${testNamespace}`).code, + 'creating job should work.' + ) + + t.is( + '1', + sh.exec( + `kubectl get deployment.apps/test-app --namespace ${testNamespace} -o jsonpath="{.status.readyReplicas}"` + ).stdout, + 'There should be 1 replica for the test-app deployment' + ) + + // keda based deployment should start scaling up with http requests issued + let replicaCount = '0' + for (let i = 0; i < 60 && replicaCount !== '5'; i++) { + t.log(`Waited ${5 * i} seconds for predictkube-based deployments to scale up`) + const jobLogs = sh.exec(`kubectl logs -l job-name=generate-requests -n ${testNamespace}`).stdout + t.log(`Logs from the generate requests: ${jobLogs}`) + + replicaCount = sh.exec( + `kubectl get deployment.apps/keda-test-app --namespace ${testNamespace} -o jsonpath="{.spec.replicas}"` + ).stdout + if (replicaCount !== '5') { + sh.exec('sleep 5s') + } + } + + t.is('5', replicaCount, 'Replica count should be maxed at 5') + + for (let i = 0; i < 60 && replicaCount !== '0'; i++) { + t.log(`Waited ${5 * i} seconds for predictkube-based deployments to scale down`) + replicaCount = sh.exec( + `kubectl get deployment.apps/keda-test-app --namespace ${testNamespace} -o jsonpath="{.spec.replicas}"` + ).stdout + if (replicaCount !== '0') { + sh.exec('sleep 5s') + } + } + + t.is('0', replicaCount, 'Replica count should be 0 after 3 minutes') +}) + +test.after.always.cb('clean up predictkube deployment', t => { + const resources = [ + 'scaledobject.keda.sh/predictkube-scaledobject', + 'deployment.apps/test-app', + 'deployment.apps/keda-test-app', + 'service/test-app', + 'job/generate-requests', + ] + + for (const resource of resources) { + sh.exec(`kubectl delete ${resource} --namespace ${testNamespace}`) + } + sh.exec(`kubectl delete namespace ${testNamespace}`) + + // uninstall prometheus + sh.exec(`kubectl delete --namespace ${prometheusNamespace} -f ${prometheusDeploymentFile}`) + sh.exec(`kubectl delete namespace ${prometheusNamespace}`) + + t.end() +}) + +const deployYaml = `apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: test-app + name: test-app +spec: + replicas: 1 + selector: + matchLabels: + app: test-app + template: + metadata: + labels: + app: test-app + type: keda-testing + spec: + containers: + - name: prom-test-app + image: tbickford/simple-web-app-prometheus:a13ade9 + imagePullPolicy: IfNotPresent +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: keda-test-app + name: keda-test-app +spec: + replicas: 0 + selector: + matchLabels: + app: keda-test-app + template: + metadata: + labels: + app: keda-test-app + type: keda-testing + spec: + containers: + - name: prom-test-app + image: tbickford/simple-web-app-prometheus:a13ade9 + imagePullPolicy: IfNotPresent +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: test-app + annotations: + prometheus.io/scrape: "true" + name: test-app +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: 8080 + selector: + type: keda-testing +--- +apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: predictkube-trigger +spec: + secretTargetRef: + - parameter: apiKey + name: predictkube-secret + key: apiKey +--- +apiVersion: v1 +kind: Secret +metadata: + name: predictkube-secret +type: Opaque +data: + apiKey: {{PREDICTKUBE_API_KEY}} +--- +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: predictkube-scaledobject +spec: + scaleTargetRef: + name: keda-test-app + minReplicaCount: 0 + maxReplicaCount: 5 + pollingInterval: 5 + cooldownPeriod: 10 + triggers: + - type: predictkube + metadata: + predictHorizon: "2h" + historyTimeWindow: "7d" + prometheusAddress: http://prometheus-server.{{PROMETHEUS_NAMESPACE}}.svc + threshold: '100' + query: sum(rate(http_requests_total{app="test-app"}[2m])) + queryStep: "2m" + authenticationRef: + name: predictkube-trigger` + +const generateRequestsYaml = `apiVersion: batch/v1 +kind: Job +metadata: + name: generate-requests +spec: + template: + spec: + containers: + - image: jordi/ab + name: test + command: ["/bin/sh"] + args: ["-c", "for i in $(seq 1 60);do echo $i;ab -c 5 -n 1000 -v 2 http://test-app.{{NAMESPACE}}.svc/;sleep 1;done"] + restartPolicy: Never + activeDeadlineSeconds: 120 + backoffLimit: 2`