Skip to content

Commit b3fee0d

Browse files
authored
Merge pull request kubernetes-sigs#642 from joelanford/multi-path-client-loader
✨ Add support for multi-value KUBECONFIG
2 parents 40070e2 + 2fe837f commit b3fee0d

File tree

3 files changed

+302
-17
lines changed

3 files changed

+302
-17
lines changed

pkg/client/config/config.go

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ import (
2020
"flag"
2121
"fmt"
2222
"os"
23-
"os/user"
24-
"path/filepath"
2523

2624
"k8s.io/client-go/rest"
2725
"k8s.io/client-go/tools/clientcmd"
@@ -95,35 +93,40 @@ func GetConfigWithContext(context string) (*rest.Config, error) {
9593
return cfg, nil
9694
}
9795

96+
// loadInClusterConfig is a function used to load the in-cluster
97+
// Kubernetes client config. This variable makes is possible to
98+
// test the precedence of loading the config.
99+
var loadInClusterConfig = rest.InClusterConfig
100+
98101
// loadConfig loads a REST Config as per the rules specified in GetConfig
99102
func loadConfig(context string) (*rest.Config, error) {
100103

101104
// If a flag is specified with the config location, use that
102105
if len(kubeconfig) > 0 {
103-
return loadConfigWithContext(apiServerURL, kubeconfig, context)
104-
}
105-
// If an env variable is specified with the config location, use that
106-
if len(os.Getenv("KUBECONFIG")) > 0 {
107-
return loadConfigWithContext(apiServerURL, os.Getenv("KUBECONFIG"), context)
108-
}
109-
// If no explicit location, try the in-cluster config
110-
if c, err := rest.InClusterConfig(); err == nil {
111-
return c, nil
106+
return loadConfigWithContext(apiServerURL, &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig}, context)
112107
}
113-
// If no in-cluster config, try the default location in the user's home directory
114-
if usr, err := user.Current(); err == nil {
115-
if c, err := loadConfigWithContext(apiServerURL, filepath.Join(usr.HomeDir, ".kube", "config"),
116-
context); err == nil {
108+
109+
// If the recommended kubeconfig env variable is not specified,
110+
// try the in-cluster config.
111+
kubeconfigPath := os.Getenv(clientcmd.RecommendedConfigPathEnvVar)
112+
if len(kubeconfigPath) == 0 {
113+
if c, err := loadInClusterConfig(); err == nil {
117114
return c, nil
118115
}
119116
}
120117

118+
// If the recommended kubeconfig env variable is set, or there
119+
// is no in-cluster config, try the default recommended locations.
120+
if c, err := loadConfigWithContext(apiServerURL, clientcmd.NewDefaultClientConfigLoadingRules(), context); err == nil {
121+
return c, nil
122+
}
123+
121124
return nil, fmt.Errorf("could not locate a kubeconfig")
122125
}
123126

124-
func loadConfigWithContext(apiServerURL, kubeconfig, context string) (*rest.Config, error) {
127+
func loadConfigWithContext(apiServerURL string, loader clientcmd.ClientConfigLoader, context string) (*rest.Config, error) {
125128
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
126-
&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig},
129+
loader,
127130
&clientcmd.ConfigOverrides{
128131
ClusterInfo: clientcmdapi.Cluster{
129132
Server: apiServerURL,
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
Copyright 2019 The Kubernetes Authors.
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 config
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/ginkgo"
23+
. "github.com/onsi/gomega"
24+
25+
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
26+
logf "sigs.k8s.io/controller-runtime/pkg/log"
27+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
28+
)
29+
30+
func TestConfig(t *testing.T) {
31+
RegisterFailHandler(Fail)
32+
RunSpecsWithDefaultAndCustomReporters(t, "Client Config Test Suite", []Reporter{printer.NewlineReporter{}})
33+
}
34+
35+
var _ = BeforeSuite(func(done Done) {
36+
logf.SetLogger(zap.LoggerTo(GinkgoWriter, true))
37+
38+
close(done)
39+
}, 60)

pkg/client/config/config_test.go

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
/*
2+
Copyright 2019 The Kubernetes Authors.
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 config
18+
19+
import (
20+
"io/ioutil"
21+
"os"
22+
"path/filepath"
23+
"strings"
24+
25+
. "github.com/onsi/ginkgo"
26+
. "github.com/onsi/gomega"
27+
"k8s.io/client-go/rest"
28+
"k8s.io/client-go/tools/clientcmd"
29+
)
30+
31+
type testCase struct {
32+
text string
33+
apiServerURL string
34+
context string
35+
kubeconfigFlag string
36+
kubeconfigEnv []string
37+
wantHost string
38+
}
39+
40+
var _ = Describe("Config", func() {
41+
42+
var dir string
43+
44+
origRecommendedHomeFile := clientcmd.RecommendedHomeFile
45+
46+
BeforeEach(func() {
47+
// create temporary directory for test case
48+
var err error
49+
dir, err = ioutil.TempDir("", "cr-test")
50+
Expect(err).NotTo(HaveOccurred())
51+
52+
// override $HOME/.kube/config
53+
clientcmd.RecommendedHomeFile = filepath.Join(dir, ".kubeconfig")
54+
})
55+
56+
AfterEach(func() {
57+
os.Unsetenv(clientcmd.RecommendedConfigPathEnvVar)
58+
kubeconfig = ""
59+
apiServerURL = ""
60+
clientcmd.RecommendedHomeFile = origRecommendedHomeFile
61+
62+
err := os.RemoveAll(dir)
63+
Expect(err).NotTo(HaveOccurred())
64+
})
65+
66+
Describe("GetConfigWithContext", func() {
67+
defineTests := func(testCases []testCase) {
68+
for _, testCase := range testCases {
69+
tc := testCase
70+
It(tc.text, func() {
71+
// set global and environment configs
72+
setConfigs(tc, dir)
73+
74+
// run the test
75+
cfg, err := GetConfigWithContext(tc.context)
76+
Expect(err).NotTo(HaveOccurred())
77+
Expect(cfg.Host).To(Equal(tc.wantHost))
78+
})
79+
}
80+
}
81+
82+
Context("when kubeconfig files don't exist", func() {
83+
It("should fail", func() {
84+
cfg, err := GetConfigWithContext("")
85+
Expect(cfg).To(BeNil())
86+
Expect(err).To(HaveOccurred())
87+
})
88+
})
89+
90+
Context("when in-cluster", func() {
91+
kubeconfigFiles := map[string]string{
92+
"kubeconfig-multi-context": genKubeconfig("from-multi-env-1", "from-multi-env-2"),
93+
".kubeconfig": genKubeconfig("from-home"),
94+
}
95+
BeforeEach(func() {
96+
err := createFiles(kubeconfigFiles, dir)
97+
Expect(err).NotTo(HaveOccurred())
98+
99+
// override in-cluster config loader
100+
loadInClusterConfig = func() (*rest.Config, error) {
101+
return &rest.Config{Host: "from-in-cluster"}, nil
102+
}
103+
})
104+
AfterEach(func() { loadInClusterConfig = rest.InClusterConfig })
105+
106+
testCases := []testCase{
107+
{
108+
text: "should prefer the envvar over the in-cluster config",
109+
kubeconfigEnv: []string{"kubeconfig-multi-context"},
110+
wantHost: "from-multi-env-1",
111+
},
112+
{
113+
text: "should prefer in-cluster over the recommended home file",
114+
wantHost: "from-in-cluster",
115+
},
116+
}
117+
defineTests(testCases)
118+
})
119+
120+
Context("when outside the cluster", func() {
121+
kubeconfigFiles := map[string]string{
122+
"kubeconfig-flag": genKubeconfig("from-flag"),
123+
"kubeconfig-multi-context": genKubeconfig("from-multi-env-1", "from-multi-env-2"),
124+
"kubeconfig-env-1": genKubeconfig("from-env-1"),
125+
"kubeconfig-env-2": genKubeconfig("from-env-2"),
126+
".kubeconfig": genKubeconfig("from-home"),
127+
}
128+
BeforeEach(func() {
129+
err := createFiles(kubeconfigFiles, dir)
130+
Expect(err).NotTo(HaveOccurred())
131+
})
132+
testCases := []testCase{
133+
{
134+
text: "should use the --kubeconfig flag",
135+
kubeconfigFlag: "kubeconfig-flag",
136+
wantHost: "from-flag",
137+
},
138+
{
139+
text: "should use the envvar",
140+
kubeconfigEnv: []string{"kubeconfig-multi-context"},
141+
wantHost: "from-multi-env-1",
142+
},
143+
{
144+
text: "should use the recommended home file",
145+
wantHost: "from-home",
146+
},
147+
{
148+
text: "should prefer the flag over the envvar",
149+
kubeconfigFlag: "kubeconfig-flag",
150+
kubeconfigEnv: []string{"kubeconfig-multi-context"},
151+
wantHost: "from-flag",
152+
},
153+
{
154+
text: "should prefer the envvar over the recommended home file",
155+
kubeconfigEnv: []string{"kubeconfig-multi-context"},
156+
wantHost: "from-multi-env-1",
157+
},
158+
{
159+
text: "should allow overriding the API server URL",
160+
apiServerURL: "override",
161+
kubeconfigEnv: []string{"kubeconfig-multi-context"},
162+
wantHost: "override",
163+
},
164+
{
165+
text: "should allow overriding the context",
166+
context: "from-multi-env-2",
167+
kubeconfigEnv: []string{"kubeconfig-multi-context"},
168+
wantHost: "from-multi-env-2",
169+
},
170+
{
171+
text: "should support a multi-value envvar",
172+
context: "from-env-2",
173+
kubeconfigEnv: []string{"kubeconfig-env-1", "kubeconfig-env-2"},
174+
wantHost: "from-env-2",
175+
},
176+
}
177+
defineTests(testCases)
178+
})
179+
})
180+
})
181+
182+
func setConfigs(tc testCase, dir string) {
183+
// Set API Server URL
184+
apiServerURL = tc.apiServerURL
185+
186+
// Set kubeconfig flag value
187+
if len(tc.kubeconfigFlag) > 0 {
188+
kubeconfig = filepath.Join(dir, tc.kubeconfigFlag)
189+
}
190+
191+
// Set KUBECONFIG env value
192+
if len(tc.kubeconfigEnv) > 0 {
193+
kubeconfigEnvPaths := []string{}
194+
for _, k := range tc.kubeconfigEnv {
195+
kubeconfigEnvPaths = append(kubeconfigEnvPaths, filepath.Join(dir, k))
196+
}
197+
os.Setenv(clientcmd.RecommendedConfigPathEnvVar, strings.Join(kubeconfigEnvPaths, ":"))
198+
}
199+
}
200+
201+
func createFiles(files map[string]string, dir string) error {
202+
for path, data := range files {
203+
if err := ioutil.WriteFile(filepath.Join(dir, path), []byte(data), 0644); err != nil {
204+
return err
205+
}
206+
}
207+
return nil
208+
}
209+
210+
func genKubeconfig(contexts ...string) string {
211+
var sb strings.Builder
212+
sb.WriteString(`---
213+
apiVersion: v1
214+
kind: Config
215+
clusters:
216+
`)
217+
for _, ctx := range contexts {
218+
sb.WriteString(`- cluster:
219+
server: ` + ctx + `
220+
name: ` + ctx + `
221+
`)
222+
}
223+
sb.WriteString("contexts:\n")
224+
for _, ctx := range contexts {
225+
sb.WriteString(`- context:
226+
cluster: ` + ctx + `
227+
user: ` + ctx + `
228+
name: ` + ctx + `
229+
`)
230+
}
231+
232+
sb.WriteString("users:\n")
233+
for _, ctx := range contexts {
234+
sb.WriteString(`- name: ` + ctx + `
235+
`)
236+
}
237+
sb.WriteString("preferences: {}\n")
238+
if len(contexts) > 0 {
239+
sb.WriteString("current-context: " + contexts[0] + "\n")
240+
}
241+
242+
return sb.String()
243+
}

0 commit comments

Comments
 (0)