From 0be561695eda5e037d88ec19606f3227226218e8 Mon Sep 17 00:00:00 2001 From: Christian Schlotter Date: Mon, 30 Jan 2023 16:35:07 +0100 Subject: [PATCH] Implement metric-gen tool Implements the metric-gen tool which could get used to create custom resource configurations directly from code, similar to what controller-gen does. --- docs/cli-arguments.md | 1 + go.mod | 19 +- go.sum | 46 +++- main.go | 2 + pkg/customresourcestate/config.go | 12 +- .../config_metrics_types.go | 2 +- pkg/customresourcestate/generate/cmd.go | 125 +++++++++ .../generate/generator/generator.go | 140 ++++++++++ .../generate/generator/parser.go | 256 ++++++++++++++++++ .../generate/markers/gvk.go | 60 ++++ .../generate/markers/helper.go | 121 +++++++++ .../generate/markers/labelfrompath.go | 83 ++++++ .../generate/markers/markers.go | 49 ++++ .../generate/markers/metric_gauge.go | 87 ++++++ .../generate/markers/metric_info.go | 72 +++++ .../generate/markers/metric_stateset.go | 86 ++++++ 16 files changed, 1134 insertions(+), 27 deletions(-) create mode 100644 pkg/customresourcestate/generate/cmd.go create mode 100644 pkg/customresourcestate/generate/generator/generator.go create mode 100644 pkg/customresourcestate/generate/generator/parser.go create mode 100644 pkg/customresourcestate/generate/markers/gvk.go create mode 100644 pkg/customresourcestate/generate/markers/helper.go create mode 100644 pkg/customresourcestate/generate/markers/labelfrompath.go create mode 100644 pkg/customresourcestate/generate/markers/markers.go create mode 100644 pkg/customresourcestate/generate/markers/metric_gauge.go create mode 100644 pkg/customresourcestate/generate/markers/metric_info.go create mode 100644 pkg/customresourcestate/generate/markers/metric_stateset.go diff --git a/docs/cli-arguments.md b/docs/cli-arguments.md index 962798d0f2..6936fff2cf 100644 --- a/docs/cli-arguments.md +++ b/docs/cli-arguments.md @@ -31,6 +31,7 @@ Usage: Available Commands: completion Generate completion script for kube-state-metrics. + generate Generate custom resource metrics configuration from go-code markers. help Help about any command version Print version information. diff --git a/go.mod b/go.mod index 975bffca03..a793035412 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( k8s.io/klog/v2 v2.100.1 k8s.io/sample-controller v0.28.2 k8s.io/utils v0.0.0-20230726121419-3b25d923346b + sigs.k8s.io/controller-tools v0.13.0 ) require ( @@ -34,6 +35,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect + github.com/fatih/color v1.15.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-logr/logr v1.2.4 // indirect @@ -53,6 +55,8 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -68,19 +72,22 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.2 // indirect - golang.org/x/crypto v0.11.0 // indirect - golang.org/x/net v0.13.0 // indirect + golang.org/x/crypto v0.12.0 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/net v0.14.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect - golang.org/x/sync v0.2.0 // indirect - golang.org/x/sys v0.10.0 // indirect - golang.org/x/term v0.10.0 // indirect - golang.org/x/text v0.11.0 // indirect + golang.org/x/sync v0.3.0 // indirect + golang.org/x/sys v0.11.0 // indirect + golang.org/x/term v0.11.0 // indirect + golang.org/x/text v0.12.0 // indirect golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.12.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/apiextensions-apiserver v0.28.0 // indirect k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect diff --git a/go.sum b/go.sum index e36056b047..eaa28c36f1 100644 --- a/go.sum +++ b/go.sum @@ -71,6 +71,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= @@ -196,6 +198,11 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -209,10 +216,12 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE= -github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -281,8 +290,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -316,6 +325,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -348,8 +359,8 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY= -golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -371,8 +382,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -408,12 +419,13 @@ golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= -golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -422,8 +434,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -478,7 +490,8 @@ 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.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= +golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -585,6 +598,7 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -601,6 +615,8 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.28.2 h1:9mpl5mOb6vXZvqbQmankOfPIGiudghwCoLl1EYfUZbw= k8s.io/api v0.28.2/go.mod h1:RVnJBsjU8tcMq7C3iaRSGMeaKt2TWEUXcpIt/90fjEg= +k8s.io/apiextensions-apiserver v0.28.0 h1:CszgmBL8CizEnj4sj7/PtLGey6Na3YgWyGCPONv7E9E= +k8s.io/apiextensions-apiserver v0.28.0/go.mod h1:uRdYiwIuu0SyqJKriKmqEN2jThIJPhVmOWETm8ud1VE= k8s.io/apimachinery v0.28.2 h1:KCOJLrc6gu+wV1BYgwik4AF4vXOlVJPdiqn0yAWWwXQ= k8s.io/apimachinery v0.28.2/go.mod h1:RdzF87y/ngqk9H4z3EL2Rppv5jj95vGS/HaFXrLDApU= k8s.io/client-go v0.28.2 h1:DNoYI1vGq0slMBN/SWKMZMw0Rq+0EQW6/AK4v9+3VeY= @@ -618,6 +634,8 @@ k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/controller-tools v0.13.0 h1:NfrvuZ4bxyolhDBt/rCZhDnx3M2hzlhgo5n3Iv2RykI= +sigs.k8s.io/controller-tools v0.13.0/go.mod h1:5vw3En2NazbejQGCeWKRrE7q4P+CW8/klfVqP8QZkgA= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= diff --git a/main.go b/main.go index 5bdfaecaf8..973a98e612 100644 --- a/main.go +++ b/main.go @@ -21,6 +21,7 @@ import ( "k8s.io/klog/v2" "k8s.io/kube-state-metrics/v2/internal" + "k8s.io/kube-state-metrics/v2/pkg/customresourcestate/generate" "k8s.io/kube-state-metrics/v2/pkg/options" ) @@ -30,6 +31,7 @@ func main() { cmd.Run = func(cmd *cobra.Command, args []string) { internal.RunKubeStateMetricsWrapper(opts) } + cmd.AddCommand(generate.GenerateCommand) opts.AddFlags(cmd) if err := opts.Parse(); err != nil { klog.FlushAndExit(klog.ExitFlushTimeout, 1) diff --git a/pkg/customresourcestate/config.go b/pkg/customresourcestate/config.go index 385858b8ba..f9c25ea8ac 100644 --- a/pkg/customresourcestate/config.go +++ b/pkg/customresourcestate/config.go @@ -100,9 +100,9 @@ func (gvk GroupVersionKind) String() string { // Labels is common configuration of labels to add to metrics. type Labels struct { // CommonLabels are added to all metrics. - CommonLabels map[string]string `yaml:"commonLabels" json:"commonLabels"` + CommonLabels map[string]string `yaml:"commonLabels" json:"commonLabels,omitempty"` // LabelsFromPath adds additional labels where the value is taken from a field in the resource. - LabelsFromPath map[string][]string `yaml:"labelsFromPath" json:"labelsFromPath"` + LabelsFromPath map[string][]string `yaml:"labelsFromPath" json:"labelsFromPath,omitempty"` } // Merge combines the labels from two configs, returning a new config. The other Labels will overwrite keys in this Labels. @@ -140,7 +140,7 @@ type Generator struct { // Labels are added to all metrics. Labels from Each will overwrite these if using the same key. Labels `yaml:",inline" json:",inline"` // json will inline because it is already tagged // ErrorLogV defines the verbosity threshold for errors logged for this metric. Must be non-zero to override the resource setting. - ErrorLogV klog.Level `yaml:"errorLogV" json:"errorLogV"` + ErrorLogV klog.Level `yaml:"errorLogV" json:"errorLogV,omitempty"` } // Metric defines a metric to expose. @@ -152,13 +152,13 @@ type Metric struct { // Gauge defines a gauge metric. // +optional - Gauge *MetricGauge `yaml:"gauge" json:"gauge"` + Gauge *MetricGauge `yaml:"gauge,omitempty" json:"gauge,omitempty"` // StateSet defines a state set metric. // +optional - StateSet *MetricStateSet `yaml:"stateSet" json:"stateSet"` + StateSet *MetricStateSet `yaml:"stateSet,omitempty" json:"stateSet,omitempty"` // Info defines an info metric. // +optional - Info *MetricInfo `yaml:"info" json:"info"` + Info *MetricInfo `yaml:"info,omitempty" json:"info,omitempty"` } // ConfigDecoder is for use with FromConfig. diff --git a/pkg/customresourcestate/config_metrics_types.go b/pkg/customresourcestate/config_metrics_types.go index 6e8e9167cd..12d0dac8ba 100644 --- a/pkg/customresourcestate/config_metrics_types.go +++ b/pkg/customresourcestate/config_metrics_types.go @@ -29,7 +29,7 @@ const ( // MetricMeta are variables which may used for any metric type. type MetricMeta struct { // LabelsFromPath adds additional labels where the value of the label is taken from a field under Path. - LabelsFromPath map[string][]string `yaml:"labelsFromPath" json:"labelsFromPath"` + LabelsFromPath map[string][]string `yaml:"labelsFromPath,omitempty" json:"labelsFromPath,omitempty"` // Path is the path to to generate metric(s) for. Path []string `yaml:"path" json:"path"` } diff --git a/pkg/customresourcestate/generate/cmd.go b/pkg/customresourcestate/generate/cmd.go new file mode 100644 index 0000000000..3fb5e30ff5 --- /dev/null +++ b/pkg/customresourcestate/generate/cmd.go @@ -0,0 +1,125 @@ +/* +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 generate + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "sigs.k8s.io/controller-tools/pkg/genall" + "sigs.k8s.io/controller-tools/pkg/genall/help" + prettyhelp "sigs.k8s.io/controller-tools/pkg/genall/help/pretty" + "sigs.k8s.io/controller-tools/pkg/loader" + "sigs.k8s.io/controller-tools/pkg/markers" + + "k8s.io/kube-state-metrics/v2/pkg/customresourcestate/generate/generator" +) + +const ( + generatorName = "metric" +) + +var ( + // optionsRegistry contains all the marker definitions used to process command line options + optionsRegistry = &markers.Registry{} + + generateWhichMarkersFlag bool +) + +// GenerateCommand runs the kube-state-metrics custom resource config generator. +var GenerateCommand = &cobra.Command{ + Use: "generate [flags] /path/to/package [/path/to/package]", + Short: "Generate custom resource metrics configuration from go-code markers.", + DisableFlagsInUseLine: true, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if generateWhichMarkersFlag { + PrintMarkerDocs() + return nil + } + + // Register the metric generator itself as marker so genall.FromOptions is able to initialize the runtime properly. + // This also registers the markers inside the optionsRegistry so its available to print the marker docs. + metricGenerator := generator.CustomResourceConfigGenerator{} + defn := markers.Must(markers.MakeDefinition(generatorName, markers.DescribesPackage, metricGenerator)) + if err := optionsRegistry.Register(defn); err != nil { + return err + } + + // Load the passed packages as roots. + roots, err := loader.LoadRoots(args...) + if err != nil { + return fmt.Errorf("loading packages %w", err) + } + + // Set up the generator runtime using controller-tools and passing our optionsRegistry. + rt, err := genall.FromOptions(optionsRegistry, []string{generatorName}) + if err != nil { + return fmt.Errorf("%v", err) + } + + // Setup the generation context with the loaded roots. + rt.GenerationContext.Roots = roots + // Setup the runtime to output to stdout. + rt.OutputRules = genall.OutputRules{Default: genall.OutputToStdout} + + // Run the generator using the runtime. + if hadErrs := rt.Run(); hadErrs { + return fmt.Errorf("generator did not run successfully") + } + + return nil + }, + Example: "kube-state-metrics generate ./apis/... > custom-resource-config.yaml", +} + +func init() { + GenerateCommand.Flags().BoolVarP(&generateWhichMarkersFlag, "which-markers", "w", false, "Print out all markers available with the requested generators.") +} + +// PrintMarkerDocs prints out marker help for the given generators specified in +// the rawOptions +func PrintMarkerDocs() error { + // Register the metric generator itself as marker so genall.FromOptions is able to initialize the runtime properly. + // This also registers the markers inside the optionsRegistry so its available to print the marker docs. + metricGenerator := generator.CustomResourceConfigGenerator{} + defn := markers.Must(markers.MakeDefinition(generatorName, markers.DescribesPackage, metricGenerator)) + if err := optionsRegistry.Register(defn); err != nil { + return err + } + + // just grab a registry so we don't lag while trying to load roots + // (like we'd do if we just constructed the full runtime). + reg, err := genall.RegistryFromOptions(optionsRegistry, []string{generatorName}) + if err != nil { + return err + } + + helpInfo := help.ByCategory(reg, help.SortByCategory) + + for _, cat := range helpInfo { + if cat.Category == "" { + continue + } + contents := prettyhelp.MarkersDetails(false, cat.Category, cat.Markers) + if err := contents.WriteTo(os.Stderr); err != nil { + return err + } + } + return nil +} diff --git a/pkg/customresourcestate/generate/generator/generator.go b/pkg/customresourcestate/generate/generator/generator.go new file mode 100644 index 0000000000..15ac07ac9c --- /dev/null +++ b/pkg/customresourcestate/generate/generator/generator.go @@ -0,0 +1,140 @@ +/* +Copyright 2023 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 generator + +import ( + "fmt" + "sort" + + "k8s.io/klog/v2" + "sigs.k8s.io/controller-tools/pkg/crd" + "sigs.k8s.io/controller-tools/pkg/genall" + "sigs.k8s.io/controller-tools/pkg/loader" + ctrlmarkers "sigs.k8s.io/controller-tools/pkg/markers" + + "k8s.io/kube-state-metrics/v2/pkg/customresourcestate" + "k8s.io/kube-state-metrics/v2/pkg/customresourcestate/generate/markers" +) + +// CustomResourceConfigGenerator implements the Generator interface from controller-tools. +// It uses markers to generate a custom resource configuration for kube-state-metrics from go code. +type CustomResourceConfigGenerator struct{} + +var _ genall.Generator = &CustomResourceConfigGenerator{} +var _ genall.NeedsTypeChecking = &CustomResourceConfigGenerator{} + +// RegisterMarkers registers all markers needed by this Generator +// into the given registry. +func (g CustomResourceConfigGenerator) RegisterMarkers(into *ctrlmarkers.Registry) error { + for _, m := range markers.MarkerDefinitions { + if err := m.Register(into); err != nil { + return err + } + } + + return nil +} + +// Generate generates artifacts produced by this marker. +// It's called after RegisterMarkers has been called. +func (g CustomResourceConfigGenerator) Generate(ctx *genall.GenerationContext) error { + // Create the parser which is specific to the metric generator. + parser := newParser( + &crd.Parser{ + Collector: ctx.Collector, + Checker: ctx.Checker, + }, + ) + + // Loop over all passed packages. + for _, pkg := range ctx.Roots { + // skip packages which don't import metav1 because they can't define a CRD without meta v1. + metav1 := pkg.Imports()["k8s.io/apimachinery/pkg/apis/meta/v1"] + if metav1 == nil { + continue + } + + // parse the given package to feed crd.FindKubeKinds with Kubernetes Objects. + parser.NeedPackage(pkg) + + kubeKinds := crd.FindKubeKinds(parser.Parser, metav1) + if len(kubeKinds) == 0 { + klog.Fatalf("no objects in the roots") + } + + // Create metrics for all Custom Resources in this package. + // This creates the customresourcestate.Resource object which contains all metric + // definitions for the Custom Resource, if it is part of the package. + for _, gv := range kubeKinds { + parser.NeedResourceFor(pkg, gv) + } + } + + // Initialize empty customresourcestate configuration file and fill it with the + // customresourcestate.Resource objects from the parser. + metrics := customresourcestate.Metrics{ + Spec: customresourcestate.MetricsSpec{ + Resources: []customresourcestate.Resource{}, + }, + } + + for _, resource := range parser.CustomResourceStates { + if resource == nil { + continue + } + if len(resource.Metrics) > 0 { + // Sort the metrics to get a deterministic output. + sort.Slice(resource.Metrics, func(i, j int) bool { + return resource.Metrics[i].Name < resource.Metrics[j].Name + }) + + metrics.Spec.Resources = append(metrics.Spec.Resources, *resource) + } + } + + // Sort the resources by GVK to get a deterministic output. + sort.Slice(metrics.Spec.Resources, func(i, j int) bool { + a := metrics.Spec.Resources[i].GroupVersionKind.String() + b := metrics.Spec.Resources[j].GroupVersionKind.String() + return a < b + }) + + // Write the rendered yaml to the context which will result in stdout. + filePath := "metrics.yaml" + if err := ctx.WriteYAML(filePath, "", []interface{}{metrics}, genall.WithTransform(addCustomResourceStateKind)); err != nil { + return fmt.Errorf("WriteYAML to %s: %w", filePath, err) + } + + return nil +} + +// CheckFilter indicates the loader.NodeFilter (if any) that should be used +// to prune out unused types/packages when type-checking (nodes for which +// the filter returns true are considered "interesting"). This filter acts +// as a baseline -- all types the pass through this filter will be checked, +// but more than that may also be checked due to other generators' filters. +func (CustomResourceConfigGenerator) CheckFilter() loader.NodeFilter { + // Re-use controller-tools filter to filter out unrelated nodes that aren't used + // in CRD generation, like interfaces and struct fields without JSON tag. + return crd.Generator{}.CheckFilter() +} + +// addCustomResourceStateKind adds the correct kind because we don't have a correct +// kubernetes-style object as configuration definition. +func addCustomResourceStateKind(obj map[string]interface{}) error { + obj["kind"] = "CustomResourceStateMetrics" + return nil +} diff --git a/pkg/customresourcestate/generate/generator/parser.go b/pkg/customresourcestate/generate/generator/parser.go new file mode 100644 index 0000000000..432912892c --- /dev/null +++ b/pkg/customresourcestate/generate/generator/parser.go @@ -0,0 +1,256 @@ +/* +Copyright 2023 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 generator + +import ( + "fmt" + "go/ast" + "go/types" + "strings" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-tools/pkg/crd" + "sigs.k8s.io/controller-tools/pkg/loader" + ctrlmarkers "sigs.k8s.io/controller-tools/pkg/markers" + + "k8s.io/kube-state-metrics/v2/pkg/customresourcestate" + "k8s.io/kube-state-metrics/v2/pkg/customresourcestate/generate/markers" +) + +type parser struct { + *crd.Parser + + CustomResourceStates map[crd.TypeIdent]*customresourcestate.Resource +} + +func newParser(p *crd.Parser) *parser { + return &parser{ + Parser: p, + CustomResourceStates: make(map[crd.TypeIdent]*customresourcestate.Resource), + } +} + +// NeedResourceFor creates the customresourcestate.Resource object for the given +// GroupKind located at the package identified by packageID. +func (p *parser) NeedResourceFor(pkg *loader.Package, groupKind schema.GroupKind) { + typeIdent := crd.TypeIdent{Package: pkg, Name: groupKind.Kind} + // Skip if type was already processed. + if _, exists := p.CustomResourceStates[typeIdent]; exists { + return + } + + // Already mark the cacheID so the next time it enters NeedResourceFor it skips early. + p.CustomResourceStates[typeIdent] = nil + + // Build the type identifier for the custom resource. + typeInfo := p.Types[typeIdent] + // typeInfo is nil if this GroupKind is not part of this package. In that case + // we have nothing to process. + if typeInfo == nil { + return + } + + // Skip if gvk marker is not set. This marker is the opt-in for creating metrics + // for a custom resource. + if m := typeInfo.Markers.Get(markers.GVKMarkerName); m == nil { + return + } + + // Initialize the Resource object. + resource := customresourcestate.Resource{ + GroupVersionKind: customresourcestate.GroupVersionKind{ + Group: groupKind.Group, + Kind: groupKind.Kind, + Version: p.GroupVersions[pkg].Version, + }, + // Create the metrics generators for the custom resource. + Metrics: p.NeedMetricsGeneratorFor(typeIdent), + } + + // Iterate through all markers and run the ApplyToResource function of the ResourceMarkers. + for _, markerVals := range typeInfo.Markers { + for _, val := range markerVals { + if resourceMarker, isResourceMarker := val.(markers.ResourceMarker); isResourceMarker { + if err := resourceMarker.ApplyToResource(&resource); err != nil { + pkg.AddError(loader.ErrFromNode(err /* an okay guess */, typeInfo.RawSpec)) + } + } + } + } + + p.CustomResourceStates[typeIdent] = &resource +} + +type generatorRequester interface { + NeedMetricsGeneratorFor(typ crd.TypeIdent) []customresourcestate.Generator +} + +// generatorContext stores and provides information across a hierarchy of metric generators generation. +type generatorContext struct { + pkg *loader.Package + info *ctrlmarkers.TypeInfo + generatorRequester generatorRequester + + PackageMarkers ctrlmarkers.MarkerValues +} + +func newGeneratorContext(pkg *loader.Package, req generatorRequester) *generatorContext { + pkg.NeedTypesInfo() + return &generatorContext{ + pkg: pkg, + generatorRequester: req, + } +} + +func generatorsFromMarkers(m ctrlmarkers.MarkerValues, basePath ...string) []customresourcestate.Generator { + generators := []customresourcestate.Generator{} + + for _, markerVals := range m { + for _, val := range markerVals { + if generatorMarker, isGeneratorMarker := val.(markers.LocalGeneratorMarker); isGeneratorMarker { + if g := generatorMarker.ToGenerator(basePath...); g != nil { + generators = append(generators, *g) + } + } + } + } + + return generators +} + +// NeedMetricsGeneratorFor creates the customresourcestate.Generator object for a +// Custom Resource. +func (p *parser) NeedMetricsGeneratorFor(typ crd.TypeIdent) []customresourcestate.Generator { + info, gotInfo := p.Types[typ] + if !gotInfo { + klog.Fatal("expected to get info for %v but does not exist", typ) + } + + // Add metric generators defined by markers at the type. + generators := generatorsFromMarkers(info.Markers) + + // Traverse fields of the object and process markers. + // Note: Partially inspired by controller-tools. + // xref: https://github.com/kubernetes-sigs/controller-tools/blob/d89d6ae3df218a85f7cd9e477157cace704b37d1/pkg/crd/schema.go#L350 + for _, f := range info.Fields { + // Only fields with the `json:"..."` tag are relevant. Others are not part of the Custom Resource. + jsonTag, hasTag := f.Tag.Lookup("json") + if !hasTag { + // if the field doesn't have a JSON tag, it doesn't belong in output (and shouldn't exist in a serialized type) + continue + } + jsonOpts := strings.Split(jsonTag, ",") + if len(jsonOpts) == 1 && jsonOpts[0] == "-" { + // skipped fields have the tag "-" (note that "-," means the field is named "-") + continue + } + + // Add metric generators defined by markers at the field. + generators = append(generators, generatorsFromMarkers(f.Markers, jsonOpts[0])...) + + // Create new generator context and recursively process the fields. + generatorCtx := newGeneratorContext(typ.Package, p) + for _, generator := range generatorsFor(generatorCtx, f.RawField.Type) { + generators = append(generators, addPathPrefixOnGenerator(generator, jsonOpts[0])) + } + } + + return generators +} + +// generatorsFor creates generators for the given AST type. +// Note: Partially inspired by controller-tools. +// xref: https://github.com/kubernetes-sigs/controller-tools/blob/d89d6ae3df218a85f7cd9e477157cace704b37d1/pkg/crd/schema.go#L167-L193 +func generatorsFor(ctx *generatorContext, rawType ast.Expr) []customresourcestate.Generator { + switch expr := rawType.(type) { + case *ast.Ident: + return localNamedToGenerators(ctx, expr) + case *ast.SelectorExpr: + // Results in using transitive markers from external packages. + return generatorsFor(ctx, expr.X) + case *ast.ArrayType: + // The current configuration does not allow creating metric configurations inside arrays + return nil + case *ast.MapType: + // The current configuration does not allow creating metric configurations inside maps + return nil + case *ast.StarExpr: + return generatorsFor(ctx, expr.X) + case *ast.StructType: + klog.Fatal(loader.ErrFromNode(fmt.Errorf("unsupported AST kind %T", expr), rawType)) + default: + klog.Fatal(loader.ErrFromNode(fmt.Errorf("unsupported AST kind %T", expr), rawType)) + // NB(directxman12): we explicitly don't handle interfaces + return nil + } + + return nil +} + +// localNamedToGenerators recurses back to NeedMetricsGeneratorFor for the type to +// get generators defined at the objects in a custom resource. +func localNamedToGenerators(ctx *generatorContext, ident *ast.Ident) []customresourcestate.Generator { + typeInfo := ctx.pkg.TypesInfo.TypeOf(ident) + if typeInfo == types.Typ[types.Invalid] { + // It is expected to hit this error for types from not loaded transitive package dependencies. + // This leads to ignoring markers defined on the transitive types. Otherwise + // markers on transitive types would lead to additional metrics. + return nil + } + + if _, isBasic := typeInfo.(*types.Basic); isBasic { + // There can't be markers for basic go types for this generator. + return nil + } + + // NB(directxman12): if there are dot imports, this might be an external reference, + // so use typechecking info to get the actual object + typeNameInfo := typeInfo.(*types.Named).Obj() + pkg := typeNameInfo.Pkg() + pkgPath := loader.NonVendorPath(pkg.Path()) + if pkg == ctx.pkg.Types { + pkgPath = "" + } + return ctx.requestGenerator(pkgPath, typeNameInfo.Name()) +} + +// requestGenerator asks for the generator for a type in the package with the +// given import path. +func (c *generatorContext) requestGenerator(pkgPath, typeName string) []customresourcestate.Generator { + pkg := c.pkg + if pkgPath != "" { + pkg = c.pkg.Imports()[pkgPath] + } + return c.generatorRequester.NeedMetricsGeneratorFor(crd.TypeIdent{ + Package: pkg, + Name: typeName, + }) +} + +// addPathPrefixOnGenerator prefixes the path set at the generators MetricMeta object. +func addPathPrefixOnGenerator(generator customresourcestate.Generator, pathPrefix string) customresourcestate.Generator { + switch generator.Each.Type { + case customresourcestate.MetricTypeGauge: + generator.Each.Gauge.MetricMeta.Path = append([]string{pathPrefix}, generator.Each.Gauge.MetricMeta.Path...) + case customresourcestate.MetricTypeStateSet: + generator.Each.StateSet.MetricMeta.Path = append([]string{pathPrefix}, generator.Each.StateSet.MetricMeta.Path...) + case customresourcestate.MetricTypeInfo: + generator.Each.Info.MetricMeta.Path = append([]string{pathPrefix}, generator.Each.Info.MetricMeta.Path...) + } + + return generator +} diff --git a/pkg/customresourcestate/generate/markers/gvk.go b/pkg/customresourcestate/generate/markers/gvk.go new file mode 100644 index 0000000000..e51f45a5a2 --- /dev/null +++ b/pkg/customresourcestate/generate/markers/gvk.go @@ -0,0 +1,60 @@ +/* +Copyright 2023 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 markers + +import ( + "sigs.k8s.io/controller-tools/pkg/markers" + + "k8s.io/kube-state-metrics/v2/pkg/customresourcestate" +) + +const ( + // GVKMarkerName is the marker for a GVK. Without a set GVKMarkerName the + // generator will not generate any configuration for this GVK. + GVKMarkerName = "Metrics:gvk" +) + +func init() { + MarkerDefinitions = append( + MarkerDefinitions, + must(markers.MakeDefinition(GVKMarkerName, markers.DescribesType, gvkMarker{})). + help(gvkMarker{}.Help()), + ) +} + +// gvkMarker implements ResourceMarker to opt-in metric generation for a gvk and configure a name prefix. +type gvkMarker struct { + NamePrefix string `marker:"namePrefix,optional"` +} + +var _ ResourceMarker = gvkMarker{} + +// Help prints the help information for the gvkMarker. +func (gvkMarker) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "Metrics", + DetailedHelp: markers.DetailedHelp{ + Summary: "enables the creation of a customresourcestate Resource for the CRD and uses the given prefix for the metrics if configured.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (n gvkMarker) ApplyToResource(resource *customresourcestate.Resource) error { + resource.MetricNamePrefix = &n.NamePrefix + return nil +} diff --git a/pkg/customresourcestate/generate/markers/helper.go b/pkg/customresourcestate/generate/markers/helper.go new file mode 100644 index 0000000000..7c45b0d192 --- /dev/null +++ b/pkg/customresourcestate/generate/markers/helper.go @@ -0,0 +1,121 @@ +/* +Copyright 2023 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 markers + +import ( + "fmt" + + "k8s.io/client-go/util/jsonpath" + "k8s.io/klog/v2" + ctrlmarkers "sigs.k8s.io/controller-tools/pkg/markers" + + "k8s.io/kube-state-metrics/v2/pkg/customresourcestate" +) + +type markerDefinitionWithHelp struct { + *ctrlmarkers.Definition + Help *ctrlmarkers.DefinitionHelp +} + +func must(def *ctrlmarkers.Definition, err error) *markerDefinitionWithHelp { + return &markerDefinitionWithHelp{ + Definition: ctrlmarkers.Must(def, err), + } +} + +func (d *markerDefinitionWithHelp) help(help *ctrlmarkers.DefinitionHelp) *markerDefinitionWithHelp { + d.Help = help + return d +} + +func (d *markerDefinitionWithHelp) Register(reg *ctrlmarkers.Registry) error { + if err := reg.Register(d.Definition); err != nil { + return err + } + if d.Help != nil { + reg.AddHelp(d.Definition, d.Help) + } + return nil +} + +// jsonPath is a simple JSON path, i.e. without array notation. +type jsonPath string + +// Parse is implemented to overwrite how json.Marshal and json.Unmarshal handles +// this type and parses the string to a string array instead. It is inspired by +// `kubectl explain` parsing the json path parameter. +// xref: https://github.com/kubernetes/kubectl/blob/release-1.28/pkg/explain/explain.go#L35 +func (j jsonPath) Parse() ([]string, error) { + ret := []string{} + + jpp, err := jsonpath.Parse("JSONPath", `{`+string(j)+`}`) + if err != nil { + return nil, fmt.Errorf("parse JSONPath: %w", err) + } + + // Because of the way the jsonpath library works, the schema of the parser is [][]NodeList + // meaning we need to get the outer node list, make sure it's only length 1, then get the inner node + // list, and only then can we look at the individual nodes themselves. + outerNodeList := jpp.Root.Nodes + if len(outerNodeList) > 1 { + return nil, fmt.Errorf("must pass in 1 jsonpath string, got %d", len(outerNodeList)) + } + + list, ok := outerNodeList[0].(*jsonpath.ListNode) + if !ok { + return nil, fmt.Errorf("unable to typecast to jsonpath.ListNode") + } + for _, n := range list.Nodes { + nf, ok := n.(*jsonpath.FieldNode) + if !ok { + return nil, fmt.Errorf("unable to typecast to jsonpath.NodeField") + } + ret = append(ret, nf.Value) + } + + return ret, nil +} + +func newMetricMeta(basePath []string, j jsonPath, jsonLabelsFromPath map[string]jsonPath) customresourcestate.MetricMeta { + path := basePath + if j != "" { + valueFrom, err := j.Parse() + if err != nil { + klog.Fatal(err) + } + if len(valueFrom) > 0 { + path = append(path, valueFrom...) + } + } + + labelsFromPath := map[string][]string{} + for k, v := range jsonLabelsFromPath { + path := []string{} + var err error + if v != "." { + path, err = v.Parse() + if err != nil { + klog.Fatal(err) + } + } + labelsFromPath[k] = path + } + + return customresourcestate.MetricMeta{ + Path: path, + LabelsFromPath: labelsFromPath, + } +} diff --git a/pkg/customresourcestate/generate/markers/labelfrompath.go b/pkg/customresourcestate/generate/markers/labelfrompath.go new file mode 100644 index 0000000000..76c1eaeb87 --- /dev/null +++ b/pkg/customresourcestate/generate/markers/labelfrompath.go @@ -0,0 +1,83 @@ +/* +Copyright 2023 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 markers + +import ( + "fmt" + + "sigs.k8s.io/controller-tools/pkg/markers" + + "k8s.io/kube-state-metrics/v2/pkg/customresourcestate" +) + +const ( + labelFromPathMarkerName = "Metrics:labelFromPath" +) + +func init() { + MarkerDefinitions = append( + MarkerDefinitions, + must(markers.MakeDefinition(labelFromPathMarkerName, markers.DescribesType, labelFromPathMarker{})). + help(labelFromPathMarker{}.Help()), + must(markers.MakeDefinition(labelFromPathMarkerName, markers.DescribesField, labelFromPathMarker{})). + help(labelFromPathMarker{}.Help()), + ) +} + +// labelFromPathMarker is the marker to configure a labelFromPath for a gvk. +type labelFromPathMarker struct { + // +Metrics:labelFromPath:name=,JSONPath= on API type struct + Name string + JSONPath jsonPath `marker:"JSONPath"` +} + +var _ ResourceMarker = labelFromPathMarker{} + +// Help prints the help information for the LabelFromPathMarker. +func (labelFromPathMarker) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "Metrics", + DetailedHelp: markers.DetailedHelp{ + Summary: "adds an additional label to all metrics of this field or type with a value from the given JSONPath.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (n labelFromPathMarker) ApplyToResource(resource *customresourcestate.Resource) error { + if resource.LabelsFromPath == nil { + resource.LabelsFromPath = map[string][]string{} + } + jsonPathElems, err := n.JSONPath.Parse() + if err != nil { + return err + } + + if jsonPath, labelExists := resource.LabelsFromPath[n.Name]; labelExists { + if len(jsonPathElems) != len(jsonPath) { + return fmt.Errorf("duplicate definition for label %q", n.Name) + } + for i, v := range jsonPath { + if v != jsonPathElems[i] { + return fmt.Errorf("duplicate definition for label %q", n.Name) + } + } + } + + resource.LabelsFromPath[n.Name] = jsonPathElems + return nil +} diff --git a/pkg/customresourcestate/generate/markers/markers.go b/pkg/customresourcestate/generate/markers/markers.go new file mode 100644 index 0000000000..1c644675ee --- /dev/null +++ b/pkg/customresourcestate/generate/markers/markers.go @@ -0,0 +1,49 @@ +/* +Copyright 2023 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 markers + +import ( + "sigs.k8s.io/controller-tools/pkg/markers" + + "k8s.io/kube-state-metrics/v2/pkg/customresourcestate" +) + +var ( + // MarkerDefinitions contains all marker definitions defined by this package so + // they can get used in a generator. + MarkerDefinitions = []*markerDefinitionWithHelp{ + // GroupName is a marker copied from controller-runtime to identify the API Group. + // It needs to get added as marker so the parser will be able to read the API + // which is Group set for a package. + must(markers.MakeDefinition("groupName", markers.DescribesPackage, "")), + } +) + +// +controllertools:marker:generateHelp:category=CRD + +// ResourceMarker is a marker that configures a custom resource. +type ResourceMarker interface { + // ApplyToCRD applies this marker to the given CRD, in the given version + // within that CRD. It's called after everything else in the CRD is populated. + ApplyToResource(resource *customresourcestate.Resource) error +} + +// LocalGeneratorMarker is a marker that creates a custom resource metric generator. +type LocalGeneratorMarker interface { + // ApplyToCRD applies this marker to the given CRD, in the given version + // within that CRD. It's called after everything else in the CRD is populated. + ToGenerator(basePath ...string) *customresourcestate.Generator +} diff --git a/pkg/customresourcestate/generate/markers/metric_gauge.go b/pkg/customresourcestate/generate/markers/metric_gauge.go new file mode 100644 index 0000000000..2ea39ec6b9 --- /dev/null +++ b/pkg/customresourcestate/generate/markers/metric_gauge.go @@ -0,0 +1,87 @@ +/* +Copyright 2023 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 markers + +import ( + "sigs.k8s.io/controller-tools/pkg/markers" + + "k8s.io/klog/v2" + + "k8s.io/kube-state-metrics/v2/pkg/customresourcestate" +) + +const ( + gaugeMarkerName = "Metrics:gauge" +) + +func init() { + MarkerDefinitions = append( + MarkerDefinitions, + must(markers.MakeDefinition(gaugeMarkerName, markers.DescribesField, gaugeMarker{})). + help(gaugeMarker{}.help()), + must(markers.MakeDefinition(gaugeMarkerName, markers.DescribesType, gaugeMarker{})). + help(gaugeMarker{}.help()), + ) +} + +// gaugeMarker implements localGeneratorMarker to generate a gauge type metric. +type gaugeMarker struct { + Name string + Help string `marker:"help,optional"` + JSONPath jsonPath `marker:"JSONPath,optional"` + LabelFromKey string `marker:"labelFromKey,optional"` + LabelsFromPath map[string]jsonPath `marker:"labelsFromPath,optional"` + NilIsZero bool `marker:"nilIsZero,optional"` + ValueFrom *jsonPath `marker:"valueFrom,optional"` +} + +var _ LocalGeneratorMarker = &gaugeMarker{} + +func (gaugeMarker) help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "Metrics", + DetailedHelp: markers.DetailedHelp{ + Summary: "Defines a Gauge metric and uses the implicit path to the field joined by the provided JSONPath as path for the metric configuration.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (g gaugeMarker) ToGenerator(basePath ...string) *customresourcestate.Generator { + var err error + var valueFrom []string + if g.ValueFrom != nil { + valueFrom, err = g.ValueFrom.Parse() + if err != nil { + klog.Fatal(err) + } + } + + return &customresourcestate.Generator{ + Name: g.Name, + Help: g.Help, + Each: customresourcestate.Metric{ + Type: customresourcestate.MetricTypeGauge, + Gauge: &customresourcestate.MetricGauge{ + NilIsZero: g.NilIsZero, + MetricMeta: newMetricMeta(basePath, g.JSONPath, g.LabelsFromPath), + LabelFromKey: g.LabelFromKey, + ValueFrom: valueFrom, + }, + }, + } +} diff --git a/pkg/customresourcestate/generate/markers/metric_info.go b/pkg/customresourcestate/generate/markers/metric_info.go new file mode 100644 index 0000000000..d0f91f97b2 --- /dev/null +++ b/pkg/customresourcestate/generate/markers/metric_info.go @@ -0,0 +1,72 @@ +/* +Copyright 2023 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 markers + +import ( + "sigs.k8s.io/controller-tools/pkg/markers" + + "k8s.io/kube-state-metrics/v2/pkg/customresourcestate" +) + +const ( + infoMarkerName = "Metrics:info" +) + +func init() { + MarkerDefinitions = append( + MarkerDefinitions, + must(markers.MakeDefinition(infoMarkerName, markers.DescribesField, infoMarker{})). + help(infoMarker{}.help()), + must(markers.MakeDefinition(infoMarkerName, markers.DescribesType, infoMarker{})). + help(infoMarker{}.help()), + ) +} + +// infoMarker implements localGeneratorMarker to generate a info type metric. +type infoMarker struct { + Name string + Help string `marker:"help,optional"` + LabelsFromPath map[string]jsonPath `marker:"labelsFromPath,optional"` + JSONPath jsonPath `marker:"JSONPath,optional"` + LabelFromKey string `marker:"labelFromKey,optional"` +} + +var _ LocalGeneratorMarker = &infoMarker{} + +func (infoMarker) help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "Metrics", + DetailedHelp: markers.DetailedHelp{ + Summary: "Defines a Info metric and uses the implicit path to the field as path for the metric configuration.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (i infoMarker) ToGenerator(basePath ...string) *customresourcestate.Generator { + return &customresourcestate.Generator{ + Name: i.Name, + Help: i.Help, + Each: customresourcestate.Metric{ + Type: customresourcestate.MetricTypeInfo, + Info: &customresourcestate.MetricInfo{ + MetricMeta: newMetricMeta(basePath, i.JSONPath, i.LabelsFromPath), + LabelFromKey: i.LabelFromKey, + }, + }, + } +} diff --git a/pkg/customresourcestate/generate/markers/metric_stateset.go b/pkg/customresourcestate/generate/markers/metric_stateset.go new file mode 100644 index 0000000000..eddcacc485 --- /dev/null +++ b/pkg/customresourcestate/generate/markers/metric_stateset.go @@ -0,0 +1,86 @@ +/* +Copyright 2023 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 markers + +import ( + "sigs.k8s.io/controller-tools/pkg/markers" + + "k8s.io/klog/v2" + + "k8s.io/kube-state-metrics/v2/pkg/customresourcestate" +) + +const ( + stateSetMarkerName = "Metrics:stateset" +) + +func init() { + MarkerDefinitions = append( + MarkerDefinitions, + must(markers.MakeDefinition(stateSetMarkerName, markers.DescribesField, stateSetMarker{})). + help(stateSetMarker{}.help()), + must(markers.MakeDefinition(stateSetMarkerName, markers.DescribesType, stateSetMarker{})). + help(stateSetMarker{}.help()), + ) +} + +// stateSetMarker is the marker to generate a stateSet type metric. +type stateSetMarker struct { + Name string + Help string `marker:"help,optional"` + LabelsFromPath map[string]jsonPath `marker:"labelsFromPath,optional"` + JSONPath *jsonPath `marker:"JSONPath,optional"` + LabelName string `marker:"labelName,optional"` + List []string `marker:"list"` +} + +var _ LocalGeneratorMarker = &stateSetMarker{} + +func (stateSetMarker) help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "Metrics", + DetailedHelp: markers.DetailedHelp{ + Summary: "Defines a StateSet metric and uses the implicit path to the field as path for the metric configuration.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (s stateSetMarker) ToGenerator(basePath ...string) *customresourcestate.Generator { + var valueFrom []string + var err error + if s.JSONPath != nil { + valueFrom, err = s.JSONPath.Parse() + if err != nil { + klog.Fatal(err) + } + } + + return &customresourcestate.Generator{ + Name: s.Name, + Help: s.Help, + Each: customresourcestate.Metric{ + Type: customresourcestate.MetricTypeStateSet, + StateSet: &customresourcestate.MetricStateSet{ + MetricMeta: newMetricMeta(basePath, "", s.LabelsFromPath), + List: s.List, + LabelName: s.LabelName, + ValueFrom: valueFrom, + }, + }, + } +}