diff --git a/go.mod b/go.mod
index a915dc7af5..db764e5206 100644
--- a/go.mod
+++ b/go.mod
@@ -21,6 +21,7 @@ require (
k8s.io/autoscaler/vertical-pod-autoscaler v0.9.2
k8s.io/client-go v0.23.0
k8s.io/klog/v2 v2.30.0
+ k8s.io/sample-controller v0.23.0
)
require (
diff --git a/go.sum b/go.sum
index 95146b968c..7232a7f1e9 100644
--- a/go.sum
+++ b/go.sum
@@ -148,11 +148,13 @@ github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
+github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -249,6 +251,7 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -280,6 +283,7 @@ github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
+github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@@ -379,6 +383,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
@@ -478,6 +483,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -565,6 +571,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -641,6 +648,7 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM=
golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -793,6 +801,7 @@ k8s.io/client-go v0.18.3/go.mod h1:4a/dpQEvzAhT1BbuWW09qvIaGw6Gbu1gZYiQZIi1DMw=
k8s.io/client-go v0.23.0 h1:vcsOqyPq7XV3QmQRCBH/t9BICJM9Q1M18qahjv+rebY=
k8s.io/client-go v0.23.0/go.mod h1:hrDnpnK1mSr65lHHcUuIZIXDgEbzc7/683c6hyG4jTA=
k8s.io/code-generator v0.18.3/go.mod h1:TgNEVx9hCyPGpdtCWA34olQYLkh3ok9ar7XfSsr8b6c=
+k8s.io/code-generator v0.23.0/go.mod h1:vQvOhDXhuzqiVfM/YHp+dmg10WDZCchJVObc9MvowsE=
k8s.io/component-base v0.18.3/go.mod h1:bp5GzGR0aGkYEfTj+eTY0AN/vXTgkJdQXjNTTVUaa3k=
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/gengo v0.0.0-20200114144118-36b2048a9120/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
@@ -809,6 +818,8 @@ k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4=
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk=
k8s.io/metrics v0.18.3/go.mod h1:TkuJE3ezDZ1ym8pYkZoEzJB7HDiFE7qxl+EmExEBoPA=
+k8s.io/sample-controller v0.23.0 h1:Zwxi+BQxEQDg63jGzxrLUbWBp+Qkqse/web1nkpSG9g=
+k8s.io/sample-controller v0.23.0/go.mod h1:8a1Cgok9A5JRa1rJgg9AQKrOF0hqwbaHt/wcndZ6fmY=
k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b h1:wxEMGetGMur3J1xuGLQY7GEQYg9bZxKn3tKo5k/eYcs=
diff --git a/internal/store/builder.go b/internal/store/builder.go
index aace8bad75..1564a0feab 100644
--- a/internal/store/builder.go
+++ b/internal/store/builder.go
@@ -43,6 +43,7 @@ import (
"k8s.io/klog/v2"
ksmtypes "k8s.io/kube-state-metrics/v2/pkg/builder/types"
+ "k8s.io/kube-state-metrics/v2/pkg/customresource"
generator "k8s.io/kube-state-metrics/v2/pkg/metric_generator"
metricsstore "k8s.io/kube-state-metrics/v2/pkg/metrics_store"
"k8s.io/kube-state-metrics/v2/pkg/options"
@@ -57,21 +58,23 @@ var _ ksmtypes.BuilderInterface = &Builder{}
// Builder helps to build store. It follows the builder pattern
// (https://en.wikipedia.org/wiki/Builder_pattern).
type Builder struct {
- kubeClient clientset.Interface
- vpaClient vpaclientset.Interface
- namespaces options.NamespaceList
- namespaceFilter string
- ctx context.Context
- enabledResources []string
- familyGeneratorFilter generator.FamilyGeneratorFilter
- listWatchMetrics *watch.ListWatchMetrics
- shardingMetrics *sharding.Metrics
- shard int32
- totalShards int
- buildStoresFunc ksmtypes.BuildStoresFunc
- allowAnnotationsList map[string][]string
- allowLabelsList map[string][]string
- useAPIServerCache bool
+ kubeClient clientset.Interface
+ customResourceClients map[string]interface{}
+ vpaClient vpaclientset.Interface
+ namespaces options.NamespaceList
+ namespaceFilter string
+ ctx context.Context
+ enabledResources []string
+ familyGeneratorFilter generator.FamilyGeneratorFilter
+ listWatchMetrics *watch.ListWatchMetrics
+ shardingMetrics *sharding.Metrics
+ shard int32
+ totalShards int
+ buildStoresFunc ksmtypes.BuildStoresFunc
+ buildCustomResourceStoresFunc ksmtypes.BuildCustomResourceStoresFunc
+ allowAnnotationsList map[string][]string
+ allowLabelsList map[string][]string
+ useAPIServerCache bool
}
// NewBuilder returns a new builder.
@@ -134,6 +137,16 @@ func (b *Builder) WithVPAClient(c vpaclientset.Interface) {
b.vpaClient = c
}
+// WithCustomResourceClients sets the customResourceClients property of a Builder.
+func (b *Builder) WithCustomResourceClients(cs map[string]interface{}) {
+ b.customResourceClients = cs
+}
+
+// WithUsingAPIServerCache configures whether using APIServer cache or not.
+func (b *Builder) WithUsingAPIServerCache(u bool) {
+ b.useAPIServerCache = u
+}
+
// WithFamilyGeneratorFilter configures the family generator filter which decides which
// metrics are to be exposed by the store build by the Builder.
func (b *Builder) WithFamilyGeneratorFilter(l generator.FamilyGeneratorFilter) {
@@ -141,9 +154,13 @@ func (b *Builder) WithFamilyGeneratorFilter(l generator.FamilyGeneratorFilter) {
}
// WithGenerateStoresFunc configures a custom generate store function
-func (b *Builder) WithGenerateStoresFunc(f ksmtypes.BuildStoresFunc, u bool) {
+func (b *Builder) WithGenerateStoresFunc(f ksmtypes.BuildStoresFunc) {
b.buildStoresFunc = f
- b.useAPIServerCache = u
+}
+
+// WithGenerateCustomResourceStoresFunc configures a custom generate custom resource store function
+func (b *Builder) WithGenerateCustomResourceStoresFunc(f ksmtypes.BuildCustomResourceStoresFunc) {
+ b.buildCustomResourceStoresFunc = f
}
// DefaultGenerateStoresFunc returns default buildStores function
@@ -151,6 +168,30 @@ func (b *Builder) DefaultGenerateStoresFunc() ksmtypes.BuildStoresFunc {
return b.buildStores
}
+// DefaultGenerateCustomResourceStoresFunc returns default buildCustomResourceStores function
+func (b *Builder) DefaultGenerateCustomResourceStoresFunc() ksmtypes.BuildCustomResourceStoresFunc {
+ return b.buildCustomResourceStores
+}
+
+// WithCustomResourceStoreFactories returns configures a custom resource stores factory
+func (b *Builder) WithCustomResourceStoreFactories(fs ...customresource.RegistryFactory) {
+ for i := range fs {
+ f := fs[i]
+ if _, ok := availableStores[f.Name()]; ok {
+ klog.Warningf("The internal resource store named %s already exists and is overridden by a custom resource store with the same name, please make sure it meets your expectation", f.Name())
+ }
+ availableStores[f.Name()] = func(b *Builder) []cache.Store {
+ return b.buildCustomResourceStoresFunc(
+ f.Name(),
+ f.MetricFamilyGenerators(b.allowAnnotationsList[f.Name()], b.allowLabelsList[f.Name()]),
+ f.ExpectedType(),
+ f.ListWatch,
+ b.useAPIServerCache,
+ )
+ }
+ }
+}
+
// WithAllowAnnotations configures which annotations can be returned for metrics
func (b *Builder) WithAllowAnnotations(annotations map[string][]string) {
if len(annotations) > 0 {
@@ -414,6 +455,47 @@ func (b *Builder) buildStores(
return stores
}
+// TODO(Garrybest): Merge `buildStores` and `buildCustomResourceStores`
+func (b *Builder) buildCustomResourceStores(resourceName string,
+ metricFamilies []generator.FamilyGenerator,
+ expectedType interface{},
+ listWatchFunc func(customResourceClient interface{}, ns string, fieldSelector string) cache.ListerWatcher,
+ useAPIServerCache bool,
+) []cache.Store {
+ metricFamilies = generator.FilterFamilyGenerators(b.familyGeneratorFilter, metricFamilies)
+ composedMetricGenFuncs := generator.ComposeMetricGenFuncs(metricFamilies)
+ familyHeaders := generator.ExtractMetricFamilyHeaders(metricFamilies)
+
+ customResourceClient, ok := b.customResourceClients[resourceName]
+ if !ok {
+ klog.Warningf("Custom resource client %s does not exist", resourceName)
+ return []cache.Store{}
+ }
+
+ if b.namespaces.IsAllNamespaces() {
+ store := metricsstore.NewMetricsStore(
+ familyHeaders,
+ composedMetricGenFuncs,
+ )
+ listWatcher := listWatchFunc(customResourceClient, v1.NamespaceAll, b.namespaceFilter)
+ b.startReflector(expectedType, store, listWatcher, useAPIServerCache)
+ return []cache.Store{store}
+ }
+
+ stores := make([]cache.Store, 0, len(b.namespaces))
+ for _, ns := range b.namespaces {
+ store := metricsstore.NewMetricsStore(
+ familyHeaders,
+ composedMetricGenFuncs,
+ )
+ listWatcher := listWatchFunc(customResourceClient, ns, b.namespaceFilter)
+ b.startReflector(expectedType, store, listWatcher, useAPIServerCache)
+ stores = append(stores, store)
+ }
+
+ return stores
+}
+
// startReflector starts a Kubernetes client-go reflector with the given
// listWatcher and registers it with the given store.
func (b *Builder) startReflector(
diff --git a/main.go b/main.go
index a1a15b3ef5..ff8e5ff0cd 100644
--- a/main.go
+++ b/main.go
@@ -19,65 +19,21 @@ package main
import (
"context"
"fmt"
- "net"
- "net/http"
- "net/http/pprof"
"os"
- "strconv"
- "time"
- "github.com/oklog/run"
- "github.com/pkg/errors"
- "github.com/prometheus/client_golang/prometheus"
- "github.com/prometheus/client_golang/prometheus/collectors"
- "github.com/prometheus/client_golang/prometheus/promauto"
- "github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/version"
- "github.com/prometheus/exporter-toolkit/web"
- vpaclientset "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/client/clientset/versioned"
- clientset "k8s.io/client-go/kubernetes"
- _ "k8s.io/client-go/plugin/pkg/client/auth"
- "k8s.io/client-go/tools/clientcmd"
"k8s.io/klog/v2"
- "k8s.io/kube-state-metrics/v2/internal/store"
- "k8s.io/kube-state-metrics/v2/pkg/allowdenylist"
- generator "k8s.io/kube-state-metrics/v2/pkg/metric_generator"
- "k8s.io/kube-state-metrics/v2/pkg/metricshandler"
- "k8s.io/kube-state-metrics/v2/pkg/optin"
+ "k8s.io/kube-state-metrics/v2/pkg/app"
"k8s.io/kube-state-metrics/v2/pkg/options"
- "k8s.io/kube-state-metrics/v2/pkg/util/proc"
)
-const (
- metricsPath = "/metrics"
- healthzPath = "/healthz"
-)
-
-// promLogger implements promhttp.Logger
-type promLogger struct{}
-
-func (pl promLogger) Println(v ...interface{}) {
- klog.Error(v...)
-}
-
-// promLogger implements the Logger interface
-func (pl promLogger) Log(v ...interface{}) error {
- klog.Info(v...)
- return nil
-}
-
func main() {
opts := options.NewOptions()
opts.AddFlags()
- promLogger := promLogger{}
-
- ctx := context.Background()
-
- err := opts.Parse()
- if err != nil {
- klog.Fatalf("Error: %s", err)
+ if err := opts.Parse(); err != nil {
+ klog.Fatalf("Parsing flag definitions error: %v", err)
}
if opts.Version {
@@ -89,222 +45,9 @@ func main() {
opts.Usage()
os.Exit(0)
}
- storeBuilder := store.NewBuilder()
-
- ksmMetricsRegistry := prometheus.NewRegistry()
- ksmMetricsRegistry.MustRegister(version.NewCollector("kube_state_metrics"))
- durationVec := promauto.With(ksmMetricsRegistry).NewHistogramVec(
- prometheus.HistogramOpts{
- Name: "http_request_duration_seconds",
- Help: "A histogram of requests for kube-state-metrics metrics handler.",
- Buckets: prometheus.DefBuckets,
- ConstLabels: prometheus.Labels{"handler": "metrics"},
- }, []string{"method"},
- )
- storeBuilder.WithMetrics(ksmMetricsRegistry)
-
- var resources []string
- if len(opts.Resources) == 0 {
- klog.Info("Using default resources")
- resources = options.DefaultResources.AsSlice()
- } else {
- klog.Infof("Using resources %s", opts.Resources.String())
- resources = opts.Resources.AsSlice()
- }
-
- if err := storeBuilder.WithEnabledResources(resources); err != nil {
- klog.Fatalf("Failed to set up resources: %v", err)
- }
-
- namespaces := opts.Namespaces.GetNamespaces()
- nsFieldSelector := namespaces.GetExcludeNSFieldSelector(opts.NamespacesDenylist)
- storeBuilder.WithNamespaces(namespaces, nsFieldSelector)
-
- allowDenyList, err := allowdenylist.New(opts.MetricAllowlist, opts.MetricDenylist)
- if err != nil {
- klog.Fatal(err)
- }
-
- err = allowDenyList.Parse()
- if err != nil {
- klog.Fatalf("error initializing the allowdeny list : %v", err)
- }
-
- klog.Infof("metric allow-denylisting: %v", allowDenyList.Status())
-
- optInMetricFamilyFilter, err := optin.NewMetricFamilyFilter(opts.MetricOptInList)
- if err != nil {
- klog.Fatalf("error initializing the opt-in metric list : %v", err)
- }
-
- if optInMetricFamilyFilter.Count() > 0 {
- klog.Infof("metrics which were opted into: %v", optInMetricFamilyFilter.Status())
- }
-
- storeBuilder.WithFamilyGeneratorFilter(generator.NewCompositeFamilyGeneratorFilter(
- allowDenyList,
- optInMetricFamilyFilter,
- ))
-
- storeBuilder.WithGenerateStoresFunc(storeBuilder.DefaultGenerateStoresFunc(), opts.UseAPIServerCache)
-
- proc.StartReaper()
-
- kubeClient, vpaClient, err := createKubeClient(opts.Apiserver, opts.Kubeconfig)
- if err != nil {
- klog.Fatalf("Failed to create client: %v", err)
- }
- storeBuilder.WithKubeClient(kubeClient)
- storeBuilder.WithVPAClient(vpaClient)
- storeBuilder.WithSharding(opts.Shard, opts.TotalShards)
- storeBuilder.WithAllowAnnotations(opts.AnnotationsAllowList)
- storeBuilder.WithAllowLabels(opts.LabelsAllowList)
- ksmMetricsRegistry.MustRegister(
- collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
- collectors.NewGoCollector(),
- )
-
- var g run.Group
-
- m := metricshandler.New(
- opts,
- kubeClient,
- storeBuilder,
- opts.EnableGZIPEncoding,
- )
- // Run MetricsHandler
- {
- ctxMetricsHandler, cancel := context.WithCancel(ctx)
- g.Add(func() error {
- return m.Run(ctxMetricsHandler)
- }, func(error) {
- cancel()
- })
- }
-
- tlsConfig := opts.TLSConfig
-
- telemetryMux := buildTelemetryServer(ksmMetricsRegistry)
- telemetryListenAddress := net.JoinHostPort(opts.TelemetryHost, strconv.Itoa(opts.TelemetryPort))
- telemetryServer := http.Server{Handler: telemetryMux, Addr: telemetryListenAddress}
-
- metricsMux := buildMetricsServer(m, durationVec)
- metricsServerListenAddress := net.JoinHostPort(opts.Host, strconv.Itoa(opts.Port))
- metricsServer := http.Server{Handler: metricsMux, Addr: metricsServerListenAddress}
-
- // Run Telemetry server
- {
- g.Add(func() error {
- klog.Infof("Starting kube-state-metrics self metrics server: %s", telemetryListenAddress)
- return web.ListenAndServe(&telemetryServer, tlsConfig, promLogger)
- }, func(error) {
- ctxShutDown, cancel := context.WithTimeout(ctx, 3*time.Second)
- defer cancel()
- telemetryServer.Shutdown(ctxShutDown)
- })
- }
- // Run Metrics server
- {
- g.Add(func() error {
- klog.Infof("Starting metrics server: %s", metricsServerListenAddress)
- return web.ListenAndServe(&metricsServer, tlsConfig, promLogger)
- }, func(error) {
- ctxShutDown, cancel := context.WithTimeout(ctx, 3*time.Second)
- defer cancel()
- metricsServer.Shutdown(ctxShutDown)
- })
- }
-
- if err := g.Run(); err != nil {
- klog.Fatalf("RunGroup Error: %v", err)
- }
- klog.Info("Exiting")
-}
-
-func createKubeClient(apiserver string, kubeconfig string) (clientset.Interface, vpaclientset.Interface, error) {
- config, err := clientcmd.BuildConfigFromFlags(apiserver, kubeconfig)
- if err != nil {
- return nil, nil, err
- }
-
- config.UserAgent = version.Version
- config.AcceptContentTypes = "application/vnd.kubernetes.protobuf,application/json"
- config.ContentType = "application/vnd.kubernetes.protobuf"
-
- kubeClient, err := clientset.NewForConfig(config)
- if err != nil {
- return nil, nil, err
- }
-
- vpaClient, err := vpaclientset.NewForConfig(config)
- if err != nil {
- return nil, nil, err
- }
- // Informers don't seem to do a good job logging error messages when it
- // can't reach the server, making debugging hard. This makes it easier to
- // figure out if apiserver is configured incorrectly.
- klog.Infof("Testing communication with server")
- v, err := kubeClient.Discovery().ServerVersion()
- if err != nil {
- return nil, nil, errors.Wrap(err, "error while trying to communicate with apiserver")
+ ctx := context.Background()
+ if err := app.RunKubeStateMetrics(ctx, opts); err != nil {
+ klog.Fatalf("Failed to run kube-state-metrics: %v", err)
}
- klog.Infof("Running with Kubernetes cluster version: v%s.%s. git version: %s. git tree state: %s. commit: %s. platform: %s",
- v.Major, v.Minor, v.GitVersion, v.GitTreeState, v.GitCommit, v.Platform)
- klog.Infof("Communication with server successful")
-
- return kubeClient, vpaClient, nil
-}
-
-func buildTelemetryServer(registry prometheus.Gatherer) *http.ServeMux {
- mux := http.NewServeMux()
-
- // Add metricsPath
- mux.Handle(metricsPath, promhttp.HandlerFor(registry, promhttp.HandlerOpts{ErrorLog: promLogger{}}))
- // Add index
- mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- w.Write([]byte(`
-
Kube-State-Metrics Metrics Server
-
- Kube-State-Metrics Metrics
-
-
- `))
- })
- return mux
-}
-
-func buildMetricsServer(m *metricshandler.MetricsHandler, durationObserver prometheus.ObserverVec) *http.ServeMux {
- mux := http.NewServeMux()
-
- // TODO: This doesn't belong into serveMetrics
- mux.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index))
- mux.Handle("/debug/pprof/cmdline", http.HandlerFunc(pprof.Cmdline))
- mux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile))
- mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol))
- mux.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace))
-
- mux.Handle(metricsPath, promhttp.InstrumentHandlerDuration(durationObserver, m))
-
- // Add healthzPath
- mux.HandleFunc(healthzPath, func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusOK)
- w.Write([]byte(http.StatusText(http.StatusOK)))
- })
- // Add index
- mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- w.Write([]byte(`
- Kube Metrics Server
-
- Kube Metrics
-
-
- `))
- })
- return mux
}
diff --git a/pkg/app/server.go b/pkg/app/server.go
new file mode 100644
index 0000000000..d8205de28f
--- /dev/null
+++ b/pkg/app/server.go
@@ -0,0 +1,313 @@
+/*
+Copyright 2021 The Kubernetes Authors All rights reserved.
+
+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 app
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "net/http"
+ "net/http/pprof"
+ "strconv"
+ "time"
+
+ "github.com/oklog/run"
+ "github.com/pkg/errors"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/collectors"
+ "github.com/prometheus/client_golang/prometheus/promauto"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
+ "github.com/prometheus/common/version"
+ "github.com/prometheus/exporter-toolkit/web"
+ vpaclientset "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/client/clientset/versioned"
+ clientset "k8s.io/client-go/kubernetes"
+ _ "k8s.io/client-go/plugin/pkg/client/auth" // Initialize common client auth plugins.
+ "k8s.io/client-go/tools/clientcmd"
+ "k8s.io/klog/v2"
+
+ "k8s.io/kube-state-metrics/v2/internal/store"
+ "k8s.io/kube-state-metrics/v2/pkg/allowdenylist"
+ "k8s.io/kube-state-metrics/v2/pkg/customresource"
+ generator "k8s.io/kube-state-metrics/v2/pkg/metric_generator"
+ "k8s.io/kube-state-metrics/v2/pkg/metricshandler"
+ "k8s.io/kube-state-metrics/v2/pkg/optin"
+ "k8s.io/kube-state-metrics/v2/pkg/options"
+ "k8s.io/kube-state-metrics/v2/pkg/util/proc"
+)
+
+const (
+ metricsPath = "/metrics"
+ healthzPath = "/healthz"
+)
+
+// promLogger implements promhttp.Logger
+type promLogger struct{}
+
+func (pl promLogger) Println(v ...interface{}) {
+ klog.Error(v...)
+}
+
+// promLogger implements the Logger interface
+func (pl promLogger) Log(v ...interface{}) error {
+ klog.Info(v...)
+ return nil
+}
+
+// RunKubeStateMetrics will build and run the kube-state-metrics.
+// Any out-of-tree custom resource metrics could be registered by newing a registry factory
+// which implements customresource.RegistryFactory and pass all factories into this function.
+func RunKubeStateMetrics(ctx context.Context, opts *options.Options, factories ...customresource.RegistryFactory) error {
+ promLogger := promLogger{}
+
+ storeBuilder := store.NewBuilder()
+ storeBuilder.WithCustomResourceStoreFactories(factories...)
+
+ ksmMetricsRegistry := prometheus.NewRegistry()
+ ksmMetricsRegistry.MustRegister(version.NewCollector("kube_state_metrics"))
+ durationVec := promauto.With(ksmMetricsRegistry).NewHistogramVec(
+ prometheus.HistogramOpts{
+ Name: "http_request_duration_seconds",
+ Help: "A histogram of requests for kube-state-metrics metrics handler.",
+ Buckets: prometheus.DefBuckets,
+ ConstLabels: prometheus.Labels{"handler": "metrics"},
+ }, []string{"method"},
+ )
+ storeBuilder.WithMetrics(ksmMetricsRegistry)
+
+ var resources []string
+ if len(opts.Resources) == 0 {
+ klog.Info("Using default resources")
+ resources = options.DefaultResources.AsSlice()
+ // enable custom resource
+ for _, factory := range factories {
+ resources = append(resources, factory.Name())
+ }
+ } else {
+ klog.Infof("Using resources %s", opts.Resources.String())
+ resources = opts.Resources.AsSlice()
+ }
+
+ if err := storeBuilder.WithEnabledResources(resources); err != nil {
+ return fmt.Errorf("failed to set up resources: %v", err)
+ }
+
+ namespaces := opts.Namespaces.GetNamespaces()
+ nsFieldSelector := namespaces.GetExcludeNSFieldSelector(opts.NamespacesDenylist)
+ storeBuilder.WithNamespaces(namespaces, nsFieldSelector)
+
+ allowDenyList, err := allowdenylist.New(opts.MetricAllowlist, opts.MetricDenylist)
+ if err != nil {
+ return err
+ }
+
+ err = allowDenyList.Parse()
+ if err != nil {
+ return fmt.Errorf("error initializing the allowdeny list: %v", err)
+ }
+
+ klog.Infof("Metric allow-denylisting: %v", allowDenyList.Status())
+
+ optInMetricFamilyFilter, err := optin.NewMetricFamilyFilter(opts.MetricOptInList)
+ if err != nil {
+ return fmt.Errorf("error initializing the opt-in metric list: %v", err)
+ }
+
+ if optInMetricFamilyFilter.Count() > 0 {
+ klog.Infof("Metrics which were opted into: %v", optInMetricFamilyFilter.Status())
+ }
+
+ storeBuilder.WithFamilyGeneratorFilter(generator.NewCompositeFamilyGeneratorFilter(
+ allowDenyList,
+ optInMetricFamilyFilter,
+ ))
+
+ storeBuilder.WithUsingAPIServerCache(opts.UseAPIServerCache)
+ storeBuilder.WithGenerateStoresFunc(storeBuilder.DefaultGenerateStoresFunc())
+ storeBuilder.WithGenerateCustomResourceStoresFunc(storeBuilder.DefaultGenerateCustomResourceStoresFunc())
+
+ proc.StartReaper()
+
+ kubeClient, vpaClient, customResourceClients, err := createKubeClient(opts.Apiserver, opts.Kubeconfig, factories...)
+ if err != nil {
+ return fmt.Errorf("failed to create client: %v", err)
+ }
+ storeBuilder.WithKubeClient(kubeClient)
+ storeBuilder.WithVPAClient(vpaClient)
+ storeBuilder.WithCustomResourceClients(customResourceClients)
+ storeBuilder.WithSharding(opts.Shard, opts.TotalShards)
+ storeBuilder.WithAllowAnnotations(opts.AnnotationsAllowList)
+ storeBuilder.WithAllowLabels(opts.LabelsAllowList)
+
+ ksmMetricsRegistry.MustRegister(
+ collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
+ collectors.NewGoCollector(),
+ )
+
+ var g run.Group
+
+ m := metricshandler.New(
+ opts,
+ kubeClient,
+ storeBuilder,
+ opts.EnableGZIPEncoding,
+ )
+ // Run MetricsHandler
+ {
+ ctxMetricsHandler, cancel := context.WithCancel(ctx)
+ g.Add(func() error {
+ return m.Run(ctxMetricsHandler)
+ }, func(error) {
+ cancel()
+ })
+ }
+
+ tlsConfig := opts.TLSConfig
+
+ telemetryMux := buildTelemetryServer(ksmMetricsRegistry)
+ telemetryListenAddress := net.JoinHostPort(opts.TelemetryHost, strconv.Itoa(opts.TelemetryPort))
+ telemetryServer := http.Server{Handler: telemetryMux, Addr: telemetryListenAddress}
+
+ metricsMux := buildMetricsServer(m, durationVec)
+ metricsServerListenAddress := net.JoinHostPort(opts.Host, strconv.Itoa(opts.Port))
+ metricsServer := http.Server{Handler: metricsMux, Addr: metricsServerListenAddress}
+
+ // Run Telemetry server
+ {
+ g.Add(func() error {
+ klog.Infof("Starting kube-state-metrics self metrics server: %s", telemetryListenAddress)
+ return web.ListenAndServe(&telemetryServer, tlsConfig, promLogger)
+ }, func(error) {
+ ctxShutDown, cancel := context.WithTimeout(ctx, 3*time.Second)
+ defer cancel()
+ telemetryServer.Shutdown(ctxShutDown)
+ })
+ }
+ // Run Metrics server
+ {
+ g.Add(func() error {
+ klog.Infof("Starting metrics server: %s", metricsServerListenAddress)
+ return web.ListenAndServe(&metricsServer, tlsConfig, promLogger)
+ }, func(error) {
+ ctxShutDown, cancel := context.WithTimeout(ctx, 3*time.Second)
+ defer cancel()
+ metricsServer.Shutdown(ctxShutDown)
+ })
+ }
+
+ if err := g.Run(); err != nil {
+ return fmt.Errorf("run server group error: %v", err)
+ }
+ klog.Info("Exiting")
+ return nil
+}
+
+func createKubeClient(apiserver string, kubeconfig string, factories ...customresource.RegistryFactory) (clientset.Interface, vpaclientset.Interface, map[string]interface{}, error) {
+ config, err := clientcmd.BuildConfigFromFlags(apiserver, kubeconfig)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+
+ config.UserAgent = version.Version
+ config.AcceptContentTypes = "application/vnd.kubernetes.protobuf,application/json"
+ config.ContentType = "application/vnd.kubernetes.protobuf"
+
+ kubeClient, err := clientset.NewForConfig(config)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+
+ vpaClient, err := vpaclientset.NewForConfig(config)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+
+ customResourceClients := make(map[string]interface{}, len(factories))
+ for _, f := range factories {
+ customResourceClient, err := f.CreateClient(config)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ customResourceClients[f.Name()] = customResourceClient
+ }
+
+ // Informers don't seem to do a good job logging error messages when it
+ // can't reach the server, making debugging hard. This makes it easier to
+ // figure out if apiserver is configured incorrectly.
+ klog.Infof("Testing communication with server")
+ v, err := kubeClient.Discovery().ServerVersion()
+ if err != nil {
+ return nil, nil, nil, errors.Wrap(err, "error while trying to communicate with apiserver")
+ }
+ klog.Infof("Running with Kubernetes cluster version: v%s.%s. git version: %s. git tree state: %s. commit: %s. platform: %s",
+ v.Major, v.Minor, v.GitVersion, v.GitTreeState, v.GitCommit, v.Platform)
+ klog.Infof("Communication with server successful")
+
+ return kubeClient, vpaClient, customResourceClients, nil
+}
+
+func buildTelemetryServer(registry prometheus.Gatherer) *http.ServeMux {
+ mux := http.NewServeMux()
+
+ // Add metricsPath
+ mux.Handle(metricsPath, promhttp.HandlerFor(registry, promhttp.HandlerOpts{ErrorLog: promLogger{}}))
+ // Add index
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ w.Write([]byte(`
+ Kube-State-Metrics Metrics Server
+
+ Kube-State-Metrics Metrics
+
+
+ `))
+ })
+ return mux
+}
+
+func buildMetricsServer(m *metricshandler.MetricsHandler, durationObserver prometheus.ObserverVec) *http.ServeMux {
+ mux := http.NewServeMux()
+
+ // TODO: This doesn't belong into serveMetrics
+ mux.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index))
+ mux.Handle("/debug/pprof/cmdline", http.HandlerFunc(pprof.Cmdline))
+ mux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile))
+ mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol))
+ mux.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace))
+
+ mux.Handle(metricsPath, promhttp.InstrumentHandlerDuration(durationObserver, m))
+
+ // Add healthzPath
+ mux.HandleFunc(healthzPath, func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(http.StatusText(http.StatusOK)))
+ })
+ // Add index
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ w.Write([]byte(`
+ Kube Metrics Server
+
+ Kube Metrics
+
+
+ `))
+ })
+ return mux
+}
diff --git a/main_test.go b/pkg/app/server_test.go
similarity index 77%
rename from main_test.go
rename to pkg/app/server_test.go
index fc21c38c0f..53cd0356dd 100644
--- a/main_test.go
+++ b/pkg/app/server_test.go
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-package main
+package app
import (
"bytes"
@@ -28,17 +28,24 @@ import (
"testing"
"time"
- generator "k8s.io/kube-state-metrics/v2/pkg/metric_generator"
-
"github.com/prometheus/client_golang/prometheus"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
+ "k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes/fake"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/cache"
+ samplev1alpha1 "k8s.io/sample-controller/pkg/apis/samplecontroller/v1alpha1"
+ samplefake "k8s.io/sample-controller/pkg/generated/clientset/versioned/fake"
"k8s.io/kube-state-metrics/v2/internal/store"
"k8s.io/kube-state-metrics/v2/pkg/allowdenylist"
+ "k8s.io/kube-state-metrics/v2/pkg/customresource"
+ "k8s.io/kube-state-metrics/v2/pkg/metric"
+ generator "k8s.io/kube-state-metrics/v2/pkg/metric_generator"
"k8s.io/kube-state-metrics/v2/pkg/metricshandler"
"k8s.io/kube-state-metrics/v2/pkg/options"
)
@@ -69,7 +76,7 @@ func BenchmarkKubeStateMetrics(b *testing.B) {
builder.WithSharding(0, 1)
builder.WithContext(ctx)
builder.WithNamespaces(options.DefaultNamespaces, "")
- builder.WithGenerateStoresFunc(builder.DefaultGenerateStoresFunc(), false)
+ builder.WithGenerateStoresFunc(builder.DefaultGenerateStoresFunc())
allowDenyListFilter, err := allowdenylist.New(map[string]struct{}{}, map[string]struct{}{})
if err != nil {
@@ -138,7 +145,7 @@ func TestFullScrapeCycle(t *testing.T) {
builder.WithEnabledResources(options.DefaultResources.AsSlice())
builder.WithKubeClient(kubeClient)
builder.WithNamespaces(options.DefaultNamespaces, "")
- builder.WithGenerateStoresFunc(builder.DefaultGenerateStoresFunc(), false)
+ builder.WithGenerateStoresFunc(builder.DefaultGenerateStoresFunc())
l, err := allowdenylist.New(map[string]struct{}{}, map[string]struct{}{})
if err != nil {
@@ -407,7 +414,7 @@ func TestShardingEquivalenceScrapeCycle(t *testing.T) {
unshardedBuilder.WithNamespaces(options.DefaultNamespaces, "")
unshardedBuilder.WithFamilyGeneratorFilter(l)
unshardedBuilder.WithAllowLabels(map[string][]string{})
- unshardedBuilder.WithGenerateStoresFunc(unshardedBuilder.DefaultGenerateStoresFunc(), false)
+ unshardedBuilder.WithGenerateStoresFunc(unshardedBuilder.DefaultGenerateStoresFunc())
unshardedHandler := metricshandler.New(&options.Options{}, kubeClient, unshardedBuilder, false)
unshardedHandler.ConfigureSharding(ctx, 0, 1)
@@ -420,7 +427,7 @@ func TestShardingEquivalenceScrapeCycle(t *testing.T) {
shardedBuilder1.WithNamespaces(options.DefaultNamespaces, "")
shardedBuilder1.WithFamilyGeneratorFilter(l)
shardedBuilder1.WithAllowLabels(map[string][]string{})
- shardedBuilder1.WithGenerateStoresFunc(shardedBuilder1.DefaultGenerateStoresFunc(), false)
+ shardedBuilder1.WithGenerateStoresFunc(shardedBuilder1.DefaultGenerateStoresFunc())
shardedHandler1 := metricshandler.New(&options.Options{}, kubeClient, shardedBuilder1, false)
shardedHandler1.ConfigureSharding(ctx, 0, 2)
@@ -433,7 +440,7 @@ func TestShardingEquivalenceScrapeCycle(t *testing.T) {
shardedBuilder2.WithNamespaces(options.DefaultNamespaces, "")
shardedBuilder2.WithFamilyGeneratorFilter(l)
shardedBuilder2.WithAllowLabels(map[string][]string{})
- shardedBuilder2.WithGenerateStoresFunc(shardedBuilder2.DefaultGenerateStoresFunc(), false)
+ shardedBuilder2.WithGenerateStoresFunc(shardedBuilder2.DefaultGenerateStoresFunc())
shardedHandler2 := metricshandler.New(&options.Options{}, kubeClient, shardedBuilder2, false)
shardedHandler2.ConfigureSharding(ctx, 1, 2)
@@ -541,6 +548,121 @@ func TestShardingEquivalenceScrapeCycle(t *testing.T) {
}
}
+// TestCustomResourceExtension is a simple smoke test covering the custom resource metrics collection.
+// We use custom resource object samplev1alpha1.Foo in kubernetes/sample-controller as an example.
+func TestCustomResourceExtension(t *testing.T) {
+ kubeClient := fake.NewSimpleClientset()
+ factories := []customresource.RegistryFactory{new(fooFactory)}
+ resources := options.DefaultResources.AsSlice()
+ customResourceClients := make(map[string]interface{}, len(factories))
+ // enable custom resource
+ for _, f := range factories {
+ resources = append(resources, f.Name())
+ customResourceClient, err := f.CreateClient(nil)
+ if err != nil {
+ t.Fatalf("Failed to create customResourceClient for foo: %v", err)
+ }
+ customResourceClients[f.Name()] = customResourceClient
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ reg := prometheus.NewRegistry()
+ builder := store.NewBuilder()
+ builder.WithCustomResourceStoreFactories(factories...)
+ builder.WithMetrics(reg)
+ builder.WithEnabledResources(resources)
+ builder.WithKubeClient(kubeClient)
+ builder.WithCustomResourceClients(customResourceClients)
+ builder.WithNamespaces(options.DefaultNamespaces, "")
+ builder.WithGenerateStoresFunc(builder.DefaultGenerateStoresFunc())
+ builder.WithGenerateCustomResourceStoresFunc(builder.DefaultGenerateCustomResourceStoresFunc())
+
+ l, err := allowdenylist.New(map[string]struct{}{}, map[string]struct{}{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ builder.WithFamilyGeneratorFilter(l)
+ builder.WithAllowLabels(map[string][]string{
+ "kube_foo_labels": {
+ "namespace",
+ "foo",
+ "uid",
+ },
+ })
+
+ handler := metricshandler.New(&options.Options{}, kubeClient, builder, false)
+ handler.ConfigureSharding(ctx, 0, 1)
+
+ // Wait for caches to fill
+ time.Sleep(time.Second)
+
+ req := httptest.NewRequest("GET", "http://localhost:8080/metrics", nil)
+
+ w := httptest.NewRecorder()
+ handler.ServeHTTP(w, req)
+
+ resp := w.Result()
+ if resp.StatusCode != 200 {
+ t.Fatalf("expected 200 status code but got %v", resp.StatusCode)
+ }
+
+ body, _ := io.ReadAll(resp.Body)
+
+ expected := `# HELP kube_foo_spec_replicas Number of desired replicas for a foo.
+# HELP kube_foo_status_replicas_available The number of available replicas per foo.
+# TYPE kube_foo_spec_replicas gauge
+# TYPE kube_foo_status_replicas_available gauge
+kube_foo_spec_replicas{namespace="default",foo="foo0"} 0
+kube_foo_spec_replicas{namespace="default",foo="foo1"} 1
+kube_foo_spec_replicas{namespace="default",foo="foo2"} 2
+kube_foo_spec_replicas{namespace="default",foo="foo3"} 3
+kube_foo_spec_replicas{namespace="default",foo="foo4"} 4
+kube_foo_spec_replicas{namespace="default",foo="foo5"} 5
+kube_foo_spec_replicas{namespace="default",foo="foo6"} 6
+kube_foo_spec_replicas{namespace="default",foo="foo7"} 7
+kube_foo_spec_replicas{namespace="default",foo="foo8"} 8
+kube_foo_spec_replicas{namespace="default",foo="foo9"} 9
+kube_foo_status_replicas_available{namespace="default",foo="foo0"} 0
+kube_foo_status_replicas_available{namespace="default",foo="foo1"} 1
+kube_foo_status_replicas_available{namespace="default",foo="foo2"} 2
+kube_foo_status_replicas_available{namespace="default",foo="foo3"} 3
+kube_foo_status_replicas_available{namespace="default",foo="foo5"} 5
+kube_foo_status_replicas_available{namespace="default",foo="foo6"} 6
+kube_foo_status_replicas_available{namespace="default",foo="foo7"} 7
+kube_foo_status_replicas_available{namespace="default",foo="foo8"} 8
+kube_foo_status_replicas_available{namespace="default",foo="foo4"} 4
+kube_foo_status_replicas_available{namespace="default",foo="foo9"} 9
+`
+
+ expectedSplit := strings.Split(strings.TrimSpace(expected), "\n")
+ sort.Strings(expectedSplit)
+
+ gotSplit := strings.Split(strings.TrimSpace(string(body)), "\n")
+
+ gotFiltered := []string{}
+ for _, l := range gotSplit {
+ if strings.Contains(l, "kube_foo_") {
+ gotFiltered = append(gotFiltered, l)
+ }
+ }
+
+ sort.Strings(gotFiltered)
+
+ if len(expectedSplit) != len(gotFiltered) {
+ fmt.Println(len(expectedSplit))
+ fmt.Println(len(gotFiltered))
+ t.Fatalf("expected different output length, expected \n\n%s\n\ngot\n\n%s", expected, strings.Join(gotFiltered, "\n"))
+ }
+
+ for i := 0; i < len(expectedSplit); i++ {
+ if expectedSplit[i] != gotFiltered[i] {
+ t.Fatalf("expected:\n\n%v\n, but got:\n\n%v", expectedSplit[i], gotFiltered[i])
+ }
+ }
+}
+
func injectFixtures(client *fake.Clientset, multiplier int) error {
creators := []func(*fake.Clientset, int) error{
configMap,
@@ -673,3 +795,116 @@ func pod(client *fake.Clientset, index int) error {
_, err := client.CoreV1().Pods(metav1.NamespaceDefault).Create(context.TODO(), &pod, metav1.CreateOptions{})
return err
}
+
+func foo(client *samplefake.Clientset, index int) error {
+ i := strconv.Itoa(index)
+ desiredReplicas := int32(index)
+
+ foo := samplev1alpha1.Foo{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "foo" + i,
+ CreationTimestamp: metav1.Time{Time: time.Unix(1500000000, 0)},
+ UID: types.UID("abc-" + i),
+ },
+ Spec: samplev1alpha1.FooSpec{
+ DeploymentName: "foo" + i,
+ Replicas: &desiredReplicas,
+ },
+ Status: samplev1alpha1.FooStatus{
+ AvailableReplicas: desiredReplicas,
+ },
+ }
+
+ _, err := client.SamplecontrollerV1alpha1().Foos(metav1.NamespaceDefault).Create(context.TODO(), &foo, metav1.CreateOptions{})
+ return err
+}
+
+var (
+ descFooLabelsDefaultLabels = []string{"namespace", "foo"}
+)
+
+type fooFactory struct{}
+
+func (f *fooFactory) Name() string {
+ return "foos"
+}
+
+// CreateClient use fake client set to establish 10 foos.
+func (f *fooFactory) CreateClient(cfg *rest.Config) (interface{}, error) {
+ fooClient := samplefake.NewSimpleClientset()
+ for i := 0; i < 10; i++ {
+ err := foo(fooClient, i)
+ if err != nil {
+ return nil, fmt.Errorf("failed to insert sample pod %v", err)
+ }
+ }
+ return fooClient, nil
+}
+
+func (f *fooFactory) MetricFamilyGenerators(allowAnnotationsList, allowLabelsList []string) []generator.FamilyGenerator {
+ return []generator.FamilyGenerator{
+ *generator.NewFamilyGenerator(
+ "kube_foo_spec_replicas",
+ "Number of desired replicas for a foo.",
+ metric.Gauge,
+ "",
+ wrapFooFunc(func(f *samplev1alpha1.Foo) *metric.Family {
+ return &metric.Family{
+ Metrics: []*metric.Metric{
+ {
+ Value: float64(*f.Spec.Replicas),
+ },
+ },
+ }
+ }),
+ ),
+ *generator.NewFamilyGenerator(
+ "kube_foo_status_replicas_available",
+ "The number of available replicas per foo.",
+ metric.Gauge,
+ "",
+ wrapFooFunc(func(f *samplev1alpha1.Foo) *metric.Family {
+ return &metric.Family{
+ Metrics: []*metric.Metric{
+ {
+ Value: float64(f.Status.AvailableReplicas),
+ },
+ },
+ }
+ }),
+ ),
+ }
+}
+
+func wrapFooFunc(f func(*samplev1alpha1.Foo) *metric.Family) func(interface{}) *metric.Family {
+ return func(obj interface{}) *metric.Family {
+ foo := obj.(*samplev1alpha1.Foo)
+
+ metricFamily := f(foo)
+
+ for _, m := range metricFamily.Metrics {
+ m.LabelKeys = append(descFooLabelsDefaultLabels, m.LabelKeys...)
+ m.LabelValues = append([]string{foo.Namespace, foo.Name}, m.LabelValues...)
+ }
+
+ return metricFamily
+ }
+}
+
+func (f *fooFactory) ExpectedType() interface{} {
+ return &samplev1alpha1.Foo{}
+}
+
+func (f *fooFactory) ListWatch(customResourceClient interface{}, ns string, fieldSelector string) cache.ListerWatcher {
+ client := customResourceClient.(*samplefake.Clientset)
+ return &cache.ListWatch{
+ ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) {
+ opts.FieldSelector = fieldSelector
+ return client.SamplecontrollerV1alpha1().Foos(ns).List(context.Background(), opts)
+ },
+ WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) {
+ opts.FieldSelector = fieldSelector
+ return client.SamplecontrollerV1alpha1().Foos(ns).Watch(context.Background(), opts)
+ },
+ }
+}
diff --git a/pkg/builder/builder.go b/pkg/builder/builder.go
index cd28bf1a0a..1e2763c455 100644
--- a/pkg/builder/builder.go
+++ b/pkg/builder/builder.go
@@ -28,6 +28,7 @@ import (
internalstore "k8s.io/kube-state-metrics/v2/internal/store"
ksmtypes "k8s.io/kube-state-metrics/v2/pkg/builder/types"
+ "k8s.io/kube-state-metrics/v2/pkg/customresource"
metricsstore "k8s.io/kube-state-metrics/v2/pkg/metrics_store"
"k8s.io/kube-state-metrics/v2/pkg/options"
)
@@ -81,12 +82,27 @@ func (b *Builder) WithVPAClient(c vpaclientset.Interface) {
b.internal.WithVPAClient(c)
}
+// WithCustomResourceClients sets the customResourceClients property of a Builder.
+func (b *Builder) WithCustomResourceClients(cs map[string]interface{}) {
+ b.internal.WithCustomResourceClients(cs)
+}
+
+// WithUsingAPIServerCache configures whether using APIServer cache or not.
+func (b *Builder) WithUsingAPIServerCache(u bool) {
+ b.internal.WithUsingAPIServerCache(u)
+}
+
// WithFamilyGeneratorFilter configures the family generator filter which decides which
// metrics are to be exposed by the store build by the Builder.
func (b *Builder) WithFamilyGeneratorFilter(l generator.FamilyGeneratorFilter) {
b.internal.WithFamilyGeneratorFilter(l)
}
+// WithAllowAnnotations configures which annotations can be returned for metrics
+func (b *Builder) WithAllowAnnotations(annotations map[string][]string) {
+ b.internal.WithAllowAnnotations(annotations)
+}
+
// WithAllowLabels configures which labels can be returned for metrics
func (b *Builder) WithAllowLabels(l map[string][]string) {
b.internal.WithAllowLabels(l)
@@ -94,7 +110,12 @@ func (b *Builder) WithAllowLabels(l map[string][]string) {
// WithGenerateStoresFunc configures a custom generate store function
func (b *Builder) WithGenerateStoresFunc(f ksmtypes.BuildStoresFunc) {
- b.internal.WithGenerateStoresFunc(f, false)
+ b.internal.WithGenerateStoresFunc(f)
+}
+
+// WithGenerateCustomResourceStoresFunc configures a custom generate custom resource store function
+func (b *Builder) WithGenerateCustomResourceStoresFunc(f ksmtypes.BuildCustomResourceStoresFunc) {
+ b.internal.WithGenerateCustomResourceStoresFunc(f)
}
// DefaultGenerateStoresFunc returns default buildStore function
@@ -102,6 +123,16 @@ func (b *Builder) DefaultGenerateStoresFunc() ksmtypes.BuildStoresFunc {
return b.internal.DefaultGenerateStoresFunc()
}
+// DefaultGenerateCustomResourceStoresFunc returns default buildStores function
+func (b *Builder) DefaultGenerateCustomResourceStoresFunc() ksmtypes.BuildCustomResourceStoresFunc {
+ return b.internal.DefaultGenerateCustomResourceStoresFunc()
+}
+
+// WithCustomResourceStoreFactories returns configures a custom resource stores factory
+func (b *Builder) WithCustomResourceStoreFactories(fs ...customresource.RegistryFactory) {
+ b.internal.WithCustomResourceStoreFactories(fs...)
+}
+
// Build initializes and registers all enabled stores.
// Returns metric writers.
func (b *Builder) Build() []metricsstore.MetricsWriter {
diff --git a/pkg/builder/types/interfaces.go b/pkg/builder/types/interfaces.go
index 15995435ad..21b657a24f 100644
--- a/pkg/builder/types/interfaces.go
+++ b/pkg/builder/types/interfaces.go
@@ -26,6 +26,7 @@ import (
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
+ "k8s.io/kube-state-metrics/v2/pkg/customresource"
generator "k8s.io/kube-state-metrics/v2/pkg/metric_generator"
"k8s.io/kube-state-metrics/v2/pkg/options"
)
@@ -39,10 +40,16 @@ type BuilderInterface interface {
WithContext(ctx context.Context)
WithKubeClient(c clientset.Interface)
WithVPAClient(c vpaclientset.Interface)
+ WithCustomResourceClients(cs map[string]interface{})
+ WithUsingAPIServerCache(u bool)
WithFamilyGeneratorFilter(l generator.FamilyGeneratorFilter)
+ WithAllowAnnotations(a map[string][]string)
WithAllowLabels(l map[string][]string)
- WithGenerateStoresFunc(f BuildStoresFunc, useAPIServerCache bool)
+ WithGenerateStoresFunc(f BuildStoresFunc)
+ WithGenerateCustomResourceStoresFunc(f BuildCustomResourceStoresFunc)
DefaultGenerateStoresFunc() BuildStoresFunc
+ DefaultGenerateCustomResourceStoresFunc() BuildCustomResourceStoresFunc
+ WithCustomResourceStoreFactories(fs ...customresource.RegistryFactory)
Build() []metricsstore.MetricsWriter
BuildStores() [][]cache.Store
}
@@ -54,6 +61,14 @@ type BuildStoresFunc func(metricFamilies []generator.FamilyGenerator,
useAPIServerCache bool,
) []cache.Store
+// BuildCustomResourceStoresFunc function signature that is used to return a list of custom resource cache.Store
+type BuildCustomResourceStoresFunc func(resourceName string,
+ metricFamilies []generator.FamilyGenerator,
+ expectedType interface{},
+ listWatchFunc func(customResourceClient interface{}, ns string, fieldSelector string) cache.ListerWatcher,
+ useAPIServerCache bool,
+) []cache.Store
+
// AllowDenyLister interface for AllowDeny lister that can allow or exclude metrics by there names
type AllowDenyLister interface {
IsIncluded(string) bool
diff --git a/pkg/customresource/registry_factory.go b/pkg/customresource/registry_factory.go
new file mode 100644
index 0000000000..e4ccee9b1e
--- /dev/null
+++ b/pkg/customresource/registry_factory.go
@@ -0,0 +1,116 @@
+/*
+Copyright 2021 The Kubernetes Authors All rights reserved.
+
+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 customresource
+
+import (
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/cache"
+
+ generator "k8s.io/kube-state-metrics/v2/pkg/metric_generator"
+)
+
+// RegistryFactory is a registry interface for a CustomResourceStore.
+// Users who want to extend the kube-state-metrics to support Custom Resource metrics should
+// implement this interface.
+type RegistryFactory interface {
+ // Name returns the name of custom resource.
+ //
+ // Example:
+ //
+ // func (f *FooFactory) Name() string {
+ // return "foos"
+ // }
+ Name() string
+
+ // CreateClient creates a new custom resource client for the given config.
+ //
+ // Example:
+ //
+ // func (f *FooFactory) CreateClient(cfg *rest.Config) (interface{}, error) {
+ // return clientset.NewForConfig(cfg)
+ // }
+ CreateClient(cfg *rest.Config) (interface{}, error)
+
+ // MetricFamilyGenerators returns the metric family generators to generate metric families with a
+ // Kubernetes custom resource object.
+ //
+ // Example:
+ //
+ // func (f *FooFactory) MetricFamilyGenerators(allowAnnotationsList, allowLabelsList []string) []generator.FamilyGenerator {
+ // return []generator.FamilyGenerator{
+ // *generator.NewFamilyGenerator(
+ // "kube_foo_spec_replicas",
+ // "Number of desired replicas for a foo.",
+ // metric.Gauge,
+ // "",
+ // wrapFooFunc(func(f *samplev1alpha1.Foo) *metric.Family {
+ // return &metric.Family{
+ // Metrics: []*metric.Metric{
+ // {
+ // Value: float64(*f.Spec.Replicas),
+ // },
+ // },
+ // }
+ // }),
+ // ),
+ // *generator.NewFamilyGenerator(
+ // "kube_foo_status_replicas_available",
+ // "The number of available replicas per foo.",
+ // metric.Gauge,
+ // "",
+ // wrapFooFunc(func(f *samplev1alpha1.Foo) *metric.Family {
+ // return &metric.Family{
+ // Metrics: []*metric.Metric{
+ // {
+ // Value: float64(f.Status.AvailableReplicas),
+ // },
+ // },
+ // }
+ // }),
+ // ),
+ // }
+ // }
+ MetricFamilyGenerators(allowAnnotationsList, allowLabelsList []string) []generator.FamilyGenerator
+
+ // ExpectedType returns a pointer to an empty custom resource object.
+ //
+ // Example:
+ //
+ // func (f *FooFactory) ExpectedType() interface{} {
+ // return &samplev1alpha1.Foo{}
+ // }
+ ExpectedType() interface{}
+
+ // ListWatch constructs a cache.ListerWatcher of the custom resource object.
+ //
+ // Example:
+ //
+ // func (f *FooFactory) ListWatch(customResourceClient interface{}, ns string, fieldSelector string) cache.ListerWatcher {
+ // client := customResourceClient.(*clientset.Clientset)
+ // return &cache.ListWatch{
+ // ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) {
+ // opts.FieldSelector = fieldSelector
+ // return client.SamplecontrollerV1alpha1().Foos(ns).List(context.Background(), opts)
+ // },
+ // WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) {
+ // opts.FieldSelector = fieldSelector
+ // return client.SamplecontrollerV1alpha1().Foos(ns).Watch(context.Background(), opts)
+ // },
+ // }
+ // }
+ ListWatch(customResourceClient interface{}, ns string, fieldSelector string) cache.ListerWatcher
+}