From ee82097ffeffe7d03521984b33574e87d7f24611 Mon Sep 17 00:00:00 2001 From: Christian Schlotter Date: Mon, 30 Jan 2023 16:35:07 +0100 Subject: [PATCH 1/6] 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 | 7 + go.sum | 20 +- 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, 1115 insertions(+), 8 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 4644daff42..6a5c5b2376 100644 --- a/docs/cli-arguments.md +++ b/docs/cli-arguments.md @@ -34,6 +34,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 df9e0739e9..0a4b5df6e3 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( k8s.io/klog/v2 v2.120.1 k8s.io/sample-controller v0.28.4 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.2-0.20180830191138-d8f796af33cc // 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.4.1 // 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/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -72,6 +76,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.18.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/mod v0.14.0 // indirect golang.org/x/net v0.20.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/sync v0.5.0 // indirect @@ -79,11 +84,13 @@ require ( golang.org/x/term v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.13.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.32.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 327ea86e07..b982a1f9ec 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhF github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 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.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= @@ -78,6 +80,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/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -89,10 +96,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.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -154,6 +163,8 @@ golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjs golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 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.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -171,6 +182,7 @@ golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= @@ -187,6 +199,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 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= @@ -204,6 +217,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.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= @@ -212,6 +226,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY= k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0= +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.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8= k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg= k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY= @@ -226,6 +242,8 @@ k8s.io/sample-controller v0.28.4 h1:qghAHWGAFbDaTssOEiktdjbpq9avioOKRMB+KEwBIR0= k8s.io/sample-controller v0.28.4/go.mod h1:XXL627j2rVrUQTMpebt6imNnSE30tmFnAOvZsYUsWpo= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +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..866728af61 --- /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.Fatalf("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, + }, + }, + } +} From f0c5ca908f5619664a1d6eeb4f77b0f7e8be0d11 Mon Sep 17 00:00:00 2001 From: Christian Schlotter Date: Wed, 30 Aug 2023 16:00:00 +0200 Subject: [PATCH 2/6] Implement unit and integration tests --- .../generator/generate_integration_test.go | 97 +++++++++++++ .../generator/testdata/foo-config.yaml | 84 +++++++++++ .../generate/generator/testdata/foo_types.go | 66 +++++++++ .../generate/markers/helper_test.go | 123 ++++++++++++++++ .../generate/markers/labelfrompath.go | 10 +- .../generate/markers/labelfrompath_test.go | 132 ++++++++++++++++++ .../generate/markers/metric_gauge_test.go | 59 ++++++++ .../generate/markers/metric_info_test.go | 56 ++++++++ .../generate/markers/metric_stateset_test.go | 64 +++++++++ 9 files changed, 689 insertions(+), 2 deletions(-) create mode 100644 pkg/customresourcestate/generate/generator/generate_integration_test.go create mode 100644 pkg/customresourcestate/generate/generator/testdata/foo-config.yaml create mode 100644 pkg/customresourcestate/generate/generator/testdata/foo_types.go create mode 100644 pkg/customresourcestate/generate/markers/helper_test.go create mode 100644 pkg/customresourcestate/generate/markers/labelfrompath_test.go create mode 100644 pkg/customresourcestate/generate/markers/metric_gauge_test.go create mode 100644 pkg/customresourcestate/generate/markers/metric_info_test.go create mode 100644 pkg/customresourcestate/generate/markers/metric_stateset_test.go diff --git a/pkg/customresourcestate/generate/generator/generate_integration_test.go b/pkg/customresourcestate/generate/generator/generate_integration_test.go new file mode 100644 index 0000000000..5291110d16 --- /dev/null +++ b/pkg/customresourcestate/generate/generator/generate_integration_test.go @@ -0,0 +1,97 @@ +/* +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 ( + "bytes" + "io" + "os" + "path" + "testing" + + "sigs.k8s.io/controller-tools/pkg/genall" + "sigs.k8s.io/controller-tools/pkg/loader" + "sigs.k8s.io/controller-tools/pkg/markers" +) + +func Test_Generate(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Error(err) + } + + optionsRegistry := &markers.Registry{} + + metricGenerator := CustomResourceConfigGenerator{} + if err := metricGenerator.RegisterMarkers(optionsRegistry); err != nil { + t.Error(err) + } + + out := &outputRule{ + buf: &bytes.Buffer{}, + } + + // Load the passed packages as roots. + roots, err := loader.LoadRoots(path.Join(cwd, "testdata", "...")) + if err != nil { + t.Errorf("loading packages %v", err) + } + + gen := CustomResourceConfigGenerator{} + + generationContext := &genall.GenerationContext{ + Collector: &markers.Collector{Registry: optionsRegistry}, + Roots: roots, + Checker: &loader.TypeChecker{}, + OutputRule: out, + } + + t.Log("Trying to generate a custom resource configuration from the loaded packages") + + if err := gen.Generate(generationContext); err != nil { + t.Error(err) + } + output := out.buf.String() + + t.Log("Comparing output to testdata to check for regressions") + + expectedFile, err := os.ReadFile(path.Clean(path.Join(cwd, "testdata", "foo-config.yaml"))) + if err != nil { + t.Error(err) + } + + if string(expectedFile) != output { + t.Log("output:") + t.Log(output) + t.Error("Expected output to match file testdata/foo-config.yaml") + } +} + +type outputRule struct { + buf *bytes.Buffer +} + +func (o *outputRule) Open(_ *loader.Package, _ string) (io.WriteCloser, error) { + return nopCloser{o.buf}, nil +} + +type nopCloser struct { + io.Writer +} + +func (n nopCloser) Close() error { + return nil +} diff --git a/pkg/customresourcestate/generate/generator/testdata/foo-config.yaml b/pkg/customresourcestate/generate/generator/testdata/foo-config.yaml new file mode 100644 index 0000000000..0d71e69469 --- /dev/null +++ b/pkg/customresourcestate/generate/generator/testdata/foo-config.yaml @@ -0,0 +1,84 @@ +--- +kind: CustomResourceStateMetrics +spec: + resources: + - errorLogV: 0 + groupVersionKind: + group: bar.example.com + kind: Foo + version: foo + labelsFromPath: + cluster_name: + - metadata + - labels + - cluster.x-k8s.io/cluster-name + name: + - metadata + - name + metricNamePrefix: foo + metrics: + - each: + gauge: + labelFromKey: "" + nilIsZero: false + path: + - metadata + - creationTimestamp + valueFrom: null + type: Gauge + help: Unix creation timestamp. + name: created + - each: + info: + labelFromKey: "" + labelsFromPath: + owner_is_controller: + - controller + owner_kind: + - kind + owner_name: + - name + owner_uid: + - uid + path: + - metadata + - ownerReferences + type: Info + help: Owner references. + name: owner + - each: + stateSet: + labelName: status + labelsFromPath: + type: + - type + list: + - "True" + - "False" + - Unknown + path: + - status + - conditions + valueFrom: + - status + type: StateSet + help: The condition of a foo. + name: status_condition + - each: + gauge: + labelFromKey: "" + labelsFromPath: + status: + - status + type: + - type + nilIsZero: false + path: + - status + - conditions + valueFrom: + - lastTransitionTime + type: Gauge + help: The condition last transition time of a foo. + name: status_condition_last_transition_time + resourcePlural: "" diff --git a/pkg/customresourcestate/generate/generator/testdata/foo_types.go b/pkg/customresourcestate/generate/generator/testdata/foo_types.go new file mode 100644 index 0000000000..b193e1ace3 --- /dev/null +++ b/pkg/customresourcestate/generate/generator/testdata/foo_types.go @@ -0,0 +1,66 @@ +/* +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. +*/ + +//go:generate sh -c "go run ../../../../../ generate ./... > foo-config.yaml" + +// +groupName=bar.example.com +package foo + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// FooSpec is the spec of Foo. +type FooSpec struct { + // This tests that defaulted fields are stripped for v1beta1, + // but not for v1 + DefaultedString string `json:"defaultedString"` +} + +// FooStatus is the status of Foo. +type FooStatus struct { + // +Metrics:stateset:name="status_condition",help="The condition of a foo.",labelName="status",JSONPath=".status",list={"True","False","Unknown"},labelsFromPath={"type":".type"} + // +Metrics:gauge:name="status_condition_last_transition_time",help="The condition last transition time of a foo.",valueFrom=.lastTransitionTime,labelsFromPath={"type":".type","status":".status"} + Conditions Condition `json:"conditions,omitempty"` +} + +// Foo is a test object. +// +Metrics:gvk:namePrefix="foo" +// +Metrics:labelFromPath:name="name",JSONPath=".metadata.name" +// +Metrics:gauge:name="created",JSONPath=".metadata.creationTimestamp",help="Unix creation timestamp." +// +Metrics:info:name="owner",JSONPath=".metadata.ownerReferences",help="Owner references.",labelsFromPath={owner_is_controller:".controller",owner_kind:".kind",owner_name:".name",owner_uid:".uid"} +// +Metrics:labelFromPath:name="cluster_name",JSONPath=.metadata.labels.cluster\.x-k8s\.io/cluster-name +type Foo struct { + // TypeMeta comments should NOT appear in the CRD spec + metav1.TypeMeta `json:",inline"` + // ObjectMeta comments should NOT appear in the CRD spec + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec comments SHOULD appear in the CRD spec + Spec FooSpec `json:"spec,omitempty"` + // Status comments SHOULD appear in the CRD spec + Status FooStatus `json:"status,omitempty"` +} + +// Condition is a test condition. +type Condition struct { + // Type of condition. + Type string `json:"type"` + // Status of condition. + Status string `json:"status"` + // LastTransitionTime of condition. + LastTransitionTime metav1.Time `json:"lastTransitionTime"` +} diff --git a/pkg/customresourcestate/generate/markers/helper_test.go b/pkg/customresourcestate/generate/markers/helper_test.go new file mode 100644 index 0000000000..09a52df5de --- /dev/null +++ b/pkg/customresourcestate/generate/markers/helper_test.go @@ -0,0 +1,123 @@ +/* +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 ( + "reflect" + "testing" + + "k8s.io/kube-state-metrics/v2/pkg/customresourcestate" +) + +func Test_jsonPath_Parse(t *testing.T) { + tests := []struct { + name string + j jsonPath + want []string + wantErr bool + }{ + { + name: "empty input", + j: "", + want: []string{}, + wantErr: false, + }, + { + name: "dot input", + j: ".", + want: []string{""}, + wantErr: false, + }, + { + name: "some path input", + j: ".foo.bar", + want: []string{"foo", "bar"}, + wantErr: false, + }, + { + name: "invalid character ,", + j: ".foo,.bar", + wantErr: true, + }, + { + name: "invalid closure", + j: "{.foo}", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.j.Parse() + if (err != nil) != tt.wantErr { + t.Errorf("jsonPath.Parse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("jsonPath.Parse() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_newMetricMeta(t *testing.T) { + tests := []struct { + name string + basePath []string + j jsonPath + jsonLabelsFromPath map[string]jsonPath + want customresourcestate.MetricMeta + }{ + { + name: "with basePath and jsonpath, without jsonLabelsFromPath", + basePath: []string{"foo"}, + j: jsonPath(".bar"), + jsonLabelsFromPath: map[string]jsonPath{}, + want: customresourcestate.MetricMeta{ + Path: []string{"foo", "bar"}, + LabelsFromPath: map[string][]string{}, + }, + }, + { + name: "with basePath, jsonpath and jsonLabelsFromPath", + basePath: []string{"foo"}, + j: jsonPath(".bar"), + jsonLabelsFromPath: map[string]jsonPath{"some": ".label.from.path"}, + want: customresourcestate.MetricMeta{ + Path: []string{"foo", "bar"}, + LabelsFromPath: map[string][]string{ + "some": {"label", "from", "path"}, + }, + }, + }, + { + name: "no basePath, jsonpath and jsonLabelsFromPath", + basePath: []string{}, + j: jsonPath(""), + jsonLabelsFromPath: map[string]jsonPath{}, + want: customresourcestate.MetricMeta{ + Path: []string{}, + LabelsFromPath: map[string][]string{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := newMetricMeta(tt.basePath, tt.j, tt.jsonLabelsFromPath); !reflect.DeepEqual(got, tt.want) { + t.Errorf("newMetricMeta() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/customresourcestate/generate/markers/labelfrompath.go b/pkg/customresourcestate/generate/markers/labelfrompath.go index 76c1eaeb87..215ebb0582 100644 --- a/pkg/customresourcestate/generate/markers/labelfrompath.go +++ b/pkg/customresourcestate/generate/markers/labelfrompath.go @@ -16,6 +16,7 @@ limitations under the License. package markers import ( + "errors" "fmt" "sigs.k8s.io/controller-tools/pkg/markers" @@ -59,14 +60,19 @@ func (labelFromPathMarker) Help() *markers.DefinitionHelp { } func (n labelFromPathMarker) ApplyToResource(resource *customresourcestate.Resource) error { - if resource.LabelsFromPath == nil { - resource.LabelsFromPath = map[string][]string{} + if resource == nil { + return errors.New("expected resource to not be nil") } + jsonPathElems, err := n.JSONPath.Parse() if err != nil { return err } + if resource.LabelsFromPath == nil { + resource.LabelsFromPath = map[string][]string{} + } + if jsonPath, labelExists := resource.LabelsFromPath[n.Name]; labelExists { if len(jsonPathElems) != len(jsonPath) { return fmt.Errorf("duplicate definition for label %q", n.Name) diff --git a/pkg/customresourcestate/generate/markers/labelfrompath_test.go b/pkg/customresourcestate/generate/markers/labelfrompath_test.go new file mode 100644 index 0000000000..4736b7eb05 --- /dev/null +++ b/pkg/customresourcestate/generate/markers/labelfrompath_test.go @@ -0,0 +1,132 @@ +/* +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 ( + "reflect" + "testing" + + "k8s.io/kube-state-metrics/v2/pkg/customresourcestate" +) + +func Test_labelFromPathMarker_ApplyToResource(t *testing.T) { + type fields struct { + Name string + JSONPath jsonPath + } + tests := []struct { + name string + fields fields + resource *customresourcestate.Resource + wantResource *customresourcestate.Resource + wantErr bool + }{ + { + name: "happy path", + fields: fields{ + Name: "foo", + JSONPath: ".bar", + }, + resource: &customresourcestate.Resource{}, + wantResource: &customresourcestate.Resource{ + Labels: customresourcestate.Labels{ + LabelsFromPath: map[string][]string{ + "foo": {"bar"}, + }, + }, + }, + wantErr: false, + }, + { + name: "label already exists with same path length", + fields: fields{ + Name: "foo", + JSONPath: ".bar", + }, + resource: &customresourcestate.Resource{ + Labels: customresourcestate.Labels{ + LabelsFromPath: map[string][]string{ + "foo": {"other"}, + }, + }, + }, + wantResource: &customresourcestate.Resource{ + Labels: customresourcestate.Labels{ + LabelsFromPath: map[string][]string{ + "foo": {"other"}, + }, + }, + }, + wantErr: true, + }, + { + name: "label already exists with different path length", + fields: fields{ + Name: "foo", + JSONPath: ".bar", + }, + resource: &customresourcestate.Resource{ + Labels: customresourcestate.Labels{ + LabelsFromPath: map[string][]string{ + "foo": {"other", "path"}, + }, + }, + }, + wantResource: &customresourcestate.Resource{ + Labels: customresourcestate.Labels{ + LabelsFromPath: map[string][]string{ + "foo": {"other", "path"}, + }, + }, + }, + wantErr: true, + }, + { + name: "invalid json path", + fields: fields{ + Name: "foo", + JSONPath: "{.bar}", + }, + resource: &customresourcestate.Resource{}, + wantResource: &customresourcestate.Resource{}, + wantErr: true, + }, + { + name: "nil resource", + fields: fields{ + Name: "foo", + JSONPath: "{.bar}", + }, + resource: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := labelFromPathMarker{ + Name: tt.fields.Name, + JSONPath: tt.fields.JSONPath, + } + if err := n.ApplyToResource(tt.resource); (err != nil) != tt.wantErr { + t.Errorf("labelFromPathMarker.ApplyToResource() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(tt.resource, tt.wantResource) { + t.Errorf("labelFromPathMarker.ApplyToResource() = %v, want %v", tt.resource, tt.wantResource) + } + + }) + } +} diff --git a/pkg/customresourcestate/generate/markers/metric_gauge_test.go b/pkg/customresourcestate/generate/markers/metric_gauge_test.go new file mode 100644 index 0000000000..2d44dc371b --- /dev/null +++ b/pkg/customresourcestate/generate/markers/metric_gauge_test.go @@ -0,0 +1,59 @@ +/* +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 ( + "reflect" + "testing" + + "k8s.io/kube-state-metrics/v2/pkg/customresourcestate" +) + +func Test_gaugeMarker_ToGenerator(t *testing.T) { + tests := []struct { + name string + gaugeMarker gaugeMarker + basePath []string + want *customresourcestate.Generator + }{ + { + name: "Happy path", + gaugeMarker: gaugeMarker{ + ValueFrom: jsonPathPointer(".foo"), + }, + basePath: []string{}, + want: &customresourcestate.Generator{ + Each: customresourcestate.Metric{ + Type: customresourcestate.MetricTypeGauge, + Gauge: &customresourcestate.MetricGauge{ + MetricMeta: customresourcestate.MetricMeta{ + LabelsFromPath: map[string][]string{}, + Path: []string{}, + }, + ValueFrom: []string{"foo"}, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.gaugeMarker.ToGenerator(tt.basePath...); !reflect.DeepEqual(got, tt.want) { + t.Errorf("gaugeMarker.ToGenerator() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/customresourcestate/generate/markers/metric_info_test.go b/pkg/customresourcestate/generate/markers/metric_info_test.go new file mode 100644 index 0000000000..f32761765d --- /dev/null +++ b/pkg/customresourcestate/generate/markers/metric_info_test.go @@ -0,0 +1,56 @@ +/* +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 ( + "reflect" + "testing" + + "k8s.io/kube-state-metrics/v2/pkg/customresourcestate" +) + +func Test_infoMarker_ToGenerator(t *testing.T) { + tests := []struct { + name string + infoMarker infoMarker + basePath []string + want *customresourcestate.Generator + }{ + { + name: "Happy path", + infoMarker: infoMarker{}, + basePath: []string{}, + want: &customresourcestate.Generator{ + Each: customresourcestate.Metric{ + Type: customresourcestate.MetricTypeInfo, + Info: &customresourcestate.MetricInfo{ + MetricMeta: customresourcestate.MetricMeta{ + LabelsFromPath: map[string][]string{}, + Path: []string{}, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.infoMarker.ToGenerator(tt.basePath...); !reflect.DeepEqual(got, tt.want) { + t.Errorf("infoMarker.ToGenerator() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/customresourcestate/generate/markers/metric_stateset_test.go b/pkg/customresourcestate/generate/markers/metric_stateset_test.go new file mode 100644 index 0000000000..2e04aee65a --- /dev/null +++ b/pkg/customresourcestate/generate/markers/metric_stateset_test.go @@ -0,0 +1,64 @@ +/* +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 ( + "reflect" + "testing" + + "k8s.io/kube-state-metrics/v2/pkg/customresourcestate" +) + +func Test_stateSetMarker_ToGenerator(t *testing.T) { + tests := []struct { + name string + stateSetMarker stateSetMarker + basePath []string + want *customresourcestate.Generator + }{ + { + name: "Happy path", + stateSetMarker: stateSetMarker{ + JSONPath: jsonPathPointer(".foo"), + }, + basePath: []string{}, + want: &customresourcestate.Generator{ + Each: customresourcestate.Metric{ + Type: customresourcestate.MetricTypeStateSet, + StateSet: &customresourcestate.MetricStateSet{ + MetricMeta: customresourcestate.MetricMeta{ + LabelsFromPath: map[string][]string{}, + Path: []string{}, + }, + ValueFrom: []string{"foo"}, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.stateSetMarker.ToGenerator(tt.basePath...); !reflect.DeepEqual(got, tt.want) { + t.Errorf("stateSetMarker.ToGenerator() = %v, want %v", got, tt.want) + } + }) + } +} + +func jsonPathPointer(s string) *jsonPath { + j := jsonPath(s) + return &j +} From 436f87ffe91614542910c06fb0c7b5b16bf6f9a1 Mon Sep 17 00:00:00 2001 From: Christian Schlotter Date: Thu, 12 Oct 2023 10:13:46 +0200 Subject: [PATCH 3/6] review fixes --- docs/cli-arguments.md | 2 +- pkg/customresourcestate/generate/cmd.go | 2 +- .../generate/generator/generate_integration_test.go | 10 ++++++++-- .../generate/generator/testdata/foo_types.go | 5 +++++ pkg/customresourcestate/generate/markers/gvk.go | 4 +++- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/docs/cli-arguments.md b/docs/cli-arguments.md index 6a5c5b2376..56f29ace14 100644 --- a/docs/cli-arguments.md +++ b/docs/cli-arguments.md @@ -34,7 +34,7 @@ Usage: Available Commands: completion Generate completion script for kube-state-metrics. - generate Generate custom resource metrics configuration from go-code markers. + generate Generate custom resource metrics configuration from go-code markers (experimental). help Help about any command version Print version information. diff --git a/pkg/customresourcestate/generate/cmd.go b/pkg/customresourcestate/generate/cmd.go index 3fb5e30ff5..3e291c0216 100644 --- a/pkg/customresourcestate/generate/cmd.go +++ b/pkg/customresourcestate/generate/cmd.go @@ -44,7 +44,7 @@ var ( // 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.", + Short: "Generate custom resource metrics configuration from go-code markers (experimental).", DisableFlagsInUseLine: true, Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/pkg/customresourcestate/generate/generator/generate_integration_test.go b/pkg/customresourcestate/generate/generator/generate_integration_test.go index 5291110d16..cd73a62df9 100644 --- a/pkg/customresourcestate/generate/generator/generate_integration_test.go +++ b/pkg/customresourcestate/generate/generator/generate_integration_test.go @@ -22,6 +22,7 @@ import ( "path" "testing" + "github.com/google/go-cmp/cmp" "sigs.k8s.io/controller-tools/pkg/genall" "sigs.k8s.io/controller-tools/pkg/loader" "sigs.k8s.io/controller-tools/pkg/markers" @@ -73,10 +74,15 @@ func Test_Generate(t *testing.T) { t.Error(err) } - if string(expectedFile) != output { + diff := cmp.Diff(string(expectedFile), output) + if diff != "" { t.Log("output:") t.Log(output) - t.Error("Expected output to match file testdata/foo-config.yaml") + t.Log("diff:") + t.Log(diff) + t.Log("Expected output to match file `testdata/foo-config.yaml` but it does not.") + t.Log("If the change is intended, use `go generate ./pkg/customresourcestate/generate/generator/testdata` to regenerate the `testdata/foo-config.yaml` file.") + t.Error("Detected a diff between the output of the integration test and the file `testdata/foo-config.yaml`.") } } diff --git a/pkg/customresourcestate/generate/generator/testdata/foo_types.go b/pkg/customresourcestate/generate/generator/testdata/foo_types.go index b193e1ace3..042869e079 100644 --- a/pkg/customresourcestate/generate/generator/testdata/foo_types.go +++ b/pkg/customresourcestate/generate/generator/testdata/foo_types.go @@ -14,6 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Changes to this file may require to regenerate the `foo-config.yaml`. Otherwise the +// tests in ../generate_integration_test.go may fail. +// The below marker can be used to regenerate the `foo-config.yaml` file by running +// the following command: +// $ go generate ./pkg/customresourcestate/generate/generator/testdata //go:generate sh -c "go run ../../../../../ generate ./... > foo-config.yaml" // +groupName=bar.example.com diff --git a/pkg/customresourcestate/generate/markers/gvk.go b/pkg/customresourcestate/generate/markers/gvk.go index e51f45a5a2..26dc1f4aed 100644 --- a/pkg/customresourcestate/generate/markers/gvk.go +++ b/pkg/customresourcestate/generate/markers/gvk.go @@ -55,6 +55,8 @@ func (gvkMarker) Help() *markers.DefinitionHelp { } func (n gvkMarker) ApplyToResource(resource *customresourcestate.Resource) error { - resource.MetricNamePrefix = &n.NamePrefix + if n.NamePrefix != "" { + resource.MetricNamePrefix = &n.NamePrefix + } return nil } From 732f8012affdcf27ac0f79da56bb0b7a911f2064 Mon Sep 17 00:00:00 2001 From: Christian Schlotter Date: Tue, 17 Oct 2023 14:36:39 +0200 Subject: [PATCH 4/6] crd testdata: add example files and description --- .../generate/generator/testdata/README.md | 55 +++++++++++++++++++ .../testdata/foo-cr-example-metrics.txt | 18 ++++++ .../generator/testdata/foo-cr-example.yaml | 21 +++++++ .../generate/generator/testdata/foo_types.go | 8 +-- 4 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 pkg/customresourcestate/generate/generator/testdata/README.md create mode 100644 pkg/customresourcestate/generate/generator/testdata/foo-cr-example-metrics.txt create mode 100644 pkg/customresourcestate/generate/generator/testdata/foo-cr-example.yaml diff --git a/pkg/customresourcestate/generate/generator/testdata/README.md b/pkg/customresourcestate/generate/generator/testdata/README.md new file mode 100644 index 0000000000..45b7300b72 --- /dev/null +++ b/pkg/customresourcestate/generate/generator/testdata/README.md @@ -0,0 +1,55 @@ +# Testdata for generator tests + +The files in this directory are used for testing the `kube-state-metrics generate` command and to provide an example. + +## foo-config.yaml + +This file is used in the test at [generate_integration_test.go](../generate_integration_test.go) to verify that the resulting configuration does not change during changes in the codebase. + +If there are intended changes this file needs to get regenerated to make the test succeed again. +This could be done via: + +```sh +go run generate \ + ./pkg/customresourcestate/generate/generator/testdata/ \ + > ./pkg/customresourcestate/generate/generator/testdata/foo-config.yaml +``` + +Or by using the go:generate marker inside [foo_types.go](foo_types.go): + +```sh +go generate ./pkg/customresourcestate/generate/generator/testdata/ +``` + +## Example files: foo-cr-example.yaml and foo-cr-example-metrics.txt + +There is also an example CR ([foo-cr-example.yaml](foo-cr-example.yaml)) and resulting example metrics ([foo-cr-example-metrics.txt](foo-cr-example-metrics.txt)). + +The example metrics file got created by: + +1. Generating a CustomResourceDefinition yaml by using [controller-gen](https://github.com/kubernetes-sigs/kubebuilder/blob/master/docs/book/src/reference/controller-gen.md): + + ```sh + controller-gen crd paths=./pkg/customresourcestate/generate/generator/testdata/ output:dir=./pkg/customresourcestate/generate/generator/testdata/ + ``` + +2. Creating a cluster using [kind](https://kind.sigs.k8s.io/) +3. Applying the CRD and example CR to the cluster: + + ```sh + kubectl apply -f /pkg/customresourcestate/generate/generator/testdata/bar.example.com_foos.yaml + kubectl apply -f /pkg/customresourcestate/generate/generator/testdata/foo-cr-example.yaml + ``` + +4. Running kube-state-metrics with the provided configuration file: + + ```sh + go run ./ --kubeconfig $HOME/.kube/config --custom-resource-state-only \ + --custom-resource-state-config-file pkg/customresourcestate/generate/generator/testdata/foo-config.yaml + ``` + +5. Querying the metrics endpoint in a second terminal: + + ```sh + curl localhost:8080/metrics > ./pkg/customresourcestate/generate/generator/testdata/foo-cr-example-metrics.txt + ``` diff --git a/pkg/customresourcestate/generate/generator/testdata/foo-cr-example-metrics.txt b/pkg/customresourcestate/generate/generator/testdata/foo-cr-example-metrics.txt new file mode 100644 index 0000000000..52c4e321e6 --- /dev/null +++ b/pkg/customresourcestate/generate/generator/testdata/foo-cr-example-metrics.txt @@ -0,0 +1,18 @@ +# HELP foo_created Unix creation timestamp. +# TYPE foo_created gauge +foo_created{customresource_group="bar.example.com",customresource_kind="Foo",customresource_version="foo",name="bar"} 1.697543739e+09 +# HELP foo_owner Owner references. +# TYPE foo_owner info +foo_owner{customresource_group="bar.example.com",customresource_kind="Foo",customresource_version="foo",name="bar",owner_is_controller="true",owner_kind="foo",owner_name="foo",owner_uid="someuid"} 1 +# HELP foo_status_condition The condition of a foo. +# TYPE foo_status_condition stateset +foo_status_condition{customresource_group="bar.example.com",customresource_kind="Foo",customresource_version="foo",name="bar",status="False",type="AnotherType"} 1 +foo_status_condition{customresource_group="bar.example.com",customresource_kind="Foo",customresource_version="foo",name="bar",status="False",type="SomeType"} 0 +foo_status_condition{customresource_group="bar.example.com",customresource_kind="Foo",customresource_version="foo",name="bar",status="True",type="AnotherType"} 0 +foo_status_condition{customresource_group="bar.example.com",customresource_kind="Foo",customresource_version="foo",name="bar",status="True",type="SomeType"} 1 +foo_status_condition{customresource_group="bar.example.com",customresource_kind="Foo",customresource_version="foo",name="bar",status="Unknown",type="AnotherType"} 0 +foo_status_condition{customresource_group="bar.example.com",customresource_kind="Foo",customresource_version="foo",name="bar",status="Unknown",type="SomeType"} 0 +# HELP foo_status_condition_last_transition_time The condition last transition time of a foo. +# TYPE foo_status_condition_last_transition_time gauge +foo_status_condition_last_transition_time{customresource_group="bar.example.com",customresource_kind="Foo",customresource_version="foo",name="bar",status="False",type="AnotherType"} 1.697119142e+09 +foo_status_condition_last_transition_time{customresource_group="bar.example.com",customresource_kind="Foo",customresource_version="foo",name="bar",status="True",type="SomeType"} 1.697119142e+09 diff --git a/pkg/customresourcestate/generate/generator/testdata/foo-cr-example.yaml b/pkg/customresourcestate/generate/generator/testdata/foo-cr-example.yaml new file mode 100644 index 0000000000..bcf1243efe --- /dev/null +++ b/pkg/customresourcestate/generate/generator/testdata/foo-cr-example.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: bar.example.com/foo +kind: Foo +metadata: + name: bar + ownerReferences: + - apiVersion: v1 + kind: foo + controller: true + name: foo + uid: someuid +spec: + someString: test +status: + conditions: + - lastTransitionTime: "2023-10-12T13:59:02Z" + status: "True" + type: SomeType + - lastTransitionTime: "2023-10-12T13:59:02Z" + status: "False" + type: AnotherType diff --git a/pkg/customresourcestate/generate/generator/testdata/foo_types.go b/pkg/customresourcestate/generate/generator/testdata/foo_types.go index 042869e079..cac4477845 100644 --- a/pkg/customresourcestate/generate/generator/testdata/foo_types.go +++ b/pkg/customresourcestate/generate/generator/testdata/foo_types.go @@ -20,6 +20,7 @@ limitations under the License. // the following command: // $ go generate ./pkg/customresourcestate/generate/generator/testdata //go:generate sh -c "go run ../../../../../ generate ./... > foo-config.yaml" +//go:generate sh -c "controller-gen crd paths=./ output:stdout > foo-crd.yaml" // +groupName=bar.example.com package foo @@ -30,16 +31,15 @@ import ( // FooSpec is the spec of Foo. type FooSpec struct { - // This tests that defaulted fields are stripped for v1beta1, - // but not for v1 - DefaultedString string `json:"defaultedString"` + // SomeString is a string. + SomeString string `json:"someString"` } // FooStatus is the status of Foo. type FooStatus struct { // +Metrics:stateset:name="status_condition",help="The condition of a foo.",labelName="status",JSONPath=".status",list={"True","False","Unknown"},labelsFromPath={"type":".type"} // +Metrics:gauge:name="status_condition_last_transition_time",help="The condition last transition time of a foo.",valueFrom=.lastTransitionTime,labelsFromPath={"type":".type","status":".status"} - Conditions Condition `json:"conditions,omitempty"` + Conditions []Condition `json:"conditions,omitempty"` } // Foo is a test object. From 7b6eea43e04bd3920b360aa134584a170c342ad3 Mon Sep 17 00:00:00 2001 From: Christian Schlotter Date: Wed, 18 Oct 2023 11:15:53 +0200 Subject: [PATCH 5/6] fix -w to output markers without additional argument --- pkg/customresourcestate/generate/cmd.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/customresourcestate/generate/cmd.go b/pkg/customresourcestate/generate/cmd.go index 3e291c0216..eee0e42f4e 100644 --- a/pkg/customresourcestate/generate/cmd.go +++ b/pkg/customresourcestate/generate/cmd.go @@ -46,13 +46,16 @@ var GenerateCommand = &cobra.Command{ Use: "generate [flags] /path/to/package [/path/to/package]", Short: "Generate custom resource metrics configuration from go-code markers (experimental).", DisableFlagsInUseLine: true, - Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if generateWhichMarkersFlag { PrintMarkerDocs() return nil } + if len(args) == 0 { + return fmt.Errorf("requires at least 1 arg(s), only received %d", len(args)) + } + // 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{} From a523a3dfdaa97491075bd2c28258a9c63c4379ca Mon Sep 17 00:00:00 2001 From: Christian Schlotter Date: Wed, 6 Dec 2023 08:50:57 +0100 Subject: [PATCH 6/6] review fixes --- pkg/customresourcestate/generate/cmd.go | 4 ++-- pkg/customresourcestate/generate/generator/generator.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/customresourcestate/generate/cmd.go b/pkg/customresourcestate/generate/cmd.go index eee0e42f4e..e84a7e9b20 100644 --- a/pkg/customresourcestate/generate/cmd.go +++ b/pkg/customresourcestate/generate/cmd.go @@ -1,5 +1,5 @@ /* -Copyright 2022 The Kubernetes Authors All rights reserved. +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. @@ -53,7 +53,7 @@ var GenerateCommand = &cobra.Command{ } if len(args) == 0 { - return fmt.Errorf("requires at least 1 arg(s), only received %d", len(args)) + return fmt.Errorf("requires at least 1 package argument") } // Register the metric generator itself as marker so genall.FromOptions is able to initialize the runtime properly. diff --git a/pkg/customresourcestate/generate/generator/generator.go b/pkg/customresourcestate/generate/generator/generator.go index 15ac07ac9c..16610c947f 100644 --- a/pkg/customresourcestate/generate/generator/generator.go +++ b/pkg/customresourcestate/generate/generator/generator.go @@ -113,9 +113,9 @@ func (g CustomResourceConfigGenerator) Generate(ctx *genall.GenerationContext) e }) // 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) + virtualFilePath := "metrics.yaml" + if err := ctx.WriteYAML(virtualFilePath, "", []interface{}{metrics}, genall.WithTransform(addCustomResourceStateKind)); err != nil { + return fmt.Errorf("WriteYAML to %s: %w", virtualFilePath, err) } return nil