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..8eb21c4e4e --- /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>
`automount_token`=<serviceaccount-automount-token> | EXPERIMENTAL | +| kube_serviceaccount_created | Gauge | Unix creation timestamp | | `namespace`=<serviceaccount-namespace>
`serviceaccount`=<serviceaccount-name> | EXPERIMENTAL | +| kube_serviceaccount_deleted | Gauge | Unix deletion timestamp | | `namespace`=<serviceaccount-namespace>
`serviceaccount`=<serviceaccount-name> | EXPERIMENTAL | +| kube_serviceaccount_secret | Gauge | Secret being referenced by a service account | | `namespace`=<serviceaccount-namespace>
`serviceaccount`=<serviceaccount-name>
`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>
`name`=<secret-name> | EXPERIMENTAL | +| kube_serviceaccount_annotations | Gauge | Kubernetes annotations converted to Prometheus labels | | `namespace`=<serviceaccount-namespace>
`serviceaccount`=<serviceaccount-name>
`annotation_SERVICE_ACCOUNT_ANNOTATION`=<SERVICE_ACCOUNT_ANNOTATION> | EXPERIMENTAL | +| kube_serviceaccount_labels | Gauge | Kubernetes labels converted to Prometheus labels | | `namespace`=<serviceaccount-namespace>
`serviceaccount`=<serviceaccount-name>
`label_SERVICE_ACCOUNT_LABEL`=<SERVICE_ACCOUNT_LABEL> | EXPERIMENTAL | \ No newline at end of file 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..4e7f4341dd --- /dev/null +++ b/internal/store/serviceaccount.go @@ -0,0 +1,239 @@ +/* +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" + 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" + "strconv" +) + +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 { + // TODO: "s.APIVersion", "s.Kind" e.t.c are null or an empty string at this point + + 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 { + // TODO: "s.APIVersion", "s.Kind" e.t.c are null or an empty string at this point + + 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..b0f9316a52 --- /dev/null +++ b/internal/store/serviceaccount_test.go @@ -0,0 +1,86 @@ +/* +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 ( + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + generator "k8s.io/kube-state-metrics/v2/pkg/metric_generator" + "k8s.io/utils/pointer" + "testing" + "time" +) + +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/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..9df8927a73 --- /dev/null +++ b/tests/manifests/serviceaccount.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: serviceaccount +rules: + - apiGroups: [""] + resources: [""] + verbs: ["get", "watch", "list"] \ No newline at end of file