mirror of
https://github.com/zalando-incubator/kube-metrics-adapter.git
synced 2025-01-09 18:01:36 +00:00
Compare commits
34 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
5bf87cb10e | ||
|
c6610750e4 | ||
|
04ae6d955e | ||
|
2d56c202a7 | ||
|
c9fa15c7d4 | ||
|
e3330dcf43 | ||
|
8e4662b26c | ||
|
9e211b181a | ||
|
9d78fff1b5 | ||
|
1c6f9e2ea6 | ||
|
c0eda7cd1e | ||
|
75f3e48f70 | ||
|
5b55bea994 | ||
|
4412e3dca4 | ||
|
8f9277258c | ||
|
8c3fef45fd | ||
|
120950078c | ||
|
0790bc351a | ||
|
f6b2aede5b | ||
|
7d5e719eb0 | ||
|
7497a61a2c | ||
|
a72380125f | ||
|
70c7fb843d | ||
|
79533a5a93 | ||
|
2765ff9811 | ||
|
76d2f74743 | ||
|
0de5042d3d | ||
|
07c0e179b3 | ||
|
29ee953a16 | ||
|
f78ef26857 | ||
|
a3c14e9dcb | ||
|
b6b13fb31a | ||
|
0a06691d39 | ||
|
2d1d51e829 |
@ -1,7 +1,4 @@
|
||||
run:
|
||||
skip-files:
|
||||
- "pkg/provider/generated.conversion.go"
|
||||
- "pkg/provider/conversion.go"
|
||||
linters-settings:
|
||||
golint:
|
||||
min-confidence: 0.9
|
||||
|
10
.travis.yml
10
.travis.yml
@ -2,19 +2,19 @@ language: go
|
||||
dist: xenial
|
||||
|
||||
go:
|
||||
- "1.11.x"
|
||||
- "1.13.x"
|
||||
|
||||
env:
|
||||
- GO111MODULE=on GOLANGCI_RELEASE="v1.16.0"
|
||||
- GO111MODULE=on GOLANGCI_RELEASE="v1.21.0"
|
||||
|
||||
before_install:
|
||||
- go get github.com/mattn/goveralls
|
||||
- go get github.com/lawrencewoodman/roveralls
|
||||
- GO111MODULE=off go get github.com/mattn/goveralls
|
||||
- GO111MODULE=off go get github.com/lawrencewoodman/roveralls
|
||||
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_RELEASE}
|
||||
|
||||
script:
|
||||
- make check
|
||||
- make test
|
||||
- make build.docker
|
||||
- make check
|
||||
- roveralls
|
||||
- goveralls -v -coverprofile=roveralls.coverprofile -service=travis-ci
|
||||
|
3
Makefile
3
Makefile
@ -19,7 +19,8 @@ test:
|
||||
go test -v $(GOPKGS)
|
||||
|
||||
check:
|
||||
golangci-lint run ./...
|
||||
go mod download
|
||||
golangci-lint run --timeout=2m ./...
|
||||
|
||||
build.local: build/$(BINARY)
|
||||
build.linux: build/linux/$(BINARY)
|
||||
|
@ -1,7 +1,8 @@
|
||||
We acknowledge that every line of code that we write may potentially contain security issues.
|
||||
We are trying to deal with it responsibly and provide patches as quickly as possible.
|
||||
|
||||
We are trying to deal with it responsibly and provide patches as quickly as possible. If you have anything to report to us please use the following channels:
|
||||
We host our bug bounty program on HackerOne, it is currently private, therefore if you would like to report a vulnerability and get rewarded for it, please ask to join our program by filling this form:
|
||||
|
||||
Email: Tech-Security@zalando.de
|
||||
OR
|
||||
Submit your vulnerability report through our bug bounty program at: https://hackerone.com/zalando
|
||||
https://corporate.zalando.com/en/services-and-contact#security-form
|
||||
|
||||
You can also send you report via this form if you do not want to join our bug bounty program and just want to report a vulnerability or security issue.
|
||||
|
@ -7,6 +7,8 @@ pipeline:
|
||||
- /go/pkg/mod # pkg cache for Go modules
|
||||
- ~/.cache/go-build # Go build cache
|
||||
type: script
|
||||
env:
|
||||
GOFLAGS: "-mod=readonly"
|
||||
commands:
|
||||
- desc: test
|
||||
cmd: |
|
||||
@ -18,9 +20,11 @@ pipeline:
|
||||
cmd: |
|
||||
if [[ $CDP_TARGET_BRANCH == master && ! $CDP_PULL_REQUEST_NUMBER ]]; then
|
||||
IMAGE=registry-write.opensource.zalan.do/teapot/kube-metrics-adapter
|
||||
VERSION=$(git describe --tags --always --dirty)
|
||||
VERSION=$(git describe --tags --always)
|
||||
else
|
||||
IMAGE=registry-write.opensource.zalan.do/teapot/kube-metrics-adapter-test
|
||||
VERSION=$CDP_BUILD_VERSION
|
||||
fi
|
||||
IMAGE=$IMAGE VERSION=$VERSION make build.docker
|
||||
git diff --stat --exit-code
|
||||
IMAGE=$IMAGE VERSION=$VERSION make build.push
|
||||
|
@ -1,4 +1,4 @@
|
||||
apiVersion: autoscaling/v2beta1
|
||||
apiVersion: autoscaling/v2beta2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: custom-metrics-consumer
|
||||
@ -25,24 +25,36 @@ spec:
|
||||
# - type: Resource
|
||||
# resource:
|
||||
# name: cpu
|
||||
# targetAverageUtilization: 50
|
||||
# current:
|
||||
# averageUtilization: 50
|
||||
|
||||
- type: Pods
|
||||
pods:
|
||||
metricName: queue-length
|
||||
targetAverageValue: 1k
|
||||
metric:
|
||||
name: queue-length
|
||||
target:
|
||||
averageValue: 1k
|
||||
type: AverageValue
|
||||
|
||||
- type: Object
|
||||
object:
|
||||
metricName: requests-per-second
|
||||
target:
|
||||
describedObject:
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
name: custom-metrics-consumer
|
||||
targetValue: 10 # this will be treated as targetAverageValue
|
||||
metric:
|
||||
name: requests-per-second
|
||||
target:
|
||||
averageValue: "10"
|
||||
type: AverageValue
|
||||
- type: External
|
||||
external:
|
||||
metricName: sqs-queue-length
|
||||
metricSelector:
|
||||
matchLabels:
|
||||
queue-name: foobar
|
||||
region: eu-central-1
|
||||
targetAverageValue: 30
|
||||
metric:
|
||||
name: sqs-queue-length
|
||||
selector:
|
||||
matchLabels:
|
||||
queue-name: foobar
|
||||
region: eu-central-1
|
||||
target:
|
||||
averageValue: "30"
|
||||
type: AverageValue
|
||||
|
91
go.mod
91
go.mod
@ -1,91 +1,32 @@
|
||||
module github.com/zalando-incubator/kube-metrics-adapter
|
||||
|
||||
require (
|
||||
bitbucket.org/ww/goautoneg v0.0.0-20120707110453-75cd24fc2f2c // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
||||
github.com/BurntSushi/toml v0.3.0 // indirect
|
||||
github.com/NYTimes/gziphandler v1.0.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.0 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/Sirupsen/logrus v1.0.6 // indirect
|
||||
github.com/aws/aws-sdk-go v1.16.6
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect
|
||||
github.com/boltdb/bolt v1.3.1 // indirect
|
||||
github.com/coreos/bbolt v1.3.0 // indirect
|
||||
github.com/coreos/etcd v3.3.9+incompatible // indirect
|
||||
github.com/coreos/go-semver v0.2.0 // indirect
|
||||
github.com/aws/aws-sdk-go v1.29.4
|
||||
github.com/coreos/go-systemd v0.0.0-20180705093442-88bfeed483d3 // indirect
|
||||
github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
||||
github.com/docker/docker v1.13.1 // indirect
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.0 // indirect
|
||||
github.com/emicklei/go-restful v2.8.0+incompatible // indirect
|
||||
github.com/emicklei/go-restful-swagger12 v0.0.0-20170926063155-7524189396c6 // indirect
|
||||
github.com/evanphx/json-patch v4.1.1-0.20190203004735-bbf30d639737+incompatible // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.7 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.0.0-20180322222829-3a0015ad55fa // indirect
|
||||
github.com/go-openapi/jsonreference v0.0.0-20180322222742-3fb327e6747d // indirect
|
||||
github.com/go-openapi/spec v0.0.0-20180801175345-384415f06ee2 // indirect
|
||||
github.com/go-openapi/swag v0.0.0-20180715190254-becd2f08beaf // indirect
|
||||
github.com/gogo/protobuf v1.1.1 // indirect
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
|
||||
github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7 // indirect
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
|
||||
github.com/googleapis/gnostic v0.2.0 // indirect
|
||||
github.com/gorilla/websocket v1.3.0 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.4.1 // indirect
|
||||
github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47 // indirect
|
||||
github.com/hpcloud/tail v1.0.0 // indirect
|
||||
github.com/imdario/mergo v0.3.6 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.1.0 // indirect
|
||||
github.com/json-iterator/go v1.1.5 // indirect
|
||||
github.com/kubernetes-incubator/custom-metrics-apiserver v0.0.0-20190116221620-b7016fc85e1c
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/influxdata/influxdb-client-go v0.1.4
|
||||
github.com/kubernetes-incubator/custom-metrics-apiserver v0.0.0-20190918110929-3d9be26a50eb
|
||||
github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852
|
||||
github.com/onsi/ginkgo v1.6.0 // indirect
|
||||
github.com/onsi/gomega v1.4.1 // indirect
|
||||
github.com/pborman/uuid v0.0.0-20180122190007-c65b2f87fee3 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/prometheus/client_golang v0.9.0-pre1.0.20180824101016-4eb539fa85a2
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 // indirect
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273 // indirect
|
||||
github.com/sirupsen/logrus v1.3.0
|
||||
github.com/prometheus/client_golang v0.9.2
|
||||
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/soheilhy/cmux v0.1.4 // indirect
|
||||
github.com/spf13/cobra v0.0.3
|
||||
github.com/spf13/pflag v1.0.2 // indirect
|
||||
github.com/stretchr/testify v1.2.2
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 // indirect
|
||||
github.com/ugorji/go v1.1.1 // indirect
|
||||
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 // indirect
|
||||
github.com/zalando-incubator/cluster-lifecycle-manager v0.0.0-20180921141935-824b77fb1f84
|
||||
golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e // indirect
|
||||
golang.org/x/net v0.0.0-20180824152047-4bcd98cce591 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect
|
||||
google.golang.org/appengine v1.2.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 // indirect
|
||||
google.golang.org/grpc v1.14.0 // indirect
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect
|
||||
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0-20170531160350-a96e63847dc3 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.1 // indirect
|
||||
k8s.io/api v0.0.0-20190226173710-145d52631d00
|
||||
k8s.io/apimachinery v0.0.0-20190221084156-01f179d85dbc
|
||||
k8s.io/apiserver v0.0.0-20190226174732-cf2f1d68202d
|
||||
k8s.io/client-go v2.0.0-alpha.0.0.20190226174127-78295b709ec6+incompatible
|
||||
k8s.io/kube-openapi v0.0.0-20180731170545-e3762e86a74c // indirect
|
||||
k8s.io/api v0.0.0-20190918155943-95b840bb6a1f
|
||||
k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655
|
||||
k8s.io/apiserver v0.0.0-20190918160949-bfa5e2e684ad // indirect
|
||||
k8s.io/client-go v0.0.0-20190918160344-1fbdaa4c8d90
|
||||
k8s.io/component-base v0.0.0-20190918160511-547f6c5d7090
|
||||
k8s.io/klog v0.4.0
|
||||
k8s.io/metrics v0.0.0-20190226180357-f3f09b9076d1
|
||||
)
|
||||
|
||||
go 1.13
|
||||
|
3
main.go
3
main.go
@ -18,12 +18,13 @@ package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/zalando-incubator/kube-metrics-adapter/pkg/server"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/util/logs"
|
||||
"k8s.io/component-base/logs"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -65,6 +65,27 @@ func TestParser(t *testing.T) {
|
||||
},
|
||||
PerReplica: false,
|
||||
},
|
||||
{
|
||||
Name: "influxdb metrics",
|
||||
Annotations: map[string]string{
|
||||
"metric-config.external.flux-query.influxdb/range1m": `from(bucket: "?") |> range(start: -1m)`,
|
||||
"metric-config.external.flux-query.influxdb/range2m": `from(bucket: "?") |> range(start: -2m)`,
|
||||
"metric-config.external.flux-query.influxdb/range3m": `from(bucket: "?") |> range(start: -3m)`,
|
||||
"metric-config.external.flux-query.influxdb/address": "http://localhost:9999",
|
||||
"metric-config.external.flux-query.influxdb/token": "sEcr3TT0ken",
|
||||
"metric-config.external.flux-query.influxdb/org-id": "deadbeef",
|
||||
},
|
||||
MetricName: "flux-query",
|
||||
MetricType: autoscalingv2.ExternalMetricSourceType,
|
||||
ExpectedConfig: map[string]string{
|
||||
"range1m": `from(bucket: "?") |> range(start: -1m)`,
|
||||
"range2m": `from(bucket: "?") |> range(start: -2m)`,
|
||||
"range3m": `from(bucket: "?") |> range(start: -3m)`,
|
||||
"address": "http://localhost:9999",
|
||||
"token": "sEcr3TT0ken",
|
||||
"org-id": "deadbeef",
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
hpaMap := make(AnnotationConfigMap)
|
||||
|
@ -56,11 +56,11 @@ func NewAWSSQSCollector(sessions map[string]*session.Session, config *MetricConf
|
||||
return nil, fmt.Errorf("selector for queue is not specified")
|
||||
}
|
||||
|
||||
name, ok := config.Metric.Selector.MatchLabels[sqsQueueNameLabelKey]
|
||||
name, ok := config.Config[sqsQueueNameLabelKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("sqs queue name not specified on metric")
|
||||
}
|
||||
region, ok := config.Metric.Selector.MatchLabels[sqsQueueRegionLabelKey]
|
||||
region, ok := config.Config[sqsQueueRegionLabelKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("sqs queue region is not specified on metric")
|
||||
}
|
||||
|
@ -46,6 +46,14 @@ type CollectorPlugin interface {
|
||||
NewCollector(hpa *autoscalingv2.HorizontalPodAutoscaler, config *MetricConfig, interval time.Duration) (Collector, error)
|
||||
}
|
||||
|
||||
type PluginNotFoundError struct {
|
||||
metricTypeName MetricTypeName
|
||||
}
|
||||
|
||||
func (p *PluginNotFoundError) Error() string {
|
||||
return fmt.Sprintf("no plugin found for %s", p.metricTypeName)
|
||||
}
|
||||
|
||||
func (c *CollectorFactory) RegisterPodsCollector(metricCollector string, plugin CollectorPlugin) error {
|
||||
if metricCollector == "" {
|
||||
c.podsPlugins.Any = plugin
|
||||
@ -139,7 +147,7 @@ func (c *CollectorFactory) NewCollector(hpa *autoscalingv2.HorizontalPodAutoscal
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no plugin found for %s", config.MetricTypeName)
|
||||
return nil, &PluginNotFoundError{metricTypeName: config.MetricTypeName}
|
||||
}
|
||||
|
||||
type MetricTypeName struct {
|
||||
@ -165,6 +173,7 @@ type MetricConfig struct {
|
||||
ObjectReference custom_metrics.ObjectReference
|
||||
PerReplica bool
|
||||
Interval time.Duration
|
||||
MetricSpec autoscalingv2.MetricSpec
|
||||
}
|
||||
|
||||
// ParseHPAMetrics parses the HPA object into a list of metric configurations.
|
||||
@ -206,10 +215,15 @@ func ParseHPAMetrics(hpa *autoscalingv2.HorizontalPodAutoscaler) ([]*MetricConfi
|
||||
MetricTypeName: typeName,
|
||||
ObjectReference: ref,
|
||||
Config: map[string]string{},
|
||||
MetricSpec: metric,
|
||||
}
|
||||
|
||||
if metric.Type == autoscalingv2.ExternalMetricSourceType {
|
||||
config.Config = metric.External.Metric.Selector.MatchLabels
|
||||
if metric.Type == autoscalingv2.ExternalMetricSourceType &&
|
||||
metric.External.Metric.Selector != nil &&
|
||||
metric.External.Metric.Selector.MatchLabels != nil {
|
||||
for k, v := range metric.External.Metric.Selector.MatchLabels {
|
||||
config.Config[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
annotationConfigs, present := parser.GetAnnotationConfig(typeName.Metric.Name, typeName.Type)
|
||||
@ -217,7 +231,11 @@ func ParseHPAMetrics(hpa *autoscalingv2.HorizontalPodAutoscaler) ([]*MetricConfi
|
||||
config.CollectorName = annotationConfigs.CollectorName
|
||||
config.Interval = annotationConfigs.Interval
|
||||
config.PerReplica = annotationConfigs.PerReplica
|
||||
config.Config = annotationConfigs.Configs
|
||||
// configs specified in annotations takes precedence
|
||||
// over labels
|
||||
for k, v := range annotationConfigs.Configs {
|
||||
config.Config[k] = v
|
||||
}
|
||||
}
|
||||
metricConfigs = append(metricConfigs, config)
|
||||
}
|
||||
|
152
pkg/collector/influxdb_collector.go
Normal file
152
pkg/collector/influxdb_collector.go
Normal file
@ -0,0 +1,152 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/influxdb-client-go"
|
||||
"k8s.io/api/autoscaling/v2beta2"
|
||||
autoscalingv2 "k8s.io/api/autoscaling/v2beta2"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/metrics/pkg/apis/external_metrics"
|
||||
)
|
||||
|
||||
const (
|
||||
InfluxDBMetricName = "flux-query"
|
||||
influxDBAddressKey = "address"
|
||||
influxDBTokenKey = "token"
|
||||
influxDBOrgKey = "org"
|
||||
influxDBQueryNameLabelKey = "query-name"
|
||||
)
|
||||
|
||||
type InfluxDBCollectorPlugin struct {
|
||||
kubeClient kubernetes.Interface
|
||||
address string
|
||||
token string
|
||||
org string
|
||||
}
|
||||
|
||||
func NewInfluxDBCollectorPlugin(client kubernetes.Interface, address, token, org string) (*InfluxDBCollectorPlugin, error) {
|
||||
return &InfluxDBCollectorPlugin{
|
||||
kubeClient: client,
|
||||
address: address,
|
||||
token: token,
|
||||
org: org,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *InfluxDBCollectorPlugin) NewCollector(hpa *v2beta2.HorizontalPodAutoscaler, config *MetricConfig, interval time.Duration) (Collector, error) {
|
||||
return NewInfluxDBCollector(p.address, p.token, p.org, config, interval)
|
||||
}
|
||||
|
||||
type InfluxDBCollector struct {
|
||||
address string
|
||||
token string
|
||||
org string
|
||||
|
||||
influxDBClient *influxdb.Client
|
||||
interval time.Duration
|
||||
metric autoscalingv2.MetricIdentifier
|
||||
metricType autoscalingv2.MetricSourceType
|
||||
query string
|
||||
}
|
||||
|
||||
func NewInfluxDBCollector(address string, token string, org string, config *MetricConfig, interval time.Duration) (*InfluxDBCollector, error) {
|
||||
collector := &InfluxDBCollector{
|
||||
interval: interval,
|
||||
metric: config.Metric,
|
||||
metricType: config.Type,
|
||||
}
|
||||
switch configType := config.Type; configType {
|
||||
case autoscalingv2.ObjectMetricSourceType:
|
||||
return nil, fmt.Errorf("InfluxDB does not support object, but only external custom metrics")
|
||||
case autoscalingv2.ExternalMetricSourceType:
|
||||
// `metricSelector` is flattened into the MetricConfig.Config.
|
||||
queryName, ok := config.Config[influxDBQueryNameLabelKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("selector for Flux query is not specified, "+
|
||||
"please add metricSelector.matchLabels.%s: <...> to .yml description", influxDBQueryNameLabelKey)
|
||||
}
|
||||
if query, ok := config.Config[queryName]; ok {
|
||||
// TODO(affo): validate the query once this is done:
|
||||
// https://github.com/influxdata/influxdb-client-go/issues/73.
|
||||
collector.query = query
|
||||
} else {
|
||||
return nil, fmt.Errorf("no Flux query defined for metric \"%s\"", config.Metric.Name)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown metric type: %v", configType)
|
||||
}
|
||||
// Use custom InfluxDB config if defined in HPA annotation.
|
||||
if v, ok := config.Config[influxDBAddressKey]; ok {
|
||||
address = v
|
||||
}
|
||||
if v, ok := config.Config[influxDBTokenKey]; ok {
|
||||
token = v
|
||||
}
|
||||
if v, ok := config.Config[influxDBOrgKey]; ok {
|
||||
org = v
|
||||
}
|
||||
influxDbClient, err := influxdb.New(address, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
collector.address = address
|
||||
collector.token = token
|
||||
collector.org = org
|
||||
collector.influxDBClient = influxDbClient
|
||||
return collector, nil
|
||||
}
|
||||
|
||||
// queryResult is for unmarshaling the result from InfluxDB.
|
||||
// The FluxQuery should make it so that the resulting table contains the column "metricvalue".
|
||||
type queryResult struct {
|
||||
MetricValue float64
|
||||
}
|
||||
|
||||
// getValue returns the first result gathered from an InfluxDB instance.
|
||||
func (c *InfluxDBCollector) getValue() (resource.Quantity, error) {
|
||||
res, err := c.influxDBClient.QueryCSV(context.Background(), c.query, c.org)
|
||||
if err != nil {
|
||||
return resource.Quantity{}, err
|
||||
}
|
||||
defer res.Close()
|
||||
// Keeping just the first result.
|
||||
if res.Next() {
|
||||
qr := queryResult{}
|
||||
if err := res.Unmarshal(&qr); err != nil {
|
||||
return resource.Quantity{}, fmt.Errorf("error in unmarshaling query result: %v", err)
|
||||
}
|
||||
return *resource.NewMilliQuantity(int64(qr.MetricValue*1000), resource.DecimalSI), nil
|
||||
}
|
||||
if err := res.Err; err != nil {
|
||||
return resource.Quantity{}, fmt.Errorf("error in query result: %v", err)
|
||||
}
|
||||
return resource.Quantity{}, fmt.Errorf("empty result returned")
|
||||
}
|
||||
|
||||
func (c *InfluxDBCollector) GetMetrics() ([]CollectedMetric, error) {
|
||||
v, err := c.getValue()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cm := CollectedMetric{
|
||||
Type: c.metricType,
|
||||
External: external_metrics.ExternalMetricValue{
|
||||
MetricName: c.metric.Name,
|
||||
MetricLabels: c.metric.Selector.MatchLabels,
|
||||
Timestamp: metav1.Time{
|
||||
Time: time.Now().UTC(),
|
||||
},
|
||||
Value: v,
|
||||
},
|
||||
}
|
||||
return []CollectedMetric{cm}, nil
|
||||
}
|
||||
|
||||
func (c *InfluxDBCollector) Interval() time.Duration {
|
||||
return c.interval
|
||||
}
|
155
pkg/collector/influxdb_collector_test.go
Normal file
155
pkg/collector/influxdb_collector_test.go
Normal file
@ -0,0 +1,155 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/api/autoscaling/v2beta2"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestInfluxDBCollector_New(t *testing.T) {
|
||||
t.Run("simple", func(t *testing.T) {
|
||||
m := &MetricConfig{
|
||||
MetricTypeName: MetricTypeName{
|
||||
Type: v2beta2.ExternalMetricSourceType,
|
||||
Metric: v2beta2.MetricIdentifier{
|
||||
Name: "flux-query",
|
||||
// This is actually useless, because the selector should be flattened in Config when parsing.
|
||||
Selector: &v1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"query-name": "range2m",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CollectorName: "influxdb",
|
||||
Config: map[string]string{
|
||||
"range1m": `from(bucket: "?") |> range(start: -1m)`,
|
||||
"range2m": `from(bucket: "?") |> range(start: -2m)`,
|
||||
"range3m": `from(bucket: "?") |> range(start: -3m)`,
|
||||
"query-name": "range2m",
|
||||
},
|
||||
}
|
||||
c, err := NewInfluxDBCollector("http://localhost:9999", "secret", "deadbeef", m, time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got, want := c.org, "deadbeef"; want != got {
|
||||
t.Errorf("unexpected value -want/+got:\n\t-%s\n\t+%s", want, got)
|
||||
}
|
||||
if got, want := c.address, "http://localhost:9999"; want != got {
|
||||
t.Errorf("unexpected value -want/+got:\n\t-%s\n\t+%s", want, got)
|
||||
}
|
||||
if got, want := c.token, "secret"; want != got {
|
||||
t.Errorf("unexpected value -want/+got:\n\t-%s\n\t+%s", want, got)
|
||||
}
|
||||
if got, want := c.query, `from(bucket: "?") |> range(start: -2m)`; want != got {
|
||||
t.Errorf("unexpected value -want/+got:\n\t-%s\n\t+%s", want, got)
|
||||
}
|
||||
})
|
||||
t.Run("override params", func(t *testing.T) {
|
||||
m := &MetricConfig{
|
||||
MetricTypeName: MetricTypeName{
|
||||
Type: v2beta2.ExternalMetricSourceType,
|
||||
Metric: v2beta2.MetricIdentifier{
|
||||
Name: "flux-query",
|
||||
Selector: &v1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"query-name": "range2m",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CollectorName: "influxdb",
|
||||
Config: map[string]string{
|
||||
"range1m": `from(bucket: "?") |> range(start: -1m)`,
|
||||
"range2m": `from(bucket: "?") |> range(start: -2m)`,
|
||||
"range3m": `from(bucket: "?") |> range(start: -3m)`,
|
||||
"address": "http://localhost:9999",
|
||||
"token": "sEcr3TT0ken",
|
||||
"org": "deadbeef1234",
|
||||
"query-name": "range3m",
|
||||
},
|
||||
}
|
||||
c, err := NewInfluxDBCollector("http://localhost:8888", "secret", "deadbeef", m, time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got, want := c.org, "deadbeef1234"; want != got {
|
||||
t.Errorf("unexpected value -want/+got:\n\t-%s\n\t+%s", want, got)
|
||||
}
|
||||
if got, want := c.address, "http://localhost:9999"; want != got {
|
||||
t.Errorf("unexpected value -want/+got:\n\t-%s\n\t+%s", want, got)
|
||||
}
|
||||
if got, want := c.token, "sEcr3TT0ken"; want != got {
|
||||
t.Errorf("unexpected value -want/+got:\n\t-%s\n\t+%s", want, got)
|
||||
}
|
||||
if got, want := c.query, `from(bucket: "?") |> range(start: -3m)`; want != got {
|
||||
t.Errorf("unexpected value -want/+got:\n\t-%s\n\t+%s", want, got)
|
||||
}
|
||||
})
|
||||
// Errors.
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
mTypeName MetricTypeName
|
||||
config map[string]string
|
||||
errorStartsWith string
|
||||
}{
|
||||
{
|
||||
name: "object metric",
|
||||
mTypeName: MetricTypeName{
|
||||
Type: v2beta2.ObjectMetricSourceType,
|
||||
},
|
||||
errorStartsWith: "InfluxDB does not support object",
|
||||
},
|
||||
{
|
||||
name: "no selector",
|
||||
mTypeName: MetricTypeName{
|
||||
Type: v2beta2.ExternalMetricSourceType,
|
||||
Metric: v2beta2.MetricIdentifier{
|
||||
Name: "flux-query",
|
||||
},
|
||||
},
|
||||
// The selector should be flattened into the config by the parsing step, but it isn't.
|
||||
config: map[string]string{
|
||||
"range1m": `from(bucket: "?") |> range(start: -1m)`,
|
||||
"range2m": `from(bucket: "?") |> range(start: -2m)`,
|
||||
"range3m": `from(bucket: "?") |> range(start: -3m)`,
|
||||
},
|
||||
errorStartsWith: "selector for Flux query is not specified",
|
||||
},
|
||||
{
|
||||
name: "referencing non-existing query",
|
||||
mTypeName: MetricTypeName{
|
||||
Type: v2beta2.ExternalMetricSourceType,
|
||||
Metric: v2beta2.MetricIdentifier{
|
||||
Name: "flux-query",
|
||||
},
|
||||
},
|
||||
config: map[string]string{
|
||||
"range1m": `from(bucket: "?") |> range(start: -1m)`,
|
||||
"range2m": `from(bucket: "?") |> range(start: -2m)`,
|
||||
"range3m": `from(bucket: "?") |> range(start: -3m)`,
|
||||
"query-name": "rangeXm",
|
||||
},
|
||||
errorStartsWith: "no Flux query defined for metric",
|
||||
},
|
||||
} {
|
||||
t.Run("error - "+tc.name, func(t *testing.T) {
|
||||
m := &MetricConfig{
|
||||
MetricTypeName: tc.mTypeName,
|
||||
CollectorName: "influxdb",
|
||||
Config: tc.config,
|
||||
}
|
||||
_, err := NewInfluxDBCollector("http://localhost:9999", "secret", "deadbeef", m, time.Second)
|
||||
if err == nil {
|
||||
t.Fatal("expected error got none")
|
||||
}
|
||||
if want, got := tc.errorStartsWith, err.Error(); !strings.HasPrefix(got, want) {
|
||||
t.Fatalf("%s should start with %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
@ -17,23 +19,27 @@ import (
|
||||
// querying the pods metrics endpoint and lookup the metric value as defined by
|
||||
// the json path query.
|
||||
type JSONPathMetricsGetter struct {
|
||||
jsonPath *jsonpath.Compiled
|
||||
scheme string
|
||||
path string
|
||||
port int
|
||||
jsonPath *jsonpath.Compiled
|
||||
scheme string
|
||||
path string
|
||||
port int
|
||||
aggregator string
|
||||
client *http.Client
|
||||
rawQuery string
|
||||
}
|
||||
|
||||
// NewJSONPathMetricsGetter initializes a new JSONPathMetricsGetter.
|
||||
func NewJSONPathMetricsGetter(config map[string]string) (*JSONPathMetricsGetter, error) {
|
||||
getter := &JSONPathMetricsGetter{}
|
||||
httpClient := defaultHTTPClient()
|
||||
getter := &JSONPathMetricsGetter{client: httpClient}
|
||||
|
||||
if v, ok := config["json-key"]; ok {
|
||||
pat, err := jsonpath.Compile(v)
|
||||
path, err := jsonpath.Compile(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse json path definition: %v", err)
|
||||
}
|
||||
|
||||
getter.jsonPath = pat
|
||||
getter.jsonPath = path
|
||||
}
|
||||
|
||||
if v, ok := config["scheme"]; ok {
|
||||
@ -44,6 +50,10 @@ func NewJSONPathMetricsGetter(config map[string]string) (*JSONPathMetricsGetter,
|
||||
getter.path = v
|
||||
}
|
||||
|
||||
if v, ok := config["raw-query"]; ok {
|
||||
getter.rawQuery = v
|
||||
}
|
||||
|
||||
if v, ok := config["port"]; ok {
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
@ -52,14 +62,33 @@ func NewJSONPathMetricsGetter(config map[string]string) (*JSONPathMetricsGetter,
|
||||
getter.port = n
|
||||
}
|
||||
|
||||
if v, ok := config["aggregator"]; ok {
|
||||
getter.aggregator = v
|
||||
}
|
||||
|
||||
return getter, nil
|
||||
}
|
||||
|
||||
func defaultHTTPClient() *http.Client {
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 15 * time.Second,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 50,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
},
|
||||
Timeout: 15 * time.Second,
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
// GetMetric gets metric from pod by fetching json metrics from the pods metric
|
||||
// endpoint and extracting the desired value using the specified json path
|
||||
// query.
|
||||
func (g *JSONPathMetricsGetter) GetMetric(pod *corev1.Pod) (float64, error) {
|
||||
data, err := getPodMetrics(pod, g.scheme, g.path, g.port)
|
||||
data, err := g.getPodMetrics(pod)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@ -83,38 +112,52 @@ func (g *JSONPathMetricsGetter) GetMetric(pod *corev1.Pod) (float64, error) {
|
||||
return float64(res), nil
|
||||
case float64:
|
||||
return res, nil
|
||||
case []interface{}:
|
||||
s, err := castSlice(res)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return reduce(s, g.aggregator)
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported type %T", res)
|
||||
}
|
||||
}
|
||||
|
||||
// castSlice takes a slice of interface and returns a slice of float64 if all
|
||||
// values in slice were castable, else returns an error
|
||||
func castSlice(in []interface{}) ([]float64, error) {
|
||||
out := []float64{}
|
||||
|
||||
for _, v := range in {
|
||||
switch v := v.(type) {
|
||||
case int:
|
||||
out = append(out, float64(v))
|
||||
case float32:
|
||||
out = append(out, float64(v))
|
||||
case float64:
|
||||
out = append(out, v)
|
||||
default:
|
||||
return nil, fmt.Errorf("slice was returned by JSONPath, but value inside is unsupported: %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// getPodMetrics returns the content of the pods metrics endpoint.
|
||||
func getPodMetrics(pod *corev1.Pod, scheme, path string, port int) ([]byte, error) {
|
||||
func (g *JSONPathMetricsGetter) getPodMetrics(pod *corev1.Pod) ([]byte, error) {
|
||||
if pod.Status.PodIP == "" {
|
||||
return nil, fmt.Errorf("pod %s/%s does not have a pod IP", pod.Namespace, pod.Namespace)
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
Timeout: 15 * time.Second,
|
||||
Transport: &http.Transport{},
|
||||
}
|
||||
|
||||
if scheme == "" {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
metricsURL := url.URL{
|
||||
Scheme: scheme,
|
||||
Host: fmt.Sprintf("%s:%d", pod.Status.PodIP, port),
|
||||
Path: path,
|
||||
}
|
||||
metricsURL := g.buildMetricsURL(pod.Status.PodIP)
|
||||
|
||||
request, err := http.NewRequest(http.MethodGet, metricsURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(request)
|
||||
resp, err := g.client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -131,3 +174,80 @@ func getPodMetrics(pod *corev1.Pod, scheme, path string, port int) ([]byte, erro
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// buildMetricsURL will build the full URL needed to hit the pod metric endpoint.
|
||||
func (g *JSONPathMetricsGetter) buildMetricsURL(podIP string) url.URL {
|
||||
var scheme = g.scheme
|
||||
|
||||
if scheme == "" {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
return url.URL{
|
||||
Scheme: scheme,
|
||||
Host: fmt.Sprintf("%s:%d", podIP, g.port),
|
||||
Path: g.path,
|
||||
RawQuery: g.rawQuery,
|
||||
}
|
||||
}
|
||||
|
||||
// reduce will reduce a slice of numbers given a aggregator function's name. If it's empty or not recognized, an error is returned.
|
||||
func reduce(values []float64, aggregator string) (float64, error) {
|
||||
switch aggregator {
|
||||
case "avg":
|
||||
return avg(values), nil
|
||||
case "min":
|
||||
return min(values), nil
|
||||
case "max":
|
||||
return max(values), nil
|
||||
case "sum":
|
||||
return sum(values), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("slice of numbers was returned by JSONPath, but no valid aggregator function was specified: %v", aggregator)
|
||||
}
|
||||
}
|
||||
|
||||
// avg implements the average mathematical function over a slice of float64
|
||||
func avg(values []float64) float64 {
|
||||
sum := sum(values)
|
||||
return sum / float64(len(values))
|
||||
}
|
||||
|
||||
// min implements the absolute minimum mathematical function over a slice of float64
|
||||
func min(values []float64) float64 {
|
||||
// initialized with positive infinity, all finite numbers are smaller than it
|
||||
curMin := math.Inf(1)
|
||||
|
||||
for _, v := range values {
|
||||
if v < curMin {
|
||||
curMin = v
|
||||
}
|
||||
}
|
||||
|
||||
return curMin
|
||||
}
|
||||
|
||||
// max implements the absolute maximum mathematical function over a slice of float64
|
||||
func max(values []float64) float64 {
|
||||
// initialized with negative infinity, all finite numbers are bigger than it
|
||||
curMax := math.Inf(-1)
|
||||
|
||||
for _, v := range values {
|
||||
if v > curMax {
|
||||
curMax = v
|
||||
}
|
||||
}
|
||||
|
||||
return curMax
|
||||
}
|
||||
|
||||
// sum implements the summation mathematical function over a slice of float64
|
||||
func sum(values []float64) float64 {
|
||||
res := 0.0
|
||||
|
||||
for _, v := range values {
|
||||
res += v
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
173
pkg/collector/json_path_collector_test.go
Normal file
173
pkg/collector/json_path_collector_test.go
Normal file
@ -0,0 +1,173 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/oliveagle/jsonpath"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func compareMetricsGetter(t *testing.T, first, second *JSONPathMetricsGetter) {
|
||||
require.Equal(t, first.jsonPath, second.jsonPath)
|
||||
require.Equal(t, first.scheme, second.scheme)
|
||||
require.Equal(t, first.path, second.path)
|
||||
require.Equal(t, first.port, second.port)
|
||||
}
|
||||
|
||||
func TestNewJSONPathMetricsGetter(t *testing.T) {
|
||||
configNoAggregator := map[string]string{
|
||||
"json-key": "$.value",
|
||||
"scheme": "http",
|
||||
"path": "/metrics",
|
||||
"port": "9090",
|
||||
}
|
||||
jpath1, _ := jsonpath.Compile(configNoAggregator["json-key"])
|
||||
getterNoAggregator, err1 := NewJSONPathMetricsGetter(configNoAggregator)
|
||||
|
||||
require.NoError(t, err1)
|
||||
compareMetricsGetter(t, &JSONPathMetricsGetter{
|
||||
jsonPath: jpath1,
|
||||
scheme: "http",
|
||||
path: "/metrics",
|
||||
port: 9090,
|
||||
}, getterNoAggregator)
|
||||
|
||||
configAggregator := map[string]string{
|
||||
"json-key": "$.values",
|
||||
"scheme": "http",
|
||||
"path": "/metrics",
|
||||
"port": "9090",
|
||||
"aggregator": "avg",
|
||||
}
|
||||
jpath2, _ := jsonpath.Compile(configAggregator["json-key"])
|
||||
getterAggregator, err2 := NewJSONPathMetricsGetter(configAggregator)
|
||||
|
||||
require.NoError(t, err2)
|
||||
compareMetricsGetter(t, &JSONPathMetricsGetter{
|
||||
jsonPath: jpath2,
|
||||
scheme: "http",
|
||||
path: "/metrics",
|
||||
port: 9090,
|
||||
aggregator: "avg",
|
||||
}, getterAggregator)
|
||||
|
||||
configErrorJSONPath := map[string]string{
|
||||
"json-key": "{}",
|
||||
"scheme": "http",
|
||||
"path": "/metrics",
|
||||
"port": "9090",
|
||||
}
|
||||
|
||||
_, err3 := NewJSONPathMetricsGetter(configErrorJSONPath)
|
||||
require.Error(t, err3)
|
||||
|
||||
configErrorPort := map[string]string{
|
||||
"json-key": "$.values",
|
||||
"scheme": "http",
|
||||
"path": "/metrics",
|
||||
"port": "a9090",
|
||||
}
|
||||
|
||||
_, err4 := NewJSONPathMetricsGetter(configErrorPort)
|
||||
require.Error(t, err4)
|
||||
|
||||
configWithRawQuery := map[string]string{
|
||||
"json-key": "$.values",
|
||||
"scheme": "http",
|
||||
"path": "/metrics",
|
||||
"port": "9090",
|
||||
"raw-query": "foo=bar&baz=bop",
|
||||
}
|
||||
jpath5, _ := jsonpath.Compile(configWithRawQuery["json-key"])
|
||||
getterWithRawQuery, err5 := NewJSONPathMetricsGetter(configWithRawQuery)
|
||||
|
||||
require.NoError(t, err5)
|
||||
compareMetricsGetter(t, &JSONPathMetricsGetter{
|
||||
jsonPath: jpath5,
|
||||
scheme: "http",
|
||||
path: "/metrics",
|
||||
port: 9090,
|
||||
rawQuery: "foo=bar&baz=bop",
|
||||
}, getterWithRawQuery)
|
||||
}
|
||||
|
||||
func TestCastSlice(t *testing.T) {
|
||||
res1, err1 := castSlice([]interface{}{1, 2, 3})
|
||||
require.NoError(t, err1)
|
||||
require.Equal(t, []float64{1.0, 2.0, 3.0}, res1)
|
||||
|
||||
res2, err2 := castSlice([]interface{}{float32(1.0), float32(2.0), float32(3.0)})
|
||||
require.NoError(t, err2)
|
||||
require.Equal(t, []float64{1.0, 2.0, 3.0}, res2)
|
||||
|
||||
res3, err3 := castSlice([]interface{}{float64(1.0), float64(2.0), float64(3.0)})
|
||||
require.NoError(t, err3)
|
||||
require.Equal(t, []float64{1.0, 2.0, 3.0}, res3)
|
||||
|
||||
res4, err4 := castSlice([]interface{}{1, 2, "some string"})
|
||||
require.Errorf(t, err4, "slice was returned by JSONPath, but value inside is unsupported: %T", "string")
|
||||
require.Equal(t, []float64(nil), res4)
|
||||
}
|
||||
|
||||
func TestReduce(t *testing.T) {
|
||||
average, err1 := reduce([]float64{1, 2, 3}, "avg")
|
||||
require.NoError(t, err1)
|
||||
require.Equal(t, 2.0, average)
|
||||
|
||||
min, err2 := reduce([]float64{1, 2, 3}, "min")
|
||||
require.NoError(t, err2)
|
||||
require.Equal(t, 1.0, min)
|
||||
|
||||
max, err3 := reduce([]float64{1, 2, 3}, "max")
|
||||
require.NoError(t, err3)
|
||||
require.Equal(t, 3.0, max)
|
||||
|
||||
sum, err4 := reduce([]float64{1, 2, 3}, "sum")
|
||||
require.NoError(t, err4)
|
||||
require.Equal(t, 6.0, sum)
|
||||
|
||||
_, err5 := reduce([]float64{1, 2, 3}, "inexistent_function")
|
||||
require.Errorf(t, err5, "slice of numbers was returned by JSONPath, but no valid aggregator function was specified: %v", "inexistent_function")
|
||||
}
|
||||
|
||||
func TestBuildMetricsURL(t *testing.T) {
|
||||
scheme := "http"
|
||||
ip := "1.2.3.4"
|
||||
port := "9090"
|
||||
path := "/v1/test/"
|
||||
rawQuery := "foo=bar&baz=bop"
|
||||
|
||||
// Test building URL with rawQuery
|
||||
configWithRawQuery := map[string]string{
|
||||
"json-key": "$.value",
|
||||
"scheme": scheme,
|
||||
"path": path,
|
||||
"port": port,
|
||||
"raw-query": rawQuery,
|
||||
}
|
||||
_, err := jsonpath.Compile(configWithRawQuery["json-key"])
|
||||
require.NoError(t, err)
|
||||
getterWithRawQuery, err1 := NewJSONPathMetricsGetter(configWithRawQuery)
|
||||
require.NoError(t, err1)
|
||||
|
||||
expectedURLWithQuery := fmt.Sprintf("%s://%s:%s%s?%s", scheme, ip, port, path, rawQuery)
|
||||
receivedURLWithQuery := getterWithRawQuery.buildMetricsURL(ip)
|
||||
require.Equal(t, receivedURLWithQuery.String(), expectedURLWithQuery)
|
||||
|
||||
// Test building URL without rawQuery
|
||||
configWithNoQuery := map[string]string{
|
||||
"json-key": "$.value",
|
||||
"scheme": scheme,
|
||||
"path": path,
|
||||
"port": port,
|
||||
}
|
||||
_, err2 := jsonpath.Compile(configWithNoQuery["json-key"])
|
||||
require.NoError(t, err2)
|
||||
getterWithNoQuery, err3 := NewJSONPathMetricsGetter(configWithNoQuery)
|
||||
require.NoError(t, err3)
|
||||
|
||||
expectedURLNoQuery := fmt.Sprintf("%s://%s:%s%s", scheme, ip, port, path)
|
||||
receivedURLNoQuery := getterWithNoQuery.buildMetricsURL(ip)
|
||||
require.Equal(t, receivedURLNoQuery.String(), expectedURLNoQuery)
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
)
|
||||
|
||||
// MaxWeightedCollector is a simple aggregator collector that returns the maximum value
|
||||
// of metrics from all collectors.
|
||||
type MaxWeightedCollector struct {
|
||||
collectors []Collector
|
||||
interval time.Duration
|
||||
weight float64
|
||||
}
|
||||
|
||||
// NewMaxWeightedCollector initializes a new MaxWeightedCollector.
|
||||
func NewMaxWeightedCollector(interval time.Duration, weight float64, collectors ...Collector) *MaxWeightedCollector {
|
||||
return &MaxWeightedCollector{
|
||||
collectors: collectors,
|
||||
interval: interval,
|
||||
weight: weight,
|
||||
}
|
||||
}
|
||||
|
||||
// GetMetrics gets metrics from all collectors and return the higest value.
|
||||
func (c *MaxWeightedCollector) GetMetrics() ([]CollectedMetric, error) {
|
||||
errors := make([]error, 0)
|
||||
collectedMetrics := make([]CollectedMetric, 0)
|
||||
for _, collector := range c.collectors {
|
||||
values, err := collector.GetMetrics()
|
||||
if err != nil {
|
||||
if _, ok := err.(NoResultError); ok {
|
||||
errors = append(errors, err)
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
collectedMetrics = append(collectedMetrics, values...)
|
||||
}
|
||||
if len(collectedMetrics) == 0 {
|
||||
if len(errors) == 0 {
|
||||
return nil, fmt.Errorf("no metrics collected, cannot determine max")
|
||||
}
|
||||
errorStrings := make([]string, len(errors))
|
||||
for i, e := range errors {
|
||||
errorStrings[i] = e.Error()
|
||||
}
|
||||
allErrors := strings.Join(errorStrings, ",")
|
||||
return nil, fmt.Errorf("could not determine maximum due to errors: %s", allErrors)
|
||||
}
|
||||
max := collectedMetrics[0]
|
||||
for _, value := range collectedMetrics {
|
||||
if value.Custom.Value.MilliValue() > max.Custom.Value.MilliValue() {
|
||||
max = value
|
||||
}
|
||||
}
|
||||
max.Custom.Value = *resource.NewMilliQuantity(int64(c.weight*float64(max.Custom.Value.MilliValue())), resource.DecimalSI)
|
||||
return []CollectedMetric{max}, nil
|
||||
}
|
||||
|
||||
// Interval returns the interval at which the collector should run.
|
||||
func (c *MaxWeightedCollector) Interval() time.Duration {
|
||||
return c.interval
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
"k8s.io/metrics/pkg/apis/custom_metrics"
|
||||
)
|
||||
|
||||
type dummyCollector struct {
|
||||
value int64
|
||||
}
|
||||
|
||||
func (c dummyCollector) Interval() time.Duration {
|
||||
return time.Second
|
||||
}
|
||||
|
||||
func (c dummyCollector) GetMetrics() ([]CollectedMetric, error) {
|
||||
switch c.value {
|
||||
case 0:
|
||||
return nil, NoResultError{query: "invalid query"}
|
||||
case -1:
|
||||
return nil, fmt.Errorf("test error")
|
||||
default:
|
||||
quantity := resource.NewQuantity(c.value, resource.DecimalSI)
|
||||
return []CollectedMetric{
|
||||
{
|
||||
Custom: custom_metrics.MetricValue{
|
||||
Value: *quantity,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxCollector(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
values []int64
|
||||
expected int
|
||||
weight float64
|
||||
errored bool
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
values: []int64{100, 10, 9},
|
||||
expected: 100,
|
||||
weight: 1,
|
||||
errored: false,
|
||||
},
|
||||
{
|
||||
name: "weighted",
|
||||
values: []int64{100, 10, 9},
|
||||
expected: 20,
|
||||
weight: 0.2,
|
||||
errored: false,
|
||||
},
|
||||
{
|
||||
name: "with error",
|
||||
values: []int64{10, 9, -1},
|
||||
weight: 0.5,
|
||||
errored: true,
|
||||
},
|
||||
{
|
||||
name: "some invalid results",
|
||||
values: []int64{0, 1, 0, 10, 9},
|
||||
expected: 5,
|
||||
weight: 0.5,
|
||||
errored: false,
|
||||
},
|
||||
{
|
||||
name: "both invalid results and errors",
|
||||
values: []int64{0, 1, 0, -1, 10, 9},
|
||||
weight: 0.5,
|
||||
errored: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
collectors := make([]Collector, len(tc.values))
|
||||
for i, v := range tc.values {
|
||||
collectors[i] = dummyCollector{value: v}
|
||||
}
|
||||
wc := NewMaxWeightedCollector(time.Second, tc.weight, collectors...)
|
||||
metrics, err := wc.GetMetrics()
|
||||
if tc.errored {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, metrics, 1)
|
||||
require.EqualValues(t, tc.expected, metrics[0].Custom.Value.Value())
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package collector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
@ -37,6 +38,7 @@ type PodCollector struct {
|
||||
metricType autoscalingv2.MetricSourceType
|
||||
interval time.Duration
|
||||
logger *log.Entry
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
type PodMetricsGetter interface {
|
||||
|
@ -3,6 +3,7 @@ package collector
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@ -18,8 +19,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
PrometheusMetricName = "prometheus-query"
|
||||
prometheusQueryNameLabelKey = "query-name"
|
||||
PrometheusMetricName = "prometheus-query"
|
||||
prometheusQueryNameLabelKey = "query-name"
|
||||
prometheusServerAnnotationKey = "prometheus-server"
|
||||
)
|
||||
|
||||
type NoResultError struct {
|
||||
@ -38,7 +40,7 @@ type PrometheusCollectorPlugin struct {
|
||||
func NewPrometheusCollectorPlugin(client kubernetes.Interface, prometheusServer string) (*PrometheusCollectorPlugin, error) {
|
||||
cfg := api.Config{
|
||||
Address: prometheusServer,
|
||||
RoundTripper: &http.Transport{},
|
||||
RoundTripper: http.DefaultTransport,
|
||||
}
|
||||
|
||||
promClient, err := api.NewClient(cfg)
|
||||
@ -90,7 +92,11 @@ func NewPrometheusCollector(client kubernetes.Interface, promAPI promv1.API, hpa
|
||||
return nil, fmt.Errorf("no prometheus query defined")
|
||||
}
|
||||
case autoscalingv2.ExternalMetricSourceType:
|
||||
queryName, ok := config.Metric.Selector.MatchLabels[prometheusQueryNameLabelKey]
|
||||
if config.Metric.Selector == nil {
|
||||
return nil, fmt.Errorf("selector for prometheus query is not specified")
|
||||
}
|
||||
|
||||
queryName, ok := config.Config[prometheusQueryNameLabelKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("query name not specified on metric")
|
||||
}
|
||||
@ -101,6 +107,20 @@ func NewPrometheusCollector(client kubernetes.Interface, promAPI promv1.API, hpa
|
||||
} else {
|
||||
return nil, fmt.Errorf("no prometheus query defined for metric")
|
||||
}
|
||||
|
||||
// Use custom Prometheus URL if defined in HPA annotation.
|
||||
if promServer, ok := config.Config[prometheusServerAnnotationKey]; ok {
|
||||
cfg := api.Config{
|
||||
Address: promServer,
|
||||
RoundTripper: http.DefaultTransport,
|
||||
}
|
||||
|
||||
promClient, err := api.NewClient(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.promAPI = promv1.NewAPI(promClient)
|
||||
}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
@ -127,7 +147,7 @@ func (c *PrometheusCollector) GetMetrics() ([]CollectedMetric, error) {
|
||||
sampleValue = scalar.Value
|
||||
}
|
||||
|
||||
if sampleValue.String() == "NaN" {
|
||||
if math.IsNaN(float64(sampleValue)) {
|
||||
return nil, &NoResultError{query: c.query}
|
||||
}
|
||||
|
||||
|
@ -2,8 +2,10 @@ package collector
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -15,11 +17,15 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
rpsQuery = `scalar(sum(rate(skipper_serve_host_duration_seconds_count{host="%s"}[1m])))`
|
||||
rpsQuery = `scalar(sum(rate(skipper_serve_host_duration_seconds_count{host=~"%s"}[1m])) * %.4f)`
|
||||
rpsMetricName = "requests-per-second"
|
||||
rpsMetricBackendSeparator = ","
|
||||
)
|
||||
|
||||
var (
|
||||
errBackendNameMissing = errors.New("backend name must be specified for requests-per-second when traffic switching is used")
|
||||
)
|
||||
|
||||
// SkipperCollectorPlugin is a collector plugin for initializing metrics
|
||||
// collectors for getting skipper ingress metrics.
|
||||
type SkipperCollectorPlugin struct {
|
||||
@ -67,8 +73,7 @@ type SkipperCollector struct {
|
||||
}
|
||||
|
||||
// NewSkipperCollector initializes a new SkipperCollector.
|
||||
func NewSkipperCollector(client kubernetes.Interface, plugin CollectorPlugin, hpa *autoscalingv2.HorizontalPodAutoscaler,
|
||||
config *MetricConfig, interval time.Duration, backendAnnotations []string, backend string) (*SkipperCollector, error) {
|
||||
func NewSkipperCollector(client kubernetes.Interface, plugin CollectorPlugin, hpa *autoscalingv2.HorizontalPodAutoscaler, config *MetricConfig, interval time.Duration, backendAnnotations []string, backend string) (*SkipperCollector, error) {
|
||||
return &SkipperCollector{
|
||||
client: client,
|
||||
objectReference: config.ObjectReference,
|
||||
@ -94,7 +99,7 @@ func getAnnotationWeight(backendWeights string, backend string) float64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func getWeights(ingressAnnotations map[string]string, backendAnnotations []string, backend string) float64 {
|
||||
func getWeights(ingressAnnotations map[string]string, backendAnnotations []string, backend string) (float64, error) {
|
||||
maxWeight := 0.0
|
||||
annotationsPresent := false
|
||||
|
||||
@ -107,10 +112,15 @@ func getWeights(ingressAnnotations map[string]string, backendAnnotations []strin
|
||||
|
||||
// Fallback for ingresses that don't use traffic switching
|
||||
if !annotationsPresent {
|
||||
return 1.0
|
||||
return 1.0, nil
|
||||
}
|
||||
|
||||
return maxWeight
|
||||
// Require backend name here
|
||||
if backend != "" {
|
||||
return maxWeight, nil
|
||||
}
|
||||
|
||||
return 0.0, errBackendNameMissing
|
||||
}
|
||||
|
||||
// getCollector returns a collector for getting the metrics.
|
||||
@ -120,32 +130,31 @@ func (c *SkipperCollector) getCollector() (Collector, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
backendWeight := getWeights(ingress.Annotations, c.backendAnnotations, c.backend)
|
||||
backendWeight, err := getWeights(ingress.Annotations, c.backendAnnotations, c.backend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config := c.config
|
||||
|
||||
var collector Collector
|
||||
collectors := make([]Collector, 0, len(ingress.Spec.Rules))
|
||||
var escapedHostnames []string
|
||||
for _, rule := range ingress.Spec.Rules {
|
||||
host := strings.Replace(rule.Host, ".", "_", -1)
|
||||
config.Config = map[string]string{
|
||||
"query": fmt.Sprintf(rpsQuery, host),
|
||||
}
|
||||
|
||||
config.PerReplica = false // per replica is handled outside of the prometheus collector
|
||||
collector, err := c.plugin.NewCollector(c.hpa, &config, c.interval)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
collectors = append(collectors, collector)
|
||||
escapedHostnames = append(escapedHostnames, regexp.QuoteMeta(strings.Replace(rule.Host, ".", "_", -1)))
|
||||
}
|
||||
|
||||
if len(collectors) > 0 {
|
||||
collector = NewMaxWeightedCollector(c.interval, backendWeight, collectors...)
|
||||
} else {
|
||||
if len(escapedHostnames) == 0 {
|
||||
return nil, fmt.Errorf("no hosts defined on ingress %s/%s, unable to create collector", c.objectReference.Namespace, c.objectReference.Name)
|
||||
}
|
||||
|
||||
config.Config = map[string]string{
|
||||
"query": fmt.Sprintf(rpsQuery, strings.Join(escapedHostnames, "|"), backendWeight),
|
||||
}
|
||||
|
||||
config.PerReplica = false // per replica is handled outside of the prometheus collector
|
||||
collector, err := c.plugin.NewCollector(c.hpa, &config, c.interval)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return collector, nil
|
||||
}
|
||||
|
||||
@ -165,22 +174,26 @@ func (c *SkipperCollector) GetMetrics() ([]CollectedMetric, error) {
|
||||
return nil, fmt.Errorf("expected to only get one metric value, got %d", len(values))
|
||||
}
|
||||
|
||||
// get current replicas for the targeted scale object. This is used to
|
||||
// calculate an average metric instead of total.
|
||||
// targetAverageValue will be available in Kubernetes v1.12
|
||||
// https://github.com/kubernetes/kubernetes/pull/64097
|
||||
replicas, err := targetRefReplicas(c.client, c.hpa)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if replicas < 1 {
|
||||
return nil, fmt.Errorf("unable to get average value for %d replicas", replicas)
|
||||
}
|
||||
|
||||
value := values[0]
|
||||
avgValue := float64(value.Custom.Value.MilliValue()) / float64(replicas)
|
||||
value.Custom.Value = *resource.NewMilliQuantity(int64(avgValue), resource.DecimalSI)
|
||||
|
||||
// For Kubernetes <v1.14 we have to fall back to manual average
|
||||
if c.config.MetricSpec.Object.Target.AverageValue == nil {
|
||||
// get current replicas for the targeted scale object. This is used to
|
||||
// calculate an average metric instead of total.
|
||||
// targetAverageValue will be available in Kubernetes v1.12
|
||||
// https://github.com/kubernetes/kubernetes/pull/64097
|
||||
replicas, err := targetRefReplicas(c.client, c.hpa)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if replicas < 1 {
|
||||
return nil, fmt.Errorf("unable to get average value for %d replicas", replicas)
|
||||
}
|
||||
|
||||
avgValue := float64(value.Custom.Value.MilliValue()) / float64(replicas)
|
||||
value.Custom.Value = *resource.NewMilliQuantity(int64(avgValue), resource.DecimalSI)
|
||||
}
|
||||
|
||||
return []CollectedMetric{value}, nil
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -44,11 +44,7 @@ func NewZMONCollectorPlugin(zmon zmon.ZMON) (*ZMONCollectorPlugin, error) {
|
||||
func (c *ZMONCollectorPlugin) NewCollector(hpa *autoscalingv2.HorizontalPodAutoscaler, config *MetricConfig, interval time.Duration) (Collector, error) {
|
||||
switch config.Metric.Name {
|
||||
case ZMONCheckMetric:
|
||||
annotations := map[string]string{}
|
||||
if hpa != nil {
|
||||
annotations = hpa.Annotations
|
||||
}
|
||||
return NewZMONCollector(c.zmon, config, annotations, interval)
|
||||
return NewZMONCollector(c.zmon, config, interval)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("metric '%s' not supported", config.Metric.Name)
|
||||
@ -68,7 +64,11 @@ type ZMONCollector struct {
|
||||
}
|
||||
|
||||
// NewZMONCollector initializes a new ZMONCollector.
|
||||
func NewZMONCollector(zmon zmon.ZMON, config *MetricConfig, annotations map[string]string, interval time.Duration) (*ZMONCollector, error) {
|
||||
func NewZMONCollector(zmon zmon.ZMON, config *MetricConfig, interval time.Duration) (*ZMONCollector, error) {
|
||||
if config.Metric.Selector == nil {
|
||||
return nil, fmt.Errorf("selector for zmon-check is not specified")
|
||||
}
|
||||
|
||||
checkIDStr, ok := config.Config[zmonCheckIDLabelKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("ZMON check ID not specified on metric")
|
||||
@ -86,11 +86,6 @@ func NewZMONCollector(zmon zmon.ZMON, config *MetricConfig, annotations map[stri
|
||||
key = k
|
||||
}
|
||||
|
||||
// annotations takes precedence over label
|
||||
if k, ok := annotations[zmonKeyAnnotationKey]; ok {
|
||||
key = k
|
||||
}
|
||||
|
||||
duration := defaultQueryDuration
|
||||
|
||||
// parse optional duration value
|
||||
@ -110,16 +105,6 @@ func NewZMONCollector(zmon zmon.ZMON, config *MetricConfig, annotations map[stri
|
||||
}
|
||||
}
|
||||
|
||||
// parse tags from annotations
|
||||
// tags defined in annotations takes precedence over tags defined in
|
||||
// the labels.
|
||||
for k, v := range annotations {
|
||||
if strings.HasPrefix(k, zmonTagPrefixAnnotationKey) {
|
||||
key := strings.TrimPrefix(k, zmonTagPrefixAnnotationKey)
|
||||
tags[key] = v
|
||||
}
|
||||
}
|
||||
|
||||
// default aggregator is last
|
||||
aggregators := []string{"last"}
|
||||
if k, ok := config.Config[zmonAggregatorsLabelKey]; ok {
|
||||
|
@ -50,20 +50,6 @@ func TestZMONCollectorNewCollector(t *testing.T) {
|
||||
require.Equal(t, []string{"max"}, zmonCollector.aggregators)
|
||||
require.Equal(t, map[string]string{"alias": "cluster_alias"}, zmonCollector.tags)
|
||||
|
||||
// check that annotations overwrites labels
|
||||
hpa.ObjectMeta = metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
zmonKeyAnnotationKey: "annotation_key",
|
||||
zmonTagPrefixAnnotationKey + "alias": "cluster_alias_annotation",
|
||||
},
|
||||
}
|
||||
collector, err = collectPlugin.NewCollector(hpa, config, 1*time.Second)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, collector)
|
||||
zmonCollector = collector.(*ZMONCollector)
|
||||
require.Equal(t, "annotation_key", zmonCollector.key)
|
||||
require.Equal(t, map[string]string{"alias": "cluster_alias_annotation"}, zmonCollector.tags)
|
||||
|
||||
// should fail if the metric name isn't ZMON
|
||||
config.Metric = newMetricIdentifier("non-zmon-check")
|
||||
_, err = collectPlugin.NewCollector(nil, config, 1*time.Second)
|
||||
@ -131,7 +117,7 @@ func TestZMONCollectorGetMetrics(tt *testing.T) {
|
||||
dataPoints: ti.dataPoints,
|
||||
}
|
||||
|
||||
zmonCollector, err := NewZMONCollector(z, config, nil, 1*time.Second)
|
||||
zmonCollector, err := NewZMONCollector(z, config, 1*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
metrics, _ := zmonCollector.GetMetrics()
|
||||
|
@ -1,247 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
autoscalingv2beta1 "k8s.io/api/autoscaling/v2beta1"
|
||||
autoscaling "k8s.io/api/autoscaling/v2beta2"
|
||||
core "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/conversion"
|
||||
)
|
||||
|
||||
// from: https://github.com/kubernetes/kubernetes/blob/v1.14.4/pkg/apis/autoscaling/v2beta1/conversion.go
|
||||
|
||||
func Convert_autoscaling_MetricTarget_To_v2beta1_CrossVersionObjectReference(in *autoscaling.MetricTarget, out *autoscalingv2beta1.CrossVersionObjectReference, s conversion.Scope) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_v2beta1_CrossVersionObjectReference_To_autoscaling_MetricTarget(in *autoscalingv2beta1.CrossVersionObjectReference, out *autoscaling.MetricTarget, s conversion.Scope) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_v2beta1_ResourceMetricStatus_To_autoscaling_ResourceMetricStatus(in *autoscalingv2beta1.ResourceMetricStatus, out *autoscaling.ResourceMetricStatus, s conversion.Scope) error {
|
||||
out.Name = core.ResourceName(in.Name)
|
||||
utilization := in.CurrentAverageUtilization
|
||||
averageValue := in.CurrentAverageValue
|
||||
out.Current = autoscaling.MetricValueStatus{
|
||||
AverageValue: &averageValue,
|
||||
AverageUtilization: utilization,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_autoscaling_ResourceMetricStatus_To_v2beta1_ResourceMetricStatus(in *autoscaling.ResourceMetricStatus, out *autoscalingv2beta1.ResourceMetricStatus, s conversion.Scope) error {
|
||||
out.Name = v1.ResourceName(in.Name)
|
||||
out.CurrentAverageUtilization = in.Current.AverageUtilization
|
||||
if in.Current.AverageValue != nil {
|
||||
out.CurrentAverageValue = *in.Current.AverageValue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_v2beta1_ResourceMetricSource_To_autoscaling_ResourceMetricSource(in *autoscalingv2beta1.ResourceMetricSource, out *autoscaling.ResourceMetricSource, s conversion.Scope) error {
|
||||
out.Name = core.ResourceName(in.Name)
|
||||
utilization := in.TargetAverageUtilization
|
||||
averageValue := in.TargetAverageValue
|
||||
|
||||
var metricType autoscaling.MetricTargetType
|
||||
if utilization == nil {
|
||||
metricType = autoscaling.AverageValueMetricType
|
||||
} else {
|
||||
metricType = autoscaling.UtilizationMetricType
|
||||
}
|
||||
out.Target = autoscaling.MetricTarget{
|
||||
Type: metricType,
|
||||
AverageValue: averageValue,
|
||||
AverageUtilization: utilization,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_autoscaling_ResourceMetricSource_To_v2beta1_ResourceMetricSource(in *autoscaling.ResourceMetricSource, out *autoscalingv2beta1.ResourceMetricSource, s conversion.Scope) error {
|
||||
out.Name = v1.ResourceName(in.Name)
|
||||
out.TargetAverageUtilization = in.Target.AverageUtilization
|
||||
out.TargetAverageValue = in.Target.AverageValue
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_autoscaling_ExternalMetricSource_To_v2beta1_ExternalMetricSource(in *autoscaling.ExternalMetricSource, out *autoscalingv2beta1.ExternalMetricSource, s conversion.Scope) error {
|
||||
out.MetricName = in.Metric.Name
|
||||
out.TargetValue = in.Target.Value
|
||||
out.TargetAverageValue = in.Target.AverageValue
|
||||
out.MetricSelector = in.Metric.Selector
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_v2beta1_ExternalMetricSource_To_autoscaling_ExternalMetricSource(in *autoscalingv2beta1.ExternalMetricSource, out *autoscaling.ExternalMetricSource, s conversion.Scope) error {
|
||||
value := in.TargetValue
|
||||
averageValue := in.TargetAverageValue
|
||||
|
||||
var metricType autoscaling.MetricTargetType
|
||||
if value == nil {
|
||||
metricType = autoscaling.AverageValueMetricType
|
||||
} else {
|
||||
metricType = autoscaling.ValueMetricType
|
||||
}
|
||||
|
||||
out.Target = autoscaling.MetricTarget{
|
||||
Type: metricType,
|
||||
Value: value,
|
||||
AverageValue: averageValue,
|
||||
}
|
||||
|
||||
out.Metric = autoscaling.MetricIdentifier{
|
||||
Name: in.MetricName,
|
||||
Selector: in.MetricSelector,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_autoscaling_ObjectMetricSource_To_v2beta1_ObjectMetricSource(in *autoscaling.ObjectMetricSource, out *autoscalingv2beta1.ObjectMetricSource, s conversion.Scope) error {
|
||||
if in.Target.Value != nil {
|
||||
out.TargetValue = *in.Target.Value
|
||||
}
|
||||
out.AverageValue = in.Target.AverageValue
|
||||
|
||||
out.Target = autoscalingv2beta1.CrossVersionObjectReference{
|
||||
Kind: in.DescribedObject.Kind,
|
||||
Name: in.DescribedObject.Name,
|
||||
APIVersion: in.DescribedObject.APIVersion,
|
||||
}
|
||||
out.MetricName = in.Metric.Name
|
||||
out.Selector = in.Metric.Selector
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_v2beta1_ObjectMetricSource_To_autoscaling_ObjectMetricSource(in *autoscalingv2beta1.ObjectMetricSource, out *autoscaling.ObjectMetricSource, s conversion.Scope) error {
|
||||
var metricType autoscaling.MetricTargetType
|
||||
if in.AverageValue == nil {
|
||||
metricType = autoscaling.ValueMetricType
|
||||
} else {
|
||||
metricType = autoscaling.AverageValueMetricType
|
||||
}
|
||||
out.Target = autoscaling.MetricTarget{
|
||||
Type: metricType,
|
||||
Value: &in.TargetValue,
|
||||
AverageValue: in.AverageValue,
|
||||
}
|
||||
out.DescribedObject = autoscaling.CrossVersionObjectReference{
|
||||
Kind: in.Target.Kind,
|
||||
Name: in.Target.Name,
|
||||
APIVersion: in.Target.APIVersion,
|
||||
}
|
||||
out.Metric = autoscaling.MetricIdentifier{
|
||||
Name: in.MetricName,
|
||||
Selector: in.Selector,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_autoscaling_PodsMetricSource_To_v2beta1_PodsMetricSource(in *autoscaling.PodsMetricSource, out *autoscalingv2beta1.PodsMetricSource, s conversion.Scope) error {
|
||||
if in.Target.AverageValue != nil {
|
||||
targetAverageValue := *in.Target.AverageValue
|
||||
out.TargetAverageValue = targetAverageValue
|
||||
}
|
||||
|
||||
out.MetricName = in.Metric.Name
|
||||
out.Selector = in.Metric.Selector
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_v2beta1_PodsMetricSource_To_autoscaling_PodsMetricSource(in *autoscalingv2beta1.PodsMetricSource, out *autoscaling.PodsMetricSource, s conversion.Scope) error {
|
||||
targetAverageValue := &in.TargetAverageValue
|
||||
var metricType autoscaling.MetricTargetType
|
||||
metricType = autoscaling.AverageValueMetricType
|
||||
|
||||
out.Target = autoscaling.MetricTarget{
|
||||
Type: metricType,
|
||||
AverageValue: targetAverageValue,
|
||||
}
|
||||
out.Metric = autoscaling.MetricIdentifier{
|
||||
Name: in.MetricName,
|
||||
Selector: in.Selector,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_autoscaling_ExternalMetricStatus_To_v2beta1_ExternalMetricStatus(in *autoscaling.ExternalMetricStatus, out *autoscalingv2beta1.ExternalMetricStatus, s conversion.Scope) error {
|
||||
if &in.Current.AverageValue != nil {
|
||||
out.CurrentAverageValue = in.Current.AverageValue
|
||||
}
|
||||
out.MetricName = in.Metric.Name
|
||||
if in.Current.Value != nil {
|
||||
out.CurrentValue = *in.Current.Value
|
||||
}
|
||||
out.MetricSelector = in.Metric.Selector
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_v2beta1_ExternalMetricStatus_To_autoscaling_ExternalMetricStatus(in *autoscalingv2beta1.ExternalMetricStatus, out *autoscaling.ExternalMetricStatus, s conversion.Scope) error {
|
||||
value := in.CurrentValue
|
||||
averageValue := in.CurrentAverageValue
|
||||
out.Current = autoscaling.MetricValueStatus{
|
||||
Value: &value,
|
||||
AverageValue: averageValue,
|
||||
}
|
||||
out.Metric = autoscaling.MetricIdentifier{
|
||||
Name: in.MetricName,
|
||||
Selector: in.MetricSelector,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_autoscaling_ObjectMetricStatus_To_v2beta1_ObjectMetricStatus(in *autoscaling.ObjectMetricStatus, out *autoscalingv2beta1.ObjectMetricStatus, s conversion.Scope) error {
|
||||
if in.Current.Value != nil {
|
||||
out.CurrentValue = *in.Current.Value
|
||||
}
|
||||
out.Target = autoscalingv2beta1.CrossVersionObjectReference{
|
||||
Kind: in.DescribedObject.Kind,
|
||||
Name: in.DescribedObject.Name,
|
||||
APIVersion: in.DescribedObject.APIVersion,
|
||||
}
|
||||
out.MetricName = in.Metric.Name
|
||||
out.Selector = in.Metric.Selector
|
||||
if in.Current.AverageValue != nil {
|
||||
currentAverageValue := *in.Current.AverageValue
|
||||
out.AverageValue = ¤tAverageValue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_v2beta1_ObjectMetricStatus_To_autoscaling_ObjectMetricStatus(in *autoscalingv2beta1.ObjectMetricStatus, out *autoscaling.ObjectMetricStatus, s conversion.Scope) error {
|
||||
out.Current = autoscaling.MetricValueStatus{
|
||||
Value: &in.CurrentValue,
|
||||
AverageValue: in.AverageValue,
|
||||
}
|
||||
out.DescribedObject = autoscaling.CrossVersionObjectReference{
|
||||
Kind: in.Target.Kind,
|
||||
Name: in.Target.Name,
|
||||
APIVersion: in.Target.APIVersion,
|
||||
}
|
||||
out.Metric = autoscaling.MetricIdentifier{
|
||||
Name: in.MetricName,
|
||||
Selector: in.Selector,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_autoscaling_PodsMetricStatus_To_v2beta1_PodsMetricStatus(in *autoscaling.PodsMetricStatus, out *autoscalingv2beta1.PodsMetricStatus, s conversion.Scope) error {
|
||||
if in.Current.AverageValue != nil {
|
||||
out.CurrentAverageValue = *in.Current.AverageValue
|
||||
}
|
||||
out.MetricName = in.Metric.Name
|
||||
out.Selector = in.Metric.Selector
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_v2beta1_PodsMetricStatus_To_autoscaling_PodsMetricStatus(in *autoscalingv2beta1.PodsMetricStatus, out *autoscaling.PodsMetricStatus, s conversion.Scope) error {
|
||||
out.Current = autoscaling.MetricValueStatus{
|
||||
AverageValue: &in.CurrentAverageValue,
|
||||
}
|
||||
out.Metric = autoscaling.MetricIdentifier{
|
||||
Name: in.MetricName,
|
||||
Selector: in.Selector,
|
||||
}
|
||||
return nil
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@ package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
@ -49,16 +50,17 @@ var (
|
||||
// HPAProvider is a base provider for initializing metric collectors based on
|
||||
// HPA resources.
|
||||
type HPAProvider struct {
|
||||
client kubernetes.Interface
|
||||
interval time.Duration
|
||||
collectorScheduler *CollectorScheduler
|
||||
collectorInterval time.Duration
|
||||
metricSink chan metricCollection
|
||||
hpaCache map[resourceReference]autoscalingv2.HorizontalPodAutoscaler
|
||||
metricStore *MetricStore
|
||||
collectorFactory *collector.CollectorFactory
|
||||
recorder kube_record.EventRecorder
|
||||
logger *log.Entry
|
||||
client kubernetes.Interface
|
||||
interval time.Duration
|
||||
collectorScheduler *CollectorScheduler
|
||||
collectorInterval time.Duration
|
||||
metricSink chan metricCollection
|
||||
hpaCache map[resourceReference]autoscalingv2.HorizontalPodAutoscaler
|
||||
metricStore *MetricStore
|
||||
collectorFactory *collector.CollectorFactory
|
||||
recorder kube_record.EventRecorder
|
||||
logger *log.Entry
|
||||
disregardIncompatibleHPAs bool
|
||||
}
|
||||
|
||||
// metricCollection is a container for sending collected metrics across a
|
||||
@ -69,7 +71,7 @@ type metricCollection struct {
|
||||
}
|
||||
|
||||
// NewHPAProvider initializes a new HPAProvider.
|
||||
func NewHPAProvider(client kubernetes.Interface, interval, collectorInterval time.Duration, collectorFactory *collector.CollectorFactory) *HPAProvider {
|
||||
func NewHPAProvider(client kubernetes.Interface, interval, collectorInterval time.Duration, collectorFactory *collector.CollectorFactory, disregardIncompatibleHPAs bool) *HPAProvider {
|
||||
metricsc := make(chan metricCollection)
|
||||
|
||||
return &HPAProvider{
|
||||
@ -80,9 +82,10 @@ func NewHPAProvider(client kubernetes.Interface, interval, collectorInterval tim
|
||||
metricStore: NewMetricStore(func() time.Time {
|
||||
return time.Now().UTC().Add(15 * time.Minute)
|
||||
}),
|
||||
collectorFactory: collectorFactory,
|
||||
recorder: recorder.CreateEventRecorder(client),
|
||||
logger: log.WithFields(log.Fields{"provider": "hpa"}),
|
||||
collectorFactory: collectorFactory,
|
||||
recorder: recorder.CreateEventRecorder(client),
|
||||
logger: log.WithFields(log.Fields{"provider": "hpa"}),
|
||||
disregardIncompatibleHPAs: disregardIncompatibleHPAs,
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,7 +119,7 @@ func (p *HPAProvider) Run(ctx context.Context) {
|
||||
func (p *HPAProvider) updateHPAs() error {
|
||||
p.logger.Info("Looking for HPAs")
|
||||
|
||||
hpas, err := p.client.AutoscalingV2beta1().HorizontalPodAutoscalers(metav1.NamespaceAll).List(metav1.ListOptions{})
|
||||
hpas, err := p.client.AutoscalingV2beta2().HorizontalPodAutoscalers(metav1.NamespaceAll).List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -125,15 +128,7 @@ func (p *HPAProvider) updateHPAs() error {
|
||||
|
||||
newHPAs := 0
|
||||
|
||||
for _, hpav1 := range hpas.Items {
|
||||
hpav1 := hpav1
|
||||
hpa := autoscalingv2.HorizontalPodAutoscaler{}
|
||||
err := Convert_v2beta1_HorizontalPodAutoscaler_To_autoscaling_HorizontalPodAutoscaler(&hpav1, &hpa, nil)
|
||||
if err != nil {
|
||||
p.logger.Errorf("Failed to convert HPA to v2beta2: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, hpa := range hpas.Items {
|
||||
resourceRef := resourceReference{
|
||||
Name: hpa.Name,
|
||||
Namespace: hpa.Namespace,
|
||||
@ -162,15 +157,20 @@ func (p *HPAProvider) updateHPAs() error {
|
||||
interval = p.collectorInterval
|
||||
}
|
||||
|
||||
collector, err := p.collectorFactory.NewCollector(&hpa, config, interval)
|
||||
c, err := p.collectorFactory.NewCollector(&hpa, config, interval)
|
||||
if err != nil {
|
||||
p.recorder.Eventf(&hpa, apiv1.EventTypeWarning, "CreateNewMetricsCollector", "Failed to create new metrics collector: %v", err)
|
||||
|
||||
// Only log when it's not a PluginNotFoundError AND flag disregardIncompatibleHPAs is true
|
||||
if !(errors.Is(err, &collector.PluginNotFoundError{}) && p.disregardIncompatibleHPAs) {
|
||||
p.recorder.Eventf(&hpa, apiv1.EventTypeWarning, "CreateNewMetricsCollector", "Failed to create new metrics collector: %v", err)
|
||||
}
|
||||
|
||||
cache = false
|
||||
continue
|
||||
}
|
||||
|
||||
p.logger.Infof("Adding new metrics collector: %T", collector)
|
||||
p.collectorScheduler.Add(resourceRef, config.MetricTypeName, collector)
|
||||
p.logger.Infof("Adding new metrics collector: %T", c)
|
||||
p.collectorScheduler.Add(resourceRef, config.MetricTypeName, c)
|
||||
}
|
||||
newHPAs++
|
||||
|
||||
@ -262,7 +262,7 @@ func (p *HPAProvider) collectMetrics(ctx context.Context) {
|
||||
}
|
||||
|
||||
// GetMetricByName gets a single metric by name.
|
||||
func (p *HPAProvider) GetMetricByName(name types.NamespacedName, info provider.CustomMetricInfo) (*custom_metrics.MetricValue, error) {
|
||||
func (p *HPAProvider) GetMetricByName(name types.NamespacedName, info provider.CustomMetricInfo, metricSelector labels.Selector) (*custom_metrics.MetricValue, error) {
|
||||
metric := p.metricStore.GetMetricsByName(name, info)
|
||||
if metric == nil {
|
||||
return nil, provider.NewMetricNotFoundForError(info.GroupResource, info.Metric, name.Name)
|
||||
@ -272,7 +272,7 @@ func (p *HPAProvider) GetMetricByName(name types.NamespacedName, info provider.C
|
||||
|
||||
// GetMetricBySelector returns metrics for namespaced resources by
|
||||
// label selector.
|
||||
func (p *HPAProvider) GetMetricBySelector(namespace string, selector labels.Selector, info provider.CustomMetricInfo) (*custom_metrics.MetricValueList, error) {
|
||||
func (p *HPAProvider) GetMetricBySelector(namespace string, selector labels.Selector, info provider.CustomMetricInfo, metricSelector labels.Selector) (*custom_metrics.MetricValueList, error) {
|
||||
return p.metricStore.GetMetricsBySelector(namespace, selector, info), nil
|
||||
}
|
||||
|
||||
|
@ -7,8 +7,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zalando-incubator/kube-metrics-adapter/pkg/collector"
|
||||
autoscalingv1 "k8s.io/api/autoscaling/v2beta1"
|
||||
autoscalingv2 "k8s.io/api/autoscaling/v2beta2"
|
||||
autoscaling "k8s.io/api/autoscaling/v2beta2"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
@ -16,7 +15,7 @@ import (
|
||||
|
||||
type mockCollectorPlugin struct{}
|
||||
|
||||
func (m mockCollectorPlugin) NewCollector(hpa *autoscalingv2.HorizontalPodAutoscaler, config *collector.MetricConfig, interval time.Duration) (collector.Collector, error) {
|
||||
func (m mockCollectorPlugin) NewCollector(hpa *autoscaling.HorizontalPodAutoscaler, config *collector.MetricConfig, interval time.Duration) (collector.Collector, error) {
|
||||
return mockCollector{}, nil
|
||||
}
|
||||
|
||||
@ -33,7 +32,7 @@ func (c mockCollector) Interval() time.Duration {
|
||||
func TestUpdateHPAs(t *testing.T) {
|
||||
value := resource.MustParse("1k")
|
||||
|
||||
hpa := &autoscalingv1.HorizontalPodAutoscaler{
|
||||
hpa := &autoscaling.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "hpa1",
|
||||
Namespace: "default",
|
||||
@ -43,20 +42,25 @@ func TestUpdateHPAs(t *testing.T) {
|
||||
"metric-config.pods.requests-per-second.json-path/port": "9090",
|
||||
},
|
||||
},
|
||||
Spec: autoscalingv1.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{
|
||||
Spec: autoscaling.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscaling.CrossVersionObjectReference{
|
||||
Kind: "Deployment",
|
||||
Name: "app",
|
||||
APIVersion: "apps/v1",
|
||||
},
|
||||
MinReplicas: &[]int32{1}[0],
|
||||
MaxReplicas: 10,
|
||||
Metrics: []autoscalingv1.MetricSpec{
|
||||
Metrics: []autoscaling.MetricSpec{
|
||||
{
|
||||
Type: autoscalingv1.PodsMetricSourceType,
|
||||
Pods: &autoscalingv1.PodsMetricSource{
|
||||
MetricName: "requests-per-second",
|
||||
TargetAverageValue: value,
|
||||
Type: autoscaling.PodsMetricSourceType,
|
||||
Pods: &autoscaling.PodsMetricSource{
|
||||
Metric: autoscaling.MetricIdentifier{
|
||||
Name: "requests-per-second",
|
||||
},
|
||||
Target: autoscaling.MetricTarget{
|
||||
Type: autoscaling.AverageValueMetricType,
|
||||
AverageValue: &value,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -66,14 +70,14 @@ func TestUpdateHPAs(t *testing.T) {
|
||||
fakeClient := fake.NewSimpleClientset()
|
||||
|
||||
var err error
|
||||
hpa, err = fakeClient.AutoscalingV2beta1().HorizontalPodAutoscalers("default").Create(hpa)
|
||||
hpa, err = fakeClient.AutoscalingV2beta2().HorizontalPodAutoscalers("default").Create(hpa)
|
||||
require.NoError(t, err)
|
||||
|
||||
collectorFactory := collector.NewCollectorFactory()
|
||||
err = collectorFactory.RegisterPodsCollector("", mockCollectorPlugin{})
|
||||
require.NoError(t, err)
|
||||
|
||||
provider := NewHPAProvider(fakeClient, 1*time.Second, 1*time.Second, collectorFactory)
|
||||
provider := NewHPAProvider(fakeClient, 1*time.Second, 1*time.Second, collectorFactory, false)
|
||||
provider.collectorScheduler = NewCollectorScheduler(context.Background(), provider.metricSink)
|
||||
|
||||
err = provider.updateHPAs()
|
||||
@ -82,7 +86,7 @@ func TestUpdateHPAs(t *testing.T) {
|
||||
|
||||
// update HPA
|
||||
hpa.Annotations["metric-config.pods.requests-per-second.json-path/port"] = "8080"
|
||||
_, err = fakeClient.AutoscalingV2beta1().HorizontalPodAutoscalers("default").Update(hpa)
|
||||
_, err = fakeClient.AutoscalingV2beta2().HorizontalPodAutoscalers("default").Update(hpa)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.updateHPAs()
|
||||
@ -90,3 +94,56 @@ func TestUpdateHPAs(t *testing.T) {
|
||||
|
||||
require.Len(t, provider.collectorScheduler.table, 1)
|
||||
}
|
||||
|
||||
func TestUpdateHPAsDisregardingIncompatibleHPA(t *testing.T) {
|
||||
// Test HPAProvider with disregardIncompatibleHPAs = true
|
||||
|
||||
value := resource.MustParse("1k")
|
||||
|
||||
hpa := &autoscaling.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "hpa1",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: autoscaling.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscaling.CrossVersionObjectReference{
|
||||
Kind: "Deployment",
|
||||
Name: "app",
|
||||
APIVersion: "apps/v1",
|
||||
},
|
||||
MinReplicas: &[]int32{1}[0],
|
||||
MaxReplicas: 10,
|
||||
Metrics: []autoscaling.MetricSpec{
|
||||
{
|
||||
Type: autoscaling.ExternalMetricSourceType,
|
||||
External: &autoscaling.ExternalMetricSource{
|
||||
Metric: autoscaling.MetricIdentifier{
|
||||
Name: "some-other-metric",
|
||||
},
|
||||
Target: autoscaling.MetricTarget{
|
||||
Type: autoscaling.AverageValueMetricType,
|
||||
AverageValue: &value,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewSimpleClientset()
|
||||
|
||||
var err error
|
||||
_, err = fakeClient.AutoscalingV2beta2().HorizontalPodAutoscalers("default").Create(hpa)
|
||||
require.NoError(t, err)
|
||||
|
||||
collectorFactory := collector.NewCollectorFactory()
|
||||
err = collectorFactory.RegisterPodsCollector("", mockCollectorPlugin{})
|
||||
require.NoError(t, err)
|
||||
|
||||
provider := NewHPAProvider(fakeClient, 1*time.Second, 1*time.Second, collectorFactory, true)
|
||||
provider.collectorScheduler = NewCollectorScheduler(context.Background(), provider.metricSink)
|
||||
|
||||
err = provider.updateHPAs()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ import (
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/golang/glog"
|
||||
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/cmd/server"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/spf13/cobra"
|
||||
@ -38,6 +37,7 @@ import (
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -88,6 +88,12 @@ func NewCommandStartAdapterServer(stopCh <-chan struct{}) *cobra.Command {
|
||||
"whether to enable External Metrics API")
|
||||
flags.StringVar(&o.PrometheusServer, "prometheus-server", o.PrometheusServer, ""+
|
||||
"url of prometheus server to query")
|
||||
flags.StringVar(&o.InfluxDBAddress, "influxdb-address", o.InfluxDBAddress, ""+
|
||||
"address of InfluxDB 2.x server to query (e.g. http://localhost:9999)")
|
||||
flags.StringVar(&o.InfluxDBToken, "influxdb-token", o.InfluxDBToken, ""+
|
||||
"token for InfluxDB 2.x server to query")
|
||||
flags.StringVar(&o.InfluxDBOrg, "influxdb-org", o.InfluxDBOrg, ""+
|
||||
"organization ID for InfluxDB 2.x server to query")
|
||||
flags.StringVar(&o.ZMONKariosDBEndpoint, "zmon-kariosdb-endpoint", o.ZMONKariosDBEndpoint, ""+
|
||||
"url of ZMON KariosDB endpoint to query for ZMON checks")
|
||||
flags.StringVar(&o.ZMONTokenName, "zmon-token-name", o.ZMONTokenName, ""+
|
||||
@ -104,14 +110,15 @@ func NewCommandStartAdapterServer(stopCh <-chan struct{}) *cobra.Command {
|
||||
"whether to enable AWS external metrics")
|
||||
flags.StringSliceVar(&o.AWSRegions, "aws-region", o.AWSRegions, "the AWS regions which should be monitored. eg: eu-central, eu-west-1")
|
||||
flags.StringVar(&o.MetricsAddress, "metrics-address", o.MetricsAddress, "The address where to serve prometheus metrics")
|
||||
|
||||
flags.BoolVar(&o.DisregardIncompatibleHPAs, "disregard-incompatible-hpas", o.DisregardIncompatibleHPAs, ""+
|
||||
"disregard failing to create collectors for incompatible HPAs")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o AdapterServerOptions) RunCustomMetricsAdapterServer(stopCh <-chan struct{}) error {
|
||||
go func() {
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
glog.Fatal(http.ListenAndServe(o.MetricsAddress, nil))
|
||||
klog.Fatal(http.ListenAndServe(o.MetricsAddress, nil))
|
||||
}()
|
||||
|
||||
config, err := o.Config()
|
||||
@ -175,6 +182,14 @@ func (o AdapterServerOptions) RunCustomMetricsAdapterServer(stopCh <-chan struct
|
||||
}
|
||||
}
|
||||
|
||||
if o.InfluxDBAddress != "" {
|
||||
influxdbPlugin, err := collector.NewInfluxDBCollectorPlugin(client, o.InfluxDBAddress, o.InfluxDBToken, o.InfluxDBOrg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize InfluxDB collector plugin: %v", err)
|
||||
}
|
||||
collectorFactory.RegisterExternalCollector([]string{collector.InfluxDBMetricName}, influxdbPlugin)
|
||||
}
|
||||
|
||||
// register generic pod collector
|
||||
err = collectorFactory.RegisterPodsCollector("", collector.NewPodCollectorPlugin(client))
|
||||
if err != nil {
|
||||
@ -214,7 +229,7 @@ func (o AdapterServerOptions) RunCustomMetricsAdapterServer(stopCh <-chan struct
|
||||
collectorFactory.RegisterExternalCollector([]string{collector.AWSSQSQueueLengthMetric}, collector.NewAWSCollectorPlugin(awsSessions))
|
||||
}
|
||||
|
||||
hpaProvider := provider.NewHPAProvider(client, 30*time.Second, 1*time.Minute, collectorFactory)
|
||||
hpaProvider := provider.NewHPAProvider(client, 30*time.Second, 1*time.Minute, collectorFactory, o.DisregardIncompatibleHPAs)
|
||||
|
||||
go hpaProvider.Run(ctx)
|
||||
|
||||
@ -290,6 +305,12 @@ type AdapterServerOptions struct {
|
||||
// PrometheusServer enables prometheus queries to the specified
|
||||
// server
|
||||
PrometheusServer string
|
||||
// InfluxDBAddress enables Flux queries to the specified InfluxDB instance
|
||||
InfluxDBAddress string
|
||||
// InfluxDBToken is the token used for querying InfluxDB
|
||||
InfluxDBToken string
|
||||
// InfluxDBOrg is the organization ID used for querying InfluxDB
|
||||
InfluxDBOrg string
|
||||
// ZMONKariosDBEndpoint enables ZMON check queries to the specified
|
||||
// kariosDB endpoint
|
||||
ZMONKariosDBEndpoint string
|
||||
@ -312,4 +333,7 @@ type AdapterServerOptions struct {
|
||||
MetricsAddress string
|
||||
// SkipperBackendWeightAnnotation is the annotation on the ingress indicating the backend weights
|
||||
SkipperBackendWeightAnnotation []string
|
||||
// Whether to disregard failing to create collectors for incompatible HPAs - such as when using
|
||||
// kube-metrics-adapter beside another Metrics Provider
|
||||
DisregardIncompatibleHPAs bool
|
||||
}
|
||||
|
Reference in New Issue
Block a user