Skip to content

Commit 027e03d

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 027e03d

File tree

11 files changed

+1143
-7
lines changed

11 files changed

+1143
-7
lines changed

exp/metric-gen/go.mod

Lines changed: 59 additions & 0 deletions
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

Lines changed: 176 additions & 0 deletions
Large diffs are not rendered by default.

exp/metric-gen/main.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
Copyright 2022 The Kubernetes Authors.
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+
17+
package main
18+
19+
import (
20+
"fmt"
21+
"os"
22+
23+
"github.com/spf13/pflag"
24+
"k8s.io/kube-state-metrics/exp/metric-gen/metric"
25+
"sigs.k8s.io/controller-tools/pkg/genall"
26+
"sigs.k8s.io/controller-tools/pkg/genall/help"
27+
prettyhelp "sigs.k8s.io/controller-tools/pkg/genall/help/pretty"
28+
"sigs.k8s.io/controller-tools/pkg/loader"
29+
"sigs.k8s.io/controller-tools/pkg/markers"
30+
)
31+
32+
const (
33+
generatorName = "metric"
34+
)
35+
36+
var (
37+
// optionsRegistry contains all the marker definitions used to process command line options
38+
optionsRegistry = &markers.Registry{}
39+
)
40+
41+
func main() {
42+
var whichMarkersFlag bool
43+
44+
pflag.CommandLine.BoolVarP(&whichMarkersFlag, "which-markers", "w", false, "print out all markers available with the requested generators")
45+
46+
pflag.Usage = func() {
47+
fmt.Fprintf(os.Stderr, "Usage of %s:\n\n", os.Args[0])
48+
fmt.Fprintf(os.Stderr, " metric-gen [flags] /path/to/package [/path/to/package]\n\n")
49+
fmt.Fprintf(os.Stderr, "Flags:\n")
50+
pflag.PrintDefaults()
51+
fmt.Fprintf(os.Stderr, "\n")
52+
}
53+
54+
pflag.Parse()
55+
56+
// Register the metric generator itself as marker so genall.FromOptions is able to initialize the runtime properly.
57+
// This also registers the markers inside the optionsRegistry so its available to print the marker docs.
58+
metricGenerator := metric.Generator{}
59+
defn := markers.Must(markers.MakeDefinition(generatorName, markers.DescribesPackage, metricGenerator))
60+
if err := optionsRegistry.Register(defn); err != nil {
61+
panic(err)
62+
}
63+
64+
if whichMarkersFlag {
65+
printMarkerDocs()
66+
return
67+
}
68+
69+
// Check if package paths got passed as input parameters.
70+
if len(os.Args[1:]) == 0 {
71+
fmt.Fprint(os.Stderr, "error: Please provide package paths as parameters\n\n")
72+
pflag.Usage()
73+
os.Exit(1)
74+
}
75+
76+
// Load the passed packages as roots.
77+
roots, err := loader.LoadRoots(os.Args[1:]...)
78+
if err != nil {
79+
fmt.Fprint(os.Stderr, fmt.Sprintf("error: loading packages %v\n", err))
80+
os.Exit(1)
81+
}
82+
83+
// Set up the generator runtime using controller-tools and passing our optionsRegistry.
84+
rt, err := genall.FromOptions(optionsRegistry, []string{generatorName})
85+
if err != nil {
86+
fmt.Fprint(os.Stderr, fmt.Sprintf("error: %v\n", err))
87+
os.Exit(1)
88+
}
89+
90+
// Setup the generation context with the loaded roots.
91+
rt.GenerationContext.Roots = roots
92+
// Setup the runtime to output to stdout.
93+
rt.OutputRules = genall.OutputRules{Default: genall.OutputToStdout}
94+
95+
// Run the generator using the runtime.
96+
if hadErrs := rt.Run(); hadErrs {
97+
fmt.Fprint(os.Stderr, "generator did not run successfully\n")
98+
os.Exit(1)
99+
}
100+
}
101+
102+
// printMarkerDocs prints out marker help for the given generators specified in
103+
// the rawOptions
104+
func printMarkerDocs() error {
105+
// just grab a registry so we don't lag while trying to load roots
106+
// (like we'd do if we just constructed the full runtime).
107+
reg, err := genall.RegistryFromOptions(optionsRegistry, []string{generatorName})
108+
if err != nil {
109+
return err
110+
}
111+
112+
helpInfo := help.ByCategory(reg, help.SortByCategory)
113+
114+
for _, cat := range helpInfo {
115+
if cat.Category == "" {
116+
continue
117+
}
118+
contents := prettyhelp.MarkersDetails(false, cat.Category, cat.Markers)
119+
if err := contents.WriteTo(os.Stderr); err != nil {
120+
return err
121+
}
122+
}
123+
return nil
124+
}

exp/metric-gen/metric/generator.go

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

exp/metric-gen/metric/marker_gauge.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package metric
2+
3+
import (
4+
"sigs.k8s.io/controller-tools/pkg/markers"
5+
6+
"k8s.io/klog/v2"
7+
"k8s.io/kube-state-metrics/v2/pkg/customresourcestate"
8+
)
9+
10+
const (
11+
// GaugeMarkerName is a marker for defining metric definitions.
12+
GaugeMarkerName = "Metrics:gauge"
13+
)
14+
15+
func init() {
16+
markerDefinitions = append(
17+
markerDefinitions,
18+
must(markers.MakeDefinition(GaugeMarkerName, markers.DescribesField, GaugeMarker{})).
19+
help(GaugeMarker{}.help()),
20+
must(markers.MakeDefinition(GaugeMarkerName, markers.DescribesType, GaugeMarker{})).
21+
help(GaugeMarker{}.help()),
22+
)
23+
}
24+
25+
type GaugeMarker struct {
26+
Name string
27+
Help string `marker:"help,optional"`
28+
NilIsZero bool `marker:"nilIsZero,optional"`
29+
JSONPath JSONPath `marker:"JSONPath,optional"`
30+
LabelFromKey string `marker:"labelFromKey,optional"`
31+
}
32+
33+
func (GaugeMarker) help() *markers.DefinitionHelp {
34+
return &markers.DefinitionHelp{
35+
Category: "Metrics",
36+
DetailedHelp: markers.DetailedHelp{
37+
Summary: "Defines a Gauge metric and uses the implicit path to the field joined by the provided JSONPath as path for the metric configuration.",
38+
Details: "",
39+
},
40+
FieldHelp: map[string]markers.DetailedHelp{},
41+
}
42+
}
43+
44+
func (g GaugeMarker) ToGenerator(basePath ...string) *customresourcestate.Generator {
45+
valueFrom, err := g.JSONPath.Parse()
46+
if err != nil {
47+
klog.Fatal(err)
48+
}
49+
50+
path := append(basePath, valueFrom...)
51+
52+
return &customresourcestate.Generator{
53+
Name: g.Name,
54+
Help: g.Help,
55+
Each: customresourcestate.Metric{
56+
Type: customresourcestate.MetricTypeGauge,
57+
Gauge: &customresourcestate.MetricGauge{
58+
NilIsZero: g.NilIsZero,
59+
MetricMeta: customresourcestate.MetricMeta{
60+
Path: path,
61+
},
62+
LabelFromKey: g.LabelFromKey,
63+
ValueFrom: nil,
64+
},
65+
},
66+
}
67+
}

0 commit comments

Comments
 (0)