diff --git a/docs/README.md b/docs/README.md
index a2903a32d3..132cf4cabf 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -54,6 +54,7 @@ Per group of metrics there is one file for each metrics. See each file for speci
- [Horizontal Pod Autoscaler Metrics](horizontalpodautoscaler-metrics.md)
- [Ingress Metrics](ingress-metrics.md)
- [Job Metrics](job-metrics.md)
+- [Lease Metrics](lease-metrics.md)
- [LimitRange Metrics](limitrange-metrics.md)
- [MutatingWebhookConfiguration Metrics](mutatingwebhookconfiguration.md)
- [Namespace Metrics](namespace-metrics.md)
diff --git a/docs/cli-arguments.md b/docs/cli-arguments.md
index eb8f95d91f..2a22807b60 100644
--- a/docs/cli-arguments.md
+++ b/docs/cli-arguments.md
@@ -28,7 +28,7 @@ Usage of ./kube-state-metrics:
--add_dir_header If true, adds the file directory to the header
--alsologtostderr log to standard error as well as files
--apiserver string The URL of the apiserver to use as a master
- --collectors string Comma-separated list of collectors to be enabled. Defaults to "certificatesigningrequests,configmaps,cronjobs,daemonsets,deployments,endpoints,horizontalpodautoscalers,ingresses,jobs,limitranges,mutatingwebhookconfigurations,namespaces,networkpolicies,nodes,persistentvolumeclaims,persistentvolumes,poddisruptionbudgets,pods,replicasets,replicationcontrollers,resourcequotas,secrets,services,statefulsets,storageclasses,validatingwebhookconfigurations,volumeattachments"
+ --collectors string Comma-separated list of collectors to be enabled. Defaults to "certificatesigningrequests,configmaps,cronjobs,daemonsets,deployments,endpoints,horizontalpodautoscalers,ingresses,jobs,leases,limitranges,mutatingwebhookconfigurations,namespaces,networkpolicies,nodes,persistentvolumeclaims,persistentvolumes,poddisruptionbudgets,pods,replicasets,replicationcontrollers,resourcequotas,secrets,services,statefulsets,storageclasses,validatingwebhookconfigurations,volumeattachments"
--disable-node-non-generic-resource-metrics Disable node non generic resource request and limit metrics
--disable-pod-non-generic-resource-metrics Disable pod non generic resource request and limit metrics
--enable-gzip-encoding Gzip responses when requested by clients via 'Accept-Encoding: gzip' header.
diff --git a/docs/lease-metrics.md b/docs/lease-metrics.md
new file mode 100644
index 0000000000..8d995116a3
--- /dev/null
+++ b/docs/lease-metrics.md
@@ -0,0 +1,6 @@
+# Lease Metrics
+
+| Metric name| Metric type | Labels/tags | Status |
+| ---------- | ----------- | ----------- | ----------- |
+| kube_lease_owner | Gauge | `lease`=<lease-name>
`owner_kind`=<onwer kind>
`owner_name`=<owner name> | EXPERIMENTAL |
+| kube_lease_renew_time | Gauge | `lease`=<lease-name> | EXPERIMENTAL |
diff --git a/examples/autosharding/cluster-role.yaml b/examples/autosharding/cluster-role.yaml
index cfbd243694..4da4a33e74 100644
--- a/examples/autosharding/cluster-role.yaml
+++ b/examples/autosharding/cluster-role.yaml
@@ -108,3 +108,10 @@ rules:
verbs:
- list
- watch
+- apiGroups:
+ - coordination.k8s.io
+ resources:
+ - leases
+ verbs:
+ - list
+ - watch
diff --git a/examples/standard/cluster-role.yaml b/examples/standard/cluster-role.yaml
index cfbd243694..4da4a33e74 100644
--- a/examples/standard/cluster-role.yaml
+++ b/examples/standard/cluster-role.yaml
@@ -108,3 +108,10 @@ rules:
verbs:
- list
- watch
+- apiGroups:
+ - coordination.k8s.io
+ resources:
+ - leases
+ verbs:
+ - list
+ - watch
diff --git a/internal/store/builder.go b/internal/store/builder.go
index a98cfceb3d..49b76e6a81 100644
--- a/internal/store/builder.go
+++ b/internal/store/builder.go
@@ -30,6 +30,7 @@ import (
batchv1 "k8s.io/api/batch/v1"
batchv1beta1 "k8s.io/api/batch/v1beta1"
certv1beta1 "k8s.io/api/certificates/v1beta1"
+ coordinationv1 "k8s.io/api/coordination/v1"
v1 "k8s.io/api/core/v1"
extensions "k8s.io/api/extensions/v1beta1"
policy "k8s.io/api/policy/v1beta1"
@@ -168,6 +169,7 @@ var availableStores = map[string]func(f *Builder) cache.Store{
"horizontalpodautoscalers": func(b *Builder) cache.Store { return b.buildHPAStore() },
"ingresses": func(b *Builder) cache.Store { return b.buildIngressStore() },
"jobs": func(b *Builder) cache.Store { return b.buildJobStore() },
+ "leases": func(b *Builder) cache.Store { return b.buildLeases() },
"limitranges": func(b *Builder) cache.Store { return b.buildLimitRangeStore() },
"mutatingwebhookconfigurations": func(b *Builder) cache.Store { return b.buildMutatingWebhookConfigurationStore() },
"namespaces": func(b *Builder) cache.Store { return b.buildNamespaceStore() },
@@ -314,6 +316,10 @@ func (b *Builder) buildVPAStore() cache.Store {
return b.buildStore(vpaMetricFamilies, &vpaautoscaling.VerticalPodAutoscaler{}, createVPAListWatchFunc(b.vpaClient))
}
+func (b *Builder) buildLeases() cache.Store {
+ return b.buildStore(leaseMetricFamilies, &coordinationv1.Lease{}, createLeaseListWatch)
+}
+
func (b *Builder) buildStore(
metricFamilies []generator.FamilyGenerator,
expectedType interface{},
diff --git a/internal/store/lease.go b/internal/store/lease.go
new file mode 100644
index 0000000000..f473d03667
--- /dev/null
+++ b/internal/store/lease.go
@@ -0,0 +1,113 @@
+/*
+Copyright 2020 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 (
+ coordinationv1 "k8s.io/api/coordination/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"
+
+ "k8s.io/kube-state-metrics/pkg/metric"
+ generator "k8s.io/kube-state-metrics/pkg/metric_generator"
+)
+
+var (
+ descLeaseLabelsDefaultLabels = []string{"lease"}
+
+ leaseMetricFamilies = []generator.FamilyGenerator{
+ {
+ Name: "kube_lease_owner",
+ Type: metric.Gauge,
+ Help: "Information about the Lease's owner.",
+ GenerateFunc: wrapLeaseFunc(func(l *coordinationv1.Lease) *metric.Family {
+ labelKeys := []string{"owner_kind", "owner_name"}
+
+ owners := l.GetOwnerReferences()
+ if len(owners) == 0 {
+ return &metric.Family{
+ Metrics: []*metric.Metric{
+ {
+ LabelKeys: labelKeys,
+ LabelValues: []string{"", ""},
+ Value: 1,
+ },
+ },
+ }
+ }
+ ms := make([]*metric.Metric, len(owners))
+
+ for i, owner := range owners {
+ ms[i] = &metric.Metric{
+ LabelKeys: labelKeys,
+ LabelValues: []string{owner.Kind, owner.Name},
+ Value: 1,
+ }
+ }
+
+ return &metric.Family{
+ Metrics: ms,
+ }
+ }),
+ },
+ {
+ Name: "kube_lease_renew_time",
+ Type: metric.Gauge,
+ Help: "Kube lease renew time.",
+ GenerateFunc: wrapLeaseFunc(func(l *coordinationv1.Lease) *metric.Family {
+ ms := []*metric.Metric{}
+
+ if !l.Spec.RenewTime.IsZero() {
+ ms = append(ms, &metric.Metric{
+ Value: float64(l.Spec.RenewTime.Unix()),
+ })
+ }
+ return &metric.Family{
+ Metrics: ms,
+ }
+ }),
+ },
+ }
+)
+
+func wrapLeaseFunc(f func(*coordinationv1.Lease) *metric.Family) func(interface{}) *metric.Family {
+ return func(obj interface{}) *metric.Family {
+ lease := obj.(*coordinationv1.Lease)
+
+ metricFamily := f(lease)
+
+ for _, m := range metricFamily.Metrics {
+ m.LabelKeys = append(descLeaseLabelsDefaultLabels, m.LabelKeys...)
+ m.LabelValues = append([]string{lease.Name}, m.LabelValues...)
+ }
+
+ return metricFamily
+ }
+}
+
+func createLeaseListWatch(kubeClient clientset.Interface, _ string) cache.ListerWatcher {
+ return &cache.ListWatch{
+ ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) {
+ return kubeClient.CoordinationV1().Leases("kube-node-lease").List(opts)
+ },
+ WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) {
+ return kubeClient.CoordinationV1().Leases("kube-node-lease").Watch(opts)
+ },
+ }
+}
diff --git a/internal/store/lease_test.go b/internal/store/lease_test.go
new file mode 100644
index 0000000000..a0d9618b4e
--- /dev/null
+++ b/internal/store/lease_test.go
@@ -0,0 +1,74 @@
+/*
+Copyright 2020 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"
+ "time"
+
+ coordinationv1 "k8s.io/api/coordination/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ generator "k8s.io/kube-state-metrics/pkg/metric_generator"
+)
+
+func TestLeaseStore(t *testing.T) {
+ const metadata = `
+ # HELP kube_lease_owner Information about the Lease's owner.
+ # TYPE kube_lease_owner gauge
+ # HELP kube_lease_renew_time Kube lease renew time.
+ # TYPE kube_lease_renew_time gauge
+ `
+
+ var (
+ cases = []generateMetricsTestCase{
+ {
+ Obj: &coordinationv1.Lease{
+ ObjectMeta: metav1.ObjectMeta{
+ Generation: 2,
+ Name: "kube-master",
+ CreationTimestamp: metav1.Time{Time: time.Unix(1500000000, 0)},
+ OwnerReferences: []metav1.OwnerReference{
+ {
+ Kind: "Node",
+ Name: "kube-master",
+ },
+ },
+ },
+ Spec: coordinationv1.LeaseSpec{
+ RenewTime: &metav1.MicroTime{Time: time.Unix(1500000000, 0)},
+ },
+ },
+ Want: metadata + `
+ kube_lease_owner{lease="kube-master",owner_kind="Node",owner_name="kube-master"} 1
+ kube_lease_renew_time{lease="kube-master"} 1.5e+09
+ `,
+ MetricNames: []string{
+ "kube_lease_owner",
+ "kube_lease_renew_time",
+ },
+ },
+ }
+ )
+ for i, c := range cases {
+ c.Func = generator.ComposeMetricGenFuncs(leaseMetricFamilies)
+ c.Headers = generator.ExtractMetricFamilyHeaders(leaseMetricFamilies)
+ if err := c.run(); err != nil {
+ t.Errorf("unexpected collecting result in %dth run:\n%v", i, err)
+ }
+ }
+}
diff --git a/jsonnet/kube-state-metrics/kube-state-metrics.libsonnet b/jsonnet/kube-state-metrics/kube-state-metrics.libsonnet
index 1169a7b903..7b2b93748e 100644
--- a/jsonnet/kube-state-metrics/kube-state-metrics.libsonnet
+++ b/jsonnet/kube-state-metrics/kube-state-metrics.libsonnet
@@ -137,6 +137,13 @@ local k = import 'ksonnet/ksonnet.beta.4/k.libsonnet';
'networkpolicies',
]) +
rulesType.withVerbs(['list', 'watch']),
+
+ rulesType.new() +
+ rulesType.withApiGroups(['coordination.k8s.io']) +
+ rulesType.withResources([
+ 'leases',
+ ]) +
+ rulesType.withVerbs(['list', 'watch']),
];
clusterRole.new() +
diff --git a/pkg/options/collector.go b/pkg/options/collector.go
index f3afaf76c1..97232214d0 100644
--- a/pkg/options/collector.go
+++ b/pkg/options/collector.go
@@ -35,6 +35,7 @@ var (
"horizontalpodautoscalers": struct{}{},
"ingresses": struct{}{},
"jobs": struct{}{},
+ "leases": struct{}{},
"limitranges": struct{}{},
"mutatingwebhookconfigurations": struct{}{},
"namespaces": struct{}{},
diff --git a/tests/manifests/lease.yaml b/tests/manifests/lease.yaml
new file mode 100644
index 0000000000..197df2a30e
--- /dev/null
+++ b/tests/manifests/lease.yaml
@@ -0,0 +1,14 @@
+apiVersion: coordination.k8s.io/v1
+kind: Lease
+metadata:
+ name: kube-master
+ namespace: kube-node-lease
+ ownerReferences:
+ - apiVersion: v1
+ kind: Node
+ name: kube-master
+ uid: 71c6ca9d-7a35-4380-b2ae-aca4d47800df
+spec:
+ holderIdentity: kube-master
+ leaseDurationSeconds: 40
+ renewTime: "2020-01-26T09:52:23.548762Z"