Skip to content

Commit 7bf825d

Browse files
committed
feat: hot reload kubeconfig
1 parent 535085e commit 7bf825d

File tree

3 files changed

+160
-0
lines changed

3 files changed

+160
-0
lines changed

internal/wrapper.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,28 @@ func RunKubeStateMetricsWrapper(opts *options.Options) {
9595
})
9696
crcViper.WatchConfig()
9797
}
98+
if opts.Kubeconfig != "" {
99+
kubecfgViper := viper.New()
100+
kubecfgViper.SetConfigType("yaml")
101+
kubecfgViper.SetConfigFile(opts.Kubeconfig)
102+
if err := kubecfgViper.ReadInConfig(); err != nil {
103+
if errors.Is(err, viper.ConfigFileNotFoundError{}) {
104+
klog.ErrorS(err, "kubeconfig file not found", "file", opts.Kubeconfig)
105+
} else {
106+
klog.ErrorS(err, "Error reading kubeconfig file", "file", opts.Kubeconfig)
107+
}
108+
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
109+
}
110+
kubecfgViper.OnConfigChange(func(e fsnotify.Event) {
111+
klog.InfoS("Changes detected", "name", e.Name)
112+
cancel()
113+
// Wait for the ports to be released.
114+
<-time.After(3 * time.Second)
115+
ctx, cancel = context.WithCancel(context.Background())
116+
go KSMRunOrDie(ctx)
117+
})
118+
kubecfgViper.WatchConfig()
119+
}
98120
klog.InfoS("Starting kube-state-metrics")
99121
KSMRunOrDie(ctx)
100122
select {}

tests/e2e.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,9 @@ go test -race -v ./tests/e2e/discovery_test.go
209209
echo "running hot-reload tests..."
210210
go test -v ./tests/e2e/hot-reload_test.go
211211

212+
echo "running hot-reload-kubeconfig tests..."
213+
go test -v ./tests/e2e/hot-reload-kubeconfig_test.go
214+
212215
output_logs=$(kubectl --namespace=kube-system logs deployment/kube-state-metrics kube-state-metrics)
213216
if echo "${output_logs}" | grep "^${klog_err}"; then
214217
echo ""
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
Copyright 2023 The Kubernetes Authors All rights reserved.
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 e2e
18+
19+
import (
20+
"context"
21+
"io"
22+
"net"
23+
"os"
24+
"os/exec"
25+
"path/filepath"
26+
"testing"
27+
"time"
28+
29+
"k8s.io/apimachinery/pkg/util/wait"
30+
31+
"k8s.io/kube-state-metrics/v2/internal"
32+
"k8s.io/kube-state-metrics/v2/pkg/options"
33+
)
34+
35+
func TestKubeConfigHotReload(t *testing.T) {
36+
37+
// Initialise options.
38+
opts := options.NewOptions()
39+
cmd := options.InitCommand
40+
opts.AddFlags(cmd)
41+
42+
// Open kubeconfig
43+
originalKubeconfig := os.Getenv("KUBECONFIG")
44+
if originalKubeconfig == "" {
45+
// Assume $HOME is always defined.
46+
originalKubeconfig = os.Getenv("HOME") + "/.kube/config"
47+
}
48+
originalKubeconfigFp, err := os.Open(filepath.Clean(originalKubeconfig))
49+
if err != nil {
50+
t.Fatalf("failed to open kubeconfig: %v", err)
51+
}
52+
defer originalKubeconfigFp.Close()
53+
54+
// Create temporal kubeconfig based on original one
55+
kubeconfigFp, err := os.CreateTemp("", "ksm-hot-reload-kubeconfig")
56+
if err != nil {
57+
t.Fatalf("failed to create temporal kubeconfig: %v", err)
58+
}
59+
defer os.Remove(kubeconfigFp.Name())
60+
61+
if _, err := io.Copy(kubeconfigFp, originalKubeconfigFp); err != nil {
62+
t.Fatalf("failed to copy from original kubeconfig to new one: %v", err)
63+
}
64+
kubeconfig := kubeconfigFp.Name()
65+
66+
opts.Kubeconfig = kubeconfig
67+
68+
// Run general validation on options.
69+
if err := opts.Parse(); err != nil {
70+
t.Fatalf("failed to parse options: %v", err)
71+
}
72+
73+
// Make the process asynchronous.
74+
go internal.RunKubeStateMetricsWrapper(opts)
75+
76+
// Wait for port 8080 to come up.
77+
err = wait.PollUntilContextTimeout(context.TODO(), 1*time.Second, 20*time.Second, true, func(ctx context.Context) (bool, error) {
78+
conn, err := net.Dial("tcp", "localhost:8080")
79+
if err != nil {
80+
return false, nil
81+
}
82+
err = conn.Close()
83+
if err != nil {
84+
return false, err
85+
}
86+
return true, nil
87+
})
88+
if err != nil {
89+
t.Fatalf("failed to wait for port 8080 to come up for the first time: %v", err)
90+
}
91+
92+
// Modify config to trigger hot reload.
93+
err = exec.Command("kubectl", "config", "set-cluster", "ksm-hot-reload-kubeconfig-test", "--kubeconfig", kubeconfig).Run() //nolint:gosec
94+
if err != nil {
95+
t.Fatalf("failed to modify kubeconfig: %v", err)
96+
}
97+
98+
// Revert kubeconfig to original one.
99+
defer func() {
100+
err := exec.Command("kubectl", "config", "delete-cluster", "ksm-hot-reload-kubeconfig-test", "--kubeconfig", kubeconfig).Run() //nolint:gosec
101+
if err != nil {
102+
t.Fatalf("failed to revert kubeconfig: %v", err)
103+
}
104+
}()
105+
106+
// Wait for new kubeconfig to be reloaded.
107+
time.Sleep(5 * time.Second)
108+
109+
// Wait for port 8080 to come up.
110+
ch := make(chan bool, 1)
111+
err = wait.PollUntilContextTimeout(context.TODO(), 1*time.Second, 20*time.Second, true, func(ctx context.Context) (bool, error) {
112+
conn, err := net.Dial("tcp", "localhost:8080")
113+
if err != nil {
114+
return false, nil
115+
}
116+
err = conn.Close()
117+
if err != nil {
118+
return false, err
119+
}
120+
// Indicate that the test has passed.
121+
ch <- true
122+
return true, nil
123+
})
124+
if err != nil {
125+
t.Fatalf("failed to wait for port 8080 to come up after restarting the process: %v", err)
126+
}
127+
128+
// Wait for process to exit.
129+
select {
130+
case <-ch:
131+
t.Log("test passed successfully")
132+
case <-time.After(20 * time.Second):
133+
t.Fatal("timed out waiting for test to pass, check the logs for more info")
134+
}
135+
}

0 commit comments

Comments
 (0)