diff --git a/docs/README.md b/docs/README.md
index 5fb8459d5a..19c979fe28 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -53,6 +53,7 @@ Per group of metrics there is one file for each metrics. See each file for speci
- [ResourceQuota Metrics](resourcequota-metrics.md)
- [Secret Metrics](secret-metrics.md)
- [Service Metrics](service-metrics.md)
+- [ServiceAccount Metrics](serviceaccount-metrics.md)
- [StatefulSet Metrics](statefulset-metrics.md)
- [StorageClass Metrics](storageclass-metrics.md)
- [ValidatingWebhookConfiguration Metrics](validatingwebhookconfiguration-metrics.md)
diff --git a/docs/cli-arguments.md b/docs/cli-arguments.md
index f70c68d3b0..fe4bc91ffa 100644
--- a/docs/cli-arguments.md
+++ b/docs/cli-arguments.md
@@ -50,7 +50,7 @@ Usage of ./kube-state-metrics:
--pod string Name of the pod that contains the kube-state-metrics container. When set, it is expected that --pod and --pod-namespace are both set. Most likely this should be passed via the downward API. This is used for auto-detecting sharding. If set, this has preference over statically configured sharding. This is experimental, it may be removed without notice.
--pod-namespace string Name of the namespace of the pod specified by --pod. When set, it is expected that --pod and --pod-namespace are both set. Most likely this should be passed via the downward API. This is used for auto-detecting sharding. If set, this has preference over statically configured sharding. This is experimental, it may be removed without notice.
--port int Port to expose metrics on. (default 8080)
- --resources string Comma-separated list of Resources 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"
+ --resources string Comma-separated list of Resources 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,serviceaccounts,services,statefulsets,storageclasses,validatingwebhookconfigurations,volumeattachments"
--shard int32 The instances shard nominal (zero indexed) within the total number of shards. (default 0)
--skip_headers If true, avoid header prefixes in the log messages
--skip_log_headers If true, avoid headers when opening log files
diff --git a/docs/serviceaccount-metrics.md b/docs/serviceaccount-metrics.md
new file mode 100644
index 0000000000..49e7ee83c7
--- /dev/null
+++ b/docs/serviceaccount-metrics.md
@@ -0,0 +1,11 @@
+# Service Metrics
+
+| Metric name | Metric type | Description | Unit (where applicable) | Labels/tags | Status |
+|---------------------------------------|-------------|--------------------------------------------------------------------------------|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------|
+| kube_serviceaccount_info | Gauge | Information about a service account | | `namespace`=<serviceaccount-namespace>
`serviceaccount`=<serviceaccount-name>
`uid`=<serviceaccount-uid>
`automount_token`=<serviceaccount-automount-token> | EXPERIMENTAL |
+| kube_serviceaccount_created | Gauge | Unix creation timestamp | | `namespace`=<serviceaccount-namespace>
`serviceaccount`=<serviceaccount-name>
`uid`=<serviceaccount-uid> | EXPERIMENTAL |
+| kube_serviceaccount_deleted | Gauge | Unix deletion timestamp | | `namespace`=<serviceaccount-namespace>
`serviceaccount`=<serviceaccount-name>
`uid`=<serviceaccount-uid> | EXPERIMENTAL |
+| kube_serviceaccount_secret | Gauge | Secret being referenced by a service account | | `namespace`=<serviceaccount-namespace>
`serviceaccount`=<serviceaccount-name>
`uid`=<serviceaccount-uid>
`name`=<secret-name> | EXPERIMENTAL |
+| kube_serviceaccount_image_pull_secret | Gauge | Secret being referenced by a service account for the purpose of pulling images | | `namespace`=<serviceaccount-namespace>
`serviceaccount`=<serviceaccount-name>
`uid`=<serviceaccount-uid>
`name`=<secret-name> | EXPERIMENTAL |
+| kube_serviceaccount_annotations | Gauge | Kubernetes annotations converted to Prometheus labels | | `namespace`=<serviceaccount-namespace>
`serviceaccount`=<serviceaccount-name>
`uid`=<serviceaccount-uid>
`annotation_SERVICE_ACCOUNT_ANNOTATION`=<SERVICE_ACCOUNT_ANNOTATION> | EXPERIMENTAL |
+| kube_serviceaccount_labels | Gauge | Kubernetes labels converted to Prometheus labels | | `namespace`=<serviceaccount-namespace>
`serviceaccount`=<serviceaccount-name>
`uid`=<serviceaccount-uid>
`label_SERVICE_ACCOUNT_LABEL`=<SERVICE_ACCOUNT_LABEL> | EXPERIMENTAL |
diff --git a/examples/autosharding/cluster-role.yaml b/examples/autosharding/cluster-role.yaml
index 7992529ac7..c82473384d 100644
--- a/examples/autosharding/cluster-role.yaml
+++ b/examples/autosharding/cluster-role.yaml
@@ -15,6 +15,7 @@ rules:
- nodes
- pods
- services
+ - serviceaccounts
- resourcequotas
- replicationcontrollers
- limitranges
diff --git a/examples/standard/cluster-role.yaml b/examples/standard/cluster-role.yaml
index 7992529ac7..c82473384d 100644
--- a/examples/standard/cluster-role.yaml
+++ b/examples/standard/cluster-role.yaml
@@ -15,6 +15,7 @@ rules:
- nodes
- pods
- services
+ - serviceaccounts
- resourcequotas
- replicationcontrollers
- limitranges
diff --git a/internal/store/builder.go b/internal/store/builder.go
index 198e47f9e8..1707ebcc7b 100644
--- a/internal/store/builder.go
+++ b/internal/store/builder.go
@@ -283,6 +283,7 @@ var availableStores = map[string]func(f *Builder) []cache.Store{
"replicationcontrollers": func(b *Builder) []cache.Store { return b.buildReplicationControllerStores() },
"resourcequotas": func(b *Builder) []cache.Store { return b.buildResourceQuotaStores() },
"secrets": func(b *Builder) []cache.Store { return b.buildSecretStores() },
+ "serviceaccounts": func(b *Builder) []cache.Store { return b.buildServiceAccountStores() },
"services": func(b *Builder) []cache.Store { return b.buildServiceStores() },
"statefulsets": func(b *Builder) []cache.Store { return b.buildStatefulSetStores() },
"storageclasses": func(b *Builder) []cache.Store { return b.buildStorageClassStores() },
@@ -384,6 +385,10 @@ func (b *Builder) buildSecretStores() []cache.Store {
return b.buildStoresFunc(secretMetricFamilies(b.allowAnnotationsList["secrets"], b.allowLabelsList["secrets"]), &v1.Secret{}, createSecretListWatch, b.useAPIServerCache)
}
+func (b *Builder) buildServiceAccountStores() []cache.Store {
+ return b.buildStoresFunc(serviceAccountMetricFamilies(b.allowAnnotationsList["serviceaccounts"], b.allowLabelsList["serviceaccounts"]), &v1.ServiceAccount{}, createServiceAccountListWatch, b.useAPIServerCache)
+}
+
func (b *Builder) buildServiceStores() []cache.Store {
return b.buildStoresFunc(serviceMetricFamilies(b.allowAnnotationsList["services"], b.allowLabelsList["services"]), &v1.Service{}, createServiceListWatch, b.useAPIServerCache)
}
diff --git a/internal/store/serviceaccount.go b/internal/store/serviceaccount.go
new file mode 100644
index 0000000000..5958d9ed77
--- /dev/null
+++ b/internal/store/serviceaccount.go
@@ -0,0 +1,237 @@
+/*
+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"
+
+ v1 "k8s.io/api/core/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/v2/pkg/metric"
+ generator "k8s.io/kube-state-metrics/v2/pkg/metric_generator"
+)
+
+var (
+ descServiceAccountLabelsDefaultLabels = []string{"namespace", "serviceaccount", "uid"}
+)
+
+func serviceAccountMetricFamilies(allowAnnotationsList, allowLabelsList []string) []generator.FamilyGenerator {
+ return []generator.FamilyGenerator{
+ createServiceAccountInfoFamilyGenerator(),
+ createServiceAccountCreatedFamilyGenerator(),
+ createServiceAccountDeletedFamilyGenerator(),
+ createServiceAccountSecretFamilyGenerator(),
+ createServiceAccountImagePullSecretFamilyGenerator(),
+ createServiceAccountAnnotationsGenerator(allowAnnotationsList),
+ createServiceAccountLabelsGenerator(allowLabelsList),
+ }
+}
+
+func createServiceAccountInfoFamilyGenerator() generator.FamilyGenerator {
+ return *generator.NewFamilyGenerator(
+ "kube_serviceaccount_info",
+ "Information about a service account",
+ metric.Gauge,
+ "",
+ wrapServiceAccountFunc(func(sa *v1.ServiceAccount) *metric.Family {
+ var labelKeys []string
+ var labelValues []string
+
+ if sa.AutomountServiceAccountToken != nil {
+ labelKeys = append(labelKeys, "automount_token")
+ labelValues = append(labelValues, strconv.FormatBool(*sa.AutomountServiceAccountToken))
+ }
+
+ return &metric.Family{
+ Metrics: []*metric.Metric{{
+ LabelKeys: labelKeys,
+ LabelValues: labelValues,
+ Value: 1,
+ }},
+ }
+ }),
+ )
+}
+
+func createServiceAccountCreatedFamilyGenerator() generator.FamilyGenerator {
+ return *generator.NewFamilyGenerator(
+ "kube_serviceaccount_created",
+ "Unix creation timestamp",
+ metric.Gauge,
+ "",
+ wrapServiceAccountFunc(func(sa *v1.ServiceAccount) *metric.Family {
+ var ms []*metric.Metric
+
+ if !sa.CreationTimestamp.IsZero() {
+ ms = append(ms, &metric.Metric{
+ LabelKeys: []string{},
+ LabelValues: []string{},
+ Value: float64(sa.CreationTimestamp.Unix()),
+ })
+ }
+
+ return &metric.Family{
+ Metrics: ms,
+ }
+ }),
+ )
+}
+
+func createServiceAccountDeletedFamilyGenerator() generator.FamilyGenerator {
+ return *generator.NewFamilyGenerator(
+ "kube_serviceaccount_deleted",
+ "Unix deletion timestamp",
+ metric.Gauge,
+ "",
+ wrapServiceAccountFunc(func(sa *v1.ServiceAccount) *metric.Family {
+ var ms []*metric.Metric
+
+ if sa.DeletionTimestamp != nil && !sa.DeletionTimestamp.IsZero() {
+ ms = append(ms, &metric.Metric{
+ LabelKeys: []string{},
+ LabelValues: []string{},
+ Value: float64(sa.DeletionTimestamp.Unix()),
+ })
+ }
+
+ return &metric.Family{
+ Metrics: ms,
+ }
+ }),
+ )
+}
+
+func createServiceAccountSecretFamilyGenerator() generator.FamilyGenerator {
+ return *generator.NewFamilyGenerator(
+ "kube_serviceaccount_secret",
+ "Secret being referenced by a service account",
+ metric.Gauge,
+ "",
+ wrapServiceAccountFunc(func(sa *v1.ServiceAccount) *metric.Family {
+ var ms []*metric.Metric
+
+ for _, s := range sa.Secrets {
+ ms = append(ms, &metric.Metric{
+ LabelKeys: []string{"name"},
+ LabelValues: []string{s.Name},
+ Value: 1,
+ })
+ }
+
+ return &metric.Family{
+ Metrics: ms,
+ }
+ }),
+ )
+}
+
+func createServiceAccountImagePullSecretFamilyGenerator() generator.FamilyGenerator {
+ return *generator.NewFamilyGenerator(
+ "kube_serviceaccount_image_pull_secret",
+ "Secret being referenced by a service account for the purpose of pulling images",
+ metric.Gauge,
+ "",
+ wrapServiceAccountFunc(func(sa *v1.ServiceAccount) *metric.Family {
+ var ms []*metric.Metric
+
+ for _, s := range sa.ImagePullSecrets {
+ ms = append(ms, &metric.Metric{
+ LabelKeys: []string{"name"},
+ LabelValues: []string{s.Name},
+ Value: 1,
+ })
+ }
+
+ return &metric.Family{
+ Metrics: ms,
+ }
+ }),
+ )
+}
+
+func createServiceAccountAnnotationsGenerator(allowAnnotations []string) generator.FamilyGenerator {
+ return *generator.NewFamilyGenerator(
+ "kube_serviceaccount_annotations",
+ "Kubernetes annotations converted to Prometheus labels.",
+ metric.Gauge,
+ "",
+ wrapServiceAccountFunc(func(sa *v1.ServiceAccount) *metric.Family {
+ annotationKeys, annotationValues := createPrometheusLabelKeysValues("annotation", sa.Annotations, allowAnnotations)
+ m := metric.Metric{
+ LabelKeys: annotationKeys,
+ LabelValues: annotationValues,
+ Value: 1,
+ }
+ return &metric.Family{
+ Metrics: []*metric.Metric{&m},
+ }
+ }),
+ )
+}
+
+func createServiceAccountLabelsGenerator(allowLabelsList []string) generator.FamilyGenerator {
+ return *generator.NewFamilyGenerator(
+ "kube_serviceaccount_labels",
+ "Kubernetes labels converted to Prometheus labels.",
+ metric.Gauge,
+ "",
+ wrapServiceAccountFunc(func(sa *v1.ServiceAccount) *metric.Family {
+ labelKeys, labelValues := createPrometheusLabelKeysValues("label", sa.Labels, allowLabelsList)
+ m := metric.Metric{
+ LabelKeys: labelKeys,
+ LabelValues: labelValues,
+ Value: 1,
+ }
+ return &metric.Family{
+ Metrics: []*metric.Metric{&m},
+ }
+ }),
+ )
+}
+
+func wrapServiceAccountFunc(f func(*v1.ServiceAccount) *metric.Family) func(interface{}) *metric.Family {
+ return func(obj interface{}) *metric.Family {
+ serviceAccount := obj.(*v1.ServiceAccount)
+
+ metricFamily := f(serviceAccount)
+
+ for _, m := range metricFamily.Metrics {
+ m.LabelKeys, m.LabelValues = mergeKeyValues(descServiceAccountLabelsDefaultLabels, []string{serviceAccount.Namespace, serviceAccount.Name, string(serviceAccount.UID)}, m.LabelKeys, m.LabelValues)
+ }
+
+ return metricFamily
+ }
+}
+
+func createServiceAccountListWatch(kubeClient clientset.Interface, ns string, fieldSelector string) cache.ListerWatcher {
+ return &cache.ListWatch{
+ ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) {
+ opts.FieldSelector = fieldSelector
+ return kubeClient.CoreV1().ServiceAccounts(ns).List(context.TODO(), opts)
+ },
+ WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) {
+ opts.FieldSelector = fieldSelector
+ return kubeClient.CoreV1().ServiceAccounts(ns).Watch(context.TODO(), opts)
+ },
+ }
+}
diff --git a/internal/store/serviceaccount_test.go b/internal/store/serviceaccount_test.go
new file mode 100644
index 0000000000..1223a5b6af
--- /dev/null
+++ b/internal/store/serviceaccount_test.go
@@ -0,0 +1,88 @@
+/*
+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"
+ "time"
+
+ v1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/utils/pointer"
+
+ generator "k8s.io/kube-state-metrics/v2/pkg/metric_generator"
+)
+
+func TestServiceAccountStore(t *testing.T) {
+ cases := []generateMetricsTestCase{
+ {
+ Obj: &v1.ServiceAccount{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "serviceAccountName",
+ CreationTimestamp: metav1.Time{Time: time.Unix(1500000000, 0)},
+ DeletionTimestamp: &metav1.Time{Time: time.Unix(3000000000, 0)},
+ Namespace: "serviceAccountNS",
+ UID: "serviceAccountUID",
+ },
+ AutomountServiceAccountToken: pointer.Bool(true),
+ Secrets: []v1.ObjectReference{
+ {
+ APIVersion: "v1",
+ Kind: "Secret",
+ Name: "secretName",
+ Namespace: "serviceAccountNS",
+ },
+ },
+ ImagePullSecrets: []v1.LocalObjectReference{
+ {
+ Name: "imagePullSecretName",
+ },
+ },
+ },
+ Want: `
+ # HELP kube_serviceaccount_info Information about a service account
+ # HELP kube_serviceaccount_created Unix creation timestamp
+ # HELP kube_serviceaccount_deleted Unix deletion timestamp
+ # HELP kube_serviceaccount_secret Secret being referenced by a service account
+ # HELP kube_serviceaccount_image_pull_secret Secret being referenced by a service account for the purpose of pulling images
+ # TYPE kube_serviceaccount_info gauge
+ # TYPE kube_serviceaccount_created gauge
+ # TYPE kube_serviceaccount_deleted gauge
+ # TYPE kube_serviceaccount_secret gauge
+ # TYPE kube_serviceaccount_image_pull_secret gauge
+ kube_serviceaccount_info{namespace="serviceAccountNS",serviceaccount="serviceAccountName",uid="serviceAccountUID",automount_token="true"} 1
+ kube_serviceaccount_created{namespace="serviceAccountNS",serviceaccount="serviceAccountName",uid="serviceAccountUID"} 1.5e+09
+ kube_serviceaccount_deleted{namespace="serviceAccountNS",serviceaccount="serviceAccountName",uid="serviceAccountUID"} 3e+09
+ kube_serviceaccount_secret{namespace="serviceAccountNS",serviceaccount="serviceAccountName",uid="serviceAccountUID",name="secretName"} 1
+ kube_serviceaccount_image_pull_secret{namespace="serviceAccountNS",serviceaccount="serviceAccountName",uid="serviceAccountUID",name="imagePullSecretName"} 1`,
+ MetricNames: []string{
+ "kube_serviceaccount_info",
+ "kube_serviceaccount_created",
+ "kube_serviceaccount_deleted",
+ "kube_serviceaccount_secret",
+ "kube_serviceaccount_image_pull_secret",
+ },
+ },
+ }
+ for i, c := range cases {
+ c.Func = generator.ComposeMetricGenFuncs(serviceAccountMetricFamilies(c.AllowAnnotationsList, c.AllowLabelsList))
+ c.Headers = generator.ExtractMetricFamilyHeaders(serviceAccountMetricFamilies(c.AllowAnnotationsList, c.AllowLabelsList))
+ 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 696ea08393..e4867afee7 100644
--- a/jsonnet/kube-state-metrics/kube-state-metrics.libsonnet
+++ b/jsonnet/kube-state-metrics/kube-state-metrics.libsonnet
@@ -50,6 +50,7 @@
'nodes',
'pods',
'services',
+ 'serviceaccounts',
'resourcequotas',
'replicationcontrollers',
'limitranges',
diff --git a/pkg/options/resource.go b/pkg/options/resource.go
index 09e0e0400c..8c4a170bbb 100644
--- a/pkg/options/resource.go
+++ b/pkg/options/resource.go
@@ -49,6 +49,7 @@ var (
"replicationcontrollers": struct{}{},
"resourcequotas": struct{}{},
"secrets": struct{}{},
+ "serviceaccounts": struct{}{},
"services": struct{}{},
"statefulsets": struct{}{},
"storageclasses": struct{}{},
diff --git a/tests/manifests/serviceaccount.yaml b/tests/manifests/serviceaccount.yaml
new file mode 100644
index 0000000000..be6c2317b9
--- /dev/null
+++ b/tests/manifests/serviceaccount.yaml
@@ -0,0 +1,4 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: test-service-account
\ No newline at end of file