diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0f04682 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +bin/ +testbin/ diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 88bd560..b0672c5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -10,6 +10,9 @@ assignees: '' **Describe the bug** A clear and concise description of what the bug is. +**Predictive Horizontal Pod Autoscaler Version** +The version of the Predictive Horizontal Pod Autoscaler the bug has been found on. + **To Reproduce** Steps to reproduce the behavior: 1. Deploy '...' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 778ab6b..2f2e9b6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,41 +9,49 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - name: Set up Go 1.17 + + - name: Set up Go 1.18 uses: actions/setup-go@v1 with: - go-version: 1.17 - id: go - - uses: actions/setup-python@v2 + go-version: 1.18 + + - uses: actions/setup-python@v4 + with: + python-version: '3.8.x' + + - uses: azure/setup-helm@v1 with: - python-version: '3.8.5' + version: 'v3.9.0' + id: helm + - name: Check out code into the Go module directory uses: actions/checkout@v1 + - name: Lint, test and build run: | - # Get staticcheck + # Get tooling export PATH=$PATH:$(go env GOPATH)/bin - go install honnef.co/go/tools/cmd/staticcheck@v0.3.0 + go get # Lint and test pip install -r requirements-dev.txt + make generate make lint make format # Exit if after formatting there are any code differences git diff --exit-code - make test - # Build if [ ${{ github.event_name }} == "release" ]; then - # github.ref is in the form refs/tags/VERSION, so apply regex to just get version - VERSION=$(echo "${{ github.ref }}" | grep -P '([^\/]+$)' -o) + VERSION="${{ github.ref_name }}" else - VERSION=$(git rev-parse --short ${{ github.sha }}) + VERSION="${{ github.sha }}" fi + make docker VERSION=${VERSION} + - name: Deploy env: DOCKER_USER: ${{ secrets.DOCKER_USER }} @@ -52,14 +60,39 @@ jobs: run: | if [ ${{ github.event_name }} == "release" ]; then - # github.ref is in the form refs/tags/VERSION, so apply regex to just get version - VERSION=$(echo "${{ github.ref }}" | grep -P '([^\/]+$)' -o) + VERSION="${{ github.ref_name }}" else - VERSION=$(git rev-parse --short ${{ github.sha }}) + VERSION="${{ github.sha }}" fi + echo "$DOCKER_PASS" | docker login --username=$DOCKER_USER --password-stdin + docker push jthomperoo/predictive-horizontal-pod-autoscaler:${VERSION} + if [ ${{ github.event_name }} == "release" ]; then docker tag jthomperoo/predictive-horizontal-pod-autoscaler:${VERSION} jthomperoo/predictive-horizontal-pod-autoscaler:latest docker push jthomperoo/predictive-horizontal-pod-autoscaler:latest fi + + - name: Bundle YAML config + if: github.event_name == 'release' && github.repository == 'jthomperoo/predictive-horizontal-pod-autoscaler' + run: | + + # Variables to sub into k8s templates + if [ ${{ github.event_name }} == "release" ]; then + VERSION="${{ github.ref_name }}" + else + VERSION="${{ github.sha }}" + fi + + export VERSION="${VERSION}" + sed -i "/version: 0.0.0/c\version: ${VERSION}" helm/Chart.yaml + helm package helm/ + + - name: Deploy helm package + if: github.event_name == 'release' && github.repository == 'jthomperoo/predictive-horizontal-pod-autoscaler' + uses: Shopify/upload-to-release@1.0.0 + with: + name: predictive-horizontal-pod-autoscaler-${{ github.ref_name }}.tgz + path: predictive-horizontal-pod-autoscaler-${{ github.ref_name }}.tgz + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index afe40fb..ade1860 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,31 @@ -/vendor/ -/dist/ *.out __pycache__ .algorithm_coverage .coverage + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin +dist +tmp +testbin/* + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +*.swp +*.swo +*~ diff --git a/CHANGELOG.md b/CHANGELOG.md index 6043cb5..93b7f01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- See the [migration guide from `v0.10.0` here](https://predictive-horizontal-pod-autoscaler.readthedocs.io/en/latest/user-guide/migration/v0_10_0-to-v0_11_0). +- BREAKING CHANGE: Major rewrite converting this project from a Custom Pod Autoscaler to have its own dedicated CRD +and operator. + - Configuration and deployment has changed completely, no longer need to install the Custom Pod Autoscaler Operator, + instead you need to install the Predictive Horizontal Pod Autoscaler as an operator. + - No longer deployed as `CustomPodAutoscaler` custom resources, now deployed as `PredictiveHorizontalPodAutoscaler` + custom resources. +- BREAKING CHANGES: Several configuration options renamed for clarity. + - `LinearModel -> storedValues` renamed to `LinearModel -> historySize` + - `Model -> perInterval` renamed to `Model -> perSyncPeriod` +- BREAKING CHANGES: `perSyncPeriod` behaviour changed slightly, now it will no longer calculate the prediction, but +it will still update the replica history available to make a prediction with and prune the replica history if needed. +- Holt-Winters runtime hooks format changed: + - `evaluations` renamed to `replicaHistory`. + - Format change for `replicaHistory`, now in the format `[{"time": "", "replicas": }]`. +- Upgrade to Go `v1.18`. +- No longer SQLite based storage, instead using Kubernetes configmaps which give persistent storage by default with +resiliency when the autoscaler operator restarts. +### Removed +- BREAKING CHANGES: Removed some no longer relevant configuration options. + - `DBPath` + - `MigrationPath` +- BREAKING CHANGE: Since no longer built as a `CustomPodAutoscaler` the `startTime` configuration is no longer +available: . +- BREAKING CHANGE: Holt-Winters runtime hooks limited to only support HTTP, shell support dropped. ## [v0.10.0] - 2022-05-14 ### Changed diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..44ce359 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,43 @@ +# Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Build the manager binary +FROM golang:1.18 as builder + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY . . + +# Build +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go + +FROM python:3.8-slim-buster +WORKDIR /app + +# Install Python dependencies +COPY algorithms/requirements.txt ./algorithms/requirements.txt +RUN python -m pip install -r algorithms/requirements.txt + +COPY algorithms/ ./algorithms +COPY --from=builder /workspace/manager . +USER 65532:65532 + +ENTRYPOINT ["/app/manager"] diff --git a/Makefile b/Makefile index c388f27..7c49fc0 100644 --- a/Makefile +++ b/Makefile @@ -2,19 +2,23 @@ REGISTRY = jthomperoo NAME = predictive-horizontal-pod-autoscaler VERSION = latest -default: - @echo "=============Building=============" - go build -o dist/$(NAME) main.go - cp LICENSE dist/LICENSE +LOCAL_HELM_CHART_NAME=predictive-horizontal-pod-autoscaler-operator -test: - @echo "=============Running tests=============" - go test ./... -cover -coverprofile application_coverage.out - pytest algorithms/ --cov-report term --cov-report=xml:algorithm_coverage.out --cov-report=html:.algorithm_coverage --cov=algorithms/ +run: deploy py_dependencies + go run github.com/cosmtrek/air + +py_dependencies: + python -m pip install -r requirements-dev.txt + +undeploy: generate + helm uninstall $(LOCAL_HELM_CHART_NAME) -lint: +deploy: generate + helm upgrade --install --set mode=development $(LOCAL_HELM_CHART_NAME) helm/ + +lint: generate @echo "=============Linting=============" - staticcheck ./... + go run honnef.co/go/tools/cmd/staticcheck ./... pylint algorithms --rcfile=.pylintrc format: @@ -23,15 +27,31 @@ format: go mod tidy find algorithms -name '*.py' -print0 | xargs -0 yapf -i +test: generate + @echo "=============Running tests=============" + go test ./... -cover -coverprofile unit_cover.out + pytest algorithms/ --cov-report term --cov-report=xml:algorithm_coverage.out --cov-report=html:.algorithm_coverage --cov=algorithms/ + docker: - @echo "=============Building docker images=============" - docker build -f build/Dockerfile -t $(REGISTRY)/$(NAME):$(VERSION) . + docker build . -t $(REGISTRY)/$(NAME):$(VERSION) -doc: - @echo "=============Serving docs=============" - mkdocs serve +generate: get_controller-gen + @echo "=============Generating Golang and YAML=============" + controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..." + controller-gen rbac:roleName=predictive-horizontal-pod-autoscaler webhook crd:allowDangerousTypes=true \ + paths="./..." \ + output:crd:artifacts:config=helm/templates/crd \ + output:rbac:artifacts:config=helm/templates/cluster \ + output:webhook:artifacts:config=helm/templates/cluster -coverage: +view_coverage: @echo "=============Loading coverage HTML=============" - go tool cover -html=application_coverage.out + go tool cover -html=unit_cover.out python -m webbrowser file://$(shell pwd)/.algorithm_coverage/index.html + +get_controller-gen: + go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2 + +doc: + @echo "=============Serving docs=============" + mkdocs serve diff --git a/PROJECT b/PROJECT new file mode 100644 index 0000000..a0620d2 --- /dev/null +++ b/PROJECT @@ -0,0 +1,23 @@ +domain: jamiethompson.me +layout: +- go.kubebuilder.io/v3 +plugins: + manifests.sdk.operatorframework.io/v2: {} + scorecard.sdk.operatorframework.io/v2: {} +projectName: predictive-horizontal-pod-autoscaler +repo: github.com/jthomperoo/predictive-horizontal-pod-autoscaler +resources: +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: jamiethompson.me + group: jamiethompson.me + kind: PredictiveHorizontalPodAutoscaler + path: github.com/jthomperoo/predictive-horizontal-pod-autoscaler/api/v1alpha1 + version: v1alpha1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 +version: "3" diff --git a/README.md b/README.md index 60f777f..4af2339 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,11 @@ [![Documentation Status](https://readthedocs.org/projects/predictive-horizontal-pod-autoscaler/badge/?version=latest)](https://predictive-horizontal-pod-autoscaler.readthedocs.io/en/latest) [![License](https://img.shields.io/:license-apache-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) -

This project is supported by:

-

- - - -

- # Predictive Horizontal Pod Autoscaler -This is a [Custom Pod Autoscaler](https://www.github.com/jthomperoo/custom-pod-autoscaler); aiming to have identical -functionality to the Horizontal Pod Autoscaler, however with added predictive elements using statistical models. +Predictive Horizontal Pod Autoscalers (PHPAs) are Horizontal Pod Autoscalers (HPAs) with extra predictive capabilities +baked in, allowing you to apply statistical models to the results of HPA calculations to make proactive scaling +decisions. This extensively uses the the [jthomperoo/k8shorizmetrics](https://github.com/jthomperoo/k8shorizmetrics) library to gather metrics and to evaluate them as the Kubernetes Horizontal Pod Autoscaler does. @@ -22,7 +16,16 @@ to gather metrics and to evaluate them as the Kubernetes Horizontal Pod Autoscal # Why would I use it? This autoscaler lets you choose models and fine tune them in order to predict how many replicas a resource should have, -preempting events such as regular, repeated high load. +preempting events such as regular, repeated high load. This allows for proactive rather than simply reactive scaling +that can make intelligent ahead of time decisions. + +# What systems would need it? + +Systems that have predictable changes in load, for example; if over a 24 hour period the load on a resource is +generally higher between 3pm and 5pm - with enough data and use of correct models and tuning the autoscaler could +predict this and preempt the load, increasing responsiveness of the system to changes in load. This could be useful for +handling different userbases across different timezones, or understanding that if a load is rapidly increasing we can +prempt the load by predicting replica counts. ## Features @@ -35,8 +38,6 @@ solutions such as EKS or GCP. * CPU Initialization Period. * Downscale Stabilization. * Sync Period. - * Initial Readiness Delay. -* Runs in Kubernetes as a standard Pod. ## How does it work? @@ -57,10 +58,9 @@ See the [`examples/` directory](./examples) for working code samples. Developing this project requires these dependencies: -* [Go](https://golang.org/doc/install) >= `1.17` -* [staticcheck](https://staticcheck.io/docs/getting-started/) == `v0.3.0 (2022.1)` -* [Docker](https://docs.docker.com/install/) -* [Python](https://www.python.org/downloads/) == `3.8.5` +* [Go](https://golang.org/doc/install) >= `1.18` +* [Python](https://www.python.org/downloads/) == `3.8.x` +* [Helm](https://helm.sh/) == `3.9.x` Any Python dependencies must be installed by running: @@ -71,24 +71,12 @@ pip install -r requirements-dev.txt It is recommended to test locally using a local Kubernetes managment system, such as [k3d](https://github.com/rancher/k3d) (allows running a small Kubernetes cluster locally using Docker). -Once you have -a cluster available, you should install the [Custom Pod Autoscaler Operator -(CPAO)](https://github.com/jthomperoo/custom-pod-autoscaler-operator/blob/master/INSTALL.md) -onto the cluster to let you install the PHPA. - -With the CPAO installed you can install your development builds of the PHPA onto the cluster by building the image -locally, and then pushing the image to the K8s cluster's registry (to do that with k3d you can use the -`k3d image import` command). - -Finally you can deploy a PHPA example (see the [`examples/` directory](./examples) for choices) to test your changes. - -> Note that the examples generally use `ImagePullPolicy: Always`, you may need to change this to -> `ImagePullPolicy: IfNotPresent` to use your local build. +You can deploy a PHPA example (see the [`examples/` directory](./examples) for choices) to test your changes. ### Commands -* `make` - builds the Predictive HPA binary. -* `make docker` - builds the Predictive HPA image. +* `make run` - runs the PHPA locally against the cluster configured in your kubeconfig file. +* `make docker` - builds the PHPA image. * `make lint` - lints the code. * `make format` - beautifies the code, must be run to pass the CI. * `make test` - runs the unit tests. diff --git a/air.toml b/air.toml new file mode 100644 index 0000000..16f5382 --- /dev/null +++ b/air.toml @@ -0,0 +1,36 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main main.go -zap-log-level 5" + delay = 1000 + exclude_dir = ["tmp", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go"] + kill_delay = "0s" + log = "build-errors.log" + send_interrupt = false + stop_on_error = true + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + time = true + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false diff --git a/algorithms/holt_winters/holt_winters.py b/algorithms/holt_winters/holt_winters.py index e24586e..4126d9e 100644 --- a/algorithms/holt_winters/holt_winters.py +++ b/algorithms/holt_winters/holt_winters.py @@ -1,4 +1,4 @@ -# Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. +# Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/algorithms/linear_regression/linear_regression.py b/algorithms/linear_regression/linear_regression.py index e2ce3f1..acf5961 100644 --- a/algorithms/linear_regression/linear_regression.py +++ b/algorithms/linear_regression/linear_regression.py @@ -1,4 +1,4 @@ -# Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. +# Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,23 +27,17 @@ import statsmodels.api as sm from dataclasses_json import dataclass_json, LetterCase -# Takes in list of stored evaluations and the look ahead value: +# Takes in a replica history and the look ahead value: # { # "lookAhead": 3, -# "evaluations": [ +# "replicaHistory": [ # { -# "id": 0, -# "created": "2020-02-01T00:55:33Z", -# "val": { -# "targetReplicas": 3 -# } +# "time": "2020-02-01T00:55:33Z", +# "replicas": 3 # }, # { -# "id": 1, -# "created": "2020-02-01T00:56:33Z", -# "val": { -# "targetReplicas": 2 -# } +# "time": "2020-02-01T00:56:33Z", +# "replicas": 6 # } # ] # } @@ -51,22 +45,12 @@ @dataclass_json(letter_case=LetterCase.CAMEL) @dataclass -class EvaluationValue: - """ - JSON data representation of an evaluation value, contains the scaling target replicas - """ - target_replicas: int - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass -class Evaluation: +class TimestampedReplica: """ JSON data representation of a timestamped evaluation """ - id: int - created: str - val: EvaluationValue + time: str + replicas: int @dataclass_json(letter_case=LetterCase.CAMEL) @@ -76,7 +60,7 @@ class AlgorithmInput: JSON data representation of the data this algorithm requires to be provided to it. """ look_ahead: int - evaluations: List[Evaluation] + replica_history: List[TimestampedReplica] current_time: Optional[str] = None @@ -111,15 +95,15 @@ class AlgorithmInput: # Build up data for linear model, in order to not deal with huge values and get rounding errors, use the difference # between the time being searched for and the metric recorded time in seconds -for i, evaluation in enumerate(algorithm_input.evaluations): +for i, timestamped_replica in enumerate(algorithm_input.replica_history): try: - created = datetime.strptime(evaluation.created, "%Y-%m-%dT%H:%M:%SZ") + created = datetime.strptime(timestamped_replica.time, "%Y-%m-%dT%H:%M:%SZ") except ValueError as ex: print(f"Invalid datetime format: {str(ex)}", file=sys.stderr) sys.exit(1) x.append(search_time - datetime.timestamp(created)) - y.append(evaluation.val.target_replicas) + y.append(timestamped_replica.replicas) # Add constant for OLS, constant is 1.0 x = sm.add_constant(x) diff --git a/algorithms/linear_regression/test_linear_regression.py b/algorithms/linear_regression/test_linear_regression.py index 97251ae..dfc1a26 100644 --- a/algorithms/linear_regression/test_linear_regression.py +++ b/algorithms/linear_regression/test_linear_regression.py @@ -1,4 +1,4 @@ -# Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. +# Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -45,13 +45,10 @@ def test_linear_regression(subtests): "", "stdin": """{ - "evaluations": [ + "replicaHistory": [ { - "id": 0, - "created": "2020-02-01T00:55:33Z", - "val": { - "targetReplicas": 2 - } + "time": "2020-02-01T00:55:33Z", + "replicas": 2 } ] }""" @@ -67,13 +64,10 @@ def test_linear_regression(subtests): "stdin": """{ "lookAhead": 10, - "evaluations": [ + "replicaHistory": [ { - "id": 0, - "created": "invalid", - "val": { - "targetReplicas": 2 - } + "time": "invalid", + "replicas": 2 } ] }""" @@ -89,8 +83,8 @@ def test_linear_regression(subtests): "stdin": """{ "lookAhead": 15000, - "current_time": "invalid", - "evaluations": [] + "currentTime": "invalid", + "replicaHistory": [] }""" }, { "description": @@ -104,35 +98,23 @@ def test_linear_regression(subtests): "stdin": """{ "lookAhead": 0, - "current_time": "2020-02-01T00:56:12Z", - "evaluations": [ + "currentTime": "2020-02-01T00:56:12Z", + "replicaHistory": [ { - "id": 0, - "created": "2020-02-01T00:55:33Z", - "val": { - "targetReplicas": 1 - } + "replicas": 1, + "time": "2020-02-01T00:55:33Z" }, { - "id": 1, - "created": "2020-02-01T00:55:43Z", - "val": { - "targetReplicas": 2 - } + "replicas": 2, + "time": "2020-02-01T00:55:43Z" }, { - "id": 2, - "created": "2020-02-01T00:55:53Z", - "val": { - "targetReplicas": 3 - } + "replicas": 3, + "time": "2020-02-01T00:55:53Z" }, { - "id": 3, - "created": "2020-02-01T00:56:03Z", - "val": { - "targetReplicas": 4 - } + "replicas": 4, + "time": "2020-02-01T00:56:03Z" } ] }""" @@ -148,35 +130,23 @@ def test_linear_regression(subtests): "stdin": """{ "lookAhead": 10000, - "current_time": "2020-02-01T00:56:12Z", - "evaluations": [ + "currentTime": "2020-02-01T00:56:12Z", + "replicaHistory": [ { - "id": 0, - "created": "2020-02-01T00:55:33Z", - "val": { - "targetReplicas": 1 - } + "replicas": 1, + "time": "2020-02-01T00:55:33Z" }, { - "id": 1, - "created": "2020-02-01T00:55:43Z", - "val": { - "targetReplicas": 2 - } + "replicas": 2, + "time": "2020-02-01T00:55:43Z" }, { - "id": 2, - "created": "2020-02-01T00:55:53Z", - "val": { - "targetReplicas": 3 - } + "replicas": 3, + "time": "2020-02-01T00:55:53Z" }, { - "id": 3, - "created": "2020-02-01T00:56:03Z", - "val": { - "targetReplicas": 4 - } + "replicas": 4, + "time": "2020-02-01T00:56:03Z" } ] }""" @@ -192,35 +162,23 @@ def test_linear_regression(subtests): "stdin": """{ "lookAhead": 15000, - "current_time": "2020-02-01T00:56:12Z", - "evaluations": [ + "currentTime": "2020-02-01T00:56:12Z", + "replicaHistory": [ { - "id": 0, - "created": "2020-02-01T00:55:33Z", - "val": { - "targetReplicas": 1 - } + "replicas": 1, + "time": "2020-02-01T00:55:33Z" }, { - "id": 1, - "created": "2020-02-01T00:55:43Z", - "val": { - "targetReplicas": 2 - } + "replicas": 2, + "time": "2020-02-01T00:55:43Z" }, { - "id": 2, - "created": "2020-02-01T00:55:53Z", - "val": { - "targetReplicas": 3 - } + "replicas": 3, + "time": "2020-02-01T00:55:53Z" }, { - "id": 3, - "created": "2020-02-01T00:56:03Z", - "val": { - "targetReplicas": 4 - } + "replicas": 4, + "time": "2020-02-01T00:56:03Z" } ] }""" diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go new file mode 100644 index 0000000..fb41dd5 --- /dev/null +++ b/api/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha1 contains API Schema definitions for the jamiethompson.me v1alpha1 API group +//+kubebuilder:object:generate=true +//+groupName=jamiethompson.me +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "jamiethompson.me", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/v1alpha1/predictivehorizontalpodautoscaler_types.go b/api/v1alpha1/predictivehorizontalpodautoscaler_types.go new file mode 100644 index 0000000..04aff47 --- /dev/null +++ b/api/v1alpha1/predictivehorizontalpodautoscaler_types.go @@ -0,0 +1,328 @@ +/* +Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + autoscalingv2 "k8s.io/api/autoscaling/v2beta2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // DecisionMaximum means use the highest predicted value from the models + DecisionMaximum = "maximum" + // DecisionMinimum means use the lowest predicted value from the models + DecisionMinimum = "minimum" + // DecisionMean means use the mean average of predicted values + DecisionMean = "mean" + // DecisionMedian means use the median average of predicted values + DecisionMedian = "median" +) + +const ( + TypeHoltWinters = "HoltWinters" + TypeLinear = "Linear" +) + +const ( + HookTypeHTTP = "http" +) + +// HookDefinition describes a hook for passing data/triggering logic, such as through a shell command +type HookDefinition struct { + // +kubebuilder:validation:Enum=http + Type string `json:"type"` + // +kubebuilder:validation:Minimum=1 + Timeout int `json:"timeout"` + // +optional + HTTP *HTTPHook `json:"http"` +} + +// HTTPHook describes configuration options for an HTTP request hook +type HTTPHook struct { + // +kubebuilder:validation:Enum=GET;HEAD;POST;PUT;DELETE;CONNECT;OPTIONS;TRACE;PATCH + Method string `json:"method"` + URL string `json:"url"` + Headers map[string]string `json:"headers,omitempty"` + SuccessCodes []int `json:"successCodes"` + // +kubebuilder:validation:Enum=query;body + ParameterMode string `json:"parameterMode"` +} + +// Linear represents a linear regression prediction model configuration +type Linear struct { + // historySize is how many timestamped replica counts should be stored for this linear regression, with older + // timestamped replica counts being removed from the data as new ones are added. For example a value of 6 means + // there will only be a maxmimu of 6 stored timestamped replica counts for this model. + // +kubebuilder:validation:Minimum=1 + HistorySize int `json:"historySize"` + // lookAhead is how far in the future should the linear regression predict in seconds. For example a value of 10 + // will predict 10 seconds into the future + // +kubebuilder:validation:Minimum=1 + LookAhead int `json:"lookAhead"` +} + +// HoltWinters represents a holt-winters exponential smoothing prediction model configuration +type HoltWinters struct { + // +kubebuilder:validation:Minimum=0 + // +optional + Alpha *float64 `json:"alpha"` + + // +kubebuilder:validation:Minimum=0 + // +optional + Beta *float64 `json:"beta"` + + // +kubebuilder:validation:Minimum=0 + // +optional + Gamma *float64 `json:"gamma"` + + // +kubebuilder:validation:Enum=add;additive;mul;multiplicative + Trend string `json:"trend"` + + // +kubebuilder:validation:Enum=add;additive;mul;multiplicative + Seasonal string `json:"seasonal"` + + // +kubebuilder:validation:Minimum=1 + SeasonalPeriods int `json:"seasonalPeriods"` + + // +kubebuilder:validation:Minimum=1 + StoredSeasons int `json:"storedSeasons"` + + // +optional + DampedTrend *bool `json:"dampedTrend"` + + // +optional + // +kubebuilder:validation:Enum=estimated;heuristic;known;legacy-heuristic + InitializationMethod *string `json:"initializationMethod"` + + // +optional + InitialLevel *float64 `json:"initialLevel"` + + // +optional + InitialTrend *float64 `json:"initialTrend"` + + // +optional + InitialSeasonal *float64 `json:"initialSeasonal"` + + // +optional + RuntimeTuningFetchHook *HookDefinition `json:"runtimeTuningFetchHook"` +} + +// Model represents a prediction model to use, e.g. a linear regression +type Model struct { + // type is the type of the model, for example 'Linear'. To see a full list of supported model types visit + // https://predictive-horizontal-pod-autoscaler.readthedocs.io/en/latest/user-guide/models/. + // +kubebuilder:validation:Enum=Linear;HoltWinters + Type string `json:"type"` + + // name is the name of the model, this can be any arbitrary name and is just used to distinguish between models if + // you have multiple and to keep track of model data if you modify your model parameters. + Name string `json:"name"` + + // calculationTimeout is how long the PHPA should allow for the model to calculate a value in milliseconds, if it + // takes longer than this timeout it should skip processing the model. + // Default varies based on model type: + // Linear is 30000 milliseconds (30 seconds) + // +kubebuilder:validation:Minimum=1 + // +optional + CalculationTimeout *int `json:"calculationTimeout"` + + // perSyncPeriod is how frequently this model will run, with the syncPeriod as a base unit. This allows for you to + // have multiple models which run at different time intervals, or only run the model every x number of sync periods + // if the model is computation intensive. + // For sync periods that the model is not run on, it will still add the calculated replica values to the model data + // history and then prune that history if needs. + // Default value is 1 (run every sync period) + // +kubebuilder:validation:Minimum=1 + // +optional + PerSyncPeriod *int `json:"perSyncPeriod"` + + // linear is the configuration to use for the linear regression model, it will only be used if the type is set to + // 'Linear'. + // +optional + Linear *Linear `json:"linear"` + + // holtWinters is the configuration to use for the holt winters model, it will only be used if the type is set to + // 'HoltWinters' + // +optional + HoltWinters *HoltWinters `json:"holtWinters"` +} + +// TimestampedReplicas is a replica count paired with the time that the replica count was created at. +type TimestampedReplicas struct { + // time is the time that the replica count was created at. + Time *metav1.Time `json:"time"` + // replicas is the replica count at the time. + Replicas int32 `json:"replicas"` +} + +// PredictiveHorizontalPodAutoscalerData is the data storage format for the PHPA, this is stored in a ConfigMap +type PredictiveHorizontalPodAutoscalerData struct { + // modelHistories is a mapping of model names to model histories. This allows looking up a model's model history, + // while allowing all of the model histories for a single PHPA to be stored in a single place. + ModelHistories map[string]ModelHistory `json:"modelHistories"` +} + +// ModelHistory is the data stored for a single model's history, with all of the replica data the model is fed to +// calculate replicas. +type ModelHistory struct { + // type is the type of the model the history is for, useful to check for model mismatches with the data. + Type string `json:"type"` + // syncPeriodsPassed is the number of sync periods that have passed since the last time this model was used, used + // when determining if a model should run based on the perSyncPeriod + SyncPeriodsPassed int `json:"syncPeriodsPassed"` + // replicaHistory is a list of timestamped replicas, this data is fed into the model to calculate a predicted value. + ReplicaHistroy []TimestampedReplicas `json:"replicaHistory"` +} + +// PredictiveHorizontalPodAutoscalerSpec defines the desired state of PredictiveHorizontalPodAutoscaler +type PredictiveHorizontalPodAutoscalerSpec struct { + // scaleTargetRef points to the target resource to scale, and is used to the pods for which metrics + // should be collected, as well as to actually change the replica count. + ScaleTargetRef autoscalingv2.CrossVersionObjectReference `json:"scaleTargetRef"` + + // minReplicas is the lower limit for the number of replicas to which the autoscaler + // can scale down. It defaults to 1 pod. minReplicas is allowed to be 0 if at least one Object or + // External metric is configured. Scaling is active as long as at least one metric value is + // available. + // +kubebuilder:validation:Minimum=0 + // +optional + MinReplicas *int32 `json:"minReplicas"` + + // maxReplicas is the upper limit for the number of replicas to which the autoscaler can scale up. + // It cannot be less than minReplicas. + // +kubebuilder:validation:Minimum=1 + MaxReplicas int32 `json:"maxReplicas"` + + // metrics contains the specifications for which to use to calculate the desired replica count (the maximum replica + // count across all metrics will be used). The desired replica count is calculated multiplying the ratio between + // the target value and the current value by the current number of pods. Ergo, metrics used must decrease as the + // pod count is increased, and vice-versa. See the individual metric source types for more information about how + // each type of metric must respond. If not set, the default metric will be set to 80% average CPU utilization. + // +listType=atomic + // +optional + Metrics []autoscalingv2.MetricSpec `json:"metrics"` + + // downscaleStabilization defines in seconds the length of the downscale stabilization window; based on the + // Horizontal Pod Autoscaler downscale stabilization. Downscale stabilization works by recording all evaluations + // over the window specified and picking out the maximum target replicas from these evaluations. This results in a + // more smoothed downscaling and a cooldown, which can reduce the effect of thrashing. + // Default value 300 seconds (5 minutes). + // +kubebuilder:validation:Minimum=0 + // +optional + DownscaleStabilization *int `json:"downscaleStabilization"` + + // cpuInitializationPeriod is equivalent to --horizontal-pod-autoscaler-cpu-initialization-period; the period after + // pod start when CPU samples might be skipped. + // Default value 300 seconds (5 minutes). + // +kubebuilder:validation:Minimum=0 + // +optional + CPUInitializationPeriod *int `json:"cpuInitializationPeriod"` + + // initialReadinessDelay is equivalent to --horizontal-pod-autoscaler-initial-readiness-delay; the period after pod + // start during which readiness changes will be treated as initial readiness. + // Default value 30 seconds. + // +kubebuilder:validation:Minimum=0 + // +optional + InitialReadinessDelay *int `json:"initialReadinessDelay"` + + // tolerance is equivalent to --horizontal-pod-autoscaler-tolerance; the minimum change (from 1.0) in the + // desired-to-actual metrics ratio for the predictive horizontal pod autoscaler to consider scaling. + // Default value 0.1. + // +kubebuilder:validation:Minimum=0 + // +optional + Tolerance *float64 `json:"tolerance"` + + // syncPeriod is equivalent to --horizontal-pod-autoscaler-sync-period; the frequency with which the PHPA + // calculates replica counts and scales in milliseconds. + // Default value 15000 milliseconds (15 seconds). + // +kubebuilder:validation:Minimum=1 + // +optional + SyncPeriod *int `json:"syncPeriod"` + + // models is the list of models to apply to the calculated replica count to calculate predicted replica values. + // +kubebuilder:validation:Required + Models []Model `json:"models"` + + // decisionType is the strategy to use when picking which replica count to use if you have multiple models, or even + // just choosing between the calculculated replicas and the predicted replicas of a single model. For details on + // which decisionTypes are available visit + // https://predictive-horizontal-pod-autoscaler.readthedocs.io/en/latest/reference/configuration/#decisiontype + // Default strategy is 'maximum' + // +kubebuilder:validation:Enum=maximum;minimum;mean;median + // +optional + DecisionType *string `json:"decisionType"` +} + +// PredictiveHorizontalPodAutoscalerStatus defines the observed state of PredictiveHorizontalPodAutoscaler +type PredictiveHorizontalPodAutoscalerStatus struct { + // lastScaleTime is the last time the PredictiveHorizontalPodAutoscaler scaled the number of pods, + // used by the autoscaler to control how often the number of pods is changed. + // +optional + LastScaleTime *metav1.Time `json:"lastScaleTime,omitempty"` + + // replicaHistory is a timestamped history of all the calculated replica values, used for calculating downscale + // stabilization. + // +optional + ReplicaHistory []TimestampedReplicas `json:"replicaHistory"` + + // reference is the resource being referenced and targeted for scaling. + Reference string `json:"reference"` + + // currentReplicas is current number of replicas of pods managed by this autoscaler, + // as last seen by the autoscaler. + // +optional + CurrentReplicas int32 `json:"currentReplicas,omitempty"` + + // desiredReplicas is the desired number of replicas of pods managed by this autoscaler, + // as last calculated by the autoscaler. + DesiredReplicas int32 `json:"desiredReplicas"` + + // currentMetrics is the last read state of the metrics used by this autoscaler. + // +listType=atomic + // +optional + CurrentMetrics []autoscalingv2.MetricStatus `json:"currentMetrics"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:resource:shortName=phpa +// +kubebuilder:printcolumn:name="Reference",type="string",JSONPath=`.status.reference`,description="The identifier for the resource being scaled in the format /" +// +kubebuilder:printcolumn:name="Min Pods",type="integer",JSONPath=`.spec.minReplicas`,description="The minimum number of replicas of pods that the resource being managed by the autoscaler can have" +// +kubebuilder:printcolumn:name="Max Pods",type="integer",JSONPath=`.spec.maxReplicas`,description="The maximum number of replicas of pods that the resource being managed by the autoscaler can have" +// +kubebuilder:printcolumn:name="Replicas",type="integer",JSONPath=`.status.desiredReplicas`,description="The desired number of replicas of pods managed by this autoscaler as last calculated by the autoscaler" +// +kubebuilder:printcolumn:name="Last Scale Time",type="date",JSONPath=`.status.lastScaleTime`,description="The last time the PredictiveHorizontalPodAutoscaler scaled the number of pods" +// PredictiveHorizontalPodAutoscaler is the Schema for the predictivehorizontalpodautoscalers API +type PredictiveHorizontalPodAutoscaler struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PredictiveHorizontalPodAutoscalerSpec `json:"spec,omitempty"` + Status PredictiveHorizontalPodAutoscalerStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// PredictiveHorizontalPodAutoscalerList contains a list of PredictiveHorizontalPodAutoscaler +type PredictiveHorizontalPodAutoscalerList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []PredictiveHorizontalPodAutoscaler `json:"items"` +} + +func init() { + SchemeBuilder.Register(&PredictiveHorizontalPodAutoscaler{}, &PredictiveHorizontalPodAutoscalerList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..c238db8 --- /dev/null +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,404 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/api/autoscaling/v2beta2" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPHook) DeepCopyInto(out *HTTPHook) { + *out = *in + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.SuccessCodes != nil { + in, out := &in.SuccessCodes, &out.SuccessCodes + *out = make([]int, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPHook. +func (in *HTTPHook) DeepCopy() *HTTPHook { + if in == nil { + return nil + } + out := new(HTTPHook) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HoltWinters) DeepCopyInto(out *HoltWinters) { + *out = *in + if in.Alpha != nil { + in, out := &in.Alpha, &out.Alpha + *out = new(float64) + **out = **in + } + if in.Beta != nil { + in, out := &in.Beta, &out.Beta + *out = new(float64) + **out = **in + } + if in.Gamma != nil { + in, out := &in.Gamma, &out.Gamma + *out = new(float64) + **out = **in + } + if in.DampedTrend != nil { + in, out := &in.DampedTrend, &out.DampedTrend + *out = new(bool) + **out = **in + } + if in.InitializationMethod != nil { + in, out := &in.InitializationMethod, &out.InitializationMethod + *out = new(string) + **out = **in + } + if in.InitialLevel != nil { + in, out := &in.InitialLevel, &out.InitialLevel + *out = new(float64) + **out = **in + } + if in.InitialTrend != nil { + in, out := &in.InitialTrend, &out.InitialTrend + *out = new(float64) + **out = **in + } + if in.InitialSeasonal != nil { + in, out := &in.InitialSeasonal, &out.InitialSeasonal + *out = new(float64) + **out = **in + } + if in.RuntimeTuningFetchHook != nil { + in, out := &in.RuntimeTuningFetchHook, &out.RuntimeTuningFetchHook + *out = new(HookDefinition) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HoltWinters. +func (in *HoltWinters) DeepCopy() *HoltWinters { + if in == nil { + return nil + } + out := new(HoltWinters) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HookDefinition) DeepCopyInto(out *HookDefinition) { + *out = *in + if in.HTTP != nil { + in, out := &in.HTTP, &out.HTTP + *out = new(HTTPHook) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HookDefinition. +func (in *HookDefinition) DeepCopy() *HookDefinition { + if in == nil { + return nil + } + out := new(HookDefinition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Linear) DeepCopyInto(out *Linear) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Linear. +func (in *Linear) DeepCopy() *Linear { + if in == nil { + return nil + } + out := new(Linear) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Model) DeepCopyInto(out *Model) { + *out = *in + if in.CalculationTimeout != nil { + in, out := &in.CalculationTimeout, &out.CalculationTimeout + *out = new(int) + **out = **in + } + if in.PerSyncPeriod != nil { + in, out := &in.PerSyncPeriod, &out.PerSyncPeriod + *out = new(int) + **out = **in + } + if in.Linear != nil { + in, out := &in.Linear, &out.Linear + *out = new(Linear) + **out = **in + } + if in.HoltWinters != nil { + in, out := &in.HoltWinters, &out.HoltWinters + *out = new(HoltWinters) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Model. +func (in *Model) DeepCopy() *Model { + if in == nil { + return nil + } + out := new(Model) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ModelHistory) DeepCopyInto(out *ModelHistory) { + *out = *in + if in.ReplicaHistroy != nil { + in, out := &in.ReplicaHistroy, &out.ReplicaHistroy + *out = make([]TimestampedReplicas, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ModelHistory. +func (in *ModelHistory) DeepCopy() *ModelHistory { + if in == nil { + return nil + } + out := new(ModelHistory) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PredictiveHorizontalPodAutoscaler) DeepCopyInto(out *PredictiveHorizontalPodAutoscaler) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PredictiveHorizontalPodAutoscaler. +func (in *PredictiveHorizontalPodAutoscaler) DeepCopy() *PredictiveHorizontalPodAutoscaler { + if in == nil { + return nil + } + out := new(PredictiveHorizontalPodAutoscaler) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PredictiveHorizontalPodAutoscaler) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PredictiveHorizontalPodAutoscalerData) DeepCopyInto(out *PredictiveHorizontalPodAutoscalerData) { + *out = *in + if in.ModelHistories != nil { + in, out := &in.ModelHistories, &out.ModelHistories + *out = make(map[string]ModelHistory, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PredictiveHorizontalPodAutoscalerData. +func (in *PredictiveHorizontalPodAutoscalerData) DeepCopy() *PredictiveHorizontalPodAutoscalerData { + if in == nil { + return nil + } + out := new(PredictiveHorizontalPodAutoscalerData) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PredictiveHorizontalPodAutoscalerList) DeepCopyInto(out *PredictiveHorizontalPodAutoscalerList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PredictiveHorizontalPodAutoscaler, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PredictiveHorizontalPodAutoscalerList. +func (in *PredictiveHorizontalPodAutoscalerList) DeepCopy() *PredictiveHorizontalPodAutoscalerList { + if in == nil { + return nil + } + out := new(PredictiveHorizontalPodAutoscalerList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PredictiveHorizontalPodAutoscalerList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PredictiveHorizontalPodAutoscalerSpec) DeepCopyInto(out *PredictiveHorizontalPodAutoscalerSpec) { + *out = *in + out.ScaleTargetRef = in.ScaleTargetRef + if in.MinReplicas != nil { + in, out := &in.MinReplicas, &out.MinReplicas + *out = new(int32) + **out = **in + } + if in.Metrics != nil { + in, out := &in.Metrics, &out.Metrics + *out = make([]v2beta2.MetricSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.DownscaleStabilization != nil { + in, out := &in.DownscaleStabilization, &out.DownscaleStabilization + *out = new(int) + **out = **in + } + if in.CPUInitializationPeriod != nil { + in, out := &in.CPUInitializationPeriod, &out.CPUInitializationPeriod + *out = new(int) + **out = **in + } + if in.InitialReadinessDelay != nil { + in, out := &in.InitialReadinessDelay, &out.InitialReadinessDelay + *out = new(int) + **out = **in + } + if in.Tolerance != nil { + in, out := &in.Tolerance, &out.Tolerance + *out = new(float64) + **out = **in + } + if in.SyncPeriod != nil { + in, out := &in.SyncPeriod, &out.SyncPeriod + *out = new(int) + **out = **in + } + if in.Models != nil { + in, out := &in.Models, &out.Models + *out = make([]Model, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.DecisionType != nil { + in, out := &in.DecisionType, &out.DecisionType + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PredictiveHorizontalPodAutoscalerSpec. +func (in *PredictiveHorizontalPodAutoscalerSpec) DeepCopy() *PredictiveHorizontalPodAutoscalerSpec { + if in == nil { + return nil + } + out := new(PredictiveHorizontalPodAutoscalerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PredictiveHorizontalPodAutoscalerStatus) DeepCopyInto(out *PredictiveHorizontalPodAutoscalerStatus) { + *out = *in + if in.LastScaleTime != nil { + in, out := &in.LastScaleTime, &out.LastScaleTime + *out = (*in).DeepCopy() + } + if in.ReplicaHistory != nil { + in, out := &in.ReplicaHistory, &out.ReplicaHistory + *out = make([]TimestampedReplicas, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.CurrentMetrics != nil { + in, out := &in.CurrentMetrics, &out.CurrentMetrics + *out = make([]v2beta2.MetricStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PredictiveHorizontalPodAutoscalerStatus. +func (in *PredictiveHorizontalPodAutoscalerStatus) DeepCopy() *PredictiveHorizontalPodAutoscalerStatus { + if in == nil { + return nil + } + out := new(PredictiveHorizontalPodAutoscalerStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TimestampedReplicas) DeepCopyInto(out *TimestampedReplicas) { + *out = *in + if in.Time != nil { + in, out := &in.Time, &out.Time + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TimestampedReplicas. +func (in *TimestampedReplicas) DeepCopy() *TimestampedReplicas { + if in == nil { + return nil + } + out := new(TimestampedReplicas) + in.DeepCopyInto(out) + return out +} diff --git a/build/Dockerfile b/build/Dockerfile deleted file mode 100644 index 39e2305..0000000 --- a/build/Dockerfile +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Build stage, must be done in Docker for cross compatiblity because CGO is required for go-sqlite3 -FROM golang:1.17 AS build - -WORKDIR /app - -COPY . ./ - -RUN make - -# CPA base image -FROM custompodautoscaler/python-3-8:v2.7.0 - -ENV USER_UID=1001 \ - USER_NAME=phpa - -# Install sqlite and container deps -RUN apt-get -y update && apt-get install -y sqlite3 libsqlite3-dev - -WORKDIR /app - -# Install any python requirements -COPY algorithms/requirements.txt ./algorithms/requirements.txt -RUN pip install -r ./algorithms/requirements.txt - -ENV configPath /app/config.yaml -# Add in entry point script -COPY build/entrypoint.sh / -# Add in config and horizontal-pod-autoscaler binary -COPY config.yaml ./ -# Add in db migrations -COPY sql ./sql -# Add in Python aglorithms -COPY algorithms ./algorithms -# Add in built binary -COPY --from=build /app/dist/* ./ - -RUN mkdir -p ${HOME} && \ - chown ${USER_UID}:0 ${HOME} && \ - chmod ug+rwx $HOME && \ - chmod g+rw /etc/passwd - -RUN mkdir /store && \ - chown ${USER_UID}:0 /store - -RUN useradd -u ${USER_UID} ${USER_NAME} - -USER ${USER_UID} - -CMD [ "/entrypoint.sh" ] diff --git a/build/entrypoint.sh b/build/entrypoint.sh deleted file mode 100755 index 2c3e3f8..0000000 --- a/build/entrypoint.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/sh -# Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -e - -echo "Setting up local database" -/app/predictive-horizontal-pod-autoscaler -mode=setup - -echo "Starting autoscaler" - -exec /app/custom-pod-autoscaler diff --git a/config.yaml b/config.yaml deleted file mode 100644 index a0512dc..0000000 --- a/config.yaml +++ /dev/null @@ -1,22 +0,0 @@ -evaluate: - type: shell - timeout: 5000 - shell: - entrypoint: "/bin/sh" - command: - - "-c" - - "/app/predictive-horizontal-pod-autoscaler -mode=evaluate" -metric: - type: shell - timeout: 5000 - shell: - entrypoint: "/bin/sh" - command: - - "-c" - - "/app/predictive-horizontal-pod-autoscaler -mode=metric" -runMode: per-resource -# Default values, matching K8s Horizontal Pod Autoscaler defaults -downscaleStabilization: 300 -interval: 15000 -minReplicas: 1 -maxReplicas: 10 \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 34d0f2a..3545b8c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,6 +3,7 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/jthomperoo/predictive-horizontal-pod-autoscaler)](https://goreportcard.com/report/github.com/jthomperoo/predictive-horizontal-pod-autoscaler) [![Documentation Status](https://readthedocs.org/projects/predictive-horizontal-pod-autoscaler/badge/?version=latest)](https://predictive-horizontal-pod-autoscaler.readthedocs.io/en/latest) [![License](https://img.shields.io/:license-apache-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) + # Predictive Horizontal Pod Autoscaler Visit the GitHub repository at to see examples, @@ -10,13 +11,15 @@ raise issues, and to contribute to the project. # What is it? -This is a [Custom Pod Autoscaler](https://www.github.com/jthomperoo/custom-pod-autoscaler); aiming to have identical -functionality to the Horizontal Pod Autoscaler, however with added predictive elements using statistical models. +Predictive Horizontal Pod Autoscalers (PHPAs) are Horizontal Pod Autoscalers (HPAs) with extra predictive capabilities +baked in, allowing you to apply statistical models to the results of HPA calculations to make proactive scaling +decisions. # Why would I use it? This autoscaler lets you choose models and fine tune them in order to predict how many replicas a resource should have, -preempting events such as regular, repeated high load. +preempting events such as regular, repeated high load. This allows for proactive rather than simply reactive scaling +that can make intelligent ahead of time decisions. # What systems would need it? @@ -38,4 +41,3 @@ solutions such as EKS or GCP. * Downscale Stabilization. * Sync Period. * Initial Readiness Delay. -* Runs in Kubernetes as a standard Pod. diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index b4d22dc..a741795 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -1,145 +1,120 @@ -# Predictive Configuration +# Configuration -Beyond specifying models, other configuration options can be set in the `predictiveConfig` YAML: +Predictive Horizontal Pod Autoscalers have a number of configuration options available. -## Providing Predictive Configuration - -Predictive Configuration is provided through environment variables, which can be supplied through the Custom Pod -Autoscaler YAML shorthand: +## minReplicas ```yaml -apiVersion: custompodautoscaler.com/v1 -kind: CustomPodAutoscaler -metadata: - name: simple-linear-example -spec: - template: - spec: - containers: - - name: simple-linear-example - image: jthomperoo/predictive-horizontal-pod-autoscaler:latest - imagePullPolicy: Always - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: php-apache - roleRequiresMetricsServer: true - config: - - name: minReplicas - value: "1" - - name: maxReplicas - value: "5" - - name: predictiveConfig - value: | - models: - - type: Linear - name: LinearPrediction - perInterval: 1 - linear: - lookAhead: 10000 - storedValues: 6 - decisionType: "maximum" - metrics: - - type: Resource - resource: - name: cpu - target: - averageUtilization: 50 - type: Utilization - - name: interval - value: "10000" +minReplicas: 2 ``` -The predictiveConfig is provided through this environment variable, and represented in YAML: -```yaml - - name: predictiveConfig - value: | - models: - - type: Linear - name: LinearPrediction - perInterval: 1 - linear: - lookAhead: 10000 - storedValues: 6 - decisionType: "maximum" -``` +The lower limit for the number of replicas to which the autoscaler can scale down to. `minReplicas` is allowed to be 0 +if at least one Object or External metric is configured. Scaling is active as long as at least one metric value is +available. -## decisionType +Default value: `1`. + +## maxReplicas -Example: ```yaml -decisionType: mean +maxReplicas: 15 ``` -Default value: `maximum`. -Possible values: - -- **maximum** - pick the highest evaluation of the models. -- **minimum** - pick the lowest evaluation of the models. -- **mean** - calculate the mean number of replicas between the models. -- **median** - calculate the median number of replicas between the models. +The upper limit for the number of replicas to which the autoscaler can scale up. +It cannot be less than minReplicas. -Decider on which evaluation to pick if there are multiple models provided. +Default value: `10`. -## dbPath +## syncPeriod -Example: ```yaml -dbPath: "/tmp/path/store.db" +syncPeriod: 10000 ``` -Default value: `/store/predictive-horizontal-pod-autoscaler.db`. -The path to store the SQLite3 database, e.g. for storing the DB in a volume to persist it. +Equivalent to `--horizontal-pod-autoscaler-sync-period`; the frequency with which the PHPA calculates replica counts and +scales in milliseconds. -## migrationPath +Set in milliseconds. -Example: -```yaml -migrationPath: "/tmp/migrations/sql" -``` +Default value: `15000` (15 seconds). -Default value: `/app/sql`. +Set in milliseconds. -The path of the SQL migrations for the SQLite3 database. +## downscaleStabilization -## models +```yaml +downscaleStabilization: 150 +``` -List of statistical models to apply. -See [the models section for details](../../user-guide/models). +Equivalent to `--horizontal-pod-autoscaler-downscale-stabilization`; the length of the downscale stabilization window. +Downscale stabilization works by recording all evaluations over the window specified and picking out the maximum target +replicas from these evaluations. This results in a more smoothed downscaling and a cooldown, which can reduce the +effect of thrashing. -## metrics +Set in seconds. -List of metrics to target for evaluating replica counts. -See [the metrics section for details](../../user-guide/metrics). +Default value: `300` (5 minutes). ## cpuInitializationPeriod -Example: ```yaml cpuInitializationPeriod: 150 ``` -Default value: `300` (5 minutes). -Set in seconds. + Equivalent to `--horizontal-pod-autoscaler-cpu-initialization-period`; the period after pod start when CPU samples might be skipped. +Set in seconds. + +Default value: `300` (5 minutes). + ## initialReadinessDelay -Example: ```yaml initialReadinessDelay: 45 ``` -Default value: `30` (30 seconds). -Set in seconds. + Equivalent to `--horizontal-pod-autoscaler-initial-readiness-delay`; the period after pod start during which readiness changes will be treated as initial readiness. +Set in seconds. + +Default value: `30` (30 seconds). + ## tolerance -Example: ```yaml tolerance: 0.25 ``` -Default value: `0.1`. + Equivalent to `--horizontal-pod-autoscaler-tolerance`; the minimum change (from 1.0) in the desired-to-actual metrics ratio for the horizontal pod autoscaler to consider scaling. + +Default value: `0.1`. + +## decisionType + +```yaml +decisionType: mean +``` + +Decider on which evaluation to pick if there are multiple models provided. + +Possible values: + +- **maximum** - pick the highest evaluation of the models. +- **minimum** - pick the lowest evaluation of the models. +- **mean** - calculate the mean number of replicas between the models. +- **median** - calculate the median number of replicas between the models. + +Default value: `maximum`. + +## models + +List of statistical models to apply. +See [the models section for details](../../user-guide/models). + +## metrics + +List of metrics to target for evaluating replica counts. +See [the metrics section for details](../../user-guide/metrics). diff --git a/docs/requirements.txt b/docs/requirements.txt index 87bd8d6..0d87251 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ -mkdocs==1.2.3 +mkdocs==1.3.1 mdx-truly-sane-lists==1.2 -mkdocs-material==5.5.5 -mkdocs-material-extensions==1.0 +mkdocs-material==8.3.9 +mkdocs-material-extensions==1.0.3 diff --git a/docs/user-guide/cpu-initialization-period.md b/docs/user-guide/cpu-initialization-period.md deleted file mode 100644 index 05aadb7..0000000 --- a/docs/user-guide/cpu-initialization-period.md +++ /dev/null @@ -1,9 +0,0 @@ -# CPU Initialization Period - -The equivalent of the Kubernetes HPA `--horizontal-pod-autoscaler-cpu-initialization-period` flag can be set by -providing the parameter `cpuInitializationPeriod` in the predicitve config YAML. Unlike the HPA this does not need to -be set as a flag for the kube-controller-manager on the master node, and can be autoscaler specific. - -This option is set in seconds. -This option has a default value of `300` (5 minutes). -See the [configuration reference for more details](../../reference/configuration#cpuinitializationperiod). diff --git a/docs/user-guide/downscale-stabilization.md b/docs/user-guide/downscale-stabilization.md deleted file mode 100644 index 20cff04..0000000 --- a/docs/user-guide/downscale-stabilization.md +++ /dev/null @@ -1,14 +0,0 @@ -# Downscale Stabilization - -The equivalent of the Kubernetes HPA `--horizontal-pod-autoscaler-downscale-stabilization` flag can be set by providing -the parameter `downscaleStabilization` in the as a config option in the autoscaler YAML. Unlike the HPA this does not -need to be set as a flag for the kube-controller-manager on the master node, and can be autoscaler specific. - -DownscaleStabilization can be set at deploy time; and are configuration options that are part of the [Custom Pod -Autoscaler Framework](https://custom-pod-autoscaler.readthedocs.io/en/latest). - -This option is set in seconds. -This option has a default value of `300` (5 minutes). -This option is part of the Custom Pod Autoscaler Framework, for more [information please view the Custom Pod Autoscaler -Wiki configuration -reference](https://custom-pod-autoscaler.readthedocs.io/en/latest/reference/configuration/#downscalestabilization). diff --git a/docs/user-guide/getting-started.md b/docs/user-guide/getting-started.md index 69129a5..9b0e630 100644 --- a/docs/user-guide/getting-started.md +++ b/docs/user-guide/getting-started.md @@ -13,6 +13,7 @@ This guide requires the following tools installed: - [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl) == `v1.X` - [helm](https://helm.sh/docs/intro/install/) == `v3.X` - [k3d](https://k3d.io/#installation) == `v4.X` +- [jq](https://stedolan.github.io/jq/) >= `1.6` ## Set up the cluster @@ -26,29 +27,42 @@ To provision a new cluster using k3d run the following command: k3d cluster create phpa-test-cluster ``` -## Install the Custom Pod Autoscaler Operator +## Install the Predictive Horizontal Pod Autoscaler Operator onto your cluster -The PHPA requires the [Custom Pod Autoscaler Operator -(CPAO)](https://github.com/jthomperoo/custom-pod-autoscaler-operator) to handle management of autoscalers. +Installing PHPAs requires you to have installed the PHPA operator first onto your cluster. -In this guide we are using `v1.1.1` of the CPAO, but check out the [installation -guide](https://github.com/jthomperoo/custom-pod-autoscaler-operator/blob/master/INSTALL.md) for more up to date -instructions for later releases. +In this guide we are using `v0.11.0` of the PHPA operator, but check out the [installation +guide](./installation.md) for more up to date instructions for later releases. -Run the following commands to install `v1.1.1` of the CPAO: +Run the following commands to install the PHPA operator: ```bash -VERSION=v1.1.1 -HELM_CHART=custom-pod-autoscaler-operator -helm install ${HELM_CHART} https://github.com/jthomperoo/custom-pod-autoscaler-operator/releases/download/${VERSION}/custom-pod-autoscaler-operator-${VERSION}.tgz +VERSION=v0.11.0 +HELM_CHART=predictive-horizontal-pod-autoscaler-operator +helm install ${HELM_CHART} https://github.com/jthomperoo/predictive-horizontal-pod-autoscaler/releases/download/${VERSION}/predictive-horizontal-pod-autoscaler-${VERSION}.tgz ``` -You can check the CPAO has been deployed properly by running: +You can check the PHPA operator has been deployed properly by running: ```bash -kubectl get pods +helm status predictive-horizontal-pod-autoscaler-operator +``` + +You should get a response like this: + +```bash +NAME: predictive-horizontal-pod-autoscaler-operator +LAST DEPLOYED: Thu Jul 21 20:29:06 2022 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None +NOTES: +Thanks for installing predictive-horizontal-pod-autoscaler. ``` +If you get a response that says release not found then the install has not worked correctly. + ## Create a deployment to autoscale We now need to create a test application to scale up and down based on load. In this guide we are using an example @@ -130,48 +144,34 @@ Now we need to set up the autoscaler. This autoscaler will be configured to watc apply a linear regression to predict ahead of time what the replica count should be. ```yaml -apiVersion: custompodautoscaler.com/v1 -kind: CustomPodAutoscaler +apiVersion: jamiethompson.me/v1alpha1 +kind: PredictiveHorizontalPodAutoscaler metadata: - name: simple-linear-example + name: simple-linear spec: - template: - spec: - containers: - - name: simple-linear-example - image: jthomperoo/predictive-horizontal-pod-autoscaler:latest - imagePullPolicy: Always scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: php-apache - roleRequiresMetricsServer: true - config: - - name: minReplicas - value: "1" - - name: maxReplicas - value: "10" - - name: predictiveConfig - value: | - models: - - type: Linear - name: LinearPrediction - perInterval: 1 - linear: - lookAhead: 10000 - storedValues: 6 - decisionType: "maximum" - metrics: - - type: Resource - resource: - name: cpu - target: - averageUtilization: 50 - type: Utilization - - name: interval - value: "10000" - - name: downscaleStabilization - value: "0" + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + averageUtilization: 50 + type: Utilization + models: + - type: Linear + name: simple-linear + perSyncPeriod: 1 + linear: + lookAhead: 10000 + historySize: 6 + decisionType: "maximum" + syncPeriod: 10000 + downscaleStabilization: 0 ``` This autoscaler works by using the same logic that the Horizontal Pod Autoscaler uses to calculate the number of @@ -207,60 +207,60 @@ scaleTargetRef: - The minimum and maximum replicas that the deployment can be autoscaled to are set to the range `0-10`: ```yaml -- name: minReplicas - value: "1" -- name: maxReplicas - value: "10" +minReplicas: 1 +maxReplicas: 10 ``` - The frequency that the autoscaler calculates a new target replica value is set to 10 seconds (`10000 ms`). ```yaml -- name: interval - value: "10000" +syncPeriod: 10000 ``` - The *downscale stabilization* value for the autoscaler is set to `0`, meaning it will only use the latest autoscaling -target and will not pick the highest across a window of time. For more information around this check out the [Custom Pod -Autoscaler wiki](https://custom-pod-autoscaler.readthedocs.io/en/stable/user-guide/cooldown/). +target and will not pick the highest across a window of time. ```yaml -- name: downscaleStabilization - value: "0" +downscaleStabilization: 0 ``` -- The actual autoscaling decisions are defined in the *predictive config*. - - The *model* is configured as a linear regression model. - - The linear regression is set to run every time the autoscaler is run (every interval), in this example it is - every 10 seconds (`perInterval: 1`). - - The linear regression is predicting 10 seconds into the future (`lookAhead: 10000`). - - The linear regression uses a maximum of `6` previous target values for predicting (`storedValues: 6`). - - The `decisionType` is set to be `maximum`, meaning that the target replicas will be set to whichever is higher +- A single *model* is configured as a linear regression model. + - The linear regression is set to run every time the autoscaler is run (every sync period), in this example it is + every 10 seconds (`perSyncPeriod: 1`). + - The linear regression is predicting 10 seconds into the future (`lookAhead: 10000`). + - The linear regression uses a maximum of `6` previous target values for predicting (`storedValues: 6`). + +```yaml +models: + - type: Linear + name: simple-linear + perSyncPeriod: 1 + linear: + lookAhead: 10000 + historySize: 6 +``` + +- The `decisionType` is set to be `maximum`, meaning that the target replicas will be set to whichever is higher between the calculated HPA value and the predicted model value. - - The *metrics* defines the normal Horizontal Pod Autoscaler rules to apply for autoscaling, the results of which + +```yaml +decisionType: "maximum" +``` + +- The *metrics* defines the normal Horizontal Pod Autoscaler rules to apply for autoscaling, the results of which will have the models applied to for prediction. - The metric targeted is the CPU resource of the deployment. - The targeted value is that CPU utilization across the test application's containers should be `50%`, if it goes too far above this there are not enough pods, and if it goes too far below this there are too many pods. ```yaml -- name: predictiveConfig - value: | - models: - - type: Linear - name: LinearPrediction - perInterval: 1 - linear: - lookAhead: 10000 - storedValues: 6 - decisionType: "maximum" - metrics: - - type: Resource - resource: - name: cpu - target: - averageUtilization: 50 - type: Utilization +metrics: + - type: Resource + resource: + name: cpu + target: + averageUtilization: 50 + type: Utilization ``` Now deploy the autoscaler to the K8s cluster by running: @@ -272,7 +272,7 @@ kubectl apply -f phpa.yaml You can check the autoscaler has been deployed by running: ```bash -kubectl get pods +kubectl get phpa simple-linear ``` ## Apply load and monitor the autoscaling process @@ -280,16 +280,21 @@ kubectl get pods You can monitor the autoscaling process by running: ```bash -kubectl logs simple-linear-example --follow +kubectl logs -l name=predictive-horizontal-pod-autoscaler -f ``` +This is looking at the operators logs, these are the brains of the autoscaling program and will report how all +autoscaling decisions are done. + You can see the targets calculated by the HPA logic before the linear regression has been applied to them by querying -the autoscaler's internal database: +the autoscaler's config map: ```bash -kubectl exec -it simple-linear-example -- sqlite3 /store/predictive-horizontal-pod-autoscaler.db 'SELECT * FROM evaluation;' +kubectl get configmap predictive-horizontal-pod-autoscaler-simple-linear-data -o=json | jq -r '.data.data | fromjson | .modelHistories["simple-linear"].replicaHistory[] | .time,.replicas' ``` +This prints out all of the timestamped replica counts that the PHPA will use for its prediction. + You can increase the load by starting a new container, and looping to send a bunch of HTTP requests to our test application: @@ -313,7 +318,7 @@ values and the target values predicted by the linear regression. Once you have finished testing the autoscaler, you can clean up any K8s resources by running: ```bash -HELM_CHART=custom-pod-autoscaler-operator +HELM_CHART=predictive-horizontal-pod-autoscaler-operator kubectl delete -f deployment.yaml kubectl delete -f phpa.yaml helm uninstall ${HELM_CHART} diff --git a/docs/user-guide/hooks.md b/docs/user-guide/hooks.md index b29294e..c1ff520 100644 --- a/docs/user-guide/hooks.md +++ b/docs/user-guide/hooks.md @@ -2,70 +2,12 @@ Hooks specify how user logic should be called by the Predictive Horizontal Pod Autoscaler. -## shell - -The shell hook allows specifying a shell command, run through `/bin/sh`. Any relevant information will be provided to -the command specified by piping the information in through standard in. Data is returned by writing to standard out. -An error is signified by exiting with a non-zero exit code; if an error occurs the autoscaler will capture all standard -error and out and log it. - -### Example - -This is an example configuration of the shell hook for runtime tuning fetching with Holt Winters: -```yaml -holtWinters: - runtimeTuningFetchHook: - type: "shell" - timeout: 2500 - shell: - entrypoint: "python" - command: - - "/metric.py" -``` -Breaking this example down: - -- `type` = the type of the hook, for this example it is a `shell` hook. -- `timeout` = the maximum time the hook can take in milliseconds, for this example it is `2500` (2.5 seconds), if it -takes longer than this it will count the hook as failing. -- `shell` = the shell hook to execute. - - `entrypoint` = the entrypoint of the shell command, e.g. `/bin/bash`, defaults to `/bin/sh`. - - `command` = the command to execute. - -### Always Fail Example - -This is a metric configuration that will always fail: -```yaml -runtimeTuningFetchHook: - type: "shell" - timeout: 2500 - shell: - entrypoint: "/bin/sh" - command: - - "-c" - - "exit 1" -``` - -### Always Return 5 Example - -This is a metric configuration that will return `5` as a metric. -```yaml -runtimeTuningFetchHook: - type: "shell" - timeout: 2500 - shell: - entrypoint: "/bin/sh" - command: - - "-c" - - "echo '5'" -``` - ## http -The http hook allows defining an HTTP request for the autoscaler to make. Any -relevant information will be provided to the target of the request by HTTP -parameters - either `query` or `body` parameters. An error is signified by a -status code that is not defined to be successful in the configuration; if this -kind of error occurs the autoscaler will capture the response body and log it. +The http hook allows defining an HTTP request for the autoscaler to make. Any relevant information will be provided to +the target of the request by HTTP parameters - either `query` or `body` parameters. An error is signified by a status +code that is not defined to be successful in the configuration; if this kind of error occurs the autoscaler will +capture the response body and log it. ### Example @@ -78,7 +20,7 @@ holtWinters: timeout: 2500 http: method: "GET" - url: "https://www.custompodautoscaler.com" + url: "https://www.jamiethompson.me" successCodes: - 200 headers: @@ -115,7 +57,7 @@ runtimeTuningFetchHook: timeout: 2500 http: method: "POST" - url: "https://www.custompodautoscaler.com" + url: "https://www.jamiethompson.me" successCodes: - 200 - 202 diff --git a/docs/user-guide/initial-readiness-delay.md b/docs/user-guide/initial-readiness-delay.md deleted file mode 100644 index 90c0f7a..0000000 --- a/docs/user-guide/initial-readiness-delay.md +++ /dev/null @@ -1,9 +0,0 @@ -# Initial Readiness Delay - -The equivalent of the Kubernetes HPA `--horizontal-pod-autoscaler-initial-readiness-delay` flag can be set by providing -the parameter `initialReadinessDelay` in the predicitve config YAML. Unlike the HPA this does not need to be set as a -flag for the kube-controller-manager on the master node, and can be autoscaler specific. - -This option is set in seconds. -This option has a default value of `30` (30 seconds). -See the [configuration reference for more details](../../reference/configuration#initialreadinessdelay). diff --git a/docs/user-guide/installation.md b/docs/user-guide/installation.md new file mode 100644 index 0000000..d5d2e82 --- /dev/null +++ b/docs/user-guide/installation.md @@ -0,0 +1,17 @@ +# Installation + +You can install Predictive Horizontal Pod Autoscalers (PHPAs) on your cluster after you have installed the Predictive +Horizontal Pod Autoscaler Operator (PHPA operator) onto your cluster. + +The PHPA operator can be installed using Helm, run this command to install the operator onto your cluster with +cluster-wide scope: + +```bash +VERSION=v0.11.0 +HELM_CHART=predictive-horizontal-pod-autoscaler-operator +helm install ${HELM_CHART} https://github.com/jthomperoo/predictive-horizontal-pod-autoscaler/releases/download/${VERSION}/predictive-horizontal-pod-autoscaler-${VERSION}.tgz +``` + +After you have done that you can install PHPAs onto your cluster, check out the [examples for PHPAs you can +deploy](https://github.com/jthomperoo/predictive-horizontal-pod-autoscaler/tree/master/examples) or follow the [getting +started guide](./getting-started.md). diff --git a/docs/user-guide/metrics.md b/docs/user-guide/metrics.md index 3407a8c..755d08f 100644 --- a/docs/user-guide/metrics.md +++ b/docs/user-guide/metrics.md @@ -10,21 +10,19 @@ Deciding which metrics to use is done by using `MetricSpecs`, which are a key pa averageUtilization: 50 ``` -To send these specs to the Predictive HPA, add a config option called `metrics` to the CPA, with a multiline string +To send these specs to the Predictive HPA, add a config option called `metrics` to the PHPA, with a multiline string containing the metric list. For example: ```yaml -- name: predictiveConfig - value: | - ... - metrics: - - type: Resource - resource: - name: cpu - target: - averageUtilization: 50 - type: Utilization +netrics: +- type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 ``` This allows porting over existing Kubernetes HPA metric configurations to the Predictive Horizontal Pod Autoscaler. -Equivalent to K8s HPA metric specs; which are [demonstrated in this HPA walkthrough](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/#autoscaling-on-multiple-metrics-and-custom-metrics). +Equivalent to K8s HPA metric specs; which are [demonstrated in this HPA +walkthrough](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/#autoscaling-on-multiple-metrics-and-custom-metrics). Can hold multiple values as it is an array. diff --git a/docs/user-guide/migration/v0_10_0-to-v0_11_0.md b/docs/user-guide/migration/v0_10_0-to-v0_11_0.md new file mode 100644 index 0000000..9bf2dcf --- /dev/null +++ b/docs/user-guide/migration/v0_10_0-to-v0_11_0.md @@ -0,0 +1,214 @@ +# Migration from v0.10.0 to v0.11.0 + +The change from `v0.10.0` to `v0.11.0` is a large one, primarily because of the change from being a +`CustomPodAutoscaler` to becoming a fully fledged `PredictiveHorizontalPodAutoscaler` custom resource and operator. + +Some key differences for this is that you no longer need the +[custom-pod-autoscaler-operator](https://github.com/jthomperoo/custom-pod-autoscaler-operator) to run PHPAs, and if +you have no other Custom Pod Autoscalers on your cluster you can safely uninstall this operator. + +Now to deploy PHPAs you need to install the Predictive Horizontal Pod Autoscaler operator instead, you see how to +do that by following the [installation guide](../installation.md). + +## Example migration + +Let's take migrating one of the examples as a basis for how the migration looks, we'll look at the `simple-linear` +example. + +In `v0.10.0` the `simple-linear` YAML to deploy the PHPA was this: + +```yaml +apiVersion: custompodautoscaler.com/v1 +kind: CustomPodAutoscaler +metadata: + name: simple-linear +spec: + template: + spec: + containers: + - name: simple-linear + image: jthomperoo/predictive-horizontal-pod-autoscaler:v0.10.0 + imagePullPolicy: IfNotPresent + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: php-apache + roleRequiresMetricsServer: true + config: + - name: minReplicas + value: "1" + - name: maxReplicas + value: "10" + - name: predictiveConfig + value: | + models: + - type: Linear + name: LinearPrediction + perInterval: 1 + linear: + lookAhead: 10000 + storedValues: 6 + decisionType: "maximum" + metrics: + - type: Resource + resource: + name: cpu + target: + averageUtilization: 50 + type: Utilization + - name: interval + value: "10000" + - name: downscaleStabilization + value: "0" +``` + +In `v0.11.0` the `simple-linear` YAML now looks like this: + +```yaml +apiVersion: jamiethompson.me/v1alpha1 +kind: PredictiveHorizontalPodAutoscaler +metadata: + name: simple-linear +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: php-apache + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + averageUtilization: 50 + type: Utilization + models: + - type: Linear + name: simple-linear + perSyncPeriod: 1 + linear: + lookAhead: 10000 + historySize: 6 + decisionType: "maximum" + syncPeriod: 10000 + downscaleStabilization: 0 +``` + +These are still similar, but there are some key differences, first the resource kind has changed from a +`CustomPodAutoscaler` to being a `PredictiveHorizontalPodAutoscaler`: + +In `v0.10.0`: + +```yaml +apiVersion: custompodautoscaler.com/v1 +kind: CustomPodAutoscaler +metadata: + name: simple-linear +spec: + template: + spec: + containers: + - name: simple-linear + image: jthomperoo/predictive-horizontal-pod-autoscaler:v0.10.0 + imagePullPolicy: IfNotPresent +``` + +In `v0.11.0` the same information is captured here: + +```yaml +apiVersion: jamiethompson.me/v1alpha1 +kind: PredictiveHorizontalPodAutoscaler +metadata: + name: simple-linear +spec: +``` + +We no longer need to provide containers/an image for the PHPA to use, the operator handles all of the processing +internally so a separate image is no longer needed. + +We still provide a `scaleTargetRef` in the `spec` as before: + +```yaml +scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: php-apache +``` + +We no longer need this line: + +```yaml +roleRequiresMetricsServer: true +``` + +The operator will handle all needed permissions automatically. + +This configuration expressed in `v0.10.0`: + +```yaml + config: + - name: minReplicas + value: "1" + - name: maxReplicas + value: "10" + - name: predictiveConfig + value: | + models: + - type: Linear + name: LinearPrediction + perInterval: 1 + linear: + lookAhead: 10000 + storedValues: 6 + decisionType: "maximum" + metrics: + - type: Resource + resource: + name: cpu + target: + averageUtilization: 50 + type: Utilization + - name: interval + value: "10000" + - name: downscaleStabilization + value: "0" +``` + +Is now expressed like this: + +```yaml + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + averageUtilization: 50 + type: Utilization + models: + - type: Linear + name: simple-linear + perSyncPeriod: 1 + linear: + lookAhead: 10000 + historySize: 6 + decisionType: "maximum" + syncPeriod: 10000 + downscaleStabilization: 0 +``` + +You can see how instead of providing key value pairs we can now provide normal YAML directly. + +Some key differences highlighted here: + +- `interval` has been renamed to `syncPeriod`. +- `perInterval` has been renamed to `perSyncPeriod`. +- `storedValues` has been renamed to `historySize`. +- `predictiveConfig` has been removed and replaced with `metrics`, `models`, and `decisionType` directly. + +## What has changed in full + +See the [changelog for what has changed in the `v0.11.0` release in +full](https://github.com/jthomperoo/predictive-horizontal-pod-autoscaler/blob/master/CHANGELOG.md). diff --git a/docs/user-guide/min-max-replicas.md b/docs/user-guide/min-max-replicas.md deleted file mode 100644 index 0f0eff2..0000000 --- a/docs/user-guide/min-max-replicas.md +++ /dev/null @@ -1,9 +0,0 @@ -# Minimum and Maximum Replicas - -Minimum and maximum replicas can be set at deploy time; and are configuration options that are part of the [Custom Pod -Autoscaler Framework](https://custom-pod-autoscaler.readthedocs.io/en/latest). - -Minimum is by default set to `1` and maximum is by default set to `10`. -This option is part of the Custom Pod Autoscaler Framework, for more [information please view the Custom Pod Autoscaler -Wiki configuration -reference](https://custom-pod-autoscaler.readthedocs.io/en/latest/reference/configuration/#minreplicas). diff --git a/docs/user-guide/models.md b/docs/user-guide/models.md index ff72d91..4c051a5 100644 --- a/docs/user-guide/models.md +++ b/docs/user-guide/models.md @@ -6,14 +6,14 @@ All models share these three options: - **type** - The type of the model, for example 'Linear'. - **name** - The name of the model, must be unique and not shared by multiple models. -- **perInterval** - The frequency that the model is used to recalculate and store values - tied to the interval as a -base unit, with a value of `1` resulting in the model being recalculated every interval, a value of `2` meaning -recalculated every other interval, `3` waits for two intervals after every calculation and so on. +- **perSyncPeriod** - The frequency that the model is used to recalculate and store values - tied to the sync period as +a base unit, with a value of `1` resulting in the model being recalculated every sync period, a value of `2` meaning +recalculated every other sync period, `3` waits for two sync periods after every calculation and so on. - **calculationTimeout** - The timeout for calculating using an algorithm, if this timeout is exceeded the calculation is skipped. Defaults set based on the algorithm used, see below. -All models use `interval` as a base unit, so if the interval is defined as `10000` (10 seconds), the models will base -their timings and calculations as multiples of 10 seconds. +All models use `syncPeriod` as a base unit, so if the sync period is defined as `10000` (10 seconds), the models will +base their timings and calculations as multiples of 10 seconds. ## Linear Regression @@ -21,24 +21,20 @@ The linear regression model uses a default calculation timeout of `30000` (30 se Example: ```yaml -- name: predictiveConfig - value: | - models: - - type: Linear - name: LinearPrediction - perInterval: 1 - calculationTimeout: 25000 - linear: - lookAhead: 10000 - storedValues: 6 - decisionType: "maximum" +models: + - type: Linear + name: simple-linear + perSyncPeriod: 1 + calculationTimeout: 25000 + linear: + lookAhead: 10000 + historySize: 6 ``` The **linear** component of the configuration handles configuration of the Linear regression options: - **lookAhead** - sets up the model to try to predict `10 seconds` ahead of time (time in milliseconds). -- **storedValues** - sets up the model to store the past `6` evaluations and to use these for predictions. If there +- **historySize** - sets up the model to store the past `6` evaluations and to use these for predictions. If there are `> 6` evaluations, the oldest will be removed. -- **calculationTimeout** - sets the timeout for calculating the linear regression to be 25 seconds. For a more detailed example, [see the example in `/examples/simple-linear`](https://github.com/jthomperoo/predictive-horizontal-pod-autoscaler/tree/master/examples/simple-linear). @@ -49,21 +45,18 @@ The Holt-Winters time series model uses a default calculation timeout of `30000` Example: ```yaml -- name: predictiveConfig - value: | - models: - - type: HoltWinters - name: HoltWintersPrediction - perInterval: 1 - holtWinters: - alpha: 0.9 - beta: 0.9 - gamma: 0.9 - seasonalPeriods: 6 - storedSeasons: 4 - trend: additive - seasonal: additive - decisionType: "maximum" +models: +- type: HoltWinters + name: simple-holt-winters + perSyncPeriod: 1 + holtWinters: + alpha: 0.9 + beta: 0.9 + gamma: 0.9 + seasonalPeriods: 6 + storedSeasons: 4 + trend: additive + seasonal: additive ``` The **holtWinters** component of the configuration handles configuration of the Linear regression options: @@ -71,7 +64,7 @@ The **holtWinters** component of the configuration handles configuration of the - **alpha**, **beta**, **gamma** - these are the smoothing coefficients for level, trend and seasonality respectively, requires tweaking and analysis to be able to optimise. See [here](https://github.com/jthomperoo/holtwinters) or [here](https://grisha.org/blog/2016/01/29/triple-exponential-smoothing-forecasting/) for more details. -- **seasonalPeriods** - the length of a season in base unit intervals, for example if your interval was `10000` +- **seasonalPeriods** - the length of a season in base unit sync periods, for example if your sync period was `10000` (10 seconds), and your repeated season was 60 seconds long, this value would be `6`. - **storedSeasons** - the number of seasons to store, for example `4`, if there are `>4` seasons stored, the oldest season will be removed. @@ -112,33 +105,30 @@ user guide](./hooks.md) For example, a hook using a HTTP request to fetch the values of runtime is configured as: ```yaml -- name: predictiveConfig - value: | - models: - - type: HoltWinters - name: HoltWintersPrediction - perInterval: 1 - holtWinters: - runtimeTuningFetchHook: - type: "http" - timeout: 2500 - http: - method: "GET" - url: "http://tuning/holt_winters" - successCodes: - - 200 - parameterMode: query - seasonalPeriods: 6 - storedSeasons: 4 - trend: "additive" - decisionType: "maximum" +models: +- type: HoltWinters + name: simple-holt-winters + perSyncPeriod: 1 + holtWinters: + runtimeTuningFetchHook: + type: "http" + timeout: 2500 + http: + method: "GET" + url: "http://tuning/holt_winters" + successCodes: + - 200 + parameterMode: query + seasonalPeriods: 6 + storedSeasons: 4 + trend: additive + seasonal: additive ``` The hook is defined with the name `runtimeTuningFetchHook`. The supported hook types for the PHPA are: -- Shell scripts - HTTP requests The process is as follows: @@ -158,57 +148,53 @@ The tuning values can be both hardcoded and fetched at runtime, for example the runtime, and the `beta` and `gamma` values could be hardcoded in configuration: ```yaml -- name: predictiveConfig - value: | - models: - - type: HoltWinters - name: HoltWintersPrediction - perInterval: 1 - holtWinters: - runtimeTuningFetchHook: - type: "http" - timeout: 2500 - http: - method: "GET" - url: "http://tuning/holt_winters" - successCodes: - - 200 - parameterMode: query - beta: 0.9 - gamma: 0.9 - seasonalPeriods: 6 - storedSeasons: 4 - trend: "additive" - decisionType: "maximum" +models: +- type: HoltWinters + name: simple-holt-winters + perSyncPeriod: 1 + holtWinters: + runtimeTuningFetchHook: + type: "http" + timeout: 2500 + http: + method: "GET" + url: "http://tuning/holt_winters" + successCodes: + - 200 + parameterMode: query + beta: 0.9 + gamma: 0.9 + seasonalPeriods: 6 + storedSeasons: 4 + trend: additive + seasonal: additive ``` Or the values could be provided, and if they are not returned by the external source the hardcoded values will be used as a backup: ```yaml -- name: predictiveConfig - value: | - models: - - type: HoltWinters - name: HoltWintersPrediction - perInterval: 1 - holtWinters: - runtimeTuningFetchHook: - type: "http" - timeout: 2500 - http: - method: "GET" - url: "http://tuning/holt_winters" - successCodes: - - 200 - parameterMode: query - alpha: 0.9 - beta: 0.9 - gamma: 0.9 - seasonalPeriods: 6 - storedSeasons: 4 - trend: "additive" - decisionType: "maximum" +models: +- type: HoltWinters + name: simple-holt-winters + perSyncPeriod: 1 + holtWinters: + runtimeTuningFetchHook: + type: "http" + timeout: 2500 + http: + method: "GET" + url: "http://tuning/holt_winters" + successCodes: + - 200 + parameterMode: query + alpha: 0.9 + beta: 0.9 + gamma: 0.9 + seasonalPeriods: 6 + storedSeasons: 4 + trend: additive + seasonal: additive ``` If any value is missing, the PHPA will report it as an error (e.g. @@ -247,34 +233,25 @@ The data that the external source will recieve will be formatted as: "trend": "additive" } }, - "evaluations": [ + "replicaHistory": [ { - "id": 1, - "created": "2020-10-19T19:12:20Z", - "val": { - "targetReplicas": 0 - } + "time": "2020-10-19T19:12:20Z", + "replicas": 0 }, { - "id": 2, - "created": "2020-10-19T19:12:40Z", - "val": { - "targetReplicas": 0 - } + "time": "2020-10-19T19:12:40Z", + "replicas": 0 }, { - "id": 3, - "created": "2020-10-19T19:13:00Z", - "val": { - "targetReplicas": 0 - } + "time": "2020-10-19T19:13:00Z", + "replicas": 0 }, ] } ``` -This provides information around the model being used, how it is configured, and previous scaling decisions -(`evaluations`). This data could be used to help calculate the tuning values, or it could be ignored. +This provides information around the model being used, how it is configured, and previous replica values +(`replicaHistory`). This data could be used to help calculate the tuning values, or it could be ignored. #### Response Format diff --git a/docs/user-guide/sync-period.md b/docs/user-guide/sync-period.md deleted file mode 100644 index e90c3fe..0000000 --- a/docs/user-guide/sync-period.md +++ /dev/null @@ -1,13 +0,0 @@ -# Downscale Stabilization - -The equivalent of the Kubernetes HPA `--horizontal-pod-autoscaler-sync-period` flag can be set by providing the -parameter `interval` config option in the autoscaler YAML. Unlike the HPA this does not need to be set as a flag for -the kube-controller-manager on the master node, and can be autoscaler specific. - -Interval can be set at deploy time; and are configuration options that are part of the [Custom Pod Autoscaler -Framework](https://custom-pod-autoscaler.readthedocs.io/en/latest). - -This option is set in milliseconds. -This option has a default value of `15000` (15 seconds). -This option is part of the Custom Pod Autoscaler Framework, for more [information please view the Custom Pod Autoscaler -Wiki configuration reference](https://custom-pod-autoscaler.readthedocs.io/en/latest/reference/configuration/#interval). diff --git a/docs/user-guide/tolerance.md b/docs/user-guide/tolerance.md deleted file mode 100644 index 7bcf38a..0000000 --- a/docs/user-guide/tolerance.md +++ /dev/null @@ -1,8 +0,0 @@ -# Tolerance - -The equivalent of the Kubernetes HPA `--horizontal-pod-autoscaler-tolerance` flag can be set by providing the parameter -`tolerance` in the predicitve config YAML. Unlike the HPA this does not need to be set as a flag for the -kube-controller-manager on the master node, and can be autoscaler specific. - -This option has a default value of `0.1`. -See the [configuration reference for more details](../../reference/configuration#tolerance). diff --git a/examples/argo-rollout/README.md b/examples/argo-rollout/README.md deleted file mode 100644 index 6991ace..0000000 --- a/examples/argo-rollout/README.md +++ /dev/null @@ -1,78 +0,0 @@ -# Argo Rollout - -> Note: this feature is only available in the Predictive Horizontal Pod Autoscaler v0.9.0 and above. - -> Note: this example requires using a Custom Pod Autoscaler Operator `v1.2.0` and above. - -## Overview - -This example is showing how to target an [Argo Rollout](https://argoproj.github.io/argo-rollouts/), other than that it -is identical to the [simple-linear example](../simple-linear). - -This example was based on the [Horizontal Pod Autoscaler -Walkthrough](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/). - -## Usage - -### Enable Argo Rollouts - -Using this requires Argo Rollouts to be enabled on your Kubernetes cluster, [follow this guide to set up Argo Rollouts -on your cluster](https://argoproj.github.io/argo-rollouts/installation/) - -### Enable CPAs - -Using this CPA requires CPAs to be enabled on your Kubernetes cluster, [follow this guide to set up CPAs on your -cluster](https://github.com/jthomperoo/custom-pod-autoscaler-operator#installation). - -### Deploy an Argo Rollout to Manage - -First a rollout needs to be deployed that the CPA can manage, you can deploy with the following command: - -```bash -kubectl apply -f rollout.yaml -``` - -You can check if the rollout is deployed by running this command: - -```bash -kubectl argo rollouts get rollout php-apache -``` - -You should see that the rollout is set up to initially have only `1` replica. - -### Deploy the Predictive Horizontal Pod Autoscaler - -Deploy the PHPA with the following command: - -```bash -kubectl apply -f phpa.yaml -``` - -### Increase the CPU Load - -Run the following command to increase the CPU load by making HTTP requests, this may take a while: - -```bash -kubectl run -i --tty load-generator --rm --image=busybox --restart=Never -- /bin/sh -c "while sleep 0.01; do wget -q -O- http://php-apache; done" -``` - -You can stop increasing the load by using Ctrl+C. - -You should see the number of replicas of the `php-apache` rollout increase. - -## Configuration - -This example targets the rollout using a `scaleTargetRef` configured using the following: - -```yaml -scaleTargetRef: - apiVersion: argoproj.io/v1alpha1 - kind: Rollout - name: php-apache -``` - -This example also requests that the CPA is provisioned with the role required by providing the following option: - -```yaml -roleRequiresArgoRollouts: true -``` diff --git a/examples/argo-rollout/phpa.yaml b/examples/argo-rollout/phpa.yaml deleted file mode 100644 index 98fc738..0000000 --- a/examples/argo-rollout/phpa.yaml +++ /dev/null @@ -1,43 +0,0 @@ -apiVersion: custompodautoscaler.com/v1 -kind: CustomPodAutoscaler -metadata: - name: argo-rollout -spec: - template: - spec: - containers: - - name: argo-rollout - image: jthomperoo/predictive-horizontal-pod-autoscaler:latest - imagePullPolicy: IfNotPresent - scaleTargetRef: - apiVersion: argoproj.io/v1alpha1 - kind: Rollout - name: php-apache - roleRequiresArgoRollouts: true - roleRequiresMetricsServer: true - config: - - name: minReplicas - value: "1" - - name: maxReplicas - value: "10" - - name: predictiveConfig - value: | - models: - - type: Linear - name: LinearPrediction - perInterval: 1 - linear: - lookAhead: 10000 - storedValues: 6 - decisionType: "maximum" - metrics: - - type: Resource - resource: - name: cpu - target: - averageUtilization: 50 - type: Utilization - - name: interval - value: "10000" - - name: downscaleStabilization - value: "0" diff --git a/examples/argo-rollout/rollout.yaml b/examples/argo-rollout/rollout.yaml deleted file mode 100644 index 5d9737d..0000000 --- a/examples/argo-rollout/rollout.yaml +++ /dev/null @@ -1,56 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Rollout -metadata: - labels: - run: php-apache - name: php-apache -spec: - replicas: 1 - strategy: - canary: - steps: - - setWeight: 20 - - pause: {} - - setWeight: 40 - - pause: {duration: 10} - - setWeight: 60 - - pause: {duration: 10} - - setWeight: 80 - - pause: {duration: 10} - selector: - matchLabels: - run: php-apache - template: - metadata: - creationTimestamp: null - labels: - run: php-apache - spec: - containers: - - image: k8s.gcr.io/hpa-example - imagePullPolicy: Always - name: php-apache - ports: - - containerPort: 80 - protocol: TCP - resources: - limits: - cpu: 500m - requests: - cpu: 200m - restartPolicy: Always ---- -apiVersion: v1 -kind: Service -metadata: - name: php-apache - namespace: default -spec: - ports: - - port: 80 - protocol: TCP - targetPort: 80 - selector: - run: php-apache - sessionAffinity: None - type: ClusterIP diff --git a/examples/dynamic-holt-winters/README.md b/examples/dynamic-holt-winters/README.md index ca44e33..0194e27 100644 --- a/examples/dynamic-holt-winters/README.md +++ b/examples/dynamic-holt-winters/README.md @@ -1,36 +1,70 @@ # Dynamic Holt Winters -This example shows how Holt-Winters can be used to with the Predictive Horizontal Pod Autoscaler to predict scaling -demand based on seasonal data. This example is described as *dynamic* as it fetches it's tuning values from an external -source at runtime, allowing these values to be dynamically calculated at runtime rather than being hardcoded. +This example shows how Holt-Winters can be used to with the Predictive Horizontal Pod Autoscaler (PHPA) to predict +scaling demand based on seasonal data. This example is described as *dynamic* as it fetches it's tuning values from +an external source at runtime, allowing these values to be dynamically calculated at runtime rather than being +hardcoded. This example specifically uses a HTTP request to a tuning service to fetch the `alpha`, `beta` and `gamma` Holt Winters tuning values at runtime. +This uses the Holt-Winters time series prediction method, which allows for defining seasons to predict how to scale. +For example, defining a season as 24 hours,a deployment regularly has a higher CPU load between 3pm and 5pm, the model +will gather data and once enough seasons have been gathered, will make predictions based on its knowledge of CPU load +being higher between 3pm and 5pm, leading to pre-emptive scaling that will keep latency down and keep the system ready +and responsive. + +This example is a smaller scale of the example described above, with an interval time of 20 seconds, and a season of +length 6 (6 * 20 = 120 seconds = 2 minutes). The example will store up to 4 previous seasons to make predictions with. +The example includes a load tester, which runs for 30 seconds every minute. + +This is the result of running the example plotted, with red values being predicted values and blue values being actual +values: +![Predicted values overestimating but still fitting actual values](../../docs/img/holt_winters_prediction_vs_actual.svg) +From this you can see that the prediction is overestimating, but still pre-emptively scaling - storing more seasons and +adjusting alpha, beta and gamma values would reduce the overestimation and produce more accurate results. + +## Requirements + +To set up this example and follow the steps listed here you need: + +- [kubectl](https://kubernetes.io/docs/tasks/tools/). +- A Kubernetes cluster that kubectl is configured to use - [k3d](https://github.com/rancher/k3d) is good for local +testing. +- [helm](https://helm.sh/docs/intro/install/) to install the PHPA operator. +- [jq](https://stedolan.github.io/jq/) to format some JSON output. + ## Usage -If you want to deploy this onto your cluster, you first need to install the [Custom Pod Autoscaler -Operator](https://github.com/jthomperoo/custom-pod-autoscaler-operator), follow the [installation guide for -instructions for installing the -operator](https://github.com/jthomperoo/custom-pod-autoscaler-operator/blob/master/INSTALL.md). +If you want to deploy this onto your cluster, you first need to install the Predictive Horizontal Pod Autoscaler +Operator, follow the [installation guide for instructions for installing the +operator](https://predictive-horizontal-pod-autoscaler.readthedocs.io/en/latest/user-guide/installation). This example was based on the [Horizontal Pod Autoscaler -Walkthrough](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/). This example -assumes you are using Minikube, or working out of the same Docker registry as your Kubernetes cluster. +Walkthrough](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/). 1. Use `kubectl apply -f deployment.yaml` to spin up the app/deployment to manage, called `php-apache`. -2. Build the tuning service. - - Point to the cluster's Docker registry (e.g. for this Minikube) `eval $(minikube docker-env)`. - - Build the load tester image `docker build -t tuning tuning` +2. Build the tuning image `docker build -t tuning tuning` and import it into your Kubernetes cluster + (`k3d image import tuning`). 4. Deploy the tuning service `kubectl apply -f tuning/tuning.yaml`. 5. Use `kubectl apply -f phpa.yaml` to start the autoscaler, pointing at the previously created deployment. -6. Build the load tester. - - Point to the cluster's Docker registry (e.g. for this Minikube) `eval $(minikube docker-env)`. - - Build the load tester image `docker build -t load-tester load` +6. Build the load tester image `docker build -t load-tester load` and import it into your Kubernetes cluster + (`k3d image import load`). 7. Deploy the load tester, note the time as it will run for 30 seconds every minute `kubectl apply -f load/load.yaml`. -8. Use `kubectl logs simple-linear-example --follow` to see the autoscaler working and the log output it produces. -9. Use `kubectl logs tuning --follow` to see the logs of the tuning service, it will report any time it is queried and +8. Use `kubectl logs -l name=predictive-horizontal-pod-autoscaler -f` to see the autoscaler working and the log output +it produces. +9. Use `kubectl logs -l run=tuning -f` to see the logs of the tuning service, it will report any time it is queried and it will print the value provided to it. +10. Use +`kubectl get configmap predictive-horizontal-pod-autoscaler-dynamic-holt-winters-data -o=json | jq -r '.data.data | fromjson | .modelHistories["dynamic-holt-winters"].replicaHistory[] | .time,.replicas'` +to see the replica history for the autoscaler stored in a configmap and tracked by the autoscaler. + +Every minute the load tester will increase the load on the application we are autoscaling for 30 seconds. The PHPA will +initially without any data just act like a Horizontal Pod Autoscaler and will reactively scale up to meet this demand +as best as it can after the demand has already started. After the load tester has run a couple of times the PHPA will +have built up enough data that it can start to make predictions ahead of time using the Holt Winters model, and it +will start calculating these predictions and proactively scaling up ahead of time to meet demand that it expects based +on the data collected in the past. ## Explanation @@ -49,74 +83,71 @@ and down. ### Predictive Horizontal Pod Autoscaler -The PHPA part of the example is just a PHPA that contains some configuration for how the scaling should be applied, -the configuration defines how the autoscaler will act: +The PHPA contains some configuration for how the scaling should be applied, the configuration defines how the +autoscaler will act: ```yaml - config: - - name: minReplicas - value: "1" - - name: maxReplicas - value: "10" - - name: predictiveConfig - value: | - models: - - type: HoltWinters - name: HoltWintersPrediction - perInterval: 1 - holtWinters: - runtimeTuningFetchHook: - type: "http" - timeout: 2500 - http: - method: "GET" - url: "http://tuning/holt_winters" - successCodes: - - 200 - parameterMode: query - seasonalPeriods: 6 - storedSeasons: 4 - trend: "additive" - seasonal: additive - decisionType: "maximum" - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 50 - - name: interval - value: "20000" - - name: startTime - value: "60000" - - name: downscaleStabilization - value: "30" +apiVersion: jamiethompson.me/v1alpha1 +kind: PredictiveHorizontalPodAutoscaler +metadata: + name: dynamic-holt-winters +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: php-apache + minReplicas: 1 + maxReplicas: 10 + syncPeriod: 20000 + downscaleStabilization: 30 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + models: + - type: HoltWinters + name: HoltWintersPrediction + holtWinters: + runtimeTuningFetchHook: + type: "http" + timeout: 2500 + http: + method: "GET" + url: "http://tuning/holt_winters" + successCodes: + - 200 + parameterMode: query + seasonalPeriods: 6 + storedSeasons: 4 + trend: "additive" + seasonal: additive ``` -- **minReplicas**, **maxReplicas**, **startTime** and **interval** - Custom Pod Autoscaler options, setting minimum and -maximum replicas, the starting time - for this example will start at the nearest full minute, and the time interval -inbetween each autoscale being run, i.e. the autoscaler checks every 20 seconds. -- **downscaleStabilization** is also a Custom Pod Autoscaler option, in this case changing the `downscaleStabilization` -from the default 300 seconds (5 minutes), to 30 seconds. The `downscaleStabilization` option handles how quickly an -autoscaler can scale down, ensuring that it will pick the highest evaluation that has occurred within the last time -period described, in this case it will pick the highest evaluation over the past 30 seconds. -- **predictiveConfig** - configuration of the predictive elements. - * **models** - predictive models to apply. - - **type** - 'HoltWinters', using a Holt-Winters predictive model. - - **name** - Unique name of the model. - - **holtWinters** - Holt-Winters specific configuration. - * **runtimeTuningFetchHook** - This is a [method] that is used to dynamically fetch the `alpha`, `beta` and - `gamma` values at runtime, in this example it is using a `HTTP` request to `http://tuning/holt_winters`. - * **seasonalPeriods** - the length of a season in base unit intervals, for this example interval is `20000` - (20 seconds), and season length is `6`, resulting in a season length of 20 * 6 = 120 seconds = 2 minutes. - * **storedSeasons** - the number of seasons to store, for this example `4`, if there are more than 4 seasons - stored, the oldest ones are removed. - * **trend** - Either `add`/`additive` or `mul`/`multiplicative`, defines the method for the trend element. - * **seasonal** - Either `add`/`additive` or `mul`/`multiplicative`, defines the method for the seasonal element. - * **decisionType** - strategy for resolving multiple models, either `maximum`, `minimum` or `mean`, in this case - `maximum`, meaning take the highest predicted value. - * **metrics** - Horizontal Pod Autoscaler option, targeting 50% CPU utilisation. +- `scaleTargetRef` is the resource the autoscaler is targeting for scaling. +- `minReplicas` and `maxReplicas` are the minimum and maximum number of replicas the autoscaler can scale the resource +between. +- `syncPeriod` is how frequently this autoscaler will run in milliseconds, so this autoscaler will run every 20000 +milliseconds (20 seconds). +- `downscaleStabilization` handles how quickly an autoscaler can scale down, ensuring that it will pick the highest evaluation that has occurred within the last time period described, by default it will pick the highest evaluation over +the past 5 minutes. In this case it will pick the highest evaluation over the past 30 seconds. +- `metrics` defines the metrics that the PHPA should use to scale with, in this example it will try to keep average +CPU utilization at 50% per pod. +- `models` - predictive models to apply. + - `type` - 'HoltWinters', using a Holt-Winters predictive model. + - `name` - Unique name of the model. + - `holtWinters` - Holt-Winters specific configuration. + * `runtimeTuningFetchHook` - This is a [hook](https://predictive-horizontal-pod-autoscaler.readthedocs.io/en/latest/user-guide/hooks) + that is used to dynamically fetch the `alpha`, `beta` and `gamma` values at runtime, in this example it is using a + `HTTP` request to `http://tuning/holt_winters`. + * `seasonalPeriods` - the length of a season in base unit sync periods, for this example sync period is `20000` + (20 seconds), and season length is `6`, resulting in a season length of 20 * 6 = 120 seconds = 2 minutes. + * `storedSeasons` - the number of seasons to store, for this example `4`, if there are more than 4 seasons + stored, the oldest ones are removed. + * `trend` - Either `add`/`additive` or `mul`/`multiplicative`, defines the method for the trend element. + * `seasonal` - Either `add`/`additive` or `mul`/`multiplicative`, defines the method for the seasonal element. ### Tuning Service diff --git a/examples/dynamic-holt-winters/load/Dockerfile b/examples/dynamic-holt-winters/load/Dockerfile index 84d29d5..d060dc2 100644 --- a/examples/dynamic-holt-winters/load/Dockerfile +++ b/examples/dynamic-holt-winters/load/Dockerfile @@ -1,4 +1,4 @@ -# Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. +# Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/dynamic-holt-winters/load/load_tester.sh b/examples/dynamic-holt-winters/load/load_tester.sh index 22525ee..98918d8 100644 --- a/examples/dynamic-holt-winters/load/load_tester.sh +++ b/examples/dynamic-holt-winters/load/load_tester.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. +# Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/dynamic-holt-winters/phpa.yaml b/examples/dynamic-holt-winters/phpa.yaml index 4cb3ba6..a6b6ca3 100644 --- a/examples/dynamic-holt-winters/phpa.yaml +++ b/examples/dynamic-holt-winters/phpa.yaml @@ -1,55 +1,37 @@ -apiVersion: custompodautoscaler.com/v1 -kind: CustomPodAutoscaler +apiVersion: jamiethompson.me/v1alpha1 +kind: PredictiveHorizontalPodAutoscaler metadata: - name: dynamic-holt-winters-example + name: dynamic-holt-winters spec: - template: - spec: - containers: - - name: dynamic-holt-winters-example - image: jthomperoo/predictive-horizontal-pod-autoscaler:latest - imagePullPolicy: Always scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: php-apache - roleRequiresMetricsServer: true - config: - - name: minReplicas - value: "1" - - name: maxReplicas - value: "10" - - name: predictiveConfig - value: | - models: - - type: HoltWinters - name: HoltWintersPrediction - perInterval: 1 - holtWinters: - runtimeTuningFetchHook: - type: "http" - timeout: 2500 - http: - method: "GET" - url: "http://tuning/holt_winters" - successCodes: - - 200 - parameterMode: query - seasonalPeriods: 6 - storedSeasons: 4 - trend: "additive" - seasonal: additive - decisionType: "maximum" - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 50 - - name: interval - value: "20000" - - name: startTime - value: "60000" - - name: downscaleStabilization - value: "30" + minReplicas: 1 + maxReplicas: 10 + syncPeriod: 20000 + downscaleStabilization: 30 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + models: + - type: HoltWinters + name: HoltWintersPrediction + holtWinters: + runtimeTuningFetchHook: + type: "http" + timeout: 2500 + http: + method: "GET" + url: "http://tuning/holt_winters" + successCodes: + - 200 + parameterMode: query + seasonalPeriods: 6 + storedSeasons: 4 + trend: "additive" + seasonal: additive diff --git a/examples/dynamic-holt-winters/tuning/Dockerfile b/examples/dynamic-holt-winters/tuning/Dockerfile index cc40681..60c6380 100644 --- a/examples/dynamic-holt-winters/tuning/Dockerfile +++ b/examples/dynamic-holt-winters/tuning/Dockerfile @@ -1,4 +1,4 @@ -# Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. +# Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM python:3.6-slim +FROM python:3.8-slim # We copy just the requirements.txt first to leverage Docker cache COPY ./requirements.txt /app/requirements.txt WORKDIR /app diff --git a/examples/dynamic-holt-winters/tuning/api.py b/examples/dynamic-holt-winters/tuning/api.py index 5e2facc..97d4d3c 100644 --- a/examples/dynamic-holt-winters/tuning/api.py +++ b/examples/dynamic-holt-winters/tuning/api.py @@ -1,4 +1,4 @@ -# Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. +# Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/persistent-linear/README.md b/examples/persistent-linear/README.md deleted file mode 100644 index 31888c6..0000000 --- a/examples/persistent-linear/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# Persistent Linear Example - -## Overview - -This example is showing a predictive horizontal pod autoscaler using a linear regression model, with persistent volume storage, so if the scaler is deleted the data will persist. - -## Usage -If you want to deploy this onto your cluster, you first need to install the [Custom Pod Autoscaler Operator](https://github.com/jthomperoo/custom-pod-autoscaler-operator), follow the [installation guide for instructions for installing the operator](https://github.com/jthomperoo/custom-pod-autoscaler-operator/blob/master/INSTALL.md). - -This example was based on the [Horizontal Pod Autoscaler Walkthrough](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/). - -You must first set up a Persistent volume on your cluster, this example will assume you are using Minikube. -1. Open a shell to the node `minikube ssh`. -2. Create a data dir `sudo mkdir /mnt/data`. -3. Exit the shell `exit`. -Now your persistent volume is set up, you can set up the autoscaler. - -1. Use `kubectl apply -f deployment.yaml` to spin up the app/deployment to manage, called `php-apache`. -2. Use `kubectl apply -f phpa.yaml` to start the autoscaler, pointing at the previously created deployment. -3. Use `kubectl logs simple-linear-example --follow` to see the autoscaler working and the log output it produces. -4. Increase the load with: `kubectl run --generator=run-pod/v1 -it --rm load-generator --image=busybox /bin/sh` - * Once it has loaded, run this command to create load `while true; do wget -q -O- http://php-apache.default.svc.cluster.local; done` -5. Watch as the number of replicas increases. -6. Use `kubectl exec -it simple-linear-example sqlite3 /store/predictive-horizontal-pod-autoscaler.db 'SELECT * FROM evaluation;'` to see the evaluations stored locally and tracked by the autoscaler. - -## Explained - -The example has some configuration -```yaml -config: - - name: minReplicas - value: "1" - - name: maxReplicas - value: "5" - - name: predictiveConfig - value: | - models: - - type: Linear - name: LinearPrediction - perInterval: 1 - linear: - lookAhead: 10000 - storedValues: 6 - decisionType: "maximum" - metrics: - - type: Resource - resource: - name: cpu - target: - averageUtilization: 50 - type: Utilization - - name: interval - value: "10000" - - name: downscaleStabilization - value: "30" -``` -The `minReplicas`, `maxReplicas` and `interval` are Custom Pod Autoscaler options, setting minimum and maximum replicas, and the time interval inbetween each autoscale being run, i.e. the autoscaler checks every 10 seconds. -The `downscaleStabilization` is also a Custom Pod Autoscaler option, in this case changing the `downscaleStabilization` from the default 300 seconds (5 minutes), to 30 seconds. The `downscaleStabilization` option handles how quickly an autoscaler can scale down, ensuring that it will pick the highest evaluation that has occurred within the last time period described, in this case it will pick the highest evaluation over the past 30 seconds. -The `predictiveConfig` option is the Predictive Horizontal Pod Autoscaler options, detailing a linear regression model that runs on every interval, looking 10 seconds ahead, keeping track of the past 6 replica values in order to predict the next result, and the `decisionType` is maximum, which if there were multiple models provided would mean that the PHPA would use the one with the highest replica count; there are two other options, `mean` and `minimum`. The `metrics` option is a Horizontal Pod Autoscaler option, targeting CPU utilisation. - diff --git a/examples/persistent-linear/deployment.yaml b/examples/persistent-linear/deployment.yaml deleted file mode 100644 index 6c9a42a..0000000 --- a/examples/persistent-linear/deployment.yaml +++ /dev/null @@ -1,50 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - run: php-apache - name: php-apache -spec: - replicas: 1 - selector: - matchLabels: - run: php-apache - strategy: - rollingUpdate: - maxSurge: 25% - maxUnavailable: 25% - type: RollingUpdate - template: - metadata: - creationTimestamp: null - labels: - run: php-apache - spec: - containers: - - image: k8s.gcr.io/hpa-example - imagePullPolicy: Always - name: php-apache - ports: - - containerPort: 80 - protocol: TCP - resources: - limits: - cpu: 500m - requests: - cpu: 200m - restartPolicy: Always ---- -apiVersion: v1 -kind: Service -metadata: - name: php-apache - namespace: default -spec: - ports: - - port: 80 - protocol: TCP - targetPort: 80 - selector: - run: php-apache - sessionAffinity: None - type: ClusterIP \ No newline at end of file diff --git a/examples/persistent-linear/phpa.yaml b/examples/persistent-linear/phpa.yaml deleted file mode 100644 index ad4098d..0000000 --- a/examples/persistent-linear/phpa.yaml +++ /dev/null @@ -1,77 +0,0 @@ -apiVersion: v1 -kind: PersistentVolume -metadata: - name: predictive-horizontal-pod-autoscaler-example - labels: - type: local -spec: - storageClassName: manual - capacity: - storage: 10Mi - volumeMode: Filesystem - accessModes: - - ReadWriteOnce - hostPath: - path: "/mnt/data" ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: predictive-horizontal-pod-autoscaler-example -spec: - storageClassName: manual - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 10Mi ---- -apiVersion: custompodautoscaler.com/v1 -kind: CustomPodAutoscaler -metadata: - name: predictive-horizontal-pod-autoscaler-example -spec: - template: - spec: - volumes: - - name: predictive-horizontal-pod-autoscaler-example - persistentVolumeClaim: - claimName: predictive-horizontal-pod-autoscaler-example - containers: - - name: predictive-horizontal-pod-autoscaler-example - image: jthomperoo/predictive-horizontal-pod-autoscaler:latest - imagePullPolicy: Always - volumeMounts: - - mountPath: "/store/" - name: predictive-horizontal-pod-autoscaler-example - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: php-apache - roleRequiresMetricsServer: true - config: - - name: minReplicas - value: "1" - - name: maxReplicas - value: "10" - - name: predictiveConfig - value: | - models: - - type: Linear - name: LinearPrediction - perInterval: 1 - linear: - lookAhead: 10000 - storedValues: 6 - decisionType: "maximum" - metrics: - - type: Resource - resource: - name: cpu - target: - averageUtilization: 50 - type: Utilization - - name: interval - value: "10000" - - name: downscaleStabilization - value: "0" diff --git a/examples/simple-holt-winters/README.md b/examples/simple-holt-winters/README.md index a031828..f225f3e 100644 --- a/examples/simple-holt-winters/README.md +++ b/examples/simple-holt-winters/README.md @@ -1,9 +1,7 @@ -# Simple Holt-Winters Example +# Simple Holt Winters -## Overview - -This example is showing a predictive horizontal pod autoscaler using holt-winters exponential smoothing time series -prediction, with no persistent storage, so if the scaler is deleted the data will not persist. +This example shows how Holt-Winters can be used to with the Predictive Horizontal Pod Autoscaler (PHPA) to predict +scaling demand based on seasonal data. This uses the Holt-Winters time series prediction method, which allows for defining seasons to predict how to scale. For example, defining a season as 24 hours,a deployment regularly has a higher CPU load between 3pm and 5pm, the model @@ -17,85 +15,136 @@ The example includes a load tester, which runs for 30 seconds every minute. This is the result of running the example plotted, with red values being predicted values and blue values being actual values: -![Predicted values overestimating but still fitting actual values](./graph.svg) +![Predicted values overestimating but still fitting actual values](../../docs/img/holt_winters_prediction_vs_actual.svg) From this you can see that the prediction is overestimating, but still pre-emptively scaling - storing more seasons and adjusting alpha, beta and gamma values would reduce the overestimation and produce more accurate results. +## Requirements + +To set up this example and follow the steps listed here you need: + +- [kubectl](https://kubernetes.io/docs/tasks/tools/). +- A Kubernetes cluster that kubectl is configured to use - [k3d](https://github.com/rancher/k3d) is good for local +testing. +- [helm](https://helm.sh/docs/intro/install/) to install the PHPA operator. +- [jq](https://stedolan.github.io/jq/) to format some JSON output. + ## Usage -If you want to deploy this onto your cluster, you first need to install the [Custom Pod Autoscaler -Operator](https://github.com/jthomperoo/custom-pod-autoscaler-operator), follow the [installation guide for -instructions for installing the -operator](https://github.com/jthomperoo/custom-pod-autoscaler-operator/blob/master/INSTALL.md). -This example was based on the [Horizontal Pod Autoscaler Walkthrough](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/). This example assumes you are using Minikube, or working out of the same Docker -registry as your Kubernetes cluster. +If you want to deploy this onto your cluster, you first need to install the Predictive Horizontal Pod Autoscaler +Operator, follow the [installation guide for instructions for installing the +operator](https://predictive-horizontal-pod-autoscaler.readthedocs.io/en/latest/user-guide/installation). + +This example was based on the [Horizontal Pod Autoscaler +Walkthrough](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/). 1. Use `kubectl apply -f deployment.yaml` to spin up the app/deployment to manage, called `php-apache`. -2. Use `kubectl apply -f phpa.yaml` to start the autoscaler, pointing at the previously created deployment. -3. Build the load tester. - - Point to the cluster's Docker registry (e.g. for this Minikube) `eval $(minikube docker-env)`. - - Build the load tester image `docker build -t load-tester load` -4. Deploy the load tester, note the time as it will run for 30 seconds every minute `kubectl apply -f load/load.yaml`. -5. Use `kubectl logs simple-linear-example --follow` to see the autoscaler working and the log output it produces. +2. Build the tuning image `docker build -t tuning tuning` and import it into your Kubernetes cluster + (`k3d image import tuning`). +4. Deploy the tuning service `kubectl apply -f tuning/tuning.yaml`. +5. Use `kubectl apply -f phpa.yaml` to start the autoscaler, pointing at the previously created deployment. +6. Build the load tester image `docker build -t load-tester load` and import it into your Kubernetes cluster + (`k3d image import load`). +7. Deploy the load tester, note the time as it will run for 30 seconds every minute `kubectl apply -f load/load.yaml`. +8. Use `kubectl logs -l name=predictive-horizontal-pod-autoscaler -f` to see the autoscaler working and the log output +it produces. +9. Use `kubectl logs -l run=tuning -f` to see the logs of the tuning service, it will report any time it is queried and +it will print the value provided to it. +10. Use +`kubectl get configmap predictive-horizontal-pod-autoscaler-simple-holt-winters-data -o=json | jq -r '.data.data | fromjson | .modelHistories["simple-holt-winters"].replicaHistory[] | .time,.replicas'` +to see the replica history for the autoscaler stored in a configmap and tracked by the autoscaler. + +Every minute the load tester will increase the load on the application we are autoscaling for 30 seconds. The PHPA will +initially without any data just act like a Horizontal Pod Autoscaler and will reactively scale up to meet this demand +as best as it can after the demand has already started. After the load tester has run a couple of times the PHPA will +have built up enough data that it can start to make predictions ahead of time using the Holt Winters model, and it +will start calculating these predictions and proactively scaling up ahead of time to meet demand that it expects based +on the data collected in the past. + +## Explanation + +This example is split into four parts: + +- The deployment to autoscale +- Predictive Horizontal Pod Autoscaler (PHPA) +- Tuning Service +- Load Tester -## Explained +### Deployment + +The deployment to autoscale is a simple service that responds to HTTP requests, it uses the `k8s.gcr.io/hpa-example` +image to return `OK!` to any HTTP GET requests. This deployment will have the number of pods assigned to it scaled up +and down. + +### Predictive Horizontal Pod Autoscaler + +The PHPA contains some configuration for how the scaling should be applied, the configuration defines how the +autoscaler will act: -The example has some configuration: ```yaml -config: - - name: minReplicas - value: "1" - - name: maxReplicas - value: "10" - - name: predictiveConfig - value: | - models: - - type: HoltWinters - name: HoltWintersPrediction - perInterval: 1 - holtWinters: - alpha: 0.9 - beta: 0.9 - gamma: 0.9 - seasonalPeriods: 6 - storedSeasons: 4 - trend: additive - seasonal: additive - decisionType: "maximum" - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 50 - - name: interval - value: "20000" - - name: startTime - value: "60000" - - name: downscaleStabilization - value: "30" +apiVersion: jamiethompson.me/v1alpha1 +kind: PredictiveHorizontalPodAutoscaler +metadata: + name: simple-holt-winters +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: php-apache + minReplicas: 1 + maxReplicas: 10 + syncPeriod: 20000 + downscaleStabilization: 30 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + models: + - type: HoltWinters + name: simple-holt-winters + holtWinters: + alpha: 0.9 + beta: 0.9 + gamma: 0.9 + seasonalPeriods: 6 + storedSeasons: 4 + trend: additive + seasonal: additive + ``` -- **minReplicas**, **maxReplicas**, **startTime** and **interval** - Custom Pod Autoscaler options, setting minimum and -maximum replicas, the starting time - for this example will start at the nearest full minute, and the time interval -inbetween each autoscale being run, i.e. the autoscaler checks every 20 seconds. -- **downscaleStabilization** is also a Custom Pod Autoscaler option, in this case changing the `downscaleStabilization` -from the default 300 seconds (5 minutes), to 30 seconds. The `downscaleStabilization` option handles how quickly an -autoscaler can scale down, ensuring that it will pick the highest evaluation that has occurred within the last time -period described, in this case it will pick the highest evaluation over the past 30 seconds. -- **predictiveConfig** - configuration of the predictive elements. - * **models** - predictive models to apply. - - **type** - 'HoltWinters', using a Holt-Winters predictive model. - - **name** - Unique name of the model. - - **holtWinters** - Holt-Winters specific configuration. - * **alpha**, **beta**, **gamma** - these are the smoothing coefficients for level, trend and seasonality + +- `scaleTargetRef` is the resource the autoscaler is targeting for scaling. +- `minReplicas` and `maxReplicas` are the minimum and maximum number of replicas the autoscaler can scale the resource +between. +- `syncPeriod` is how frequently this autoscaler will run in milliseconds, so this autoscaler will run every 20000 +milliseconds (20 seconds). +- `downscaleStabilization` handles how quickly an autoscaler can scale down, ensuring that it will pick the highest evaluation that has occurred within the last time period described, by default it will pick the highest evaluation over +the past 5 minutes. In this case it will pick the highest evaluation over the past 30 seconds. +- `metrics` defines the metrics that the PHPA should use to scale with, in this example it will try to keep average +CPU utilization at 50% per pod. +- `models` - predictive models to apply. + - `type` - 'HoltWinters', using a Holt-Winters predictive model. + - `name` - Unique name of the model. + - `holtWinters` - Holt-Winters specific configuration. + * `alpha`, `beta`, `gamma` - these are the smoothing coefficients for level, trend and seasonality respectively. - * **seasonalPeriods** - the length of a season in base unit intervals, for this example interval is `20000` - (20 seconds), and season length is `6`, resulting in a season length of 20 * 6 = 120 seconds = 2 minutes. - * **storedSeasons** - the number of seasons to store, for this example `4`, if there are more than 4 seasons - stored, the oldest ones are removed. - * **trend** - Either `add`/`additive` or `mul`/`multiplicative`, defines the method for the trend element. - * **seasonal** - Either `add`/`additive` or `mul`/`multiplicative`, defines the method for the seasonal element. - * **decisionType** - strategy for resolving multiple models, either `maximum`, `minimum` or `mean`, in this case - `maximum`, meaning take the highest predicted value. - * **metrics** - Horizontal Pod Autoscaler option, targeting 50% CPU utilisation. + * `seasonalPeriods` - the length of a season in base unit sync periods, for this example sync period is `20000` + (20 seconds), and season length is `6`, resulting in a season length of 20 * 6 = 120 seconds = 2 minutes. + * `storedSeasons` - the number of seasons to store, for this example `4`, if there are more than 4 seasons + stored, the oldest ones are removed. + * `trend` - Either `add`/`additive` or `mul`/`multiplicative`, defines the method for the trend element. + * `seasonal` - Either `add`/`additive` or `mul`/`multiplicative`, defines the method for the seasonal element. + +### Tuning Service + +The tuning service is a simple Flask service that returns the `alpha`, `beta` and `gamma` values in JSON form (in the +format required by the Holt Winters runtime tuning). It also prints out the values provided to it by the Holt Winters +request, these could be used to help calculate the tuning values. + +### Load Tester + +This is a simple pod that runs a bash script to send HTTP requests as fast as possible to the `php-apache` deplyoment +being autoscaled to simulate increased load. diff --git a/examples/simple-holt-winters/load/Dockerfile b/examples/simple-holt-winters/load/Dockerfile index 84d29d5..d060dc2 100644 --- a/examples/simple-holt-winters/load/Dockerfile +++ b/examples/simple-holt-winters/load/Dockerfile @@ -1,4 +1,4 @@ -# Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. +# Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/simple-holt-winters/load/load_tester.sh b/examples/simple-holt-winters/load/load_tester.sh index 22525ee..98918d8 100644 --- a/examples/simple-holt-winters/load/load_tester.sh +++ b/examples/simple-holt-winters/load/load_tester.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. +# Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/simple-holt-winters/phpa.yaml b/examples/simple-holt-winters/phpa.yaml index b261cac..1d3d9c0 100644 --- a/examples/simple-holt-winters/phpa.yaml +++ b/examples/simple-holt-winters/phpa.yaml @@ -1,49 +1,31 @@ -apiVersion: custompodautoscaler.com/v1 -kind: CustomPodAutoscaler +apiVersion: jamiethompson.me/v1alpha1 +kind: PredictiveHorizontalPodAutoscaler metadata: - name: simple-holt-winters-example + name: simple-holt-winters spec: - template: - spec: - containers: - - name: simple-holt-winters-example - image: jthomperoo/predictive-horizontal-pod-autoscaler:latest - imagePullPolicy: Always scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: php-apache - roleRequiresMetricsServer: true - config: - - name: minReplicas - value: "1" - - name: maxReplicas - value: "10" - - name: predictiveConfig - value: | - models: - - type: HoltWinters - name: HoltWintersPrediction - perInterval: 1 - holtWinters: - alpha: 0.9 - beta: 0.9 - gamma: 0.9 - seasonalPeriods: 6 - storedSeasons: 4 - trend: additive - seasonal: additive - decisionType: "maximum" - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 50 - - name: interval - value: "10000" - - name: startTime - value: "60000" - - name: downscaleStabilization - value: "30" + minReplicas: 1 + maxReplicas: 10 + syncPeriod: 20000 + downscaleStabilization: 30 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + models: + - type: HoltWinters + name: simple-holt-winters + holtWinters: + alpha: 0.9 + beta: 0.9 + gamma: 0.9 + seasonalPeriods: 6 + storedSeasons: 4 + trend: additive + seasonal: additive diff --git a/examples/simple-linear/README.md b/examples/simple-linear/README.md index 96f337b..94549be 100644 --- a/examples/simple-linear/README.md +++ b/examples/simple-linear/README.md @@ -1,52 +1,94 @@ # Simple Linear Example -## Overview +This example is showing a Predictive Horizontal Pod Autoscaler (PHPA) using a linear regression model. -This example is showing a predictive horizontal pod autoscaler using a linear regression model, with no persistent storage, so if the scaler is deleted the data will not persist. +## Requirements + +To set up this example and follow the steps listed here you need: + +- [kubectl](https://kubernetes.io/docs/tasks/tools/). +- A Kubernetes cluster that kubectl is configured to use - [k3d](https://github.com/rancher/k3d) is good for local +testing. +- [helm](https://helm.sh/docs/intro/install/) to install the PHPA operator. +- [jq](https://stedolan.github.io/jq/) to format some JSON output. ## Usage -If you want to deploy this onto your cluster, you first need to install the [Custom Pod Autoscaler Operator](https://github.com/jthomperoo/custom-pod-autoscaler-operator), follow the [installation guide for instructions for installing the operator](https://github.com/jthomperoo/custom-pod-autoscaler-operator/blob/master/INSTALL.md). + +If you want to deploy this onto your cluster, you first need to install the Predictive Horizontal Pod Autoscaler +Operator, follow the [installation guide for instructions for installing the +operator](https://predictive-horizontal-pod-autoscaler.readthedocs.io/en/latest/user-guide/installation). This example was based on the [Horizontal Pod Autoscaler Walkthrough](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/). 1. Use `kubectl apply -f deployment.yaml` to spin up the app/deployment to manage, called `php-apache`. 2. Use `kubectl apply -f phpa.yaml` to start the autoscaler, pointing at the previously created deployment. -3. Use `kubectl logs simple-linear-example --follow` to see the autoscaler working and the log output it produces. +3. Use `kubectl logs -l name=predictive-horizontal-pod-autoscaler -f` to see the autoscaler working and the log output +it produces. 4. Increase the load with: `kubectl run -i --tty load-generator --rm --image=busybox --restart=Never -- /bin/sh -c "while sleep 0.01; do wget -q -O- http://php-apache; done"` 5. Watch as the number of replicas increases. -6. Use `kubectl exec -it simple-linear-example sqlite3 /store/predictive-horizontal-pod-autoscaler.db 'SELECT * FROM evaluation;'` to see the evaluations stored locally and tracked by the autoscaler. +6. Use +`kubectl get configmap predictive-horizontal-pod-autoscaler-simple-linear-data -o=json | jq -r '.data.data | fromjson | .modelHistories["simple-linear"].replicaHistory[] | .time,.replicas'` +to see the replica history for the autoscaler stored in a configmap and tracked by the autoscaler. + +As the load is increased the replica count will increase as the PHPA detects average CPU utilization of the pods has +gone above 50%, so will provision more pods to try and bring this value down. As the replica count changes the PHPA +will store the history of replica changes that have been made, and will feed this data into the linear regression model +every time the autoscaler runs too. This results in two values, the raw replica count calculated at the instant the +autoscaler has run, and a predicted value that the model has calculated based on the replica count history. The resource +will then be scaled to the greater of these two values (though there are other options than picking the maximum, see +the [decisionType configuration option for more +details](https://predictive-horizontal-pod-autoscaler.readthedocs.io/en/latest/reference/configuration/#decisiontype)). + +An example of how this predictive scaling looks is this graph: + +![Calculated HPA values vs linear regression predicted values](../../docs/img/getting_started_linear_regression.svg) + +This shows the replica count decreasing, with the raw calculated replica counts and predicted replica counts compared. +As the raw calculated replica count drops of drastically the linear regression takes a smoother scale down approach +based on calculated history. ## Explained -The example has some configuration +This example uses the following YAML to define the autoscaler: + ```yaml -config: - - name: minReplicas - value: "1" - - name: maxReplicas - value: "10" - - name: predictiveConfig - value: | - models: - - type: Linear - name: LinearPrediction - perInterval: 1 - linear: - lookAhead: 10000 - storedValues: 6 - decisionType: "maximum" - metrics: - - type: Resource - resource: - name: cpu - target: - averageUtilization: 50 - type: Utilization - - name: interval - value: "10000" - - name: downscaleStabilization - value: "30" +apiVersion: jamiethompson.me/v1alpha1 +kind: PredictiveHorizontalPodAutoscaler +metadata: + name: simple-linear +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: php-apache + minReplicas: 1 + maxReplicas: 10 + syncPeriod: 10000 + downscaleStabilization: 0 + metrics: + - type: Resource + resource: + name: cpu + target: + averageUtilization: 50 + type: Utilization + models: + - type: Linear + name: simple-linear + linear: + lookAhead: 10000 + historySize: 6 ``` -The `minReplicas`, `maxReplicas` and `interval` are Custom Pod Autoscaler options, setting minimum and maximum replicas, and the time interval inbetween each autoscale being run, i.e. the autoscaler checks every 10 seconds. -The `downscaleStabilization` is also a Custom Pod Autoscaler option, in this case changing the `downscaleStabilization` from the default 300 seconds (5 minutes), to 30 seconds. The `downscaleStabilization` option handles how quickly an autoscaler can scale down, ensuring that it will pick the highest evaluation that has occurred within the last time period described, in this case it will pick the highest evaluation over the past 30 seconds. -The `predictiveConfig` option is the Predictive Horizontal Pod Autoscaler options, detailing a linear regression model that runs on every interval, looking 10 seconds ahead, keeping track of the past 6 replica values in order to predict the next result, and the `decisionType` is maximum, which if there were multiple models provided would mean that the PHPA would use the one with the highest replica count; there are two other options, `mean` and `minimum`. The `metrics` option is a Horizontal Pod Autoscaler option, targeting CPU utilisation. + +- `scaleTargetRef` is the resource the autoscaler is targeting for scaling. +- `minReplicas` and `maxReplicas` are the minimum and maximum number of replicas the autoscaler can scale the resource +between. +- `syncPeriod` is how frequently this autoscaler will run in milliseconds, so this autoscaler will run every 10000 +milliseconds (10 seconds). +- `downscaleStabilization` handles how quickly an autoscaler can scale down, ensuring that it will pick the highest evaluation that has occurred within the last time period described, by default it will pick the highest evaluation over +the past 5 minutes. To stop this from happening we have set the downscale stablilization to `0` to disable it. +- `metrics` defines the metrics that the PHPA should use to scale with, in this example it will try to keep average +CPU utilization at 50% per pod. +- `models` contains the statistical models we're applying to the resulting replica count, this this example it's just +a linear regression model that stores the past 6 replica counts (`historySize: 6`) and uses that to predict what the +replica count will be 10000 milliseconds (10 seconds) in the future (`lookAhead: 10000`). diff --git a/examples/simple-linear/phpa.yaml b/examples/simple-linear/phpa.yaml index ce77881..c823023 100644 --- a/examples/simple-linear/phpa.yaml +++ b/examples/simple-linear/phpa.yaml @@ -1,42 +1,25 @@ -apiVersion: custompodautoscaler.com/v1 -kind: CustomPodAutoscaler +apiVersion: jamiethompson.me/v1alpha1 +kind: PredictiveHorizontalPodAutoscaler metadata: - name: simple-linear-example + name: simple-linear spec: - template: - spec: - containers: - - name: simple-linear-example - image: jthomperoo/predictive-horizontal-pod-autoscaler:latest - imagePullPolicy: IfNotPresent scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: php-apache - roleRequiresMetricsServer: true - config: - - name: minReplicas - value: "1" - - name: maxReplicas - value: "10" - - name: predictiveConfig - value: | - models: - - type: Linear - name: LinearPrediction - perInterval: 1 - linear: - lookAhead: 10000 - storedValues: 6 - decisionType: "maximum" - metrics: - - type: Resource - resource: - name: cpu - target: - averageUtilization: 50 - type: Utilization - - name: interval - value: "10000" - - name: downscaleStabilization - value: "0" + minReplicas: 1 + maxReplicas: 10 + downscaleStabilization: 0 + metrics: + - type: Resource + resource: + name: cpu + target: + averageUtilization: 50 + type: Utilization + models: + - type: Linear + name: simple-linear + linear: + lookAhead: 10000 + historySize: 6 diff --git a/go.mod b/go.mod index e243aa5..5ce8d9e 100644 --- a/go.mod +++ b/go.mod @@ -1,51 +1,90 @@ module github.com/jthomperoo/predictive-horizontal-pod-autoscaler -go 1.17 +go 1.18 require ( - github.com/golang-migrate/migrate/v4 v4.7.0 - github.com/google/go-cmp v0.5.8 - github.com/jthomperoo/custom-pod-autoscaler/v2 v2.7.0 - github.com/jthomperoo/k8shorizmetrics v1.0.0 - github.com/mattn/go-sqlite3 v1.14.13 - k8s.io/api v0.24.0 - k8s.io/apimachinery v0.24.0 - k8s.io/client-go v0.24.0 + github.com/cosmtrek/air v1.40.4 + github.com/google/go-cmp v0.5.5 + github.com/jthomperoo/k8shorizmetrics v1.0.1-0.20220717205713-fa28a589e16a + honnef.co/go/tools v0.3.2 + k8s.io/api v0.24.2 + k8s.io/apimachinery v0.24.2 + k8s.io/client-go v0.24.2 + sigs.k8s.io/controller-runtime v0.12.2 ) require ( + cloud.google.com/go v0.81.0 // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.18 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect + github.com/Azure/go-autorest/logger v0.2.1 // indirect + github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/BurntSushi/toml v0.4.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/creack/pty v1.1.11 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/emicklei/go-restful v2.15.0+incompatible // indirect - github.com/go-logr/logr v1.2.3 // indirect + github.com/emicklei/go-restful v2.9.5+incompatible // indirect + github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/fatih/color v1.10.0 // indirect + github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect + github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/go-logr/logr v1.2.0 // 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.20.0 // indirect - github.com/go-openapi/swag v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.19.5 // indirect + github.com/go-openapi/swag v0.19.14 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect - github.com/google/gnostic v0.6.9 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/hashicorp/errwrap v1.0.0 // indirect - github.com/hashicorp/go-multierror v1.0.0 // indirect + github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/google/gofuzz v1.1.0 // indirect + github.com/google/uuid v1.1.2 // indirect + github.com/imdario/mergo v0.3.12 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-colorable v0.1.8 // indirect + github.com/mattn/go-isatty v0.0.12 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 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 - golang.org/x/net v0.0.0-20220513224357-95641704303c // indirect - golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect - golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a // indirect - golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect + github.com/pelletier/go-toml v1.8.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.12.1 // 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/spf13/pflag v1.0.5 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.19.1 // indirect + golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect + golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e // indirect + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect + golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect + golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect + golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect - golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect + golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect + golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f // indirect + gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.28.0 // indirect + google.golang.org/protobuf v1.27.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + k8s.io/apiextensions-apiserver v0.24.2 // indirect + k8s.io/component-base v0.24.2 // indirect k8s.io/klog/v2 v2.60.1 // indirect - k8s.io/kube-openapi v0.0.0-20220413171646-5e7f5fdc6da6 // indirect - k8s.io/metrics v0.24.0 // indirect + k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect + k8s.io/metrics v0.21.8 // indirect k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect diff --git a/go.sum b/go.sum index 8d968c3..2f70b52 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,5 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= @@ -18,6 +17,7 @@ cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKP cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= @@ -27,6 +27,7 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -37,40 +38,64 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= +github.com/Azure/go-autorest/autorest v0.11.18 h1:90Y4srNYrwOtAgVo3ndrQkTYn6kf1Eg/AjTFJ8Is2aM= github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q= github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= +github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 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/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +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= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -79,102 +104,104 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= -github.com/containerd/containerd v1.2.7/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= +github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cosmtrek/air v1.40.4 h1:AjSlvS7IofbSf4m0BkJLm6TnBlfREwkJ9eCmLR3FLHc= +github.com/cosmtrek/air v1.40.4/go.mod h1:Urz3nl9UBvc/rntZkXRBttYWt4sBeh2NZaGcdBbkNak= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8= -github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg= -github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc= -github.com/cznic/internal v0.0.0-20180608152220-f44710a21d00/go.mod h1:olo7eAdKwJdXxb55TKGLiJ6xt1H0/tiiRCWKVLmtjY4= -github.com/cznic/lldb v1.1.0/go.mod h1:FIZVUmYUVhPwRiPzL8nD/mpFcJ/G7SSXjjXYG4uRI3A= -github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= -github.com/cznic/ql v1.2.0/go.mod h1:FbpzhyZrqr0PVlK6ury+PoW3T0ODUV22OeWIxcaOrSE= -github.com/cznic/sortutil v0.0.0-20150617083342-4c7342852e65/go.mod h1:q2w6Bg5jeox1B+QkJ6Wp/+Vn0G/bo3f1uY7Fn3vivIQ= -github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc= -github.com/cznic/zappy v0.0.0-20160723133515-2533cb5b45cc/go.mod h1:Y1SNZ4dRUOKXshKUbwUapqNncRrho4mkjQebgEHZLj8= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= -github.com/dhui/dktest v0.3.0/go.mod h1:cyzIUfGsBEbZ6BT7tnXqAShHSXCZhSNmFl70sZ7c1yc= -github.com/docker/distribution v2.7.0+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v0.7.3-0.20190103212154-2b7e084dc98b/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v0.7.3-0.20190817195342-4760db040282/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful v2.15.0+incompatible h1:8KpYO/Xl/ZudZs5RNOEhWMBY4hmzlZhhRd9cu+jrZP4= -github.com/emicklei/go-restful v2.15.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +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= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c= github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsouza/fake-gcs-server v1.7.0/go.mod h1:5XIRs4YvwNbNoz+1JF8j6KLAyDh7RHGAyAK3EP2EsNk= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +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.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= -github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= -github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU= -github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-migrate/migrate/v4 v4.7.0 h1:gONcHxHApDTKXDyLH/H97gEHmpu1zcnnbAaq2zgrPrs= -github.com/golang-migrate/migrate/v4 v4.7.0/go.mod h1:Qvut3N4xKWjoH3sokBccML6WyHSnggXm/DvMMnTsQIc= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -202,15 +229,13 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/cel-go v0.10.1/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w= +github.com/google/cel-spec v0.6.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA= +github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= -github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= -github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -221,15 +246,11 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -251,47 +272,67 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= -github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jthomperoo/custom-pod-autoscaler/v2 v2.7.0 h1:dftgFehBCCd0JHZ9vmTvI10Dcicgpg/SHIT0E+2vH80= -github.com/jthomperoo/custom-pod-autoscaler/v2 v2.7.0/go.mod h1:jLcBdWoe5AH5VFzgcci/z2WValVxDw/xVlGtzy5GToA= -github.com/jthomperoo/k8shorizmetrics v1.0.0 h1:GYAOPSbfE760WGj8ic6PH4IQetFuiXjQJUOPFQMIi5Q= -github.com/jthomperoo/k8shorizmetrics v1.0.0/go.mod h1:MnIAv05IAb/DxcoyvbBrPTjBIfkQbrcBJfZw+hsg+Zo= +github.com/jthomperoo/k8shorizmetrics v1.0.1-0.20220717205713-fa28a589e16a h1:DYttTAgI4hJ8mVZhIl9nOvddRLauYw2yG84cyV++jKQ= +github.com/jthomperoo/k8shorizmetrics v1.0.1-0.20220717205713-fa28a589e16a/go.mod h1:MnIAv05IAb/DxcoyvbBrPTjBIfkQbrcBJfZw+hsg+Zo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -300,20 +341,33 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= 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/kshvakov/clickhouse v1.3.5/go.mod h1:DMzX7FxRymoNkVgizH0DWAL8Cur7wHLgx3MUnGwJqpE= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I= -github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 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= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -321,63 +375,100 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 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= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4= -github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/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.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs= -github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= +github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 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= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +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= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -387,24 +478,30 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= -github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= -github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= -go.mongodb.org/mongo-driver v1.1.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= -go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= +go.etcd.io/etcd/client/v3 v3.5.1/go.mod h1:OnjH4M8OnAotwaB2l9bVgZzRFKru7/ZMoS46OtKyd3Q= +go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= +go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= +go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -412,19 +509,45 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= +go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= +go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= +go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= +go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= +go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= +go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= +go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= +go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -436,6 +559,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e h1:qyrTQ++p1afMkO4DPEeLGq/3oTsdlvdH4vqZUBWzUKM= +golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -449,6 +574,7 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -462,18 +588,19 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= @@ -500,22 +627,21 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/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-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220513224357-95641704303c h1:nF9mHSvoKBLkQNQhJZNsc66z2UzAMUbLGjC95CF3pU0= -golang.org/x/net v0.0.0-20220513224357-95641704303c/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -525,9 +651,9 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -539,16 +665,17 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190102155601-82a175fd1598/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190426135247-a129542de9ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -562,7 +689,9 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -575,33 +704,41 @@ golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a h1:N2T1jUrTQE9Re6TFF5PhvEHXHCguynGhKjWVsIUt5cY= -golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8= -golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -616,28 +753,30 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w= -golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190425222832-ad9eeb80039a/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -672,15 +811,18 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f h1:OKYpQQVE3DKSc3r3zHVzq46vq5YH7x8xpR3/k9ixmUg= +golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= -google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= +gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -703,7 +845,6 @@ google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjR google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= @@ -713,7 +854,6 @@ google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -733,6 +873,7 @@ google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= @@ -744,6 +885,7 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201102152239-715cce707fb0/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -753,8 +895,9 @@ google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -774,6 +917,8 @@ google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA5 google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -787,9 +932,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -800,23 +944,28 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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= -gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 h1:dbuHpmKjkDzSOMKAWl10QNlgaZUd3V1q99xc81tt2Kc= -gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +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= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -824,17 +973,24 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 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= +honnef.co/go/tools v0.3.2 h1:ytYb4rOqyp1TSa2EPvNVwtPQJctSELKaMyLfqNP4+34= +honnef.co/go/tools v0.3.2/go.mod h1:jzwdWgg7Jdq75wlfblQxO4neNaFFSvgc1tD5Wv8U0Yw= k8s.io/api v0.21.8/go.mod h1:4A6eY7AJ+cAgEws3bGRIP2Wyia6VYmHbwIoQu83yyok= -k8s.io/api v0.24.0 h1:J0hann2hfxWr1hinZIDefw7Q96wmCBx6SSB8IY0MdDg= -k8s.io/api v0.24.0/go.mod h1:5Jl90IUrJHUJYEMANRURMiVvJ0g7Ax7r3R1bqO8zx8I= +k8s.io/api v0.24.2 h1:g518dPU/L7VRLxWfcadQn2OnsiGWVOadTLpdnqgY2OI= +k8s.io/api v0.24.2/go.mod h1:AHqbSkTm6YrQ0ObxjO3Pmp/ubFF/KuM7jU+3khoBsOg= +k8s.io/apiextensions-apiserver v0.24.2 h1:/4NEQHKlEz1MlaK/wHT5KMKC9UKYz6NZz6JE6ov4G6k= +k8s.io/apiextensions-apiserver v0.24.2/go.mod h1:e5t2GMFVngUEHUd0wuCJzw8YDwZoqZfJiGOW6mm2hLQ= k8s.io/apimachinery v0.21.8/go.mod h1:VdCLtKQwpSUQEWDaRsWR5ESDRqqhY2NeVzo9KCQ9uy0= -k8s.io/apimachinery v0.24.0 h1:ydFCyC/DjCvFCHK5OPMKBlxayQytB8pxy8YQInd5UyQ= -k8s.io/apimachinery v0.24.0/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= +k8s.io/apimachinery v0.24.2 h1:5QlH9SL2C8KMcrNJPor+LbXVTaZRReml7svPEh4OKDM= +k8s.io/apimachinery v0.24.2/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= +k8s.io/apiserver v0.24.2/go.mod h1:pSuKzr3zV+L+MWqsEo0kHHYwCo77AT5qXbFXP2jbvFI= k8s.io/client-go v0.21.8/go.mod h1:FcrKJITUrWtQSX3SCmqswlETK8AcVDTTA5PxDPxNtcg= -k8s.io/client-go v0.24.0 h1:lbE4aB1gTHvYFSwm6eD3OF14NhFDKCejlnsGYlSJe5U= -k8s.io/client-go v0.24.0/go.mod h1:VFPQET+cAFpYxh6Bq6f4xyMY80G6jKKktU6G0m00VDw= +k8s.io/client-go v0.24.2 h1:CoXFSf8if+bLEbinDqN9ePIDGzcLtqhfd6jpfnwGOFA= +k8s.io/client-go v0.24.2/go.mod h1:zg4Xaoo+umDsfCWr4fCnmLEtQXyCNXCvJuSsglNcV30= k8s.io/code-generator v0.21.8/go.mod h1:py8RM46Y1+H0IWM1oO9pOGz60GhD9uW2SFlnXI5jZno= -k8s.io/code-generator v0.24.0/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI15w= +k8s.io/code-generator v0.24.2/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI15w= +k8s.io/component-base v0.24.2 h1:kwpQdoSfbcH+8MPN4tALtajLDfSfYxBDYlXobNWI6OU= +k8s.io/component-base v0.24.2/go.mod h1:ucHwW76dajvQ9B7+zecZAP3BVqvrHoOxm8olHEg0nmM= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= @@ -845,12 +1001,10 @@ k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/klog/v2 v2.60.1 h1:VW25q3bZx9uE3vvdL6M8ezOX79vA2Aq1nEWLqNQclHc= k8s.io/klog/v2 v2.60.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20211110012726-3cc51fd1e909/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= +k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 h1:Gii5eqf+GmIEwGNKQYQClCayuJCe2/4fZUvF7VG99sU= k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42/go.mod h1:Z/45zLw8lUo4wdiUkI+v/ImEGAvu3WatcZl3lPMR4Rk= -k8s.io/kube-openapi v0.0.0-20220413171646-5e7f5fdc6da6 h1:nBQrWPlrNIiw0BsX6a6MKr1itkm0ZS0Nl97kNLitFfI= -k8s.io/kube-openapi v0.0.0-20220413171646-5e7f5fdc6da6/go.mod h1:daOouuuwd9JXpv1L7Y34iV3yf6nxzipkKMWWlqlvK9M= +k8s.io/metrics v0.21.8 h1:gup3ZCg3dKhy2rLvl5bVIh38LPSTshNszj1bKldpkGo= k8s.io/metrics v0.21.8/go.mod h1:EL6uOMDkCCjXUB25K4VteweC1MQMOWsHCaU3C+3LdFs= -k8s.io/metrics v0.24.0 h1:nsFLJBDgj+B8mXvVBWFxTZBRRDJ8uTdf4C/Gedjy9BA= -k8s.io/metrics v0.24.0/go.mod h1:jrLlFGdKl3X+szubOXPG0Lf2aVxuV3QJcbsgVRAM6fI= k8s.io/utils v0.0.0-20210521133846-da695404a2bc/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 h1:HNSDgDCrr/6Ly3WEGKZftiE7IY19Vz2GdbOCyI4qqhc= @@ -858,6 +1012,9 @@ k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30/go.mod h1:fEO7lRTdivWO2qYVCVG7dEADOMo/MLDCVr8So2g88Uw= +sigs.k8s.io/controller-runtime v0.12.2 h1:nqV02cvhbAj7tbt21bpPpTByrXGn2INHRsi39lXy9sE= +sigs.k8s.io/controller-runtime v0.12.2/go.mod h1:qKsk4WE6zW2Hfj0G4v10EnNB2jMG1C+NTb8h+DwCoU0= sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 h1:kDi4JBNAsJWfz1aEXhO8Jg87JJaPNLh5tIzYHgStQ9Y= sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= diff --git a/internal/stored/stored_test.go b/hack/boilerplate.go.txt similarity index 86% rename from internal/stored/stored_test.go rename to hack/boilerplate.go.txt index 5e3ef06..439b002 100644 --- a/internal/stored/stored_test.go +++ b/hack/boilerplate.go.txt @@ -1,5 +1,5 @@ /* -Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. +Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -13,5 +13,3 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ - -package stored_test diff --git a/helm/.helmignore b/helm/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/Chart.yaml b/helm/Chart.yaml new file mode 100644 index 0000000..8a0151a --- /dev/null +++ b/helm/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: predictive-horizontal-pod-autoscaler +description: The Predictive Horizontal Pod Autoscaler Operator allows deployment of Predictive Horizontal Pod Autoscalers + +type: application + +version: 0.0.0 diff --git a/helm/templates/NOTES.txt b/helm/templates/NOTES.txt new file mode 100644 index 0000000..037460d --- /dev/null +++ b/helm/templates/NOTES.txt @@ -0,0 +1,6 @@ +Thanks for installing {{ .Chart.Name }}. + +Your release is named {{ .Release.Name }}. + +To find out more/request features/report bugs, check out the Predictive Horizontal Pod Autoscaler repo here: +https://github.com/jthomperoo/predictive-horizontal-pod-autoscaler diff --git a/helm/templates/cluster/cluster_role_binding.yaml b/helm/templates/cluster/cluster_role_binding.yaml new file mode 100644 index 0000000..7667975 --- /dev/null +++ b/helm/templates/cluster/cluster_role_binding.yaml @@ -0,0 +1,12 @@ +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ .Chart.Name }} +subjects: +- kind: ServiceAccount + name: {{ .Chart.Name }}-cluster + namespace: {{ .Release.Namespace }} +roleRef: + kind: ClusterRole + name: {{ .Chart.Name }} + apiGroup: rbac.authorization.k8s.io diff --git a/helm/templates/cluster/deployment.yaml b/helm/templates/cluster/deployment.yaml new file mode 100644 index 0000000..5005c26 --- /dev/null +++ b/helm/templates/cluster/deployment.yaml @@ -0,0 +1,21 @@ +{{ if eq .Values.mode "cluster"}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Chart.Name }} +spec: + replicas: 1 + selector: + matchLabels: + name: {{ .Chart.Name }} + template: + metadata: + labels: + name: {{ .Chart.Name }} + spec: + serviceAccountName: {{ .Chart.Name }}-cluster + containers: + - name: {{ .Chart.Name }} + image: "jthomperoo/predictive-horizontal-pod-autoscaler:{{ .Chart.Version }}" + imagePullPolicy: IfNotPresent +{{ end }} diff --git a/helm/templates/cluster/manifests.yaml b/helm/templates/cluster/manifests.yaml new file mode 100644 index 0000000..57cc3a0 --- /dev/null +++ b/helm/templates/cluster/manifests.yaml @@ -0,0 +1,54 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + creationTimestamp: null + name: mutating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-jamiethompson-me-jamiethompson-me-v1alpha1-predictivehorizontalpodautoscaler + failurePolicy: Fail + name: mpredictivehorizontalpodautoscaler.kb.io + rules: + - apiGroups: + - jamiethompson.me.jamiethompson.me + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - predictivehorizontalpodautoscalers + sideEffects: None +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + creationTimestamp: null + name: validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-jamiethompson-me-jamiethompson-me-v1alpha1-predictivehorizontalpodautoscaler + failurePolicy: Fail + name: vpredictivehorizontalpodautoscaler.kb.io + rules: + - apiGroups: + - jamiethompson.me.jamiethompson.me + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - predictivehorizontalpodautoscalers + sideEffects: None diff --git a/helm/templates/cluster/role.yaml b/helm/templates/cluster/role.yaml new file mode 100644 index 0000000..1fcd903 --- /dev/null +++ b/helm/templates/cluster/role.yaml @@ -0,0 +1,91 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: predictive-horizontal-pod-autoscaler +rules: +- apiGroups: + - apps + resources: + - deployments/scale + - replicaset/scale + - statefulset/scale + verbs: + - get + - patch + - update +- apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - pods + verbs: + - get + - list +- apiGroups: + - "" + resources: + - replicationcontrollers/scale + verbs: + - get + - patch + - update +- apiGroups: + - custom.metrics.k8s.io + resources: + - '*' + verbs: + - get + - list +- apiGroups: + - external.metrics.k8s.io + resources: + - '*' + verbs: + - get + - list +- apiGroups: + - jamiethompson.me + resources: + - predictivehorizontalpodautoscalers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - jamiethompson.me + resources: + - predictivehorizontalpodautoscalers/finalizers + verbs: + - update +- apiGroups: + - jamiethompson.me + resources: + - predictivehorizontalpodautoscalers/status + verbs: + - get + - patch + - update +- apiGroups: + - metrics.k8s.io + resources: + - '*' + verbs: + - get + - list diff --git a/helm/templates/cluster/service_account.yaml b/helm/templates/cluster/service_account.yaml new file mode 100644 index 0000000..6c2c757 --- /dev/null +++ b/helm/templates/cluster/service_account.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Chart.Name }}-cluster diff --git a/helm/templates/crd/jamiethompson.me_predictivehorizontalpodautoscalers.yaml b/helm/templates/crd/jamiethompson.me_predictivehorizontalpodautoscalers.yaml new file mode 100644 index 0000000..0601fd7 --- /dev/null +++ b/helm/templates/crd/jamiethompson.me_predictivehorizontalpodautoscalers.yaml @@ -0,0 +1,1291 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: predictivehorizontalpodautoscalers.jamiethompson.me +spec: + group: jamiethompson.me + names: + kind: PredictiveHorizontalPodAutoscaler + listKind: PredictiveHorizontalPodAutoscalerList + plural: predictivehorizontalpodautoscalers + shortNames: + - phpa + singular: predictivehorizontalpodautoscaler + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The identifier for the resource being scaled in the format / + jsonPath: .status.reference + name: Reference + type: string + - description: The minimum number of replicas of pods that the resource being + managed by the autoscaler can have + jsonPath: .spec.minReplicas + name: Min Pods + type: integer + - description: The maximum number of replicas of pods that the resource being + managed by the autoscaler can have + jsonPath: .spec.maxReplicas + name: Max Pods + type: integer + - description: The desired number of replicas of pods managed by this autoscaler + as last calculated by the autoscaler + jsonPath: .status.desiredReplicas + name: Replicas + type: integer + - description: The last time the PredictiveHorizontalPodAutoscaler scaled the + number of pods + jsonPath: .status.lastScaleTime + name: Last Scale Time + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: PredictiveHorizontalPodAutoscaler is the Schema for the predictivehorizontalpodautoscalers + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: PredictiveHorizontalPodAutoscalerSpec defines the desired + state of PredictiveHorizontalPodAutoscaler + properties: + cpuInitializationPeriod: + description: cpuInitializationPeriod is equivalent to --horizontal-pod-autoscaler-cpu-initialization-period; + the period after pod start when CPU samples might be skipped. Default + value 300 seconds (5 minutes). + minimum: 0 + type: integer + decisionType: + description: decisionType is the strategy to use when picking which + replica count to use if you have multiple models, or even just choosing + between the calculculated replicas and the predicted replicas of + a single model. For details on which decisionTypes are available + visit https://predictive-horizontal-pod-autoscaler.readthedocs.io/en/latest/reference/configuration/#decisiontype + Default strategy is 'maximum' + enum: + - maximum + - minimum + - mean + - median + type: string + downscaleStabilization: + description: downscaleStabilization defines in seconds the length + of the downscale stabilization window; based on the Horizontal Pod + Autoscaler downscale stabilization. Downscale stabilization works + by recording all evaluations over the window specified and picking + out the maximum target replicas from these evaluations. This results + in a more smoothed downscaling and a cooldown, which can reduce + the effect of thrashing. Default value 300 seconds (5 minutes). + minimum: 0 + type: integer + initialReadinessDelay: + description: initialReadinessDelay is equivalent to --horizontal-pod-autoscaler-initial-readiness-delay; + the period after pod start during which readiness changes will be + treated as initial readiness. Default value 30 seconds. + minimum: 0 + type: integer + maxReplicas: + description: maxReplicas is the upper limit for the number of replicas + to which the autoscaler can scale up. It cannot be less than minReplicas. + format: int32 + minimum: 1 + type: integer + metrics: + description: metrics contains the specifications for which to use + to calculate the desired replica count (the maximum replica count + across all metrics will be used). The desired replica count is + calculated multiplying the ratio between the target value and the + current value by the current number of pods. Ergo, metrics used + must decrease as the pod count is increased, and vice-versa. See + the individual metric source types for more information about how + each type of metric must respond. If not set, the default metric + will be set to 80% average CPU utilization. + items: + description: MetricSpec specifies how to scale based on a single + metric (only `type` and one other matching field should be set + at once). + properties: + containerResource: + description: container resource refers to a resource metric + (such as those specified in requests and limits) known to + Kubernetes describing a single container in each pod of the + current scale target (e.g. CPU or memory). Such metrics are + built in to Kubernetes, and have special scaling options on + top of those available to normal per-pod metrics using the + "pods" source. This is an alpha feature and can be enabled + by the HPAContainerMetrics feature flag. + properties: + container: + description: container is the name of the container in the + pods of the scaling target + type: string + name: + description: name is the name of the resource in question. + type: string + target: + description: target specifies the target value for the given + metric + properties: + averageUtilization: + description: averageUtilization is the target value + of the average of the resource metric across all relevant + pods, represented as a percentage of the requested + value of the resource for the pods. Currently only + valid for Resource metric source type + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: averageValue is the target value of the + average of the metric across all relevant pods (as + a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the metric type + is Utilization, Value, or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of the metric + (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - container + - name + - target + type: object + external: + description: external refers to a global metric that is not + associated with any Kubernetes object. It allows autoscaling + based on information coming from components running outside + of cluster (for example length of queue in cloud messaging + service, or QPS from loadbalancer running outside of cluster). + properties: + metric: + description: metric identifies the target metric by name + and selector + properties: + name: + description: name is the name of the given metric + type: string + selector: + description: selector is the string-encoded form of + a standard kubernetes label selector for the given + metric When set, it is passed as an additional parameter + to the metrics server for more specific metrics scoping. + When unset, just the metricName will be used to gather + metrics. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a + selector that contains values, a key, and an + operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If the + operator is Exists or DoesNotExist, the + values array must be empty. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". The + requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value for the given + metric + properties: + averageUtilization: + description: averageUtilization is the target value + of the average of the resource metric across all relevant + pods, represented as a percentage of the requested + value of the resource for the pods. Currently only + valid for Resource metric source type + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: averageValue is the target value of the + average of the metric across all relevant pods (as + a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the metric type + is Utilization, Value, or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of the metric + (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + object: + description: object refers to a metric describing a single kubernetes + object (for example, hits-per-second on an Ingress object). + properties: + describedObject: + description: CrossVersionObjectReference contains enough + information to let you identify the referred resource. + properties: + apiVersion: + description: API version of the referent + type: string + kind: + description: 'Kind of the referent; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds"' + type: string + name: + description: 'Name of the referent; More info: http://kubernetes.io/docs/user-guide/identifiers#names' + type: string + required: + - kind + - name + type: object + metric: + description: metric identifies the target metric by name + and selector + properties: + name: + description: name is the name of the given metric + type: string + selector: + description: selector is the string-encoded form of + a standard kubernetes label selector for the given + metric When set, it is passed as an additional parameter + to the metrics server for more specific metrics scoping. + When unset, just the metricName will be used to gather + metrics. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a + selector that contains values, a key, and an + operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If the + operator is Exists or DoesNotExist, the + values array must be empty. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". The + requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value for the given + metric + properties: + averageUtilization: + description: averageUtilization is the target value + of the average of the resource metric across all relevant + pods, represented as a percentage of the requested + value of the resource for the pods. Currently only + valid for Resource metric source type + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: averageValue is the target value of the + average of the metric across all relevant pods (as + a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the metric type + is Utilization, Value, or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of the metric + (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - describedObject + - metric + - target + type: object + pods: + description: pods refers to a metric describing each pod in + the current scale target (for example, transactions-processed-per-second). The + values will be averaged together before being compared to + the target value. + properties: + metric: + description: metric identifies the target metric by name + and selector + properties: + name: + description: name is the name of the given metric + type: string + selector: + description: selector is the string-encoded form of + a standard kubernetes label selector for the given + metric When set, it is passed as an additional parameter + to the metrics server for more specific metrics scoping. + When unset, just the metricName will be used to gather + metrics. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a + selector that contains values, a key, and an + operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If the + operator is Exists or DoesNotExist, the + values array must be empty. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". The + requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value for the given + metric + properties: + averageUtilization: + description: averageUtilization is the target value + of the average of the resource metric across all relevant + pods, represented as a percentage of the requested + value of the resource for the pods. Currently only + valid for Resource metric source type + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: averageValue is the target value of the + average of the metric across all relevant pods (as + a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the metric type + is Utilization, Value, or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of the metric + (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + resource: + description: resource refers to a resource metric (such as those + specified in requests and limits) known to Kubernetes describing + each pod in the current scale target (e.g. CPU or memory). + Such metrics are built in to Kubernetes, and have special + scaling options on top of those available to normal per-pod + metrics using the "pods" source. + properties: + name: + description: name is the name of the resource in question. + type: string + target: + description: target specifies the target value for the given + metric + properties: + averageUtilization: + description: averageUtilization is the target value + of the average of the resource metric across all relevant + pods, represented as a percentage of the requested + value of the resource for the pods. Currently only + valid for Resource metric source type + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: averageValue is the target value of the + average of the metric across all relevant pods (as + a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the metric type + is Utilization, Value, or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of the metric + (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - name + - target + type: object + type: + description: 'type is the type of metric source. It should + be one of "ContainerResource", "External", "Object", "Pods" + or "Resource", each mapping to a matching field in the object. + Note: "ContainerResource" type is available on when the feature-gate + HPAContainerMetrics is enabled' + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + minReplicas: + description: minReplicas is the lower limit for the number of replicas + to which the autoscaler can scale down. It defaults to 1 pod. minReplicas + is allowed to be 0 if at least one Object or External metric is + configured. Scaling is active as long as at least one metric value + is available. + format: int32 + minimum: 0 + type: integer + models: + description: models is the list of models to apply to the calculated + replica count to calculate predicted replica values. + items: + description: Model represents a prediction model to use, e.g. a + linear regression + properties: + calculationTimeout: + description: 'calculationTimeout is how long the PHPA should + allow for the model to calculate a value in milliseconds, + if it takes longer than this timeout it should skip processing + the model. Default varies based on model type: Linear is 30000 + milliseconds (30 seconds)' + minimum: 1 + type: integer + holtWinters: + description: holtWinters is the configuration to use for the + holt winters model, it will only be used if the type is set + to 'HoltWinters' + properties: + alpha: + minimum: 0 + type: number + beta: + minimum: 0 + type: number + dampedTrend: + type: boolean + gamma: + minimum: 0 + type: number + initialLevel: + type: number + initialSeasonal: + type: number + initialTrend: + type: number + initializationMethod: + enum: + - estimated + - heuristic + - known + - legacy-heuristic + type: string + runtimeTuningFetchHook: + description: HookDefinition describes a hook for passing + data/triggering logic, such as through a shell command + properties: + http: + description: HTTPHook describes configuration options + for an HTTP request hook + properties: + headers: + additionalProperties: + type: string + type: object + method: + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + parameterMode: + enum: + - query + - body + type: string + successCodes: + items: + type: integer + type: array + url: + type: string + required: + - method + - parameterMode + - successCodes + - url + type: object + timeout: + minimum: 1 + type: integer + type: + enum: + - http + type: string + required: + - timeout + - type + type: object + seasonal: + enum: + - add + - additive + - mul + - multiplicative + type: string + seasonalPeriods: + minimum: 1 + type: integer + storedSeasons: + minimum: 1 + type: integer + trend: + enum: + - add + - additive + - mul + - multiplicative + type: string + required: + - seasonal + - seasonalPeriods + - storedSeasons + - trend + type: object + linear: + description: linear is the configuration to use for the linear + regression model, it will only be used if the type is set + to 'Linear'. + properties: + historySize: + description: historySize is how many timestamped replica + counts should be stored for this linear regression, with + older timestamped replica counts being removed from the + data as new ones are added. For example a value of 6 means + there will only be a maxmimu of 6 stored timestamped replica + counts for this model. + minimum: 1 + type: integer + lookAhead: + description: lookAhead is how far in the future should the + linear regression predict in seconds. For example a value + of 10 will predict 10 seconds into the future + minimum: 1 + type: integer + required: + - historySize + - lookAhead + type: object + name: + description: name is the name of the model, this can be any + arbitrary name and is just used to distinguish between models + if you have multiple and to keep track of model data if you + modify your model parameters. + type: string + perSyncPeriod: + description: perSyncPeriod is how frequently this model will + run, with the syncPeriod as a base unit. This allows for you + to have multiple models which run at different time intervals, + or only run the model every x number of sync periods if the + model is computation intensive. For sync periods that the + model is not run on, it will still add the calculated replica + values to the model data history and then prune that history + if needs. Default value is 1 (run every sync period) + minimum: 1 + type: integer + type: + description: type is the type of the model, for example 'Linear'. + To see a full list of supported model types visit https://predictive-horizontal-pod-autoscaler.readthedocs.io/en/latest/user-guide/models/. + enum: + - Linear + - HoltWinters + type: string + required: + - name + - type + type: object + type: array + scaleTargetRef: + description: scaleTargetRef points to the target resource to scale, + and is used to the pods for which metrics should be collected, as + well as to actually change the replica count. + properties: + apiVersion: + description: API version of the referent + type: string + kind: + description: 'Kind of the referent; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds"' + type: string + name: + description: 'Name of the referent; More info: http://kubernetes.io/docs/user-guide/identifiers#names' + type: string + required: + - kind + - name + type: object + syncPeriod: + description: syncPeriod is equivalent to --horizontal-pod-autoscaler-sync-period; + the frequency with which the PHPA calculates replica counts and + scales in milliseconds. Default value 15000 milliseconds (15 seconds). + minimum: 1 + type: integer + tolerance: + description: tolerance is equivalent to --horizontal-pod-autoscaler-tolerance; + the minimum change (from 1.0) in the desired-to-actual metrics ratio + for the predictive horizontal pod autoscaler to consider scaling. + Default value 0.1. + minimum: 0 + type: number + required: + - maxReplicas + - models + - scaleTargetRef + type: object + status: + description: PredictiveHorizontalPodAutoscalerStatus defines the observed + state of PredictiveHorizontalPodAutoscaler + properties: + currentMetrics: + description: currentMetrics is the last read state of the metrics + used by this autoscaler. + items: + description: MetricStatus describes the last-read state of a single + metric. + properties: + containerResource: + description: container resource refers to a resource metric + (such as those specified in requests and limits) known to + Kubernetes describing a single container in each pod in the + current scale target (e.g. CPU or memory). Such metrics are + built in to Kubernetes, and have special scaling options on + top of those available to normal per-pod metrics using the + "pods" source. + properties: + container: + description: Container is the name of the container in the + pods of the scaling target + type: string + current: + description: current contains the current value for the + given metric + properties: + averageUtilization: + description: currentAverageUtilization is the current + value of the average of the resource metric across + all relevant pods, represented as a percentage of + the requested value of the resource for the pods. + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: averageValue is the current value of the + average of the metric across all relevant pods (as + a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + description: value is the current value of the metric + (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + name: + description: Name is the name of the resource in question. + type: string + required: + - container + - current + - name + type: object + external: + description: external refers to a global metric that is not + associated with any Kubernetes object. It allows autoscaling + based on information coming from components running outside + of cluster (for example length of queue in cloud messaging + service, or QPS from loadbalancer running outside of cluster). + properties: + current: + description: current contains the current value for the + given metric + properties: + averageUtilization: + description: currentAverageUtilization is the current + value of the average of the resource metric across + all relevant pods, represented as a percentage of + the requested value of the resource for the pods. + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: averageValue is the current value of the + average of the metric across all relevant pods (as + a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + description: value is the current value of the metric + (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + metric: + description: metric identifies the target metric by name + and selector + properties: + name: + description: name is the name of the given metric + type: string + selector: + description: selector is the string-encoded form of + a standard kubernetes label selector for the given + metric When set, it is passed as an additional parameter + to the metrics server for more specific metrics scoping. + When unset, just the metricName will be used to gather + metrics. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a + selector that contains values, a key, and an + operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If the + operator is Exists or DoesNotExist, the + values array must be empty. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". The + requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + required: + - current + - metric + type: object + object: + description: object refers to a metric describing a single kubernetes + object (for example, hits-per-second on an Ingress object). + properties: + current: + description: current contains the current value for the + given metric + properties: + averageUtilization: + description: currentAverageUtilization is the current + value of the average of the resource metric across + all relevant pods, represented as a percentage of + the requested value of the resource for the pods. + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: averageValue is the current value of the + average of the metric across all relevant pods (as + a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + description: value is the current value of the metric + (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + describedObject: + description: CrossVersionObjectReference contains enough + information to let you identify the referred resource. + properties: + apiVersion: + description: API version of the referent + type: string + kind: + description: 'Kind of the referent; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds"' + type: string + name: + description: 'Name of the referent; More info: http://kubernetes.io/docs/user-guide/identifiers#names' + type: string + required: + - kind + - name + type: object + metric: + description: metric identifies the target metric by name + and selector + properties: + name: + description: name is the name of the given metric + type: string + selector: + description: selector is the string-encoded form of + a standard kubernetes label selector for the given + metric When set, it is passed as an additional parameter + to the metrics server for more specific metrics scoping. + When unset, just the metricName will be used to gather + metrics. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a + selector that contains values, a key, and an + operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If the + operator is Exists or DoesNotExist, the + values array must be empty. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". The + requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + required: + - current + - describedObject + - metric + type: object + pods: + description: pods refers to a metric describing each pod in + the current scale target (for example, transactions-processed-per-second). The + values will be averaged together before being compared to + the target value. + properties: + current: + description: current contains the current value for the + given metric + properties: + averageUtilization: + description: currentAverageUtilization is the current + value of the average of the resource metric across + all relevant pods, represented as a percentage of + the requested value of the resource for the pods. + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: averageValue is the current value of the + average of the metric across all relevant pods (as + a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + description: value is the current value of the metric + (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + metric: + description: metric identifies the target metric by name + and selector + properties: + name: + description: name is the name of the given metric + type: string + selector: + description: selector is the string-encoded form of + a standard kubernetes label selector for the given + metric When set, it is passed as an additional parameter + to the metrics server for more specific metrics scoping. + When unset, just the metricName will be used to gather + metrics. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a + selector that contains values, a key, and an + operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If the + operator is Exists or DoesNotExist, the + values array must be empty. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". The + requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + required: + - current + - metric + type: object + resource: + description: resource refers to a resource metric (such as those + specified in requests and limits) known to Kubernetes describing + each pod in the current scale target (e.g. CPU or memory). + Such metrics are built in to Kubernetes, and have special + scaling options on top of those available to normal per-pod + metrics using the "pods" source. + properties: + current: + description: current contains the current value for the + given metric + properties: + averageUtilization: + description: currentAverageUtilization is the current + value of the average of the resource metric across + all relevant pods, represented as a percentage of + the requested value of the resource for the pods. + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: averageValue is the current value of the + average of the metric across all relevant pods (as + a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + description: value is the current value of the metric + (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + name: + description: Name is the name of the resource in question. + type: string + required: + - current + - name + type: object + type: + description: 'type is the type of metric source. It will be + one of "ContainerResource", "External", "Object", "Pods" or + "Resource", each corresponds to a matching field in the object. + Note: "ContainerResource" type is available on when the feature-gate + HPAContainerMetrics is enabled' + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + currentReplicas: + description: currentReplicas is current number of replicas of pods + managed by this autoscaler, as last seen by the autoscaler. + format: int32 + type: integer + desiredReplicas: + description: desiredReplicas is the desired number of replicas of + pods managed by this autoscaler, as last calculated by the autoscaler. + format: int32 + type: integer + lastScaleTime: + description: lastScaleTime is the last time the PredictiveHorizontalPodAutoscaler + scaled the number of pods, used by the autoscaler to control how + often the number of pods is changed. + format: date-time + type: string + reference: + description: reference is the resource being referenced and targeted + for scaling. + type: string + replicaHistory: + description: replicaHistory is a timestamped history of all the calculated + replica values, used for calculating downscale stabilization. + items: + description: TimestampedReplicas is a replica count paired with + the time that the replica count was created at. + properties: + replicas: + description: replicas is the replica count at the time. + format: int32 + type: integer + time: + description: time is the time that the replica count was created + at. + format: date-time + type: string + required: + - replicas + - time + type: object + type: array + required: + - desiredReplicas + - reference + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/helm/values.yaml b/helm/values.yaml new file mode 100644 index 0000000..0f259a0 --- /dev/null +++ b/helm/values.yaml @@ -0,0 +1 @@ +mode: cluster diff --git a/internal/algorithm/algorithm.go b/internal/algorithm/algorithm.go index b11009c..0260a87 100644 --- a/internal/algorithm/algorithm.go +++ b/internal/algorithm/algorithm.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. +Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,31 +17,75 @@ limitations under the License. package algorithm import ( - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook" + "bytes" + "fmt" + "os" + "os/exec" + "path" + "time" ) const ( entrypoint = "python" ) -// Runner defines an algorithm runner, allowing algorithms to be run -type Runner interface { - RunAlgorithmWithValue(algorithmPath string, value string, timeout int) (string, error) +type command = func(name string, arg ...string) *exec.Cmd + +func NewAlgorithmPython() *Python { + return &Python{ + Command: exec.Command, + Getwd: os.Getwd, + } } -// Run is an implementation of an algorithm runner that uses CPA executers to run shell commands -type Run struct { - Executer hook.Executer +// Python is an implementation of an algorithm runner that runs algorithms using Python commands +type Python struct { + Command command + Getwd func() (dir string, err error) } // RunAlgorithmWithValue runs an algorithm at the path provided, passing through the value provided -func (r *Run) RunAlgorithmWithValue(algorithmPath string, value string, timeout int) (string, error) { - return r.Executer.ExecuteWithValue(&hook.Definition{ - Type: "shell", - Timeout: timeout, - Shell: &hook.Shell{ - Entrypoint: entrypoint, - Command: []string{algorithmPath}, - }, - }, value) +func (r *Python) RunAlgorithmWithValue(algorithmPath string, value string, timeout int) (string, error) { + + wd, err := r.Getwd() + if err != nil { + return "", err + } + + cmd := r.Command(entrypoint, path.Join(wd, algorithmPath)) + + // Set up byte buffer to write values to stdin + inb := bytes.Buffer{} + // No need to catch error, doesn't produce error, instead it panics if buffer too large + inb.WriteString(value) + cmd.Stdin = &inb + + // Set up byte buffers to read stdout and stderr + var outb, errb bytes.Buffer + cmd.Stdout = &outb + cmd.Stderr = &errb + + // Start command + err = cmd.Start() + if err != nil { + return "", err + } + + // Set up channel to wait for command to finish + done := make(chan error) + go func() { done <- cmd.Wait() }() + + // Set up a timeout, after which if the command hasn't finished it will be stopped + timeoutListener := time.After(time.Duration(timeout) * time.Millisecond) + + select { + case <-timeoutListener: + cmd.Process.Kill() + return "", fmt.Errorf("entrypoint '%s', command '%s' timed out", entrypoint, algorithmPath) + case err = <-done: + if err != nil { + return "", fmt.Errorf("%v: %s", err, errb.String()) + } + } + return outb.String(), nil } diff --git a/internal/algorithm/algorithm_test.go b/internal/algorithm/algorithm_test.go index a9a9e03..036bf75 100644 --- a/internal/algorithm/algorithm_test.go +++ b/internal/algorithm/algorithm_test.go @@ -18,71 +18,215 @@ package algorithm_test import ( "errors" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "strings" "testing" + "time" "github.com/google/go-cmp/cmp" "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/algorithm" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/fake" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook" ) -func TestRunAlgorithmWithValue(t *testing.T) { - equateErrorMessage := cmp.Comparer(func(x, y error) bool { - if x == nil || y == nil { - return x == nil && y == nil - } - return x.Error() == y.Error() - }) +type command func(name string, arg ...string) *exec.Cmd + +type process func(t *testing.T) + +func TestShellProcess(t *testing.T) { + if os.Getenv("GO_TEST_PROCESS") != "1" { + return + } + + processName := strings.Split(os.Args[3], "=")[1] + process := processes[processName] + + if process == nil { + t.Errorf("Process %s not found", processName) + os.Exit(1) + } + + process(t) - var tests = []struct { - description string - expected string - expectedErr error - runner algorithm.Run - algorithmPath string - value string - timeout int - }{ + // Process should call os.Exit itself, if not exit with error + os.Exit(1) +} + +func fakeExecCommandAndStart(name string, process process) command { + processes[name] = process + return func(command string, args ...string) *exec.Cmd { + cs := []string{"-test.run=TestShellProcess", "--", fmt.Sprintf("-process=%s", name), command} + cs = append(cs, args...) + cmd := exec.Command(os.Args[0], cs...) + cmd.Env = []string{"GO_TEST_PROCESS=1"} + cmd.Start() + return cmd + } +} + +func fakeExecCommand(name string, process process) command { + processes[name] = process + return func(command string, args ...string) *exec.Cmd { + cs := []string{"-test.run=TestShellProcess", "--", fmt.Sprintf("-process=%s", name), command} + cs = append(cs, args...) + cmd := exec.Command(os.Args[0], cs...) + cmd.Env = []string{"GO_TEST_PROCESS=1"} + return cmd + } +} + +type test struct { + description string + expectedErr error + expected string + algorithmPath string + pipeValue string + timeout int + python *algorithm.Python +} + +var tests []test + +var processes map[string]process + +func TestMain(m *testing.M) { + wd, _ := os.Getwd() + processes = map[string]process{} + tests = []test{ { - "Fail to run shell command", - "", - errors.New("fail to run shell command"), - algorithm.Run{ - Executer: &fake.Execute{ - ExecuteWithValueReactor: func(definition *hook.Definition, value string) (string, error) { - return "", errors.New("fail to run shell command") - }, - }, + description: "Successful python command", + expectedErr: nil, + expected: "test std out", + algorithmPath: "test-algorithm.py", + pipeValue: "pipe value", + timeout: 100, + python: &algorithm.Python{ + Command: fakeExecCommand("success", func(t *testing.T) { + stdinb, err := ioutil.ReadAll(os.Stdin) + if err != nil { + fmt.Fprint(os.Stderr, err.Error()) + os.Exit(1) + } + + stdin := string(stdinb) + entrypoint := strings.TrimSpace(os.Args[4]) + algorithmPath := strings.TrimSpace(strings.Join(os.Args[5:len(os.Args)], " ")) + + expectedAlgorithmPath := path.Join(wd, "test-algorithm.py") + + // Check entrypoint is correct + if !cmp.Equal(entrypoint, "python") { + fmt.Fprintf(os.Stderr, "entrypoint mismatch (-want +got):\n%s", cmp.Diff("python", entrypoint)) + os.Exit(1) + } + + // Check command is correct + if !cmp.Equal(algorithmPath, expectedAlgorithmPath) { + fmt.Fprintf(os.Stderr, "algorithmPath mismatch (-want +got):\n%s", cmp.Diff(expectedAlgorithmPath, algorithmPath)) + os.Exit(1) + } + + // Check piped value in is correct + if !cmp.Equal(stdin, "pipe value") { + fmt.Fprintf(os.Stderr, "stdin mismatch (-want +got):\n%s", cmp.Diff("pipe value", stdin)) + os.Exit(1) + } + + fmt.Fprint(os.Stdout, "test std out") + os.Exit(0) + }), + Getwd: os.Getwd, }, - "test", - "test", - 10, }, { - "Successfully run shell command", - "Success!", - nil, - algorithm.Run{ - Executer: &fake.Execute{ - ExecuteWithValueReactor: func(definition *hook.Definition, value string) (string, error) { - return "Success!", nil - }, + description: "Failed python command", + expectedErr: errors.New("exit status 1: shell command failed"), + expected: "", + algorithmPath: "test-algorithm.py", + pipeValue: "pipe value", + timeout: 100, + python: &algorithm.Python{ + Command: fakeExecCommand("failed", func(t *testing.T) { + fmt.Fprint(os.Stderr, "shell command failed") + os.Exit(1) + }), + Getwd: os.Getwd, + }, + }, + { + description: "Failed python command timeout", + expectedErr: errors.New("entrypoint 'python', command 'test-algorithm.py' timed out"), + expected: "", + algorithmPath: "test-algorithm.py", + pipeValue: "pipe value", + timeout: 5, + python: &algorithm.Python{ + Command: fakeExecCommand("timeout", func(t *testing.T) { + fmt.Fprint(os.Stdout, "test std out") + time.Sleep(10 * time.Millisecond) + os.Exit(0) + }), + Getwd: os.Getwd, + }, + }, + { + description: "Failed python command fail to start", + expectedErr: errors.New("exec: already started"), + expected: "", + algorithmPath: "test-algorithm.py", + pipeValue: "pipe value", + timeout: 100, + python: &algorithm.Python{ + Command: fakeExecCommandAndStart("fail to start", func(t *testing.T) { + fmt.Fprint(os.Stdout, "test std out") + os.Exit(0) + }), + Getwd: os.Getwd, + }, + }, + { + description: "Fail to get working directory", + expectedErr: errors.New("fail to get working directory"), + expected: "", + algorithmPath: "test-algorithm.py", + pipeValue: "pipe value", + timeout: 100, + python: &algorithm.Python{ + Command: fakeExecCommandAndStart("fail to start", func(t *testing.T) { + fmt.Fprint(os.Stdout, "test std out") + os.Exit(0) + }), + Getwd: func() (dir string, err error) { + return "", errors.New("fail to get working directory") }, }, - "test", - "test", - 10, }, } + code := m.Run() + os.Exit(code) +} + +func TestPython_RunAlgorithmWithValue(t *testing.T) { + equateErrorMessage := cmp.Comparer(func(x, y error) bool { + if x == nil || y == nil { + return x == nil && y == nil + } + return x.Error() == y.Error() + }) + for _, test := range tests { t.Run(test.description, func(t *testing.T) { - result, err := test.runner.RunAlgorithmWithValue(test.algorithmPath, test.value, test.timeout) + result, err := test.python.RunAlgorithmWithValue(test.algorithmPath, test.pipeValue, test.timeout) if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { + t.Errorf(result) t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) return } - if !cmp.Equal(test.expected, result) { - t.Errorf("config mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) + + if !cmp.Equal(result, test.expected) { + t.Errorf("stdout mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) } }) } diff --git a/internal/config/config.go b/internal/config/config.go deleted file mode 100644 index a11a2c1..0000000 --- a/internal/config/config.go +++ /dev/null @@ -1,111 +0,0 @@ -/* -Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package config provides configuration options for the Predictive Horizontal Pod Autoscaler -package config - -import ( - "io" - - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook" - autoscalingv2 "k8s.io/api/autoscaling/v2beta2" - "k8s.io/apimachinery/pkg/util/yaml" -) - -const ( - // DecisionMaximum means use the highest predicted value from the models - DecisionMaximum = "maximum" - // DecisionMinimum means use the lowest predicted value from the models - DecisionMinimum = "minimum" - // DecisionMean means use the mean average of predicted values - DecisionMean = "mean" - // DecisionMedian means use the median average of predicted values - DecisionMedian = "median" -) - -const ( - defaultTolerance = float64(0.1) - // 5 minute CPU initialization period - defaultCPUInitializationPeriod = 300 - // 30 second initial readiness delay - defaultInitialReadinessDelay = 30 -) - -// Config holds the configuration of the Predictive element of the PHPA -type Config struct { - Models []*Model `json:"models"` - Metrics []autoscalingv2.MetricSpec `json:"metrics"` - DecisionType string `json:"decisionType"` - DBPath string `json:"dbPath"` - MigrationPath string `json:"migrationPath"` - Tolerance float64 `json:"tolerance"` - CPUInitializationPeriod int `json:"cpuInitializationPeriod"` - InitialReadinessDelay int `json:"initialReadinessDelay"` -} - -// Model represents a prediction model to use, e.g. a linear regression -type Model struct { - Type string `json:"type"` - Name string `json:"name"` - PerInterval int `json:"perInterval"` - CalculationTimeout *int `json:"calculationTimeout"` - Linear *Linear `json:"linear"` - HoltWinters *HoltWinters `json:"holtWinters"` -} - -// HoltWinters represents a holt-winters exponential smoothing prediction model configuration -type HoltWinters struct { - Alpha *float64 `json:"alpha"` - Beta *float64 `json:"beta"` - Gamma *float64 `json:"gamma"` - Trend string `json:"trend"` - Seasonal string `json:"seasonal"` - SeasonalPeriods int `json:"seasonalPeriods"` - StoredSeasons int `json:"storedSeasons"` - DampedTrend *bool `json:"dampedTrend"` - InitializationMethod *string `json:"initializationMethod"` - InitialLevel *float64 `json:"initialLevel"` - InitialTrend *float64 `json:"initialTrend"` - InitialSeasonal *float64 `json:"initialSeasonal"` - RuntimeTuningFetchHook *hook.Definition `json:"runtimeTuningFetchHook"` -} - -// Linear represents a linear regression prediction model configuration -type Linear struct { - StoredValues int `json:"storedValues"` - LookAhead int `json:"lookAhead"` -} - -// LoadConfig takes in the predictive config as a byte array and uses it to build the config, overriding default values -func LoadConfig(configEnv io.Reader) (*Config, error) { - config := newDefaultConfig() - err := yaml.NewYAMLOrJSONDecoder(configEnv, 10).Decode(&config) - if err != nil { - return nil, err - } - return config, nil -} - -func newDefaultConfig() *Config { - return &Config{ - DecisionType: "maximum", - DBPath: "/store/predictive-horizontal-pod-autoscaler.db", - MigrationPath: "/app/sql", - Tolerance: defaultTolerance, - CPUInitializationPeriod: defaultCPUInitializationPeriod, - InitialReadinessDelay: defaultInitialReadinessDelay, - } -} diff --git a/internal/config/config_test.go b/internal/config/config_test.go deleted file mode 100644 index efaee32..0000000 --- a/internal/config/config_test.go +++ /dev/null @@ -1,205 +0,0 @@ -/* -Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package config_test - -import ( - "errors" - "io" - "strings" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/config" - autoscalingv2 "k8s.io/api/autoscaling/v2beta2" - v1 "k8s.io/api/core/v1" -) - -const ( - defaultTolerance = float64(0.1) - defaultCPUInitializationPeriod = 300 - defaultInitialReadinessDelay = 30 -) - -func TestLoadConfig(t *testing.T) { - equateErrorMessage := cmp.Comparer(func(x, y error) bool { - if x == nil || y == nil { - return x == nil && y == nil - } - return x.Error() == y.Error() - }) - - var tests = []struct { - description string - expected *config.Config - expectedErr error - configBytes io.Reader - }{ - { - "Invalid YAML", - nil, - errors.New(`error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type config.Config`), - strings.NewReader("invalid-test"), - }, - { - "Success default config", - &config.Config{ - DecisionType: "maximum", - DBPath: "/store/predictive-horizontal-pod-autoscaler.db", - MigrationPath: "/app/sql", - Tolerance: defaultTolerance, - CPUInitializationPeriod: defaultCPUInitializationPeriod, - InitialReadinessDelay: defaultInitialReadinessDelay, - }, - nil, - strings.NewReader("valid: true"), - }, - { - "Success custom config, YAML", - &config.Config{ - DecisionType: "testDecision", - DBPath: "testPath", - MigrationPath: "testMigrationPath", - Tolerance: 0.5, - CPUInitializationPeriod: 25, - InitialReadinessDelay: 321, - Models: []*config.Model{ - { - Type: "test", - Name: "testPrediction", - PerInterval: 1, - Linear: &config.Linear{ - LookAhead: 50, - StoredValues: 10, - }, - }, - }, - Metrics: []autoscalingv2.MetricSpec{ - { - Type: autoscalingv2.ResourceMetricSourceType, - Resource: &autoscalingv2.ResourceMetricSource{ - Name: v1.ResourceCPU, - Target: autoscalingv2.MetricTarget{ - Type: autoscalingv2.UtilizationMetricType, - AverageUtilization: func() *int32 { i := int32(50); return &i }(), - }, - }, - }, - }, - }, - nil, - strings.NewReader(strings.Replace(` - decisionType: testDecision - dbPath: testPath - migrationPath: testMigrationPath - tolerance: 0.5 - cpuInitializationPeriod: 25 - initialReadinessDelay: 321 - models: - - type: test - name: testPrediction - perInterval: 1 - linear: - lookAhead: 50 - storedValues: 10 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 50 - `, "\t", "", -1)), - }, - { - "Success custom config, JSON", - &config.Config{ - DecisionType: "testDecision", - DBPath: "testPath", - MigrationPath: "testMigrationPath", - Tolerance: defaultTolerance, - CPUInitializationPeriod: defaultCPUInitializationPeriod, - InitialReadinessDelay: defaultInitialReadinessDelay, - Models: []*config.Model{ - { - Type: "test", - Name: "testPrediction", - PerInterval: 1, - Linear: &config.Linear{ - LookAhead: 50, - StoredValues: 10, - }, - }, - }, - Metrics: []autoscalingv2.MetricSpec{ - { - Type: autoscalingv2.ResourceMetricSourceType, - Resource: &autoscalingv2.ResourceMetricSource{ - Name: v1.ResourceCPU, - Target: autoscalingv2.MetricTarget{ - Type: autoscalingv2.UtilizationMetricType, - AverageUtilization: func() *int32 { i := int32(50); return &i }(), - }, - }, - }, - }, - }, - nil, - strings.NewReader(strings.Replace(` - { - "decisionType": "testDecision", - "dbPath": "testPath", - "migrationPath": "testMigrationPath", - "models": [ - { - "type": "test", - "name": "testPrediction", - "perInterval": 1, - "linear": { - "lookAhead": 50, - "storedValues": 10 - } - } - ], - "metrics": [ - { - "type": "Resource", - "resource": { - "name": "cpu", - "target": { - "type": "Utilization", - "averageUtilization": 50 - } - } - } - ] - } - `, "\t", "", -1)), - }, - } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - result, err := config.LoadConfig(test.configBytes) - if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { - t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) - return - } - if !cmp.Equal(test.expected, result) { - t.Errorf("config mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) - } - }) - } -} diff --git a/internal/controllers/predictivehorizontalpodautoscaler_controller.go b/internal/controllers/predictivehorizontalpodautoscaler_controller.go new file mode 100644 index 0000000..b62de4a --- /dev/null +++ b/internal/controllers/predictivehorizontalpodautoscaler_controller.go @@ -0,0 +1,646 @@ +/* +Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "sort" + "time" + + autoscalingv1 "k8s.io/api/autoscaling/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2beta2" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/scale" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/jthomperoo/k8shorizmetrics" + jamiethompsonmev1alpha1 "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/api/v1alpha1" + "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/prediction" +) + +const ( + defaultMinReplicas = 1 + defaultSyncPeriod = 15 * time.Second + defaultErrorRetryPeriod = 10 * time.Second + defaultDownscaleStabilization = 300 + defaultCPUInitializationPeriod = 30 + defaultInitialReadinessDelay = 30 + defaultTolerance = 0.1 + defaultDecisionType = jamiethompsonmev1alpha1.DecisionMaximum + defaultPerSyncPeriod = 1 +) + +const ( + configMapDataKey = "data" +) + +// PredictiveHorizontalPodAutoscalerReconciler reconciles a PredictiveHorizontalPodAutoscaler object +type PredictiveHorizontalPodAutoscalerReconciler struct { + client.Client + ScaleClient scale.ScalesGetter + Scheme *runtime.Scheme + Gatherer k8shorizmetrics.Gatherer + Evaluator k8shorizmetrics.Evaluator + Predicter prediction.Predicter +} + +//+kubebuilder:rbac:groups=jamiethompson.me,resources=predictivehorizontalpodautoscalers,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=jamiethompson.me,resources=predictivehorizontalpodautoscalers/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=jamiethompson.me,resources=predictivehorizontalpodautoscalers/finalizers,verbs=update +//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list +//+kubebuilder:rbac:groups=core,resources=replicationcontrollers/scale,verbs=get;update;patch +//+kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=apps,resources=deployments/scale;replicaset/scale;statefulset/scale,verbs=get;update;patch +//+kubebuilder:rbac:groups=metrics.k8s.io,resources=*,verbs=get;list +//+kubebuilder:rbac:groups=custom.metrics.k8s.io,resources=*,verbs=get;list +//+kubebuilder:rbac:groups=external.metrics.k8s.io,resources=*,verbs=get;list + +func (r *PredictiveHorizontalPodAutoscalerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + instance := &jamiethompsonmev1alpha1.PredictiveHorizontalPodAutoscaler{} + err := r.Client.Get(ctx, req.NamespacedName, instance) + if err != nil { + if k8serrors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. + // Return and don't requeue + return reconcile.Result{}, nil + } + + logger.Error(err, "failed to get PredictiveHorizontalPodAutoscaler") + return reconcile.Result{RequeueAfter: defaultErrorRetryPeriod}, err + } + + err = r.validate(instance) + if err != nil { + logger.Error(err, "invalid PredictiveHorizontalPodAutoscaler, disabling PHPA until changed to be valid") + // We stop processing here without requeueing since the PHPA is invalid, if changes are made to the spec that + // make it valid it will be reconciled again and the validation checked + return reconcile.Result{}, nil + } + + err = r.preScaleStatusCheck(ctx, instance) + if err != nil { + logger.Error(err, "failed pre scale status check", "scaleTargetRef", instance.Spec.ScaleTargetRef) + return reconcile.Result{RequeueAfter: defaultErrorRetryPeriod}, err + } + + configMap, phpaData, err := r.getPHPAConfigMapAndData(ctx, instance) + if err != nil { + logger.Error(err, "failed to get PHPA config map and data", "scaleTargetRef", instance.Spec.ScaleTargetRef) + return reconcile.Result{RequeueAfter: defaultErrorRetryPeriod}, err + } + + syncPeriod := defaultSyncPeriod + if instance.Spec.SyncPeriod != nil { + syncPeriod = time.Duration(*instance.Spec.SyncPeriod) * time.Millisecond + } + + now := time.Now().UTC() + + // Check the last scale of the PHPA, make sure we're not scaling too early + lastScaleTime := instance.Status.LastScaleTime + if lastScaleTime != nil && now.Add(-syncPeriod).Before(lastScaleTime.Time) { + timeUntilReconcile := instance.Status.LastScaleTime.Time.Add(syncPeriod).Sub(now) + logger.V(1).Info("Resource already scaled, queueing up reconcile for the next sync period", + "ScaleTargetRef", instance.Spec.ScaleTargetRef, + "syncPeriod", syncPeriod, + "timeUntilReconcile", timeUntilReconcile.Seconds()) + return reconcile.Result{RequeueAfter: timeUntilReconcile}, nil + } + + scale, err := r.getScaleSubresource(ctx, instance) + if err != nil { + logger.Error(err, "failed to get scale subresource", "ScaleTargetRef", instance.Spec.ScaleTargetRef) + return reconcile.Result{RequeueAfter: defaultErrorRetryPeriod}, err + } + + calculatedReplicas, err := r.calculateReplicas(instance, scale) + if err != nil { + logger.Error(err, "failed to calculate replicas based on metrics", + "ScaleTargetRef", instance.Spec.ScaleTargetRef, + "currentReplicas", scale.Spec.Replicas) + return reconcile.Result{RequeueAfter: defaultErrorRetryPeriod}, err + } + + // This function doesn't return any errors, since if it fails to process a model it will skip and continue + // processing without that model's results + predictedReplicas, phpaData := r.processModels(ctx, instance, phpaData, now, scale.Spec.Replicas, + calculatedReplicas) + + err = r.updateConfigMapData(ctx, configMap, phpaData) + if err != nil { + logger.Error(err, "failed to update PHPA configmap", + "ScaleTargetRef", instance.Spec.ScaleTargetRef) + return reconcile.Result{RequeueAfter: defaultErrorRetryPeriod}, err + } + + targetReplicas, downscaleStabilizationHistory, err := r.decideTargetReplicas(instance, predictedReplicas, now) + if err != nil { + logger.Error(err, "failed to decide targer replicas", + "ScaleTargetRef", instance.Spec.ScaleTargetRef, + "currentReplicas", scale.Spec.Replicas, + "predictedReplicas", predictedReplicas, + ) + return reconcile.Result{RequeueAfter: defaultErrorRetryPeriod}, err + } + + // Only scale if the current replicas is different than the target + if scale.Spec.Replicas != targetReplicas { + err = r.updateScaleResource(ctx, instance, scale, targetReplicas) + if err != nil { + logger.Error(err, "failed to update scale resource", + "ScaleTargetRef", instance.Spec.ScaleTargetRef, + "currentReplicas", scale.Spec.Replicas, + "targetReplicas", targetReplicas) + return reconcile.Result{RequeueAfter: defaultErrorRetryPeriod}, err + } + } + + instance.Status.LastScaleTime = &metav1.Time{Time: now} + instance.Status.DesiredReplicas = targetReplicas + instance.Status.CurrentReplicas = scale.Spec.Replicas + instance.Status.ReplicaHistory = downscaleStabilizationHistory + err = r.Client.Status().Update(ctx, instance) + if err != nil { + logger.Error(err, "failed to update status of resource", + "ScaleTargetRef", instance.Spec.ScaleTargetRef, + "currentReplicas", scale.Spec.Replicas, + "targetReplicas", targetReplicas, + "scaleTime", now) + return reconcile.Result{RequeueAfter: defaultErrorRetryPeriod}, err + } + + logger.V(0).Info("Scaled resource", + "ScaleTargetRef", instance.Spec.ScaleTargetRef, + "currentReplicas", scale.Spec.Replicas, + "targetReplicas", targetReplicas) + + return reconcile.Result{RequeueAfter: syncPeriod}, nil + +} + +// updateScaleResource updates the replica count on the scale subresource +func (r *PredictiveHorizontalPodAutoscalerReconciler) updateScaleResource( + ctx context.Context, instance *jamiethompsonmev1alpha1.PredictiveHorizontalPodAutoscaler, + scale *autoscalingv1.Scale, targetReplicas int32) error { + scale.Spec.Replicas = targetReplicas + + targetGR := schema.GroupResource{ + Group: scale.GroupVersionKind().Group, + Resource: scale.GroupVersionKind().Kind, + } + + _, err := r.ScaleClient.Scales(instance.Namespace).Update(ctx, targetGR, scale, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("failed to scale resource: %w", err) + } + + return nil +} + +// updateConfigMapData updates the PHPA's configmap and the data it holds +func (r *PredictiveHorizontalPodAutoscalerReconciler) updateConfigMapData(ctx context.Context, configMap *corev1.ConfigMap, + phpaData *jamiethompsonmev1alpha1.PredictiveHorizontalPodAutoscalerData) error { + data, err := json.Marshal(phpaData) + if err != nil { + // Should not occur, panic + panic(err) + } + + configMap.Data = map[string]string{ + configMapDataKey: string(data), + } + + err = r.Client.Update(ctx, configMap) + if err != nil { + return fmt.Errorf("failed to update config map data: %w", err) + } + + return nil +} + +// decideTargetReplicas uses the list of predicted replicas (from the calculated HPA value and the model predictions) +// and returns a single value to use for scaling. This accounts for both downscale stabilization and the decisionType +// scaling strategy. +func (r *PredictiveHorizontalPodAutoscalerReconciler) decideTargetReplicas( + instance *jamiethompsonmev1alpha1.PredictiveHorizontalPodAutoscaler, predictedReplicas []int32, + now time.Time) (int32, []jamiethompsonmev1alpha1.TimestampedReplicas, error) { + + minReplicas := int32(defaultMinReplicas) + if instance.Spec.MinReplicas != nil { + minReplicas = *instance.Spec.MinReplicas + } + + decisionType := defaultDecisionType + if instance.Spec.DecisionType != nil { + decisionType = *instance.Spec.DecisionType + } + + downscaleStabilization := defaultDownscaleStabilization + if instance.Spec.DownscaleStabilization != nil { + downscaleStabilization = *instance.Spec.DownscaleStabilization + } + + // Sort in ascending order + sort.Slice(predictedReplicas, func(i, j int) bool { return predictedReplicas[i] < predictedReplicas[j] }) + + // Decide which replica count to use based on decision type + var targetReplicas int32 + switch decisionType { + case jamiethompsonmev1alpha1.DecisionMaximum: + max := int32(0) + for i, predictedReplica := range predictedReplicas { + if i == 0 || predictedReplica > max { + max = predictedReplica + } + } + targetReplicas = max + case jamiethompsonmev1alpha1.DecisionMinimum: + min := int32(0) + for i, predictedReplica := range predictedReplicas { + if i == 0 || predictedReplica < min { + min = predictedReplica + } + } + targetReplicas = min + case jamiethompsonmev1alpha1.DecisionMean: + total := int32(0) + for _, predictedReplica := range predictedReplicas { + total += predictedReplica + } + targetReplicas = int32(float64(int(total) / len(predictedReplicas))) + case jamiethompsonmev1alpha1.DecisionMedian: + halfIndex := len(predictedReplicas) / 2 + if len(predictedReplicas)%2 == 0 { + // Even + targetReplicas = (predictedReplicas[halfIndex-1] + predictedReplicas[halfIndex]) / 2 + } else { + // Odd + targetReplicas = predictedReplicas[halfIndex] + } + default: + return 0, nil, fmt.Errorf("unknown decision type '%s'", decisionType) + } + + if targetReplicas < minReplicas { + targetReplicas = minReplicas + } + + if targetReplicas > instance.Spec.MaxReplicas { + targetReplicas = instance.Spec.MaxReplicas + } + + downscaleStabilizationHistory := instance.Status.ReplicaHistory + + // Prune old evaluations + // Cutoff is current time - stabilization window + cutoff := &metav1.Time{Time: now.Add(time.Duration(-downscaleStabilization) * time.Second)} + // Loop backwards over stabilization evaluations to prune old ones + // Backwards loop to allow values to be removed mid-loop without breaking it + for i := len(downscaleStabilizationHistory) - 1; i >= 0; i-- { + timestampedReplica := downscaleStabilizationHistory[i] + if timestampedReplica.Time.Before(cutoff) { + downscaleStabilizationHistory = append(downscaleStabilizationHistory[:i], downscaleStabilizationHistory[i+1:]...) + } + } + + downscaleStabilizationHistory = append(downscaleStabilizationHistory, jamiethompsonmev1alpha1.TimestampedReplicas{ + Time: &metav1.Time{Time: now}, + Replicas: targetReplicas, + }) + + for _, timestampedReplica := range downscaleStabilizationHistory { + if timestampedReplica.Replicas > targetReplicas { + targetReplicas = timestampedReplica.Replicas + } + } + + return targetReplicas, downscaleStabilizationHistory, nil +} + +// processModels processes every model provided in the spec, it does not return any errors and will instead simply +// log if a model has failed to be processed, allowing the other models/the HPA calculated replicas to be used instead +func (r *PredictiveHorizontalPodAutoscalerReconciler) processModels(ctx context.Context, + instance *jamiethompsonmev1alpha1.PredictiveHorizontalPodAutoscaler, + phpaData *jamiethompsonmev1alpha1.PredictiveHorizontalPodAutoscalerData, now time.Time, currentReplicas int32, + calculatedReplicas int32) ([]int32, *jamiethompsonmev1alpha1.PredictiveHorizontalPodAutoscalerData) { + + logger := log.FromContext(ctx) + + scaleTargetRef := instance.Spec.ScaleTargetRef + + // Set up a slice with the calculated replicas as the first prediction + predictedReplicas := []int32{calculatedReplicas} + + // Add the calculated replicas to a list of past replicas + for _, model := range instance.Spec.Models { + logger.V(2).Info("Processing model to determine replica count", + "ScaleTargetRef", scaleTargetRef, + "model", model.Name) + + perSyncPeriod := defaultPerSyncPeriod + if model.PerSyncPeriod != nil { + perSyncPeriod = *model.PerSyncPeriod + } + + modelHistory, exists := phpaData.ModelHistories[model.Name] + if !exists || modelHistory.Type != model.Type { + // Create new if model doesn't exist or has a type mismatch + modelHistory = jamiethompsonmev1alpha1.ModelHistory{ + Type: model.Type, + SyncPeriodsPassed: 1, + ReplicaHistroy: []jamiethompsonmev1alpha1.TimestampedReplicas{}, + } + } + + shouldRunOnThisSyncPeriod := modelHistory.SyncPeriodsPassed >= perSyncPeriod + + modelHistory.ReplicaHistroy = append(modelHistory.ReplicaHistroy, jamiethompsonmev1alpha1.TimestampedReplicas{ + Time: &metav1.Time{ + Time: now, + }, + Replicas: calculatedReplicas, + }) + + if shouldRunOnThisSyncPeriod { + logger.V(1).Info("Using model to calculate predicted target replicas", + "ScaleTargetRef", scaleTargetRef, + "model", model.Name) + replicas, err := r.Predicter.GetPrediction(&model, modelHistory.ReplicaHistroy) + if err != nil { + // Skip this model, errored out + logger.Error(err, "failed to get predicted replica count", + "ScaleTargetRef", scaleTargetRef, + "currentReplicas", currentReplicas, + "targetReplicas", calculatedReplicas) + continue + } + predictedReplicas = append(predictedReplicas, replicas) + modelHistory.SyncPeriodsPassed = 1 + } else { + logger.V(1).Info("Skipping model for this sync period, should not run on this sync period", + "ScaleTargetRef", scaleTargetRef, + "syncPeriodsPassed", modelHistory.SyncPeriodsPassed, + "perSyncPeriod", perSyncPeriod, + "model", model.Name) + modelHistory.SyncPeriodsPassed += 1 + } + + prunedHistory, err := r.Predicter.PruneHistory(&model, modelHistory.ReplicaHistroy) + if err != nil { + // Skip this model, errored out + logger.Error(err, "failed to prune replica history", + "ScaleTargetRef", scaleTargetRef) + continue + } + + modelHistory.ReplicaHistroy = prunedHistory + phpaData.ModelHistories[model.Name] = modelHistory + } + + // Delete any model data that exists without a corresponding model spec + for modelName := range phpaData.ModelHistories { + exists := false + for _, model := range instance.Spec.Models { + if modelName == model.Name { + exists = true + break + } + } + + if !exists { + delete(phpaData.ModelHistories, modelName) + } + } + + return predictedReplicas, phpaData +} + +// calculateReplicas does the HPA processing part of the autoscaling based on the metrics provided in the spec, +// returns the calculated value (the value the HPA would calculate based on these metrics). +func (r *PredictiveHorizontalPodAutoscalerReconciler) calculateReplicas( + instance *jamiethompsonmev1alpha1.PredictiveHorizontalPodAutoscaler, scale *autoscalingv1.Scale) (int32, error) { + cpuInitializationPeriod := defaultCPUInitializationPeriod + if instance.Spec.CPUInitializationPeriod != nil { + cpuInitializationPeriod = *instance.Spec.CPUInitializationPeriod + } + + initialReadinessDelay := defaultInitialReadinessDelay + if instance.Spec.InitialReadinessDelay != nil { + initialReadinessDelay = *instance.Spec.InitialReadinessDelay + } + + tolerance := defaultTolerance + if instance.Spec.Tolerance != nil { + tolerance = *instance.Spec.Tolerance + } + + selector, err := labels.Parse(scale.Status.Selector) + if err != nil { + return 0, fmt.Errorf("failed to parse pod selector from scale subresource selector: %w", err) + } + + // Gather K8s metrics using the spec + metrics, err := r.Gatherer.GatherWithOptions(instance.Spec.Metrics, scale.Namespace, selector, + time.Duration(cpuInitializationPeriod)*time.Second, time.Duration(initialReadinessDelay)*time.Second) + if err != nil { + return 0, fmt.Errorf("failed to gather metrics using provided metric specs: %w", err) + } + + // Calculate the targetReplicas using these metrics + currentReplicas := scale.Spec.Replicas + calculatedReplicas, err := r.Evaluator.EvaluateWithOptions(metrics, currentReplicas, tolerance) + if err != nil { + return 0, fmt.Errorf("failed to evaluate metrics and calculate target replica count: %w", err) + } + + return calculatedReplicas, nil +} + +// getScaleSubresource gets the scale subresource for the resource being targeted +func (r *PredictiveHorizontalPodAutoscalerReconciler) getScaleSubresource(ctx context.Context, + instance *jamiethompsonmev1alpha1.PredictiveHorizontalPodAutoscaler) (*autoscalingv1.Scale, error) { + scaleTargetRef := instance.Spec.ScaleTargetRef + + // Get targeted scale subresource + resourceGV, err := schema.ParseGroupVersion(scaleTargetRef.APIVersion) + if err != nil { + return nil, fmt.Errorf("failed to parse group version of target resource: %w", err) + } + + targetGR := schema.GroupResource{ + Group: resourceGV.Group, + Resource: scaleTargetRef.Kind, + } + + scale, err := r.ScaleClient.Scales(instance.Namespace).Get(ctx, targetGR, scaleTargetRef.Name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("ailed to get the scale subresource of the target resource: %w", err) + } + + return scale, nil +} + +// getPHPAConfigMapAndData returns the config map and parsed data for the PHPA +func (r *PredictiveHorizontalPodAutoscalerReconciler) getPHPAConfigMapAndData(ctx context.Context, + instance *jamiethompsonmev1alpha1.PredictiveHorizontalPodAutoscaler) (*corev1.ConfigMap, *jamiethompsonmev1alpha1.PredictiveHorizontalPodAutoscalerData, error) { + + logger := log.FromContext(ctx) + + scaleTargetRef := instance.Spec.ScaleTargetRef + + // Check if configmap exists, if not create a blank one + phpaData := &jamiethompsonmev1alpha1.PredictiveHorizontalPodAutoscalerData{ + ModelHistories: map[string]jamiethompsonmev1alpha1.ModelHistory{}, + } + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("predictive-horizontal-pod-autoscaler-%s-data", instance.Name), + Namespace: instance.Namespace, + }, + } + configMap.SetOwnerReferences([]metav1.OwnerReference{{ + APIVersion: instance.APIVersion, + Kind: instance.Kind, + Name: instance.Name, + UID: instance.UID, + }}) + + err := r.Client.Get(context.Background(), types.NamespacedName{Name: configMap.GetName(), Namespace: configMap.GetNamespace()}, configMap) + if err != nil { + if !k8serrors.IsNotFound(err) { + return nil, nil, fmt.Errorf("failed to get PHPA configmap: %w", err) + } + + logger.V(1).Info("No configmap found for PHPA, creating a new one", + "ScaleTargetRef", scaleTargetRef) + + data, err := json.Marshal(phpaData) + if err != nil { + // Should not occur, panic + panic(err) + } + + configMap.Data = map[string]string{ + configMapDataKey: string(data), + } + + err = r.Client.Create(ctx, configMap) + if err != nil { + return nil, nil, fmt.Errorf("failed to create PHPA configmap: %w", err) + } + } + + err = json.Unmarshal([]byte(configMap.Data[configMapDataKey]), phpaData) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse PHPA data: %w", err) + } + + return configMap, phpaData, nil +} + +// preScaleStatusCheck makes sure that the PHPAs status fields are correct before scaling, e.g. the reference field +// is set +func (r *PredictiveHorizontalPodAutoscalerReconciler) preScaleStatusCheck(ctx context.Context, + instance *jamiethompsonmev1alpha1.PredictiveHorizontalPodAutoscaler) error { + + scaleTargetRef := instance.Spec.ScaleTargetRef + + reference := fmt.Sprintf("%s/%s", scaleTargetRef.Kind, scaleTargetRef.Name) + if instance.Status.Reference != reference { + instance.Status.Reference = reference + err := r.Client.Status().Update(ctx, instance) + if err != nil { + return fmt.Errorf("failed to update status of resource: %w", err) + } + } + + return nil +} + +// validate performs validation on the PHPA, will return an error if the PHPA is not valid +func (r *PredictiveHorizontalPodAutoscalerReconciler) validate(instance *jamiethompsonmev1alpha1.PredictiveHorizontalPodAutoscaler) error { + spec := instance.Spec + + if spec.MinReplicas != nil && spec.MaxReplicas < *spec.MinReplicas { + return fmt.Errorf("spec.maxReplicas (%d) cannot be less than spec.minReplicas (%d)", + spec.MaxReplicas, *spec.MinReplicas) + } + + if spec.MinReplicas != nil && *spec.MinReplicas == 0 { + // We need to check that if they set min replicas to zero they have at least 1 object or external metric + // configured + valid := false + for _, metric := range spec.Metrics { + if metric.Type == autoscalingv2.ObjectMetricSourceType || metric.Type == autoscalingv2.ExternalMetricSourceType { + valid = true + break + } + } + if !valid { + return errors.New("spec.minReplicas can only be 0 if you have at least 1 object or external metric configured") + } + } + + for _, model := range spec.Models { + if model.Type == jamiethompsonmev1alpha1.TypeHoltWinters { + hw := model.HoltWinters + if hw == nil { + return fmt.Errorf("invalid model '%s', type is '%s' but no Holt Winters configuration provided", + model.Name, model.Type) + } + + if hw.RuntimeTuningFetchHook != nil { + hook := hw.RuntimeTuningFetchHook + if hook.Type == jamiethompsonmev1alpha1.HookTypeHTTP && hook.HTTP == nil { + return fmt.Errorf("invalid model '%s', runtimeTuningFetchHook is type '%s' but no HTTP hook configuration provided", + model.Name, hook.Type) + } + } + } + + if model.Type == jamiethompsonmev1alpha1.TypeLinear && model.Linear == nil { + return fmt.Errorf("invalid model '%s', type is '%s' but no Linear Regression configuration provided", + model.Name, model.Type) + } + } + + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *PredictiveHorizontalPodAutoscalerReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&jamiethompsonmev1alpha1.PredictiveHorizontalPodAutoscaler{}). + Owns(&corev1.ConfigMap{}). + Complete(r) +} diff --git a/internal/evaluate/evaluate.go b/internal/evaluate/evaluate.go deleted file mode 100644 index efad4c2..0000000 --- a/internal/evaluate/evaluate.go +++ /dev/null @@ -1,175 +0,0 @@ -/* -Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package evaluate - -import ( - "database/sql" - "fmt" - "sort" - - cpaconfig "github.com/jthomperoo/custom-pod-autoscaler/v2/config" - cpaevaluate "github.com/jthomperoo/custom-pod-autoscaler/v2/evaluate" - "github.com/jthomperoo/k8shorizmetrics/metrics" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/config" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/prediction" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/stored" -) - -type HPAEvaluator interface { - Evaluate(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) -} - -// PredictiveEvaluate provides a way to make predictive evaluations -type PredictiveEvaluate struct { - HPAEvaluator HPAEvaluator - Store stored.Storer - Predicters []prediction.Predicter -} - -// GetEvaluation takes a predictive horizontal pod autoscaler configuration and gathered metrics piped in through stdin and -// evaluates using these, returning a value of how many replicas a resource should have -func (p *PredictiveEvaluate) GetEvaluation(predictiveConfig *config.Config, metrics []*metrics.Metric, currentReplicas int32, runType string) (*cpaevaluate.Evaluation, error) { - targetReplicas, err := p.HPAEvaluator.Evaluate(metrics, currentReplicas) - if err != nil { - return nil, err - } - - predictions := []int32{targetReplicas} - - // Set up predicter with available models - predicter := prediction.ModelPredict{ - Predicters: p.Predicters, - } - - for _, model := range predictiveConfig.Models { - // Get model from local storage, if it doesn't exist create it - dbModel, err := p.Store.GetModel(model.Name) - if err == sql.ErrNoRows { - err = p.Store.UpdateModel(model.Name, 1) - if err != nil { - return nil, err - } - dbModel, err = p.Store.GetModel(model.Name) - if err != nil { - return nil, err - } - } else if err != nil { - return nil, err - } - - isRunInterval := dbModel.IntervalsPassed >= model.PerInterval - isRunType := runType == cpaconfig.ScalerRunType - - // If not enough intervals have passed, increment the number of intervals passed, - // prediction will still be calculated, but current value will not be inserted/values - // expired - if !isRunInterval && isRunType { - err = p.Store.UpdateModel(model.Name, dbModel.IntervalsPassed+1) - if err != nil { - return nil, err - } - } - - // Only add new values if requested during scale and on the required interval - if isRunInterval && isRunType { - // Reset number of passed intervals - err = p.Store.UpdateModel(model.Name, 1) - if err != nil { - return nil, err - } - // Add new value - err = p.Store.AddEvaluation(model.Name, &cpaevaluate.Evaluation{ - TargetReplicas: targetReplicas, - }) - if err != nil { - return nil, err - } - } - - // Get saved values - saved, err := p.Store.GetEvaluation(model.Name) - if err != nil { - return nil, err - } - - // Get prediction, add it to the slice of predictions - prediction, err := predicter.GetPrediction(model, saved) - if err != nil { - return nil, err - } - predictions = append(predictions, prediction) - - // Only remove values if requested during scale and on the required interval - if isRunInterval && isRunType { - valuesToRemove, err := predicter.GetIDsToRemove(model, saved) - if err != nil { - return nil, err - } - for _, val := range valuesToRemove { - err := p.Store.RemoveEvaluation(val) - if err != nil { - return nil, err - } - } - } - } - - // Sort predictions - sort.Slice(predictions, func(i, j int) bool { return predictions[i] < predictions[j] }) - - // Decide which prediction to use - var targetPrediction int32 - switch predictiveConfig.DecisionType { - case config.DecisionMaximum: - max := int32(0) - for i, prediction := range predictions { - if i == 0 || prediction > max { - max = prediction - } - } - targetPrediction = max - case config.DecisionMinimum: - min := int32(0) - for i, prediction := range predictions { - if i == 0 || prediction < min { - min = prediction - } - } - targetPrediction = min - case config.DecisionMean: - total := int32(0) - for _, prediction := range predictions { - total += prediction - } - targetPrediction = int32(float64(int(total) / len(predictions))) - case config.DecisionMedian: - halfIndex := len(predictions) / 2 - if len(predictions)%2 == 0 { - // Even - targetPrediction = (predictions[halfIndex-1] + predictions[halfIndex]) / 2 - } else { - // Odd - targetPrediction = predictions[halfIndex] - } - default: - return nil, fmt.Errorf("unknown decision type '%s'", predictiveConfig.DecisionType) - } - - return &cpaevaluate.Evaluation{ - TargetReplicas: targetPrediction, - }, nil -} diff --git a/internal/evaluate/evaluate_test.go b/internal/evaluate/evaluate_test.go deleted file mode 100644 index 1ea0506..0000000 --- a/internal/evaluate/evaluate_test.go +++ /dev/null @@ -1,1009 +0,0 @@ -/* -Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package evaluate_test - -import ( - "database/sql" - "errors" - "testing" - - "github.com/google/go-cmp/cmp" - cpaconfig "github.com/jthomperoo/custom-pod-autoscaler/v2/config" - cpaevaluate "github.com/jthomperoo/custom-pod-autoscaler/v2/evaluate" - "github.com/jthomperoo/k8shorizmetrics/metrics" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/config" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/evaluate" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/fake" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/prediction" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/stored" -) - -func TestGetEvaluation(t *testing.T) { - equateErrorMessage := cmp.Comparer(func(x, y error) bool { - if x == nil || y == nil { - return x == nil && y == nil - } - return x.Error() == y.Error() - }) - - var tests = []struct { - description string - expected *cpaevaluate.Evaluation - expectedErr error - hpaEvaluator evaluate.HPAEvaluator - store stored.Storer - predicters []prediction.Predicter - predictiveConfig *config.Config - metrics []*metrics.Metric - currentReplicas int32 - runType string - }{ - { - "Fail, fail to get evaluation", - nil, - errors.New(`fail to get evaluation`), - &fake.Evaluater{ - EvaluateReactor: func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return 0, errors.New("fail to get evaluation") - }, - }, - nil, - nil, - nil, - nil, - 0, - cpaconfig.ScalerRunType, - }, - { - "Fail, fail to retrieve evaluations from store", - nil, - errors.New(`fail to get model from store`), - &fake.Evaluater{ - EvaluateReactor: func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return 3, nil - }, - }, - &fake.Store{ - GetModelReactor: func(model string) (*stored.Model, error) { - return nil, errors.New("fail to get model from store") - }, - }, - []prediction.Predicter{ - &fake.Predicter{ - GetTypeReactor: func() string { - return "fake" - }, - }, - }, - &config.Config{ - Models: []*config.Model{ - { - Type: "fake", - }, - }, - }, - nil, - 0, - cpaconfig.ScalerRunType, - }, - { - "Fail, no model exists, fail to add new", - nil, - errors.New(`fail to add new model`), - &fake.Evaluater{ - EvaluateReactor: func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return 3, nil - }, - }, - &fake.Store{ - GetModelReactor: func(model string) (*stored.Model, error) { - return nil, sql.ErrNoRows - }, - UpdateModelReactor: func(model string, intervalsPassed int) error { - return errors.New("fail to add new model") - }, - }, - []prediction.Predicter{ - &fake.Predicter{ - GetTypeReactor: func() string { - return "fake" - }, - }, - }, - &config.Config{ - Models: []*config.Model{ - { - Type: "fake", - }, - }, - }, - nil, - 0, - cpaconfig.ScalerRunType, - }, - { - "Fail, no model exists, add new and fail to retrieve newly added", - nil, - errors.New(`fail to get added model`), - &fake.Evaluater{ - EvaluateReactor: func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return 3, nil - }, - }, - func() *fake.Store { - timesCalled := 0 - return &fake.Store{ - GetModelReactor: func(model string) (*stored.Model, error) { - // First time respond that no model has been found - if timesCalled == 0 { - timesCalled++ - return nil, sql.ErrNoRows - } - return nil, errors.New("fail to get added model") - }, - UpdateModelReactor: func(model string, intervalsPassed int) error { - return nil - }, - } - }(), - []prediction.Predicter{ - &fake.Predicter{ - GetTypeReactor: func() string { - return "fake" - }, - }, - }, - &config.Config{ - Models: []*config.Model{ - { - Type: "fake", - }, - }, - }, - nil, - 0, - cpaconfig.ScalerRunType, - }, - { - "Fail, scaler not interval run, fail to update model", - nil, - errors.New(`fail to update model`), - &fake.Evaluater{ - EvaluateReactor: func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return 3, nil - }, - }, - &fake.Store{ - GetModelReactor: func(model string) (*stored.Model, error) { - return &stored.Model{ - ID: 2, - IntervalsPassed: 1, - }, nil - }, - UpdateModelReactor: func(model string, intervalsPassed int) error { - return errors.New("fail to update model") - }, - }, - []prediction.Predicter{ - &fake.Predicter{ - GetTypeReactor: func() string { - return "fake" - }, - }, - }, - &config.Config{ - Models: []*config.Model{ - { - Type: "fake", - PerInterval: 3, - }, - }, - }, - nil, - 0, - cpaconfig.ScalerRunType, - }, - { - "Fail, scaler interval run, fail to update model", - nil, - errors.New(`fail to update model`), - &fake.Evaluater{ - EvaluateReactor: func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return 3, nil - }, - }, - &fake.Store{ - GetModelReactor: func(model string) (*stored.Model, error) { - return &stored.Model{ - ID: 2, - IntervalsPassed: 3, - }, nil - }, - UpdateModelReactor: func(model string, intervalsPassed int) error { - return errors.New("fail to update model") - }, - }, - []prediction.Predicter{ - &fake.Predicter{ - GetTypeReactor: func() string { - return "fake" - }, - }, - }, - &config.Config{ - Models: []*config.Model{ - { - Type: "fake", - PerInterval: 3, - }, - }, - }, - nil, - 0, - cpaconfig.ScalerRunType, - }, - { - "Fail, scaler inteval run, fail to add evaluation", - nil, - errors.New(`fail to add evaluation`), - &fake.Evaluater{ - EvaluateReactor: func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return 3, nil - }, - }, - &fake.Store{ - GetModelReactor: func(model string) (*stored.Model, error) { - return &stored.Model{ - ID: 2, - IntervalsPassed: 3, - }, nil - }, - UpdateModelReactor: func(model string, intervalsPassed int) error { - return nil - }, - AddEvaluationReactor: func(model string, evaluation *cpaevaluate.Evaluation) error { - return errors.New("fail to add evaluation") - }, - }, - []prediction.Predicter{ - &fake.Predicter{ - GetTypeReactor: func() string { - return "fake" - }, - }, - }, - &config.Config{ - Models: []*config.Model{ - { - Type: "fake", - PerInterval: 3, - }, - }, - }, - nil, - 0, - cpaconfig.ScalerRunType, - }, - { - "Fail, scaler inteval run, fail to get evaluations", - nil, - errors.New(`fail to get evaluations`), - &fake.Evaluater{ - EvaluateReactor: func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return 3, nil - }, - }, - &fake.Store{ - GetModelReactor: func(model string) (*stored.Model, error) { - return &stored.Model{ - ID: 2, - IntervalsPassed: 3, - }, nil - }, - UpdateModelReactor: func(model string, intervalsPassed int) error { - return nil - }, - AddEvaluationReactor: func(model string, evaluation *cpaevaluate.Evaluation) error { - return nil - }, - GetEvaluationReactor: func(model string) ([]*stored.Evaluation, error) { - return nil, errors.New("fail to get evaluations") - }, - }, - []prediction.Predicter{ - &fake.Predicter{ - GetTypeReactor: func() string { - return "fake" - }, - }, - }, - &config.Config{ - Models: []*config.Model{ - { - Type: "fake", - PerInterval: 3, - }, - }, - }, - nil, - 0, - cpaconfig.ScalerRunType, - }, - { - "Fail, scaler inteval run, fail to get prediction", - nil, - errors.New(`fail to get prediction`), - &fake.Evaluater{ - EvaluateReactor: func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return 3, nil - }, - }, - &fake.Store{ - GetModelReactor: func(model string) (*stored.Model, error) { - return &stored.Model{ - ID: 2, - IntervalsPassed: 3, - }, nil - }, - UpdateModelReactor: func(model string, intervalsPassed int) error { - return nil - }, - AddEvaluationReactor: func(model string, evaluation *cpaevaluate.Evaluation) error { - return nil - }, - GetEvaluationReactor: func(model string) ([]*stored.Evaluation, error) { - return []*stored.Evaluation{}, nil - }, - }, - []prediction.Predicter{ - &fake.Predicter{ - GetTypeReactor: func() string { - return "fake" - }, - GetPredictionReactor: func(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { - return 0, errors.New("fail to get prediction") - }, - }, - }, - &config.Config{ - Models: []*config.Model{ - { - Type: "fake", - PerInterval: 3, - }, - }, - }, - nil, - 0, - cpaconfig.ScalerRunType, - }, - { - "Fail, scaler inteval run, fail to get IDs to remove", - nil, - errors.New(`fail to get IDs to remove`), - &fake.Evaluater{ - EvaluateReactor: func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return 3, nil - }, - }, - &fake.Store{ - GetModelReactor: func(model string) (*stored.Model, error) { - return &stored.Model{ - ID: 2, - IntervalsPassed: 3, - }, nil - }, - UpdateModelReactor: func(model string, intervalsPassed int) error { - return nil - }, - AddEvaluationReactor: func(model string, evaluation *cpaevaluate.Evaluation) error { - return nil - }, - GetEvaluationReactor: func(model string) ([]*stored.Evaluation, error) { - return []*stored.Evaluation{}, nil - }, - }, - []prediction.Predicter{ - &fake.Predicter{ - GetTypeReactor: func() string { - return "fake" - }, - GetPredictionReactor: func(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { - return 3, nil - }, - GetIDsToRemoveReactor: func(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) { - return nil, errors.New("fail to get IDs to remove") - }, - }, - }, - &config.Config{ - Models: []*config.Model{ - { - Type: "fake", - PerInterval: 3, - }, - }, - }, - nil, - 0, - cpaconfig.ScalerRunType, - }, - { - "Fail, scaler inteval run, fail to remove evaluations", - nil, - errors.New(`fail to remove evaluation`), - &fake.Evaluater{ - EvaluateReactor: func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return 3, nil - }, - }, - &fake.Store{ - GetModelReactor: func(model string) (*stored.Model, error) { - return &stored.Model{ - ID: 2, - IntervalsPassed: 3, - }, nil - }, - UpdateModelReactor: func(model string, intervalsPassed int) error { - return nil - }, - AddEvaluationReactor: func(model string, evaluation *cpaevaluate.Evaluation) error { - return nil - }, - GetEvaluationReactor: func(model string) ([]*stored.Evaluation, error) { - return []*stored.Evaluation{}, nil - }, - RemoveEvaluationReactor: func(id int) error { - return errors.New("fail to remove evaluation") - }, - }, - []prediction.Predicter{ - &fake.Predicter{ - GetTypeReactor: func() string { - return "fake" - }, - GetPredictionReactor: func(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { - return 3, nil - }, - GetIDsToRemoveReactor: func(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) { - return []int{0, 1, 2}, nil - }, - }, - }, - &config.Config{ - Models: []*config.Model{ - { - Type: "fake", - PerInterval: 3, - }, - }, - }, - nil, - 0, - cpaconfig.ScalerRunType, - }, - { - "Fail, unknown decision type", - nil, - errors.New("unknown decision type 'unknown'"), - &fake.Evaluater{ - EvaluateReactor: func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return 0, nil - }, - }, - &fake.Store{ - GetModelReactor: func(model string) (*stored.Model, error) { - return &stored.Model{ - ID: 2, - IntervalsPassed: 3, - }, nil - }, - UpdateModelReactor: func(model string, intervalsPassed int) error { - return nil - }, - AddEvaluationReactor: func(model string, evaluation *cpaevaluate.Evaluation) error { - return nil - }, - GetEvaluationReactor: func(model string) ([]*stored.Evaluation, error) { - return []*stored.Evaluation{}, nil - }, - RemoveEvaluationReactor: func(id int) error { - return nil - }, - }, - []prediction.Predicter{ - &fake.Predicter{ - GetTypeReactor: func() string { - return "fake" - }, - GetPredictionReactor: func(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { - if model.Name == "lower" { - return 1, nil - } - return 3, nil - }, - GetIDsToRemoveReactor: func(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) { - return []int{0, 1, 2}, nil - }, - }, - }, - &config.Config{ - Models: []*config.Model{ - { - Type: "fake", - PerInterval: 3, - Name: "lower", - }, - { - Type: "fake", - PerInterval: 3, - Name: "higher", - }, - }, - DecisionType: "unknown", - }, - nil, - 0, - cpaconfig.ScalerRunType, - }, - { - "Success, two models, pick maximum replicas, evaluation lower than prediction", - &cpaevaluate.Evaluation{ - TargetReplicas: 3, - }, - nil, - &fake.Evaluater{ - EvaluateReactor: func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return 0, nil - }, - }, - &fake.Store{ - GetModelReactor: func(model string) (*stored.Model, error) { - return &stored.Model{ - ID: 2, - IntervalsPassed: 3, - }, nil - }, - UpdateModelReactor: func(model string, intervalsPassed int) error { - return nil - }, - AddEvaluationReactor: func(model string, evaluation *cpaevaluate.Evaluation) error { - return nil - }, - GetEvaluationReactor: func(model string) ([]*stored.Evaluation, error) { - return []*stored.Evaluation{}, nil - }, - RemoveEvaluationReactor: func(id int) error { - return nil - }, - }, - []prediction.Predicter{ - &fake.Predicter{ - GetTypeReactor: func() string { - return "fake" - }, - GetPredictionReactor: func(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { - if model.Name == "lower" { - return 1, nil - } - return 3, nil - }, - GetIDsToRemoveReactor: func(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) { - return []int{0, 1, 2}, nil - }, - }, - }, - &config.Config{ - Models: []*config.Model{ - { - Type: "fake", - PerInterval: 3, - Name: "lower", - }, - { - Type: "fake", - PerInterval: 3, - Name: "higher", - }, - }, - DecisionType: config.DecisionMaximum, - }, - nil, - 0, - cpaconfig.ScalerRunType, - }, - { - "Success, two models, pick minimum replicas, evaluation lower than prediction", - &cpaevaluate.Evaluation{ - TargetReplicas: 1, - }, - nil, - &fake.Evaluater{ - EvaluateReactor: func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return 2, nil - }, - }, - &fake.Store{ - GetModelReactor: func(model string) (*stored.Model, error) { - return &stored.Model{ - ID: 2, - IntervalsPassed: 3, - }, nil - }, - UpdateModelReactor: func(model string, intervalsPassed int) error { - return nil - }, - AddEvaluationReactor: func(model string, evaluation *cpaevaluate.Evaluation) error { - return nil - }, - GetEvaluationReactor: func(model string) ([]*stored.Evaluation, error) { - return []*stored.Evaluation{}, nil - }, - RemoveEvaluationReactor: func(id int) error { - return nil - }, - }, - []prediction.Predicter{ - &fake.Predicter{ - GetTypeReactor: func() string { - return "fake" - }, - GetPredictionReactor: func(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { - if model.Name == "lower" { - return 1, nil - } - return 3, nil - }, - GetIDsToRemoveReactor: func(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) { - return []int{0, 1, 2}, nil - }, - }, - }, - &config.Config{ - Models: []*config.Model{ - { - Type: "fake", - PerInterval: 3, - Name: "lower", - }, - { - Type: "fake", - PerInterval: 3, - Name: "higher", - }, - }, - DecisionType: config.DecisionMinimum, - }, - nil, - 0, - cpaconfig.ScalerRunType, - }, - { - "Success, two models, pick mean replicas, evaluation lower than prediction", - &cpaevaluate.Evaluation{ - TargetReplicas: 1, - }, - nil, - &fake.Evaluater{ - EvaluateReactor: func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return 0, nil - }, - }, - &fake.Store{ - GetModelReactor: func(model string) (*stored.Model, error) { - return &stored.Model{ - ID: 2, - IntervalsPassed: 3, - }, nil - }, - UpdateModelReactor: func(model string, intervalsPassed int) error { - return nil - }, - AddEvaluationReactor: func(model string, evaluation *cpaevaluate.Evaluation) error { - return nil - }, - GetEvaluationReactor: func(model string) ([]*stored.Evaluation, error) { - return []*stored.Evaluation{}, nil - }, - RemoveEvaluationReactor: func(id int) error { - return nil - }, - }, - []prediction.Predicter{ - &fake.Predicter{ - GetTypeReactor: func() string { - return "fake" - }, - GetPredictionReactor: func(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { - if model.Name == "lower" { - return 1, nil - } - return 3, nil - }, - GetIDsToRemoveReactor: func(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) { - return []int{0, 1, 2}, nil - }, - }, - }, - &config.Config{ - Models: []*config.Model{ - { - Type: "fake", - PerInterval: 3, - Name: "lower", - }, - { - Type: "fake", - PerInterval: 3, - Name: "higher", - }, - }, - DecisionType: config.DecisionMean, - }, - nil, - 0, - cpaconfig.ScalerRunType, - }, - { - "Success, four models, pick median replicas", - &cpaevaluate.Evaluation{ - TargetReplicas: 3, - }, - nil, - &fake.Evaluater{ - EvaluateReactor: func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return 0, nil - }, - }, - &fake.Store{ - GetModelReactor: func(model string) (*stored.Model, error) { - return &stored.Model{ - ID: 2, - IntervalsPassed: 3, - }, nil - }, - UpdateModelReactor: func(model string, intervalsPassed int) error { - return nil - }, - AddEvaluationReactor: func(model string, evaluation *cpaevaluate.Evaluation) error { - return nil - }, - GetEvaluationReactor: func(model string) ([]*stored.Evaluation, error) { - return []*stored.Evaluation{}, nil - }, - RemoveEvaluationReactor: func(id int) error { - return nil - }, - }, - []prediction.Predicter{ - &fake.Predicter{ - GetTypeReactor: func() string { - return "fake" - }, - GetPredictionReactor: func(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { - if model.Name == "a" { - return 10, nil - } - if model.Name == "b" { - return 2, nil - } - if model.Name == "c" { - return 3, nil - } - return 9, nil - }, - GetIDsToRemoveReactor: func(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) { - return []int{0, 1, 2}, nil - }, - }, - }, - &config.Config{ - Models: []*config.Model{ - { - Type: "fake", - PerInterval: 3, - Name: "a", - }, - { - Type: "fake", - PerInterval: 3, - Name: "b", - }, - { - Type: "fake", - PerInterval: 3, - Name: "c", - }, - { - Type: "fake", - PerInterval: 3, - Name: "d", - }, - }, - DecisionType: config.DecisionMedian, - }, - nil, - 0, - cpaconfig.ScalerRunType, - }, - { - "Success, five models, pick median replicas", - &cpaevaluate.Evaluation{ - TargetReplicas: 4, - }, - nil, - &fake.Evaluater{ - EvaluateReactor: func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return 0, nil - }, - }, - &fake.Store{ - GetModelReactor: func(model string) (*stored.Model, error) { - return &stored.Model{ - ID: 2, - IntervalsPassed: 3, - }, nil - }, - UpdateModelReactor: func(model string, intervalsPassed int) error { - return nil - }, - AddEvaluationReactor: func(model string, evaluation *cpaevaluate.Evaluation) error { - return nil - }, - GetEvaluationReactor: func(model string) ([]*stored.Evaluation, error) { - return []*stored.Evaluation{}, nil - }, - RemoveEvaluationReactor: func(id int) error { - return nil - }, - }, - []prediction.Predicter{ - &fake.Predicter{ - GetTypeReactor: func() string { - return "fake" - }, - GetPredictionReactor: func(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { - if model.Name == "a" { - return 10, nil - } - if model.Name == "b" { - return 2, nil - } - if model.Name == "c" { - return 3, nil - } - if model.Name == "d" { - return 5, nil - } - return 9, nil - }, - GetIDsToRemoveReactor: func(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) { - return []int{0, 1, 2}, nil - }, - }, - }, - &config.Config{ - Models: []*config.Model{ - { - Type: "fake", - PerInterval: 3, - Name: "a", - }, - { - Type: "fake", - PerInterval: 3, - Name: "b", - }, - { - Type: "fake", - PerInterval: 3, - Name: "c", - }, - { - Type: "fake", - PerInterval: 3, - Name: "d", - }, - { - Type: "fake", - PerInterval: 3, - Name: "e", - }, - }, - DecisionType: config.DecisionMedian, - }, - nil, - 0, - cpaconfig.ScalerRunType, - }, - { - "Success, one model, evaluation higher than prediction", - &cpaevaluate.Evaluation{ - TargetReplicas: 4, - }, - nil, - &fake.Evaluater{ - EvaluateReactor: func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return 4, nil - }, - }, - &fake.Store{ - GetModelReactor: func(model string) (*stored.Model, error) { - return &stored.Model{ - ID: 2, - IntervalsPassed: 3, - }, nil - }, - UpdateModelReactor: func(model string, intervalsPassed int) error { - return nil - }, - AddEvaluationReactor: func(model string, evaluation *cpaevaluate.Evaluation) error { - return nil - }, - GetEvaluationReactor: func(model string) ([]*stored.Evaluation, error) { - return []*stored.Evaluation{}, nil - }, - RemoveEvaluationReactor: func(id int) error { - return nil - }, - }, - []prediction.Predicter{ - &fake.Predicter{ - GetTypeReactor: func() string { - return "fake" - }, - GetPredictionReactor: func(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { - return 1, nil - }, - GetIDsToRemoveReactor: func(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) { - return []int{0, 1, 2}, nil - }, - }, - }, - &config.Config{ - Models: []*config.Model{ - { - Type: "fake", - PerInterval: 3, - Name: "test", - }, - }, - DecisionType: config.DecisionMaximum, - }, - nil, - 0, - cpaconfig.ScalerRunType, - }, - } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - evaluator := &evaluate.PredictiveEvaluate{ - HPAEvaluator: test.hpaEvaluator, - Store: test.store, - Predicters: test.predicters, - } - result, err := evaluator.GetEvaluation(test.predictiveConfig, test.metrics, test.currentReplicas, test.runType) - if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { - t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) - return - } - if !cmp.Equal(test.expected, result) { - t.Errorf("result mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) - } - }) - } -} diff --git a/internal/fake/algorithm.go b/internal/fake/algorithm.go index 0b5fb26..6cced3a 100644 --- a/internal/fake/algorithm.go +++ b/internal/fake/algorithm.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. +Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/fake/algorithm_test.go b/internal/fake/algorithm_test.go deleted file mode 100644 index fb9a082..0000000 --- a/internal/fake/algorithm_test.go +++ /dev/null @@ -1,83 +0,0 @@ -/* -Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package fake_test - -import ( - "errors" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/fake" -) - -func TestAlgorithm_RunAlgorithmWithValue(t *testing.T) { - equateErrorMessage := cmp.Comparer(func(x, y error) bool { - if x == nil || y == nil { - return x == nil && y == nil - } - return x.Error() == y.Error() - }) - - var tests = []struct { - description string - expected string - expectedErr error - run fake.Run - algorithmPath string - value string - timeout int - }{ - { - "Return error", - "", - errors.New("runner error"), - fake.Run{ - RunAlgorithmWithValueReactor: func(algorithmPath, value string, timeout int) (string, error) { - return "", errors.New("runner error") - }, - }, - "", - "", - 10, - }, - { - "Return test value", - "test", - nil, - fake.Run{ - RunAlgorithmWithValueReactor: func(algorithmPath, value string, timeout int) (string, error) { - return "test", nil - }, - }, - "", - "", - 10, - }, - } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - result, err := test.run.RunAlgorithmWithValue(test.algorithmPath, test.value, test.timeout) - if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { - t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) - return - } - if !cmp.Equal(test.expected, result) { - t.Errorf("config mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) - } - }) - } -} diff --git a/internal/fake/evaluater.go b/internal/fake/evaluater.go deleted file mode 100644 index 61b90f3..0000000 --- a/internal/fake/evaluater.go +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package fake - -import ( - "github.com/jthomperoo/k8shorizmetrics/metrics" -) - -// Evaluater (fake) provides a way to insert functionality into a Evaluater -type Evaluater struct { - EvaluateReactor func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) -} - -// GetEvaluation calls the fake Evaluater function -func (f *Evaluater) Evaluate(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return f.EvaluateReactor(gatheredMetrics, currentReplicas) -} diff --git a/internal/fake/evaluater_test.go b/internal/fake/evaluater_test.go deleted file mode 100644 index ea902b2..0000000 --- a/internal/fake/evaluater_test.go +++ /dev/null @@ -1,81 +0,0 @@ -/* -Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package fake_test - -import ( - "errors" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/jthomperoo/k8shorizmetrics/metrics" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/fake" -) - -func TestEvaluater_GetEvaluation(t *testing.T) { - equateErrorMessage := cmp.Comparer(func(x, y error) bool { - if x == nil || y == nil { - return x == nil && y == nil - } - return x.Error() == y.Error() - }) - - var tests = []struct { - description string - expected int32 - expectedErr error - evaluater fake.Evaluater - metrics []*metrics.Metric - currentReplicas int32 - }{ - { - "Return error", - 0, - errors.New("evaluater error"), - fake.Evaluater{ - EvaluateReactor: func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return 0, errors.New("evaluater error") - }, - }, - []*metrics.Metric{}, - 0, - }, - { - "Return evaluation", - 5, - nil, - fake.Evaluater{ - EvaluateReactor: func(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { - return 5, nil - }, - }, - []*metrics.Metric{}, - 0, - }, - } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - result, err := test.evaluater.Evaluate(test.metrics, test.currentReplicas) - if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { - t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) - return - } - if !cmp.Equal(test.expected, result) { - t.Errorf("config mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) - } - }) - } -} diff --git a/internal/fake/hook.go b/internal/fake/hook.go index 7cc2f12..23f703d 100644 --- a/internal/fake/hook.go +++ b/internal/fake/hook.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. +Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,16 +16,16 @@ limitations under the License. package fake -import "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook" +import jamiethompsonmev1alpha1 "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/api/v1alpha1" // Execute (fake) provides a way to insert functionality into a hook executer type Execute struct { - ExecuteWithValueReactor func(definition *hook.Definition, value string) (string, error) + ExecuteWithValueReactor func(definition *jamiethompsonmev1alpha1.HookDefinition, value string) (string, error) GetTypeReactor func() string } // ExecuteWithValue calls the fake Executer function -func (e *Execute) ExecuteWithValue(definition *hook.Definition, value string) (string, error) { +func (e *Execute) ExecuteWithValue(definition *jamiethompsonmev1alpha1.HookDefinition, value string) (string, error) { return e.ExecuteWithValueReactor(definition, value) } diff --git a/internal/fake/hook_test.go b/internal/fake/hook_test.go deleted file mode 100644 index 480fc26..0000000 --- a/internal/fake/hook_test.go +++ /dev/null @@ -1,114 +0,0 @@ -/* -Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package fake_test - -import ( - "errors" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/fake" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook" -) - -func TestExecute_ExecuteWithValue(t *testing.T) { - equateErrorMessage := cmp.Comparer(func(x, y error) bool { - if x == nil || y == nil { - return x == nil && y == nil - } - return x.Error() == y.Error() - }) - - var tests = []struct { - description string - expected string - expectedErr error - execute fake.Execute - definition *hook.Definition - value string - }{ - { - "Return error", - "", - errors.New("execute error"), - fake.Execute{ - ExecuteWithValueReactor: func(definition *hook.Definition, value string) (string, error) { - return "", errors.New("execute error") - }, - }, - &hook.Definition{ - Type: "test", - }, - "test", - }, - { - "Return test value", - "test", - nil, - fake.Execute{ - ExecuteWithValueReactor: func(definition *hook.Definition, value string) (string, error) { - return "test", nil - }, - }, - &hook.Definition{ - Type: "test", - }, - "test", - }, - } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - result, err := test.execute.ExecuteWithValue(test.definition, test.value) - if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { - t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) - return - } - if !cmp.Equal(test.expected, result) { - t.Errorf("config mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) - } - }) - } -} - -func TestExecute_GetType(t *testing.T) { - var tests = []struct { - description string - expected string - execute fake.Execute - }{ - { - "Return type", - "test", - fake.Execute{ - GetTypeReactor: func() string { - return "test" - }, - ExecuteWithValueReactor: func(definition *hook.Definition, value string) (string, error) { - return "", errors.New("execute error") - }, - }, - }, - } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - result := test.execute.GetType() - if !cmp.Equal(test.expected, result) { - t.Errorf("config mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) - } - }) - } -} diff --git a/internal/fake/predicter.go b/internal/fake/predicter.go index 48e9dd9..e9cbb80 100644 --- a/internal/fake/predicter.go +++ b/internal/fake/predicter.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. +Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,25 +17,24 @@ limitations under the License. package fake import ( - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/config" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/stored" + jamiethompsonmev1alpha1 "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/api/v1alpha1" ) // Predicter (fake) provides a way to insert functionality into a Predicter type Predicter struct { - GetPredictionReactor func(model *config.Model, evaluations []*stored.Evaluation) (int32, error) - GetIDsToRemoveReactor func(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) - GetTypeReactor func() string + GetPredictionReactor func(model *jamiethompsonmev1alpha1.Model, replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas) (int32, error) + PruneHistoryReactor func(model *jamiethompsonmev1alpha1.Model, replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas) ([]jamiethompsonmev1alpha1.TimestampedReplicas, error) + GetTypeReactor func() string } // GetIDsToRemove calls the fake Predicter function -func (f *Predicter) GetIDsToRemove(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) { - return f.GetIDsToRemoveReactor(model, evaluations) +func (f *Predicter) PruneHistory(model *jamiethompsonmev1alpha1.Model, replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas) ([]jamiethompsonmev1alpha1.TimestampedReplicas, error) { + return f.PruneHistoryReactor(model, replicaHistory) } // GetPrediction calls the fake Predicter function -func (f *Predicter) GetPrediction(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { - return f.GetPredictionReactor(model, evaluations) +func (f *Predicter) GetPrediction(model *jamiethompsonmev1alpha1.Model, replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas) (int32, error) { + return f.GetPredictionReactor(model, replicaHistory) } // GetType calls the fake Predicter function diff --git a/internal/fake/predicter_test.go b/internal/fake/predicter_test.go deleted file mode 100644 index 6d441fc..0000000 --- a/internal/fake/predicter_test.go +++ /dev/null @@ -1,162 +0,0 @@ -/* -Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package fake_test - -import ( - "errors" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/config" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/fake" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/stored" -) - -func TestPredicter_GetIDsToRemove(t *testing.T) { - equateErrorMessage := cmp.Comparer(func(x, y error) bool { - if x == nil || y == nil { - return x == nil && y == nil - } - return x.Error() == y.Error() - }) - - var tests = []struct { - description string - expected []int - expectedErr error - predicter fake.Predicter - model *config.Model - evaluations []*stored.Evaluation - }{ - { - "Return error", - nil, - errors.New("predicter error"), - fake.Predicter{ - GetIDsToRemoveReactor: func(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) { - return nil, errors.New("predicter error") - }, - }, - &config.Model{}, - []*stored.Evaluation{}, - }, - { - "Return IDs", - []int{2, 3, 6}, - nil, - fake.Predicter{ - GetIDsToRemoveReactor: func(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) { - return []int{2, 3, 6}, nil - }, - }, - &config.Model{}, - []*stored.Evaluation{}, - }, - } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - result, err := test.predicter.GetIDsToRemove(test.model, test.evaluations) - if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { - t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) - return - } - if !cmp.Equal(test.expected, result) { - t.Errorf("config mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) - } - }) - } -} - -func TestPredicter_GetPredictionReactor(t *testing.T) { - equateErrorMessage := cmp.Comparer(func(x, y error) bool { - if x == nil || y == nil { - return x == nil && y == nil - } - return x.Error() == y.Error() - }) - - var tests = []struct { - description string - expected int32 - expectedErr error - predicter fake.Predicter - model *config.Model - evaluations []*stored.Evaluation - }{ - { - "Return error", - 0, - errors.New("predicter error"), - fake.Predicter{ - GetPredictionReactor: func(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { - return 0, errors.New("predicter error") - }, - }, - &config.Model{}, - []*stored.Evaluation{}, - }, - { - "Return IDs", - 52, - nil, - fake.Predicter{ - GetPredictionReactor: func(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { - return 52, nil - }, - }, - &config.Model{}, - []*stored.Evaluation{}, - }, - } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - result, err := test.predicter.GetPrediction(test.model, test.evaluations) - if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { - t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) - return - } - if !cmp.Equal(test.expected, result) { - t.Errorf("config mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) - } - }) - } -} - -func TestPredicter_GetTypeReactor(t *testing.T) { - var tests = []struct { - description string - expected string - predicter fake.Predicter - }{ - { - "Return type", - "example type", - fake.Predicter{ - GetTypeReactor: func() string { - return "example type" - }, - }, - }, - } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - result := test.predicter.GetType() - if !cmp.Equal(test.expected, result) { - t.Errorf("config mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) - } - }) - } -} diff --git a/internal/fake/stored.go b/internal/fake/stored.go deleted file mode 100644 index e6ec5fb..0000000 --- a/internal/fake/stored.go +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package fake - -import ( - "github.com/jthomperoo/custom-pod-autoscaler/v2/evaluate" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/stored" -) - -// Store (fake) provides a way to insert functionality into a Store -type Store struct { - GetEvaluationReactor func(model string) ([]*stored.Evaluation, error) - AddEvaluationReactor func(model string, evaluation *evaluate.Evaluation) error - RemoveEvaluationReactor func(id int) error - GetModelReactor func(model string) (*stored.Model, error) - UpdateModelReactor func(model string, intervalsPassed int) error -} - -// GetEvaluation calls the fake Store function -func (f *Store) GetEvaluation(model string) ([]*stored.Evaluation, error) { - return f.GetEvaluationReactor(model) -} - -// AddEvaluation calls the fake Store function -func (f *Store) AddEvaluation(model string, evaluation *evaluate.Evaluation) error { - return f.AddEvaluationReactor(model, evaluation) -} - -// RemoveEvaluation calls the fake Store function -func (f *Store) RemoveEvaluation(id int) error { - return f.RemoveEvaluationReactor(id) -} - -// GetModel calls the fake Store function -func (f *Store) GetModel(model string) (*stored.Model, error) { - return f.GetModelReactor(model) -} - -// UpdateModel calls the fake Store function -func (f *Store) UpdateModel(model string, intervalsPassed int) error { - return f.UpdateModelReactor(model, intervalsPassed) -} diff --git a/internal/fake/stored_test.go b/internal/fake/stored_test.go deleted file mode 100644 index 94ff55f..0000000 --- a/internal/fake/stored_test.go +++ /dev/null @@ -1,297 +0,0 @@ -/* -Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package fake_test - -import ( - "errors" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/jthomperoo/custom-pod-autoscaler/v2/evaluate" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/fake" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/stored" -) - -func TestStore_GetEvaluation(t *testing.T) { - equateErrorMessage := cmp.Comparer(func(x, y error) bool { - if x == nil || y == nil { - return x == nil && y == nil - } - return x.Error() == y.Error() - }) - - var tests = []struct { - description string - expected []*stored.Evaluation - expectedErr error - store fake.Store - model string - }{ - { - "Return error", - nil, - errors.New("store error"), - fake.Store{ - GetEvaluationReactor: func(model string) ([]*stored.Evaluation, error) { - return nil, errors.New("store error") - }, - }, - "test", - }, - { - "Return evaluation", - []*stored.Evaluation{ - { - ID: 3, - Evaluation: stored.DBEvaluation{ - TargetReplicas: 2, - }, - }, - }, - nil, - fake.Store{ - GetEvaluationReactor: func(model string) ([]*stored.Evaluation, error) { - return []*stored.Evaluation{ - { - ID: 3, - Evaluation: stored.DBEvaluation{ - TargetReplicas: 2, - }, - }, - }, nil - }, - }, - "test", - }, - } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - result, err := test.store.GetEvaluation(test.model) - if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { - t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) - return - } - if !cmp.Equal(test.expected, result) { - t.Errorf("config mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) - } - }) - } -} - -func TestStore_AddEvaluation(t *testing.T) { - equateErrorMessage := cmp.Comparer(func(x, y error) bool { - if x == nil || y == nil { - return x == nil && y == nil - } - return x.Error() == y.Error() - }) - - var tests = []struct { - description string - expectedErr error - store fake.Store - model string - evaluation *evaluate.Evaluation - }{ - { - "Return error", - errors.New("store error"), - fake.Store{ - AddEvaluationReactor: func(model string, evaluation *evaluate.Evaluation) error { - return errors.New("store error") - }, - }, - "test", - &evaluate.Evaluation{}, - }, - { - "Return no error", - nil, - fake.Store{ - AddEvaluationReactor: func(model string, evaluation *evaluate.Evaluation) error { - return nil - }, - }, - "test", - &evaluate.Evaluation{}, - }, - } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - err := test.store.AddEvaluation(test.model, test.evaluation) - if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { - t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) - return - } - }) - } -} - -func TestStore_RemoveEvaluation(t *testing.T) { - equateErrorMessage := cmp.Comparer(func(x, y error) bool { - if x == nil || y == nil { - return x == nil && y == nil - } - return x.Error() == y.Error() - }) - - var tests = []struct { - description string - expectedErr error - store fake.Store - id int - }{ - { - "Return error", - errors.New("store error"), - fake.Store{ - RemoveEvaluationReactor: func(id int) error { - return errors.New("store error") - }, - }, - 1, - }, - { - "Return no error", - nil, - fake.Store{ - RemoveEvaluationReactor: func(id int) error { - return nil - }, - }, - 3, - }, - } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - err := test.store.RemoveEvaluation(test.id) - if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { - t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) - return - } - }) - } -} - -func TestStore_GetModelReactor(t *testing.T) { - equateErrorMessage := cmp.Comparer(func(x, y error) bool { - if x == nil || y == nil { - return x == nil && y == nil - } - return x.Error() == y.Error() - }) - - var tests = []struct { - description string - expected *stored.Model - expectedErr error - store fake.Store - model string - }{ - { - "Return error", - nil, - errors.New("store error"), - fake.Store{ - GetModelReactor: func(model string) (*stored.Model, error) { - return nil, errors.New("store error") - }, - }, - "test", - }, - { - "Return model", - &stored.Model{ - ID: 3, - IntervalsPassed: 2, - Name: "test", - }, - nil, - fake.Store{ - GetModelReactor: func(model string) (*stored.Model, error) { - return &stored.Model{ - ID: 3, - IntervalsPassed: 2, - Name: "test", - }, nil - }, - }, - "test", - }, - } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - result, err := test.store.GetModel(test.model) - if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { - t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) - return - } - if !cmp.Equal(test.expected, result) { - t.Errorf("config mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) - } - }) - } -} - -func TestStore_UpdateModelReactor(t *testing.T) { - equateErrorMessage := cmp.Comparer(func(x, y error) bool { - if x == nil || y == nil { - return x == nil && y == nil - } - return x.Error() == y.Error() - }) - - var tests = []struct { - description string - expectedErr error - store fake.Store - model string - intervalsPassed int - }{ - { - "Return error", - errors.New("store error"), - fake.Store{ - UpdateModelReactor: func(model string, intervalsPassed int) error { - return errors.New("store error") - }, - }, - "test", - 1, - }, - { - "Return no error", - nil, - fake.Store{ - UpdateModelReactor: func(model string, intervalsPassed int) error { - return nil - }, - }, - "test", - 3, - }, - } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - err := test.store.UpdateModel(test.model, test.intervalsPassed) - if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { - t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) - return - } - }) - } -} diff --git a/internal/hook/hook.go b/internal/hook/hook.go index 9cc7d7d..841549c 100644 --- a/internal/hook/hook.go +++ b/internal/hook/hook.go @@ -18,64 +18,11 @@ limitations under the License. package hook import ( - "fmt" + jamiethompsonmev1alpha1 "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/api/v1alpha1" ) -// Definition describes a hook for passing data/triggering logic, such as through a shell command -type Definition struct { - Type string `json:"type"` - Timeout int `json:"timeout"` - Shell *Shell `json:"shell"` - HTTP *HTTP `json:"http"` -} - -// Shell describes configuration options for a shell command hook -type Shell struct { - Command []string `json:"command"` - Entrypoint string `json:"entrypoint"` -} - -// HTTP describes configuration options for an HTTP request hook -type HTTP struct { - Method string `json:"method"` - URL string `json:"url"` - Headers map[string]string `json:"headers,omitempty"` - SuccessCodes []int `json:"successCodes"` - ParameterMode string `json:"parameterMode"` -} - // Executer interface provides methods for executing user logic with a value passed through to it type Executer interface { - ExecuteWithValue(definition *Definition, value string) (string, error) + ExecuteWithValue(definition *jamiethompsonmev1alpha1.HookDefinition, value string) (string, error) GetType() string } - -// CombinedType is the type of the CombinedExecute; designed to link together multiple executers -// and to provide a simplified single entry point -const CombinedType = "combined" - -// CombinedExecute is an executer that contains subexecuters that it will forward hook requests -// to; designed to link together multiple executers and to provide a simplified single entry point -type CombinedExecute struct { - Executers []Executer -} - -// ExecuteWithValue takes in a hook definition and a value to pass, it will look at the stored sub executers -// and decide which executer to use for the hook provided -func (e *CombinedExecute) ExecuteWithValue(definition *Definition, value string) (string, error) { - for _, executer := range e.Executers { - if executer.GetType() == definition.Type { - gathered, err := executer.ExecuteWithValue(definition, value) - if err != nil { - return "", err - } - return gathered, nil - } - } - return "", fmt.Errorf("unknown execution method: '%s'", definition.Type) -} - -// GetType returns the CombinedExecute type -func (e *CombinedExecute) GetType() string { - return CombinedType -} diff --git a/internal/hook/hook_test.go b/internal/hook/hook_test.go deleted file mode 100644 index 45a7020..0000000 --- a/internal/hook/hook_test.go +++ /dev/null @@ -1,186 +0,0 @@ -/* -Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package hook_test - -import ( - "errors" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/fake" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook" -) - -func TestCombinedExecute_ExecuteWithValue(t *testing.T) { - equateErrorMessage := cmp.Comparer(func(x, y error) bool { - if x == nil || y == nil { - return x == nil && y == nil - } - return x.Error() == y.Error() - }) - var tests = []struct { - description string - expected string - expectedErr error - method *hook.Definition - value string - executers []hook.Executer - }{ - { - "Fail, no executers provided", - "", - errors.New(`unknown execution method: 'unknown'`), - &hook.Definition{ - Type: "unknown", - }, - "test", - []hook.Executer{}, - }, - { - "Fail, unknown execution method", - "", - errors.New(`unknown execution method: 'unknown'`), - &hook.Definition{ - Type: "unknown", - }, - "test", - []hook.Executer{ - &fake.Execute{ - GetTypeReactor: func() string { - return "fake" - }, - ExecuteWithValueReactor: func(method *hook.Definition, value string) (string, error) { - return "fake", nil - }, - }, - }, - }, - { - "Fail, sub executer fails", - "", - errors.New("execute error"), - &hook.Definition{ - Type: "test", - }, - "test", - []hook.Executer{ - &fake.Execute{ - GetTypeReactor: func() string { - return "test" - }, - ExecuteWithValueReactor: func(method *hook.Definition, value string) (string, error) { - return "", errors.New("execute error") - }, - }, - }, - }, - { - "Successful execute, one executer", - "test", - nil, - &hook.Definition{ - Type: "test", - }, - "test", - []hook.Executer{ - &fake.Execute{ - GetTypeReactor: func() string { - return "test" - }, - ExecuteWithValueReactor: func(method *hook.Definition, value string) (string, error) { - return "test", nil - }, - }, - }, - }, - { - "Successful execute, three executers", - "test", - nil, - &hook.Definition{ - Type: "test1", - }, - "test", - []hook.Executer{ - &fake.Execute{ - GetTypeReactor: func() string { - return "test1" - }, - ExecuteWithValueReactor: func(method *hook.Definition, value string) (string, error) { - return "test", nil - }, - }, - &fake.Execute{ - GetTypeReactor: func() string { - return "test2" - }, - ExecuteWithValueReactor: func(method *hook.Definition, value string) (string, error) { - return "", nil - }, - }, - &fake.Execute{ - GetTypeReactor: func() string { - return "test3" - }, - ExecuteWithValueReactor: func(method *hook.Definition, value string) (string, error) { - return "", nil - }, - }, - }, - }, - } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - execute := &hook.CombinedExecute{ - Executers: test.executers, - } - result, err := execute.ExecuteWithValue(test.method, test.value) - if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { - t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) - return - } - if !cmp.Equal(test.expected, result) { - t.Errorf("metrics mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) - } - }) - } -} - -func TestCombinedExecute_GetType(t *testing.T) { - var tests = []struct { - description string - expected string - executers []hook.Executer - }{ - { - "Return type", - "combined", - nil, - }, - } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - execute := &hook.CombinedExecute{ - Executers: test.executers, - } - result := execute.GetType() - if !cmp.Equal(test.expected, result) { - t.Errorf("metrics mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) - } - }) - } -} diff --git a/internal/hook/http/http.go b/internal/hook/http/http.go index 36dd7bf..99cac47 100644 --- a/internal/hook/http/http.go +++ b/internal/hook/http/http.go @@ -25,7 +25,7 @@ import ( "strings" "time" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook" + jamiethompsonmev1alpha1 "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/api/v1alpha1" ) // Type http represents an HTTP request @@ -53,7 +53,7 @@ type Execute struct { // ExecuteWithValue executes an HTTP request with the value provided as // parameter, configurable to be either in the body or query string -func (e *Execute) ExecuteWithValue(definition *hook.Definition, value string) (string, error) { +func (e *Execute) ExecuteWithValue(definition *jamiethompsonmev1alpha1.HookDefinition, value string) (string, error) { if definition.HTTP == nil { return "", fmt.Errorf("missing required 'http' configuration on hook definition") } diff --git a/internal/hook/http/http_test.go b/internal/hook/http/http_test.go index 901cb19..d950d8c 100644 --- a/internal/hook/http/http_test.go +++ b/internal/hook/http/http_test.go @@ -27,7 +27,7 @@ import ( "time" "github.com/google/go-cmp/cmp" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook" + jamiethompsonmev1alpha1 "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/api/v1alpha1" "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook/http" ) @@ -63,63 +63,63 @@ func TestExecute_ExecuteWithValue(t *testing.T) { description string expected string expectedErr error - definition *hook.Definition + definition *jamiethompsonmev1alpha1.HookDefinition value string execute http.Execute }{ { - "Fail, missing HTTP method configuration", - "", - errors.New(`missing required 'http' configuration on hook definition`), - &hook.Definition{ + description: "Fail, missing HTTP method configuration", + expected: "", + expectedErr: errors.New(`missing required 'http' configuration on hook definition`), + definition: &jamiethompsonmev1alpha1.HookDefinition{ Type: "http", }, - "test", - http.Execute{}, + value: "test", + execute: http.Execute{}, }, { - "Fail, invalid HTTP method", - "", - errors.New(`net/http: invalid method "*?"`), - &hook.Definition{ + description: "Fail, invalid HTTP method", + expected: "", + expectedErr: errors.New(`net/http: invalid method "*?"`), + definition: &jamiethompsonmev1alpha1.HookDefinition{ Type: "http", - HTTP: &hook.HTTP{ + HTTP: &jamiethompsonmev1alpha1.HTTPHook{ Method: "*?", URL: "https://custompodautoscaler.com", }, }, - "test", - http.Execute{}, + value: "test", + execute: http.Execute{}, }, { - "Fail, unknown parameter mode", - "", - errors.New(`unknown parameter mode 'unknown'`), - &hook.Definition{ + description: "Fail, unknown parameter mode", + expected: "", + expectedErr: errors.New(`unknown parameter mode 'unknown'`), + definition: &jamiethompsonmev1alpha1.HookDefinition{ Type: "http", - HTTP: &hook.HTTP{ + HTTP: &jamiethompsonmev1alpha1.HTTPHook{ Method: "GET", URL: "https://custompodautoscaler.com", ParameterMode: "unknown", }, }, - "test", - http.Execute{}, + value: "test", + execute: http.Execute{}, }, { - "Fail, request fail", - "", - errors.New(`Get "https://custompodautoscaler.com?value=test": Test network error!`), - &hook.Definition{ + description: "Fail, request fail", + expected: "", + expectedErr: errors.New(`Get "https://custompodautoscaler.com?value=test": Test network error!`), + definition: &jamiethompsonmev1alpha1.HookDefinition{ Type: "http", - HTTP: &hook.HTTP{ + HTTP: &jamiethompsonmev1alpha1.HTTPHook{ Method: "GET", URL: "https://custompodautoscaler.com", ParameterMode: "query", }, }, - "test", - http.Execute{ + value: "test", + execute: http.Execute{ Client: gohttp.Client{ Transport: &testHTTPClient{ func(req *gohttp.Request) (*gohttp.Response, error) { @@ -130,20 +130,20 @@ func TestExecute_ExecuteWithValue(t *testing.T) { }, }, { - "Fail, timeout", - "", - errors.New(`Get "https://custompodautoscaler.com?value=test": context deadline exceeded`), - &hook.Definition{ + description: "Fail, timeout", + expected: "", + expectedErr: errors.New(`Get "https://custompodautoscaler.com?value=test": context deadline exceeded`), + definition: &jamiethompsonmev1alpha1.HookDefinition{ Type: "http", - HTTP: &hook.HTTP{ + HTTP: &jamiethompsonmev1alpha1.HTTPHook{ Method: "GET", URL: "https://custompodautoscaler.com", ParameterMode: "query", }, Timeout: 5, }, - "test", - http.Execute{ + value: "test", + execute: http.Execute{ Client: func() gohttp.Client { testserver := httptest.NewServer(gohttp.HandlerFunc(func(rw gohttp.ResponseWriter, req *gohttp.Request) { time.Sleep(10 * time.Millisecond) @@ -157,19 +157,19 @@ func TestExecute_ExecuteWithValue(t *testing.T) { }, }, { - "Fail, invalid response body", - "", - errors.New(`Fail to read body!`), - &hook.Definition{ + description: "Fail, invalid response body", + expected: "", + expectedErr: errors.New(`Fail to read body!`), + definition: &jamiethompsonmev1alpha1.HookDefinition{ Type: "http", - HTTP: &hook.HTTP{ + HTTP: &jamiethompsonmev1alpha1.HTTPHook{ Method: "GET", URL: "https://custompodautoscaler.com", ParameterMode: "query", }, }, - "test", - http.Execute{ + value: "test", + execute: http.Execute{ Client: gohttp.Client{ Transport: &testHTTPClient{ func(req *gohttp.Request) (*gohttp.Response, error) { @@ -189,12 +189,12 @@ func TestExecute_ExecuteWithValue(t *testing.T) { }, }, { - "Fail, bad response code", - "", - errors.New(`http request failed, status: [400], response: 'bad request!'`), - &hook.Definition{ + description: "Fail, bad response code", + expected: "", + expectedErr: errors.New(`http request failed, status: [400], response: 'bad request!'`), + definition: &jamiethompsonmev1alpha1.HookDefinition{ Type: "http", - HTTP: &hook.HTTP{ + HTTP: &jamiethompsonmev1alpha1.HTTPHook{ Method: "GET", URL: "https://custompodautoscaler.com", ParameterMode: "query", @@ -204,8 +204,8 @@ func TestExecute_ExecuteWithValue(t *testing.T) { }, }, }, - "test", - http.Execute{ + value: "test", + execute: http.Execute{ Client: gohttp.Client{ Transport: &testHTTPClient{ func(req *gohttp.Request) (*gohttp.Response, error) { @@ -220,12 +220,12 @@ func TestExecute_ExecuteWithValue(t *testing.T) { }, }, { - "Success, POST, body parameter, 3 headers", - "Success!", - nil, - &hook.Definition{ + description: "Success, POST, body parameter, 3 headers", + expected: "Success!", + expectedErr: nil, + definition: &jamiethompsonmev1alpha1.HookDefinition{ Type: "http", - HTTP: &hook.HTTP{ + HTTP: &jamiethompsonmev1alpha1.HTTPHook{ Method: "POST", URL: "https://custompodautoscaler.com", ParameterMode: "body", @@ -240,8 +240,8 @@ func TestExecute_ExecuteWithValue(t *testing.T) { }, }, }, - "test", - http.Execute{ + value: "test", + execute: http.Execute{ Client: gohttp.Client{ Transport: &testHTTPClient{ func(req *gohttp.Request) (*gohttp.Response, error) { @@ -280,12 +280,12 @@ func TestExecute_ExecuteWithValue(t *testing.T) { }, }, { - "Success, GET, query parameter, 1 header", - "Success!", - nil, - &hook.Definition{ + description: "Success, GET, query parameter, 1 header", + expected: "Success!", + expectedErr: nil, + definition: &jamiethompsonmev1alpha1.HookDefinition{ Type: "http", - HTTP: &hook.HTTP{ + HTTP: &jamiethompsonmev1alpha1.HTTPHook{ Method: "GET", URL: "https://custompodautoscaler.com", ParameterMode: "query", @@ -298,8 +298,8 @@ func TestExecute_ExecuteWithValue(t *testing.T) { }, }, }, - "test", - http.Execute{ + value: "test", + execute: http.Execute{ Client: gohttp.Client{ Transport: &testHTTPClient{ func(req *gohttp.Request) (*gohttp.Response, error) { @@ -329,12 +329,12 @@ func TestExecute_ExecuteWithValue(t *testing.T) { }, }, { - "Success, GET, query parameter, 0 headers", - "Success!", - nil, - &hook.Definition{ + description: "Success, GET, query parameter, 0 headers", + expected: "Success!", + expectedErr: nil, + definition: &jamiethompsonmev1alpha1.HookDefinition{ Type: "http", - HTTP: &hook.HTTP{ + HTTP: &jamiethompsonmev1alpha1.HTTPHook{ Method: "GET", URL: "https://custompodautoscaler.com", ParameterMode: "query", @@ -344,8 +344,8 @@ func TestExecute_ExecuteWithValue(t *testing.T) { }, }, }, - "test", - http.Execute{ + value: "test", + execute: http.Execute{ Client: gohttp.Client{ Transport: &testHTTPClient{ func(req *gohttp.Request) (*gohttp.Response, error) { @@ -371,12 +371,12 @@ func TestExecute_ExecuteWithValue(t *testing.T) { }, }, { - "Success, PUT, body parameter, 0 headers", - "Success!", - nil, - &hook.Definition{ + description: "Success, PUT, body parameter, 0 headers", + expected: "Success!", + expectedErr: nil, + definition: &jamiethompsonmev1alpha1.HookDefinition{ Type: "http", - HTTP: &hook.HTTP{ + HTTP: &jamiethompsonmev1alpha1.HTTPHook{ Method: "PUT", URL: "https://custompodautoscaler.com", ParameterMode: "body", @@ -386,8 +386,8 @@ func TestExecute_ExecuteWithValue(t *testing.T) { }, }, }, - "test", - http.Execute{ + value: "test", + execute: http.Execute{ Client: gohttp.Client{ Transport: &testHTTPClient{ func(req *gohttp.Request) (*gohttp.Response, error) { @@ -438,9 +438,9 @@ func TestExecute_GetType(t *testing.T) { execute http.Execute }{ { - "Return type", - "http", - http.Execute{}, + description: "Return type", + expected: "http", + execute: http.Execute{}, }, } for _, test := range tests { diff --git a/internal/hook/shell/shell.go b/internal/hook/shell/shell.go deleted file mode 100644 index 8c1c61a..0000000 --- a/internal/hook/shell/shell.go +++ /dev/null @@ -1,74 +0,0 @@ -// Package shell handles interactions with the OS shell -package shell - -import ( - "bytes" - "fmt" - "os/exec" - "time" - - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook" -) - -// Type shell represents a shell command -const Type = "shell" - -// Command represents the function that builds the exec.Cmd to be used in shell commands. -type command = func(name string, arg ...string) *exec.Cmd - -// Execute represents a way to execute shell commands with values piped to them. -type Execute struct { - Command command -} - -// ExecuteWithValue executes a shell command with a value piped to it. -// If it exits with code 0, no error is returned and the stdout is captured and returned. -// If it exits with code 1, an error is returned and the stderr is captured and returned. -// If the timeout is reached, an error is returned. -func (e *Execute) ExecuteWithValue(definition *hook.Definition, value string) (string, error) { - if definition.Shell == nil { - return "", fmt.Errorf("missing required 'shell' configuration on hook definition") - } - // Build command string with value piped into it - cmd := e.Command(definition.Shell.Entrypoint, definition.Shell.Command...) - - // Set up byte buffer to write values to stdin - inb := bytes.Buffer{} - // No need to catch error, doesn't produce error, instead it panics if buffer too large - inb.WriteString(value) - cmd.Stdin = &inb - - // Set up byte buffers to read stdout and stderr - var outb, errb bytes.Buffer - cmd.Stdout = &outb - cmd.Stderr = &errb - - // Start command - err := cmd.Start() - if err != nil { - return "", err - } - - // Set up channel to wait for command to finish - done := make(chan error) - go func() { done <- cmd.Wait() }() - - // Set up a timeout, after which if the command hasn't finished it will be stopped - timeoutListener := time.After(time.Duration(definition.Timeout) * time.Millisecond) - - select { - case <-timeoutListener: - cmd.Process.Kill() - return "", fmt.Errorf("entrypoint '%s', command '%s' timed out", definition.Shell.Entrypoint, definition.Shell.Command) - case err = <-done: - if err != nil { - return "", fmt.Errorf("%v: %s", err, errb.String()) - } - } - return outb.String(), nil -} - -// GetType returns the shell executer type -func (e *Execute) GetType() string { - return Type -} diff --git a/internal/hook/shell/shell_test.go b/internal/hook/shell/shell_test.go deleted file mode 100644 index 51400fc..0000000 --- a/internal/hook/shell/shell_test.go +++ /dev/null @@ -1,325 +0,0 @@ -/* -Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package shell_test - -import ( - "errors" - "fmt" - "io/ioutil" - "os" - "os/exec" - "strings" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook/shell" -) - -type command func(name string, arg ...string) *exec.Cmd - -type process func(t *testing.T) - -func TestShellProcess(t *testing.T) { - if os.Getenv("GO_TEST_PROCESS") != "1" { - return - } - - processName := strings.Split(os.Args[3], "=")[1] - process := processes[processName] - - if process == nil { - t.Errorf("Process %s not found", processName) - os.Exit(1) - } - - process(t) - - // Process should call os.Exit itself, if not exit with error - os.Exit(1) -} - -func fakeExecCommandAndStart(name string, process process) command { - processes[name] = process - return func(command string, args ...string) *exec.Cmd { - cs := []string{"-test.run=TestShellProcess", "--", fmt.Sprintf("-process=%s", name), command} - cs = append(cs, args...) - cmd := exec.Command(os.Args[0], cs...) - cmd.Env = []string{"GO_TEST_PROCESS=1"} - cmd.Start() - return cmd - } -} - -func fakeExecCommand(name string, process process) command { - processes[name] = process - return func(command string, args ...string) *exec.Cmd { - cs := []string{"-test.run=TestShellProcess", "--", fmt.Sprintf("-process=%s", name), command} - cs = append(cs, args...) - cmd := exec.Command(os.Args[0], cs...) - cmd.Env = []string{"GO_TEST_PROCESS=1"} - return cmd - } -} - -type test struct { - description string - expectedErr error - expected string - definition *hook.Definition - pipeValue string - command command -} - -var tests []test - -var processes map[string]process - -func TestMain(m *testing.M) { - processes = map[string]process{} - tests = []test{ - { - "Missing shell method configuration", - errors.New(`missing required 'shell' configuration on hook definition`), - "", - &hook.Definition{ - Type: "shell", - }, - "test", - exec.Command, - }, - { - "Successful shell command", - nil, - "test std out", - &hook.Definition{ - Type: shell.Type, - Timeout: 100, - Shell: &hook.Shell{ - Command: []string{"command"}, - Entrypoint: "/bin/sh", - }, - }, - "pipe value", - fakeExecCommand("success", func(t *testing.T) { - stdinb, err := ioutil.ReadAll(os.Stdin) - if err != nil { - fmt.Fprint(os.Stderr, err.Error()) - os.Exit(1) - } - - stdin := string(stdinb) - entrypoint := strings.TrimSpace(os.Args[4]) - command := strings.TrimSpace(os.Args[5]) - - // Check entrypoint is correct - if !cmp.Equal(entrypoint, "/bin/sh") { - fmt.Fprintf(os.Stderr, "entrypoint mismatch (-want +got):\n%s", cmp.Diff("/bin/sh", entrypoint)) - os.Exit(1) - } - - // Check command is correct - if !cmp.Equal(command, "command") { - fmt.Fprintf(os.Stderr, "command mismatch (-want +got):\n%s", cmp.Diff("command", command)) - os.Exit(1) - } - - // Check piped value in is correct - if !cmp.Equal(stdin, "pipe value") { - fmt.Fprintf(os.Stderr, "stdin mismatch (-want +got):\n%s", cmp.Diff("pipe value", stdin)) - os.Exit(1) - } - - fmt.Fprint(os.Stdout, "test std out") - os.Exit(0) - }), - }, - { - "Successful shell command, multiple args", - nil, - "test std out", - &hook.Definition{ - Type: shell.Type, - Timeout: 100, - Shell: &hook.Shell{ - Command: []string{"command", "arg1"}, - Entrypoint: "/bin/sh", - }, - }, - "pipe value", - fakeExecCommand("multiple-success", func(t *testing.T) { - stdinb, err := ioutil.ReadAll(os.Stdin) - if err != nil { - fmt.Fprint(os.Stderr, err.Error()) - os.Exit(1) - } - - stdin := string(stdinb) - entrypoint := strings.TrimSpace(os.Args[4]) - command := strings.TrimSpace(strings.Join(os.Args[5:len(os.Args)], " ")) - - // Check entrypoint is correct - if !cmp.Equal(entrypoint, "/bin/sh") { - fmt.Fprintf(os.Stderr, "entrypoint mismatch (-want +got):\n%s", cmp.Diff("/bin/sh", entrypoint)) - os.Exit(1) - } - - // Check command is correct - if !cmp.Equal(command, "command arg1") { - fmt.Fprintf(os.Stderr, "command mismatch (-want +got):\n%s", cmp.Diff("command arg1", command)) - os.Exit(1) - } - - // Check piped value in is correct - if !cmp.Equal(stdin, "pipe value") { - fmt.Fprintf(os.Stderr, "stdin mismatch (-want +got):\n%s", cmp.Diff("pipe value", stdin)) - os.Exit(1) - } - - fmt.Fprint(os.Stdout, "test std out") - os.Exit(0) - }), - }, - { - "Failed shell command", - errors.New("exit status 1: shell command failed"), - "", - &hook.Definition{ - Type: shell.Type, - Timeout: 100, - Shell: &hook.Shell{ - Command: []string{"command"}, - Entrypoint: "/bin/sh", - }, - }, - "pipe value", - fakeExecCommand("failed", func(t *testing.T) { - fmt.Fprint(os.Stderr, "shell command failed") - os.Exit(1) - }), - }, - { - "Failed shell command timeout", - errors.New("entrypoint '/bin/sh', command '[command]' timed out"), - "", - &hook.Definition{ - Type: shell.Type, - Timeout: 5, - Shell: &hook.Shell{ - Command: []string{"command"}, - Entrypoint: "/bin/sh", - }, - }, - "pipe value", - fakeExecCommand("timeout", func(t *testing.T) { - fmt.Fprint(os.Stdout, "test std out") - time.Sleep(10 * time.Millisecond) - os.Exit(0) - }), - }, - { - "Failed shell command timeout, multiple args", - errors.New("entrypoint '/bin/sh', command '[command arg1]' timed out"), - "", - &hook.Definition{ - Type: shell.Type, - Timeout: 5, - Shell: &hook.Shell{ - Command: []string{"command", "arg1"}, - Entrypoint: "/bin/sh", - }, - }, - "pipe value", - fakeExecCommand("timeout", func(t *testing.T) { - fmt.Fprint(os.Stdout, "test std out") - time.Sleep(10 * time.Millisecond) - os.Exit(0) - }), - }, - { - "Failed shell command fail to start", - errors.New("exec: already started"), - "", - &hook.Definition{ - Type: shell.Type, - Timeout: 100, - Shell: &hook.Shell{ - Command: []string{"command"}, - Entrypoint: "/bin/sh", - }, - }, - "pipe value", - fakeExecCommandAndStart("fail to start", func(t *testing.T) { - fmt.Fprint(os.Stdout, "test std out") - os.Exit(0) - }), - }, - } - code := m.Run() - os.Exit(code) -} - -func TestExecute_ExecuteWithValue(t *testing.T) { - equateErrorMessage := cmp.Comparer(func(x, y error) bool { - if x == nil || y == nil { - return x == nil && y == nil - } - return x.Error() == y.Error() - }) - - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - s := &shell.Execute{test.command} - result, err := s.ExecuteWithValue(test.definition, test.pipeValue) - if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { - t.Errorf(result) - t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) - return - } - - if !cmp.Equal(result, test.expected) { - t.Errorf("stdout mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) - } - }) - } -} - -func TestExecute_GetType(t *testing.T) { - var tests = []struct { - description string - expected string - command func(name string, arg ...string) *exec.Cmd - }{ - { - "Return type", - "shell", - nil, - }, - } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - execute := &shell.Execute{ - Command: test.command, - } - result := execute.GetType() - if !cmp.Equal(test.expected, result) { - t.Errorf("metrics mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) - } - }) - } -} diff --git a/internal/prediction/holtwinters/holtwinters.go b/internal/prediction/holtwinters/holtwinters.go index 8da4534..0f7196d 100644 --- a/internal/prediction/holtwinters/holtwinters.go +++ b/internal/prediction/holtwinters/holtwinters.go @@ -22,25 +22,25 @@ import ( "sort" "strconv" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/algorithm" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/config" + jamiethompsonmev1alpha1 "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/api/v1alpha1" "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/stored" ) -// Type HoltWinters is the type of the HoltWinters predicter -const Type = "HoltWinters" - -const algorithmPath = "/app/algorithms/holt_winters/holt_winters.py" +const algorithmPath = "algorithms/holt_winters/holt_winters.py" const ( defaultTimeout = 30000 ) -// Predict provides logic for using Linear Regression to make a prediction +// Runner defines an algorithm runner, allowing algorithms to be run +type AlgorithmRunner interface { + RunAlgorithmWithValue(algorithmPath string, value string, timeout int) (string, error) +} + +// Predict provides logic for using Holt Winters to make a prediction type Predict struct { - Execute hook.Executer - Runner algorithm.Runner + HookExecute hook.Executer + Runner AlgorithmRunner } type holtWintersParametersParameters struct { @@ -59,8 +59,8 @@ type holtWintersParametersParameters struct { } type runTimeTuningFetchHookRequest struct { - Model *config.Model `json:"model"` - Evaluations []*stored.Evaluation `json:"evaluations"` + Model jamiethompsonmev1alpha1.Model `json:"model"` + ReplicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas `json:"replicaHistory"` } type runTimeTuningFetchHookResult struct { @@ -69,15 +69,15 @@ type runTimeTuningFetchHookResult struct { Gamma *float64 `json:"gamma"` } -// GetPrediction uses a linear regression to predict what the replica count should be based on historical evaluations -func (p *Predict) GetPrediction(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { +// GetPrediction uses holt winters to predict what the replica count should be based on historical evaluations +func (p *Predict) GetPrediction(model *jamiethompsonmev1alpha1.Model, replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas) (int32, error) { err := p.validate(model) if err != nil { return 0, err } // Statsmodels requires at least 10 + 2 * (seasonal_periods // 2) to make a prediction with Holt Winters - if len(evaluations) < 10+2*(model.HoltWinters.SeasonalPeriods/2) { + if len(replicaHistory) < 10+2*(model.HoltWinters.SeasonalPeriods/2) { return 0, nil } @@ -89,8 +89,8 @@ func (p *Predict) GetPrediction(model *config.Model, evaluations []*stored.Evalu // Convert request into JSON string request, err := json.Marshal(&runTimeTuningFetchHookRequest{ - Model: model, - Evaluations: evaluations, + Model: *model, + ReplicaHistory: replicaHistory, }) if err != nil { // Should not occur @@ -98,7 +98,7 @@ func (p *Predict) GetPrediction(model *config.Model, evaluations []*stored.Evalu } // Request runtime tuning values - hookResult, err := p.Execute.ExecuteWithValue(model.HoltWinters.RuntimeTuningFetchHook, string(request)) + hookResult, err := p.HookExecute.ExecuteWithValue(model.HoltWinters.RuntimeTuningFetchHook, string(request)) if err != nil { return 0, err } @@ -132,9 +132,9 @@ func (p *Predict) GetPrediction(model *config.Model, evaluations []*stored.Evalu } // Collect data for historical series - series := make([]float64, len(evaluations)) - for i, evaluation := range evaluations { - series[i] = float64(evaluation.Evaluation.TargetReplicas) + series := make([]float64, len(replicaHistory)) + for i, timestampedReplica := range replicaHistory { + series[i] = float64(timestampedReplica.Replicas) } parameters, err := json.Marshal(holtWintersParametersParameters{ @@ -174,36 +174,38 @@ func (p *Predict) GetPrediction(model *config.Model, evaluations []*stored.Evalu return int32(prediction), nil } -// GetIDsToRemove provides the list of stored evaluation IDs to remove, if there are too many stored seasons -// it will remove the oldest seasons -func (p *Predict) GetIDsToRemove(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) { - if model.HoltWinters == nil { - return nil, errors.New("no HoltWinters configuration provided for model") +func (p *Predict) PruneHistory(model *jamiethompsonmev1alpha1.Model, replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas) ([]jamiethompsonmev1alpha1.TimestampedReplicas, error) { + err := p.validate(model) + if err != nil { + return nil, err } // Sort by date created - sort.Slice(evaluations, func(i, j int) bool { - return evaluations[i].Created.Before(evaluations[j].Created) + sort.Slice(replicaHistory, func(i, j int) bool { + return !replicaHistory[i].Time.Before(replicaHistory[j].Time) }) - var markedForRemove []int - // If there are too many stored seasons, remove the oldest ones - seasonsToRemove := len(evaluations)/model.HoltWinters.SeasonalPeriods - model.HoltWinters.StoredSeasons - for i := 0; i < seasonsToRemove; i++ { - for j := 0; j < model.HoltWinters.SeasonalPeriods; j++ { - markedForRemove = append(markedForRemove, evaluations[i+j].ID) - } + + // This rounds down, so if you have 7 replica data, with seasonal period of 3 and only 2 stored seasons it will + // round the 7 / 3 (2.34) down to 2, then it will do 2 - 2 resulting in not removing any seasons + // This is deliberate to allow full seasons to build up before pruning the old ones + numberOfSeasonsToRemove := len(replicaHistory)/model.HoltWinters.SeasonalPeriods - model.HoltWinters.StoredSeasons + numberOfReplicasToRemove := len(replicaHistory) - numberOfSeasonsToRemove*model.HoltWinters.SeasonalPeriods + + for i := len(replicaHistory) - 1; i >= numberOfReplicasToRemove; i-- { + replicaHistory = append(replicaHistory[:i], replicaHistory[i+1:]...) } - return markedForRemove, nil + + return replicaHistory, nil } // GetType returns the type of the Prediction model func (p *Predict) GetType() string { - return Type + return jamiethompsonmev1alpha1.TypeHoltWinters } -func (p *Predict) validate(model *config.Model) error { +func (p *Predict) validate(model *jamiethompsonmev1alpha1.Model) error { if model.HoltWinters == nil { return errors.New("no HoltWinters configuration provided for model") } diff --git a/internal/prediction/holtwinters/holtwinters_test.go b/internal/prediction/holtwinters/holtwinters_test.go index ad97850..9ebcffa 100644 --- a/internal/prediction/holtwinters/holtwinters_test.go +++ b/internal/prediction/holtwinters/holtwinters_test.go @@ -22,14 +22,16 @@ import ( "time" "github.com/google/go-cmp/cmp" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/config" + jamiethompsonmev1alpha1 "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/api/v1alpha1" "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/fake" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook" "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/prediction/holtwinters" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/prediction/linear" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/stored" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +func intPtr(i int) *int { + return &i +} + func float64Ptr(val float64) *float64 { return &val } @@ -43,29 +45,42 @@ func TestPredict_GetPrediction(t *testing.T) { }) var tests = []struct { - description string - expected int32 - expectedErr error - predicter *holtwinters.Predict - model *config.Model - evaluations []*stored.Evaluation + description string + expected int32 + expectedErr error + predicter *holtwinters.Predict + model *jamiethompsonmev1alpha1.Model + replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas }{ { "Fail no HoltWinters configuration", 0, errors.New("no HoltWinters configuration provided for model"), &holtwinters.Predict{}, - &config.Model{}, - []*stored.Evaluation{}, + &jamiethompsonmev1alpha1.Model{}, + []jamiethompsonmev1alpha1.TimestampedReplicas{}, + }, + { + "Fail no trend configuration", + 0, + errors.New("no required 'trend' value provided for model"), + &holtwinters.Predict{}, + &jamiethompsonmev1alpha1.Model{ + Type: jamiethompsonmev1alpha1.TypeLinear, + HoltWinters: &jamiethompsonmev1alpha1.HoltWinters{ + Trend: "", + }, + }, + []jamiethompsonmev1alpha1.TimestampedReplicas{}, }, { "Success, less than 10 + 2 * (seasonal_periods // 2) observations", 0, nil, &holtwinters.Predict{}, - &config.Model{ - Type: linear.Type, - HoltWinters: &config.HoltWinters{ + &jamiethompsonmev1alpha1.Model{ + Type: jamiethompsonmev1alpha1.TypeLinear, + HoltWinters: &jamiethompsonmev1alpha1.HoltWinters{ Alpha: float64Ptr(0.9), Beta: float64Ptr(0.9), Gamma: float64Ptr(0.9), @@ -74,48 +89,34 @@ func TestPredict_GetPrediction(t *testing.T) { Trend: "add", }, }, - []*stored.Evaluation{ + []jamiethompsonmev1alpha1.TimestampedReplicas{ { - Created: time.Now().UTC().Add(time.Duration(-80) * time.Second), - Evaluation: stored.DBEvaluation{ - TargetReplicas: 1, - }, + Time: &metav1.Time{Time: time.Now().UTC().Add(time.Duration(-80) * time.Second)}, + Replicas: 1, }, { - Created: time.Now().UTC().Add(time.Duration(-70) * time.Second), - Evaluation: stored.DBEvaluation{ - TargetReplicas: 3, - }, + Time: &metav1.Time{Time: time.Now().UTC().Add(time.Duration(-70) * time.Second)}, + Replicas: 3, }, { - Created: time.Now().UTC().Add(time.Duration(-60) * time.Second), - Evaluation: stored.DBEvaluation{ - TargetReplicas: 1, - }, + Time: &metav1.Time{Time: time.Now().UTC().Add(time.Duration(-60) * time.Second)}, + Replicas: 1, }, { - Created: time.Now().UTC().Add(time.Duration(-50) * time.Second), - Evaluation: stored.DBEvaluation{ - TargetReplicas: 1, - }, + Time: &metav1.Time{Time: time.Now().UTC().Add(time.Duration(-50) * time.Second)}, + Replicas: 1, }, { - Created: time.Now().UTC().Add(time.Duration(-40) * time.Second), - Evaluation: stored.DBEvaluation{ - TargetReplicas: 3, - }, + Time: &metav1.Time{Time: time.Now().UTC().Add(time.Duration(-40) * time.Second)}, + Replicas: 3, }, { - Created: time.Now().UTC().Add(time.Duration(-30) * time.Second), - Evaluation: stored.DBEvaluation{ - TargetReplicas: 1, - }, + Time: &metav1.Time{Time: time.Now().UTC().Add(time.Duration(-30) * time.Second)}, + Replicas: 1, }, { - Created: time.Now().UTC().Add(time.Duration(-20) * time.Second), - Evaluation: stored.DBEvaluation{ - TargetReplicas: 1, - }, + Time: &metav1.Time{Time: time.Now().UTC().Add(time.Duration(-20) * time.Second)}, + Replicas: 1, }, }, }, @@ -124,17 +125,17 @@ func TestPredict_GetPrediction(t *testing.T) { 0, errors.New("fail runtime fetch"), &holtwinters.Predict{ - Execute: func() *fake.Execute { + HookExecute: func() *fake.Execute { execute := fake.Execute{} - execute.ExecuteWithValueReactor = func(definition *hook.Definition, value string) (string, error) { + execute.ExecuteWithValueReactor = func(definition *jamiethompsonmev1alpha1.HookDefinition, value string) (string, error) { return "", errors.New("fail runtime fetch") } return &execute }(), }, - &config.Model{ - HoltWinters: &config.HoltWinters{ - RuntimeTuningFetchHook: &hook.Definition{ + &jamiethompsonmev1alpha1.Model{ + HoltWinters: &jamiethompsonmev1alpha1.HoltWinters{ + RuntimeTuningFetchHook: &jamiethompsonmev1alpha1.HookDefinition{ Type: "test", Timeout: 2500, }, @@ -143,48 +144,48 @@ func TestPredict_GetPrediction(t *testing.T) { Seasonal: "add", }, }, - []*stored.Evaluation{ + []jamiethompsonmev1alpha1.TimestampedReplicas{ { - ID: 1, + Replicas: 1, }, { - ID: 2, + Replicas: 2, }, { - ID: 3, + Replicas: 3, }, { - ID: 4, + Replicas: 4, }, { - ID: 5, + Replicas: 5, }, { - ID: 6, + Replicas: 6, }, { - ID: 7, + Replicas: 7, }, { - ID: 8, + Replicas: 8, }, { - ID: 9, + Replicas: 9, }, { - ID: 10, + Replicas: 10, }, { - ID: 11, + Replicas: 11, }, { - ID: 12, + Replicas: 12, }, { - ID: 13, + Replicas: 13, }, { - ID: 14, + Replicas: 14, }, }, }, @@ -193,17 +194,17 @@ func TestPredict_GetPrediction(t *testing.T) { 0, errors.New("invalid character 'i' looking for beginning of value"), &holtwinters.Predict{ - Execute: func() *fake.Execute { + HookExecute: func() *fake.Execute { execute := fake.Execute{} - execute.ExecuteWithValueReactor = func(definition *hook.Definition, value string) (string, error) { + execute.ExecuteWithValueReactor = func(definition *jamiethompsonmev1alpha1.HookDefinition, value string) (string, error) { return "invalid json", nil } return &execute }(), }, - &config.Model{ - HoltWinters: &config.HoltWinters{ - RuntimeTuningFetchHook: &hook.Definition{ + &jamiethompsonmev1alpha1.Model{ + HoltWinters: &jamiethompsonmev1alpha1.HoltWinters{ + RuntimeTuningFetchHook: &jamiethompsonmev1alpha1.HookDefinition{ Type: "test", Timeout: 2500, }, @@ -212,48 +213,48 @@ func TestPredict_GetPrediction(t *testing.T) { Seasonal: "add", }, }, - []*stored.Evaluation{ + []jamiethompsonmev1alpha1.TimestampedReplicas{ { - ID: 1, + Replicas: 1, }, { - ID: 2, + Replicas: 2, }, { - ID: 3, + Replicas: 3, }, { - ID: 4, + Replicas: 4, }, { - ID: 5, + Replicas: 5, }, { - ID: 6, + Replicas: 6, }, { - ID: 7, + Replicas: 7, }, { - ID: 8, + Replicas: 8, }, { - ID: 9, + Replicas: 9, }, { - ID: 10, + Replicas: 10, }, { - ID: 11, + Replicas: 11, }, { - ID: 12, + Replicas: 12, }, { - ID: 13, + Replicas: 13, }, { - ID: 14, + Replicas: 14, }, }, }, @@ -262,8 +263,8 @@ func TestPredict_GetPrediction(t *testing.T) { 0, errors.New("no alpha tuning value provided for Holt-Winters prediction"), &holtwinters.Predict{}, - &config.Model{ - HoltWinters: &config.HoltWinters{ + &jamiethompsonmev1alpha1.Model{ + HoltWinters: &jamiethompsonmev1alpha1.HoltWinters{ Beta: float64Ptr(0.9), Gamma: float64Ptr(0.9), SeasonalPeriods: 2, @@ -271,48 +272,48 @@ func TestPredict_GetPrediction(t *testing.T) { Seasonal: "add", }, }, - []*stored.Evaluation{ + []jamiethompsonmev1alpha1.TimestampedReplicas{ { - ID: 1, + Replicas: 1, }, { - ID: 2, + Replicas: 2, }, { - ID: 3, + Replicas: 3, }, { - ID: 4, + Replicas: 4, }, { - ID: 5, + Replicas: 5, }, { - ID: 6, + Replicas: 6, }, { - ID: 7, + Replicas: 7, }, { - ID: 8, + Replicas: 8, }, { - ID: 9, + Replicas: 9, }, { - ID: 10, + Replicas: 10, }, { - ID: 11, + Replicas: 11, }, { - ID: 12, + Replicas: 12, }, { - ID: 13, + Replicas: 13, }, { - ID: 14, + Replicas: 14, }, }, }, @@ -321,8 +322,8 @@ func TestPredict_GetPrediction(t *testing.T) { 0, errors.New("no beta tuning value provided for Holt-Winters prediction"), &holtwinters.Predict{}, - &config.Model{ - HoltWinters: &config.HoltWinters{ + &jamiethompsonmev1alpha1.Model{ + HoltWinters: &jamiethompsonmev1alpha1.HoltWinters{ Alpha: float64Ptr(0.9), Gamma: float64Ptr(0.9), SeasonalPeriods: 2, @@ -330,48 +331,48 @@ func TestPredict_GetPrediction(t *testing.T) { Seasonal: "add", }, }, - []*stored.Evaluation{ + []jamiethompsonmev1alpha1.TimestampedReplicas{ { - ID: 1, + Replicas: 1, }, { - ID: 2, + Replicas: 2, }, { - ID: 3, + Replicas: 3, }, { - ID: 4, + Replicas: 4, }, { - ID: 5, + Replicas: 5, }, { - ID: 6, + Replicas: 6, }, { - ID: 7, + Replicas: 7, }, { - ID: 8, + Replicas: 8, }, { - ID: 9, + Replicas: 9, }, { - ID: 10, + Replicas: 10, }, { - ID: 11, + Replicas: 11, }, { - ID: 12, + Replicas: 12, }, { - ID: 13, + Replicas: 13, }, { - ID: 14, + Replicas: 14, }, }, }, @@ -380,8 +381,8 @@ func TestPredict_GetPrediction(t *testing.T) { 0, errors.New("no gamma tuning value provided for Holt-Winters prediction"), &holtwinters.Predict{}, - &config.Model{ - HoltWinters: &config.HoltWinters{ + &jamiethompsonmev1alpha1.Model{ + HoltWinters: &jamiethompsonmev1alpha1.HoltWinters{ Alpha: float64Ptr(0.9), Beta: float64Ptr(0.9), SeasonalPeriods: 2, @@ -389,48 +390,48 @@ func TestPredict_GetPrediction(t *testing.T) { Seasonal: "add", }, }, - []*stored.Evaluation{ + []jamiethompsonmev1alpha1.TimestampedReplicas{ { - ID: 1, + Replicas: 1, }, { - ID: 2, + Replicas: 2, }, { - ID: 3, + Replicas: 3, }, { - ID: 4, + Replicas: 4, }, { - ID: 5, + Replicas: 5, }, { - ID: 6, + Replicas: 6, }, { - ID: 7, + Replicas: 7, }, { - ID: 8, + Replicas: 8, }, { - ID: 9, + Replicas: 9, }, { - ID: 10, + Replicas: 10, }, { - ID: 11, + Replicas: 11, }, { - ID: 12, + Replicas: 12, }, { - ID: 13, + Replicas: 13, }, { - ID: 14, + Replicas: 14, }, }, }, @@ -445,8 +446,8 @@ func TestPredict_GetPrediction(t *testing.T) { }, }, }, - &config.Model{ - HoltWinters: &config.HoltWinters{ + &jamiethompsonmev1alpha1.Model{ + HoltWinters: &jamiethompsonmev1alpha1.HoltWinters{ Alpha: float64Ptr(0.9), Beta: float64Ptr(0.9), Gamma: float64Ptr(0.9), @@ -454,48 +455,48 @@ func TestPredict_GetPrediction(t *testing.T) { Trend: "additive", }, }, - []*stored.Evaluation{ + []jamiethompsonmev1alpha1.TimestampedReplicas{ { - ID: 1, + Replicas: 1, }, { - ID: 2, + Replicas: 2, }, { - ID: 3, + Replicas: 3, }, { - ID: 4, + Replicas: 4, }, { - ID: 5, + Replicas: 5, }, { - ID: 6, + Replicas: 6, }, { - ID: 7, + Replicas: 7, }, { - ID: 8, + Replicas: 8, }, { - ID: 9, + Replicas: 9, }, { - ID: 10, + Replicas: 10, }, { - ID: 11, + Replicas: 11, }, { - ID: 12, + Replicas: 12, }, { - ID: 13, + Replicas: 13, }, { - ID: 14, + Replicas: 14, }, }, }, @@ -510,8 +511,8 @@ func TestPredict_GetPrediction(t *testing.T) { }, }, }, - &config.Model{ - HoltWinters: &config.HoltWinters{ + &jamiethompsonmev1alpha1.Model{ + HoltWinters: &jamiethompsonmev1alpha1.HoltWinters{ Alpha: float64Ptr(0.9), Beta: float64Ptr(0.9), Gamma: float64Ptr(0.9), @@ -519,48 +520,181 @@ func TestPredict_GetPrediction(t *testing.T) { Trend: "additive", }, }, - []*stored.Evaluation{ + []jamiethompsonmev1alpha1.TimestampedReplicas{ { - ID: 1, + Replicas: 1, }, { - ID: 2, + Replicas: 2, }, { - ID: 3, + Replicas: 3, }, { - ID: 4, + Replicas: 4, }, { - ID: 5, + Replicas: 5, }, { - ID: 6, + Replicas: 6, }, { - ID: 7, + Replicas: 7, }, { - ID: 8, + Replicas: 8, }, { - ID: 9, + Replicas: 9, }, { - ID: 10, + Replicas: 10, }, { - ID: 11, + Replicas: 11, }, { - ID: 12, + Replicas: 12, }, { - ID: 13, + Replicas: 13, }, { - ID: 14, + Replicas: 14, + }, + }, + }, + { + "Success", + 0, + nil, + &holtwinters.Predict{ + Runner: &fake.Run{ + RunAlgorithmWithValueReactor: func(algorithmPath, value string, timeout int) (string, error) { + return "0", nil + }, + }, + }, + &jamiethompsonmev1alpha1.Model{ + HoltWinters: &jamiethompsonmev1alpha1.HoltWinters{ + Alpha: float64Ptr(0.9), + Beta: float64Ptr(0.9), + Gamma: float64Ptr(0.9), + SeasonalPeriods: 2, + Trend: "add", + Seasonal: "add", + }, + }, + []jamiethompsonmev1alpha1.TimestampedReplicas{ + { + Replicas: 1, + }, + { + Replicas: 2, + }, + { + Replicas: 3, + }, + { + Replicas: 4, + }, + { + Replicas: 5, + }, + { + Replicas: 6, + }, + { + Replicas: 7, + }, + { + Replicas: 8, + }, + { + Replicas: 9, + }, + { + Replicas: 10, + }, + { + Replicas: 11, + }, + { + Replicas: 12, + }, + { + Replicas: 13, + }, + { + Replicas: 14, + }, + }, + }, + { + "Success, configure calculation timeout", + 0, + nil, + &holtwinters.Predict{ + Runner: &fake.Run{ + RunAlgorithmWithValueReactor: func(algorithmPath, value string, timeout int) (string, error) { + return "0", nil + }, + }, + }, + &jamiethompsonmev1alpha1.Model{ + HoltWinters: &jamiethompsonmev1alpha1.HoltWinters{ + Alpha: float64Ptr(0.9), + Beta: float64Ptr(0.9), + Gamma: float64Ptr(0.9), + SeasonalPeriods: 2, + Trend: "add", + Seasonal: "add", + }, + CalculationTimeout: intPtr(10), + }, + []jamiethompsonmev1alpha1.TimestampedReplicas{ + { + Replicas: 1, + }, + { + Replicas: 2, + }, + { + Replicas: 3, + }, + { + Replicas: 4, + }, + { + Replicas: 5, + }, + { + Replicas: 6, + }, + { + Replicas: 7, + }, + { + Replicas: 8, + }, + { + Replicas: 9, + }, + { + Replicas: 10, + }, + { + Replicas: 11, + }, + { + Replicas: 12, + }, + { + Replicas: 13, + }, + { + Replicas: 14, }, }, }, @@ -574,17 +708,17 @@ func TestPredict_GetPrediction(t *testing.T) { return "0", nil }, }, - Execute: func() *fake.Execute { + HookExecute: func() *fake.Execute { execute := fake.Execute{} - execute.ExecuteWithValueReactor = func(definition *hook.Definition, value string) (string, error) { + execute.ExecuteWithValueReactor = func(definition *jamiethompsonmev1alpha1.HookDefinition, value string) (string, error) { return `{}`, nil } return &execute }(), }, - &config.Model{ - HoltWinters: &config.HoltWinters{ - RuntimeTuningFetchHook: &hook.Definition{ + &jamiethompsonmev1alpha1.Model{ + HoltWinters: &jamiethompsonmev1alpha1.HoltWinters{ + RuntimeTuningFetchHook: &jamiethompsonmev1alpha1.HookDefinition{ Type: "test", Timeout: 2500, }, @@ -596,48 +730,48 @@ func TestPredict_GetPrediction(t *testing.T) { Seasonal: "add", }, }, - []*stored.Evaluation{ + []jamiethompsonmev1alpha1.TimestampedReplicas{ { - ID: 1, + Replicas: 1, }, { - ID: 2, + Replicas: 2, }, { - ID: 3, + Replicas: 3, }, { - ID: 4, + Replicas: 4, }, { - ID: 5, + Replicas: 5, }, { - ID: 6, + Replicas: 6, }, { - ID: 7, + Replicas: 7, }, { - ID: 8, + Replicas: 8, }, { - ID: 9, + Replicas: 9, }, { - ID: 10, + Replicas: 10, }, { - ID: 11, + Replicas: 11, }, { - ID: 12, + Replicas: 12, }, { - ID: 13, + Replicas: 13, }, { - ID: 14, + Replicas: 14, }, }, }, @@ -651,17 +785,17 @@ func TestPredict_GetPrediction(t *testing.T) { return "2", nil }, }, - Execute: func() *fake.Execute { + HookExecute: func() *fake.Execute { execute := fake.Execute{} - execute.ExecuteWithValueReactor = func(definition *hook.Definition, value string) (string, error) { + execute.ExecuteWithValueReactor = func(definition *jamiethompsonmev1alpha1.HookDefinition, value string) (string, error) { return `{"alpha":0.2, "beta":0.2, "gamma": 0.2}`, nil } return &execute }(), }, - &config.Model{ - HoltWinters: &config.HoltWinters{ - RuntimeTuningFetchHook: &hook.Definition{ + &jamiethompsonmev1alpha1.Model{ + HoltWinters: &jamiethompsonmev1alpha1.HoltWinters{ + RuntimeTuningFetchHook: &jamiethompsonmev1alpha1.HookDefinition{ Type: "test", Timeout: 2500, }, @@ -670,48 +804,48 @@ func TestPredict_GetPrediction(t *testing.T) { Seasonal: "add", }, }, - []*stored.Evaluation{ + []jamiethompsonmev1alpha1.TimestampedReplicas{ { - ID: 1, + Replicas: 1, }, { - ID: 2, + Replicas: 2, }, { - ID: 3, + Replicas: 3, }, { - ID: 4, + Replicas: 4, }, { - ID: 5, + Replicas: 5, }, { - ID: 6, + Replicas: 6, }, { - ID: 7, + Replicas: 7, }, { - ID: 8, + Replicas: 8, }, { - ID: 9, + Replicas: 9, }, { - ID: 10, + Replicas: 10, }, { - ID: 11, + Replicas: 11, }, { - ID: 12, + Replicas: 12, }, { - ID: 13, + Replicas: 13, }, { - ID: 14, + Replicas: 14, }, }, }, @@ -725,17 +859,17 @@ func TestPredict_GetPrediction(t *testing.T) { return "3", nil }, }, - Execute: func() *fake.Execute { + HookExecute: func() *fake.Execute { execute := fake.Execute{} - execute.ExecuteWithValueReactor = func(definition *hook.Definition, value string) (string, error) { + execute.ExecuteWithValueReactor = func(definition *jamiethompsonmev1alpha1.HookDefinition, value string) (string, error) { return `{"alpha":0.2, "beta":0.2}`, nil } return &execute }(), }, - &config.Model{ - HoltWinters: &config.HoltWinters{ - RuntimeTuningFetchHook: &hook.Definition{ + &jamiethompsonmev1alpha1.Model{ + HoltWinters: &jamiethompsonmev1alpha1.HoltWinters{ + RuntimeTuningFetchHook: &jamiethompsonmev1alpha1.HookDefinition{ Type: "test", Timeout: 2500, }, @@ -745,55 +879,55 @@ func TestPredict_GetPrediction(t *testing.T) { Seasonal: "add", }, }, - []*stored.Evaluation{ + []jamiethompsonmev1alpha1.TimestampedReplicas{ { - ID: 1, + Replicas: 1, }, { - ID: 2, + Replicas: 2, }, { - ID: 3, + Replicas: 3, }, { - ID: 4, + Replicas: 4, }, { - ID: 5, + Replicas: 5, }, { - ID: 6, + Replicas: 6, }, { - ID: 7, + Replicas: 7, }, { - ID: 8, + Replicas: 8, }, { - ID: 9, + Replicas: 9, }, { - ID: 10, + Replicas: 10, }, { - ID: 11, + Replicas: 11, }, { - ID: 12, + Replicas: 12, }, { - ID: 13, + Replicas: 13, }, { - ID: 14, + Replicas: 14, }, }, }, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { - result, err := test.predicter.GetPrediction(test.model, test.evaluations) + result, err := test.predicter.GetPrediction(test.model, test.replicaHistory) if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) return @@ -805,7 +939,7 @@ func TestPredict_GetPrediction(t *testing.T) { } } -func TestModelPredict_GetIDsToRemove(t *testing.T) { +func TestModelPredict_PruneHistory(t *testing.T) { equateErrorMessage := cmp.Comparer(func(x, y error) bool { if x == nil || y == nil { return x == nil && y == nil @@ -814,104 +948,301 @@ func TestModelPredict_GetIDsToRemove(t *testing.T) { }) var tests = []struct { - description string - expected []int - expectedErr error - model *config.Model - evaluations []*stored.Evaluation + description string + expected []jamiethompsonmev1alpha1.TimestampedReplicas + expectedErr error + model *jamiethompsonmev1alpha1.Model + replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas }{ { - "Fail no HoltWinters configuration", - nil, - errors.New("no HoltWinters configuration provided for model"), - &config.Model{}, - []*stored.Evaluation{}, + description: "Fail no HoltWinters configuration", + expected: nil, + expectedErr: errors.New("no HoltWinters configuration provided for model"), + model: &jamiethompsonmev1alpha1.Model{}, + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{}, }, { - "Success remove one old season", - []int{14, 12, 10}, - nil, - &config.Model{ - Type: holtwinters.Type, - HoltWinters: &config.HoltWinters{ - SeasonalPeriods: 3, - StoredSeasons: 2, + description: "Fail no trend configuration", + expected: nil, + expectedErr: errors.New("no required 'trend' value provided for model"), + model: &jamiethompsonmev1alpha1.Model{ + HoltWinters: &jamiethompsonmev1alpha1.HoltWinters{}, + }, + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{}, + }, + { + description: "6 in history, seasonal period 2, 3 stored seasons, don't prune", + expected: []jamiethompsonmev1alpha1.TimestampedReplicas{ + { + Replicas: 6, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(6) * time.Second)}, + }, + { + Replicas: 5, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(5) * time.Second)}, + }, + { + Replicas: 4, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(4) * time.Second)}, + }, + { + Replicas: 3, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(3) * time.Second)}, + }, + { + Replicas: 2, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(2) * time.Second)}, + }, + { + Replicas: 1, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(1) * time.Second)}, + }, + }, + expectedErr: nil, + model: &jamiethompsonmev1alpha1.Model{ + HoltWinters: &jamiethompsonmev1alpha1.HoltWinters{ + Trend: "add", + StoredSeasons: 3, + SeasonalPeriods: 2, + }, + }, + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{ + { + Replicas: 6, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(6) * time.Second)}, + }, + { + Replicas: 5, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(5) * time.Second)}, + }, + { + Replicas: 4, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(4) * time.Second)}, + }, + { + Replicas: 3, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(3) * time.Second)}, + }, + { + Replicas: 2, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(2) * time.Second)}, + }, + { + Replicas: 1, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(1) * time.Second)}, }, }, - []*stored.Evaluation{ + }, + { + description: "7 in history, seasonal period 2, 3 stored seasons, don't prune", + expected: []jamiethompsonmev1alpha1.TimestampedReplicas{ + { + Replicas: 7, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(7) * time.Second)}, + }, + { + Replicas: 6, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(6) * time.Second)}, + }, + { + Replicas: 5, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(5) * time.Second)}, + }, + { + Replicas: 4, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(4) * time.Second)}, + }, + { + Replicas: 3, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(3) * time.Second)}, + }, { - ID: 1, - Created: time.Time{}.Add(time.Duration(60) * time.Millisecond), + Replicas: 2, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(2) * time.Second)}, }, { - ID: 2, - Created: time.Time{}.Add(time.Duration(59) * time.Millisecond), + Replicas: 1, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(1) * time.Second)}, }, + }, + expectedErr: nil, + model: &jamiethompsonmev1alpha1.Model{ + HoltWinters: &jamiethompsonmev1alpha1.HoltWinters{ + Trend: "add", + StoredSeasons: 3, + SeasonalPeriods: 2, + }, + }, + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{ { - ID: 3, - Created: time.Time{}.Add(time.Duration(58) * time.Millisecond), + Replicas: 7, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(7) * time.Second)}, }, { - ID: 4, - Created: time.Time{}.Add(time.Duration(57) * time.Millisecond), + Replicas: 6, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(6) * time.Second)}, }, { - ID: 5, - Created: time.Time{}.Add(time.Duration(56) * time.Millisecond), + Replicas: 5, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(5) * time.Second)}, }, { - ID: 6, - Created: time.Time{}.Add(time.Duration(55) * time.Millisecond), + Replicas: 4, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(4) * time.Second)}, }, { - ID: 10, - Created: time.Time{}.Add(time.Duration(54) * time.Millisecond), + Replicas: 3, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(3) * time.Second)}, }, { - ID: 12, - Created: time.Time{}.Add(time.Duration(53) * time.Millisecond), + Replicas: 2, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(2) * time.Second)}, }, { - ID: 14, - Created: time.Time{}.Add(time.Duration(52) * time.Millisecond), + Replicas: 1, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(1) * time.Second)}, }, }, }, { - "Success remove two old seasons", - []int{1, 2}, - nil, - &config.Model{ - Type: holtwinters.Type, - HoltWinters: &config.HoltWinters{ + description: "8 in history, seasonal period 2, 3 stored seasons, prune oldest season", + expected: []jamiethompsonmev1alpha1.TimestampedReplicas{ + { + Replicas: 8, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(8) * time.Second)}, + }, + { + Replicas: 7, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(7) * time.Second)}, + }, + { + Replicas: 6, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(6) * time.Second)}, + }, + { + Replicas: 5, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(5) * time.Second)}, + }, + { + Replicas: 4, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(4) * time.Second)}, + }, + { + Replicas: 3, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(3) * time.Second)}, + }, + }, + expectedErr: nil, + model: &jamiethompsonmev1alpha1.Model{ + HoltWinters: &jamiethompsonmev1alpha1.HoltWinters{ + Trend: "add", + StoredSeasons: 3, + SeasonalPeriods: 2, + }, + }, + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{ + { + Replicas: 8, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(8) * time.Second)}, + }, + { + Replicas: 7, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(7) * time.Second)}, + }, + { + Replicas: 6, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(6) * time.Second)}, + }, + { + Replicas: 5, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(5) * time.Second)}, + }, + { + Replicas: 4, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(4) * time.Second)}, + }, + { + Replicas: 3, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(3) * time.Second)}, + }, + { + Replicas: 2, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(2) * time.Second)}, + }, + { + Replicas: 1, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(1) * time.Second)}, + }, + }, + }, + { + description: "8 in history, unsorted, seasonal period 2, 3 stored seasons, prune oldest season", + expected: []jamiethompsonmev1alpha1.TimestampedReplicas{ + { + Replicas: 8, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(8) * time.Second)}, + }, + { + Replicas: 7, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(7) * time.Second)}, + }, + { + Replicas: 6, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(6) * time.Second)}, + }, + { + Replicas: 5, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(5) * time.Second)}, + }, + { + Replicas: 4, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(4) * time.Second)}, + }, + { + Replicas: 3, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(3) * time.Second)}, + }, + }, + expectedErr: nil, + model: &jamiethompsonmev1alpha1.Model{ + HoltWinters: &jamiethompsonmev1alpha1.HoltWinters{ + Trend: "add", + StoredSeasons: 3, SeasonalPeriods: 2, - StoredSeasons: 2, }, }, - []*stored.Evaluation{ + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{ + { + Replicas: 6, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(6) * time.Second)}, + }, + { + Replicas: 1, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(1) * time.Second)}, + }, { - ID: 1, - Created: time.Time{}.Add(time.Duration(55) * time.Millisecond), + Replicas: 7, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(7) * time.Second)}, }, { - ID: 2, - Created: time.Time{}.Add(time.Duration(56) * time.Millisecond), + Replicas: 2, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(2) * time.Second)}, }, { - ID: 3, - Created: time.Time{}.Add(time.Duration(57) * time.Millisecond), + Replicas: 5, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(5) * time.Second)}, }, { - ID: 4, - Created: time.Time{}.Add(time.Duration(58) * time.Millisecond), + Replicas: 4, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(4) * time.Second)}, }, { - ID: 5, - Created: time.Time{}.Add(time.Duration(59) * time.Millisecond), + Replicas: 3, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(3) * time.Second)}, }, { - ID: 6, - Created: time.Time{}.Add(time.Duration(60) * time.Millisecond), + Replicas: 8, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(8) * time.Second)}, }, }, }, @@ -919,7 +1250,7 @@ func TestModelPredict_GetIDsToRemove(t *testing.T) { for _, test := range tests { t.Run(test.description, func(t *testing.T) { predicter := &holtwinters.Predict{} - result, err := predicter.GetIDsToRemove(test.model, test.evaluations) + result, err := predicter.PruneHistory(test.model, test.replicaHistory) if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) return diff --git a/internal/prediction/linear/linear.go b/internal/prediction/linear/linear.go index 5f7a6ad..9afcd75 100644 --- a/internal/prediction/linear/linear.go +++ b/internal/prediction/linear/linear.go @@ -22,23 +22,18 @@ import ( "sort" "strconv" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/algorithm" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/config" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/stored" + jamiethompsonmev1alpha1 "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/api/v1alpha1" ) const ( defaultTimeout = 30000 ) -// Type linear is the type of the linear predicter -const Type = "Linear" - -const algorithmPath = "/app/algorithms/linear_regression/linear_regression.py" +const algorithmPath = "algorithms/linear_regression/linear_regression.py" type linearRegressionParameters struct { - LookAhead int `json:"lookAhead"` - Evaluations []*stored.Evaluation `json:"evaluations"` + LookAhead int `json:"lookAhead"` + ReplicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas `json:"replicaHistory"` } // Config represents a linear regression prediction model configuration @@ -47,30 +42,35 @@ type Config struct { LookAhead int `yaml:"lookAhead"` } +// Runner defines an algorithm runner, allowing algorithms to be run +type AlgorithmRunner interface { + RunAlgorithmWithValue(algorithmPath string, value string, timeout int) (string, error) +} + // Predict provides logic for using Linear Regression to make a prediction type Predict struct { - Runner algorithm.Runner + Runner AlgorithmRunner } // GetPrediction uses a linear regression to predict what the replica count should be based on historical evaluations -func (p *Predict) GetPrediction(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { +func (p *Predict) GetPrediction(model *jamiethompsonmev1alpha1.Model, replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas) (int32, error) { if model.Linear == nil { return 0, errors.New("no Linear configuration provided for model") } - if len(evaluations) == 0 { + if len(replicaHistory) == 0 { return 0, errors.New("no evaluations provided for Linear regression model") } - if len(evaluations) == 1 { + if len(replicaHistory) == 1 { // If only 1 evaluation is provided do not try and calculate using the linear regression model, just return // the target replicas from the only evaluation - return evaluations[0].Evaluation.TargetReplicas, nil + return replicaHistory[0].Replicas, nil } parameters, err := json.Marshal(linearRegressionParameters{ - LookAhead: model.Linear.LookAhead, - Evaluations: evaluations, + LookAhead: model.Linear.LookAhead, + ReplicaHistory: replicaHistory, }) if err != nil { // Should not occur, panic @@ -95,29 +95,29 @@ func (p *Predict) GetPrediction(model *config.Model, evaluations []*stored.Evalu return int32(prediction), nil } -// GetIDsToRemove provides the list of stored evaluation IDs to remove, if there are too many stored values -// it will remove the oldest ones -func (p *Predict) GetIDsToRemove(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) { +func (p *Predict) PruneHistory(model *jamiethompsonmev1alpha1.Model, replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas) ([]jamiethompsonmev1alpha1.TimestampedReplicas, error) { if model.Linear == nil { return nil, errors.New("no Linear configuration provided for model") } - // Sort by date created - sort.Slice(evaluations, func(i, j int) bool { - return evaluations[i].Created.Before(evaluations[j].Created) + if len(replicaHistory) < model.Linear.HistorySize { + return replicaHistory, nil + } + + // Sort by date created, newest first + sort.Slice(replicaHistory, func(i, j int) bool { + return !replicaHistory[i].Time.Before(replicaHistory[j].Time) }) - var markedForRemove []int - // Remove any expired values - if len(evaluations) > model.Linear.StoredValues { - // Remove oldest to fit into requirements - for i := 0; i < len(evaluations)-model.Linear.StoredValues; i++ { - markedForRemove = append(markedForRemove, evaluations[i].ID) - } + + // Remove oldest to fit into requirements, have to loop from the end to allow deletion without affecting indices + for i := len(replicaHistory) - 1; i >= model.Linear.HistorySize; i-- { + replicaHistory = append(replicaHistory[:i], replicaHistory[i+1:]...) } - return markedForRemove, nil + + return replicaHistory, nil } // GetType returns the type of the Prediction model func (p *Predict) GetType() string { - return Type + return jamiethompsonmev1alpha1.TypeLinear } diff --git a/internal/prediction/linear/linear_test.go b/internal/prediction/linear/linear_test.go index 60c3678..f615a9f 100644 --- a/internal/prediction/linear/linear_test.go +++ b/internal/prediction/linear/linear_test.go @@ -22,12 +22,16 @@ import ( "time" "github.com/google/go-cmp/cmp" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/config" + jamiethompsonmev1alpha1 "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/api/v1alpha1" "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/fake" "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/prediction/linear" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/stored" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +func intPtr(i int) *int { + return &i +} + func TestPredict_GetPrediction(t *testing.T) { equateErrorMessage := cmp.Comparer(func(x, y error) bool { if x == nil || y == nil { @@ -37,141 +41,166 @@ func TestPredict_GetPrediction(t *testing.T) { }) var tests = []struct { - description string - expected int32 - expectedErr error - predicter *linear.Predict - model *config.Model - evaluations []*stored.Evaluation + description string + expected int32 + expectedErr error + predicter *linear.Predict + model *jamiethompsonmev1alpha1.Model + replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas }{ { - "Fail no Linear configuration", - 0, - errors.New("no Linear configuration provided for model"), - &linear.Predict{}, - &config.Model{}, - []*stored.Evaluation{}, + description: "Fail no Linear configuration", + expected: 0, + expectedErr: errors.New("no Linear configuration provided for model"), + predicter: &linear.Predict{}, + model: &jamiethompsonmev1alpha1.Model{}, + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{}, }, { - "Fail no evaluations", - 0, - errors.New("no evaluations provided for Linear regression model"), - &linear.Predict{}, - &config.Model{ - Type: linear.Type, - Linear: &config.Linear{ - StoredValues: 5, - LookAhead: 0, + description: "Fail no evaluations", + expected: 0, + expectedErr: errors.New("no evaluations provided for Linear regression model"), + predicter: &linear.Predict{}, + model: &jamiethompsonmev1alpha1.Model{ + Type: jamiethompsonmev1alpha1.TypeLinear, + Linear: &jamiethompsonmev1alpha1.Linear{ + HistorySize: 5, + LookAhead: 0, }, }, - []*stored.Evaluation{}, + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{}, }, { - "Success, only one evaluation, return without the prediction", - 32, - nil, - &linear.Predict{}, - &config.Model{ - Type: linear.Type, - Linear: &config.Linear{ - StoredValues: 5, - LookAhead: 0, + description: "Success, only one evaluation, return without the prediction", + expected: 32, + expectedErr: nil, + predicter: &linear.Predict{}, + model: &jamiethompsonmev1alpha1.Model{ + Type: jamiethompsonmev1alpha1.TypeLinear, + Linear: &jamiethompsonmev1alpha1.Linear{ + HistorySize: 5, + LookAhead: 0, }, }, - []*stored.Evaluation{ + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{ { - ID: 0, - Evaluation: stored.DBEvaluation{ - TargetReplicas: 32, - }, + Replicas: 32, }, }, }, { - "Fail execution of algorithm fails", - 0, - errors.New("algorithm fail"), - &linear.Predict{ + description: "Fail execution of algorithm fails", + expected: 0, + expectedErr: errors.New("algorithm fail"), + predicter: &linear.Predict{ Runner: &fake.Run{ RunAlgorithmWithValueReactor: func(algorithmPath, value string, timeout int) (string, error) { return "", errors.New("algorithm fail") }, }, }, - &config.Model{ - Type: linear.Type, - Linear: &config.Linear{ - StoredValues: 5, - LookAhead: 0, + model: &jamiethompsonmev1alpha1.Model{ + Type: jamiethompsonmev1alpha1.TypeLinear, + Linear: &jamiethompsonmev1alpha1.Linear{ + HistorySize: 5, + LookAhead: 0, }, }, - []*stored.Evaluation{ + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{ { - ID: 0, + Replicas: 1, }, { - ID: 1, + Replicas: 2, }, }, }, { - "Fail algorithm returns non-integer castable value", - 0, - errors.New(`strconv.Atoi: parsing "invalid": invalid syntax`), - &linear.Predict{ + description: "Fail algorithm returns non-integer castable value", + expected: 0, + expectedErr: errors.New(`strconv.Atoi: parsing "invalid": invalid syntax`), + predicter: &linear.Predict{ Runner: &fake.Run{ RunAlgorithmWithValueReactor: func(algorithmPath, value string, timeout int) (string, error) { return "invalid", nil }, }, }, - &config.Model{ - Type: linear.Type, - Linear: &config.Linear{ - StoredValues: 5, - LookAhead: 0, + model: &jamiethompsonmev1alpha1.Model{ + Type: jamiethompsonmev1alpha1.TypeLinear, + Linear: &jamiethompsonmev1alpha1.Linear{ + HistorySize: 5, + LookAhead: 0, }, }, - []*stored.Evaluation{ + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{ { - ID: 0, + Replicas: 1, }, { - ID: 1, + Replicas: 2, }, }, }, { - "Success", - 3, - nil, - &linear.Predict{ + description: "Success", + expected: 3, + expectedErr: nil, + predicter: &linear.Predict{ Runner: &fake.Run{ RunAlgorithmWithValueReactor: func(algorithmPath, value string, timeout int) (string, error) { return "3", nil }, }, }, - &config.Model{ - Type: linear.Type, - Linear: &config.Linear{ - StoredValues: 5, - LookAhead: 0, + model: &jamiethompsonmev1alpha1.Model{ + Type: jamiethompsonmev1alpha1.TypeLinear, + Linear: &jamiethompsonmev1alpha1.Linear{ + HistorySize: 5, + LookAhead: 0, }, }, - []*stored.Evaluation{ + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{ { - ID: 0, + Replicas: 1, }, { - ID: 1, + Replicas: 2, + }, + }, + }, + { + description: "Success, use custom timeout", + expected: 3, + expectedErr: nil, + predicter: &linear.Predict{ + Runner: &fake.Run{ + RunAlgorithmWithValueReactor: func(algorithmPath, value string, timeout int) (string, error) { + return "3", nil + }, + }, + }, + model: &jamiethompsonmev1alpha1.Model{ + Type: jamiethompsonmev1alpha1.TypeLinear, + Linear: &jamiethompsonmev1alpha1.Linear{ + HistorySize: 5, + LookAhead: 0, + }, + CalculationTimeout: intPtr(10), + }, + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{ + { + Replicas: 1, + }, + { + Replicas: 2, }, }, }, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { - result, err := test.predicter.GetPrediction(test.model, test.evaluations) + result, err := test.predicter.GetPrediction(test.model, test.replicaHistory) if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) return @@ -183,7 +212,7 @@ func TestPredict_GetPrediction(t *testing.T) { } } -func TestModelPredict_GetIDsToRemove(t *testing.T) { +func TestModelPredict_PruneHistory(t *testing.T) { equateErrorMessage := cmp.Comparer(func(x, y error) bool { if x == nil || y == nil { return x == nil && y == nil @@ -192,54 +221,104 @@ func TestModelPredict_GetIDsToRemove(t *testing.T) { }) var tests = []struct { - description string - expected []int - expectedErr error - model *config.Model - evaluations []*stored.Evaluation + description string + expected []jamiethompsonmev1alpha1.TimestampedReplicas + expectedErr error + model *jamiethompsonmev1alpha1.Model + replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas }{ { - "Fail no Linear configuration", - nil, - errors.New("no Linear configuration provided for model"), - &config.Model{}, - []*stored.Evaluation{}, + description: "Fail no Linear configuration", + expected: nil, + expectedErr: errors.New("no Linear configuration provided for model"), + model: &jamiethompsonmev1alpha1.Model{}, + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{}, }, { - "3 IDs too many, mark 3 for removal", - []int{5, 3, 8}, - nil, - &config.Model{ - Linear: &config.Linear{ - StoredValues: 3, + description: "Only 3 in history, max size 4", + expected: []jamiethompsonmev1alpha1.TimestampedReplicas{ + { + Replicas: 4, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(6) * time.Second)}, + }, + { + Replicas: 2, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(5) * time.Second)}, + }, + { + Replicas: 1, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(4) * time.Second)}, + }, + }, + expectedErr: nil, + model: &jamiethompsonmev1alpha1.Model{ + Linear: &jamiethompsonmev1alpha1.Linear{ + HistorySize: 4, + }, + }, + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{ + { + Replicas: 4, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(6) * time.Second)}, + }, + { + Replicas: 2, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(5) * time.Second)}, + }, + { + Replicas: 1, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(4) * time.Second)}, + }, + }, + }, + { + description: "3 too many, remove oldest 3", + expected: []jamiethompsonmev1alpha1.TimestampedReplicas{ + { + Replicas: 4, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(6) * time.Second)}, + }, + { + Replicas: 2, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(5) * time.Second)}, + }, + { + Replicas: 1, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(4) * time.Second)}, + }, + }, + expectedErr: nil, + model: &jamiethompsonmev1alpha1.Model{ + Linear: &jamiethompsonmev1alpha1.Linear{ + HistorySize: 3, }, }, - []*stored.Evaluation{ + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{ { - ID: 1, - Created: time.Time{}.Add(time.Duration(4) * time.Second), + Replicas: 1, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(4) * time.Second)}, }, { - ID: 2, - Created: time.Time{}.Add(time.Duration(5) * time.Second), + Replicas: 2, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(5) * time.Second)}, }, // START OLDEST { - ID: 5, - Created: time.Time{}.Add(time.Duration(1) * time.Second), + Replicas: 5, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(1) * time.Second)}, }, { - ID: 3, - Created: time.Time{}.Add(time.Duration(2) * time.Second), + Replicas: 3, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(2) * time.Second)}, }, { - ID: 8, - Created: time.Time{}.Add(time.Duration(3) * time.Second), + Replicas: 8, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(3) * time.Second)}, }, // END OLDEST { - ID: 4, - Created: time.Time{}.Add(time.Duration(6) * time.Second), + Replicas: 4, + Time: &metav1.Time{Time: time.Time{}.Add(time.Duration(6) * time.Second)}, }, }, }, @@ -247,7 +326,7 @@ func TestModelPredict_GetIDsToRemove(t *testing.T) { for _, test := range tests { t.Run(test.description, func(t *testing.T) { predicter := &linear.Predict{} - result, err := predicter.GetIDsToRemove(test.model, test.evaluations) + result, err := predicter.PruneHistory(test.model, test.replicaHistory) if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) return @@ -265,8 +344,8 @@ func TestPredict_GetType(t *testing.T) { expected string }{ { - "Successful get type", - "Linear", + description: "Successful get type", + expected: "Linear", }, } for _, test := range tests { diff --git a/internal/prediction/prediction.go b/internal/prediction/prediction.go index 2247d21..7108bd1 100644 --- a/internal/prediction/prediction.go +++ b/internal/prediction/prediction.go @@ -20,14 +20,13 @@ package prediction import ( "fmt" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/config" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/stored" + jamiethompsonmev1alpha1 "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/api/v1alpha1" ) // Predicter is an interface providing methods for making a prediction based on a model, a time to predict and values type Predicter interface { - GetPrediction(model *config.Model, evaluations []*stored.Evaluation) (int32, error) - GetIDsToRemove(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) + GetPrediction(model *jamiethompsonmev1alpha1.Model, replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas) (int32, error) + PruneHistory(model *jamiethompsonmev1alpha1.Model, replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas) ([]jamiethompsonmev1alpha1.TimestampedReplicas, error) GetType() string } @@ -38,20 +37,20 @@ type ModelPredict struct { } // GetPrediction generates a prediction for any model that the ModelPredict has been set up to use -func (m *ModelPredict) GetPrediction(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { +func (m *ModelPredict) GetPrediction(model *jamiethompsonmev1alpha1.Model, replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas) (int32, error) { for _, predicter := range m.Predicters { if predicter.GetType() == model.Type { - return predicter.GetPrediction(model, evaluations) + return predicter.GetPrediction(model, replicaHistory) } } return 0, fmt.Errorf("unknown model type '%s'", model.Type) } // GetIDsToRemove finds the appropriate logic for the model and gets a list of stored IDs to remove -func (m *ModelPredict) GetIDsToRemove(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) { +func (m *ModelPredict) PruneHistory(model *jamiethompsonmev1alpha1.Model, replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas) ([]jamiethompsonmev1alpha1.TimestampedReplicas, error) { for _, predicter := range m.Predicters { if predicter.GetType() == model.Type { - return predicter.GetIDsToRemove(model, evaluations) + return predicter.PruneHistory(model, replicaHistory) } } return nil, fmt.Errorf("unknown model type '%s'", model.Type) diff --git a/internal/prediction/prediction_test.go b/internal/prediction/prediction_test.go index 41e013e..bbd7473 100644 --- a/internal/prediction/prediction_test.go +++ b/internal/prediction/prediction_test.go @@ -21,10 +21,10 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/config" + jamiethompsonmev1alpha1 "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/api/v1alpha1" "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/fake" "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/prediction" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/stored" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestModelPredict_GetPrediction(t *testing.T) { @@ -36,33 +36,33 @@ func TestModelPredict_GetPrediction(t *testing.T) { }) var tests = []struct { - description string - expected int32 - expectedErr error - predicters []prediction.Predicter - model *config.Model - evaluations []*stored.Evaluation + description string + expected int32 + expectedErr error + predicters []prediction.Predicter + model *jamiethompsonmev1alpha1.Model + replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas }{ { - "Unknown model type", - 0, - errors.New(`unknown model type 'invalid'`), - []prediction.Predicter{}, - &config.Model{ + description: "Unknown model type", + expected: 0, + expectedErr: errors.New(`unknown model type 'invalid'`), + predicters: []prediction.Predicter{}, + model: &jamiethompsonmev1alpha1.Model{ Type: "invalid", - Linear: &config.Linear{ + Linear: &jamiethompsonmev1alpha1.Linear{ LookAhead: 10, }, }, - []*stored.Evaluation{}, + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{}, }, { - "GetIDsToRemove fail child predictor", - 0, - errors.New("fail to get prediction from child"), - []prediction.Predicter{ + description: "GetIDsToRemove fail child predictor", + expected: 0, + expectedErr: errors.New("fail to get prediction from child"), + predicters: []prediction.Predicter{ &fake.Predicter{ - GetPredictionReactor: func(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { + GetPredictionReactor: func(model *jamiethompsonmev1alpha1.Model, evaluations []jamiethompsonmev1alpha1.TimestampedReplicas) (int32, error) { return 0, errors.New("fail to get prediction from child") }, GetTypeReactor: func() string { @@ -70,20 +70,20 @@ func TestModelPredict_GetPrediction(t *testing.T) { }, }, }, - &config.Model{ + model: &jamiethompsonmev1alpha1.Model{ Type: "test", - Linear: &config.Linear{ + Linear: &jamiethompsonmev1alpha1.Linear{ LookAhead: 10, }}, - []*stored.Evaluation{}, + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{}, }, { - "Successful prediction, single available model", - 3, - nil, - []prediction.Predicter{ + description: "Successful prediction, single available model", + expected: 3, + expectedErr: nil, + predicters: []prediction.Predicter{ &fake.Predicter{ - GetPredictionReactor: func(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { + GetPredictionReactor: func(model *jamiethompsonmev1alpha1.Model, evaluations []jamiethompsonmev1alpha1.TimestampedReplicas) (int32, error) { return 3, nil }, GetTypeReactor: func() string { @@ -91,20 +91,20 @@ func TestModelPredict_GetPrediction(t *testing.T) { }, }, }, - &config.Model{ + model: &jamiethompsonmev1alpha1.Model{ Type: "test", - Linear: &config.Linear{ + Linear: &jamiethompsonmev1alpha1.Linear{ LookAhead: 10, }}, - []*stored.Evaluation{}, + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{}, }, { - "Successful prediction, three available models", - 5, - nil, - []prediction.Predicter{ + description: "Successful prediction, three available models", + expected: 5, + expectedErr: nil, + predicters: []prediction.Predicter{ &fake.Predicter{ - GetPredictionReactor: func(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { + GetPredictionReactor: func(model *jamiethompsonmev1alpha1.Model, evaluations []jamiethompsonmev1alpha1.TimestampedReplicas) (int32, error) { return 0, errors.New("incorrect model") }, GetTypeReactor: func() string { @@ -112,7 +112,7 @@ func TestModelPredict_GetPrediction(t *testing.T) { }, }, &fake.Predicter{ - GetPredictionReactor: func(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { + GetPredictionReactor: func(model *jamiethompsonmev1alpha1.Model, evaluations []jamiethompsonmev1alpha1.TimestampedReplicas) (int32, error) { return 0, errors.New("incorrect model") }, GetTypeReactor: func() string { @@ -120,7 +120,7 @@ func TestModelPredict_GetPrediction(t *testing.T) { }, }, &fake.Predicter{ - GetPredictionReactor: func(model *config.Model, evaluations []*stored.Evaluation) (int32, error) { + GetPredictionReactor: func(model *jamiethompsonmev1alpha1.Model, evaluations []jamiethompsonmev1alpha1.TimestampedReplicas) (int32, error) { return 5, nil }, GetTypeReactor: func() string { @@ -128,13 +128,13 @@ func TestModelPredict_GetPrediction(t *testing.T) { }, }, }, - &config.Model{ + model: &jamiethompsonmev1alpha1.Model{ Type: "test", - Linear: &config.Linear{ + Linear: &jamiethompsonmev1alpha1.Linear{ LookAhead: 10, }, }, - []*stored.Evaluation{}, + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{}, }, } for _, test := range tests { @@ -142,7 +142,7 @@ func TestModelPredict_GetPrediction(t *testing.T) { predicter := &prediction.ModelPredict{ Predicters: test.predicters, } - result, err := predicter.GetPrediction(test.model, test.evaluations) + result, err := predicter.GetPrediction(test.model, test.replicaHistory) if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) return @@ -154,7 +154,7 @@ func TestModelPredict_GetPrediction(t *testing.T) { } } -func TestModelPredict_GetIDsToRemove(t *testing.T) { +func TestModelPredict_PruneHistory(t *testing.T) { equateErrorMessage := cmp.Comparer(func(x, y error) bool { if x == nil || y == nil { return x == nil && y == nil @@ -163,105 +163,132 @@ func TestModelPredict_GetIDsToRemove(t *testing.T) { }) var tests = []struct { - description string - expected []int - expectedErr error - predicters []prediction.Predicter - model *config.Model - evaluations []*stored.Evaluation + description string + expected []jamiethompsonmev1alpha1.TimestampedReplicas + expectedErr error + predicters []prediction.Predicter + model *jamiethompsonmev1alpha1.Model + replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas }{ { - "Unknown model type", - nil, - errors.New(`unknown model type 'invalid'`), - []prediction.Predicter{}, - &config.Model{ + description: "Unknown model type", + expected: nil, + expectedErr: errors.New(`unknown model type 'invalid'`), + predicters: []prediction.Predicter{}, + model: &jamiethompsonmev1alpha1.Model{ Type: "invalid", - Linear: &config.Linear{ + Linear: &jamiethompsonmev1alpha1.Linear{ LookAhead: 10, }, }, - []*stored.Evaluation{}, + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{}, }, { - "GetIDsToRemove fail child predictor", - nil, - errors.New("fail to get IDs to remove"), - []prediction.Predicter{ - &fake.Predicter{ - GetIDsToRemoveReactor: func(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) { - return nil, errors.New("fail to get IDs to remove") - }, - GetTypeReactor: func() string { - return "test" - }, + description: "Successful PruneHistory, single available model, remove last", + expected: []jamiethompsonmev1alpha1.TimestampedReplicas{ + { + Time: &v1.Time{}, + Replicas: 1, + }, + { + Time: &v1.Time{}, + Replicas: 2, }, }, - &config.Model{ - Type: "test", - Linear: &config.Linear{ - LookAhead: 10, - }}, - []*stored.Evaluation{}, - }, - { - "Successful GetIDsToRemove, single available model", - []int{5}, - nil, - []prediction.Predicter{ + expectedErr: nil, + predicters: []prediction.Predicter{ &fake.Predicter{ - GetIDsToRemoveReactor: func(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) { - return []int{5}, nil + PruneHistoryReactor: func(model *jamiethompsonmev1alpha1.Model, replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas) ([]jamiethompsonmev1alpha1.TimestampedReplicas, error) { + replicaHistory = replicaHistory[:len(replicaHistory)-1] + return replicaHistory, nil }, GetTypeReactor: func() string { return "test" }, }, }, - &config.Model{ + model: &jamiethompsonmev1alpha1.Model{ Type: "test", - Linear: &config.Linear{ + Linear: &jamiethompsonmev1alpha1.Linear{ LookAhead: 10, }}, - []*stored.Evaluation{}, + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{ + { + Time: &v1.Time{}, + Replicas: 1, + }, + { + Time: &v1.Time{}, + Replicas: 2, + }, + { + Time: &v1.Time{}, + Replicas: 3, + }, + }, }, { - "Successful GetIDsToRemove, three available models", - []int{5}, - nil, - []prediction.Predicter{ + description: "Successful PruneHistory, three available models, remove last", + expected: []jamiethompsonmev1alpha1.TimestampedReplicas{ + { + Time: &v1.Time{}, + Replicas: 1, + }, + { + Time: &v1.Time{}, + Replicas: 2, + }, + }, + expectedErr: nil, + predicters: []prediction.Predicter{ &fake.Predicter{ - GetIDsToRemoveReactor: func(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) { - return []int{1}, nil + PruneHistoryReactor: func(model *jamiethompsonmev1alpha1.Model, replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas) ([]jamiethompsonmev1alpha1.TimestampedReplicas, error) { + replicaHistory = replicaHistory[:len(replicaHistory)-1] + return replicaHistory, nil }, GetTypeReactor: func() string { return "incorrect-model" }, }, &fake.Predicter{ - GetIDsToRemoveReactor: func(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) { - return []int{2}, nil + PruneHistoryReactor: func(model *jamiethompsonmev1alpha1.Model, replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas) ([]jamiethompsonmev1alpha1.TimestampedReplicas, error) { + replicaHistory = replicaHistory[:len(replicaHistory)-1] + return replicaHistory, nil }, GetTypeReactor: func() string { return "incorrect-model-2" }, }, &fake.Predicter{ - GetIDsToRemoveReactor: func(model *config.Model, evaluations []*stored.Evaluation) ([]int, error) { - return []int{5}, nil + PruneHistoryReactor: func(model *jamiethompsonmev1alpha1.Model, replicaHistory []jamiethompsonmev1alpha1.TimestampedReplicas) ([]jamiethompsonmev1alpha1.TimestampedReplicas, error) { + replicaHistory = replicaHistory[:len(replicaHistory)-1] + return replicaHistory, nil }, GetTypeReactor: func() string { return "test" }, }, }, - &config.Model{ + model: &jamiethompsonmev1alpha1.Model{ Type: "test", - Linear: &config.Linear{ + Linear: &jamiethompsonmev1alpha1.Linear{ LookAhead: 10, }, }, - []*stored.Evaluation{}, + replicaHistory: []jamiethompsonmev1alpha1.TimestampedReplicas{ + { + Time: &v1.Time{}, + Replicas: 1, + }, + { + Time: &v1.Time{}, + Replicas: 2, + }, + { + Time: &v1.Time{}, + Replicas: 3, + }, + }, }, } for _, test := range tests { @@ -269,7 +296,7 @@ func TestModelPredict_GetIDsToRemove(t *testing.T) { predicter := &prediction.ModelPredict{ Predicters: test.predicters, } - result, err := predicter.GetIDsToRemove(test.model, test.evaluations) + result, err := predicter.PruneHistory(test.model, test.replicaHistory) if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) return @@ -287,8 +314,8 @@ func TestModelPredict_GetType(t *testing.T) { expected string }{ { - "Successful get type", - "Model", + description: "Successful get type", + expected: "Model", }, } for _, test := range tests { diff --git a/internal/stored/stored.go b/internal/stored/stored.go deleted file mode 100644 index a593de1..0000000 --- a/internal/stored/stored.go +++ /dev/null @@ -1,182 +0,0 @@ -/* -Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package stored provides interfacing methods for updating/retrieving data from the local sqlite3 database -package stored - -import ( - "database/sql" - "encoding/json" - "fmt" - "log" - "time" - - "github.com/jthomperoo/custom-pod-autoscaler/v2/evaluate" -) - -// DBEvaluation is an evaluation that can be stored in a SQL db, it is exactly the same as the evaluation but just with -// a Scan method implementation for marshalling from JSON -type DBEvaluation evaluate.Evaluation - -// Scan is used when reading the DBEvaluation field in from the SQL db, marshalling from a JSON string -func (d *DBEvaluation) Scan(src interface{}) error { - strValue, ok := src.(string) - - if !ok { - return fmt.Errorf("evaluation field must be a string, got %T instead", src) - } - - return json.Unmarshal([]byte(strValue), d) -} - -// Evaluation is an evaluation stored in the database, with an ID and a timestamp -type Evaluation struct { - ID int `db:"id" json:"id"` - Created time.Time `db:"created" json:"created"` - Evaluation DBEvaluation `db:"val" json:"val"` -} - -// Model is a model stored in the database, with an ID and the number of intervals since it was last used -type Model struct { - ID int `db:"id" json:"id"` - Name string `db:"model_name" json:"model_name"` - IntervalsPassed int `db:"intervals_passed" json:"intervals_passed"` -} - -// Storer allows updating/retrieving evaluations from a data source -type Storer interface { - GetEvaluation(model string) ([]*Evaluation, error) - AddEvaluation(model string, evaluation *evaluate.Evaluation) error - RemoveEvaluation(id int) error - GetModel(model string) (*Model, error) - UpdateModel(model string, intervalsPassed int) error -} - -// LocalStore is the implementation of a Store for updating/retrieving evaluations from a SQL db -type LocalStore struct { - DB *sql.DB -} - -// GetEvaluation returns all evaluations associated with a model -func (s *LocalStore) GetEvaluation(model string) ([]*Evaluation, error) { - rows, err := s.DB.Query("SELECT evaluation.id, evaluation.created, evaluation.val FROM evaluation, model WHERE evaluation.model_id = model.id AND model.model_name = ?;", model) - if err != nil { - return nil, err - } - - var saved []*Evaluation - for rows.Next() { - evaluation := Evaluation{} - err = rows.Scan(&evaluation.ID, &evaluation.Created, &evaluation.Evaluation) - if err != nil { - return nil, err - } - saved = append(saved, &evaluation) - } - - return saved, nil -} - -// AddEvaluation inserts a new evaluation associated with a model -func (s *LocalStore) AddEvaluation(model string, evaluation *evaluate.Evaluation) error { - // Get the model for the evaluation - modelObj, err := s.GetModel(model) - if err != nil { - return err - } - - // Convert evaluation into JSON - evaluationJSON, err := json.Marshal(evaluation) - if err != nil { - // Should not occur, panic - log.Panic(err) - } - - // Start db transaction - tx, err := s.DB.Begin() - if err != nil { - return err - } - // Prepare insert statement - stmt, err := tx.Prepare("INSERT INTO evaluation(model_id, val, created) VALUES(?, ?, ?);") - if err != nil { - return err - } - defer stmt.Close() - // Execute statement - stmt.Exec(modelObj.ID, string(evaluationJSON), time.Now().UTC().Unix()) - // Execute transaction, return any error - return tx.Commit() -} - -// RemoveEvaluation deletes an evaluation by the ID provided -func (s *LocalStore) RemoveEvaluation(id int) error { - _, err := s.DB.Exec("DELETE FROM evaluation WHERE id = $1;", id) - return err -} - -// GetModel gets the model with the name provided -func (s *LocalStore) GetModel(model string) (*Model, error) { - row := s.DB.QueryRow("SELECT id, model_name, intervals_passed FROM model WHERE model_name = ?;", model) - - result := Model{} - err := row.Scan(&result.ID, &result.Name, &result.IntervalsPassed) - if err != nil { - return nil, err - } - - return &result, nil -} - -// UpdateModel updates the model in the DB with the name provided, setting the intervals_passed value -func (s *LocalStore) UpdateModel(model string, intervalsPassed int) error { - modelObj, err := s.GetModel(model) - if err == sql.ErrNoRows { - // Start db transaction - tx, err := s.DB.Begin() - if err != nil { - return err - } - // Prepare insert statement - stmt, err := tx.Prepare("INSERT INTO model(model_name, intervals_passed) VALUES(?, ?);") - if err != nil { - return err - } - defer stmt.Close() - // Execute statement - stmt.Exec(model, intervalsPassed) - // Execute transaction, return any error - return tx.Commit() - } - if err != nil { - return err - } - // Start db transaction - tx, err := s.DB.Begin() - if err != nil { - return err - } - // Prepare insert statement - stmt, err := tx.Prepare("UPDATE model SET intervals_passed = ? WHERE model.id = ?;") - if err != nil { - return err - } - defer stmt.Close() - // Execute statement - stmt.Exec(intervalsPassed, modelObj.ID) - // Execute transaction, return any error - return tx.Commit() -} diff --git a/main.go b/main.go index 0b95ee5..5f58b41 100644 --- a/main.go +++ b/main.go @@ -1,5 +1,5 @@ /* -Copyright 2022 The Predictive Horizontal Pod Autoscaler Authors. +Copyright 2022. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,333 +14,154 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Predictive Horizontal Pod Autoscaler provides executable Predictive Horizontal Pod Autoscaler logic, which -// can be built into a Custom Pod Autoscaler. -// The Horizontal Pod Autoscaler has two modes, metric gathering and evaluation. -// Metric mode gathers metrics, taking in a resource to get the metrics for and outputting -// these metrics as serialised JSON. -// Evaluation mode makes decisions on how many replicas a resource should have, taking in -// metrics and outputting evaluation decisions as seralised JSON. -// The predictive element uses past evaluations to predict how to scale in the future. package main import ( - "bytes" - "context" - "database/sql" - "encoding/json" "flag" - "fmt" - "io" - "io/ioutil" - "log" "os" - "os/exec" - "path/filepath" - "strings" "time" - "github.com/golang-migrate/migrate/v4" - "github.com/golang-migrate/migrate/v4/database/sqlite3" - _ "github.com/golang-migrate/migrate/v4/source/file" // Driver for loading evaluations from file system - cpametric "github.com/jthomperoo/custom-pod-autoscaler/v2/metric" + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + _ "k8s.io/client-go/plugin/pkg/client/auth" + "k8s.io/client-go/restmapper" + "k8s.io/client-go/scale" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + cached "k8s.io/client-go/discovery/cached" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "github.com/jthomperoo/k8shorizmetrics" - "github.com/jthomperoo/k8shorizmetrics/metrics" "github.com/jthomperoo/k8shorizmetrics/metricsclient" "github.com/jthomperoo/k8shorizmetrics/podsclient" + + jamiethompsonmev1alpha1 "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/api/v1alpha1" "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/algorithm" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/config" - phpaevaluate "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/evaluate" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook" + "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/controllers" "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook/http" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook/shell" "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/prediction" "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/prediction/holtwinters" "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/prediction/linear" - "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/stored" - _ "github.com/mattn/go-sqlite3" // Driver for sqlite3 database appsv1 "k8s.io/api/apps/v1" - autoscalingv1 "k8s.io/api/autoscaling/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/yaml" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/client-go/restmapper" - k8sscale "k8s.io/client-go/scale" + //+kubebuilder:scaffold:imports ) -const ( - predictiveConfigEnvName = "predictiveConfig" +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") ) -// EvaluateSpec represents the information fed to the evaluator -type EvaluateSpec struct { - Metrics []*cpametric.ResourceMetric `json:"metrics"` - UnstructuredResource unstructured.Unstructured `json:"resource"` - Resource metav1.Object `json:"-"` - RunType string `json:"runType"` -} +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) -// MetricSpec represents the information fed to the metric gatherer -type MetricSpec struct { - UnstructuredResource unstructured.Unstructured `json:"resource"` - Resource metav1.Object `json:"-"` - RunType string `json:"runType"` + utilruntime.Must(jamiethompsonmev1alpha1.AddToScheme(scheme)) + //+kubebuilder:scaffold:scheme } func main() { - stdin, err := ioutil.ReadAll(os.Stdin) - if err != nil { - log.Fatal(err) - } - - modePtr := flag.String("mode", "no_mode", "command mode, either metric or evaluate") + var metricsAddr string + var enableLeaderElection bool + var probeAddr string + flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) flag.Parse() - // Get config env variable - predictiveConfigEnv, exists := os.LookupEnv(predictiveConfigEnvName) - if !exists { - log.Fatal("Missing required predictiveConfig environment variable.") - } - - // Parse config - predictiveConfig, err := config.LoadConfig(strings.NewReader(predictiveConfigEnv)) - if err != nil { - log.Fatal(err) - } - - switch *modePtr { - case "metric": - gather(bytes.NewReader(stdin), predictiveConfig) - case "evaluate": - evaluate(bytes.NewReader(stdin), predictiveConfig) - case "setup": - setup(predictiveConfig) - default: - log.Fatalf("Unknown command mode: %s", *modePtr) - } -} - -func setup(predictiveConfig *config.Config) { - // Make folders for storing DB - err := os.MkdirAll(filepath.Dir(predictiveConfig.DBPath), os.ModePerm) - if err != nil { - log.Fatal(err) - } - - // Open DB connection - db, err := sql.Open("sqlite3", predictiveConfig.DBPath) - if err != nil { - log.Fatal(err) - } - defer db.Close() - - // Get DB driver - driver, err := sqlite3.WithInstance(db, &sqlite3.Config{}) - if err != nil { - log.Fatal(err) - } - - // Load migrations - m, err := migrate.NewWithDatabaseInstance(fmt.Sprintf("file:///%s", predictiveConfig.MigrationPath), "evaluations", driver) - if err != nil { - log.Fatal(err) - } - - // Apply migrations - err = m.Up() - if err != nil { - log.Fatal(err) - } -} - -func gather(stdin io.Reader, predictiveConfig *config.Config) { - var spec MetricSpec - err := yaml.NewYAMLOrJSONDecoder(stdin, 10).Decode(&spec) - if err != nil { - log.Fatal(err) - } - - if len(predictiveConfig.Metrics) == 0 { - log.Fatal("Metric specs not supplied") - } - - clusterConfig, clientset, err := getKubernetesClients() - if err != nil { - log.Fatal(err) - os.Exit(1) - } - - scale, err := getScaleSubResource(clientset, &spec.UnstructuredResource) - if err != nil { - log.Fatal(err) - os.Exit(1) - } - - metricsclient := metricsclient.NewClient(clusterConfig, clientset.Discovery()) - podsclient := &podsclient.OnDemandPodLister{ - Clientset: clientset, - } - cpuInitializationPeriod := time.Duration(predictiveConfig.CPUInitializationPeriod) * time.Second - initialReadinessDelay := time.Duration(predictiveConfig.InitialReadinessDelay) * time.Second - - // Create metric gatherer, with required clients and configuration - gatherer := k8shorizmetrics.NewGatherer(metricsclient, podsclient, cpuInitializationPeriod, initialReadinessDelay) - - selector, err := labels.Parse(scale.Status.Selector) - if err != nil { - log.Fatal(err) + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + MetricsBindAddress: metricsAddr, + Port: 9443, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "a07591f8.jamiethompson.me", + // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily + // when the Manager ends. This requires the binary to immediately end when the + // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly + // speeds up voluntary leader transitions as the new leader don't have to wait + // LeaseDuration time first. + // + // In the default scaffold provided, the program ends immediately after + // the manager stops, so would be fine to enable this option. However, + // if you are doing or is intended to do any operation such as perform cleanups + // after the manager stops then its usage might be unsafe. + // LeaderElectionReleaseOnCancel: true, + }) + if err != nil { + setupLog.Error(err, "unable to start manager") os.Exit(1) } - // Get metrics for deployment - metrics, err := gatherer.Gather(predictiveConfig.Metrics, scale.GetNamespace(), selector) - if err != nil { - log.Fatal(err) - } - - // Marshal metrics into JSON - jsonMetrics, err := json.Marshal(metrics) - if err != nil { - log.Fatal(err) - } - - // Write serialised metrics to stdout - fmt.Print(string(jsonMetrics)) -} - -func evaluate(stdin io.Reader, predictiveConfig *config.Config) { - // Open DB connection - db, err := sql.Open("sqlite3", predictiveConfig.DBPath) - if err != nil { - log.Fatal(err) - } - defer db.Close() - - var spec EvaluateSpec - err = yaml.NewYAMLOrJSONDecoder(stdin, 10).Decode(&spec) + clientSet, err := kubernetes.NewForConfig(mgr.GetConfig()) if err != nil { - log.Fatal(err) - } - - _, clientset, err := getKubernetesClients() - if err != nil { - log.Fatal(err) + setupLog.Error(err, "unable to get clientset from config") os.Exit(1) } - scale, err := getScaleSubResource(clientset, &spec.UnstructuredResource) + scaleClient, err := scale.NewForConfig(mgr.GetConfig(), + restmapper.NewDeferredDiscoveryRESTMapper(cached.NewMemCacheClient(clientSet.Discovery())), + dynamic.LegacyAPIPathResolverFunc, + scale.NewDiscoveryScaleKindResolver(clientSet.Discovery())) if err != nil { - log.Fatal(err) + setupLog.Error(err, "unable to set up scale client from config") os.Exit(1) } - var combinedMetrics []*metrics.Metric - err = yaml.NewYAMLOrJSONDecoder(strings.NewReader(spec.Metrics[0].Value), 10).Decode(&combinedMetrics) - if err != nil { - log.Fatal(err) - } - - shellExec := &shell.Execute{ - Command: exec.Command, - } - + metricsclient := metricsclient.NewClient(mgr.GetConfig(), clientSet.Discovery()) + podsclient := &podsclient.OnDemandPodLister{Clientset: clientSet} + cpuInitializationPeriod := time.Duration(300) * time.Second + initialReadinessDelay := time.Duration(30) * time.Second + tolerance := 0.1 + pyRunner := algorithm.NewAlgorithmPython() httpExec := &http.Execute{} - // Combine executers - combinedExecute := &hook.CombinedExecute{ - Executers: []hook.Executer{ - shellExec, - httpExec, - }, - } - - algorithmRunner := &algorithm.Run{ - Executer: shellExec, - } - - // K8s evaluator - k8sevaluator := k8shorizmetrics.NewEvaluator(predictiveConfig.Tolerance) - - // Set up evaluator - evaluator := &phpaevaluate.PredictiveEvaluate{ - HPAEvaluator: k8sevaluator, - Store: &stored.LocalStore{ - DB: db, - }, - Predicters: []prediction.Predicter{ - &linear.Predict{ - Runner: algorithmRunner, - }, - &holtwinters.Predict{ - Runner: algorithmRunner, - Execute: combinedExecute, + if err = (&controllers.PredictiveHorizontalPodAutoscalerReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + ScaleClient: scaleClient, + Gatherer: *k8shorizmetrics.NewGatherer(metricsclient, podsclient, cpuInitializationPeriod, initialReadinessDelay), + Evaluator: *k8shorizmetrics.NewEvaluator(tolerance), + Predicter: &prediction.ModelPredict{ + Predicters: []prediction.Predicter{ + &linear.Predict{ + Runner: pyRunner, + }, + &holtwinters.Predict{ + HookExecute: httpExec, + Runner: pyRunner, + }, }, }, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "PredictiveHorizontalPodAutoscaler") + os.Exit(1) } + //+kubebuilder:scaffold:builder - // Get evaluation - result, err := evaluator.GetEvaluation(predictiveConfig, combinedMetrics, scale.Spec.Replicas, spec.RunType) - if err != nil { - log.Fatal(err) - } - - // Marshal evaluation into JSON - jsonEvaluation, err := json.Marshal(result) - if err != nil { - log.Fatal(err) - } - - fmt.Print(string(jsonEvaluation)) -} - -func getScaleSubResource(clientset *kubernetes.Clientset, resource *unstructured.Unstructured) (*autoscalingv1.Scale, error) { - groupResources, err := restmapper.GetAPIGroupResources(clientset.Discovery()) - if err != nil { - return nil, err - } - - scaleClient := k8sscale.New( - clientset.RESTClient(), - restmapper.NewDiscoveryRESTMapper(groupResources), - dynamic.LegacyAPIPathResolverFunc, - k8sscale.NewDiscoveryScaleKindResolver( - clientset.Discovery(), - ), - ) - - resourceGV, err := schema.ParseGroupVersion(resource.GetAPIVersion()) - if err != nil { - return nil, err - } - - targetGR := schema.GroupResource{ - Group: resourceGV.Group, - Resource: resource.GetKind(), - } - - scale, err := scaleClient.Scales(resource.GetNamespace()).Get(context.Background(), targetGR, resource.GetName(), metav1.GetOptions{}) - if err != nil { - return nil, err + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) } - - return scale, nil -} - -func getKubernetesClients() (*rest.Config, *kubernetes.Clientset, error) { - clusterConfig, err := rest.InClusterConfig() - if err != nil { - return nil, nil, err + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) } - clientset, err := kubernetes.NewForConfig(clusterConfig) - if err != nil { - return nil, nil, err - + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) } - - return clusterConfig, clientset, nil } diff --git a/mkdocs.yml b/mkdocs.yml index 82d8606..93787ca 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,21 +2,10 @@ site_name: Predictive Horizontal Pod Autoscaler theme: material markdown_extensions: - mdx_truly_sane_lists - - codehilite + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences plugins: - search -nav: -- Home: 'index.md' -- 'User Guide': - - 'Getting Started': 'user-guide/getting-started.md' - - Metrics: 'user-guide/metrics.md' - - Models: 'user-guide/models.md' - - 'Sync Period': 'user-guide/sync-period.md' - - 'Downscale Stabilization': 'user-guide/downscale-stabilization.md' - - 'Minimum and Maximum Replicas': 'user-guide/min-max-replicas.md' - - 'Initial Readiness Delay': 'user-guide/initial-readiness-delay.md' - - 'CPU Initialization Period': 'user-guide/cpu-initialization-period.md' - - Tolerance: 'user-guide/tolerance.md' - - Hooks: 'user-guide/hooks.md' -- Reference: - - Configuration: 'reference/configuration.md' diff --git a/readthedocs.yml b/readthedocs.yml index bb7dbb9..fb53a90 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -6,6 +6,6 @@ mkdocs: formats: all python: - version: 3.7 + version: 3.8 install: - requirements: docs/requirements.txt diff --git a/sql/1_init_schema.up.sql b/sql/1_init_schema.up.sql deleted file mode 100644 index 3c902c8..0000000 --- a/sql/1_init_schema.up.sql +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2021 The Predictive Horizontal Pod Autoscaler Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -CREATE TABLE model ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - model_name TEXT NOT NULL, - intervals_passed INTEGER NOT NULL -); - -CREATE TABLE evaluation ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - model_id INTEGER NOT NULL, - val TEXT NOT NULL, - created DATETIME NOT NULL, - CONSTRAINT fk_model FOREIGN KEY(model_id) REFERENCES model (id) -); diff --git a/tools.go b/tools.go new file mode 100644 index 0000000..0ad86ee --- /dev/null +++ b/tools.go @@ -0,0 +1,8 @@ +//go:build tools + +package main + +import ( + _ "github.com/cosmtrek/air" + _ "honnef.co/go/tools/cmd/staticcheck" +)