Skip to content

Commit e37da20

Browse files
committed
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.
1 parent 3a7e617 commit e37da20

File tree

11 files changed

+1216
-7
lines changed

11 files changed

+1216
-7
lines changed

exp/metric-gen/go.mod

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
module k8s.io/kube-state-metrics/exp/metric-gen
2+
3+
go 1.19
4+
5+
replace k8s.io/kube-state-metrics/v2 => ../..
6+
7+
require (
8+
github.com/spf13/cobra v1.6.1
9+
k8s.io/apimachinery v0.26.0
10+
k8s.io/klog/v2 v2.80.1
11+
k8s.io/kube-state-metrics/v2 v2.0.0-00010101000000-000000000000
12+
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448
13+
sigs.k8s.io/controller-tools v0.11.1
14+
)
15+
16+
require (
17+
github.com/beorn7/perks v1.0.1 // indirect
18+
github.com/blang/semver/v4 v4.0.0 // indirect
19+
github.com/cespare/xxhash/v2 v2.1.2 // indirect
20+
github.com/davecgh/go-spew v1.1.1 // indirect
21+
github.com/fatih/color v1.13.0 // indirect
22+
github.com/go-logr/logr v1.2.3 // indirect
23+
github.com/gobuffalo/flect v0.3.0 // indirect
24+
github.com/gogo/protobuf v1.3.2 // indirect
25+
github.com/golang/protobuf v1.5.2 // indirect
26+
github.com/google/go-cmp v0.5.9 // indirect
27+
github.com/google/gofuzz v1.1.0 // indirect
28+
github.com/inconshreveable/mousetrap v1.0.1 // indirect
29+
github.com/json-iterator/go v1.1.12 // indirect
30+
github.com/mattn/go-colorable v0.1.9 // indirect
31+
github.com/mattn/go-isatty v0.0.14 // indirect
32+
github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect
33+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
34+
github.com/modern-go/reflect2 v1.0.2 // indirect
35+
github.com/prometheus/client_golang v1.14.0 // indirect
36+
github.com/prometheus/client_model v0.3.0 // indirect
37+
github.com/prometheus/common v0.38.0 // indirect
38+
github.com/prometheus/procfs v0.8.0 // indirect
39+
github.com/rogpeppe/go-internal v1.9.0 // indirect
40+
github.com/spf13/pflag v1.0.5 // indirect
41+
golang.org/x/mod v0.7.0 // indirect
42+
golang.org/x/net v0.4.0 // indirect
43+
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
44+
golang.org/x/sys v0.3.0 // indirect
45+
golang.org/x/term v0.3.0 // indirect
46+
golang.org/x/text v0.5.0 // indirect
47+
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
48+
golang.org/x/tools v0.4.0 // indirect
49+
google.golang.org/appengine v1.6.7 // indirect
50+
google.golang.org/protobuf v1.28.1 // indirect
51+
gopkg.in/inf.v0 v0.9.1 // indirect
52+
gopkg.in/yaml.v2 v2.4.0 // indirect
53+
k8s.io/apiextensions-apiserver v0.26.0 // indirect
54+
k8s.io/client-go v0.26.0 // indirect
55+
k8s.io/component-base v0.26.0 // indirect
56+
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
57+
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
58+
sigs.k8s.io/yaml v1.3.0 // indirect
59+
)

exp/metric-gen/go.sum

+176
Large diffs are not rendered by default.

exp/metric-gen/main.go

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
Copyright 2023 The Kubernetes Authors All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package main
17+
18+
import (
19+
"fmt"
20+
"os"
21+
22+
"github.com/spf13/pflag"
23+
"k8s.io/kube-state-metrics/exp/metric-gen/metric"
24+
"sigs.k8s.io/controller-tools/pkg/genall"
25+
"sigs.k8s.io/controller-tools/pkg/genall/help"
26+
prettyhelp "sigs.k8s.io/controller-tools/pkg/genall/help/pretty"
27+
"sigs.k8s.io/controller-tools/pkg/loader"
28+
"sigs.k8s.io/controller-tools/pkg/markers"
29+
)
30+
31+
const (
32+
generatorName = "metric"
33+
)
34+
35+
var (
36+
// optionsRegistry contains all the marker definitions used to process command line options
37+
optionsRegistry = &markers.Registry{}
38+
)
39+
40+
func main() {
41+
var whichMarkersFlag bool
42+
43+
pflag.CommandLine.BoolVarP(&whichMarkersFlag, "which-markers", "w", false, "print out all markers available with the requested generators")
44+
45+
pflag.Usage = func() {
46+
fmt.Fprintf(os.Stderr, "Usage of %s:\n\n", os.Args[0])
47+
fmt.Fprintf(os.Stderr, " metric-gen [flags] /path/to/package [/path/to/package]\n\n")
48+
fmt.Fprintf(os.Stderr, "Flags:\n")
49+
pflag.PrintDefaults()
50+
fmt.Fprintf(os.Stderr, "\n")
51+
}
52+
53+
pflag.Parse()
54+
55+
// Register the metric generator itself as marker so genall.FromOptions is able to initialize the runtime properly.
56+
// This also registers the markers inside the optionsRegistry so its available to print the marker docs.
57+
metricGenerator := metric.Generator{}
58+
defn := markers.Must(markers.MakeDefinition(generatorName, markers.DescribesPackage, metricGenerator))
59+
if err := optionsRegistry.Register(defn); err != nil {
60+
panic(err)
61+
}
62+
63+
if whichMarkersFlag {
64+
printMarkerDocs()
65+
return
66+
}
67+
68+
// Check if package paths got passed as input parameters.
69+
if len(os.Args[1:]) == 0 {
70+
fmt.Fprint(os.Stderr, "error: Please provide package paths as parameters\n\n")
71+
pflag.Usage()
72+
os.Exit(1)
73+
}
74+
75+
// Load the passed packages as roots.
76+
roots, err := loader.LoadRoots(os.Args[1:]...)
77+
if err != nil {
78+
fmt.Fprint(os.Stderr, fmt.Sprintf("error: loading packages %v\n", err))
79+
os.Exit(1)
80+
}
81+
82+
// Set up the generator runtime using controller-tools and passing our optionsRegistry.
83+
rt, err := genall.FromOptions(optionsRegistry, []string{generatorName})
84+
if err != nil {
85+
fmt.Fprint(os.Stderr, fmt.Sprintf("error: %v\n", err))
86+
os.Exit(1)
87+
}
88+
89+
// Setup the generation context with the loaded roots.
90+
rt.GenerationContext.Roots = roots
91+
// Setup the runtime to output to stdout.
92+
rt.OutputRules = genall.OutputRules{Default: genall.OutputToStdout}
93+
94+
// Run the generator using the runtime.
95+
if hadErrs := rt.Run(); hadErrs {
96+
fmt.Fprint(os.Stderr, "generator did not run successfully\n")
97+
os.Exit(1)
98+
}
99+
}
100+
101+
// printMarkerDocs prints out marker help for the given generators specified in
102+
// the rawOptions
103+
func printMarkerDocs() error {
104+
// just grab a registry so we don't lag while trying to load roots
105+
// (like we'd do if we just constructed the full runtime).
106+
reg, err := genall.RegistryFromOptions(optionsRegistry, []string{generatorName})
107+
if err != nil {
108+
return err
109+
}
110+
111+
helpInfo := help.ByCategory(reg, help.SortByCategory)
112+
113+
for _, cat := range helpInfo {
114+
if cat.Category == "" {
115+
continue
116+
}
117+
contents := prettyhelp.MarkersDetails(false, cat.Category, cat.Markers)
118+
if err := contents.WriteTo(os.Stderr); err != nil {
119+
return err
120+
}
121+
}
122+
return nil
123+
}

exp/metric-gen/metric/generator.go

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
Copyright 2023 The Kubernetes Authors All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package metric
17+
18+
import (
19+
"fmt"
20+
"sort"
21+
22+
"k8s.io/klog/v2"
23+
"sigs.k8s.io/controller-tools/pkg/crd"
24+
"sigs.k8s.io/controller-tools/pkg/genall"
25+
"sigs.k8s.io/controller-tools/pkg/loader"
26+
"sigs.k8s.io/controller-tools/pkg/markers"
27+
28+
"k8s.io/kube-state-metrics/v2/pkg/customresourcestate"
29+
)
30+
31+
type Generator struct{}
32+
33+
func (Generator) CheckFilter() loader.NodeFilter {
34+
// Re-use controller-tools filter to filter out unrelated nodes that aren't used
35+
// in CRD generation, like interfaces and struct fields without JSON tag.
36+
return crd.Generator{}.CheckFilter()
37+
}
38+
39+
func (g Generator) Generate(ctx *genall.GenerationContext) error {
40+
// Create the parser which is specific to the metric generator.
41+
parser := newParser(
42+
&crd.Parser{
43+
Collector: ctx.Collector,
44+
Checker: ctx.Checker,
45+
},
46+
)
47+
48+
// Loop over all passed packages.
49+
for _, root := range ctx.Roots {
50+
// skip packages which don't import metav1 because they can't define a CRD without meta v1.
51+
metav1 := root.Imports()["k8s.io/apimachinery/pkg/apis/meta/v1"]
52+
if metav1 == nil {
53+
continue
54+
}
55+
56+
// parse the given package to feed crd.FindKubeKinds to find CRD objects.
57+
parser.NeedPackage(root)
58+
kubeKinds := crd.FindKubeKinds(parser.Parser, metav1)
59+
if len(kubeKinds) == 0 {
60+
klog.Fatalf("no objects in the roots")
61+
}
62+
63+
for _, gv := range kubeKinds {
64+
// Create customresourcestate.Resource for each CRD which contains all metric
65+
// definitions for the CRD.
66+
parser.NeedResourceFor(gv)
67+
}
68+
}
69+
70+
// Build customresourcestate configuration file from generated data.
71+
metrics := customresourcestate.Metrics{
72+
Spec: customresourcestate.MetricsSpec{
73+
Resources: []customresourcestate.Resource{},
74+
},
75+
}
76+
77+
// Sort the resources to get a deterministic output.
78+
79+
for _, resource := range parser.CustomResourceStates {
80+
if len(resource.Metrics) > 0 {
81+
// sort the metrics
82+
sort.Slice(resource.Metrics, func(i, j int) bool {
83+
return resource.Metrics[i].Name < resource.Metrics[j].Name
84+
})
85+
86+
metrics.Spec.Resources = append(metrics.Spec.Resources, resource)
87+
}
88+
}
89+
90+
sort.Slice(metrics.Spec.Resources, func(i, j int) bool {
91+
if metrics.Spec.Resources[i].MetricNamePrefix == nil && metrics.Spec.Resources[j].MetricNamePrefix == nil {
92+
a := metrics.Spec.Resources[i].GroupVersionKind.Group + "/" + metrics.Spec.Resources[i].GroupVersionKind.Version + "/" + metrics.Spec.Resources[i].GroupVersionKind.Kind
93+
b := metrics.Spec.Resources[j].GroupVersionKind.Group + "/" + metrics.Spec.Resources[j].GroupVersionKind.Version + "/" + metrics.Spec.Resources[j].GroupVersionKind.Kind
94+
return a < b
95+
}
96+
97+
// Either a or b will not be the empty string, so we can compare them.
98+
var a, b string
99+
if metrics.Spec.Resources[i].MetricNamePrefix == nil {
100+
a = *metrics.Spec.Resources[i].MetricNamePrefix
101+
}
102+
if metrics.Spec.Resources[j].MetricNamePrefix != nil {
103+
b = *metrics.Spec.Resources[j].MetricNamePrefix
104+
}
105+
return a < b
106+
})
107+
108+
// Write the rendered yaml to the context which will result in stdout.
109+
filePath := "metrics.yaml"
110+
if err := ctx.WriteYAML(filePath, []interface{}{metrics}, genall.WithTransform(addCustomResourceStateKind)); err != nil {
111+
return fmt.Errorf("WriteYAML to %s: %w", filePath, err)
112+
}
113+
114+
return nil
115+
}
116+
117+
// addCustomResourceStateKind adds the correct kind because we don't have a correct
118+
// kubernetes-style object as configuration definition.
119+
func addCustomResourceStateKind(obj map[string]interface{}) error {
120+
obj["kind"] = "CustomResourceStateMetrics"
121+
return nil
122+
}
123+
124+
func (g Generator) RegisterMarkers(into *markers.Registry) error {
125+
for _, m := range markerDefinitions {
126+
if err := m.Register(into); err != nil {
127+
return err
128+
}
129+
}
130+
131+
return nil
132+
}

exp/metric-gen/metric/marker_gauge.go

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
Copyright 2023 The Kubernetes Authors All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package metric
17+
18+
import (
19+
"sigs.k8s.io/controller-tools/pkg/markers"
20+
21+
"k8s.io/klog/v2"
22+
"k8s.io/kube-state-metrics/v2/pkg/customresourcestate"
23+
)
24+
25+
const (
26+
// GaugeMarkerName is a marker for defining metric definitions.
27+
GaugeMarkerName = "Metrics:gauge"
28+
)
29+
30+
func init() {
31+
markerDefinitions = append(
32+
markerDefinitions,
33+
must(markers.MakeDefinition(GaugeMarkerName, markers.DescribesField, GaugeMarker{})).
34+
help(GaugeMarker{}.help()),
35+
must(markers.MakeDefinition(GaugeMarkerName, markers.DescribesType, GaugeMarker{})).
36+
help(GaugeMarker{}.help()),
37+
)
38+
}
39+
40+
type GaugeMarker struct {
41+
Name string
42+
Help string `marker:"help,optional"`
43+
NilIsZero bool `marker:"nilIsZero,optional"`
44+
JSONPath JSONPath `marker:"JSONPath,optional"`
45+
LabelFromKey string `marker:"labelFromKey,optional"`
46+
}
47+
48+
func (GaugeMarker) help() *markers.DefinitionHelp {
49+
return &markers.DefinitionHelp{
50+
Category: "Metrics",
51+
DetailedHelp: markers.DetailedHelp{
52+
Summary: "Defines a Gauge metric and uses the implicit path to the field joined by the provided JSONPath as path for the metric configuration.",
53+
Details: "",
54+
},
55+
FieldHelp: map[string]markers.DetailedHelp{},
56+
}
57+
}
58+
59+
func (g GaugeMarker) ToGenerator(basePath ...string) *customresourcestate.Generator {
60+
valueFrom, err := g.JSONPath.Parse()
61+
if err != nil {
62+
klog.Fatal(err)
63+
}
64+
65+
path := append(basePath, valueFrom...)
66+
67+
return &customresourcestate.Generator{
68+
Name: g.Name,
69+
Help: g.Help,
70+
Each: customresourcestate.Metric{
71+
Type: customresourcestate.MetricTypeGauge,
72+
Gauge: &customresourcestate.MetricGauge{
73+
NilIsZero: g.NilIsZero,
74+
MetricMeta: customresourcestate.MetricMeta{
75+
Path: path,
76+
},
77+
LabelFromKey: g.LabelFromKey,
78+
ValueFrom: nil,
79+
},
80+
},
81+
}
82+
}

0 commit comments

Comments
 (0)