Skip to content

Commit f3bf9a4

Browse files
committed
Add support for multiple kubeconfigs in the KUBECONFIG env var.
1 parent 64d2de5 commit f3bf9a4

File tree

7 files changed

+218
-24
lines changed

7 files changed

+218
-24
lines changed

src/config.ts

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,6 @@ export class KubeConfig {
4545
*/
4646
public 'currentContext': string;
4747

48-
/**
49-
* Root directory for a config file driven config. Used for loading relative cert paths.
50-
*/
51-
public 'rootDirectory': string;
52-
5348
public getContexts() {
5449
return this.contexts;
5550
}
@@ -102,8 +97,9 @@ export class KubeConfig {
10297
}
10398

10499
public loadFromFile(file: string) {
105-
this.rootDirectory = path.dirname(file);
100+
const rootDirectory = path.dirname(file);
106101
this.loadFromString(fs.readFileSync(file, 'utf8'));
102+
this.makePathsAbsolute(rootDirectory);
107103
}
108104

109105
public applytoHTTPSOptions(opts: https.RequestOptions) {
@@ -201,9 +197,55 @@ export class KubeConfig {
201197
this.currentContext = contextName;
202198
}
203199

200+
public mergeConfig(config: KubeConfig) {
201+
this.currentContext = config.currentContext;
202+
config.clusters.forEach((cluster: Cluster) => {
203+
this.addCluster(cluster);
204+
});
205+
config.users.forEach((user: User) => {
206+
this.addUser(user);
207+
});
208+
config.contexts.forEach((ctx: Context) => {
209+
this.addContext(ctx);
210+
});
211+
}
212+
213+
public addCluster(cluster: Cluster) {
214+
this.clusters.forEach((c: Cluster, ix: number) => {
215+
if (c.name === cluster.name) {
216+
throw new Error(`Duplicate cluster: ${c.name}`);
217+
}
218+
});
219+
this.clusters.push(cluster);
220+
}
221+
222+
public addUser(user: User) {
223+
this.users.forEach((c: User, ix: number) => {
224+
if (c.name === user.name) {
225+
throw new Error(`Duplicate user: ${c.name}`);
226+
}
227+
});
228+
this.users.push(user);
229+
}
230+
231+
public addContext(ctx: Context) {
232+
this.contexts.forEach((c: Context, ix: number) => {
233+
if (c.name === ctx.name) {
234+
throw new Error(`Duplicate context: ${c.name}`);
235+
}
236+
});
237+
this.contexts.push(ctx);
238+
}
239+
204240
public loadFromDefault() {
205241
if (process.env.KUBECONFIG && process.env.KUBECONFIG.length > 0) {
206-
this.loadFromFile(process.env.KUBECONFIG);
242+
const files = process.env.KUBECONFIG.split(path.delimiter);
243+
this.loadFromFile(files[0]);
244+
for (let i = 1; i < files.length; i++) {
245+
const kc = new KubeConfig();
246+
kc.loadFromFile(files[i]);
247+
this.mergeConfig(kc);
248+
}
207249
return;
208250
}
209251
const home = findHomeDir();
@@ -247,6 +289,22 @@ export class KubeConfig {
247289
return apiClient;
248290
}
249291

292+
public makePathsAbsolute(rootDirectory: string) {
293+
this.clusters.forEach((cluster: Cluster) => {
294+
if (cluster.caFile) {
295+
cluster.caFile = makeAbsolutePath(rootDirectory, cluster.caFile);
296+
}
297+
});
298+
this.users.forEach((user: User) => {
299+
if (user.certFile) {
300+
user.certFile = makeAbsolutePath(rootDirectory, user.certFile);
301+
}
302+
if (user.keyFile) {
303+
user.keyFile = makeAbsolutePath(rootDirectory, user.keyFile);
304+
}
305+
});
306+
}
307+
250308
private getCurrentContextObject() {
251309
return this.getContextObject(this.currentContext);
252310
}
@@ -261,18 +319,15 @@ export class KubeConfig {
261319
if (cluster != null && cluster.skipTLSVerify) {
262320
opts.rejectUnauthorized = false;
263321
}
264-
const ca =
265-
cluster != null
266-
? bufferFromFileOrString(this.rootDirectory, cluster.caFile, cluster.caData)
267-
: null;
322+
const ca = cluster != null ? bufferFromFileOrString(cluster.caFile, cluster.caData) : null;
268323
if (ca) {
269324
opts.ca = ca;
270325
}
271-
const cert = bufferFromFileOrString(this.rootDirectory, user.certFile, user.certData);
326+
const cert = bufferFromFileOrString(user.certFile, user.certData);
272327
if (cert) {
273328
opts.cert = cert;
274329
}
275-
const key = bufferFromFileOrString(this.rootDirectory, user.keyFile, user.keyData);
330+
const key = bufferFromFileOrString(user.keyFile, user.keyData);
276331
if (key) {
277332
opts.key = key;
278333
}
@@ -363,12 +418,16 @@ export class Config {
363418
}
364419
}
365420

421+
export function makeAbsolutePath(root: string, file: string): string {
422+
if (!root || path.isAbsolute(file)) {
423+
return file;
424+
}
425+
return path.join(root, file);
426+
}
427+
366428
// This is public really only for testing.
367-
export function bufferFromFileOrString(root?: string, file?: string, data?: string): Buffer | null {
429+
export function bufferFromFileOrString(file?: string, data?: string): Buffer | null {
368430
if (file) {
369-
if (!path.isAbsolute(file) && root) {
370-
file = path.join(root, file);
371-
}
372431
return fs.readFileSync(file);
373432
}
374433
if (data) {

src/config_test.ts

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@ import { dirname, join } from 'path';
55
import { expect } from 'chai';
66
import mockfs from 'mock-fs';
77
import * as requestlib from 'request';
8+
import * as path from 'path';
89

910
import { CoreV1Api } from './api';
10-
import { bufferFromFileOrString, findHomeDir, findObject, KubeConfig, Named } from './config';
11+
import { bufferFromFileOrString, findHomeDir, findObject, KubeConfig, makeAbsolutePath } from './config';
1112
import { Cluster, newClusters, newContexts, newUsers, User } from './config_types';
1213

1314
const kcFileName = 'testdata/kubeconfig.yaml';
15+
const kc2FileName = 'testdata/kubeconfig-2.yaml';
16+
const kcDupeCluster = 'testdata/kubeconfig-dupe-cluster.yaml';
17+
const kcDupeContext = 'testdata/kubeconfig-dupe-context.yaml';
18+
const kcDupeUser = 'testdata/kubeconfig-dupe-user.yaml';
19+
1420
const kcNoUserFileName = 'testdata/empty-user-kubeconfig.yaml';
1521

1622
/* tslint:disable: no-empty */
@@ -174,7 +180,6 @@ describe('KubeConfig', () => {
174180
it('should load the kubeconfig file properly', () => {
175181
const kc = new KubeConfig();
176182
kc.loadFromFile(kcFileName);
177-
expect(kc.rootDirectory).to.equal(dirname(kcFileName));
178183
validateFileLoad(kc);
179184
});
180185
it('should fail to load a missing kubeconfig file', () => {
@@ -890,6 +895,52 @@ describe('KubeConfig', () => {
890895
});
891896
});
892897

898+
describe('load from multi $KUBECONFIG', () => {
899+
it('should load from multiple files', () => {
900+
process.env.KUBECONFIG = kcFileName + path.delimiter + kc2FileName;
901+
902+
const kc = new KubeConfig();
903+
kc.loadFromDefault();
904+
905+
// 2 in the first config, 1 in the second config
906+
expect(kc.clusters.length).to.equal(3);
907+
expect(kc.users.length).to.equal(6);
908+
expect(kc.contexts.length).to.equal(4);
909+
expect(kc.getCurrentContext()).to.equal('contextA');
910+
});
911+
it('should throw with duplicate clusters', () => {
912+
process.env.KUBECONFIG = kcFileName + path.delimiter + kcDupeCluster;
913+
914+
const kc = new KubeConfig();
915+
expect(() => kc.loadFromDefault()).to.throw('Duplicate cluster: cluster1');
916+
});
917+
918+
it('should throw with duplicate contexts', () => {
919+
process.env.KUBECONFIG = kcFileName + path.delimiter + kcDupeContext;
920+
921+
const kc = new KubeConfig();
922+
expect(() => kc.loadFromDefault()).to.throw('Duplicate context: context1');
923+
});
924+
925+
it('should throw with duplicate users', () => {
926+
process.env.KUBECONFIG = kcFileName + path.delimiter + kcDupeUser;
927+
928+
const kc = new KubeConfig();
929+
expect(() => kc.loadFromDefault()).to.throw('Duplicate user: user1');
930+
});
931+
});
932+
933+
describe('MakeAbsolutePaths', () => {
934+
it('should correctly make absolute paths', () => {
935+
const relative = 'foo/bar';
936+
const absolute = '/tmp/foo/bar';
937+
const root = '/usr/';
938+
939+
expect(makeAbsolutePath(root, relative)).to.equal('/usr/foo/bar');
940+
expect(makeAbsolutePath(root, absolute)).to.equal(absolute);
941+
});
942+
});
943+
893944
describe('loadFromDefault', () => {
894945
it('should load from $KUBECONFIG', () => {
895946
process.env.KUBECONFIG = kcFileName;
@@ -1047,7 +1098,7 @@ describe('KubeConfig', () => {
10471098
},
10481099
};
10491100
mockfs(arg);
1050-
const inputData = bufferFromFileOrString('configDir', 'config');
1101+
const inputData = bufferFromFileOrString('configDir/config');
10511102
expect(inputData).to.not.equal(null);
10521103
if (inputData) {
10531104
expect(inputData.toString()).to.equal(data);
@@ -1060,7 +1111,7 @@ describe('KubeConfig', () => {
10601111
config: data,
10611112
};
10621113
mockfs(arg);
1063-
const inputData = bufferFromFileOrString(undefined, 'config');
1114+
const inputData = bufferFromFileOrString('config');
10641115
expect(inputData).to.not.equal(null);
10651116
if (inputData) {
10661117
expect(inputData.toString()).to.equal(data);

src/config_types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ExtensionsV1beta1RollbackConfig } from './api';
55
export interface Cluster {
66
readonly name: string;
77
readonly caData?: string;
8-
readonly caFile?: string;
8+
caFile?: string;
99
readonly server: string;
1010
readonly skipTLSVerify: boolean;
1111
}
@@ -38,10 +38,10 @@ function clusterIterator(): u.ListIterator<any, Cluster> {
3838
export interface User {
3939
readonly name: string;
4040
readonly certData?: string;
41-
readonly certFile?: string;
41+
certFile?: string;
4242
readonly exec?: any;
4343
readonly keyData?: string;
44-
readonly keyFile?: string;
44+
keyFile?: string;
4545
readonly authProvider?: any;
4646
readonly token?: string;
4747
readonly username?: string;

testdata/kubeconfig-2.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
apiVersion: v1
2+
clusters:
3+
- cluster:
4+
certificate-authority-data: Q0FEQVRA
5+
server: http://example2.com
6+
name: clusterA
7+
8+
contexts:
9+
- context:
10+
cluster: clusterA
11+
user: userA
12+
name: contextA
13+
14+
current-context: contextA
15+
kind: Config
16+
preferences: {}
17+
users:
18+
- name: userA
19+
user:
20+
client-certificate-data: XVNFUl9DQURBVEE=
21+
client-key-data: XVNFUl9DS0RBVEE=
22+
- name: userB
23+
user:
24+
client-certificate-data: XVNFUjJfQ0FEQVRB
25+
client-key-data: XVNFUjJfQ0tEQVRB
26+
- name: userC
27+
user:
28+
username: foo
29+
password: bar
30+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: v1
2+
clusters:
3+
- cluster:
4+
certificate-authority-data: Q0FEQVRB
5+
server: http://example.com
6+
name: cluster1
7+
8+
contexts:
9+
current-context: context2
10+
kind: Config
11+
preferences: {}
12+
users:
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
apiVersion: v1
2+
clusters:
3+
- cluster:
4+
certificate-authority-data: Q0FEQVRB
5+
server: http://example.com
6+
name: clusterA
7+
8+
contexts:
9+
- context:
10+
cluster: cluster1
11+
user: user1
12+
name: context1
13+
14+
current-context: context2
15+
kind: Config
16+
preferences: {}
17+
users:
18+
- name: userA
19+
user:
20+
client-certificate-data: VVNFUl9DQURBVEE=
21+
client-key-data: VVNFUl9DS0RBVEE=

testdata/kubeconfig-dupe-user.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
apiVersion: v1
2+
clusters:
3+
- cluster:
4+
certificate-authority-data: Q0FEQVRB
5+
server: http://example.com
6+
name: clusterA
7+
8+
contexts:
9+
- context:
10+
cluster: clusterA
11+
user: user1
12+
name: contextA
13+
14+
current-context: context2
15+
kind: Config
16+
preferences: {}
17+
users:
18+
- name: user1
19+
user:
20+
client-certificate-data: VVNFUl9DQURBVEE=
21+
client-key-data: VVNFUl9DS0RBVEE=

0 commit comments

Comments
 (0)