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 Aug 25, 2023
1 parent e3dd5ff commit 6f0d7a4
Show file tree
Hide file tree
Showing 11 changed files with 1,286 additions and 7 deletions.
74 changes: 74 additions & 0 deletions exp/metric-gen/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
module k8s.io/kube-state-metrics/v2/exp/metric-gen

go 1.19

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

require (
github.com/spf13/pflag v1.0.5
k8s.io/apimachinery v0.28.0
k8s.io/client-go v0.28.0
k8s.io/klog/v2 v2.100.1
k8s.io/kube-state-metrics/v2 v2.0.0-00010101000000-000000000000
k8s.io/utils v0.0.0-20230711102312-30195339c3c7
sigs.k8s.io/controller-tools v0.13.0
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-jump v0.0.0-20211018200510-ba001c3ffce0 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/gobuffalo/flect v1.0.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/spf13/cobra v1.7.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/term v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.12.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.28.0 // indirect
k8s.io/apiextensions-apiserver v0.28.0 // indirect
k8s.io/component-base v0.28.0 // indirect
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
k8s.io/sample-controller v0.27.4 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
204 changes: 204 additions & 0 deletions exp/metric-gen/go.sum

Large diffs are not rendered by default.

123 changes: 123 additions & 0 deletions exp/metric-gen/main.go
Original file line number Diff line number Diff line change
@@ -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 main

import (
"fmt"
"os"

"github.com/spf13/pflag"
"k8s.io/kube-state-metrics/v2/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
}
132 changes: 132 additions & 0 deletions exp/metric-gen/metric/generator.go
Original file line number Diff line number Diff line change
@@ -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 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
}
Loading

0 comments on commit 6f0d7a4

Please sign in to comment.