diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 36c79ef..df26e7a 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -9,11 +9,14 @@ jobs:
name: Build
runs-on: ubuntu-latest
steps:
- - name: Set up Go 1.13
+ - name: Set up Go 1.16.2
uses: actions/setup-go@v1
with:
- go-version: 1.13
+ go-version: 1.16.2
id: go
+ - uses: actions/setup-python@v2
+ with:
+ python-version: '3.8.5'
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: Lint, test and build
@@ -23,8 +26,13 @@ jobs:
GO111MODULE=off go get -u golang.org/x/lint/golint
# Lint and test
go mod vendor
+ pip install -r requirements-dev.txt
+ pip install -r algorithms/requirements.txt
make lint
- make unittest
+ make beautify
+ # Exit if after beautification 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
@@ -37,9 +45,16 @@ jobs:
if: github.repository == 'jthomperoo/predictive-horizontal-pod-autoscaler'
with:
token: ${{secrets.CODECOV_TOKEN}}
- file: ./unit_cover.out
+ file: ./application_coverage.out
+ flags: unittests
+ name: application-unittests
+ - uses: codecov/codecov-action@v1.0.3
+ if: github.repository == 'jthomperoo/predictive-horizontal-pod-autoscaler'
+ with:
+ token: ${{secrets.CODECOV_TOKEN}}
+ file: ./algorithm_coverage.out
flags: unittests
- name: predictive-horizontal-pod-autoscaler-unittests
+ name: algorithm-unittests
- name: Deploy
env:
DOCKER_USER: ${{ secrets.DOCKER_USER }}
diff --git a/.gitignore b/.gitignore
index fdfac0b..afe40fb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
/vendor/
/dist/
-*.out
\ No newline at end of file
+*.out
+__pycache__
+.algorithm_coverage
+.coverage
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..0f50506
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,595 @@
+[MASTER]
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code.
+extension-pkg-whitelist=
+
+# Specify a score threshold to be exceeded before program exits with error.
+fail-under=10
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Add files or directories matching the regex patterns to the blacklist. The
+# regex matches against base names, not paths.
+ignore-patterns=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
+# number of processors available to use.
+jobs=1
+
+# Control the amount of potential inferred values when inferring a single
+# object. This can help the performance when dealing with large functions or
+# complex, nested conditions.
+limit-inference-results=100
+
+# List of plugins (as comma separated values of python module names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# When enabled, pylint would attempt to guess common misconfiguration and emit
+# user-friendly hints instead of false-positive error messages.
+suggestion-mode=yes
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
+confidence=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once). You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use "--disable=all --enable=classes
+# --disable=W".
+disable=print-statement,
+ parameter-unpacking,
+ unpacking-in-except,
+ old-raise-syntax,
+ backtick,
+ long-suffix,
+ old-ne-operator,
+ old-octal-literal,
+ import-star-module-level,
+ non-ascii-bytes-literal,
+ raw-checker-failed,
+ bad-inline-option,
+ locally-disabled,
+ file-ignored,
+ suppressed-message,
+ useless-suppression,
+ deprecated-pragma,
+ use-symbolic-message-instead,
+ apply-builtin,
+ basestring-builtin,
+ buffer-builtin,
+ cmp-builtin,
+ coerce-builtin,
+ execfile-builtin,
+ file-builtin,
+ long-builtin,
+ raw_input-builtin,
+ reduce-builtin,
+ standarderror-builtin,
+ unicode-builtin,
+ xrange-builtin,
+ coerce-method,
+ delslice-method,
+ getslice-method,
+ setslice-method,
+ no-absolute-import,
+ old-division,
+ dict-iter-method,
+ dict-view-method,
+ next-method-called,
+ metaclass-assignment,
+ indexing-exception,
+ raising-string,
+ reload-builtin,
+ oct-method,
+ hex-method,
+ nonzero-method,
+ cmp-method,
+ input-builtin,
+ round-builtin,
+ intern-builtin,
+ unichr-builtin,
+ map-builtin-not-iterating,
+ zip-builtin-not-iterating,
+ range-builtin-not-iterating,
+ filter-builtin-not-iterating,
+ using-cmp-argument,
+ eq-without-hash,
+ div-method,
+ idiv-method,
+ rdiv-method,
+ exception-message-attribute,
+ invalid-str-codec,
+ sys-max-int,
+ bad-python3-import,
+ deprecated-string-function,
+ deprecated-str-translate-call,
+ deprecated-itertools-function,
+ deprecated-types-field,
+ next-method-defined,
+ dict-items-not-iterating,
+ dict-keys-not-iterating,
+ dict-values-not-iterating,
+ deprecated-operator-function,
+ deprecated-urllib-function,
+ xreadlines-attribute,
+ deprecated-sys-function,
+ exception-escape,
+ comprehension-escape
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+enable=c-extension-no-member
+
+
+[REPORTS]
+
+# Python expression which should return a score less than or equal to 10. You
+# have access to the variables 'error', 'warning', 'refactor', and 'convention'
+# which contain the number of messages in each category, as well as 'statement'
+# which is the total number of statements analyzed. This score is used by the
+# global evaluation report (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details.
+#msg-template=
+
+# Set the output format. Available formats are text, parseable, colorized, json
+# and msvs (visual studio). You can also give a reporter class, e.g.
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Tells whether to display a full report or only the messages.
+reports=no
+
+# Activate the evaluation score.
+score=yes
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+# Complete name of functions that never returns. When checking for
+# inconsistent-return-statements if a never returning function is called then
+# it will be considered as an explicit return statement and no message will be
+# printed.
+never-returning-functions=sys.exit
+
+
+[SIMILARITIES]
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+# Minimum lines number of a similarity.
+min-similarity-lines=20
+
+
+[BASIC]
+
+# Naming style matching correct argument names.
+argument-naming-style=snake_case
+
+# Regular expression matching correct argument names. Overrides argument-
+# naming-style.
+#argument-rgx=
+
+# Naming style matching correct attribute names.
+attr-naming-style=snake_case
+
+# Regular expression matching correct attribute names. Overrides attr-naming-
+# style.
+#attr-rgx=
+
+# Bad variable names which should always be refused, separated by a comma.
+bad-names=foo,
+ bar,
+ baz,
+ toto,
+ tutu,
+ tata
+
+# Bad variable names regexes, separated by a comma. If names match any regex,
+# they will always be refused
+bad-names-rgxs=
+
+# Naming style matching correct class attribute names.
+class-attribute-naming-style=any
+
+# Regular expression matching correct class attribute names. Overrides class-
+# attribute-naming-style.
+#class-attribute-rgx=
+
+# Naming style matching correct class names.
+class-naming-style=PascalCase
+
+# Regular expression matching correct class names. Overrides class-naming-
+# style.
+#class-rgx=
+
+# Naming style matching correct constant names.
+const-naming-style=UPPER_CASE
+
+# Regular expression matching correct constant names. Overrides const-naming-
+# style.
+#const-rgx=
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+# Naming style matching correct function names.
+function-naming-style=snake_case
+
+# Regular expression matching correct function names. Overrides function-
+# naming-style.
+#function-rgx=
+
+# Good variable names which should always be accepted, separated by a comma.
+good-names=i,
+ j,
+ k,
+ ex,
+ Run,
+ _
+
+# Good variable names regexes, separated by a comma. If names match any regex,
+# they will always be accepted
+good-names-rgxs=
+
+# Include a hint for the correct naming format with invalid-name.
+include-naming-hint=no
+
+# Naming style matching correct inline iteration names.
+inlinevar-naming-style=any
+
+# Regular expression matching correct inline iteration names. Overrides
+# inlinevar-naming-style.
+#inlinevar-rgx=
+
+# Naming style matching correct method names.
+method-naming-style=snake_case
+
+# Regular expression matching correct method names. Overrides method-naming-
+# style.
+#method-rgx=
+
+# Naming style matching correct module names.
+module-naming-style=snake_case
+
+# Regular expression matching correct module names. Overrides module-naming-
+# style.
+#module-rgx=
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+# These decorators are taken in consideration only for invalid-name.
+property-classes=abc.abstractproperty
+
+# Naming style matching correct variable names.
+variable-naming-style=snake_case
+
+# Regular expression matching correct variable names. Overrides variable-
+# naming-style.
+#variable-rgx=
+
+
+[VARIABLES]
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid defining new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,
+ _cb
+
+# A regular expression matching the name of dummy variables (i.e. expected to
+# not be used).
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore.
+ignored-argument-names=_.*|^ignored_|^unused_
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
+
+
+[FORMAT]
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )??$
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+# Maximum number of characters on a single line.
+max-line-length=120
+
+# Maximum number of lines in a module.
+max-module-lines=1000
+
+# List of optional constructs for which whitespace checking is disabled. `dict-
+# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
+# `trailing-comma` allows a space between comma and closing bracket: (a, ).
+# `empty-line` allows space-only lines.
+no-space-check=trailing-comma,
+ dict-separator
+
+# Allow the body of a class to be on the same line as the declaration if body
+# contains single statement.
+single-line-class-stmt=no
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,
+ XXX,
+ TODO
+
+# Regular expression of note tags to take in consideration.
+#notes-rgx=
+
+
+[LOGGING]
+
+# The type of string formatting that logging methods do. `old` means using %
+# formatting, `new` is for `{}` formatting.
+logging-format-style=old
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format.
+logging-modules=logging
+
+
+[STRING]
+
+# This flag controls whether inconsistent-quotes generates a warning when the
+# character used as a quote delimiter is used inconsistently within a module.
+check-quote-consistency=no
+
+# This flag controls whether the implicit-str-concat should generate a warning
+# on implicit string concatenation in sequences defined over several lines.
+check-str-concat-over-line-jumps=no
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# Tells whether to warn about missing members when the owner of the attribute
+# is inferred to be None.
+ignore-none=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis). It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+# List of decorators that change the signature of a decorated function.
+signature-mutators=
+
+
+[SPELLING]
+
+# Limits count of emitted suggestions for spelling mistakes.
+max-spelling-suggestions=4
+
+# Spelling dictionary name. Available dictionaries: none. To make it work,
+# install the python-enchant package.
+spelling-dict=
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains the private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to the private dictionary (see the
+# --spelling-private-dict-file option) instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,
+ __new__,
+ setUp,
+ __post_init__
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,
+ _fields,
+ _replace,
+ _source,
+ _make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=cls
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method.
+max-args=5
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=20
+
+# Maximum number of boolean expressions in an if statement (see R0916).
+max-bool-expr=5
+
+# Maximum number of branch for function / method body.
+max-branches=12
+
+# Maximum number of locals for function / method body.
+max-locals=15
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body.
+max-returns=6
+
+# Maximum number of statements in function / method body.
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+
+[IMPORTS]
+
+# List of modules that can be imported at any level, not just the top level
+# one.
+allow-any-import-level=
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+# Deprecated modules which should not be used, separated by a comma.
+deprecated-modules=optparse,tkinter.tix
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled).
+ext-import-graph=
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled).
+import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled).
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant
+
+# Couples of modules and preferred modules, separated by a comma.
+preferred-modules=
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "BaseException, Exception".
+overgeneral-exceptions=BaseException,
+ Exception
diff --git a/.style.yapf b/.style.yapf
new file mode 100644
index 0000000..c9a88d5
--- /dev/null
+++ b/.style.yapf
@@ -0,0 +1,3 @@
+[style]
+based_on_style = pep8
+column_limit = 120
diff --git a/CHANGELOG.md b/CHANGELOG.md
index be7bd3d..6925ff8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,13 +1,27 @@
# Changelog
All notable changes to this project will be documented in this file.
-The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
-and this project adheres to [Semantic
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic
Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Holt Winters values can now be fetched at runtime, rather than simply being hardcoded.
+### Fixed
+- Fixed slow shutdown of PHPA due to ignoring SIGTERM from K8s.
+### Changed
+- Switched from Golang to Python for calculating statistical predictions for Linear Regression and Holt-Winters.
+- Holt-Winters now calculated using statsmodels, opening up statsmodels configuration options for tuning.
+ - `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.
+ - `dampedTrend` - Boolean value to determine if the trend should be damped.
+ - `initializationMethod` - Which initialization method to use, see statsmodels for details, either `estimated`,
+ `heuristic`, `known`, or `legacy-heuristic`
+ - `initialLevel` - The initial level value, required if `initializationMethod` is `known`.
+ - `initialTrend` - The initial trend value, required if `initializationMethod` is `known`.
+ - `initialSeasonal` - The initial seasonal value, required if `initializationMethod` is `known`.
+- Holt-Winters `seasonLength` variable renamed to `seasonalPeriods`.
+- Holt-Winters `method` split into `trend` and `seasonal` variables.
## [v0.6.0] - 2020-08-31
### Changed
@@ -16,19 +30,18 @@ Versioning](https://semver.org/spec/v2.0.0.html).
## [v0.5.0] - 2020-03-27
### Changed
-- Evaluation from HPA now included in list of predicted replica counts, rather
- than being treated separately at the end. Now included in mean, median,
- minimum calculations rather than just the maximum calculation.
+- Evaluation from HPA now included in list of predicted replica counts, rather than being treated separately at the end.
+Now included in mean, median, minimum calculations rather than just the maximum calculation.
## [v0.4.0] - 2020-03-10
### Added
- Documentation as code; configuration reference.
- New decision type `median`, returns the median average of the predictions.
- JSON support for configuration options.
-- Can now configure `tolerance`, `initialReadinessDelay` and
- `initializationPeriod` that are available to be configured in the K8s HPA.
+- Can now configure `tolerance`, `initialReadinessDelay` and `initializationPeriod` that are available to be configured
+in the K8s HPA.
- Default `downscaleStabilization` set to `300` (5 minutes) to match K8s HPA.
- ### Changed
+### Changed
- Metric specs now defined in `predictiveConfig` rather than in their own
section.
- Update Custom Pod Autoscaler version to v0.11.0.
@@ -40,11 +53,11 @@ Versioning](https://semver.org/spec/v2.0.0.html).
## [v0.3.0] - 2020-02-17
### Added
- Multiplicative method for Holt-Winters time series prediction.
- ### Changed
+### Changed
- Update Custom Pod Autoscaler version to v0.10.0.
- Update Horizontal Pod Autoscaler version to v0.4.0.
-- Holt-Winters no longer additive by default, must specify a method, either
- `additive` or `multiplicative` in the Holt-Winters configuration.
+- Holt-Winters no longer additive by default, must specify a method, either `additive` or `multiplicative` in the
+Holt-Winters configuration.
## [v0.2.0] - 2019-12-19
### Added
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 21ec806..5cec836 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -138,7 +138,8 @@ Commit messages should follow the ['How to Write a Git Commit Message'](https://
### Documentation
-Documentation should be in plain english, with 120 character max line width.
+Documentation should be in plain english, with 120 character max line width. If your changes affect
+functionality the changes should be documented, both in the `CHANGELOG.md` and the `docs/`.
### Code
@@ -146,8 +147,7 @@ Project code should pass the linter and all tests should pass.
### Pull Requests
-All pull requests must pass the CI (linting, tests), and be approved by a maintainer. If your pull request changes
-functionality the changes should be documented, both in the `CHANGELOG.md` and the `docs/`.
+All pull requests must pass the CI (linting, tests), and be approved by a maintainer.
All pull requests are squashed on merge.
diff --git a/Makefile b/Makefile
index b9b5a2b..4b17246 100644
--- a/Makefile
+++ b/Makefile
@@ -2,21 +2,26 @@ REGISTRY = jthomperoo
NAME = predictive-horizontal-pod-autoscaler
VERSION = latest
-default:
+default: vendor_modules
@echo "=============Building============="
- go mod vendor
- go build -mod vendor -o dist/$(NAME) ./cmd/predictive-horizontal-pod-autoscaler
+ go build -mod vendor -o dist/$(NAME) main.go
cp LICENSE dist/LICENSE
-unittest:
+test: vendor_modules
@echo "=============Running unit tests============="
- go mod vendor
- go test ./... -cover -mod=vendor -coverprofile unit_cover.out --tags=unit
+ go test ./... -cover -mod=vendor -coverprofile application_coverage.out --tags=unit
+ pytest algorithms/ --cov-report term --cov-report=xml:algorithm_coverage.out --cov-report=html:.algorithm_coverage --cov=algorithms/
-lint:
+lint: vendor_modules
@echo "=============Linting============="
- go mod vendor
go list -mod=vendor ./... | grep -v /vendor/ | xargs -L1 golint -set_exit_status
+ pylint algorithms --rcfile=.pylintrc
+
+beautify: vendor_modules
+ @echo "=============Beautifying============="
+ gofmt -s -w .
+ go mod tidy
+ find algorithms -name '*.py' -print0 | xargs -0 yapf -i
docker: default
@echo "=============Building docker images============="
@@ -25,3 +30,11 @@ docker: default
doc:
@echo "=============Serving docs============="
mkdocs serve
+
+view_coverage:
+ @echo "=============Loading coverage HTML============="
+ go tool cover -html=application_coverage.out
+ python -m webbrowser file://$(shell pwd)/.algorithm_coverage/index.html
+
+vendor_modules:
+ go mod vendor
diff --git a/README.md b/README.md
index 0a84f56..ae97514 100644
--- a/README.md
+++ b/README.md
@@ -13,18 +13,18 @@
# Predictive Horizontal Pod Autoscaler
-This is a [Custom Pod Autoscaler](https://www.github.com/jthomperoo/custom-pod-autoscaler); building on the Horizontal
-Pod Autoscaler functionality to add predictive capabilities by using various statistical methods.
-This uses the
-[Horizontal Pod Autoscaler Custom Pod Autoscaler](https://www.github.com/jthomperoo/horizontal-pod-autoscaler)
-extensively to provide most functionality for the Horizontal Pod Autoscaler parts.
+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.
-## How does it work?
+This uses the [Horizontal Pod Autoscaler Custom Pod
+Autoscaler](https://www.github.com/jthomperoo/horizontal-pod-autoscaler) extensively to provide most functionality for
+the Horizontal Pod Autoscaler parts.
-This project works by calculating the number of replicas a resource should have, then storing these values and using
-statistical models against them to produce predictions for the future. These predictions are compared and can be used
-instead of the raw replica count calculated by the Horizontal Pod Autoscaler logic.
+# 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.
## Features
@@ -38,30 +38,66 @@ solutions such as EKS or GCP.
* Downscale Stabilization.
* Sync Period.
* Initial Readiness Delay.
-* Runs in Kubernetes as a standard Deployment.
+* Runs in Kubernetes as a standard Pod.
+
+## How does it work?
+
+This project works by calculating the number of replicas a resource should have, then storing these values and using
+statistical models against them to produce predictions for the future. These predictions are compared and can be used
+instead of the raw replica count calculated by the Horizontal Pod Autoscaler logic.
## More information
-See the
-[wiki for more information, such as guides and references](https://predictive-horizontal-pod-autoscaler.readthedocs.io/en/latest/).
+See the [wiki for more information, such as guides and
+references](https://predictive-horizontal-pod-autoscaler.readthedocs.io/en/latest/).
+
+See the [`examples/` directory](./examples) for working code samples.
## Developing this project
+
### Environment
+
Developing this project requires these dependencies:
* [Go](https://golang.org/doc/install) >= `1.13`
* [Golint](https://github.com/golang/lint)
* [Docker](https://docs.docker.com/install/)
+* [Python]() == `3.8.5`
+
+Any Python dependencies must be installed by running:
+
+```bash
+pip install -r requirements-dev.txt
+```
To view docs locally, requires:
* [mkdocs](https://www.mkdocs.org/)
+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.
+
### Commands
* `go mod vendor` - generates a vendor folder.
* `make` - builds the Predictive HPA binary.
* `make docker` - builds the Predictive HPA image.
* `make lint` - lints the code.
-* `make unittest` - runs the unit tests
+* `make beautify` - beautifies the code, must be run to pass the CI.
+* `make unittest` - runs the unit tests.
* `make doc` - hosts the documentation locally, at `127.0.0.1:8000`.
+- `make view_coverage` - opens up any generated coverage reports in the browser.
diff --git a/algorithms/holt_winters/holt_winters.py b/algorithms/holt_winters/holt_winters.py
new file mode 100644
index 0000000..f8d30f9
--- /dev/null
+++ b/algorithms/holt_winters/holt_winters.py
@@ -0,0 +1,107 @@
+# 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.
+
+# pylint: disable=no-member, invalid-name
+"""
+This holt winters script performs a holt winters calculation using the provided values and configuration using the
+statsmodel library.
+"""
+
+import sys
+import math
+from json import JSONDecodeError
+from dataclasses import dataclass
+from typing import List, Optional
+import warnings
+import statsmodels.tsa.api as sm
+from dataclasses_json import dataclass_json, LetterCase
+from statsmodels.tools.sm_exceptions import ConvergenceWarning
+
+warnings.simplefilter('ignore', ConvergenceWarning)
+
+# Takes in the replica series, alpha, beta, gamma, season length, and the trend and seasonal (add vs mul)
+# {
+# "trend": "add",
+# "seasonal": "mul"
+# "alpha": 0.1,
+# "beta": 0.1,
+# "gamma": 0.1,
+# "seasonalPeriods": 5,
+# "series": [
+# 0,
+# 1,
+# 2,
+# 3,
+# 4
+# ],
+# "dampedTrend": false,
+# }
+
+
+@dataclass_json(letter_case=LetterCase.CAMEL)
+@dataclass
+class AlgorithmInput:
+ """
+ JSON data representation of the data this algorithm requires to be provided to it.
+ """
+ trend: str
+ seasonal: str
+ seasonal_periods: int
+ alpha: float
+ beta: float
+ gamma: float
+ series: List[int]
+ damped_trend: bool = False
+ initialization_method: str = "estimated"
+ initial_level: Optional[float] = None
+ initial_trend: Optional[float] = None
+ initial_seasonal: Optional[float] = None
+
+
+stdin = sys.stdin.read()
+
+if stdin is None or stdin == "":
+ print("No standard input provided to Holt-Winters algorithm, exiting", file=sys.stderr)
+ sys.exit(1)
+
+try:
+ algorithm_input = AlgorithmInput.from_json(stdin)
+except JSONDecodeError as ex:
+ print("Invalid JSON provided: {0}, exiting".format(str(ex)), file=sys.stderr)
+ sys.exit(1)
+except KeyError as ex:
+ print("Invalid JSON provided: missing {0}, exiting".format(str(ex)), file=sys.stderr)
+ sys.exit(1)
+
+if len(algorithm_input.series) < 10 + 2 * (algorithm_input.seasonal_periods // 2):
+ print("Invalid data provided, must be at least 10 + 2 * (seasonal_periods // 2) observations, exiting",
+ file=sys.stderr)
+ sys.exit(1)
+
+model = sm.ExponentialSmoothing(algorithm_input.series,
+ trend=algorithm_input.trend,
+ seasonal=algorithm_input.seasonal,
+ seasonal_periods=algorithm_input.seasonal_periods,
+ initialization_method=algorithm_input.initialization_method,
+ damped_trend=algorithm_input.damped_trend,
+ initial_level=algorithm_input.initial_level,
+ initial_trend=algorithm_input.initial_trend,
+ initial_seasonal=algorithm_input.initial_seasonal)
+
+fitted_model = model.fit(smoothing_level=algorithm_input.alpha,
+ smoothing_trend=algorithm_input.beta,
+ smoothing_seasonal=algorithm_input.gamma)
+
+# Predict the value one ahead
+print(math.ceil(fitted_model.forecast(steps=1)[0]), end="")
diff --git a/algorithms/holt_winters/test_holt_winters.py b/algorithms/holt_winters/test_holt_winters.py
new file mode 100644
index 0000000..96b4276
--- /dev/null
+++ b/algorithms/holt_winters/test_holt_winters.py
@@ -0,0 +1,139 @@
+"""
+Tests the linear regression algorithm by calling it from the shell, giving different stdin and checking the return
+code and stderr and stdout.
+"""
+import subprocess
+
+
+def test_holt_winters(subtests):
+ """
+ Test the holt winters algorithm
+ """
+ test_cases = [{
+ "description": "Empty stdin",
+ "expected_status_code": 1,
+ "expected_stderr": "No standard input provided to Holt-Winters algorithm, exiting\n",
+ "expected_stdout": "",
+ "stdin": ""
+ }, {
+ "description": "Invalid JSON stdin",
+ "expected_status_code": 1,
+ "expected_stderr": "Invalid JSON provided: Expecting value: line 1 column 1 (char 0), exiting\n",
+ "expected_stdout": "",
+ "stdin": "invalid"
+ }, {
+ "description":
+ "JSON stdin missing 'trend'",
+ "expected_status_code":
+ 1,
+ "expected_stderr":
+ "Invalid JSON provided: missing 'trend', exiting\n",
+ "expected_stdout":
+ "",
+ "stdin":
+ """{
+ "seasonal": "add",
+ "alpha": 0.9,
+ "beta": 0.9,
+ "gamma": 0.3,
+ "seasonalPeriods": 3,
+ "series": [1,3,1,1,3,1,1,3,1]
+ }"""
+ }, {
+ "description":
+ "Failure, less than required observations observations, 9 observations",
+ "expected_status_code":
+ 1,
+ "expected_stderr":
+ "Invalid data provided, must be at least 10 + 2 * (seasonal_periods // 2) " + "observations, exiting\n",
+ "expected_stdout":
+ "",
+ "stdin":
+ """{
+ "trend": "add",
+ "seasonal": "add",
+ "alpha": 0.9,
+ "beta": 0.9,
+ "gamma": 0.3,
+ "seasonalPeriods": 3,
+ "series": [1,3,1,1,3,1,1,3,1]
+ }"""
+ }, {
+ "description":
+ "Successful prediction, additive, 13 observations",
+ "expected_status_code":
+ 0,
+ "expected_stderr":
+ "",
+ "expected_stdout":
+ "3",
+ "stdin":
+ """{
+ "trend": "add",
+ "seasonal": "add",
+ "alpha": 0.9,
+ "beta": 0.9,
+ "gamma": 0.3,
+ "seasonalPeriods": 3,
+ "series": [1,3,1,1,3,1,1,3,1,1,3,1,1]
+ }"""
+ }, {
+ "description":
+ "Successful prediction, multiplicative trend, 15 observations",
+ "expected_status_code":
+ 0,
+ "expected_stderr":
+ "",
+ "expected_stdout":
+ "1",
+ "stdin":
+ """{
+ "trend": "mul",
+ "seasonal": "add",
+ "alpha": 0.3,
+ "beta": 0.3,
+ "gamma": 0.9,
+ "seasonalPeriods": 3,
+ "series": [1,3,1,1,3,1,1,3,1,1,3,1,1,3,1]
+ }"""
+ }, {
+ "description":
+ "Successful prediction, additive trend + multiplicative seasonal, legacy-heuristic init " +
+ "method, 19 observations",
+ "expected_status_code":
+ 0,
+ "expected_stderr":
+ "",
+ "expected_stdout":
+ "7",
+ "stdin":
+ """{
+ "trend": "add",
+ "seasonal": "mul",
+ "alpha": 0.005,
+ "beta": 0.9,
+ "gamma": 0.4,
+ "seasonalPeriods": 3,
+ "initialization_method": "legacy-heuristic",
+ "series": [1,1,1,1,2,1,1,3,1,1,4,1,1,5,1,1,6,1,1]
+ }"""
+ }]
+
+ for i, test_case in enumerate(test_cases):
+ with subtests.test(msg=test_case["description"], i=i):
+ result = subprocess.run(["python", "./algorithms/holt_winters/holt_winters.py"],
+ input=test_case["stdin"].encode("utf-8"),
+ capture_output=True,
+ check=False)
+
+ stderr = result.stderr
+ if stderr is not None:
+ stderr = stderr.decode("utf-8")
+
+ stdout = result.stdout
+ if stdout is not None:
+ stdout = stdout.decode("utf-8")
+
+ assert test_case["expected_status_code"] == result.returncode
+ assert test_case["expected_stderr"] == stderr
+ assert test_case["expected_stdout"] == stdout
diff --git a/algorithms/linear_regression/linear_regression.py b/algorithms/linear_regression/linear_regression.py
new file mode 100644
index 0000000..fe59ed9
--- /dev/null
+++ b/algorithms/linear_regression/linear_regression.py
@@ -0,0 +1,131 @@
+# 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.
+
+# pylint: disable=no-member, invalid-name
+"""
+This linear regression script performs a linear regression using the provided values and configuration using the
+statsmodel library.
+"""
+
+import sys
+import math
+from json import JSONDecodeError
+from datetime import datetime, timedelta
+from dataclasses import dataclass
+from typing import List, Optional
+import statsmodels.api as sm
+from dataclasses_json import dataclass_json, LetterCase
+
+# Takes in list of stored evaluations and the look ahead value:
+# {
+# "lookAhead": 3,
+# "evaluations": [
+# {
+# "id": 0,
+# "created": "2020-02-01T00:55:33Z",
+# "val": {
+# "targetReplicas": 3
+# }
+# },
+# {
+# "id": 1,
+# "created": "2020-02-01T00:56:33Z",
+# "val": {
+# "targetReplicas": 2
+# }
+# }
+# ]
+# }
+
+
+@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:
+ """
+ JSON data representation of a timestamped evaluation
+ """
+ id: int
+ created: str
+ val: EvaluationValue
+
+
+@dataclass_json(letter_case=LetterCase.CAMEL)
+@dataclass
+class AlgorithmInput:
+ """
+ JSON data representation of the data this algorithm requires to be provided to it.
+ """
+ look_ahead: int
+ evaluations: List[Evaluation]
+ current_time: Optional[str] = None
+
+
+stdin = sys.stdin.read()
+
+if stdin is None or stdin == "":
+ print("No standard input provided to Linear Regression algorithm, exiting", file=sys.stderr)
+ sys.exit(1)
+
+try:
+ algorithm_input = AlgorithmInput.from_json(stdin)
+except JSONDecodeError as ex:
+ print("Invalid JSON provided: {0}, exiting".format(str(ex)), file=sys.stderr)
+ sys.exit(1)
+except KeyError as ex:
+ print("Invalid JSON provided: missing {0}, exiting".format(str(ex)), file=sys.stderr)
+ sys.exit(1)
+
+current_time = datetime.utcnow()
+
+if algorithm_input.current_time is not None:
+ try:
+ current_time = datetime.strptime(algorithm_input.current_time, "%Y-%m-%dT%H:%M:%SZ")
+ except ValueError as ex:
+ print("Invalid datetime format: {0}".format(str(ex)), file=sys.stderr)
+ sys.exit(1)
+
+search_time = datetime.timestamp(current_time + timedelta(milliseconds=int(algorithm_input.look_ahead)))
+
+x = []
+y = []
+
+# 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):
+ try:
+ created = datetime.strptime(evaluation.created, "%Y-%m-%dT%H:%M:%SZ")
+ except ValueError as ex:
+ print("Invalid datetime format: {0}".format(str(ex)), file=sys.stderr)
+ sys.exit(1)
+
+ x.append(search_time - datetime.timestamp(created))
+ y.append(evaluation.val.target_replicas)
+
+# Add constant for OLS, constant is 1.0
+x = sm.add_constant(x)
+
+model = sm.OLS(y, x).fit()
+
+# Predict the value at the search time (0), include the constant (1).
+# The search time is 0 as the values used in training are search time - evaluation time, so the search time will be 0
+print(math.ceil(model.predict([[1, 0]])[0]), end="")
diff --git a/algorithms/linear_regression/test_linear_regression.py b/algorithms/linear_regression/test_linear_regression.py
new file mode 100644
index 0000000..97251ae
--- /dev/null
+++ b/algorithms/linear_regression/test_linear_regression.py
@@ -0,0 +1,246 @@
+# 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.
+"""
+Tests the linear regression algorithm by calling it from the shell, giving different stdin and checking the return
+code and stderr and stdout.
+"""
+import subprocess
+
+
+def test_linear_regression(subtests):
+ """
+ Test the linear regression algorithm
+ """
+ test_cases = [{
+ "description": "Empty stdin",
+ "expected_status_code": 1,
+ "expected_stderr": "No standard input provided to Linear Regression algorithm, exiting\n",
+ "expected_stdout": "",
+ "stdin": ""
+ }, {
+ "description": "Invalid JSON stdin",
+ "expected_status_code": 1,
+ "expected_stderr": "Invalid JSON provided: Expecting value: line 1 column 1 (char 0), exiting\n",
+ "expected_stdout": "",
+ "stdin": "invalid"
+ }, {
+ "description":
+ "JSON stdin missing 'lookAhead'",
+ "expected_status_code":
+ 1,
+ "expected_stderr":
+ "Invalid JSON provided: missing 'look_ahead', exiting\n",
+ "expected_stdout":
+ "",
+ "stdin":
+ """{
+ "evaluations": [
+ {
+ "id": 0,
+ "created": "2020-02-01T00:55:33Z",
+ "val": {
+ "targetReplicas": 2
+ }
+ }
+ ]
+ }"""
+ }, {
+ "description":
+ "Invalid timestamp provided",
+ "expected_status_code":
+ 1,
+ "expected_stderr":
+ "Invalid datetime format: time data 'invalid' does not match format " + "'%Y-%m-%dT%H:%M:%SZ'\n",
+ "expected_stdout":
+ "",
+ "stdin":
+ """{
+ "lookAhead": 10,
+ "evaluations": [
+ {
+ "id": 0,
+ "created": "invalid",
+ "val": {
+ "targetReplicas": 2
+ }
+ }
+ ]
+ }"""
+ }, {
+ "description":
+ "Invalid current time provided",
+ "expected_status_code":
+ 1,
+ "expected_stderr":
+ "Invalid datetime format: time data 'invalid' does not match format " + "'%Y-%m-%dT%H:%M:%SZ'\n",
+ "expected_stdout":
+ "",
+ "stdin":
+ """{
+ "lookAhead": 15000,
+ "current_time": "invalid",
+ "evaluations": []
+ }"""
+ }, {
+ "description":
+ "Successful prediction, now",
+ "expected_status_code":
+ 0,
+ "expected_stderr":
+ "",
+ "expected_stdout":
+ "5",
+ "stdin":
+ """{
+ "lookAhead": 0,
+ "current_time": "2020-02-01T00:56:12Z",
+ "evaluations": [
+ {
+ "id": 0,
+ "created": "2020-02-01T00:55:33Z",
+ "val": {
+ "targetReplicas": 1
+ }
+ },
+ {
+ "id": 1,
+ "created": "2020-02-01T00:55:43Z",
+ "val": {
+ "targetReplicas": 2
+ }
+ },
+ {
+ "id": 2,
+ "created": "2020-02-01T00:55:53Z",
+ "val": {
+ "targetReplicas": 3
+ }
+ },
+ {
+ "id": 3,
+ "created": "2020-02-01T00:56:03Z",
+ "val": {
+ "targetReplicas": 4
+ }
+ }
+ ]
+ }"""
+ }, {
+ "description":
+ "Successful prediction, 10 seconds in the future",
+ "expected_status_code":
+ 0,
+ "expected_stderr":
+ "",
+ "expected_stdout":
+ "6",
+ "stdin":
+ """{
+ "lookAhead": 10000,
+ "current_time": "2020-02-01T00:56:12Z",
+ "evaluations": [
+ {
+ "id": 0,
+ "created": "2020-02-01T00:55:33Z",
+ "val": {
+ "targetReplicas": 1
+ }
+ },
+ {
+ "id": 1,
+ "created": "2020-02-01T00:55:43Z",
+ "val": {
+ "targetReplicas": 2
+ }
+ },
+ {
+ "id": 2,
+ "created": "2020-02-01T00:55:53Z",
+ "val": {
+ "targetReplicas": 3
+ }
+ },
+ {
+ "id": 3,
+ "created": "2020-02-01T00:56:03Z",
+ "val": {
+ "targetReplicas": 4
+ }
+ }
+ ]
+ }"""
+ }, {
+ "description":
+ "Successful prediction, 15 seconds in the future",
+ "expected_status_code":
+ 0,
+ "expected_stderr":
+ "",
+ "expected_stdout":
+ "7",
+ "stdin":
+ """{
+ "lookAhead": 15000,
+ "current_time": "2020-02-01T00:56:12Z",
+ "evaluations": [
+ {
+ "id": 0,
+ "created": "2020-02-01T00:55:33Z",
+ "val": {
+ "targetReplicas": 1
+ }
+ },
+ {
+ "id": 1,
+ "created": "2020-02-01T00:55:43Z",
+ "val": {
+ "targetReplicas": 2
+ }
+ },
+ {
+ "id": 2,
+ "created": "2020-02-01T00:55:53Z",
+ "val": {
+ "targetReplicas": 3
+ }
+ },
+ {
+ "id": 3,
+ "created": "2020-02-01T00:56:03Z",
+ "val": {
+ "targetReplicas": 4
+ }
+ }
+ ]
+ }"""
+ }]
+
+ for i, test_case in enumerate(test_cases):
+ with subtests.test(msg=test_case["description"], i=i):
+ result = subprocess.run(["python", "./algorithms/linear_regression/linear_regression.py"],
+ input=test_case["stdin"].encode("utf-8"),
+ capture_output=True,
+ check=False)
+
+ stderr = result.stderr
+ if stderr is not None:
+ stderr = stderr.decode("utf-8")
+
+ stdout = result.stdout
+ if stdout is not None:
+ stdout = stdout.decode("utf-8")
+
+ assert test_case["expected_status_code"] == result.returncode
+ assert test_case["expected_stderr"] == stderr
+ assert test_case["expected_stdout"] == stdout
diff --git a/algorithms/requirements.txt b/algorithms/requirements.txt
new file mode 100644
index 0000000..6ad8db4
--- /dev/null
+++ b/algorithms/requirements.txt
@@ -0,0 +1,2 @@
+statsmodels==0.12.0
+dataclasses-json==0.5.2
diff --git a/build/Dockerfile b/build/Dockerfile
index dac7a97..362fce9 100644
--- a/build/Dockerfile
+++ b/build/Dockerfile
@@ -1,4 +1,4 @@
-# Copyright 2020 The Predictive Horizontal Pod Autoscaler Authors.
+# 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.
@@ -13,20 +13,20 @@
# limitations under the License.
# CPA base image
-FROM registry.access.redhat.com/ubi8/ubi
-
+FROM custompodautoscaler/python-3-8:v1.0.1
ENV USER_UID=1001 \
- USER_NAME=predictive-horizontal-pod-autoscaler
+ USER_NAME=phpa
# Install sqlite and container deps
-RUN dnf install sqlite wget -y
-
-# Install CPA
-RUN wget -qO- https://github.com/jthomperoo/custom-pod-autoscaler/releases/download/v1.0.0/custom-pod-autoscaler.tar.gz | tar xvz && mv dist cpa
+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 /
@@ -34,6 +34,8 @@ COPY build/entrypoint.sh /
COPY config.yaml dist/* ./
# Add in db migrations
COPY sql ./sql
+# Add in Python aglorithms
+COPY algorithms ./algorithms
RUN mkdir -p ${HOME} && \
chown ${USER_UID}:0 ${HOME} && \
@@ -43,6 +45,8 @@ RUN mkdir -p ${HOME} && \
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
index ab7856a..2c3e3f8 100755
--- a/build/entrypoint.sh
+++ b/build/entrypoint.sh
@@ -1,5 +1,5 @@
#!/bin/sh
-# Copyright 2019 The Predictive Horizontal Pod Autoscaler Authors.
+# 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.
@@ -19,4 +19,5 @@ echo "Setting up local database"
/app/predictive-horizontal-pod-autoscaler -mode=setup
echo "Starting autoscaler"
-exec /cpa/custom-pod-autoscaler
+
+exec /app/custom-pod-autoscaler
diff --git a/docs/index.md b/docs/index.md
index 7fe54e2..17b3261 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -6,33 +6,37 @@
[![License](https://img.shields.io/:license-apache-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
# Predictive Horizontal Pod Autoscaler
-# What is it?
+Visit the GitHub repository at to see examples,
+raise issues, and to contribute to the project.
-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.
+# What is it?
-This uses the
-[Horizontal Pod Autoscaler reimplemented as a Custom Pod Autoscaler](https://www.github.com/jthomperoo/horizontal-pod-autoscaler)
-extensively to provide most functionality for the Horizontal Pod Autoscaler parts.
+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.
# 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.
+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.
# 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.
-
-# How does it work?
-
-This project works by calculating the number of replicas a resource should have, then storing
-these values and using statistical models against them to produce predictions for the future.
-These predictions are compared and can be used instead of the raw replica count calculated by
-the Horizontal Pod Autoscaler logic.
+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
+
+* Functionally identical to Horizontal Pod Autoscaler for calculating replica counts without prediction.
+* Choice of statistical models to apply over Horizontal Pod Autoscaler replica counting logic.
+ * Holt-Winters Smoothing
+ * Linear Regression
+* Allows customisation of Kubernetes autoscaling options without master node access. Can therefore work on managed
+solutions such as EKS or GCP.
+ * CPU Initialization Period.
+ * Downscale Stabilization.
+ * Sync Period.
+ * Initial Readiness Delay.
+* Runs in Kubernetes as a standard Pod.
diff --git a/docs/user-guide/hooks.md b/docs/user-guide/hooks.md
new file mode 100644
index 0000000..b29294e
--- /dev/null
+++ b/docs/user-guide/hooks.md
@@ -0,0 +1,123 @@
+# Hooks
+
+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.
+
+### Example
+
+This is an example configuration of the http hook for runtime tuning fetching with Holt Winters:
+
+```yaml
+holtWinters:
+ runtimeTuningFetchHook:
+ type: "http"
+ timeout: 2500
+ http:
+ method: "GET"
+ url: "https://www.custompodautoscaler.com"
+ successCodes:
+ - 200
+ headers:
+ exampleHeader: exampleHeaderValue
+ parameterMode: query
+```
+
+Breaking this example down:
+
+- `type` = the type of the hook, for this example it is an `http` 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.
+- `http` = configuration of the HTTP request.
+ - `method` = the HTTP method of the HTTP request.
+ - `url` = the URL to target with the HTTP request.
+ - `successCodes` = a list of success codes defining how to determine if the
+ request is successful - if the request responds with a code not on this list
+ it will be assumed to be a failure.
+ - `headers` = a dictionary of headers that can be provided with the request,
+ in this example the key is `exampleHeader` and the value is
+ `exampleHeaderValue`. This is an optional parameter.
+ - `parameterMode` = the mode for passing parameters to the target; either
+ `query` - as a query parameter, or `body` - as a body parameter. In this
+ example it is by query parameter.
+
+### POST Example
+
+This is an example using HTTP `POST` and information passed as a body parameter.
+
+```yaml
+runtimeTuningFetchHook:
+ type: "http"
+ timeout: 2500
+ http:
+ method: "POST"
+ url: "https://www.custompodautoscaler.com"
+ successCodes:
+ - 200
+ - 202
+ parameterMode: body
+```
diff --git a/docs/user-guide/models.md b/docs/user-guide/models.md
index e42e57f..ff72d91 100644
--- a/docs/user-guide/models.md
+++ b/docs/user-guide/models.md
@@ -9,11 +9,16 @@ All models share these three options:
- **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.
+- **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.
## Linear Regression
+
+The linear regression model uses a default calculation timeout of `30000` (30 seconds).
+
Example:
```yaml
- name: predictiveConfig
@@ -22,6 +27,7 @@ Example:
- type: Linear
name: LinearPrediction
perInterval: 1
+ calculationTimeout: 25000
linear:
lookAhead: 10000
storedValues: 6
@@ -32,11 +38,15 @@ The **linear** component of the configuration handles configuration of the Linea
- **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
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 `/example/simple-linear`](https://github.com/jthomperoo/predictive-horizontal-pod-autoscaler/tree/master/example/simple-linear).
+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).
## Holt-Winters Time Series prediction
+The Holt-Winters time series model uses a default calculation timeout of `30000` (30 seconds).
+
Example:
```yaml
- name: predictiveConfig
@@ -49,20 +59,24 @@ Example:
alpha: 0.9
beta: 0.9
gamma: 0.9
- seasonLength: 6
+ seasonalPeriods: 6
storedSeasons: 4
- method: "additive"
+ trend: additive
+ seasonal: additive
decisionType: "maximum"
```
+
The **holtWinters** component of the configuration handles configuration of the Linear regression options:
- **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.
-- **seasonLength** - 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 intervals, for example if your interval 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.
+- **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.
This is the model in action, taken from the `simple-holt-winters` example:
![Predicted values overestimating but still fitting actual values](../img/holt_winters_prediction_vs_actual.svg)
@@ -71,16 +85,31 @@ is overestimating, but still pre-emptively scaling - storing more seasons and ad
would reduce the overestimation and produce more accurate results.
For a more detailed example, [see the example in
-`/example/simple-holt-winters`](https://github.com/jthomperoo/predictive-horizontal-pod-autoscaler/tree/master/example/simple-holt-winters).
+`/examples/simple-holt-winters`](https://github.com/jthomperoo/predictive-horizontal-pod-autoscaler/tree/master/examples/simple-holt-winters).
+
+### Advanced tuning
+
+There are more configuration options for the Holt-Winters algorithm, which in this project uses the
+[statsmodels](https://www.statsmodels.org/) Python package. These are the additional configuration options, which are
+documented by the [Holt-Winters Exponential Smoothing statsmodels
+documentation](https://www.statsmodels.org/dev/generated/statsmodels.tsa.holtwinters.ExponentialSmoothing.html) - the
+names of the variables in this documentation map to the camelcase names described here.
+
+- **dampedTrend** - Boolean value to determine if the trend should be damped.
+- **initializationMethod** - Which initialization method to use, see statsmodels for details, either `estimated`,
+`heuristic`, `known`, or `legacy-heuristic`
+- **initialLevel** - The initial level value, required if `initializationMethod` is `known`.
+- **initialTrend** - The initial trend value, required if `initializationMethod` is `known`.
+- **initialSeasonal** - The initial seasonal value, required if `initializationMethod` is `known`.
### Holt-Winters Runtime Tuning
The PHPA supports dynamically fetching the tuning values for the Holt-Winters algorithm (`alpha`, `beta`, and `gamma`).
-This is done using a feature of the Custom Pod Autoscaler called
-[methods](https://custom-pod-autoscaler.readthedocs.io/en/latest/user-guide/methods/) - these are configurable hooks
-that allow the PHPA to reach out to an external source to determine what the tuning values should be.
-For example, a method using a HTTP request to fetch the values of runtime is configured as:
+This is done using a `hook` system, to see more information of how the dynamic hook system works [visit the hooks
+user guide](./hooks.md)
+
+For example, a hook using a HTTP request to fetch the values of runtime is configured as:
```yaml
- name: predictiveConfig
@@ -90,7 +119,7 @@ For example, a method using a HTTP request to fetch the values of runtime is con
name: HoltWintersPrediction
perInterval: 1
holtWinters:
- runtimeTuningFetch:
+ runtimeTuningFetchHook:
type: "http"
timeout: 2500
http:
@@ -99,15 +128,15 @@ For example, a method using a HTTP request to fetch the values of runtime is con
successCodes:
- 200
parameterMode: query
- seasonLength: 6
+ seasonalPeriods: 6
storedSeasons: 4
- method: "additive"
+ trend: "additive"
decisionType: "maximum"
```
-The method is defined with the name `runtimeTuningFetch`.
+The hook is defined with the name `runtimeTuningFetchHook`.
-The supported methods for the PHPA are:
+The supported hook types for the PHPA are:
- Shell scripts
- HTTP requests
@@ -116,12 +145,12 @@ The process is as follows:
1. PHPA begins Holt-Winters calculation.
2. The values are initially set to any hardcoded values supplied in the configuration.
-3. A runtime tuning configuration has been supplied, using this configuration a method is executed (for example a HTTP
+3. A runtime tuning configuration has been supplied, using this configuration a hook is executed (for example a HTTP
request is sent).
- - This method will provide as input data in JSON that includes the current model, and an array of timestamped
+ - This hook will provide as input data in JSON that includes the current model, and an array of timestamped
previous scaling decisions (referred to as `evaluations`, [see below](#request-format)).
- The response should conform to the expected JSON structure ([see below](#response-format)).
-4. If the method execution is successful, and the response is valid, the tuning values are extracted and any provided
+4. If the hook execution is successful, and the response is valid, the tuning values are extracted and any provided
values overwrite the hardcoded values.
5. If all required tuning values are provided the tuning values are used to calculate.
@@ -136,7 +165,7 @@ runtime, and the `beta` and `gamma` values could be hardcoded in configuration:
name: HoltWintersPrediction
perInterval: 1
holtWinters:
- runtimeTuningFetch:
+ runtimeTuningFetchHook:
type: "http"
timeout: 2500
http:
@@ -147,9 +176,9 @@ runtime, and the `beta` and `gamma` values could be hardcoded in configuration:
parameterMode: query
beta: 0.9
gamma: 0.9
- seasonLength: 6
+ seasonalPeriods: 6
storedSeasons: 4
- method: "additive"
+ trend: "additive"
decisionType: "maximum"
```
@@ -164,7 +193,7 @@ as a backup:
name: HoltWintersPrediction
perInterval: 1
holtWinters:
- runtimeTuningFetch:
+ runtimeTuningFetchHook:
type: "http"
timeout: 2500
http:
@@ -176,9 +205,9 @@ as a backup:
alpha: 0.9
beta: 0.9
gamma: 0.9
- seasonLength: 6
+ seasonalPeriods: 6
storedSeasons: 4
- method: "additive"
+ trend: "additive"
decisionType: "maximum"
```
@@ -200,7 +229,7 @@ The data that the external source will recieve will be formatted as:
"alpha": null,
"beta": null,
"gamma": null,
- "runtimeTuningFetch": {
+ "runtimeTuningFetchHook": {
"type": "http",
"timeout": 2500,
"shell": null,
@@ -213,9 +242,9 @@ The data that the external source will recieve will be formatted as:
"parameterMode": "query"
}
},
- "seasonLength": 6,
+ "seasonalPeriods": 6,
"storedSeasons": 4,
- "method": "additive"
+ "trend": "additive"
}
},
"evaluations": [
@@ -280,4 +309,4 @@ rely on the hardcoded configuration value, this response would be valid:
```
For a more detailed example, [see the example in
-`/example/dynamic-holt-winters`](https://github.com/jthomperoo/predictive-horizontal-pod-autoscaler/tree/master/example/dynamic-holt-winters).
+`/examples/dynamic-holt-winters`](https://github.com/jthomperoo/predictive-horizontal-pod-autoscaler/tree/master/examples/dynamic-holt-winters).
diff --git a/example/dynamic-holt-winters/README.md b/examples/dynamic-holt-winters/README.md
similarity index 89%
rename from example/dynamic-holt-winters/README.md
rename to examples/dynamic-holt-winters/README.md
index e519c0b..ca44e33 100644
--- a/example/dynamic-holt-winters/README.md
+++ b/examples/dynamic-holt-winters/README.md
@@ -65,7 +65,7 @@ the configuration defines how the autoscaler will act:
name: HoltWintersPrediction
perInterval: 1
holtWinters:
- runtimeTuningFetch:
+ runtimeTuningFetchHook:
type: "http"
timeout: 2500
http:
@@ -74,9 +74,10 @@ the configuration defines how the autoscaler will act:
successCodes:
- 200
parameterMode: query
- seasonLength: 6
+ seasonalPeriods: 6
storedSeasons: 4
- method: "additive"
+ trend: "additive"
+ seasonal: additive
decisionType: "maximum"
metrics:
- type: Resource
@@ -105,13 +106,14 @@ period described, in this case it will pick the highest evaluation over the past
- **type** - 'HoltWinters', using a Holt-Winters predictive model.
- **name** - Unique name of the model.
- **holtWinters** - Holt-Winters specific configuration.
- * **runtimeTuningFetch** - 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`.
- * **seasonLength** - the length of a season in base unit intervals, for this example interval is `20000`
+ * **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.
- * **method** - the Holt-Winters method to use, either `additive` or `multiplicative`.
+ * **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.
diff --git a/example/dynamic-holt-winters/deployment.yaml b/examples/dynamic-holt-winters/deployment.yaml
similarity index 100%
rename from example/dynamic-holt-winters/deployment.yaml
rename to examples/dynamic-holt-winters/deployment.yaml
diff --git a/example/dynamic-holt-winters/load/Dockerfile b/examples/dynamic-holt-winters/load/Dockerfile
similarity index 91%
rename from example/dynamic-holt-winters/load/Dockerfile
rename to examples/dynamic-holt-winters/load/Dockerfile
index c543738..84d29d5 100644
--- a/example/dynamic-holt-winters/load/Dockerfile
+++ b/examples/dynamic-holt-winters/load/Dockerfile
@@ -1,4 +1,4 @@
-# Copyright 2020 The Predictive Horizontal Pod Autoscaler Authors.
+# 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.
diff --git a/example/dynamic-holt-winters/load/load.yaml b/examples/dynamic-holt-winters/load/load.yaml
similarity index 100%
rename from example/dynamic-holt-winters/load/load.yaml
rename to examples/dynamic-holt-winters/load/load.yaml
diff --git a/example/dynamic-holt-winters/load/load_tester.sh b/examples/dynamic-holt-winters/load/load_tester.sh
similarity index 91%
rename from example/dynamic-holt-winters/load/load_tester.sh
rename to examples/dynamic-holt-winters/load/load_tester.sh
index f703d78..22525ee 100644
--- a/example/dynamic-holt-winters/load/load_tester.sh
+++ b/examples/dynamic-holt-winters/load/load_tester.sh
@@ -1,6 +1,6 @@
#!/bin/bash
-# Copyright 2020 The Predictive Horizontal Pod Autoscaler Authors.
+# 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.
diff --git a/example/dynamic-holt-winters/phpa.yaml b/examples/dynamic-holt-winters/phpa.yaml
similarity index 91%
rename from example/dynamic-holt-winters/phpa.yaml
rename to examples/dynamic-holt-winters/phpa.yaml
index 1c9df87..175e6a7 100644
--- a/example/dynamic-holt-winters/phpa.yaml
+++ b/examples/dynamic-holt-winters/phpa.yaml
@@ -39,7 +39,7 @@ spec:
containers:
- name: dynamic-holt-winters-example
image: jthomperoo/predictive-horizontal-pod-autoscaler:latest
- imagePullPolicy: IfNotPresent
+ imagePullPolicy: Always
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
@@ -57,7 +57,7 @@ spec:
name: HoltWintersPrediction
perInterval: 1
holtWinters:
- runtimeTuningFetch:
+ runtimeTuningFetchHook:
type: "http"
timeout: 2500
http:
@@ -66,9 +66,10 @@ spec:
successCodes:
- 200
parameterMode: query
- seasonLength: 6
+ seasonalPeriods: 6
storedSeasons: 4
- method: "additive"
+ trend: "additive"
+ seasonal: additive
decisionType: "maximum"
metrics:
- type: Resource
diff --git a/example/dynamic-holt-winters/tuning/Dockerfile b/examples/dynamic-holt-winters/tuning/Dockerfile
similarity index 92%
rename from example/dynamic-holt-winters/tuning/Dockerfile
rename to examples/dynamic-holt-winters/tuning/Dockerfile
index 02956f7..cc40681 100644
--- a/example/dynamic-holt-winters/tuning/Dockerfile
+++ b/examples/dynamic-holt-winters/tuning/Dockerfile
@@ -1,4 +1,4 @@
-# Copyright 2020 The Predictive Horizontal Pod Autoscaler Authors.
+# 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.
diff --git a/example/dynamic-holt-winters/tuning/api.py b/examples/dynamic-holt-winters/tuning/api.py
similarity index 94%
rename from example/dynamic-holt-winters/tuning/api.py
rename to examples/dynamic-holt-winters/tuning/api.py
index 9569d4d..5e2facc 100644
--- a/example/dynamic-holt-winters/tuning/api.py
+++ b/examples/dynamic-holt-winters/tuning/api.py
@@ -1,4 +1,4 @@
-# Copyright 2020 The Predictive Horizontal Pod Autoscaler Authors.
+# 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.
diff --git a/example/dynamic-holt-winters/tuning/requirements.txt b/examples/dynamic-holt-winters/tuning/requirements.txt
similarity index 100%
rename from example/dynamic-holt-winters/tuning/requirements.txt
rename to examples/dynamic-holt-winters/tuning/requirements.txt
diff --git a/example/dynamic-holt-winters/tuning/tuning.yaml b/examples/dynamic-holt-winters/tuning/tuning.yaml
similarity index 100%
rename from example/dynamic-holt-winters/tuning/tuning.yaml
rename to examples/dynamic-holt-winters/tuning/tuning.yaml
diff --git a/example/persistent-linear/README.md b/examples/persistent-linear/README.md
similarity index 100%
rename from example/persistent-linear/README.md
rename to examples/persistent-linear/README.md
diff --git a/example/persistent-linear/deployment.yaml b/examples/persistent-linear/deployment.yaml
similarity index 100%
rename from example/persistent-linear/deployment.yaml
rename to examples/persistent-linear/deployment.yaml
diff --git a/example/persistent-linear/phpa.yaml b/examples/persistent-linear/phpa.yaml
similarity index 96%
rename from example/persistent-linear/phpa.yaml
rename to examples/persistent-linear/phpa.yaml
index 53508bf..d68bd86 100644
--- a/example/persistent-linear/phpa.yaml
+++ b/examples/persistent-linear/phpa.yaml
@@ -71,7 +71,7 @@ spec:
containers:
- name: predictive-horizontal-pod-autoscaler-example
image: jthomperoo/predictive-horizontal-pod-autoscaler:latest
- imagePullPolicy: IfNotPresent
+ imagePullPolicy: Always
volumeMounts:
- mountPath: "/store/"
name: predictive-horizontal-pod-autoscaler-example
@@ -80,11 +80,11 @@ spec:
kind: Deployment
name: php-apache
provisionRole: false
- config:
+ config:
- name: minReplicas
value: "1"
- name: maxReplicas
- value: "5"
+ value: "10"
- name: predictiveConfig
value: |
models:
@@ -105,4 +105,4 @@ spec:
- name: interval
value: "10000"
- name: downscaleStabilization
- value: "30"
+ value: "0"
diff --git a/example/simple-holt-winters/README.md b/examples/simple-holt-winters/README.md
similarity index 92%
rename from example/simple-holt-winters/README.md
rename to examples/simple-holt-winters/README.md
index 2df62b3..a031828 100644
--- a/example/simple-holt-winters/README.md
+++ b/examples/simple-holt-winters/README.md
@@ -57,9 +57,10 @@ config:
alpha: 0.9
beta: 0.9
gamma: 0.9
- seasonLength: 6
+ seasonalPeriods: 6
storedSeasons: 4
- method: "additive"
+ trend: additive
+ seasonal: additive
decisionType: "maximum"
metrics:
- type: Resource
@@ -89,11 +90,12 @@ period described, in this case it will pick the highest evaluation over the past
- **holtWinters** - Holt-Winters specific configuration.
* **alpha**, **beta**, **gamma** - these are the smoothing coefficients for level, trend and seasonality
respectively.
- * **seasonLength** - the length of a season in base unit intervals, for this example interval is `20000`
+ * **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.
- * **method** - the Holt-Winters method to use, either `additive` or `multiplicative`.
+ * **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.
diff --git a/example/simple-holt-winters/deployment.yaml b/examples/simple-holt-winters/deployment.yaml
similarity index 100%
rename from example/simple-holt-winters/deployment.yaml
rename to examples/simple-holt-winters/deployment.yaml
diff --git a/example/simple-holt-winters/load/Dockerfile b/examples/simple-holt-winters/load/Dockerfile
similarity index 86%
rename from example/simple-holt-winters/load/Dockerfile
rename to examples/simple-holt-winters/load/Dockerfile
index 3ef9e47..84d29d5 100644
--- a/example/simple-holt-winters/load/Dockerfile
+++ b/examples/simple-holt-winters/load/Dockerfile
@@ -1,4 +1,4 @@
-# Copyright 2019 The Predictive Horizontal Pod Autoscaler Authors.
+# 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.
@@ -18,4 +18,4 @@ COPY load_tester.sh /load_tester.sh
RUN chmod +x /load_tester.sh
-CMD [ "/bin/sh", "/load_tester.sh" ]
\ No newline at end of file
+CMD [ "/bin/sh", "/load_tester.sh" ]
diff --git a/example/simple-holt-winters/load/load.yaml b/examples/simple-holt-winters/load/load.yaml
similarity index 100%
rename from example/simple-holt-winters/load/load.yaml
rename to examples/simple-holt-winters/load/load.yaml
diff --git a/example/simple-holt-winters/load/load_tester.sh b/examples/simple-holt-winters/load/load_tester.sh
similarity index 91%
rename from example/simple-holt-winters/load/load_tester.sh
rename to examples/simple-holt-winters/load/load_tester.sh
index ec9b582..22525ee 100644
--- a/example/simple-holt-winters/load/load_tester.sh
+++ b/examples/simple-holt-winters/load/load_tester.sh
@@ -1,6 +1,6 @@
#!/bin/bash
-# Copyright 2019 The Predictive Horizontal Pod Autoscaler Authors.
+# 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.
@@ -17,4 +17,4 @@
while true; do
timeout -t 30 /bin/sh -c "while true; do wget -q -O- http://php-apache.default.svc.cluster.local; done"
sleep 60
-done
\ No newline at end of file
+done
diff --git a/example/simple-holt-winters/phpa.yaml b/examples/simple-holt-winters/phpa.yaml
similarity index 90%
rename from example/simple-holt-winters/phpa.yaml
rename to examples/simple-holt-winters/phpa.yaml
index 275a416..06217f7 100644
--- a/example/simple-holt-winters/phpa.yaml
+++ b/examples/simple-holt-winters/phpa.yaml
@@ -39,13 +39,13 @@ spec:
containers:
- name: simple-holt-winters-example
image: jthomperoo/predictive-horizontal-pod-autoscaler:latest
- imagePullPolicy: IfNotPresent
+ imagePullPolicy: Always
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: php-apache
provisionRole: false
- config:
+ config:
- name: minReplicas
value: "1"
- name: maxReplicas
@@ -60,9 +60,10 @@ spec:
alpha: 0.9
beta: 0.9
gamma: 0.9
- seasonLength: 6
+ seasonalPeriods: 6
storedSeasons: 4
- method: "additive"
+ trend: additive
+ seasonal: additive
decisionType: "maximum"
metrics:
- type: Resource
@@ -72,7 +73,7 @@ spec:
type: Utilization
averageUtilization: 50
- name: interval
- value: "20000"
+ value: "10000"
- name: startTime
value: "60000"
- name: downscaleStabilization
diff --git a/example/simple-linear/README.md b/examples/simple-linear/README.md
similarity index 100%
rename from example/simple-linear/README.md
rename to examples/simple-linear/README.md
diff --git a/example/simple-linear/deployment.yaml b/examples/simple-linear/deployment.yaml
similarity index 100%
rename from example/simple-linear/deployment.yaml
rename to examples/simple-linear/deployment.yaml
diff --git a/example/simple-linear/phpa.yaml b/examples/simple-linear/phpa.yaml
similarity index 96%
rename from example/simple-linear/phpa.yaml
rename to examples/simple-linear/phpa.yaml
index 94863ea..e4cfee3 100644
--- a/example/simple-linear/phpa.yaml
+++ b/examples/simple-linear/phpa.yaml
@@ -45,11 +45,11 @@ spec:
kind: Deployment
name: php-apache
provisionRole: false
- config:
+ config:
- name: minReplicas
value: "1"
- name: maxReplicas
- value: "5"
+ value: "10"
- name: predictiveConfig
value: |
models:
@@ -70,4 +70,4 @@ spec:
- name: interval
value: "10000"
- name: downscaleStabilization
- value: "30"
+ value: "0"
diff --git a/fake/evaluater.go b/fake/evaluater.go
deleted file mode 100644
index b276aed..0000000
--- a/fake/evaluater.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package fake
-
-import (
- "github.com/jthomperoo/custom-pod-autoscaler/evaluate"
- "github.com/jthomperoo/horizontal-pod-autoscaler/metric"
-)
-
-// Evaluater (fake) provides a way to insert functionality into a Evaluater
-type Evaluater struct {
- GetEvaluationReactor func(gatheredMetrics []*metric.Metric) (*evaluate.Evaluation, error)
-}
-
-// GetEvaluation calls the fake Evaluater function
-func (f *Evaluater) GetEvaluation(gatheredMetrics []*metric.Metric) (*evaluate.Evaluation, error) {
- return f.GetEvaluationReactor(gatheredMetrics)
-}
diff --git a/go.mod b/go.mod
index 40b25ac..dc9e9d0 100644
--- a/go.mod
+++ b/go.mod
@@ -6,10 +6,8 @@ require (
github.com/golang-migrate/migrate/v4 v4.7.0
github.com/google/go-cmp v0.3.1
github.com/jthomperoo/custom-pod-autoscaler v1.0.0
- github.com/jthomperoo/holtwinters v0.2.0
github.com/jthomperoo/horizontal-pod-autoscaler v0.6.0
github.com/mattn/go-sqlite3 v2.0.1+incompatible
- gonum.org/v1/gonum v0.6.1
k8s.io/api v0.17.0
k8s.io/apimachinery v0.17.0
k8s.io/client-go v11.0.0+incompatible
diff --git a/go.sum b/go.sum
index 2190e30..1802ad7 100644
--- a/go.sum
+++ b/go.sum
@@ -20,7 +20,6 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko
github.com/Rican7/retry v0.1.0/go.mod h1:FgOROf8P5bebcC1DS0PdOQiqGUridaZvikzUmkFW6gg=
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/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
@@ -81,7 +80,6 @@ github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zA
github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dhui/dktest v0.3.0/go.mod h1:cyzIUfGsBEbZ6BT7tnXqAShHSXCZhSNmFl70sZ7c1yc=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
-github.com/docker/distribution v0.0.0-20170726174610-edc3ab29cdff h1:FKH02LHYqSmeWd3GBh0KIkM8JBpw3RrShgtcWShdWJg=
github.com/docker/distribution v0.0.0-20170726174610-edc3ab29cdff/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.7.0+incompatible h1:neUDAlf3wX6Ml4HdqTrbcOHXtfRN0TFIwt6YFL7N9RU=
github.com/docker/distribution v2.7.0+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
@@ -101,13 +99,11 @@ github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/euank/go-kmsg-parser v2.0.0+incompatible/go.mod h1:MhmAMZ8V4CYH4ybgdRwPr2TU5ThnS43puaKEMpja1uw=
-github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550 h1:mV9jbLoSW/8m4VK16ZkHTozJa8sesK5u5kTMFysTYac=
github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M=
github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
github.com/fatih/camelcase v0.0.0-20160318181535-f6a740d52f96/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
-github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsouza/fake-gcs-server v1.7.0/go.mod h1:5XIRs4YvwNbNoz+1JF8j6KLAyDh7RHGAyAK3EP2EsNk=
@@ -143,28 +139,22 @@ github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
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 v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
-github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415 h1:WSBJMqJbLxsn+bTCPyPYZfqHdJmc8MK4wrBjMft6BAM=
github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
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/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903 h1:LbsanbbD6LieFkXbj9YNNBupiGHJgFeLpO0j0Fza1h8=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v0.0.0-20160127222235-bd3c8e81be01/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
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=
-github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -179,22 +169,18 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z
github.com/google/cadvisor v0.33.2-0.20190411163913-9db8c7dee20a/go.mod h1:1nql6U13uTHaLYB8rLS5x9IJc2qT6Xd/Tr1sTX6NE48=
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
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 v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k=
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/googleapis/gnostic v0.3.1 h1:WeAefnSUHlBb0iJKwxFDZdbfGwkd7xRNuV+IpXMJhYk=
github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU=
@@ -214,7 +200,6 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U
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-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
-github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -231,7 +216,6 @@ github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80s
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/jonboulle/clockwork v0.0.0-20141017032234-72f9bd7c4e0c/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
-github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be h1:AHimNtVIpiBjPUhEF5KNCkrUyqTSA5zWUl8sQ2bfGBE=
github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -239,13 +223,10 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jteeuwen/go-bindata v0.0.0-20151023091102-a0ff2567cfb7/go.mod h1:JVvhzYOiGBnFSYRyV00iY8q7/0PThjIYav1p9h5dmKs=
github.com/jthomperoo/custom-pod-autoscaler v1.0.0 h1:QvYuoidDCsks7gspno6QMwKFWu7fUQKy/AeK+wnRjhQ=
github.com/jthomperoo/custom-pod-autoscaler v1.0.0/go.mod h1:u17XF4vuU55t2/U9FesZ5gypneyGJR4avmofHQvnm2Q=
-github.com/jthomperoo/holtwinters v0.2.0 h1:4NcU3z6EfEtqlRF2P9xXv3DSY3TsRekgoJfWnprlI4Y=
-github.com/jthomperoo/holtwinters v0.2.0/go.mod h1:uo/ij0URykDyHDOIGK+Ivt5JsH0xn+uGDpYuvhnIWsU=
github.com/jthomperoo/horizontal-pod-autoscaler v0.6.0 h1:2IZ/EMUFX7UKt60coesHDoBhZsMi4Buq/ceCO80lf5Y=
github.com/jthomperoo/horizontal-pod-autoscaler v0.6.0/go.mod h1:xVxTdSmKtrKEq0eWWGm5ODck/7oLcPMizPclFSQKtnA=
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/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/kardianos/osext v0.0.0-20150410034420-8fef92e41e22/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/karrick/godirwalk v1.7.5/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34=
@@ -306,7 +287,6 @@ github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuB
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420 h1:Yu3681ykYHDfLoI6XVjL4JWmkE+3TX9yfIWwRCh1kFM=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
@@ -360,7 +340,6 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd
github.com/spf13/cast v0.0.0-20160730092037-e31f36ffc91a/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cobra v0.0.0-20180319062004-c439c4fa0937/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/jwalterweatherman v0.0.0-20160311093646-33c24e77fb80/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
-github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
@@ -368,7 +347,6 @@ github.com/spf13/viper v0.0.0-20160820190039-7fb2782df3d8/go.mod h1:A8kyI5cUJhb8
github.com/storageos/go-api v0.0.0-20180912212459-343b3eff91fc/go.mod h1:ZrLn+e0ZuF3Y65PNF6dIwbJPZqfmtCXxFm9ckv0agOY=
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=
-github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -395,18 +373,13 @@ go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df/go.mod h1:wR5kodmAFQ0UK8
go.uber.org/zap v0.0.0-20180814183419-67bc79d13d15/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
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 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495 h1:I6A9Ag9FpEKOjcKrRNjQkPHawoXIhKyTGfvvjFAiiAk=
golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
-golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -426,7 +399,6 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/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 h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
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=
@@ -435,7 +407,6 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL
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 h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA=
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -452,7 +423,6 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
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 h1:pczuHS43Cp2ktBEEmLwScxgjWsBSzdaQiKzUyf3DTTc=
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=
@@ -460,19 +430,15 @@ golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdO
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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=
-golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db h1:6/JqlYfC1CCaLnGceQTI+sDGhC9UBSPAsBqI0Gun6kU=
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/time v0.0.0-20161028155119-f51c12702a4d h1:TnM+PKb3ylGmZvyPXmo9m/wktg7Jn/a/fNmr33HSj8g=
golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20170824195420-5d2fd3ccab98/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -483,15 +449,9 @@ golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3
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-20190425222832-ad9eeb80039a/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
-gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485 h1:OB/uP/Puiu5vS5QMRPrXCDWUPb+kt8f1KW8oQzFejQw=
gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
-gonum.org/v1/gonum v0.6.1 h1:/LSrTrgZtpbXyAR6+0e152SROCkJJSh7goYWVmdPFGc=
-gonum.org/v1/gonum v0.6.1/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
-gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e h1:jRyg0XfpwWlhEV8mDfdNGBeSJM2fuyh9Yjrnd8kF2Ts=
gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ=
-gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
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=
@@ -512,25 +472,21 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.0/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
-gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o=
gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.0.0-20150622162204-20b71e5b60d7/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
-gopkg.in/square/go-jose.v2 v2.0.0-20180411045311-89060dee6a84 h1:ELQJ5WuT+ydETLCpWvAuw8iGBQRGoJq+A3RAbbAcZUY=
gopkg.in/square/go-jose.v2 v2.0.0-20180411045311-89060dee6a84/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/warnings.v0 v0.1.1/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
-gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -560,13 +516,11 @@ k8s.io/gengo v0.0.0-20190116091435-f8a0810f38af/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8
k8s.io/heapster v1.2.0-beta.1 h1:lUsE/AHOMHpi3MLlBEkaU8Esxm5QhdyCrv1o7ot0s84=
k8s.io/heapster v1.2.0-beta.1/go.mod h1:h1uhptVXMwC8xtZBYsPXKVi8fpdlYkTs6k949KozGrM=
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
-k8s.io/klog v0.3.1 h1:RVgyDHY/kFKtLqh67NvEWIgkMneNoIrdkN0CxDSQc68=
k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v0.4.0 h1:lCJCxf/LIowc2IGS9TPjWDyXY4nOmdGdfcwwDQCOURQ=
k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/kube-aggregator v0.0.0-20190620085325-f29e2b4a4f84/go.mod h1:S8URgD5o6oGCoLP9lx6oAdxWB9Ovtq9KLU9cYmU08QI=
k8s.io/kube-controller-manager v0.0.0-20190620085942-b7f18460b210/go.mod h1:FoQCodL0eAmdwbZuu2/fi7nVEh3xE4SpomwLaGO4V74=
-k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30 h1:TRb4wNWoBVrH9plmkp2q86FIDppkbrEXdXlxU3a3BMI=
k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
k8s.io/kube-openapi v0.0.0-20190320154901-5e45bb682580 h1:fq0ZXW/BAIFZH+dazlups6JTVdwzRo5d9riFA103yuQ=
k8s.io/kube-openapi v0.0.0-20190320154901-5e45bb682580/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
@@ -580,7 +534,6 @@ k8s.io/metrics v0.0.0-20190620085625-3b22d835f165 h1:QL+K/YzcCFzqnbjQkXkp1h0k0BT
k8s.io/metrics v0.0.0-20190620085625-3b22d835f165/go.mod h1:98M2pmJXrzr0W4o9N5ptSvZMygAMRVvyPr2vq7aRwT4=
k8s.io/repo-infra v0.0.0-20181204233714-00fe14e3d1a3/go.mod h1:+G1xBfZDfVFsm1Tj/HNCvg4QqWx8rJ2Fxpqr1rqp/gQ=
k8s.io/sample-apiserver v0.0.0-20190620085408-1aef9010884e/go.mod h1:uGtJFD6/KTth+Ed9NNWF8lMf5m1ued/X6qUuACRrBTs=
-k8s.io/utils v0.0.0-20190221042446-c2654d5206da h1:ElyM7RPonbKnQqOcw7dG2IK5uvQQn3b/WPHqD5mBvP4=
k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0=
k8s.io/utils v0.0.0-20190801114015-581e00157fb1 h1:+ySTxfHnfzZb9ys375PXNlLhkJPLKgHajBU0N62BDvE=
k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
@@ -589,7 +542,6 @@ modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=
modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I=
-rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
sigs.k8s.io/structured-merge-diff v0.0.0-20190302045857-e85c7b244fd2/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
diff --git a/internal/algorithm/algorithm.go b/internal/algorithm/algorithm.go
new file mode 100644
index 0000000..9909d89
--- /dev/null
+++ b/internal/algorithm/algorithm.go
@@ -0,0 +1,51 @@
+/*
+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 algorithm
+
+import (
+ "os/exec"
+
+ "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook"
+)
+
+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
+
+// Run is an implementation of an algorithm runner that uses CPA executers to run shell commands
+type Run struct {
+ Executer hook.Executer
+}
+
+// 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)
+}
diff --git a/internal/algorithm/algorithm_test.go b/internal/algorithm/algorithm_test.go
new file mode 100644
index 0000000..7aeabbe
--- /dev/null
+++ b/internal/algorithm/algorithm_test.go
@@ -0,0 +1,90 @@
+/*
+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.
+*/
+// +build unit
+
+package algorithm_test
+
+import (
+ "errors"
+ "testing"
+
+ "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()
+ })
+
+ var tests = []struct {
+ description string
+ expected string
+ expectedErr error
+ runner algorithm.Run
+ algorithmPath string
+ value string
+ timeout int
+ }{
+ {
+ "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")
+ },
+ },
+ },
+ "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
+ },
+ },
+ },
+ "test",
+ "test",
+ 10,
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.description, func(t *testing.T) {
+ result, err := test.runner.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/config/config.go b/internal/config/config.go
similarity index 71%
rename from config/config.go
rename to internal/config/config.go
index 1f0711f..a11a2c1 100644
--- a/config/config.go
+++ b/internal/config/config.go
@@ -1,5 +1,5 @@
/*
-Copyright 2020 The Predictive Horizontal Pod Autoscaler Authors.
+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.
@@ -20,7 +20,7 @@ package config
import (
"io"
- "github.com/jthomperoo/custom-pod-autoscaler/config"
+ "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook"
autoscalingv2 "k8s.io/api/autoscaling/v2beta2"
"k8s.io/apimachinery/pkg/util/yaml"
)
@@ -58,22 +58,29 @@ type Config struct {
// 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"`
- Linear *Linear `json:"linear"`
- HoltWinters *HoltWinters `json:"holtWinters"`
+ 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"`
- RuntimeTuningFetch *config.Method `json:"runtimeTuningFetch"`
- SeasonLength int `json:"seasonLength"`
- StoredSeasons int `json:"storedSeasons"`
- Method string `json:"method"`
+ 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
diff --git a/config/config_test.go b/internal/config/config_test.go
similarity index 95%
rename from config/config_test.go
rename to internal/config/config_test.go
index 491f2fa..efaee32 100644
--- a/config/config_test.go
+++ b/internal/config/config_test.go
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Predictive Horizontal Pod Autoscaler Authors.
+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.
@@ -23,7 +23,7 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/config"
+ "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/config"
autoscalingv2 "k8s.io/api/autoscaling/v2beta2"
v1 "k8s.io/api/core/v1"
)
@@ -77,7 +77,7 @@ func TestLoadConfig(t *testing.T) {
CPUInitializationPeriod: 25,
InitialReadinessDelay: 321,
Models: []*config.Model{
- &config.Model{
+ {
Type: "test",
Name: "testPrediction",
PerInterval: 1,
@@ -88,7 +88,7 @@ func TestLoadConfig(t *testing.T) {
},
},
Metrics: []autoscalingv2.MetricSpec{
- autoscalingv2.MetricSpec{
+ {
Type: autoscalingv2.ResourceMetricSourceType,
Resource: &autoscalingv2.ResourceMetricSource{
Name: v1.ResourceCPU,
@@ -134,7 +134,7 @@ func TestLoadConfig(t *testing.T) {
CPUInitializationPeriod: defaultCPUInitializationPeriod,
InitialReadinessDelay: defaultInitialReadinessDelay,
Models: []*config.Model{
- &config.Model{
+ {
Type: "test",
Name: "testPrediction",
PerInterval: 1,
@@ -145,7 +145,7 @@ func TestLoadConfig(t *testing.T) {
},
},
Metrics: []autoscalingv2.MetricSpec{
- autoscalingv2.MetricSpec{
+ {
Type: autoscalingv2.ResourceMetricSourceType,
Resource: &autoscalingv2.ResourceMetricSource{
Name: v1.ResourceCPU,
diff --git a/evaluate/evaluate.go b/internal/evaluate/evaluate.go
similarity index 94%
rename from evaluate/evaluate.go
rename to internal/evaluate/evaluate.go
index cfa4800..05d1f10 100644
--- a/evaluate/evaluate.go
+++ b/internal/evaluate/evaluate.go
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Predictive Horizontal Pod Autoscaler Authors.
+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.
@@ -26,9 +26,9 @@ import (
cpaevaluate "github.com/jthomperoo/custom-pod-autoscaler/evaluate"
hpaevaluate "github.com/jthomperoo/horizontal-pod-autoscaler/evaluate"
"github.com/jthomperoo/horizontal-pod-autoscaler/metric"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/config"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/prediction"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/stored"
+ "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"
)
// PredictiveEvaluate provides a way to make predictive evaluations
diff --git a/evaluate/evaluate_test.go b/internal/evaluate/evaluate_test.go
similarity index 96%
rename from evaluate/evaluate_test.go
rename to internal/evaluate/evaluate_test.go
index 85cb580..c28bec4 100644
--- a/evaluate/evaluate_test.go
+++ b/internal/evaluate/evaluate_test.go
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Predictive Horizontal Pod Autoscaler Authors.
+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.
@@ -26,11 +26,11 @@ import (
cpaevaluate "github.com/jthomperoo/custom-pod-autoscaler/evaluate"
hpaevaluate "github.com/jthomperoo/horizontal-pod-autoscaler/evaluate"
"github.com/jthomperoo/horizontal-pod-autoscaler/metric"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/config"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/evaluate"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/fake"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/prediction"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/stored"
+ "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) {
@@ -92,7 +92,7 @@ func TestGetEvaluation(t *testing.T) {
},
&config.Config{
Models: []*config.Model{
- &config.Model{
+ {
Type: "fake",
},
},
@@ -128,7 +128,7 @@ func TestGetEvaluation(t *testing.T) {
},
&config.Config{
Models: []*config.Model{
- &config.Model{
+ {
Type: "fake",
},
},
@@ -172,7 +172,7 @@ func TestGetEvaluation(t *testing.T) {
},
&config.Config{
Models: []*config.Model{
- &config.Model{
+ {
Type: "fake",
},
},
@@ -211,7 +211,7 @@ func TestGetEvaluation(t *testing.T) {
},
&config.Config{
Models: []*config.Model{
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
},
@@ -251,7 +251,7 @@ func TestGetEvaluation(t *testing.T) {
},
&config.Config{
Models: []*config.Model{
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
},
@@ -294,7 +294,7 @@ func TestGetEvaluation(t *testing.T) {
},
&config.Config{
Models: []*config.Model{
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
},
@@ -340,7 +340,7 @@ func TestGetEvaluation(t *testing.T) {
},
&config.Config{
Models: []*config.Model{
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
},
@@ -389,7 +389,7 @@ func TestGetEvaluation(t *testing.T) {
},
&config.Config{
Models: []*config.Model{
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
},
@@ -441,7 +441,7 @@ func TestGetEvaluation(t *testing.T) {
},
&config.Config{
Models: []*config.Model{
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
},
@@ -496,7 +496,7 @@ func TestGetEvaluation(t *testing.T) {
},
&config.Config{
Models: []*config.Model{
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
},
@@ -554,12 +554,12 @@ func TestGetEvaluation(t *testing.T) {
},
&config.Config{
Models: []*config.Model{
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
Name: "lower",
},
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
Name: "higher",
@@ -621,12 +621,12 @@ func TestGetEvaluation(t *testing.T) {
},
&config.Config{
Models: []*config.Model{
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
Name: "lower",
},
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
Name: "higher",
@@ -688,12 +688,12 @@ func TestGetEvaluation(t *testing.T) {
},
&config.Config{
Models: []*config.Model{
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
Name: "lower",
},
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
Name: "higher",
@@ -755,12 +755,12 @@ func TestGetEvaluation(t *testing.T) {
},
&config.Config{
Models: []*config.Model{
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
Name: "lower",
},
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
Name: "higher",
@@ -828,22 +828,22 @@ func TestGetEvaluation(t *testing.T) {
},
&config.Config{
Models: []*config.Model{
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
Name: "a",
},
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
Name: "b",
},
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
Name: "c",
},
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
Name: "d",
@@ -914,27 +914,27 @@ func TestGetEvaluation(t *testing.T) {
},
&config.Config{
Models: []*config.Model{
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
Name: "a",
},
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
Name: "b",
},
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
Name: "c",
},
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
Name: "d",
},
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
Name: "e",
@@ -993,7 +993,7 @@ func TestGetEvaluation(t *testing.T) {
},
&config.Config{
Models: []*config.Model{
- &config.Model{
+ {
Type: "fake",
PerInterval: 3,
Name: "test",
diff --git a/internal/fake/algorithm.go b/internal/fake/algorithm.go
new file mode 100644
index 0000000..0b5fb26
--- /dev/null
+++ b/internal/fake/algorithm.go
@@ -0,0 +1,27 @@
+/*
+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
+
+// Run (fake) provides a way to insert functionality into an algorithm Runner
+type Run struct {
+ RunAlgorithmWithValueReactor func(algorithmPath string, value string, timeout int) (string, error)
+}
+
+// RunAlgorithmWithValue calls the fake Runner function
+func (f *Run) RunAlgorithmWithValue(algorithmPath string, value string, timeout int) (string, error) {
+ return f.RunAlgorithmWithValueReactor(algorithmPath, value, timeout)
+}
diff --git a/internal/fake/algorithm_test.go b/internal/fake/algorithm_test.go
new file mode 100644
index 0000000..fb9a082
--- /dev/null
+++ b/internal/fake/algorithm_test.go
@@ -0,0 +1,83 @@
+/*
+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
new file mode 100644
index 0000000..e020bb2
--- /dev/null
+++ b/internal/fake/evaluater.go
@@ -0,0 +1,32 @@
+/*
+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/evaluate"
+ "github.com/jthomperoo/horizontal-pod-autoscaler/metric"
+)
+
+// Evaluater (fake) provides a way to insert functionality into a Evaluater
+type Evaluater struct {
+ GetEvaluationReactor func(gatheredMetrics []*metric.Metric) (*evaluate.Evaluation, error)
+}
+
+// GetEvaluation calls the fake Evaluater function
+func (f *Evaluater) GetEvaluation(gatheredMetrics []*metric.Metric) (*evaluate.Evaluation, error) {
+ return f.GetEvaluationReactor(gatheredMetrics)
+}
diff --git a/fake/evaluater_test.go b/internal/fake/evaluater_test.go
similarity index 70%
rename from fake/evaluater_test.go
rename to internal/fake/evaluater_test.go
index a1f1321..8441953 100644
--- a/fake/evaluater_test.go
+++ b/internal/fake/evaluater_test.go
@@ -1,3 +1,19 @@
+/*
+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 (
@@ -7,7 +23,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/jthomperoo/custom-pod-autoscaler/evaluate"
"github.com/jthomperoo/horizontal-pod-autoscaler/metric"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/fake"
+ "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/fake"
)
func TestEvaluater_GetEvaluation(t *testing.T) {
diff --git a/internal/fake/hook.go b/internal/fake/hook.go
new file mode 100644
index 0000000..7cc2f12
--- /dev/null
+++ b/internal/fake/hook.go
@@ -0,0 +1,35 @@
+/*
+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/predictive-horizontal-pod-autoscaler/internal/hook"
+
+// Execute (fake) provides a way to insert functionality into a hook executer
+type Execute struct {
+ ExecuteWithValueReactor func(definition *hook.Definition, value string) (string, error)
+ GetTypeReactor func() string
+}
+
+// ExecuteWithValue calls the fake Executer function
+func (e *Execute) ExecuteWithValue(definition *hook.Definition, value string) (string, error) {
+ return e.ExecuteWithValueReactor(definition, value)
+}
+
+// GetType calls the fake Executer function
+func (e *Execute) GetType() string {
+ return e.GetTypeReactor()
+}
diff --git a/internal/fake/hook_test.go b/internal/fake/hook_test.go
new file mode 100644
index 0000000..480fc26
--- /dev/null
+++ b/internal/fake/hook_test.go
@@ -0,0 +1,114 @@
+/*
+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/fake/predicter.go b/internal/fake/predicter.go
similarity index 54%
rename from fake/predicter.go
rename to internal/fake/predicter.go
index 354ca22..48e9dd9 100644
--- a/fake/predicter.go
+++ b/internal/fake/predicter.go
@@ -1,8 +1,24 @@
+/*
+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/predictive-horizontal-pod-autoscaler/config"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/stored"
+ "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/config"
+ "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/stored"
)
// Predicter (fake) provides a way to insert functionality into a Predicter
diff --git a/fake/predicter_test.go b/internal/fake/predicter_test.go
similarity index 80%
rename from fake/predicter_test.go
rename to internal/fake/predicter_test.go
index 31c384a..6d441fc 100644
--- a/fake/predicter_test.go
+++ b/internal/fake/predicter_test.go
@@ -1,3 +1,18 @@
+/*
+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 (
@@ -5,9 +20,9 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/config"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/fake"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/stored"
+ "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) {
diff --git a/fake/stored.go b/internal/fake/stored.go
similarity index 66%
rename from fake/stored.go
rename to internal/fake/stored.go
index f8ef004..43000b6 100644
--- a/fake/stored.go
+++ b/internal/fake/stored.go
@@ -1,8 +1,24 @@
+/*
+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/evaluate"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/stored"
+ "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/stored"
)
// Store (fake) provides a way to insert functionality into a Store
diff --git a/fake/stored_test.go b/internal/fake/stored_test.go
similarity index 88%
rename from fake/stored_test.go
rename to internal/fake/stored_test.go
index 5041d99..8ff10fa 100644
--- a/fake/stored_test.go
+++ b/internal/fake/stored_test.go
@@ -1,3 +1,19 @@
+/*
+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 (
@@ -6,8 +22,8 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/jthomperoo/custom-pod-autoscaler/evaluate"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/fake"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/stored"
+ "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/fake"
+ "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/stored"
)
func TestStore_GetEvaluation(t *testing.T) {
@@ -39,7 +55,7 @@ func TestStore_GetEvaluation(t *testing.T) {
{
"Return evaluation",
[]*stored.Evaluation{
- &stored.Evaluation{
+ {
ID: 3,
Evaluation: stored.DBEvaluation{
TargetReplicas: 2,
@@ -50,7 +66,7 @@ func TestStore_GetEvaluation(t *testing.T) {
fake.Store{
GetEvaluationReactor: func(model string) ([]*stored.Evaluation, error) {
return []*stored.Evaluation{
- &stored.Evaluation{
+ {
ID: 3,
Evaluation: stored.DBEvaluation{
TargetReplicas: 2,
diff --git a/internal/hook/hook.go b/internal/hook/hook.go
new file mode 100644
index 0000000..7b6c3e2
--- /dev/null
+++ b/internal/hook/hook.go
@@ -0,0 +1,81 @@
+/*
+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 hook provides standardised way to trigger hooks and provide values at different points in execution
+package hook
+
+import (
+ "fmt"
+)
+
+// 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)
+ 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
new file mode 100644
index 0000000..b6a5caa
--- /dev/null
+++ b/internal/hook/hook_test.go
@@ -0,0 +1,187 @@
+/*
+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.
+*/
+// +build unit
+
+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
new file mode 100644
index 0000000..b2f81a0
--- /dev/null
+++ b/internal/hook/http/http.go
@@ -0,0 +1,121 @@
+/*
+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 http handles interactions over HTTP
+package http
+
+import (
+ "context"
+ "fmt"
+ "io/ioutil"
+ gohttp "net/http"
+ "strings"
+ "time"
+
+ "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook"
+)
+
+// Type http represents an HTTP request
+const Type = "http"
+
+const (
+ // BodyParameterMode is a configuration flag specifying the value passed via
+ // HTTP should be through the HTTP body parameter
+ BodyParameterMode = "body"
+ // QueryParameterMode is a configuration flag specifying the value passed
+ // via HTTP should be through the HTTP query parameter
+ QueryParameterMode = "query"
+)
+
+const (
+ // QueryParameterKey is the key of the query parameter passed if the query
+ // parameter mode is used, in the form https://example.com?value="DATA"
+ QueryParameterKey = "value"
+)
+
+// Execute represents a way to execute HTTP requests with values as parameters.
+type Execute struct {
+ Client gohttp.Client
+}
+
+// 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) {
+ if definition.HTTP == nil {
+ return "", fmt.Errorf("Missing required 'http' configuration on hook definition")
+ }
+
+ // Set up request using hook definition and URL provided
+ req, err := gohttp.NewRequest(definition.HTTP.Method, definition.HTTP.URL, nil)
+ if err != nil {
+ return "", err
+ }
+
+ // Set parameter value, based on configuration option
+ switch definition.HTTP.ParameterMode {
+ case BodyParameterMode:
+ // Set body parameter
+ req.Body = ioutil.NopCloser(strings.NewReader(value))
+ case QueryParameterMode:
+ // Set query parameter
+ query := req.URL.Query()
+ query.Add(QueryParameterKey, value)
+ req.URL.RawQuery = query.Encode()
+ default:
+ return "", fmt.Errorf("Unknown parameter mode '%s'", definition.HTTP.ParameterMode)
+ }
+
+ // Add headers
+ for key, val := range definition.HTTP.Headers {
+ req.Header.Add(key, val)
+ }
+
+ // Set up a context to provide an HTTP request timeout
+ ctx, cancel := context.WithTimeout(context.Background(), time.Duration(definition.Timeout)*time.Millisecond)
+ defer cancel()
+
+ // Make request
+ resp, err := e.Client.Do(req.WithContext(ctx))
+ if err != nil {
+ return "", err
+ }
+
+ // Read the response body
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+
+ // Check for a successful response code
+ success := false
+ for _, successCode := range definition.HTTP.SuccessCodes {
+ if resp.StatusCode == successCode {
+ success = true
+ break
+ }
+ }
+
+ if !success {
+ return "", fmt.Errorf("HTTP request failed, status: [%d], response: '%s'", resp.StatusCode, string(body))
+ }
+
+ return string(body), nil
+}
+
+// GetType returns the http executer type
+func (e *Execute) GetType() string {
+ return Type
+}
diff --git a/internal/hook/http/http_test.go b/internal/hook/http/http_test.go
new file mode 100644
index 0000000..87f8a01
--- /dev/null
+++ b/internal/hook/http/http_test.go
@@ -0,0 +1,455 @@
+/*
+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.
+*/
+// +build unit
+
+package http_test
+
+import (
+ "errors"
+ "fmt"
+ "io/ioutil"
+ gohttp "net/http"
+ "net/http/httptest"
+ "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/http"
+)
+
+type testHTTPClient struct {
+ RoundTripReactor func(req *gohttp.Request) (*gohttp.Response, error)
+}
+
+func (f *testHTTPClient) RoundTrip(req *gohttp.Request) (*gohttp.Response, error) {
+ return f.RoundTripReactor(req)
+}
+
+type testReader struct {
+ ReadReactor func(p []byte) (n int, err error)
+ CloseReactor func() error
+}
+
+func (f *testReader) Read(p []byte) (n int, err error) {
+ return f.ReadReactor(p)
+}
+
+func (f *testReader) Close() error {
+ return f.CloseReactor()
+}
+
+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
+ definition *hook.Definition
+ value string
+ execute http.Execute
+ }{
+ {
+ "Fail, missing HTTP method configuration",
+ "",
+ errors.New(`Missing required 'http' configuration on hook definition`),
+ &hook.Definition{
+ Type: "http",
+ },
+ "test",
+ http.Execute{},
+ },
+ {
+ "Fail, invalid HTTP method",
+ "",
+ errors.New(`net/http: invalid method "*?"`),
+ &hook.Definition{
+ Type: "http",
+ HTTP: &hook.HTTP{
+ Method: "*?",
+ URL: "https://custompodautoscaler.com",
+ },
+ },
+ "test",
+ http.Execute{},
+ },
+ {
+ "Fail, unknown parameter mode",
+ "",
+ errors.New(`Unknown parameter mode 'unknown'`),
+ &hook.Definition{
+ Type: "http",
+ HTTP: &hook.HTTP{
+ Method: "GET",
+ URL: "https://custompodautoscaler.com",
+ ParameterMode: "unknown",
+ },
+ },
+ "test",
+ http.Execute{},
+ },
+ {
+ "Fail, request fail",
+ "",
+ errors.New(`Get "https://custompodautoscaler.com?value=test": Test network error!`),
+ &hook.Definition{
+ Type: "http",
+ HTTP: &hook.HTTP{
+ Method: "GET",
+ URL: "https://custompodautoscaler.com",
+ ParameterMode: "query",
+ },
+ },
+ "test",
+ http.Execute{
+ Client: gohttp.Client{
+ Transport: &testHTTPClient{
+ func(req *gohttp.Request) (*gohttp.Response, error) {
+ return nil, errors.New("Test network error!")
+ },
+ },
+ },
+ },
+ },
+ {
+ "Fail, timeout",
+ "",
+ errors.New(`Get "https://custompodautoscaler.com?value=test": context deadline exceeded`),
+ &hook.Definition{
+ Type: "http",
+ HTTP: &hook.HTTP{
+ Method: "GET",
+ URL: "https://custompodautoscaler.com",
+ ParameterMode: "query",
+ },
+ Timeout: 5,
+ },
+ "test",
+ http.Execute{
+ Client: func() gohttp.Client {
+ testserver := httptest.NewServer(gohttp.HandlerFunc(func(rw gohttp.ResponseWriter, req *gohttp.Request) {
+ time.Sleep(10 * time.Millisecond)
+ // Send response to be tested
+ rw.Write([]byte(`OK`))
+ }))
+ defer testserver.Close()
+
+ return *testserver.Client()
+ }(),
+ },
+ },
+ {
+ "Fail, invalid response body",
+ "",
+ errors.New(`Fail to read body!`),
+ &hook.Definition{
+ Type: "http",
+ HTTP: &hook.HTTP{
+ Method: "GET",
+ URL: "https://custompodautoscaler.com",
+ ParameterMode: "query",
+ },
+ },
+ "test",
+ http.Execute{
+ Client: gohttp.Client{
+ Transport: &testHTTPClient{
+ func(req *gohttp.Request) (*gohttp.Response, error) {
+ resp := &gohttp.Response{
+ Body: &testReader{
+ ReadReactor: func(p []byte) (n int, err error) {
+ return 0, errors.New("Fail to read body!")
+ },
+ },
+ Header: gohttp.Header{},
+ }
+ resp.Header.Set("Content-Length", "1")
+ return resp, nil
+ },
+ },
+ },
+ },
+ },
+ {
+ "Fail, bad response code",
+ "",
+ errors.New(`HTTP request failed, status: [400], response: 'bad request!'`),
+ &hook.Definition{
+ Type: "http",
+ HTTP: &hook.HTTP{
+ Method: "GET",
+ URL: "https://custompodautoscaler.com",
+ ParameterMode: "query",
+ SuccessCodes: []int{
+ 200,
+ 202,
+ },
+ },
+ },
+ "test",
+ http.Execute{
+ Client: gohttp.Client{
+ Transport: &testHTTPClient{
+ func(req *gohttp.Request) (*gohttp.Response, error) {
+ return &gohttp.Response{
+ Body: ioutil.NopCloser(strings.NewReader("bad request!")),
+ Header: gohttp.Header{},
+ StatusCode: 400,
+ }, nil
+ },
+ },
+ },
+ },
+ },
+ {
+ "Success, POST, body parameter, 3 headers",
+ "Success!",
+ nil,
+ &hook.Definition{
+ Type: "http",
+ HTTP: &hook.HTTP{
+ Method: "POST",
+ URL: "https://custompodautoscaler.com",
+ ParameterMode: "body",
+ Headers: map[string]string{
+ "a": "testa",
+ "b": "testb",
+ "c": "testc",
+ },
+ SuccessCodes: []int{
+ 200,
+ 202,
+ },
+ },
+ },
+ "test",
+ http.Execute{
+ Client: gohttp.Client{
+ Transport: &testHTTPClient{
+ func(req *gohttp.Request) (*gohttp.Response, error) {
+
+ if !cmp.Equal(req.Method, "POST") {
+ return nil, fmt.Errorf("Invalid method, expected 'POST', got '%s'", req.Method)
+ }
+
+ // Read the request body
+ body, err := ioutil.ReadAll(req.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ if !cmp.Equal(req.Header.Get("a"), "testa") {
+ return nil, fmt.Errorf("Missing header 'a'")
+ }
+ if !cmp.Equal(req.Header.Get("b"), "testb") {
+ return nil, fmt.Errorf("Missing header 'a'")
+ }
+ if !cmp.Equal(req.Header.Get("c"), "testc") {
+ return nil, fmt.Errorf("Missing header 'a'")
+ }
+ if !cmp.Equal(string(body), "test") {
+ return nil, fmt.Errorf("Invalid body, expected 'test', got '%s'", body)
+ }
+
+ return &gohttp.Response{
+ Body: ioutil.NopCloser(strings.NewReader("Success!")),
+ Header: gohttp.Header{},
+ StatusCode: 200,
+ }, nil
+ },
+ },
+ },
+ },
+ },
+ {
+ "Success, GET, query parameter, 1 header",
+ "Success!",
+ nil,
+ &hook.Definition{
+ Type: "http",
+ HTTP: &hook.HTTP{
+ Method: "GET",
+ URL: "https://custompodautoscaler.com",
+ ParameterMode: "query",
+ Headers: map[string]string{
+ "a": "testa",
+ },
+ SuccessCodes: []int{
+ 200,
+ 202,
+ },
+ },
+ },
+ "test",
+ http.Execute{
+ Client: gohttp.Client{
+ Transport: &testHTTPClient{
+ func(req *gohttp.Request) (*gohttp.Response, error) {
+
+ if !cmp.Equal(req.Method, "GET") {
+ return nil, fmt.Errorf("Invalid method, expected 'GET', got '%s'", req.Method)
+ }
+
+ query := req.URL.Query()
+
+ if !cmp.Equal(query.Get("value"), "test") {
+ return nil, fmt.Errorf("Invalid query param, expected 'test', got '%s'", query.Get("value"))
+ }
+
+ if !cmp.Equal(req.Header.Get("a"), "testa") {
+ return nil, fmt.Errorf("Missing header 'a'")
+ }
+
+ return &gohttp.Response{
+ Body: ioutil.NopCloser(strings.NewReader("Success!")),
+ Header: gohttp.Header{},
+ StatusCode: 200,
+ }, nil
+ },
+ },
+ },
+ },
+ },
+ {
+ "Success, GET, query parameter, 0 headers",
+ "Success!",
+ nil,
+ &hook.Definition{
+ Type: "http",
+ HTTP: &hook.HTTP{
+ Method: "GET",
+ URL: "https://custompodautoscaler.com",
+ ParameterMode: "query",
+ SuccessCodes: []int{
+ 200,
+ 202,
+ },
+ },
+ },
+ "test",
+ http.Execute{
+ Client: gohttp.Client{
+ Transport: &testHTTPClient{
+ func(req *gohttp.Request) (*gohttp.Response, error) {
+
+ if !cmp.Equal(req.Method, "GET") {
+ return nil, fmt.Errorf("Invalid method, expected 'GET', got '%s'", req.Method)
+ }
+
+ query := req.URL.Query()
+
+ if !cmp.Equal(query.Get("value"), "test") {
+ return nil, fmt.Errorf("Invalid query param, expected 'test', got '%s'", query.Get("value"))
+ }
+
+ return &gohttp.Response{
+ Body: ioutil.NopCloser(strings.NewReader("Success!")),
+ Header: gohttp.Header{},
+ StatusCode: 200,
+ }, nil
+ },
+ },
+ },
+ },
+ },
+ {
+ "Success, PUT, body parameter, 0 headers",
+ "Success!",
+ nil,
+ &hook.Definition{
+ Type: "http",
+ HTTP: &hook.HTTP{
+ Method: "PUT",
+ URL: "https://custompodautoscaler.com",
+ ParameterMode: "body",
+ SuccessCodes: []int{
+ 200,
+ 202,
+ },
+ },
+ },
+ "test",
+ http.Execute{
+ Client: gohttp.Client{
+ Transport: &testHTTPClient{
+ func(req *gohttp.Request) (*gohttp.Response, error) {
+
+ if !cmp.Equal(req.Method, "PUT") {
+ return nil, fmt.Errorf("Invalid method, expected 'PUT', got '%s'", req.Method)
+ }
+
+ // Read the request body
+ body, err := ioutil.ReadAll(req.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ if !cmp.Equal(string(body), "test") {
+ return nil, fmt.Errorf("Invalid body, expected 'test', got '%s'", body)
+ }
+
+ return &gohttp.Response{
+ Body: ioutil.NopCloser(strings.NewReader("Success!")),
+ Header: gohttp.Header{},
+ StatusCode: 200,
+ }, nil
+ },
+ },
+ },
+ },
+ },
+ }
+ 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("metrics mismatch (-want +got):\n%s", cmp.Diff(test.expected, result))
+ }
+ })
+ }
+}
+
+func TestExecute_GetType(t *testing.T) {
+ var tests = []struct {
+ description string
+ expected string
+ execute http.Execute
+ }{
+ {
+ "Return type",
+ "http",
+ http.Execute{},
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.description, func(t *testing.T) {
+ result := test.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/shell/shell.go b/internal/hook/shell/shell.go
new file mode 100644
index 0000000..82dbe12
--- /dev/null
+++ b/internal/hook/shell/shell.go
@@ -0,0 +1,74 @@
+// 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
new file mode 100644
index 0000000..02dc44e
--- /dev/null
+++ b/internal/hook/shell/shell_test.go
@@ -0,0 +1,326 @@
+/*
+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.
+*/
+// +build unit
+
+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.Fprintf(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.Fprintf(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/prediction/holtwinters/holtwinters.go b/internal/prediction/holtwinters/holtwinters.go
similarity index 50%
rename from prediction/holtwinters/holtwinters.go
rename to internal/prediction/holtwinters/holtwinters.go
index 45b9280..3c04899 100644
--- a/prediction/holtwinters/holtwinters.go
+++ b/internal/prediction/holtwinters/holtwinters.go
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Predictive Horizontal Pod Autoscaler Authors.
+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.
@@ -19,40 +19,51 @@ package holtwinters
import (
"encoding/json"
"errors"
- "fmt"
- "math"
"sort"
+ "strconv"
- "github.com/jthomperoo/custom-pod-autoscaler/execute"
- "github.com/jthomperoo/holtwinters"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/config"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/stored"
+ "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/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 (
- // MethodAdditive specifies a HoltWinters time series prediction using the additive method
- MethodAdditive = "additive"
- // MethodMultiplicative specifies a HoltWinters time series prediction using the multiplicative method
- MethodMultiplicative = "multiplicative"
+ defaultTimeout = 30000
)
// Predict provides logic for using Linear Regression to make a prediction
type Predict struct {
- Execute execute.Executer
+ Execute hook.Executer
+ Runner algorithm.Runner
+}
+
+type holtWintersParametersParameters struct {
+ Series []float64 `json:"series"`
+ Alpha float64 `json:"alpha"`
+ Beta float64 `json:"beta"`
+ Gamma float64 `json:"gamma"`
+ Trend string `json:"trend"`
+ Seasonal string `json:"seasonal"`
+ SeasonalPeriods int `json:"seasonalPeriods"`
+ DampedTrend *bool `json:"dampedTrend,omitempty"`
+ InitializationMethod *string `json:"initializationMethod,omitempty"`
+ InitialLevel *float64 `json:"initialLevel,omitempty"`
+ InitialTrend *float64 `json:"initialTrend,omitempty"`
+ InitialSeasonal *float64 `json:"initialSeasonal,omitempty"`
}
-// RunTimeTuningFetchRequest defines the request value sent as part of the method to determine the runtime Holt-Winters
-// values
-type RunTimeTuningFetchRequest struct {
+type runTimeTuningFetchHookRequest struct {
Model *config.Model `json:"model"`
Evaluations []*stored.Evaluation `json:"evaluations"`
}
-// RunTimeTuningFetchResult defines the expected response from the method that specifies the runtime Holt-Winters values
-type RunTimeTuningFetchResult struct {
+type runTimeTuningFetchHookResult struct {
Alpha *float64 `json:"alpha"`
Beta *float64 `json:"beta"`
Gamma *float64 `json:"gamma"`
@@ -60,12 +71,13 @@ type RunTimeTuningFetchResult struct {
// 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) {
- if model.HoltWinters == nil {
- return 0, errors.New("No HoltWinters configuration provided for model")
+ err := p.validate(model)
+ if err != nil {
+ return 0, err
}
- // If less than a full season of data, return zero without error
- if len(evaluations) < model.HoltWinters.SeasonLength {
+ // 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) {
return 0, nil
}
@@ -73,10 +85,10 @@ func (p *Predict) GetPrediction(model *config.Model, evaluations []*stored.Evalu
beta := model.HoltWinters.Beta
gamma := model.HoltWinters.Gamma
- if model.HoltWinters.RuntimeTuningFetch != nil {
+ if model.HoltWinters.RuntimeTuningFetchHook != nil {
// Convert request into JSON string
- request, err := json.Marshal(&RunTimeTuningFetchRequest{
+ request, err := json.Marshal(&runTimeTuningFetchHookRequest{
Model: model,
Evaluations: evaluations,
})
@@ -86,13 +98,13 @@ func (p *Predict) GetPrediction(model *config.Model, evaluations []*stored.Evalu
}
// Request runtime tuning values
- hookResult, err := p.Execute.ExecuteWithValue(model.HoltWinters.RuntimeTuningFetch, string(request))
+ hookResult, err := p.Execute.ExecuteWithValue(model.HoltWinters.RuntimeTuningFetchHook, string(request))
if err != nil {
return 0, err
}
// Parse result
- var result RunTimeTuningFetchResult
+ var result runTimeTuningFetchHookResult
err = json.Unmarshal([]byte(hookResult), &result)
if err != nil {
return 0, err
@@ -125,30 +137,41 @@ func (p *Predict) GetPrediction(model *config.Model, evaluations []*stored.Evalu
series[i] = float64(evaluation.Evaluation.TargetReplicas)
}
- var prediction []float64
- var err error
+ parameters, err := json.Marshal(holtWintersParametersParameters{
+ Series: series,
+ Alpha: *alpha,
+ Beta: *beta,
+ Gamma: *gamma,
+ Trend: model.HoltWinters.Trend,
+ Seasonal: model.HoltWinters.Seasonal,
+ SeasonalPeriods: model.HoltWinters.SeasonalPeriods,
+ DampedTrend: model.HoltWinters.DampedTrend,
+ InitializationMethod: model.HoltWinters.InitializationMethod,
+ InitialLevel: model.HoltWinters.InitialLevel,
+ InitialTrend: model.HoltWinters.InitialTrend,
+ InitialSeasonal: model.HoltWinters.InitialSeasonal,
+ })
+ if err != nil {
+ // Should not occur, panic
+ panic(err)
+ }
- switch model.HoltWinters.Method {
- case MethodAdditive:
- // Build prediction 1 ahead
- prediction, err = holtwinters.PredictAdditive(series, model.HoltWinters.SeasonLength, *alpha, *beta, *gamma, 1)
- if err != nil {
- return 0, err
- }
- break
- case MethodMultiplicative:
- // Build prediction 1 ahead
- prediction, err = holtwinters.PredictMultiplicative(series, model.HoltWinters.SeasonLength, *alpha, *beta, *gamma, 1)
- if err != nil {
- return 0, err
- }
- break
- default:
- return 0, fmt.Errorf("Unknown HoltWinters method '%s'", model.HoltWinters.Method)
+ timeout := defaultTimeout
+ if model.CalculationTimeout != nil {
+ timeout = *model.CalculationTimeout
}
- // Return last value in prediction
- return int32(math.Ceil(prediction[len(prediction)-1])), nil
+ value, err := p.Runner.RunAlgorithmWithValue(algorithmPath, string(parameters), timeout)
+ if err != nil {
+ return 0, err
+ }
+
+ prediction, err := strconv.Atoi(value)
+ if err != nil {
+ return 0, err
+ }
+
+ return int32(prediction), nil
}
// GetIDsToRemove provides the list of stored evaluation IDs to remove, if there are too many stored seasons
@@ -166,9 +189,9 @@ func (p *Predict) GetIDsToRemove(model *config.Model, evaluations []*stored.Eval
var markedForRemove []int
// If there are too many stored seasons, remove the oldest ones
- seasonsToRemove := len(evaluations)/model.HoltWinters.SeasonLength - model.HoltWinters.StoredSeasons
+ seasonsToRemove := len(evaluations)/model.HoltWinters.SeasonalPeriods - model.HoltWinters.StoredSeasons
for i := 0; i < seasonsToRemove; i++ {
- for j := 0; j < model.HoltWinters.SeasonLength; j++ {
+ for j := 0; j < model.HoltWinters.SeasonalPeriods; j++ {
markedForRemove = append(markedForRemove, evaluations[i+j].ID)
}
}
@@ -179,3 +202,15 @@ func (p *Predict) GetIDsToRemove(model *config.Model, evaluations []*stored.Eval
func (p *Predict) GetType() string {
return Type
}
+
+func (p *Predict) validate(model *config.Model) error {
+ if model.HoltWinters == nil {
+ return errors.New("No HoltWinters configuration provided for model")
+ }
+
+ if model.HoltWinters.Trend == "" {
+ return errors.New("No required 'trend' value provided for model")
+ }
+
+ return nil
+}
diff --git a/prediction/holtwinters/holtwinters_test.go b/internal/prediction/holtwinters/holtwinters_test.go
similarity index 57%
rename from prediction/holtwinters/holtwinters_test.go
rename to internal/prediction/holtwinters/holtwinters_test.go
index 1035589..c0f368c 100644
--- a/prediction/holtwinters/holtwinters_test.go
+++ b/internal/prediction/holtwinters/holtwinters_test.go
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Predictive Horizontal Pod Autoscaler Authors.
+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.
@@ -22,12 +22,12 @@ import (
"time"
"github.com/google/go-cmp/cmp"
- cpaconfig "github.com/jthomperoo/custom-pod-autoscaler/config"
- "github.com/jthomperoo/custom-pod-autoscaler/fake"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/config"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/prediction/holtwinters"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/prediction/linear"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/stored"
+ "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/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"
)
func float64Ptr(val float64) *float64 {
@@ -58,6 +58,67 @@ func TestPredict_GetPrediction(t *testing.T) {
&config.Model{},
[]*stored.Evaluation{},
},
+ {
+ "Success, less than 10 + 2 * (seasonal_periods // 2) observations",
+ 0,
+ nil,
+ &holtwinters.Predict{},
+ &config.Model{
+ Type: linear.Type,
+ HoltWinters: &config.HoltWinters{
+ Alpha: float64Ptr(0.9),
+ Beta: float64Ptr(0.9),
+ Gamma: float64Ptr(0.9),
+ SeasonalPeriods: 3,
+ StoredSeasons: 3,
+ Trend: "add",
+ },
+ },
+ []*stored.Evaluation{
+ {
+ Created: time.Now().UTC().Add(time.Duration(-80) * time.Second),
+ Evaluation: stored.DBEvaluation{
+ TargetReplicas: 1,
+ },
+ },
+ {
+ Created: time.Now().UTC().Add(time.Duration(-70) * time.Second),
+ Evaluation: stored.DBEvaluation{
+ TargetReplicas: 3,
+ },
+ },
+ {
+ Created: time.Now().UTC().Add(time.Duration(-60) * time.Second),
+ Evaluation: stored.DBEvaluation{
+ TargetReplicas: 1,
+ },
+ },
+ {
+ Created: time.Now().UTC().Add(time.Duration(-50) * time.Second),
+ Evaluation: stored.DBEvaluation{
+ TargetReplicas: 1,
+ },
+ },
+ {
+ Created: time.Now().UTC().Add(time.Duration(-40) * time.Second),
+ Evaluation: stored.DBEvaluation{
+ TargetReplicas: 3,
+ },
+ },
+ {
+ Created: time.Now().UTC().Add(time.Duration(-30) * time.Second),
+ Evaluation: stored.DBEvaluation{
+ TargetReplicas: 1,
+ },
+ },
+ {
+ Created: time.Now().UTC().Add(time.Duration(-20) * time.Second),
+ Evaluation: stored.DBEvaluation{
+ TargetReplicas: 1,
+ },
+ },
+ },
+ },
{
"Fail, fail to runtime fetch",
0,
@@ -65,7 +126,7 @@ func TestPredict_GetPrediction(t *testing.T) {
&holtwinters.Predict{
Execute: func() *fake.Execute {
execute := fake.Execute{}
- execute.ExecuteWithValueReactor = func(method *cpaconfig.Method, value string) (string, error) {
+ execute.ExecuteWithValueReactor = func(definition *hook.Definition, value string) (string, error) {
return "", errors.New("fail runtime fetch")
}
return &execute
@@ -73,21 +134,58 @@ func TestPredict_GetPrediction(t *testing.T) {
},
&config.Model{
HoltWinters: &config.HoltWinters{
- RuntimeTuningFetch: &cpaconfig.Method{
+ RuntimeTuningFetchHook: &hook.Definition{
Type: "test",
Timeout: 2500,
},
- SeasonLength: 2,
- Method: "additive",
+ SeasonalPeriods: 2,
+ Trend: "add",
+ Seasonal: "add",
},
},
[]*stored.Evaluation{
- &stored.Evaluation{
+ {
ID: 1,
},
- &stored.Evaluation{
+ {
ID: 2,
},
+ {
+ ID: 3,
+ },
+ {
+ ID: 4,
+ },
+ {
+ ID: 5,
+ },
+ {
+ ID: 6,
+ },
+ {
+ ID: 7,
+ },
+ {
+ ID: 8,
+ },
+ {
+ ID: 9,
+ },
+ {
+ ID: 10,
+ },
+ {
+ ID: 11,
+ },
+ {
+ ID: 12,
+ },
+ {
+ ID: 13,
+ },
+ {
+ ID: 14,
+ },
},
},
{
@@ -97,7 +195,7 @@ func TestPredict_GetPrediction(t *testing.T) {
&holtwinters.Predict{
Execute: func() *fake.Execute {
execute := fake.Execute{}
- execute.ExecuteWithValueReactor = func(method *cpaconfig.Method, value string) (string, error) {
+ execute.ExecuteWithValueReactor = func(definition *hook.Definition, value string) (string, error) {
return "invalid json", nil
}
return &execute
@@ -105,21 +203,58 @@ func TestPredict_GetPrediction(t *testing.T) {
},
&config.Model{
HoltWinters: &config.HoltWinters{
- RuntimeTuningFetch: &cpaconfig.Method{
+ RuntimeTuningFetchHook: &hook.Definition{
Type: "test",
Timeout: 2500,
},
- SeasonLength: 2,
- Method: "additive",
+ SeasonalPeriods: 2,
+ Trend: "add",
+ Seasonal: "add",
},
},
[]*stored.Evaluation{
- &stored.Evaluation{
+ {
ID: 1,
},
- &stored.Evaluation{
+ {
ID: 2,
},
+ {
+ ID: 3,
+ },
+ {
+ ID: 4,
+ },
+ {
+ ID: 5,
+ },
+ {
+ ID: 6,
+ },
+ {
+ ID: 7,
+ },
+ {
+ ID: 8,
+ },
+ {
+ ID: 9,
+ },
+ {
+ ID: 10,
+ },
+ {
+ ID: 11,
+ },
+ {
+ ID: 12,
+ },
+ {
+ ID: 13,
+ },
+ {
+ ID: 14,
+ },
},
},
{
@@ -129,19 +264,56 @@ func TestPredict_GetPrediction(t *testing.T) {
&holtwinters.Predict{},
&config.Model{
HoltWinters: &config.HoltWinters{
- Beta: float64Ptr(0.9),
- Gamma: float64Ptr(0.9),
- SeasonLength: 2,
- Method: "invalid",
+ Beta: float64Ptr(0.9),
+ Gamma: float64Ptr(0.9),
+ SeasonalPeriods: 2,
+ Trend: "add",
+ Seasonal: "add",
},
},
[]*stored.Evaluation{
- &stored.Evaluation{
+ {
ID: 1,
},
- &stored.Evaluation{
+ {
ID: 2,
},
+ {
+ ID: 3,
+ },
+ {
+ ID: 4,
+ },
+ {
+ ID: 5,
+ },
+ {
+ ID: 6,
+ },
+ {
+ ID: 7,
+ },
+ {
+ ID: 8,
+ },
+ {
+ ID: 9,
+ },
+ {
+ ID: 10,
+ },
+ {
+ ID: 11,
+ },
+ {
+ ID: 12,
+ },
+ {
+ ID: 13,
+ },
+ {
+ ID: 14,
+ },
},
},
{
@@ -151,19 +323,56 @@ func TestPredict_GetPrediction(t *testing.T) {
&holtwinters.Predict{},
&config.Model{
HoltWinters: &config.HoltWinters{
- Alpha: float64Ptr(0.9),
- Gamma: float64Ptr(0.9),
- SeasonLength: 2,
- Method: "invalid",
+ Alpha: float64Ptr(0.9),
+ Gamma: float64Ptr(0.9),
+ SeasonalPeriods: 2,
+ Trend: "add",
+ Seasonal: "add",
},
},
[]*stored.Evaluation{
- &stored.Evaluation{
+ {
ID: 1,
},
- &stored.Evaluation{
+ {
ID: 2,
},
+ {
+ ID: 3,
+ },
+ {
+ ID: 4,
+ },
+ {
+ ID: 5,
+ },
+ {
+ ID: 6,
+ },
+ {
+ ID: 7,
+ },
+ {
+ ID: 8,
+ },
+ {
+ ID: 9,
+ },
+ {
+ ID: 10,
+ },
+ {
+ ID: 11,
+ },
+ {
+ ID: 12,
+ },
+ {
+ ID: 13,
+ },
+ {
+ ID: 14,
+ },
},
},
{
@@ -173,65 +382,186 @@ func TestPredict_GetPrediction(t *testing.T) {
&holtwinters.Predict{},
&config.Model{
HoltWinters: &config.HoltWinters{
- Alpha: float64Ptr(0.9),
- Beta: float64Ptr(0.9),
- SeasonLength: 2,
- Method: "invalid",
+ Alpha: float64Ptr(0.9),
+ Beta: float64Ptr(0.9),
+ SeasonalPeriods: 2,
+ Trend: "add",
+ Seasonal: "add",
},
},
[]*stored.Evaluation{
- &stored.Evaluation{
+ {
ID: 1,
},
- &stored.Evaluation{
+ {
ID: 2,
},
+ {
+ ID: 3,
+ },
+ {
+ ID: 4,
+ },
+ {
+ ID: 5,
+ },
+ {
+ ID: 6,
+ },
+ {
+ ID: 7,
+ },
+ {
+ ID: 8,
+ },
+ {
+ ID: 9,
+ },
+ {
+ ID: 10,
+ },
+ {
+ ID: 11,
+ },
+ {
+ ID: 12,
+ },
+ {
+ ID: 13,
+ },
+ {
+ ID: 14,
+ },
},
},
{
- "Fail invalid method",
+ "Fail, additive, fail to run holt winters algorithm",
0,
- errors.New("Unknown HoltWinters method 'invalid'"),
- &holtwinters.Predict{},
+ errors.New("holt winters algorithm error"),
+ &holtwinters.Predict{
+ Runner: &fake.Run{
+ RunAlgorithmWithValueReactor: func(algorithmPath, value string, timeout int) (string, error) {
+ return "", errors.New("holt winters algorithm error")
+ },
+ },
+ },
&config.Model{
HoltWinters: &config.HoltWinters{
- Alpha: float64Ptr(0.9),
- Beta: float64Ptr(0.9),
- Gamma: float64Ptr(0.9),
- SeasonLength: 2,
- Method: "invalid",
+ Alpha: float64Ptr(0.9),
+ Beta: float64Ptr(0.9),
+ Gamma: float64Ptr(0.9),
+ SeasonalPeriods: 2,
+ Trend: "additive",
},
},
[]*stored.Evaluation{
- &stored.Evaluation{
+ {
ID: 1,
},
- &stored.Evaluation{
+ {
ID: 2,
},
+ {
+ ID: 3,
+ },
+ {
+ ID: 4,
+ },
+ {
+ ID: 5,
+ },
+ {
+ ID: 6,
+ },
+ {
+ ID: 7,
+ },
+ {
+ ID: 8,
+ },
+ {
+ ID: 9,
+ },
+ {
+ ID: 10,
+ },
+ {
+ ID: 11,
+ },
+ {
+ ID: 12,
+ },
+ {
+ ID: 13,
+ },
+ {
+ ID: 14,
+ },
},
},
{
- "Fail, additive, invalid parameters",
+ "Fail, additive, holt winters algorithm invalid response",
0,
- errors.New("Invalid parameter for prediction; alpha must be between 0 and 1, is -1.000000"),
- &holtwinters.Predict{},
+ errors.New(`strconv.Atoi: parsing "invalid": invalid syntax`),
+ &holtwinters.Predict{
+ Runner: &fake.Run{
+ RunAlgorithmWithValueReactor: func(algorithmPath, value string, timeout int) (string, error) {
+ return "invalid", nil
+ },
+ },
+ },
&config.Model{
HoltWinters: &config.HoltWinters{
- SeasonLength: 2,
- Alpha: float64Ptr(-1.0),
- Beta: float64Ptr(0.9),
- Gamma: float64Ptr(0.9),
- Method: "additive",
+ Alpha: float64Ptr(0.9),
+ Beta: float64Ptr(0.9),
+ Gamma: float64Ptr(0.9),
+ SeasonalPeriods: 2,
+ Trend: "additive",
},
},
[]*stored.Evaluation{
- &stored.Evaluation{
+ {
ID: 1,
},
- &stored.Evaluation{
+ {
ID: 2,
},
+ {
+ ID: 3,
+ },
+ {
+ ID: 4,
+ },
+ {
+ ID: 5,
+ },
+ {
+ ID: 6,
+ },
+ {
+ ID: 7,
+ },
+ {
+ ID: 8,
+ },
+ {
+ ID: 9,
+ },
+ {
+ ID: 10,
+ },
+ {
+ ID: 11,
+ },
+ {
+ ID: 12,
+ },
+ {
+ ID: 13,
+ },
+ {
+ ID: 14,
+ },
},
},
{
@@ -239,9 +569,14 @@ func TestPredict_GetPrediction(t *testing.T) {
0,
nil,
&holtwinters.Predict{
+ Runner: &fake.Run{
+ RunAlgorithmWithValueReactor: func(algorithmPath, value string, timeout int) (string, error) {
+ return "0", nil
+ },
+ },
Execute: func() *fake.Execute {
execute := fake.Execute{}
- execute.ExecuteWithValueReactor = func(method *cpaconfig.Method, value string) (string, error) {
+ execute.ExecuteWithValueReactor = func(definition *hook.Definition, value string) (string, error) {
return `{}`, nil
}
return &execute
@@ -249,34 +584,76 @@ func TestPredict_GetPrediction(t *testing.T) {
},
&config.Model{
HoltWinters: &config.HoltWinters{
- RuntimeTuningFetch: &cpaconfig.Method{
+ RuntimeTuningFetchHook: &hook.Definition{
Type: "test",
Timeout: 2500,
},
- Alpha: float64Ptr(0.9),
- Beta: float64Ptr(0.9),
- Gamma: float64Ptr(0.9),
- SeasonLength: 2,
- Method: "additive",
+ Alpha: float64Ptr(0.9),
+ Beta: float64Ptr(0.9),
+ Gamma: float64Ptr(0.9),
+ SeasonalPeriods: 2,
+ Trend: "add",
+ Seasonal: "add",
},
},
[]*stored.Evaluation{
- &stored.Evaluation{
+ {
ID: 1,
},
- &stored.Evaluation{
+ {
ID: 2,
},
+ {
+ ID: 3,
+ },
+ {
+ ID: 4,
+ },
+ {
+ ID: 5,
+ },
+ {
+ ID: 6,
+ },
+ {
+ ID: 7,
+ },
+ {
+ ID: 8,
+ },
+ {
+ ID: 9,
+ },
+ {
+ ID: 10,
+ },
+ {
+ ID: 11,
+ },
+ {
+ ID: 12,
+ },
+ {
+ ID: 13,
+ },
+ {
+ ID: 14,
+ },
},
},
{
"Success, provide all values from fetch",
- 0,
+ 2,
nil,
&holtwinters.Predict{
+ Runner: &fake.Run{
+ RunAlgorithmWithValueReactor: func(algorithmPath, value string, timeout int) (string, error) {
+ return "2", nil
+ },
+ },
Execute: func() *fake.Execute {
execute := fake.Execute{}
- execute.ExecuteWithValueReactor = func(method *cpaconfig.Method, value string) (string, error) {
+ execute.ExecuteWithValueReactor = func(definition *hook.Definition, value string) (string, error) {
return `{"alpha":0.2, "beta":0.2, "gamma": 0.2}`, nil
}
return &execute
@@ -284,31 +661,73 @@ func TestPredict_GetPrediction(t *testing.T) {
},
&config.Model{
HoltWinters: &config.HoltWinters{
- RuntimeTuningFetch: &cpaconfig.Method{
+ RuntimeTuningFetchHook: &hook.Definition{
Type: "test",
Timeout: 2500,
},
- SeasonLength: 2,
- Method: "additive",
+ SeasonalPeriods: 2,
+ Trend: "add",
+ Seasonal: "add",
},
},
[]*stored.Evaluation{
- &stored.Evaluation{
+ {
ID: 1,
},
- &stored.Evaluation{
+ {
ID: 2,
},
+ {
+ ID: 3,
+ },
+ {
+ ID: 4,
+ },
+ {
+ ID: 5,
+ },
+ {
+ ID: 6,
+ },
+ {
+ ID: 7,
+ },
+ {
+ ID: 8,
+ },
+ {
+ ID: 9,
+ },
+ {
+ ID: 10,
+ },
+ {
+ ID: 11,
+ },
+ {
+ ID: 12,
+ },
+ {
+ ID: 13,
+ },
+ {
+ ID: 14,
+ },
},
},
{
"Success, provide alpha and beta values from fetch",
- 0,
+ 3,
nil,
&holtwinters.Predict{
+ Runner: &fake.Run{
+ RunAlgorithmWithValueReactor: func(algorithmPath, value string, timeout int) (string, error) {
+ return "3", nil
+ },
+ },
Execute: func() *fake.Execute {
execute := fake.Execute{}
- execute.ExecuteWithValueReactor = func(method *cpaconfig.Method, value string) (string, error) {
+ execute.ExecuteWithValueReactor = func(definition *hook.Definition, value string) (string, error) {
return `{"alpha":0.2, "beta":0.2}`, nil
}
return &execute
@@ -316,198 +735,58 @@ func TestPredict_GetPrediction(t *testing.T) {
},
&config.Model{
HoltWinters: &config.HoltWinters{
- RuntimeTuningFetch: &cpaconfig.Method{
+ RuntimeTuningFetchHook: &hook.Definition{
Type: "test",
Timeout: 2500,
},
- Gamma: float64Ptr(0.9),
- SeasonLength: 2,
- Method: "additive",
+ Gamma: float64Ptr(0.9),
+ SeasonalPeriods: 2,
+ Trend: "add",
+ Seasonal: "add",
},
},
[]*stored.Evaluation{
- &stored.Evaluation{
+ {
ID: 1,
},
- &stored.Evaluation{
+ {
ID: 2,
},
- },
- },
- {
- "Success, additive, less than a full season",
- 0,
- nil,
- &holtwinters.Predict{},
- &config.Model{
- HoltWinters: &config.HoltWinters{
- Alpha: float64Ptr(0.9),
- Beta: float64Ptr(0.9),
- Gamma: float64Ptr(0.9),
- SeasonLength: 5,
- Method: "additive",
+ {
+ ID: 3,
},
- },
- []*stored.Evaluation{},
- },
- {
- "Successful, additive",
- 4,
- nil,
- &holtwinters.Predict{},
- &config.Model{
- Type: linear.Type,
- HoltWinters: &config.HoltWinters{
- Alpha: float64Ptr(0.9),
- Beta: float64Ptr(0.9),
- Gamma: float64Ptr(0.9),
- SeasonLength: 3,
- StoredSeasons: 3,
- Method: "additive",
+ {
+ ID: 4,
},
- },
- []*stored.Evaluation{
- &stored.Evaluation{
- Created: time.Now().UTC().Add(time.Duration(-80) * time.Second),
- Evaluation: stored.DBEvaluation{
- TargetReplicas: 1,
- },
+ {
+ ID: 5,
},
- &stored.Evaluation{
- Created: time.Now().UTC().Add(time.Duration(-70) * time.Second),
- Evaluation: stored.DBEvaluation{
- TargetReplicas: 3,
- },
+ {
+ ID: 6,
},
- &stored.Evaluation{
- Created: time.Now().UTC().Add(time.Duration(-60) * time.Second),
- Evaluation: stored.DBEvaluation{
- TargetReplicas: 1,
- },
+ {
+ ID: 7,
},
- &stored.Evaluation{
- Created: time.Now().UTC().Add(time.Duration(-50) * time.Second),
- Evaluation: stored.DBEvaluation{
- TargetReplicas: 1,
- },
+ {
+ ID: 8,
},
- &stored.Evaluation{
- Created: time.Now().UTC().Add(time.Duration(-40) * time.Second),
- Evaluation: stored.DBEvaluation{
- TargetReplicas: 3,
- },
+ {
+ ID: 9,
},
- &stored.Evaluation{
- Created: time.Now().UTC().Add(time.Duration(-30) * time.Second),
- Evaluation: stored.DBEvaluation{
- TargetReplicas: 1,
- },
+ {
+ ID: 10,
},
- &stored.Evaluation{
- Created: time.Now().UTC().Add(time.Duration(-20) * time.Second),
- Evaluation: stored.DBEvaluation{
- TargetReplicas: 1,
- },
- },
- },
- },
- {
- "Fail, multiplicative, invalid parameters",
- 0,
- errors.New("Invalid parameter for prediction; alpha must be between 0 and 1, is -1.000000"),
- &holtwinters.Predict{},
- &config.Model{
- HoltWinters: &config.HoltWinters{
- SeasonLength: 2,
- Alpha: float64Ptr(-1.0),
- Beta: float64Ptr(0.9),
- Gamma: float64Ptr(0.9),
- Method: "multiplicative",
- },
- },
- []*stored.Evaluation{
- &stored.Evaluation{
- ID: 1,
- },
- &stored.Evaluation{
- ID: 2,
+ {
+ ID: 11,
},
- },
- },
- {
- "Success, multiplicative, less than a full season",
- 0,
- nil,
- &holtwinters.Predict{},
- &config.Model{
- HoltWinters: &config.HoltWinters{
- Alpha: float64Ptr(0.9),
- Beta: float64Ptr(0.9),
- Gamma: float64Ptr(0.9),
- SeasonLength: 5,
- Method: "multiplicative",
+ {
+ ID: 12,
},
- },
- []*stored.Evaluation{},
- },
- {
- "Successful, multiplicative",
- 4,
- nil,
- &holtwinters.Predict{},
- &config.Model{
- Type: linear.Type,
- HoltWinters: &config.HoltWinters{
- Alpha: float64Ptr(0.9),
- Beta: float64Ptr(0.9),
- Gamma: float64Ptr(0.9),
- SeasonLength: 3,
- StoredSeasons: 3,
- Method: "multiplicative",
+ {
+ ID: 13,
},
- },
- []*stored.Evaluation{
- &stored.Evaluation{
- Created: time.Now().UTC().Add(time.Duration(-80) * time.Second),
- Evaluation: stored.DBEvaluation{
- TargetReplicas: 1,
- },
- },
- &stored.Evaluation{
- Created: time.Now().UTC().Add(time.Duration(-70) * time.Second),
- Evaluation: stored.DBEvaluation{
- TargetReplicas: 3,
- },
- },
- &stored.Evaluation{
- Created: time.Now().UTC().Add(time.Duration(-60) * time.Second),
- Evaluation: stored.DBEvaluation{
- TargetReplicas: 1,
- },
- },
- &stored.Evaluation{
- Created: time.Now().UTC().Add(time.Duration(-50) * time.Second),
- Evaluation: stored.DBEvaluation{
- TargetReplicas: 1,
- },
- },
- &stored.Evaluation{
- Created: time.Now().UTC().Add(time.Duration(-40) * time.Second),
- Evaluation: stored.DBEvaluation{
- TargetReplicas: 3,
- },
- },
- &stored.Evaluation{
- Created: time.Now().UTC().Add(time.Duration(-30) * time.Second),
- Evaluation: stored.DBEvaluation{
- TargetReplicas: 1,
- },
- },
- &stored.Evaluation{
- Created: time.Now().UTC().Add(time.Duration(-20) * time.Second),
- Evaluation: stored.DBEvaluation{
- TargetReplicas: 1,
- },
+ {
+ ID: 14,
},
},
},
@@ -555,44 +834,44 @@ func TestModelPredict_GetIDsToRemove(t *testing.T) {
&config.Model{
Type: holtwinters.Type,
HoltWinters: &config.HoltWinters{
- SeasonLength: 3,
- StoredSeasons: 2,
+ SeasonalPeriods: 3,
+ StoredSeasons: 2,
},
},
[]*stored.Evaluation{
- &stored.Evaluation{
+ {
ID: 1,
Created: time.Time{}.Add(time.Duration(60) * time.Millisecond),
},
- &stored.Evaluation{
+ {
ID: 2,
Created: time.Time{}.Add(time.Duration(59) * time.Millisecond),
},
- &stored.Evaluation{
+ {
ID: 3,
Created: time.Time{}.Add(time.Duration(58) * time.Millisecond),
},
- &stored.Evaluation{
+ {
ID: 4,
Created: time.Time{}.Add(time.Duration(57) * time.Millisecond),
},
- &stored.Evaluation{
+ {
ID: 5,
Created: time.Time{}.Add(time.Duration(56) * time.Millisecond),
},
- &stored.Evaluation{
+ {
ID: 6,
Created: time.Time{}.Add(time.Duration(55) * time.Millisecond),
},
- &stored.Evaluation{
+ {
ID: 10,
Created: time.Time{}.Add(time.Duration(54) * time.Millisecond),
},
- &stored.Evaluation{
+ {
ID: 12,
Created: time.Time{}.Add(time.Duration(53) * time.Millisecond),
},
- &stored.Evaluation{
+ {
ID: 14,
Created: time.Time{}.Add(time.Duration(52) * time.Millisecond),
},
@@ -605,32 +884,32 @@ func TestModelPredict_GetIDsToRemove(t *testing.T) {
&config.Model{
Type: holtwinters.Type,
HoltWinters: &config.HoltWinters{
- SeasonLength: 2,
- StoredSeasons: 2,
+ SeasonalPeriods: 2,
+ StoredSeasons: 2,
},
},
[]*stored.Evaluation{
- &stored.Evaluation{
+ {
ID: 1,
Created: time.Time{}.Add(time.Duration(55) * time.Millisecond),
},
- &stored.Evaluation{
+ {
ID: 2,
Created: time.Time{}.Add(time.Duration(56) * time.Millisecond),
},
- &stored.Evaluation{
+ {
ID: 3,
Created: time.Time{}.Add(time.Duration(57) * time.Millisecond),
},
- &stored.Evaluation{
+ {
ID: 4,
Created: time.Time{}.Add(time.Duration(58) * time.Millisecond),
},
- &stored.Evaluation{
+ {
ID: 5,
Created: time.Time{}.Add(time.Duration(59) * time.Millisecond),
},
- &stored.Evaluation{
+ {
ID: 6,
Created: time.Time{}.Add(time.Duration(60) * time.Millisecond),
},
diff --git a/prediction/linear/linear.go b/internal/prediction/linear/linear.go
similarity index 62%
rename from prediction/linear/linear.go
rename to internal/prediction/linear/linear.go
index 297ed4f..aaf1736 100644
--- a/prediction/linear/linear.go
+++ b/internal/prediction/linear/linear.go
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Predictive Horizontal Pod Autoscaler Authors.
+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.
@@ -17,19 +17,30 @@ limitations under the License.
package linear
import (
+ "encoding/json"
"errors"
- "math"
"sort"
- "time"
+ "strconv"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/config"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/stored"
- "gonum.org/v1/gonum/stat"
+ "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"
+)
+
+const (
+ defaultTimeout = 30000
)
// Type linear is the type of the linear predicter
const Type = "Linear"
+const algorithmPath = "/app/algorithms/linear_regression/linear_regression.py"
+
+type linearRegressionParameters struct {
+ LookAhead int `json:"lookAhead"`
+ Evaluations []*stored.Evaluation `json:"evaluations"`
+}
+
// Config represents a linear regression prediction model configuration
type Config struct {
StoredValues int `yaml:"storedValues"`
@@ -37,7 +48,9 @@ type Config struct {
}
// Predict provides logic for using Linear Regression to make a prediction
-type Predict struct{}
+type Predict struct {
+ Runner algorithm.Runner
+}
// 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) {
@@ -45,39 +58,30 @@ func (p *Predict) GetPrediction(model *config.Model, evaluations []*stored.Evalu
return 0, errors.New("No Linear configuration provided for model")
}
- length := len(evaluations)
- lookAhead := time.Now().UTC().Add(time.Duration(model.Linear.LookAhead) * time.Millisecond)
-
- var data = struct {
- x []float64
- y []float64
- }{
- x: make([]float64, length),
- y: make([]float64, length),
+ parameters, err := json.Marshal(linearRegressionParameters{
+ LookAhead: model.Linear.LookAhead,
+ Evaluations: evaluations,
+ })
+ if err != nil {
+ // Should not occur, panic
+ panic(err)
}
- var max float64
+ timeout := defaultTimeout
+ if model.CalculationTimeout != nil {
+ timeout = *model.CalculationTimeout
+ }
- // Determine latest timestamp
- for i, savedEvaluation := range evaluations {
- timestamp := float64(savedEvaluation.Created.Unix())
- if i == 0 || max < timestamp {
- max = timestamp
- }
+ value, err := p.Runner.RunAlgorithmWithValue(algorithmPath, string(parameters), timeout)
+ if err != nil {
+ return 0, err
}
- // 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, savedEvaluation := range evaluations {
- data.x[i] = float64(lookAhead.Unix() - savedEvaluation.Created.Unix())
- data.y[i] = float64(savedEvaluation.Evaluation.TargetReplicas)
+ prediction, err := strconv.Atoi(value)
+ if err != nil {
+ return 0, err
}
- // Build model
- beta, alpha := stat.LinearRegression(data.x, data.y, nil, false)
- // Make prediction using y = alpha + (beta/maximum) * x
- // Round up
- prediction := math.Ceil(alpha + (beta/max)*float64(lookAhead.Unix()))
return int32(prediction), nil
}
diff --git a/prediction/linear/linear_test.go b/internal/prediction/linear/linear_test.go
similarity index 70%
rename from prediction/linear/linear_test.go
rename to internal/prediction/linear/linear_test.go
index d756150..1b5a486 100644
--- a/prediction/linear/linear_test.go
+++ b/internal/prediction/linear/linear_test.go
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Predictive Horizontal Pod Autoscaler Authors.
+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.
@@ -22,9 +22,10 @@ import (
"time"
"github.com/google/go-cmp/cmp"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/config"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/prediction/linear"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/stored"
+ "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/prediction/linear"
+ "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/stored"
)
func TestPredict_GetPrediction(t *testing.T) {
@@ -39,6 +40,7 @@ func TestPredict_GetPrediction(t *testing.T) {
description string
expected int32
expectedErr error
+ predicter *linear.Predict
model *config.Model
evaluations []*stored.Evaluation
}{
@@ -46,13 +48,21 @@ func TestPredict_GetPrediction(t *testing.T) {
"Fail no Linear configuration",
0,
errors.New("No Linear configuration provided for model"),
+ &linear.Predict{},
&config.Model{},
[]*stored.Evaluation{},
},
{
- "Successful prediction",
- 5,
- nil,
+ "Fail execution of algorithm fails",
+ 0,
+ errors.New("algorithm fail"),
+ &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{
@@ -60,38 +70,52 @@ func TestPredict_GetPrediction(t *testing.T) {
LookAhead: 0,
},
},
- []*stored.Evaluation{
- &stored.Evaluation{
- Created: time.Now().UTC().Add(time.Duration(-40) * time.Second),
- Evaluation: stored.DBEvaluation{
- TargetReplicas: 1,
+ []*stored.Evaluation{},
+ },
+ {
+ "Fail algorithm returns non-integer castable value",
+ 0,
+ errors.New(`strconv.Atoi: parsing "invalid": invalid syntax`),
+ &linear.Predict{
+ Runner: &fake.Run{
+ RunAlgorithmWithValueReactor: func(algorithmPath, value string, timeout int) (string, error) {
+ return "invalid", nil
},
},
- &stored.Evaluation{
- Created: time.Now().UTC().Add(time.Duration(-30) * time.Second),
- Evaluation: stored.DBEvaluation{
- TargetReplicas: 2,
- },
+ },
+ &config.Model{
+ Type: linear.Type,
+ Linear: &config.Linear{
+ StoredValues: 5,
+ LookAhead: 0,
},
- &stored.Evaluation{
- Created: time.Now().UTC().Add(time.Duration(-20) * time.Second),
- Evaluation: stored.DBEvaluation{
- TargetReplicas: 3,
+ },
+ []*stored.Evaluation{},
+ },
+ {
+ "Success",
+ 3,
+ nil,
+ &linear.Predict{
+ Runner: &fake.Run{
+ RunAlgorithmWithValueReactor: func(algorithmPath, value string, timeout int) (string, error) {
+ return "3", nil
},
},
- &stored.Evaluation{
- Created: time.Now().UTC().Add(time.Duration(-10) * time.Second),
- Evaluation: stored.DBEvaluation{
- TargetReplicas: 4,
- },
+ },
+ &config.Model{
+ Type: linear.Type,
+ Linear: &config.Linear{
+ StoredValues: 5,
+ LookAhead: 0,
},
},
+ []*stored.Evaluation{},
},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
- predicter := &linear.Predict{}
- result, err := predicter.GetPrediction(test.model, test.evaluations)
+ 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
@@ -135,29 +159,29 @@ func TestModelPredict_GetIDsToRemove(t *testing.T) {
},
},
[]*stored.Evaluation{
- &stored.Evaluation{
+ {
ID: 1,
Created: time.Time{}.Add(time.Duration(4) * time.Second),
},
- &stored.Evaluation{
+ {
ID: 2,
Created: time.Time{}.Add(time.Duration(5) * time.Second),
},
// START OLDEST
- &stored.Evaluation{
+ {
ID: 5,
Created: time.Time{}.Add(time.Duration(1) * time.Second),
},
- &stored.Evaluation{
+ {
ID: 3,
Created: time.Time{}.Add(time.Duration(2) * time.Second),
},
- &stored.Evaluation{
+ {
ID: 8,
Created: time.Time{}.Add(time.Duration(3) * time.Second),
},
// END OLDEST
- &stored.Evaluation{
+ {
ID: 4,
Created: time.Time{}.Add(time.Duration(6) * time.Second),
},
diff --git a/prediction/prediction.go b/internal/prediction/prediction.go
similarity index 90%
rename from prediction/prediction.go
rename to internal/prediction/prediction.go
index a291071..c0b88c6 100644
--- a/prediction/prediction.go
+++ b/internal/prediction/prediction.go
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Predictive Horizontal Pod Autoscaler Authors.
+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.
@@ -20,8 +20,8 @@ package prediction
import (
"fmt"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/config"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/stored"
+ "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/config"
+ "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/stored"
)
// Predicter is an interface providing methods for making a prediction based on a model, a time to predict and values
diff --git a/prediction/prediction_test.go b/internal/prediction/prediction_test.go
similarity index 95%
rename from prediction/prediction_test.go
rename to internal/prediction/prediction_test.go
index 24a5ea0..23bc91c 100644
--- a/prediction/prediction_test.go
+++ b/internal/prediction/prediction_test.go
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Predictive Horizontal Pod Autoscaler Authors.
+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.
@@ -21,10 +21,10 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/config"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/fake"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/prediction"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/stored"
+ "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/prediction"
+ "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/stored"
)
func TestModelPredict_GetPrediction(t *testing.T) {
diff --git a/stored/stored.go b/internal/stored/stored.go
similarity index 98%
rename from stored/stored.go
rename to internal/stored/stored.go
index fa2dbe0..320ea3d 100644
--- a/stored/stored.go
+++ b/internal/stored/stored.go
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Predictive Horizontal Pod Autoscaler Authors.
+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.
diff --git a/stored/stored_test.go b/internal/stored/stored_test.go
similarity index 89%
rename from stored/stored_test.go
rename to internal/stored/stored_test.go
index 9f63ae0..5e3ef06 100644
--- a/stored/stored_test.go
+++ b/internal/stored/stored_test.go
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Predictive Horizontal Pod Autoscaler Authors.
+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.
diff --git a/cmd/predictive-horizontal-pod-autoscaler/main.go b/main.go
similarity index 88%
rename from cmd/predictive-horizontal-pod-autoscaler/main.go
rename to main.go
index b582dc0..936f94b 100644
--- a/cmd/predictive-horizontal-pod-autoscaler/main.go
+++ b/main.go
@@ -1,5 +1,5 @@
/*
-Copyright 2020 The Predictive Horizontal Pod Autoscaler Authors.
+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.
@@ -42,19 +42,20 @@ import (
"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
- "github.com/jthomperoo/custom-pod-autoscaler/execute"
- "github.com/jthomperoo/custom-pod-autoscaler/execute/http"
- "github.com/jthomperoo/custom-pod-autoscaler/execute/shell"
cpametric "github.com/jthomperoo/custom-pod-autoscaler/metric"
hpaevaluate "github.com/jthomperoo/horizontal-pod-autoscaler/evaluate"
"github.com/jthomperoo/horizontal-pod-autoscaler/metric"
"github.com/jthomperoo/horizontal-pod-autoscaler/podclient"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/config"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/evaluate"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/prediction"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/prediction/holtwinters"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/prediction/linear"
- "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/stored"
+ "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/evaluate"
+ "github.com/jthomperoo/predictive-horizontal-pod-autoscaler/internal/hook"
+ "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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -94,7 +95,6 @@ func main() {
stdin, err := ioutil.ReadAll(os.Stdin)
if err != nil {
log.Fatal(err)
- os.Exit(1)
}
modePtr := flag.String("mode", "no_mode", "command mode, either metric or evaluate")
@@ -121,7 +121,6 @@ func main() {
setup(predictiveConfig)
default:
log.Fatalf("Unknown command mode: %s", *modePtr)
- os.Exit(1)
}
}
@@ -170,7 +169,6 @@ func getEvaluation(stdin io.Reader, predictiveConfig *config.Config) {
err = yaml.NewYAMLOrJSONDecoder(stdin, 10).Decode(&spec)
if err != nil {
log.Fatal(err)
- os.Exit(1)
}
// Create object from version and kind of piped value
@@ -178,14 +176,12 @@ func getEvaluation(stdin io.Reader, predictiveConfig *config.Config) {
resourceRuntime, err := scheme.Scheme.New(resourceGVK)
if err != nil {
log.Fatal(err)
- os.Exit(1)
}
// Parse the unstructured k8s resource into the object created, then convert to generic metav1.Object
err = runtime.DefaultUnstructuredConverter.FromUnstructured(spec.UnstructuredResource.Object, resourceRuntime)
if err != nil {
log.Fatal(err)
- os.Exit(1)
}
spec.Resource = resourceRuntime.(metav1.Object)
@@ -193,10 +189,8 @@ func getEvaluation(stdin io.Reader, predictiveConfig *config.Config) {
err = yaml.NewYAMLOrJSONDecoder(strings.NewReader(spec.Metrics[0].Value), 10).Decode(&combinedMetrics)
if err != nil {
log.Fatal(err)
- os.Exit(1)
}
- // Set up shell executer
shellExec := &shell.Execute{
Command: exec.Command,
}
@@ -204,13 +198,17 @@ func getEvaluation(stdin io.Reader, predictiveConfig *config.Config) {
httpExec := &http.Execute{}
// Combine executers
- combinedExecute := &execute.CombinedExecute{
- Executers: []execute.Executer{
+ combinedExecute := &hook.CombinedExecute{
+ Executers: []hook.Executer{
shellExec,
httpExec,
},
}
+ algorithmRunner := &algorithm.Run{
+ Executer: shellExec,
+ }
+
// Set up evaluator
evaluator := &evaluate.PredictiveEvaluate{
HPAEvaluator: hpaevaluate.NewEvaluate(predictiveConfig.Tolerance),
@@ -218,8 +216,11 @@ func getEvaluation(stdin io.Reader, predictiveConfig *config.Config) {
DB: db,
},
Predicters: []prediction.Predicter{
- &linear.Predict{},
+ &linear.Predict{
+ Runner: algorithmRunner,
+ },
&holtwinters.Predict{
+ Runner: algorithmRunner,
Execute: combinedExecute,
},
},
@@ -235,7 +236,6 @@ func getEvaluation(stdin io.Reader, predictiveConfig *config.Config) {
jsonEvaluation, err := json.Marshal(result)
if err != nil {
log.Fatal(err)
- os.Exit(1)
}
fmt.Print(string(jsonEvaluation))
@@ -246,7 +246,6 @@ func getMetrics(stdin io.Reader, predictiveConfig *config.Config) {
err := yaml.NewYAMLOrJSONDecoder(stdin, 10).Decode(&spec)
if err != nil {
log.Fatal(err)
- os.Exit(1)
}
// Create object from version and kind of piped value
@@ -254,34 +253,29 @@ func getMetrics(stdin io.Reader, predictiveConfig *config.Config) {
resourceRuntime, err := scheme.Scheme.New(resourceGVK)
if err != nil {
log.Fatal(err)
- os.Exit(1)
}
// Parse the unstructured k8s resource into the object created, then convert to generic metav1.Object
err = runtime.DefaultUnstructuredConverter.FromUnstructured(spec.UnstructuredResource.Object, resourceRuntime)
if err != nil {
log.Fatal(err)
- os.Exit(1)
}
spec.Resource = resourceRuntime.(metav1.Object)
if len(predictiveConfig.Metrics) == 0 {
log.Fatal("Metric specs not supplied")
- os.Exit(1)
}
// Create the in-cluster Kubernetes config
clusterConfig, err := rest.InClusterConfig()
if err != nil {
log.Fatal(err)
- os.Exit(1)
}
// Create the Kubernetes clientset
clientset, err := kubernetes.NewForConfig(clusterConfig)
if err != nil {
log.Fatal(err)
- os.Exit(1)
}
// Create metric gatherer, with required clients and configuration
@@ -303,14 +297,12 @@ func getMetrics(stdin io.Reader, predictiveConfig *config.Config) {
metrics, err := gatherer.GetMetrics(spec.Resource, predictiveConfig.Metrics, spec.Resource.GetNamespace())
if err != nil {
log.Fatal(err)
- os.Exit(1)
}
// Marshal metrics into JSON
jsonMetrics, err := json.Marshal(metrics)
if err != nil {
log.Fatal(err)
- os.Exit(1)
}
// Write serialised metrics to stdout
diff --git a/mkdocs.yml b/mkdocs.yml
index 3ad6ab9..cd4628d 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -11,5 +11,6 @@ nav:
- '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'
\ No newline at end of file
+ - Configuration: 'reference/configuration.md'
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644
index 0000000..510fe17
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1,5 @@
+pylint==2.5.3
+pytest==5.4.3
+pytest-subtests==0.3.2
+pytest-cov==2.10.0
+yapf==0.31.0
diff --git a/sql/1_init_schema.up.sql b/sql/1_init_schema.up.sql
index 0eb5794..3c902c8 100644
--- a/sql/1_init_schema.up.sql
+++ b/sql/1_init_schema.up.sql
@@ -1,12 +1,12 @@
/*
- * Copyright 2019 The Predictive Horizontal Pod Autoscaler Authors.
- *
+ * 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.