From 704206b7162fcaa142af1fa4e2c21225a0290730 Mon Sep 17 00:00:00 2001 From: Simon Pasquier Date: Thu, 6 Jun 2024 15:16:47 +0200 Subject: [PATCH] feat: add `prometheus_operator_feature_gate_info` metric This change also moves the feature gates to the operator config struct. It means that after a feature gate is enabled/disabled, the operator will reconcile the managed Prometheus resources which should be the right thing to do. Signed-off-by: Simon Pasquier --- Documentation/operator.md | 4 +- cmd/operator/main.go | 16 ++-- pkg/operator/config.go | 23 +++++- pkg/operator/feature_gates.go | 124 ++++++++++++++++++++++------- pkg/operator/feature_gates_test.go | 64 ++++++++++----- 5 files changed, 169 insertions(+), 62 deletions(-) diff --git a/Documentation/operator.md b/Documentation/operator.md index e56c76d3f..e80ed2e1a 100644 --- a/Documentation/operator.md +++ b/Documentation/operator.md @@ -52,7 +52,9 @@ Usage of ./operator: -enable-config-reloader-probes Enable liveness and readiness for the config-reloader container. Default: false -feature-gates value - Feature gates are a set of key=value pairs that describe Prometheus-Operator features. Available features: ["PrometheusAgentDaemonSet"]. + Feature gates are a set of key=value pairs that describe Prometheus-Operator features. + Available feature gates: + PrometheusAgentDaemonSet: Enables the DaemonSet mode for PrometheusAgent (enabled: false) -key-file string - NOT RECOMMENDED FOR PRODUCTION - Path to private TLS certificate file. -kubelet-node-address-priority value diff --git a/cmd/operator/main.go b/cmd/operator/main.go index 9f9b2287a..fcaf98e27 100644 --- a/cmd/operator/main.go +++ b/cmd/operator/main.go @@ -120,7 +120,7 @@ var ( kubeletSelector operator.LabelSelector nodeAddressPriority operator.NodeAddressPriority - featureGates *k8sflag.MapStringBool + featureGates = k8sflag.NewMapStringBool(ptr.To(map[string]bool{})) ) func parseFlags(fs *flag.FlagSet) { @@ -173,11 +173,9 @@ func parseFlags(fs *flag.FlagSet) { fs.Var(&cfg.ThanosRulerSelector, "thanos-ruler-instance-selector", "Label selector to filter ThanosRuler Custom Resources to watch.") fs.Var(&cfg.SecretListWatchSelector, "secret-field-selector", "Field selector to filter Secrets to watch") - // Auto GOMEMLIMIT Ratio fs.Float64Var(&memlimitRatio, "auto-gomemlimit-ratio", defaultMemlimitRatio, "The ratio of reserved GOMEMLIMIT memory to the detected maximum container or system memory. The value should be greater than 0.0 and less than 1.0. Default: 0.0 (disabled).") - featureGates = k8sflag.NewMapStringBool(ptr.To(make(map[string]bool))) - fs.Var(featureGates, "feature-gates", fmt.Sprintf("Feature gates are a set of key=value pairs that describe Prometheus-Operator features. Available features: %q.", operator.AvailableFeatureGates())) + cfg.RegisterFeatureGatesFlags(fs, featureGates) logging.RegisterFlags(fs, &logConfig) versionutil.RegisterFlags(fs) @@ -199,17 +197,14 @@ func run(fs *flag.FlagSet) int { stdlog.Fatal(err) } - gates, err := operator.ValidateFeatureGates(featureGates) - if err != nil { - level.Error(logger).Log( - "msg", "error validating feature gates", - "error", err) + if err := cfg.Gates.UpdateFeatureGates(*featureGates.Map); err != nil { + level.Error(logger).Log("error", err) return 1 } level.Info(logger).Log("msg", "Starting Prometheus Operator", "version", version.Info()) level.Info(logger).Log("build_context", version.BuildContext()) - level.Info(logger).Log("feature_gates", gates) + level.Info(logger).Log("feature_gates", cfg.Gates.String()) goruntime.SetMaxProcs(logger) goruntime.SetMemLimit(logger, memlimitRatio) @@ -509,6 +504,7 @@ func run(fs *flag.FlagSet) int { ), collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}), versioncollector.NewCollector("prometheus_operator"), + cfg.Gates, ) mux.Handle("/metrics", promhttp.HandlerFor(r, promhttp.HandlerOpts{})) diff --git a/pkg/operator/config.go b/pkg/operator/config.go index 106f862ab..3ac32e799 100644 --- a/pkg/operator/config.go +++ b/pkg/operator/config.go @@ -15,6 +15,7 @@ package operator import ( + "flag" "fmt" "slices" "sort" @@ -26,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/version" + k8sflag "k8s.io/component-base/cli/flag" ) // Config defines configuration parameters for the Operator. @@ -60,8 +62,11 @@ type Config struct { ThanosRulerSelector LabelSelector SecretListWatchSelector FieldSelector - // Controller id for pod ownership + // Controller id for pod ownership. ControllerID string + + // Feature gates. + Gates *FeatureGates } // DefaultConfig returns a default operator configuration. @@ -81,9 +86,25 @@ func DefaultConfig(cpu, memory string) Config { AlertmanagerConfigAllowList: StringSet{}, ThanosRulerAllowList: StringSet{}, }, + Gates: &FeatureGates{ + PrometheusAgentDaemonSetFeature: FeatureGate{ + description: "Enables the DaemonSet mode for PrometheusAgent", + enabled: false, + }, + }, } } +func (c *Config) RegisterFeatureGatesFlags(fs *flag.FlagSet, flags *k8sflag.MapStringBool) { + fs.Var( + flags, + "feature-gates", + fmt.Sprintf("Feature gates are a set of key=value pairs that describe Prometheus-Operator features.\n"+ + "Available feature gates:\n %s", strings.Join(c.Gates.Descriptions(), "\n "), + ), + ) +} + // ContainerConfig holds some configuration for the ConfigReloader sidecar // that can be set through prometheus-operator command line arguments. type ContainerConfig struct { diff --git a/pkg/operator/feature_gates.go b/pkg/operator/feature_gates.go index 934a0c251..0bc8fa2f8 100644 --- a/pkg/operator/feature_gates.go +++ b/pkg/operator/feature_gates.go @@ -19,47 +19,113 @@ import ( "slices" "strings" - k8sflag "k8s.io/component-base/cli/flag" + "github.com/prometheus/client_golang/prometheus" ) -// At the moment, the are no feature gates available. -var defaultFeatureGates = map[string]bool{ - "PrometheusAgentDaemonSet": false, +const ( + // PrometheusAgentDaemonSetFeature enables the DaemonSet mode for PrometheusAgent. + PrometheusAgentDaemonSetFeature FeatureGateName = "PrometheusAgentDaemonSet" +) + +type FeatureGateName string + +type FeatureGates map[FeatureGateName]FeatureGate + +type FeatureGate struct { + description string + enabled bool } -// ValidateFeatureGates merges the feature gate default values with +func (fg FeatureGates) Enabled(name FeatureGateName) bool { + return fg[name].enabled +} + +// UpdateFeatureGates merges the current feature gate values with // the values provided by the user. -func ValidateFeatureGates(flags *k8sflag.MapStringBool) (string, error) { - gates := defaultFeatureGates - if flags.Empty() { - return mapToString(gates), nil - } - - imgs := *flags.Map - for k, v := range imgs { - if _, ok := gates[k]; !ok { - return "", fmt.Errorf("feature gate %v is unknown", k) +func (fg *FeatureGates) UpdateFeatureGates(flags map[string]bool) error { + for k := range flags { + f, found := (*fg)[FeatureGateName(k)] + if !found { + return fmt.Errorf("feature gate %q is unknown (supported feature gates: %s)", k, fg.String()) } - gates[k] = v + f.enabled = flags[k] + (*fg)[FeatureGateName(k)] = f } - return mapToString(gates), nil + + return nil } -func AvailableFeatureGates() []string { - i := 0 - gates := make([]string, len(defaultFeatureGates)) - for k := range defaultFeatureGates { - gates[i] = k - i++ +func (fg *FeatureGates) keyValuePairs() ([]FeatureGateName, []FeatureGate) { + if fg == nil { + return nil, nil } - slices.Sort(gates) - return gates + + var ( + names = make([]FeatureGateName, 0, len(*fg)) + gates = make([]FeatureGate, 0, len(*fg)) + ) + for k := range *fg { + names = append(names, k) + } + slices.Sort(names) + + for _, v := range names { + gates = append(gates, (*fg)[v]) + } + + return names, gates } -func mapToString(m map[string]bool) string { - var s []string - for k, v := range m { - s = append(s, fmt.Sprintf("%s=%t", k, v)) +func (fg *FeatureGates) Descriptions() []string { + var ( + names, gates = fg.keyValuePairs() + desc = make([]string, 0, len(names)) + ) + + for i := range names { + desc = append(desc, fmt.Sprintf("%s: %s (enabled: %t)", names[i], gates[i].description, gates[i].enabled)) } + + return desc +} + +func (fg *FeatureGates) String() string { + names, gates := fg.keyValuePairs() + + s := make([]string, len(names)) + for i := range names { + s[i] = fmt.Sprintf("%s=%t", names[i], gates[i].enabled) + } + return strings.Join(s, ",") } + +var featureGateInfoDesc = prometheus.NewDesc( + "prometheus_operator_feature_gate", + "Reports about the Prometheus operator feature gates. A value of 1 means that the feature gate is enabled. Otherwise the value is 0.", + []string{"name"}, + nil, +) + +// Describe implements the prometheus.Collector interface. +func (fg *FeatureGates) Describe(ch chan<- *prometheus.Desc) { + ch <- featureGateInfoDesc +} + +// Collect implements the prometheus.Collector interface. +func (fg *FeatureGates) Collect(ch chan<- prometheus.Metric) { + names, gates := fg.keyValuePairs() + + for i, v := range names { + var val float64 + if gates[i].enabled { + val = 1.0 + } + ch <- prometheus.MustNewConstMetric( + featureGateInfoDesc, + prometheus.GaugeValue, + val, + string(v), + ) + } +} diff --git a/pkg/operator/feature_gates_test.go b/pkg/operator/feature_gates_test.go index f09d680f3..e5b4c4b70 100644 --- a/pkg/operator/feature_gates_test.go +++ b/pkg/operator/feature_gates_test.go @@ -15,31 +15,53 @@ package operator import ( - "strings" "testing" "github.com/stretchr/testify/require" - k8sflag "k8s.io/component-base/cli/flag" ) -func TestDefaultFeatureGates(t *testing.T) { - m := AvailableFeatureGates() +func TestUpdateFeatureGates(t *testing.T) { + newFg := func() *FeatureGates { + return &FeatureGates{ + FeatureGateName("Foo"): { + description: "foo", + enabled: true, + }, + FeatureGateName("Bar"): { + description: "bar", + enabled: false, + }, + } + } - require.GreaterOrEqual(t, len(m), 1) -} - -func TestMapToString(t *testing.T) { - m := mapToString(defaultFeatureGates) - - require.True(t, strings.Contains(m, "PrometheusAgentDaemonSet=false")) -} - -func TestValidateFeatureGatesWithNotSupportFeature(t *testing.T) { - m, err := ValidateFeatureGates(k8sflag.NewMapStringBool(&map[string]bool{"NotSupportFeature1": true, "NotSupportFeature2": false})) - require.Error(t, err) - require.Equal(t, "", m) - - m, err = ValidateFeatureGates(k8sflag.NewMapStringBool(&map[string]bool{"PrometheusAgentDaemonSet": true})) - require.NoError(t, err) - require.True(t, strings.Contains(m, "PrometheusAgentDaemonSet=true")) + for _, tc := range []struct { + flags map[string]bool + + err bool + }{ + { + flags: map[string]bool{ + "Foo": false, + "Bar": true, + }, + }, + { + flags: map[string]bool{"Foox": false, "Bar": true}, + err: true, + }, + } { + t.Run("", func(t *testing.T) { + fg := newFg() + err := fg.UpdateFeatureGates(tc.flags) + if tc.err { + require.Error(t, err) + return + } + + require.NoError(t, err) + for k, v := range tc.flags { + require.Equal(t, fg.Enabled(FeatureGateName(k)), v) + } + }) + } }