Skip to content

Commit 63db483

Browse files
authored
Merge pull request #1928 from mrueg/hotreloading-crs
Reload CustomResourceState Config File on Change
2 parents b7a7070 + 06268ab commit 63db483

File tree

3 files changed

+82
-48
lines changed

3 files changed

+82
-48
lines changed

internal/wrapper.go

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import (
2121
"errors"
2222
"os"
2323
"path/filepath"
24-
"strings"
2524
"time"
2625

2726
"github.com/fsnotify/fsnotify"
@@ -30,67 +29,73 @@ import (
3029
"k8s.io/klog/v2"
3130

3231
"k8s.io/kube-state-metrics/v2/pkg/app"
33-
"k8s.io/kube-state-metrics/v2/pkg/customresource"
34-
"k8s.io/kube-state-metrics/v2/pkg/customresourcestate"
3532
"k8s.io/kube-state-metrics/v2/pkg/options"
3633
)
3734

3835
// RunKubeStateMetricsWrapper is a wrapper around KSM, delegated to the root command.
3936
func RunKubeStateMetricsWrapper(opts *options.Options) {
40-
var factories []customresource.RegistryFactory
41-
if config, set := resolveCustomResourceConfig(opts); set {
42-
crf, err := customresourcestate.FromConfig(config)
43-
if err != nil {
44-
klog.ErrorS(err, "Parsing from Custom Resource State Metrics file failed")
45-
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
46-
}
47-
factories = append(factories, crf...)
48-
}
4937

5038
KSMRunOrDie := func(ctx context.Context) {
51-
if err := app.RunKubeStateMetricsWrapper(ctx, opts, factories...); err != nil {
39+
if err := app.RunKubeStateMetricsWrapper(ctx, opts); err != nil {
5240
klog.ErrorS(err, "Failed to run kube-state-metrics")
5341
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
5442
}
5543
}
44+
5645
ctx, cancel := context.WithCancel(context.Background())
5746
if file := options.GetConfigFile(*opts); file != "" {
58-
viper.SetConfigType("yaml")
59-
viper.SetConfigFile(file)
60-
if err := viper.ReadInConfig(); err != nil {
47+
cfgViper := viper.New()
48+
cfgViper.SetConfigType("yaml")
49+
cfgViper.SetConfigFile(file)
50+
if err := cfgViper.ReadInConfig(); err != nil {
6151
if errors.Is(err, viper.ConfigFileNotFoundError{}) {
6252
klog.ErrorS(err, "Options configuration file not found", "file", file)
6353
} else {
6454
klog.ErrorS(err, "Error reading options configuration file", "file", file)
6555
}
6656
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
6757
}
68-
viper.OnConfigChange(func(e fsnotify.Event) {
58+
cfgViper.OnConfigChange(func(e fsnotify.Event) {
6959
klog.Infof("Changes detected: %s\n", e.Name)
7060
cancel()
7161
// Wait for the ports to be released.
7262
<-time.After(3 * time.Second)
7363
ctx, cancel = context.WithCancel(context.Background())
7464
go KSMRunOrDie(ctx)
7565
})
76-
viper.WatchConfig()
77-
}
78-
klog.Infoln("Starting kube-state-metrics")
79-
KSMRunOrDie(ctx)
80-
select {}
81-
}
66+
cfgViper.WatchConfig()
8267

83-
func resolveCustomResourceConfig(opts *options.Options) (customresourcestate.ConfigDecoder, bool) {
84-
if s := opts.CustomResourceConfig; s != "" {
85-
return yaml.NewDecoder(strings.NewReader(s)), true
86-
}
87-
if file := opts.CustomResourceConfigFile; file != "" {
88-
f, err := os.Open(filepath.Clean(file))
68+
// Merge configFile values with opts so we get the CustomResourceConfigFile from config as well
69+
configFile, err := os.ReadFile(filepath.Clean(file))
8970
if err != nil {
90-
klog.ErrorS(err, "Custom Resource State Metrics file could not be opened")
71+
klog.ErrorS(err, "failed to read options configuration file", "file", file)
72+
}
73+
74+
yaml.Unmarshal(configFile, opts)
75+
}
76+
if opts.CustomResourceConfigFile != "" {
77+
crcViper := viper.New()
78+
crcViper.SetConfigType("yaml")
79+
crcViper.SetConfigFile(opts.CustomResourceConfigFile)
80+
if err := crcViper.ReadInConfig(); err != nil {
81+
if errors.Is(err, viper.ConfigFileNotFoundError{}) {
82+
klog.ErrorS(err, "Custom resource configuration file not found", "file", opts.CustomResourceConfigFile)
83+
} else {
84+
klog.ErrorS(err, "Error reading Custom resource configuration file", "file", opts.CustomResourceConfigFile)
85+
}
9186
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
9287
}
93-
return yaml.NewDecoder(f), true
88+
crcViper.OnConfigChange(func(e fsnotify.Event) {
89+
klog.Infof("Changes detected: %s\n", e.Name)
90+
cancel()
91+
// Wait for the ports to be released.
92+
<-time.After(3 * time.Second)
93+
ctx, cancel = context.WithCancel(context.Background())
94+
go KSMRunOrDie(ctx)
95+
})
96+
crcViper.WatchConfig()
9497
}
95-
return nil, false
98+
klog.Infoln("Starting kube-state-metrics")
99+
KSMRunOrDie(ctx)
100+
select {}
96101
}

pkg/app/server.go

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"os"
2828
"path/filepath"
2929
"strconv"
30+
"strings"
3031
"time"
3132

3233
"gopkg.in/yaml.v3"
@@ -47,6 +48,7 @@ import (
4748
"k8s.io/kube-state-metrics/v2/internal/store"
4849
"k8s.io/kube-state-metrics/v2/pkg/allowdenylist"
4950
"k8s.io/kube-state-metrics/v2/pkg/customresource"
51+
"k8s.io/kube-state-metrics/v2/pkg/customresourcestate"
5052
generator "k8s.io/kube-state-metrics/v2/pkg/metric_generator"
5153
"k8s.io/kube-state-metrics/v2/pkg/metricshandler"
5254
"k8s.io/kube-state-metrics/v2/pkg/optin"
@@ -73,8 +75,8 @@ func (pl promLogger) Log(v ...interface{}) error {
7375
}
7476

7577
// RunKubeStateMetricsWrapper runs KSM with context cancellation.
76-
func RunKubeStateMetricsWrapper(ctx context.Context, opts *options.Options, factories ...customresource.RegistryFactory) error {
77-
err := RunKubeStateMetrics(ctx, opts, factories...)
78+
func RunKubeStateMetricsWrapper(ctx context.Context, opts *options.Options) error {
79+
err := RunKubeStateMetrics(ctx, opts)
7880
if ctx.Err() == context.Canceled {
7981
klog.Infoln("Restarting: kube-state-metrics, metrics will be reset")
8082
return nil
@@ -85,11 +87,10 @@ func RunKubeStateMetricsWrapper(ctx context.Context, opts *options.Options, fact
8587
// RunKubeStateMetrics will build and run the kube-state-metrics.
8688
// Any out-of-tree custom resource metrics could be registered by newing a registry factory
8789
// which implements customresource.RegistryFactory and pass all factories into this function.
88-
func RunKubeStateMetrics(ctx context.Context, opts *options.Options, factories ...customresource.RegistryFactory) error {
90+
func RunKubeStateMetrics(ctx context.Context, opts *options.Options) error {
8991
promLogger := promLogger{}
9092

9193
storeBuilder := store.NewBuilder()
92-
storeBuilder.WithCustomResourceStoreFactories(factories...)
9394

9495
ksmMetricsRegistry := prometheus.NewRegistry()
9596
ksmMetricsRegistry.MustRegister(version.NewCollector("kube_state_metrics"))
@@ -144,6 +145,22 @@ func RunKubeStateMetrics(ctx context.Context, opts *options.Options, factories .
144145
}
145146
}
146147

148+
// Loading custom resource state configuration from cli argument or config file
149+
config, err := resolveCustomResourceConfig(opts)
150+
if err != nil {
151+
return err
152+
}
153+
154+
var factories []customresource.RegistryFactory
155+
156+
if config != nil {
157+
factories, err = customresourcestate.FromConfig(config)
158+
if err != nil {
159+
return fmt.Errorf("Parsing from Custom Resource State Metrics file failed: %v", err)
160+
}
161+
}
162+
storeBuilder.WithCustomResourceStoreFactories(factories...)
163+
147164
if opts.CustomResourceConfigFile != "" {
148165
crcFile, err := os.ReadFile(filepath.Clean(opts.CustomResourceConfigFile))
149166
if err != nil {
@@ -156,24 +173,22 @@ func RunKubeStateMetrics(ctx context.Context, opts *options.Options, factories .
156173

157174
}
158175

159-
var resources []string
176+
resources := make([]string, len(factories))
177+
178+
for i, factory := range factories {
179+
resources[i] = factory.Name()
180+
}
181+
160182
switch {
161183
case len(opts.Resources) == 0 && !opts.CustomResourcesOnly:
184+
resources = append(resources, options.DefaultResources.AsSlice()...)
162185
klog.InfoS("Used default resources")
163-
resources = options.DefaultResources.AsSlice()
164-
// enable custom resource
165-
for _, factory := range factories {
166-
resources = append(resources, factory.Name())
167-
}
168186
case opts.CustomResourcesOnly:
169187
// enable custom resource only
170-
for _, factory := range factories {
171-
resources = append(resources, factory.Name())
172-
}
173188
klog.InfoS("Used CRD resources only", "resources", resources)
174189
default:
175-
klog.InfoS("Used resources", "resources", opts.Resources.String())
176-
resources = opts.Resources.AsSlice()
190+
resources = append(resources, opts.Resources.AsSlice()...)
191+
klog.InfoS("Used resources", "resources", resources)
177192
}
178193

179194
if err := storeBuilder.WithEnabledResources(resources); err != nil {
@@ -419,3 +434,17 @@ func md5HashAsMetricValue(data []byte) float64 {
419434
copy(bytes, smallSum)
420435
return float64(binary.LittleEndian.Uint64(bytes))
421436
}
437+
438+
func resolveCustomResourceConfig(opts *options.Options) (customresourcestate.ConfigDecoder, error) {
439+
if s := opts.CustomResourceConfig; s != "" {
440+
return yaml.NewDecoder(strings.NewReader(s)), nil
441+
}
442+
if file := opts.CustomResourceConfigFile; file != "" {
443+
f, err := os.Open(filepath.Clean(file))
444+
if err != nil {
445+
return nil, fmt.Errorf("Custom Resource State Metrics file could not be opened: %v", err)
446+
}
447+
return yaml.NewDecoder(f), nil
448+
}
449+
return nil, nil
450+
}

tests/e2e/main_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ func TestDefaultCollectorMetricsAvailable(t *testing.T) {
271271

272272
files, err := os.ReadDir("../../internal/store/")
273273
if err != nil {
274-
t.Fatalf("failed to read dir to get all resouces name: %v", err)
274+
t.Fatalf("failed to read dir to get all resources name: %v", err)
275275
}
276276

277277
re := regexp.MustCompile(`^([a-z]+).go$`)

0 commit comments

Comments
 (0)