diff --git a/docs/README.md b/docs/README.md index 6e5e48e074..a6f6840f30 100644 --- a/docs/README.md +++ b/docs/README.md @@ -65,6 +65,7 @@ See each file for specific documentation about the exposed metrics: - [ClusterRole Metrics](clusterrole-metrics.md) - [ClusterRoleBinding Metrics](clusterrolebinding-metrics.md) +- [EndpointSlice Metrics](endpointslice-metrics.md) - [IngressClass Metrics](ingressclass-metrics.md) - [Role Metrics](role-metrics.md) - [RoleBinding Metrics](rolebinding-metrics.md) diff --git a/docs/endpointslice-metrics.md b/docs/endpointslice-metrics.md new file mode 100644 index 0000000000..f2327a5265 --- /dev/null +++ b/docs/endpointslice-metrics.md @@ -0,0 +1,10 @@ +# Endpoint Metrics + +| Metric name| Metric type | Labels/tags | Status | +| ---------- | ----------- | ----------- | ----------- | +| kube_endpointslice_annotations | Gauge | `endpointslice`=<endpointslice-name>
`namespace`=<endpointslice-namespace>
`annotation_ENDPOINTSLICE_ANNOTATION`=<ENDPOINTSLICE_ANNOTATION> | EXPERIMENTAL | +| kube_endpointslice_info | Gauge | `endpointslice`=<endpointslice-name>
`namespace`=<endpointslice-namespace> | EXPERIMENTAL | +| kube_endpointslice_ports | Gauge | `endpointslice`=<endpointslice-name>
`namespace`=<endpointslice-namespace>
`port_name`=<endpointslice-port-name>
`port_protocol`=<endpointslice-port-protocol>
`port_number`=<endpointslice-port-number> | EXPERIMENTAL | +| kube_endpointslice_endpoints | Gauge | `endpointslice`=<endpointslice-name>
`namespace`=<endpointslice-namespace>
`ready`=<endpointslice-ready>
`serving`=<endpointslice-serving>
`terminating`=<endpointslice-terminating>
`hostname`=<endpointslice-hostname>
`targetref_kind`=<endpointslice-targetref-kind>
`targetref_name`=<endpointslice-targetref-name>
`targetref_namespace`=<endpointslice-targetref-namespace>
`nodename`=<endpointslice-nodename>
`endpoint_zone`=<endpointslice-zone> | EXPERIMENTAL | +| kube_endpointslice_labels | Gauge | `endpointslice`=<endpointslice-name>
`namespace`=<endpointslice-namespace>
`label_ENDPOINTSLICE_LABEL`=<ENDPOINTSLICE_LABEL> | EXPERIMENTAL | +| kube_endpointslice_created | Gauge | `endpointslice`=<endpointslice-name>
`namespace`=<endpointslice-namespace> | EXPERIMENTAL | diff --git a/examples/autosharding/cluster-role.yaml b/examples/autosharding/cluster-role.yaml index ac5436f7f1..e20db08235 100644 --- a/examples/autosharding/cluster-role.yaml +++ b/examples/autosharding/cluster-role.yaml @@ -77,6 +77,13 @@ rules: verbs: - list - watch +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - list + - watch - apiGroups: - storage.k8s.io resources: diff --git a/examples/standard/cluster-role.yaml b/examples/standard/cluster-role.yaml index ac5436f7f1..e20db08235 100644 --- a/examples/standard/cluster-role.yaml +++ b/examples/standard/cluster-role.yaml @@ -77,6 +77,13 @@ rules: verbs: - list - watch +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - list + - watch - apiGroups: - storage.k8s.io resources: diff --git a/internal/store/builder.go b/internal/store/builder.go index df00ed496d..9f58933e6f 100644 --- a/internal/store/builder.go +++ b/internal/store/builder.go @@ -32,6 +32,7 @@ import ( certv1 "k8s.io/api/certificates/v1" coordinationv1 "k8s.io/api/coordination/v1" v1 "k8s.io/api/core/v1" + discoveryv1 "k8s.io/api/discovery/v1" networkingv1 "k8s.io/api/networking/v1" policyv1 "k8s.io/api/policy/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -293,6 +294,7 @@ var availableStores = map[string]func(f *Builder) []cache.Store{ "daemonsets": func(b *Builder) []cache.Store { return b.buildDaemonSetStores() }, "deployments": func(b *Builder) []cache.Store { return b.buildDeploymentStores() }, "endpoints": func(b *Builder) []cache.Store { return b.buildEndpointsStores() }, + "endpointslices": func(b *Builder) []cache.Store { return b.buildEndpointSlicesStores() }, "horizontalpodautoscalers": func(b *Builder) []cache.Store { return b.buildHPAStores() }, "ingresses": func(b *Builder) []cache.Store { return b.buildIngressStores() }, "ingressclasses": func(b *Builder) []cache.Store { return b.buildIngressClassStores() }, @@ -355,6 +357,10 @@ func (b *Builder) buildEndpointsStores() []cache.Store { return b.buildStoresFunc(endpointMetricFamilies(b.allowAnnotationsList["endpoints"], b.allowLabelsList["endpoints"]), &v1.Endpoints{}, createEndpointsListWatch, b.useAPIServerCache) } +func (b *Builder) buildEndpointSlicesStores() []cache.Store { + return b.buildStoresFunc(endpointSliceMetricFamilies(b.allowAnnotationsList["endpointslices"], b.allowLabelsList["endpointslices"]), &discoveryv1.EndpointSlice{}, createEndpointSliceListWatch, b.useAPIServerCache) +} + func (b *Builder) buildHPAStores() []cache.Store { return b.buildStoresFunc(hpaMetricFamilies(b.allowAnnotationsList["horizontalpodautoscalers"], b.allowLabelsList["horizontalpodautoscalers"]), &autoscaling.HorizontalPodAutoscaler{}, createHPAListWatch, b.useAPIServerCache) } diff --git a/internal/store/endpointslice.go b/internal/store/endpointslice.go new file mode 100644 index 0000000000..0b01284468 --- /dev/null +++ b/internal/store/endpointslice.go @@ -0,0 +1,234 @@ +/* +Copyright 2022 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 store + +import ( + "context" + "strconv" + + basemetrics "k8s.io/component-base/metrics" + + "k8s.io/kube-state-metrics/v2/pkg/metric" + generator "k8s.io/kube-state-metrics/v2/pkg/metric_generator" + + discoveryv1 "k8s.io/api/discovery/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/cache" +) + +var ( + descEndpointSliceAnnotationsName = "kube_endpointslice_annotations" + descEndpointSliceAnnotationsHelp = "Kubernetes annotations converted to Prometheus labels." + descEndpointSliceLabelsName = "kube_endpointslice_labels" + descEndpointSliceLabelsHelp = "Kubernetes labels converted to Prometheus labels." + descEndpointSliceLabelsDefaultLabels = []string{"endpointslice"} +) + +func endpointSliceMetricFamilies(allowAnnotationsList, allowLabelsList []string) []generator.FamilyGenerator { + return []generator.FamilyGenerator{ + *generator.NewFamilyGeneratorWithStability( + "kube_endpointslice_info", + "Information about endpointslice.", + metric.Gauge, + basemetrics.ALPHA, + "", + wrapEndpointSliceFunc(func(s *discoveryv1.EndpointSlice) *metric.Family { + + m := metric.Metric{ + LabelKeys: []string{"addresstype"}, + LabelValues: []string{string(s.AddressType)}, + Value: 1, + } + return &metric.Family{Metrics: []*metric.Metric{&m}} + }), + ), + *generator.NewFamilyGeneratorWithStability( + "kube_endpointslice_created", + "Unix creation timestamp", + metric.Gauge, + basemetrics.ALPHA, + "", + wrapEndpointSliceFunc(func(s *discoveryv1.EndpointSlice) *metric.Family { + ms := []*metric.Metric{} + if !s.CreationTimestamp.IsZero() { + ms = append(ms, &metric.Metric{ + Value: float64(s.CreationTimestamp.Unix()), + }) + } + return &metric.Family{ + Metrics: ms, + } + }), + ), + *generator.NewFamilyGeneratorWithStability( + "kube_endpointslice_endpoints", + "Endpoints attached to the endpointslice.", + metric.Gauge, + basemetrics.ALPHA, + "", + wrapEndpointSliceFunc(func(e *discoveryv1.EndpointSlice) *metric.Family { + m := []*metric.Metric{} + for _, ep := range e.Endpoints { + var ( + labelKeys, + labelValues []string + ) + + if ep.Conditions.Ready != nil { + labelKeys = append(labelKeys, "ready") + labelValues = append(labelValues, strconv.FormatBool(*ep.Conditions.Ready)) + } + if ep.Conditions.Serving != nil { + labelKeys = append(labelKeys, "serving") + labelValues = append(labelValues, strconv.FormatBool(*ep.Conditions.Serving)) + } + if ep.Conditions.Terminating != nil { + labelKeys = append(labelKeys, "terminating") + labelValues = append(labelValues, strconv.FormatBool(*ep.Conditions.Terminating)) + } + + if ep.Hostname != nil { + labelKeys = append(labelKeys, "hostname") + labelValues = append(labelValues, *ep.Hostname) + } + + if ep.TargetRef != nil { + if ep.TargetRef.Kind != "" { + labelKeys = append(labelKeys, "targetref_kind") + labelValues = append(labelValues, ep.TargetRef.Kind) + } + if ep.TargetRef.Name != "" { + labelKeys = append(labelKeys, "targetref_name") + labelValues = append(labelValues, ep.TargetRef.Name) + } + if ep.TargetRef.Namespace != "" { + labelKeys = append(labelKeys, "targetref_namespace") + labelValues = append(labelValues, ep.TargetRef.Namespace) + } + } + + if ep.NodeName != nil { + labelKeys = append(labelKeys, "endpoint_nodename") + labelValues = append(labelValues, *ep.NodeName) + } + + if ep.Zone != nil { + labelKeys = append(labelKeys, "endpoint_zone") + labelValues = append(labelValues, *ep.Zone) + } + labelKeys = append(labelKeys, "address") + for _, address := range ep.Addresses { + newlabelValues := make([]string, len(labelValues)) + copy(newlabelValues, labelValues) + m = append(m, &metric.Metric{ + LabelKeys: labelKeys, + LabelValues: append(newlabelValues, address), + Value: 1, + }) + } + } + return &metric.Family{ + Metrics: m, + } + }), + ), + + *generator.NewFamilyGeneratorWithStability( + "kube_endpointslice_ports", + "Ports attached to the endpointslice.", + metric.Gauge, + basemetrics.ALPHA, + "", + wrapEndpointSliceFunc(func(e *discoveryv1.EndpointSlice) *metric.Family { + m := []*metric.Metric{} + for _, port := range e.Ports { + m = append(m, &metric.Metric{ + LabelValues: []string{*port.Name, string(*port.Protocol), strconv.FormatInt(int64(*port.Port), 10)}, + LabelKeys: []string{"port_name", "port_protocol", "port_number"}, + Value: 1, + }) + } + return &metric.Family{ + Metrics: m, + } + }), + ), + *generator.NewFamilyGeneratorWithStability( + descEndpointSliceAnnotationsName, + descEndpointSliceAnnotationsHelp, + metric.Gauge, + basemetrics.ALPHA, + "", + wrapEndpointSliceFunc(func(s *discoveryv1.EndpointSlice) *metric.Family { + annotationKeys, annotationValues := createPrometheusLabelKeysValues("annotation", s.Annotations, allowAnnotationsList) + return &metric.Family{ + Metrics: []*metric.Metric{ + { + LabelKeys: annotationKeys, + LabelValues: annotationValues, + Value: 1, + }, + }, + } + }), + ), + *generator.NewFamilyGeneratorWithStability( + descEndpointSliceLabelsName, + descEndpointSliceLabelsHelp, + metric.Gauge, + basemetrics.ALPHA, + "", + wrapEndpointSliceFunc(func(s *discoveryv1.EndpointSlice) *metric.Family { + labelKeys, labelValues := createPrometheusLabelKeysValues("label", s.Labels, allowLabelsList) + return &metric.Family{ + Metrics: []*metric.Metric{ + { + LabelKeys: labelKeys, + LabelValues: labelValues, + Value: 1, + }, + }, + } + }), + ), + } +} + +func wrapEndpointSliceFunc(f func(*discoveryv1.EndpointSlice) *metric.Family) func(interface{}) *metric.Family { + return func(obj interface{}) *metric.Family { + endpointSlice := obj.(*discoveryv1.EndpointSlice) + + metricFamily := f(endpointSlice) + + for _, m := range metricFamily.Metrics { + m.LabelKeys, m.LabelValues = mergeKeyValues(descEndpointSliceLabelsDefaultLabels, []string{endpointSlice.Name}, m.LabelKeys, m.LabelValues) + } + + return metricFamily + } +} + +func createEndpointSliceListWatch(kubeClient clientset.Interface, ns string, fieldSelector string) cache.ListerWatcher { + return &cache.ListWatch{ + ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) { + return kubeClient.DiscoveryV1().EndpointSlices(ns).List(context.TODO(), opts) + }, + WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) { + return kubeClient.DiscoveryV1().EndpointSlices(ns).Watch(context.TODO(), opts) + }, + } +} diff --git a/internal/store/endpointslice_test.go b/internal/store/endpointslice_test.go new file mode 100644 index 0000000000..cca8638cd9 --- /dev/null +++ b/internal/store/endpointslice_test.go @@ -0,0 +1,161 @@ +/* +Copyright 2022 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 store + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + discoveryv1 "k8s.io/api/discovery/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + generator "k8s.io/kube-state-metrics/v2/pkg/metric_generator" +) + +func TestEndpointSliceStore(t *testing.T) { + startTime := 1501569018 + metav1StartTime := metav1.Unix(int64(startTime), 0) + portname := "http" + portnumber := int32(80) + portprotocol := corev1.Protocol("TCP") + nodename := "node" + hostname := "host" + zone := "west" + ready := true + terminating := false + addresses := []string{"10.0.0.1", "192.168.1.10"} + + cases := []generateMetricsTestCase{ + { + Obj: &discoveryv1.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test_endpointslice-info", + }, + AddressType: "IPv4", + }, + Want: ` + # HELP kube_endpointslice_info Information about endpointslice. + # TYPE kube_endpointslice_info gauge + kube_endpointslice_info{endpointslice="test_endpointslice-info",addresstype="IPv4"} 1 + `, + MetricNames: []string{ + "kube_endpointslice_info", + }, + }, + { + Obj: &discoveryv1.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test_kube_endpointslice-created", + CreationTimestamp: metav1StartTime, + }, + AddressType: "IPv4", + }, + Want: ` + # HELP kube_endpointslice_created Unix creation timestamp + # TYPE kube_endpointslice_created gauge + kube_endpointslice_created{endpointslice="test_kube_endpointslice-created"} 1.501569018e+09 + `, + MetricNames: []string{ + "kube_endpointslice_created", + }, + }, + { + Obj: &discoveryv1.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test_endpointslice-ports", + }, + AddressType: "IPv4", + Ports: []discoveryv1.EndpointPort{ + {Name: &portname, + Port: &portnumber, + Protocol: &portprotocol, + }, + }, + }, + Want: ` + # HELP kube_endpointslice_ports Ports attached to the endpointslice. + # TYPE kube_endpointslice_ports gauge + kube_endpointslice_ports{endpointslice="test_endpointslice-ports",port_name="http",port_protocol="TCP",port_number="80"} 1 + `, + MetricNames: []string{ + "kube_endpointslice_ports", + }, + }, + { + Obj: &discoveryv1.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test_endpointslice-endpoints", + }, + AddressType: "IPv4", + Endpoints: []discoveryv1.Endpoint{ + { + NodeName: &nodename, + Conditions: discoveryv1.EndpointConditions{ + Ready: &ready, + Terminating: &terminating, + }, + Hostname: &hostname, + Zone: &zone, + Addresses: addresses, + }, + }, + }, + Want: ` + # HELP kube_endpointslice_endpoints Endpoints attached to the endpointslice. + # TYPE kube_endpointslice_endpoints gauge + kube_endpointslice_endpoints{address="10.0.0.1",endpoint_nodename="node",endpoint_zone="west",endpointslice="test_endpointslice-endpoints",hostname="host",ready="true",terminating="false"} 1 + kube_endpointslice_endpoints{address="192.168.1.10",endpoint_nodename="node",endpoint_zone="west",endpointslice="test_endpointslice-endpoints",hostname="host",ready="true",terminating="false"} 1 + `, + + MetricNames: []string{ + "kube_endpointslice_endpoints", + }, + }, + { + AllowAnnotationsList: []string{ + "foo", + }, + Obj: &discoveryv1.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test_endpointslice-labels", + Annotations: map[string]string{ + "foo": "baz", + }, + Labels: map[string]string{ + "foo": "bar", + }, + }, + AddressType: "IPv4", + }, + Want: ` + # HELP kube_endpointslice_annotations Kubernetes annotations converted to Prometheus labels. + # HELP kube_endpointslice_labels Kubernetes labels converted to Prometheus labels. + # TYPE kube_endpointslice_annotations gauge + # TYPE kube_endpointslice_labels gauge + kube_endpointslice_annotations{endpointslice="test_endpointslice-labels",annotation_foo="baz"} 1 + kube_endpointslice_labels{endpointslice="test_endpointslice-labels"} 1 + `, + MetricNames: []string{ + "kube_endpointslice_annotations", "kube_endpointslice_labels", + }, + }, + } + for i, c := range cases { + c.Func = generator.ComposeMetricGenFuncs(endpointSliceMetricFamilies(c.AllowAnnotationsList, nil)) + c.Headers = generator.ExtractMetricFamilyHeaders(endpointSliceMetricFamilies(c.AllowAnnotationsList, nil)) + if err := c.run(); err != nil { + t.Errorf("unexpected collecting result in %vth run:\n%s", i, err) + } + } +} diff --git a/jsonnet/kube-state-metrics/kube-state-metrics.libsonnet b/jsonnet/kube-state-metrics/kube-state-metrics.libsonnet index 2275c7b584..d562d02c3b 100644 --- a/jsonnet/kube-state-metrics/kube-state-metrics.libsonnet +++ b/jsonnet/kube-state-metrics/kube-state-metrics.libsonnet @@ -114,6 +114,13 @@ ], verbs: ['list', 'watch'], }, + { + apiGroups: ['discovery.k8s.io'], + resources: [ + 'endpointslices', + ], + verbs: ['list', 'watch'], + }, { apiGroups: ['storage.k8s.io'], resources: [ diff --git a/tests/e2e/main_test.go b/tests/e2e/main_test.go index 4f9d08e86e..127fc4e52e 100644 --- a/tests/e2e/main_test.go +++ b/tests/e2e/main_test.go @@ -256,6 +256,7 @@ func TestDefaultCollectorMetricsAvailable(t *testing.T) { nonDefaultResources := map[string]bool{ "clusterrole": true, "clusterrolebinding": true, + "endpointslices": true, "ingressclass": true, "role": true, "rolebinding": true, diff --git a/tests/manifests/endpointslice.yaml b/tests/manifests/endpointslice.yaml new file mode 100644 index 0000000000..078e67f2bb --- /dev/null +++ b/tests/manifests/endpointslice.yaml @@ -0,0 +1,13 @@ +apiVersion: discovery.k8s.io/v1 +kind: EndpointSlice +metadata: + name: example-endpointslice + namespace: default +addressType: IPv4 +endpoints: +- addresses: + - 10.0.0.2 + conditions: + ready: true + serving: true + terminating: false