Skip to content

Commit d9987e4

Browse files
authored
Merge pull request #158 from brendandburns/auth
Add support for exec based auth.
2 parents daa0b38 + 976446b commit d9987e4

File tree

6 files changed

+152
-41
lines changed

6 files changed

+152
-41
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949
"base-64": "^0.1.0",
5050
"bluebird": "^3.5.2",
5151
"byline": "^5.0.0",
52-
"execa": "^1.0.0",
5352
"isomorphic-ws": "^4.0.1",
5453
"js-yaml": "^3.12.0",
5554
"jsonpath": "^1.0.0",

src/auth.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { User } from './config_types';
2+
3+
export interface Authenticator {
4+
isAuthProvider(user: User): boolean;
5+
getToken(user: User): string | null;
6+
}

src/cloud_auth.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// workaround for issue https://github.com/dchester/jsonpath/issues/96
2+
import jsonpath = require('jsonpath/jsonpath.min');
3+
import * as shelljs from 'shelljs';
4+
5+
import { Authenticator } from './auth';
6+
import { User } from './config_types';
7+
8+
export class CloudAuth implements Authenticator {
9+
public isAuthProvider(user: User): boolean {
10+
return user.authProvider.name === 'azure' ||
11+
user.authProvider.name === 'gcp';
12+
}
13+
14+
public getToken(user: User): string | null {
15+
const config = user.authProvider.config;
16+
// This should probably be extracted as auth-provider specific plugins...
17+
let token: string = 'Bearer ' + config['access-token'];
18+
const expiry = config.expiry;
19+
20+
if (expiry) {
21+
const expiration = Date.parse(expiry);
22+
if (expiration < Date.now()) {
23+
if (config['cmd-path']) {
24+
const args = config['cmd-args'];
25+
// TODO: Cache to file?
26+
// TODO: do this asynchronously
27+
let result: any;
28+
try {
29+
let cmd = config['cmd-path'];
30+
if (args) {
31+
cmd = `${cmd} ${args}`;
32+
}
33+
result = shelljs.exec(cmd);
34+
if (result.code !== 0) {
35+
throw new Error(`Failed to refresh token: ${result.stderr}`);
36+
}
37+
} catch (err) {
38+
throw new Error('Failed to refresh token: ' + err.message);
39+
}
40+
41+
const output = result.stdout.toString();
42+
const resultObj = JSON.parse(output);
43+
44+
let pathKey = config['token-key'];
45+
// Format in file is {<query>}, so slice it out and add '$'
46+
pathKey = '$' + pathKey.slice(1, -1);
47+
48+
config['access-token'] = jsonpath.query(resultObj, pathKey);
49+
token = 'Bearer ' + config['access-token'];
50+
} else {
51+
throw new Error('Token is expired!');
52+
}
53+
}
54+
}
55+
return token;
56+
}
57+
}

src/config.ts

Lines changed: 14 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ import https = require('https');
33
import path = require('path');
44

55
import base64 = require('base-64');
6-
import execa = require('execa');
76
import yaml = require('js-yaml');
8-
// workaround for issue https://github.com/dchester/jsonpath/issues/96
9-
import jsonpath = require('jsonpath/jsonpath.min');
107
import request = require('request');
118
import shelljs = require('shelljs');
129

13-
import api = require('./api');
10+
import * as api from './api';
11+
import { Authenticator } from './auth';
12+
import { CloudAuth } from './cloud_auth';
1413
import { Cluster, Context, newClusters, newContexts, newUsers, User } from './config_types';
14+
import { ExecAuth } from './exec_auth';
1515

1616
export class KubeConfig {
1717
// Only public for testing.
@@ -49,6 +49,11 @@ export class KubeConfig {
4949
return null;
5050
}
5151

52+
private static authenticators: Authenticator[] = [
53+
new CloudAuth(),
54+
new ExecAuth(),
55+
];
56+
5257
/**
5358
* The list of all known clusters
5459
*/
@@ -301,41 +306,12 @@ export class KubeConfig {
301306
let token: string | null = null;
302307

303308
if (user.authProvider && user.authProvider.config) {
304-
const config = user.authProvider.config;
305-
// This should probably be extracted as auth-provider specific plugins...
306-
token = 'Bearer ' + config['access-token'];
307-
const expiry = config.expiry;
308-
309-
if (expiry) {
310-
const expiration = Date.parse(expiry);
311-
if (expiration < Date.now()) {
312-
if (config['cmd-path']) {
313-
const args = config['cmd-args'] ? [config['cmd-args']] : [];
314-
// TODO: Cache to file?
315-
// TODO: do this asynchronously
316-
let result: execa.ExecaReturns;
317-
318-
try {
319-
result = execa.sync(config['cmd-path'], args);
320-
} catch (err) {
321-
throw new Error('Failed to refresh token: ' + err.message);
322-
}
323-
324-
const output = result.stdout.toString();
325-
const resultObj = JSON.parse(output);
326-
327-
let pathKey = config['token-key'];
328-
// Format in file is {<query>}, so slice it out and add '$'
329-
pathKey = '$' + pathKey.slice(1, -1);
330-
331-
config['access-token'] = jsonpath.query(resultObj, pathKey);
332-
token = 'Bearer ' + config['access-token'];
333-
} else {
334-
throw new Error('Token is expired!');
309+
KubeConfig.authenticators.forEach(
310+
(authenticator: Authenticator) => {
311+
if (authenticator.isAuthProvider(user)) {
312+
token = authenticator.getToken(user);
335313
}
336-
}
337-
}
338-
314+
});
339315
}
340316

341317
if (user.token) {

src/config_test.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,7 @@ describe('KubeConfig', () => {
523523
{ skipTLSVerify: false } as Cluster,
524524
{
525525
authProvider: {
526+
name: 'azure',
526527
config: {
527528
'access-token': token,
528529
'expiry': 'Fri Aug 24 07:32:05 PDT 3018',
@@ -548,6 +549,7 @@ describe('KubeConfig', () => {
548549
{ skipTLSVerify: false } as Cluster,
549550
{
550551
authProvider: {
552+
name: 'azure',
551553
config: {
552554
'access-token': token,
553555
},
@@ -568,6 +570,7 @@ describe('KubeConfig', () => {
568570
{ skipTLSVerify: false } as Cluster,
569571
{
570572
authProvider: {
573+
name: 'azure',
571574
config: {
572575
expiry: 'Aug 24 07:32:05 PDT 2017',
573576
},
@@ -584,6 +587,7 @@ describe('KubeConfig', () => {
584587
{ skipTLSVerify: false } as Cluster,
585588
{
586589
authProvider: {
590+
name: 'azure',
587591
config: {
588592
'expiry': 'Aug 24 07:32:05 PDT 2017',
589593
'cmd-path': 'non-existent-command',
@@ -592,7 +596,7 @@ describe('KubeConfig', () => {
592596
} as User);
593597
const opts = {} as requestlib.Options;
594598
expect(() => config.applyToRequest(opts)).to.throw(
595-
'Failed to refresh token: spawnSync non-existent-command ENOENT');
599+
'Failed to refresh token: /bin/sh: 1: non-existent-command: not found');
596600
});
597601

598602
it('should exec with expired token', () => {
@@ -603,10 +607,11 @@ describe('KubeConfig', () => {
603607
{ skipTLSVerify: false } as Cluster,
604608
{
605609
authProvider: {
610+
name: 'azure',
606611
config: {
607612
'expiry': 'Aug 24 07:32:05 PDT 2017',
608613
'cmd-path': 'echo',
609-
'cmd-args': `${responseStr}`,
614+
'cmd-args': `'${responseStr}'`,
610615
'token-key': '{.token.accessToken}',
611616
},
612617
},
@@ -618,6 +623,38 @@ describe('KubeConfig', () => {
618623
expect(opts.headers.Authorization).to.equal(`Bearer ${token}`);
619624
}
620625
});
626+
627+
it('should exec with exec auth', () => {
628+
const config = new KubeConfig();
629+
const token = 'token';
630+
const responseStr = `'{ "token": "${token}" }'`;
631+
config.loadFromClusterAndUser(
632+
{ skipTLSVerify: false } as Cluster,
633+
{
634+
authProvider: {
635+
name: 'exec',
636+
config: {
637+
exec: {
638+
command: 'echo',
639+
args: [`${responseStr}`],
640+
env: [
641+
{
642+
name: 'foo',
643+
value: 'bar',
644+
},
645+
],
646+
},
647+
},
648+
},
649+
} as User);
650+
// TODO: inject the exec command here?
651+
const opts = {} as requestlib.Options;
652+
config.applyToRequest(opts);
653+
expect(opts.headers).to.not.be.undefined;
654+
if (opts.headers) {
655+
expect(opts.headers.Authorization).to.equal(`Bearer ${token}`);
656+
}
657+
});
621658
});
622659

623660
describe('loadFromDefault', () => {

src/exec_auth.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as shell from 'shelljs';
2+
3+
import { Authenticator } from './auth';
4+
import { User } from './config_types';
5+
6+
export class ExecAuth implements Authenticator {
7+
public isAuthProvider(user: User) {
8+
return user.authProvider.name === 'exec' ||
9+
(user.authProvider.config && user.authProvider.config.exec);
10+
}
11+
12+
public getToken(user: User): string | null {
13+
const config = user.authProvider.config;
14+
if (!config.exec.command) {
15+
throw new Error('No command was specified for exec authProvider!');
16+
}
17+
let cmd = config.exec.command;
18+
if (config.exec.args) {
19+
cmd = `${cmd} ${config.exec.args.join(' ')}`;
20+
}
21+
let opts: shell.ExecOpts;
22+
if (config.exec.env) {
23+
const env = {};
24+
if (config.exec.env) {
25+
config.exec.env.forEach((elt) => env[elt.name] = elt.value);
26+
}
27+
opts = { env };
28+
}
29+
const result = shell.exec(cmd, opts);
30+
if (result.code === 0) {
31+
const obj = JSON.parse(result.stdout);
32+
return `Bearer ${obj.token}`;
33+
}
34+
throw new Error(result.stderr);
35+
}
36+
}

0 commit comments

Comments
 (0)