Skip to content

Commit

Permalink
Implement metric-gen tool
Browse files Browse the repository at this point in the history
Implements the metric-gen tool which could get used to create custom resource
configurations directly from code, similar to what controller-gen does.
  • Loading branch information
chrischdi committed Mar 1, 2023
1 parent 3a7e617 commit 027e03d
Show file tree
Hide file tree
Showing 11 changed files with 1,143 additions and 7 deletions.
59 changes: 59 additions & 0 deletions exp/metric-gen/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
module k8s.io/kube-state-metrics/exp/metric-gen

go 1.19

replace k8s.io/kube-state-metrics/v2 => ../..

require (
github.com/spf13/cobra v1.6.1
k8s.io/apimachinery v0.26.0
k8s.io/klog/v2 v2.80.1
k8s.io/kube-state-metrics/v2 v2.0.0-00010101000000-000000000000
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448
sigs.k8s.io/controller-tools v0.11.1
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/gobuffalo/flect v0.3.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.38.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/mod v0.7.0 // indirect
golang.org/x/net v0.4.0 // indirect
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/term v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
golang.org/x/tools v0.4.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/apiextensions-apiserver v0.26.0 // indirect
k8s.io/client-go v0.26.0 // indirect
k8s.io/component-base v0.26.0 // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
176 changes: 176 additions & 0 deletions exp/metric-gen/go.sum

Large diffs are not rendered by default.

124 changes: 124 additions & 0 deletions exp/metric-gen/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
Copyright 2022 The Kubernetes Authors.
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 main

import (
"fmt"
"os"

"github.com/spf13/pflag"
"k8s.io/kube-state-metrics/exp/metric-gen/metric"
"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"
)

const (
generatorName = "metric"
)

var (
// optionsRegistry contains all the marker definitions used to process command line options
optionsRegistry = &markers.Registry{}
)

func main() {
var whichMarkersFlag bool

pflag.CommandLine.BoolVarP(&whichMarkersFlag, "which-markers", "w", false, "print out all markers available with the requested generators")

pflag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, " metric-gen [flags] /path/to/package [/path/to/package]\n\n")
fmt.Fprintf(os.Stderr, "Flags:\n")
pflag.PrintDefaults()
fmt.Fprintf(os.Stderr, "\n")
}

pflag.Parse()

// 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 := metric.Generator{}
defn := markers.Must(markers.MakeDefinition(generatorName, markers.DescribesPackage, metricGenerator))
if err := optionsRegistry.Register(defn); err != nil {
panic(err)
}

if whichMarkersFlag {
printMarkerDocs()
return
}

// Check if package paths got passed as input parameters.
if len(os.Args[1:]) == 0 {
fmt.Fprint(os.Stderr, "error: Please provide package paths as parameters\n\n")
pflag.Usage()
os.Exit(1)
}

// Load the passed packages as roots.
roots, err := loader.LoadRoots(os.Args[1:]...)
if err != nil {
fmt.Fprint(os.Stderr, fmt.Sprintf("error: loading packages %v\n", err))
os.Exit(1)
}

// Set up the generator runtime using controller-tools and passing our optionsRegistry.
rt, err := genall.FromOptions(optionsRegistry, []string{generatorName})
if err != nil {
fmt.Fprint(os.Stderr, fmt.Sprintf("error: %v\n", err))
os.Exit(1)
}

// 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 {
fmt.Fprint(os.Stderr, "generator did not run successfully\n")
os.Exit(1)
}
}

// printMarkerDocs prints out marker help for the given generators specified in
// the rawOptions
func printMarkerDocs() error {
// 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
}
117 changes: 117 additions & 0 deletions exp/metric-gen/metric/generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package metric

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"
"sigs.k8s.io/controller-tools/pkg/markers"

"k8s.io/kube-state-metrics/v2/pkg/customresourcestate"
)

type Generator struct{}

func (Generator) 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()
}

func (g Generator) 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 _, root := range ctx.Roots {
// skip packages which don't import metav1 because they can't define a CRD without meta v1.
metav1 := root.Imports()["k8s.io/apimachinery/pkg/apis/meta/v1"]
if metav1 == nil {
continue
}

// parse the given package to feed crd.FindKubeKinds to find CRD objects.
parser.NeedPackage(root)
kubeKinds := crd.FindKubeKinds(parser.Parser, metav1)
if len(kubeKinds) == 0 {
klog.Fatalf("no objects in the roots")
}

for _, gv := range kubeKinds {
// Create customresourcestate.Resource for each CRD which contains all metric
// definitions for the CRD.
parser.NeedResourceFor(gv)
}
}

// Build customresourcestate configuration file from generated data.
metrics := customresourcestate.Metrics{
Spec: customresourcestate.MetricsSpec{
Resources: []customresourcestate.Resource{},
},
}

// Sort the resources to get a deterministic output.

for _, resource := range parser.CustomResourceStates {
if len(resource.Metrics) > 0 {
// sort the metrics
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.Slice(metrics.Spec.Resources, func(i, j int) bool {
if metrics.Spec.Resources[i].MetricNamePrefix == nil && metrics.Spec.Resources[j].MetricNamePrefix == nil {
a := metrics.Spec.Resources[i].GroupVersionKind.Group + "/" + metrics.Spec.Resources[i].GroupVersionKind.Version + "/" + metrics.Spec.Resources[i].GroupVersionKind.Kind
b := metrics.Spec.Resources[j].GroupVersionKind.Group + "/" + metrics.Spec.Resources[j].GroupVersionKind.Version + "/" + metrics.Spec.Resources[j].GroupVersionKind.Kind
return a < b
}

// Either a or b will not be the empty string, so we can compare them.
var a, b string
if metrics.Spec.Resources[i].MetricNamePrefix == nil {
a = *metrics.Spec.Resources[i].MetricNamePrefix
}
if metrics.Spec.Resources[j].MetricNamePrefix != nil {
b = *metrics.Spec.Resources[j].MetricNamePrefix
}
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
}

// 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
}

func (g Generator) RegisterMarkers(into *markers.Registry) error {
for _, m := range markerDefinitions {
if err := m.Register(into); err != nil {
return err
}
}

return nil
}
67 changes: 67 additions & 0 deletions exp/metric-gen/metric/marker_gauge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package metric

import (
"sigs.k8s.io/controller-tools/pkg/markers"

"k8s.io/klog/v2"
"k8s.io/kube-state-metrics/v2/pkg/customresourcestate"
)

const (
// GaugeMarkerName is a marker for defining metric definitions.
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()),
)
}

type GaugeMarker struct {
Name string
Help string `marker:"help,optional"`
NilIsZero bool `marker:"nilIsZero,optional"`
JSONPath JSONPath `marker:"JSONPath,optional"`
LabelFromKey string `marker:"labelFromKey,optional"`
}

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 {
valueFrom, err := g.JSONPath.Parse()
if err != nil {
klog.Fatal(err)
}

path := append(basePath, valueFrom...)

return &customresourcestate.Generator{
Name: g.Name,
Help: g.Help,
Each: customresourcestate.Metric{
Type: customresourcestate.MetricTypeGauge,
Gauge: &customresourcestate.MetricGauge{
NilIsZero: g.NilIsZero,
MetricMeta: customresourcestate.MetricMeta{
Path: path,
},
LabelFromKey: g.LabelFromKey,
ValueFrom: nil,
},
},
}
}
Loading

0 comments on commit 027e03d

Please sign in to comment.