Skip to content

Commit

Permalink
Merge pull request #500 from Nokel81/config-load-filter
Browse files Browse the repository at this point in the history
Add option to filter invalid items when loading a config
  • Loading branch information
k8s-ci-robot authored Sep 17, 2020
2 parents a162209 + ce450c8 commit ef0c2f3
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 75 deletions.
31 changes: 16 additions & 15 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Authenticator } from './auth';
import { CloudAuth } from './cloud_auth';
import {
Cluster,
ConfigOptions,
Context,
exportCluster,
exportContext,
Expand All @@ -31,9 +32,9 @@ function fileExists(filepath: string): boolean {
try {
fs.accessSync(filepath);
return true;
// tslint:disable-next-line:no-empty
} catch (ignore) {}
return false;
} catch (ignore) {
return false;
}
}

export class KubeConfig {
Expand Down Expand Up @@ -121,9 +122,9 @@ export class KubeConfig {
return findObject(this.users, name, 'user');
}

public loadFromFile(file: string) {
public loadFromFile(file: string, opts?: Partial<ConfigOptions>) {
const rootDirectory = path.dirname(file);
this.loadFromString(fs.readFileSync(file, 'utf8'));
this.loadFromString(fs.readFileSync(file, 'utf8'), opts);
this.makePathsAbsolute(rootDirectory);
}

Expand Down Expand Up @@ -155,11 +156,11 @@ export class KubeConfig {
}
}

public loadFromString(config: string) {
const obj = yaml.safeLoad(config) as any;
this.clusters = newClusters(obj.clusters);
this.contexts = newContexts(obj.contexts);
this.users = newUsers(obj.users);
public loadFromString(config: string, opts?: Partial<ConfigOptions>) {
const obj = yaml.safeLoad(config);
this.clusters = newClusters(obj.clusters, opts);
this.contexts = newContexts(obj.contexts, opts);
this.users = newUsers(obj.users, opts);
this.currentContext = obj['current-context'];
}

Expand Down Expand Up @@ -279,13 +280,13 @@ export class KubeConfig {
this.contexts.push(ctx);
}

public loadFromDefault() {
public loadFromDefault(opts?: Partial<ConfigOptions>) {
if (process.env.KUBECONFIG && process.env.KUBECONFIG.length > 0) {
const files = process.env.KUBECONFIG.split(path.delimiter);
this.loadFromFile(files[0]);
this.loadFromFile(files[0], opts);
for (let i = 1; i < files.length; i++) {
const kc = new KubeConfig();
kc.loadFromFile(files[i]);
kc.loadFromFile(files[i], opts);
this.mergeConfig(kc);
}
return;
Expand All @@ -294,7 +295,7 @@ export class KubeConfig {
if (home) {
const config = path.join(home, '.kube', 'config');
if (fileExists(config)) {
this.loadFromFile(config);
this.loadFromFile(config, opts);
return;
}
}
Expand All @@ -303,7 +304,7 @@ export class KubeConfig {
try {
const result = execa.sync('wsl.exe', ['cat', shelljs.homedir() + '/.kube/config']);
if (result.code === 0) {
this.loadFromString(result.stdout);
this.loadFromString(result.std, opts);
return;
}
} catch (err) {
Expand Down
27 changes: 23 additions & 4 deletions src/config_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { fs } from 'mock-fs';
import * as os from 'os';
import { CoreV1Api } from './api';
import { bufferFromFileOrString, findHomeDir, findObject, KubeConfig, makeAbsolutePath } from './config';
import { Cluster, newClusters, newContexts, newUsers, User } from './config_types';
import { Cluster, newClusters, newContexts, newUsers, User, ActionOnInvalid } from './config_types';
import { isUndefined } from 'util';

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

const kcNoUserFileName = 'testdata/empty-user-kubeconfig.yaml';
const kcInvalidContextFileName = 'testdata/empty-context-kubeconfig.yaml';
const kcInvalidClusterFileName = 'testdata/empty-cluster-kubeconfig.yaml';

/* tslint:disable: no-empty */
describe('Config', () => {});
Expand Down Expand Up @@ -192,9 +194,26 @@ describe('KubeConfig', () => {
validateFileLoad(kc);
});
it('should fail to load a missing kubeconfig file', () => {
// TODO: make the error check work
// let kc = new KubeConfig();
// expect(kc.loadFromFile("missing.yaml")).to.throw();
const kc = new KubeConfig();
expect(kc.loadFromFile.bind('missing.yaml')).to.throw();
});

describe('filter vs throw tests', () => {
it('works for invalid users', () => {
const kc = new KubeConfig();
kc.loadFromFile(kcNoUserFileName, { onInvalidEntry: ActionOnInvalid.FILTER });
expect(kc.getUsers().length).to.be.eq(2);
});
it('works for invalid contexts', () => {
const kc = new KubeConfig();
kc.loadFromFile(kcInvalidContextFileName, { onInvalidEntry: ActionOnInvalid.FILTER });
expect(kc.getContexts().length).to.be.eq(2);
});
it('works for invalid clusters', () => {
const kc = new KubeConfig();
kc.loadFromFile(kcInvalidClusterFileName, { onInvalidEntry: ActionOnInvalid.FILTER });
expect(kc.getClusters().length).to.be.eq(1);
});
});
});

Expand Down
163 changes: 107 additions & 56 deletions src/config_types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import * as fs from 'fs';
import * as u from 'underscore';
import * as _ from 'underscore';

export enum ActionOnInvalid {
THROW = 'throw',
FILTER = 'filter',
}

export interface ConfigOptions {
onInvalidEntry: ActionOnInvalid;
}

function defaultNewConfigOptions(): ConfigOptions {
return {
onInvalidEntry: ActionOnInvalid.THROW,
};
}

export interface Cluster {
readonly name: string;
Expand All @@ -9,8 +24,10 @@ export interface Cluster {
readonly skipTLSVerify: boolean;
}

export function newClusters(a: any): Cluster[] {
return u.map(a, clusterIterator());
export function newClusters(a: any, opts?: Partial<ConfigOptions>): Cluster[] {
const options = Object.assign(defaultNewConfigOptions(), opts || {});

return _.compact(_.map(a, clusterIterator(options.onInvalidEntry)));
}

export function exportCluster(cluster: Cluster): any {
Expand All @@ -25,24 +42,34 @@ export function exportCluster(cluster: Cluster): any {
};
}

function clusterIterator(): u.ListIterator<any, Cluster> {
return (elt: any, i: number, list: u.List<any>): Cluster => {
if (!elt.name) {
throw new Error(`clusters[${i}].name is missing`);
}
if (!elt.cluster) {
throw new Error(`clusters[${i}].cluster is missing`);
function clusterIterator(onInvalidEntry: ActionOnInvalid): _.ListIterator<any, Cluster | null> {
return (elt: any, i: number, list: _.List<any>): Cluster | null => {
try {
if (!elt.name) {
throw new Error(`clusters[${i}].name is missing`);
}
if (!elt.cluster) {
throw new Error(`clusters[${i}].cluster is missing`);
}
if (!elt.cluster.server) {
throw new Error(`clusters[${i}].cluster.server is missing`);
}
return {
caData: elt.cluster['certificate-authority-data'],
caFile: elt.cluster['certificate-authority'],
name: elt.name,
server: elt.cluster.server,
skipTLSVerify: elt.cluster['insecure-skip-tls-verify'] === true,
};
} catch (err) {
switch (onInvalidEntry) {
case ActionOnInvalid.FILTER:
return null;
default:
case ActionOnInvalid.THROW:
throw err;
}
}
if (!elt.cluster.server) {
throw new Error(`clusters[${i}].cluster.server is missing`);
}
return {
caData: elt.cluster['certificate-authority-data'],
caFile: elt.cluster['certificate-authority'],
name: elt.name,
server: elt.cluster.server,
skipTLSVerify: elt.cluster['insecure-skip-tls-verify'] === true,
};
};
}

Expand All @@ -59,8 +86,10 @@ export interface User {
readonly password?: string;
}

export function newUsers(a: any): User[] {
return u.map(a, userIterator());
export function newUsers(a: any, opts?: Partial<ConfigOptions>): User[] {
const options = Object.assign(defaultNewConfigOptions(), opts || {});

return _.compact(_.map(a, userIterator(options.onInvalidEntry)));
}

export function exportUser(user: User): any {
Expand All @@ -80,23 +109,33 @@ export function exportUser(user: User): any {
};
}

function userIterator(): u.ListIterator<any, User> {
return (elt: any, i: number, list: u.List<any>): User => {
if (!elt.name) {
throw new Error(`users[${i}].name is missing`);
function userIterator(onInvalidEntry: ActionOnInvalid): _.ListIterator<any, User | null> {
return (elt: any, i: number, list: _.List<any>): User | null => {
try {
if (!elt.name) {
throw new Error(`users[${i}].name is missing`);
}
return {
authProvider: elt.user ? elt.user['auth-provider'] : null,
certData: elt.user ? elt.user['client-certificate-data'] : null,
certFile: elt.user ? elt.user['client-certificate'] : null,
exec: elt.user ? elt.user.exec : null,
keyData: elt.user ? elt.user['client-key-data'] : null,
keyFile: elt.user ? elt.user['client-key'] : null,
name: elt.name,
token: findToken(elt.user),
password: elt.user ? elt.user.password : null,
username: elt.user ? elt.user.username : null,
};
} catch (err) {
switch (onInvalidEntry) {
case ActionOnInvalid.FILTER:
return null;
default:
case ActionOnInvalid.THROW:
throw err;
}
}
return {
authProvider: elt.user ? elt.user['auth-provider'] : null,
certData: elt.user ? elt.user['client-certificate-data'] : null,
certFile: elt.user ? elt.user['client-certificate'] : null,
exec: elt.user ? elt.user.exec : null,
keyData: elt.user ? elt.user['client-key-data'] : null,
keyFile: elt.user ? elt.user['client-key'] : null,
name: elt.name,
token: findToken(elt.user),
password: elt.user ? elt.user.password : null,
username: elt.user ? elt.user.username : null,
};
};
}

Expand All @@ -118,8 +157,10 @@ export interface Context {
readonly namespace?: string;
}

export function newContexts(a: any): Context[] {
return u.map(a, contextIterator());
export function newContexts(a: any, opts?: Partial<ConfigOptions>): Context[] {
const options = Object.assign(defaultNewConfigOptions(), opts || {});

return _.compact(_.map(a, contextIterator(options.onInvalidEntry)));
}

export function exportContext(ctx: Context): any {
Expand All @@ -129,22 +170,32 @@ export function exportContext(ctx: Context): any {
};
}

function contextIterator(): u.ListIterator<any, Context> {
return (elt: any, i: number, list: u.List<any>): Context => {
if (!elt.name) {
throw new Error(`contexts[${i}].name is missing`);
}
if (!elt.context) {
throw new Error(`contexts[${i}].context is missing`);
}
if (!elt.context.cluster) {
throw new Error(`contexts[${i}].context.cluster is missing`);
function contextIterator(onInvalidEntry: ActionOnInvalid): _.ListIterator<any, Context | null> {
return (elt: any, i: number, list: _.List<any>): Context | null => {
try {
if (!elt.name) {
throw new Error(`contexts[${i}].name is missing`);
}
if (!elt.context) {
throw new Error(`contexts[${i}].context is missing`);
}
if (!elt.context.cluster) {
throw new Error(`contexts[${i}].context.cluster is missing`);
}
return {
cluster: elt.context.cluster,
name: elt.name,
user: elt.context.user || undefined,
namespace: elt.context.namespace || undefined,
};
} catch (err) {
switch (onInvalidEntry) {
case ActionOnInvalid.FILTER:
return null;
default:
case ActionOnInvalid.THROW:
throw err;
}
}
return {
cluster: elt.context.cluster,
name: elt.name,
user: elt.context.user || undefined,
namespace: elt.context.namespace || undefined,
};
};
}
43 changes: 43 additions & 0 deletions testdata/empty-cluster-kubeconfig.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: Q0FEQVRB
server: http://example.com
name: ""
- cluster:
certificate-authority-data: Q0FEQVRBMg==
server: http://example2.com
insecure-skip-tls-verify: true
name: cluster2

contexts:
- context:
cluster: cluster1
user: user1
name: context1
- context:
cluster: cluster2
namespace: namespace2
user: user2
name: context2
- context:
cluster: cluster2
user: user3
name: passwd

current-context: context2
kind: Config
preferences: {}
users:
- name: user1
user:
client-certificate-data: VVNFUl9DQURBVEE=
client-key-data: VVNFUl9DS0RBVEE=
- name: user2
user:
client-certificate-data: VVNFUjJfQ0FEQVRB
client-key-data: VVNFUjJfQ0tEQVRB
- name: user3
user:
username: foo
password: bar
Loading

0 comments on commit ef0c2f3

Please sign in to comment.