Skip to content

Commit ef0c2f3

Browse files
authored
Merge pull request #500 from Nokel81/config-load-filter
Add option to filter invalid items when loading a config
2 parents a162209 + ce450c8 commit ef0c2f3

File tree

5 files changed

+232
-75
lines changed

5 files changed

+232
-75
lines changed

src/config.ts

+16-15
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Authenticator } from './auth';
1313
import { CloudAuth } from './cloud_auth';
1414
import {
1515
Cluster,
16+
ConfigOptions,
1617
Context,
1718
exportCluster,
1819
exportContext,
@@ -31,9 +32,9 @@ function fileExists(filepath: string): boolean {
3132
try {
3233
fs.accessSync(filepath);
3334
return true;
34-
// tslint:disable-next-line:no-empty
35-
} catch (ignore) {}
36-
return false;
35+
} catch (ignore) {
36+
return false;
37+
}
3738
}
3839

3940
export class KubeConfig {
@@ -121,9 +122,9 @@ export class KubeConfig {
121122
return findObject(this.users, name, 'user');
122123
}
123124

124-
public loadFromFile(file: string) {
125+
public loadFromFile(file: string, opts?: Partial<ConfigOptions>) {
125126
const rootDirectory = path.dirname(file);
126-
this.loadFromString(fs.readFileSync(file, 'utf8'));
127+
this.loadFromString(fs.readFileSync(file, 'utf8'), opts);
127128
this.makePathsAbsolute(rootDirectory);
128129
}
129130

@@ -155,11 +156,11 @@ export class KubeConfig {
155156
}
156157
}
157158

158-
public loadFromString(config: string) {
159-
const obj = yaml.safeLoad(config) as any;
160-
this.clusters = newClusters(obj.clusters);
161-
this.contexts = newContexts(obj.contexts);
162-
this.users = newUsers(obj.users);
159+
public loadFromString(config: string, opts?: Partial<ConfigOptions>) {
160+
const obj = yaml.safeLoad(config);
161+
this.clusters = newClusters(obj.clusters, opts);
162+
this.contexts = newContexts(obj.contexts, opts);
163+
this.users = newUsers(obj.users, opts);
163164
this.currentContext = obj['current-context'];
164165
}
165166

@@ -279,13 +280,13 @@ export class KubeConfig {
279280
this.contexts.push(ctx);
280281
}
281282

282-
public loadFromDefault() {
283+
public loadFromDefault(opts?: Partial<ConfigOptions>) {
283284
if (process.env.KUBECONFIG && process.env.KUBECONFIG.length > 0) {
284285
const files = process.env.KUBECONFIG.split(path.delimiter);
285-
this.loadFromFile(files[0]);
286+
this.loadFromFile(files[0], opts);
286287
for (let i = 1; i < files.length; i++) {
287288
const kc = new KubeConfig();
288-
kc.loadFromFile(files[i]);
289+
kc.loadFromFile(files[i], opts);
289290
this.mergeConfig(kc);
290291
}
291292
return;
@@ -294,7 +295,7 @@ export class KubeConfig {
294295
if (home) {
295296
const config = path.join(home, '.kube', 'config');
296297
if (fileExists(config)) {
297-
this.loadFromFile(config);
298+
this.loadFromFile(config, opts);
298299
return;
299300
}
300301
}
@@ -303,7 +304,7 @@ export class KubeConfig {
303304
try {
304305
const result = execa.sync('wsl.exe', ['cat', shelljs.homedir() + '/.kube/config']);
305306
if (result.code === 0) {
306-
this.loadFromString(result.stdout);
307+
this.loadFromString(result.std, opts);
307308
return;
308309
}
309310
} catch (err) {

src/config_test.ts

+23-4
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { fs } from 'mock-fs';
1212
import * as os from 'os';
1313
import { CoreV1Api } from './api';
1414
import { bufferFromFileOrString, findHomeDir, findObject, KubeConfig, makeAbsolutePath } from './config';
15-
import { Cluster, newClusters, newContexts, newUsers, User } from './config_types';
15+
import { Cluster, newClusters, newContexts, newUsers, User, ActionOnInvalid } from './config_types';
1616
import { isUndefined } from 'util';
1717

1818
const kcFileName = 'testdata/kubeconfig.yaml';
@@ -22,6 +22,8 @@ const kcDupeContext = 'testdata/kubeconfig-dupe-context.yaml';
2222
const kcDupeUser = 'testdata/kubeconfig-dupe-user.yaml';
2323

2424
const kcNoUserFileName = 'testdata/empty-user-kubeconfig.yaml';
25+
const kcInvalidContextFileName = 'testdata/empty-context-kubeconfig.yaml';
26+
const kcInvalidClusterFileName = 'testdata/empty-cluster-kubeconfig.yaml';
2527

2628
/* tslint:disable: no-empty */
2729
describe('Config', () => {});
@@ -192,9 +194,26 @@ describe('KubeConfig', () => {
192194
validateFileLoad(kc);
193195
});
194196
it('should fail to load a missing kubeconfig file', () => {
195-
// TODO: make the error check work
196-
// let kc = new KubeConfig();
197-
// expect(kc.loadFromFile("missing.yaml")).to.throw();
197+
const kc = new KubeConfig();
198+
expect(kc.loadFromFile.bind('missing.yaml')).to.throw();
199+
});
200+
201+
describe('filter vs throw tests', () => {
202+
it('works for invalid users', () => {
203+
const kc = new KubeConfig();
204+
kc.loadFromFile(kcNoUserFileName, { onInvalidEntry: ActionOnInvalid.FILTER });
205+
expect(kc.getUsers().length).to.be.eq(2);
206+
});
207+
it('works for invalid contexts', () => {
208+
const kc = new KubeConfig();
209+
kc.loadFromFile(kcInvalidContextFileName, { onInvalidEntry: ActionOnInvalid.FILTER });
210+
expect(kc.getContexts().length).to.be.eq(2);
211+
});
212+
it('works for invalid clusters', () => {
213+
const kc = new KubeConfig();
214+
kc.loadFromFile(kcInvalidClusterFileName, { onInvalidEntry: ActionOnInvalid.FILTER });
215+
expect(kc.getClusters().length).to.be.eq(1);
216+
});
198217
});
199218
});
200219

src/config_types.ts

+107-56
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
import * as fs from 'fs';
2-
import * as u from 'underscore';
2+
import * as _ from 'underscore';
3+
4+
export enum ActionOnInvalid {
5+
THROW = 'throw',
6+
FILTER = 'filter',
7+
}
8+
9+
export interface ConfigOptions {
10+
onInvalidEntry: ActionOnInvalid;
11+
}
12+
13+
function defaultNewConfigOptions(): ConfigOptions {
14+
return {
15+
onInvalidEntry: ActionOnInvalid.THROW,
16+
};
17+
}
318

419
export interface Cluster {
520
readonly name: string;
@@ -9,8 +24,10 @@ export interface Cluster {
924
readonly skipTLSVerify: boolean;
1025
}
1126

12-
export function newClusters(a: any): Cluster[] {
13-
return u.map(a, clusterIterator());
27+
export function newClusters(a: any, opts?: Partial<ConfigOptions>): Cluster[] {
28+
const options = Object.assign(defaultNewConfigOptions(), opts || {});
29+
30+
return _.compact(_.map(a, clusterIterator(options.onInvalidEntry)));
1431
}
1532

1633
export function exportCluster(cluster: Cluster): any {
@@ -25,24 +42,34 @@ export function exportCluster(cluster: Cluster): any {
2542
};
2643
}
2744

28-
function clusterIterator(): u.ListIterator<any, Cluster> {
29-
return (elt: any, i: number, list: u.List<any>): Cluster => {
30-
if (!elt.name) {
31-
throw new Error(`clusters[${i}].name is missing`);
32-
}
33-
if (!elt.cluster) {
34-
throw new Error(`clusters[${i}].cluster is missing`);
45+
function clusterIterator(onInvalidEntry: ActionOnInvalid): _.ListIterator<any, Cluster | null> {
46+
return (elt: any, i: number, list: _.List<any>): Cluster | null => {
47+
try {
48+
if (!elt.name) {
49+
throw new Error(`clusters[${i}].name is missing`);
50+
}
51+
if (!elt.cluster) {
52+
throw new Error(`clusters[${i}].cluster is missing`);
53+
}
54+
if (!elt.cluster.server) {
55+
throw new Error(`clusters[${i}].cluster.server is missing`);
56+
}
57+
return {
58+
caData: elt.cluster['certificate-authority-data'],
59+
caFile: elt.cluster['certificate-authority'],
60+
name: elt.name,
61+
server: elt.cluster.server,
62+
skipTLSVerify: elt.cluster['insecure-skip-tls-verify'] === true,
63+
};
64+
} catch (err) {
65+
switch (onInvalidEntry) {
66+
case ActionOnInvalid.FILTER:
67+
return null;
68+
default:
69+
case ActionOnInvalid.THROW:
70+
throw err;
71+
}
3572
}
36-
if (!elt.cluster.server) {
37-
throw new Error(`clusters[${i}].cluster.server is missing`);
38-
}
39-
return {
40-
caData: elt.cluster['certificate-authority-data'],
41-
caFile: elt.cluster['certificate-authority'],
42-
name: elt.name,
43-
server: elt.cluster.server,
44-
skipTLSVerify: elt.cluster['insecure-skip-tls-verify'] === true,
45-
};
4673
};
4774
}
4875

@@ -59,8 +86,10 @@ export interface User {
5986
readonly password?: string;
6087
}
6188

62-
export function newUsers(a: any): User[] {
63-
return u.map(a, userIterator());
89+
export function newUsers(a: any, opts?: Partial<ConfigOptions>): User[] {
90+
const options = Object.assign(defaultNewConfigOptions(), opts || {});
91+
92+
return _.compact(_.map(a, userIterator(options.onInvalidEntry)));
6493
}
6594

6695
export function exportUser(user: User): any {
@@ -80,23 +109,33 @@ export function exportUser(user: User): any {
80109
};
81110
}
82111

83-
function userIterator(): u.ListIterator<any, User> {
84-
return (elt: any, i: number, list: u.List<any>): User => {
85-
if (!elt.name) {
86-
throw new Error(`users[${i}].name is missing`);
112+
function userIterator(onInvalidEntry: ActionOnInvalid): _.ListIterator<any, User | null> {
113+
return (elt: any, i: number, list: _.List<any>): User | null => {
114+
try {
115+
if (!elt.name) {
116+
throw new Error(`users[${i}].name is missing`);
117+
}
118+
return {
119+
authProvider: elt.user ? elt.user['auth-provider'] : null,
120+
certData: elt.user ? elt.user['client-certificate-data'] : null,
121+
certFile: elt.user ? elt.user['client-certificate'] : null,
122+
exec: elt.user ? elt.user.exec : null,
123+
keyData: elt.user ? elt.user['client-key-data'] : null,
124+
keyFile: elt.user ? elt.user['client-key'] : null,
125+
name: elt.name,
126+
token: findToken(elt.user),
127+
password: elt.user ? elt.user.password : null,
128+
username: elt.user ? elt.user.username : null,
129+
};
130+
} catch (err) {
131+
switch (onInvalidEntry) {
132+
case ActionOnInvalid.FILTER:
133+
return null;
134+
default:
135+
case ActionOnInvalid.THROW:
136+
throw err;
137+
}
87138
}
88-
return {
89-
authProvider: elt.user ? elt.user['auth-provider'] : null,
90-
certData: elt.user ? elt.user['client-certificate-data'] : null,
91-
certFile: elt.user ? elt.user['client-certificate'] : null,
92-
exec: elt.user ? elt.user.exec : null,
93-
keyData: elt.user ? elt.user['client-key-data'] : null,
94-
keyFile: elt.user ? elt.user['client-key'] : null,
95-
name: elt.name,
96-
token: findToken(elt.user),
97-
password: elt.user ? elt.user.password : null,
98-
username: elt.user ? elt.user.username : null,
99-
};
100139
};
101140
}
102141

@@ -118,8 +157,10 @@ export interface Context {
118157
readonly namespace?: string;
119158
}
120159

121-
export function newContexts(a: any): Context[] {
122-
return u.map(a, contextIterator());
160+
export function newContexts(a: any, opts?: Partial<ConfigOptions>): Context[] {
161+
const options = Object.assign(defaultNewConfigOptions(), opts || {});
162+
163+
return _.compact(_.map(a, contextIterator(options.onInvalidEntry)));
123164
}
124165

125166
export function exportContext(ctx: Context): any {
@@ -129,22 +170,32 @@ export function exportContext(ctx: Context): any {
129170
};
130171
}
131172

132-
function contextIterator(): u.ListIterator<any, Context> {
133-
return (elt: any, i: number, list: u.List<any>): Context => {
134-
if (!elt.name) {
135-
throw new Error(`contexts[${i}].name is missing`);
136-
}
137-
if (!elt.context) {
138-
throw new Error(`contexts[${i}].context is missing`);
139-
}
140-
if (!elt.context.cluster) {
141-
throw new Error(`contexts[${i}].context.cluster is missing`);
173+
function contextIterator(onInvalidEntry: ActionOnInvalid): _.ListIterator<any, Context | null> {
174+
return (elt: any, i: number, list: _.List<any>): Context | null => {
175+
try {
176+
if (!elt.name) {
177+
throw new Error(`contexts[${i}].name is missing`);
178+
}
179+
if (!elt.context) {
180+
throw new Error(`contexts[${i}].context is missing`);
181+
}
182+
if (!elt.context.cluster) {
183+
throw new Error(`contexts[${i}].context.cluster is missing`);
184+
}
185+
return {
186+
cluster: elt.context.cluster,
187+
name: elt.name,
188+
user: elt.context.user || undefined,
189+
namespace: elt.context.namespace || undefined,
190+
};
191+
} catch (err) {
192+
switch (onInvalidEntry) {
193+
case ActionOnInvalid.FILTER:
194+
return null;
195+
default:
196+
case ActionOnInvalid.THROW:
197+
throw err;
198+
}
142199
}
143-
return {
144-
cluster: elt.context.cluster,
145-
name: elt.name,
146-
user: elt.context.user || undefined,
147-
namespace: elt.context.namespace || undefined,
148-
};
149200
};
150201
}
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
apiVersion: v1
2+
clusters:
3+
- cluster:
4+
certificate-authority-data: Q0FEQVRB
5+
server: http://example.com
6+
name: ""
7+
- cluster:
8+
certificate-authority-data: Q0FEQVRBMg==
9+
server: http://example2.com
10+
insecure-skip-tls-verify: true
11+
name: cluster2
12+
13+
contexts:
14+
- context:
15+
cluster: cluster1
16+
user: user1
17+
name: context1
18+
- context:
19+
cluster: cluster2
20+
namespace: namespace2
21+
user: user2
22+
name: context2
23+
- context:
24+
cluster: cluster2
25+
user: user3
26+
name: passwd
27+
28+
current-context: context2
29+
kind: Config
30+
preferences: {}
31+
users:
32+
- name: user1
33+
user:
34+
client-certificate-data: VVNFUl9DQURBVEE=
35+
client-key-data: VVNFUl9DS0RBVEE=
36+
- name: user2
37+
user:
38+
client-certificate-data: VVNFUjJfQ0FEQVRB
39+
client-key-data: VVNFUjJfQ0tEQVRB
40+
- name: user3
41+
user:
42+
username: foo
43+
password: bar

0 commit comments

Comments
 (0)