From 5c339083d64a37d3d5805b8c3d2dccc0098f97b8 Mon Sep 17 00:00:00 2001 From: kahirokunn Date: Sun, 19 May 2024 22:23:51 +0900 Subject: [PATCH] format: prettier --- packages/kubekit-client/src/client/index.ts | 4 +- .../kubekit-client/src/lib/config/auth.ts | 10 +- .../src/lib/config/azure_auth.ts | 137 ++- .../src/lib/config/config_types.ts | 336 +++--- .../src/lib/config/exec_auth.ts | 177 ++- .../src/lib/config/file_auth.ts | 65 +- .../kubekit-client/src/lib/config/gcp_auth.ts | 131 ++- .../kubekit-client/src/lib/config/index.ts | 1007 ++++++++--------- .../src/lib/config/json_path.ts | 8 +- .../src/lib/config/oidc_auth.ts | 174 ++- .../src/lib/config/oidc_auth_delayed.ts | 38 +- .../kubekit-client/src/lib/task_manager.ts | 7 +- .../kubekit-client/test/task_manager.test.ts | 4 +- 13 files changed, 1031 insertions(+), 1067 deletions(-) diff --git a/packages/kubekit-client/src/client/index.ts b/packages/kubekit-client/src/client/index.ts index 9119b299..153ccf69 100644 --- a/packages/kubekit-client/src/client/index.ts +++ b/packages/kubekit-client/src/client/index.ts @@ -381,14 +381,14 @@ export async function apiClient( // helpful message for debugging console.info(`Did you forget to install your Custom Resources Definitions? path: ${httpsOptions.path}`); } - if (response.headers.get('content-type') === "application/json") { + if (response.headers.get('content-type') === 'application/json') { throw JSON.parse(text); } throw new Error(text); } catch (error: any) { retry++; - await options.onError(error) + await options.onError(error); if ( !(await options.retryCondition({ diff --git a/packages/kubekit-client/src/lib/config/auth.ts b/packages/kubekit-client/src/lib/config/auth.ts index 10828510..9bc8037c 100644 --- a/packages/kubekit-client/src/lib/config/auth.ts +++ b/packages/kubekit-client/src/lib/config/auth.ts @@ -5,9 +5,9 @@ import WebSocket = require('ws'); import { type User } from './config_types'; export interface Authenticator { - isAuthProvider(user: User): boolean; - applyAuthentication( - user: User, - opts: request.Options | https.RequestOptions | WebSocket.ClientOptions, - ): Promise; + isAuthProvider(user: User): boolean; + applyAuthentication( + user: User, + opts: request.Options | https.RequestOptions | WebSocket.ClientOptions + ): Promise; } diff --git a/packages/kubekit-client/src/lib/config/azure_auth.ts b/packages/kubekit-client/src/lib/config/azure_auth.ts index dae60035..4ca41563 100644 --- a/packages/kubekit-client/src/lib/config/azure_auth.ts +++ b/packages/kubekit-client/src/lib/config/azure_auth.ts @@ -11,90 +11,87 @@ Currently user.authProvider has `any` type and so we don't have a type for user. We therefore define its type here */ interface Config { - expiry?: string; - ['cmd-args']?: string; - ['cmd-path']?: string; - ['token-key']: string; - ['expiry-key']: string; - ['access-token']?: string; - ['expires-on']?: string; + expiry?: string; + ['cmd-args']?: string; + ['cmd-path']?: string; + ['token-key']: string; + ['expiry-key']: string; + ['access-token']?: string; + ['expires-on']?: string; } export class AzureAuth implements Authenticator { - public isAuthProvider(user: User): boolean { - if (!user || !user.authProvider) { - return false; - } - return user.authProvider.name === 'azure'; + public isAuthProvider(user: User): boolean { + if (!user || !user.authProvider) { + return false; } + return user.authProvider.name === 'azure'; + } - public async applyAuthentication( - user: User, - opts: request.Options | https.RequestOptions, - ): Promise { - const token = this.getToken(user); - if (token) { - opts.headers!.Authorization = `Bearer ${token}`; - } + public async applyAuthentication(user: User, opts: request.Options | https.RequestOptions): Promise { + const token = this.getToken(user); + if (token) { + opts.headers!.Authorization = `Bearer ${token}`; } + } - private getToken(user: User): string | null { - const config = user.authProvider.config; - if (this.isExpired(config)) { - this.updateAccessToken(config); - } - return config['access-token']; + private getToken(user: User): string | null { + const config = user.authProvider.config; + if (this.isExpired(config)) { + this.updateAccessToken(config); } + return config['access-token']; + } - private isExpired(config: Config): boolean { - const token = config['access-token']; - const expiry = config.expiry; - const expiresOn = config['expires-on']; + private isExpired(config: Config): boolean { + const token = config['access-token']; + const expiry = config.expiry; + const expiresOn = config['expires-on']; - if (!token) { - return true; - } - if (!expiry && !expiresOn) { - return false; - } + if (!token) { + return true; + } + if (!expiry && !expiresOn) { + return false; + } - const expiresOnDate = expiresOn ? new Date(parseInt(expiresOn, 10) * 1000).getTime() : undefined; - const expiration = expiry ? Date.parse(expiry) : expiresOnDate!; - if (expiration < Date.now()) { - return true; - } - return false; + const expiresOnDate = expiresOn ? new Date(parseInt(expiresOn, 10) * 1000).getTime() : undefined; + const expiration = expiry ? Date.parse(expiry) : expiresOnDate!; + if (expiration < Date.now()) { + return true; } + return false; + } - private updateAccessToken(config: Config): void { - let cmd = config['cmd-path']; - if (!cmd) { - throw new Error('Token is expired!'); - } - // Wrap cmd in quotes to make it cope with spaces in path - cmd = `"${cmd}"`; - const args = config['cmd-args']; - if (args) { - cmd = cmd + ' ' + args; - } - // TODO: Cache to file? - // TODO: do this asynchronously - let output: any; - try { - output = proc.execSync(cmd); - } catch (err) { - throw new Error('Failed to refresh token: ' + (err as Error).message); - } + private updateAccessToken(config: Config): void { + let cmd = config['cmd-path']; + if (!cmd) { + throw new Error('Token is expired!'); + } + // Wrap cmd in quotes to make it cope with spaces in path + cmd = `"${cmd}"`; + const args = config['cmd-args']; + if (args) { + cmd = cmd + ' ' + args; + } + // TODO: Cache to file? + // TODO: do this asynchronously + let output: any; + try { + output = proc.execSync(cmd); + } catch (err) { + throw new Error('Failed to refresh token: ' + (err as Error).message); + } - const resultObj = JSON.parse(output); + const resultObj = JSON.parse(output); - const tokenPathKeyInConfig = config['token-key']; - const expiryPathKeyInConfig = config['expiry-key']; + const tokenPathKeyInConfig = config['token-key']; + const expiryPathKeyInConfig = config['expiry-key']; - // Format in file is {}, so slice it out and add '$' - const tokenPathKey = '$' + tokenPathKeyInConfig.slice(1, -1); - const expiryPathKey = '$' + expiryPathKeyInConfig.slice(1, -1); + // Format in file is {}, so slice it out and add '$' + const tokenPathKey = '$' + tokenPathKeyInConfig.slice(1, -1); + const expiryPathKey = '$' + expiryPathKeyInConfig.slice(1, -1); - config['access-token'] = jsonpath(tokenPathKey, resultObj); - config.expiry = jsonpath(expiryPathKey, resultObj); - } + config['access-token'] = jsonpath(tokenPathKey, resultObj); + config.expiry = jsonpath(expiryPathKey, resultObj); + } } diff --git a/packages/kubekit-client/src/lib/config/config_types.ts b/packages/kubekit-client/src/lib/config/config_types.ts index b77f083e..fe5e39a2 100644 --- a/packages/kubekit-client/src/lib/config/config_types.ts +++ b/packages/kubekit-client/src/lib/config/config_types.ts @@ -1,220 +1,216 @@ import * as fs from 'fs'; export enum ActionOnInvalid { - THROW = 'throw', - FILTER = 'filter', + THROW = 'throw', + FILTER = 'filter', } export interface ConfigOptions { - onInvalidEntry: ActionOnInvalid; + onInvalidEntry: ActionOnInvalid; } function defaultNewConfigOptions(): ConfigOptions { - return { - onInvalidEntry: ActionOnInvalid.THROW, - }; + return { + onInvalidEntry: ActionOnInvalid.THROW, + }; } export interface Cluster { - readonly name: string; - readonly caData?: string; - caFile?: string; - readonly server: string; - readonly skipTLSVerify?: boolean; - readonly tlsServerName?: string; + readonly name: string; + readonly caData?: string; + caFile?: string; + readonly server: string; + readonly skipTLSVerify?: boolean; + readonly tlsServerName?: string; } export function newClusters(a: any, opts?: Partial): Cluster[] { - if (!Array.isArray(a)) { - return []; - } + if (!Array.isArray(a)) { + return []; + } - const options = Object.assign(defaultNewConfigOptions(), opts || {}); + const options = Object.assign(defaultNewConfigOptions(), opts || {}); - return a.map(clusterIterator(options.onInvalidEntry)).filter(Boolean) as Cluster[]; + return a.map(clusterIterator(options.onInvalidEntry)).filter(Boolean) as Cluster[]; } export function exportCluster(cluster: Cluster): any { - return { - name: cluster.name, - cluster: { - server: cluster.server, - 'certificate-authority-data': cluster.caData, - 'certificate-authority': cluster.caFile, - 'insecure-skip-tls-verify': cluster.skipTLSVerify, - 'tls-server-name': cluster.tlsServerName, - }, - }; -} - -function clusterIterator( - onInvalidEntry: ActionOnInvalid, -): (elt: any, i: number, list: any[]) => Cluster | null { - return (elt: any, i: number, 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.replace(/\/$/, ''), - skipTLSVerify: elt.cluster['insecure-skip-tls-verify'] === true, - tlsServerName: elt.cluster['tls-server-name'], - }; - } catch (err) { - switch (onInvalidEntry) { - case ActionOnInvalid.FILTER: - return null; - default: - case ActionOnInvalid.THROW: - throw err; - } - } - }; + return { + name: cluster.name, + cluster: { + server: cluster.server, + 'certificate-authority-data': cluster.caData, + 'certificate-authority': cluster.caFile, + 'insecure-skip-tls-verify': cluster.skipTLSVerify, + 'tls-server-name': cluster.tlsServerName, + }, + }; +} + +function clusterIterator(onInvalidEntry: ActionOnInvalid): (elt: any, i: number, list: any[]) => Cluster | null { + return (elt: any, i: number, 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.replace(/\/$/, ''), + skipTLSVerify: elt.cluster['insecure-skip-tls-verify'] === true, + tlsServerName: elt.cluster['tls-server-name'], + }; + } catch (err) { + switch (onInvalidEntry) { + case ActionOnInvalid.FILTER: + return null; + default: + case ActionOnInvalid.THROW: + throw err; + } + } + }; } export interface User { - readonly name: string; - readonly certData?: string; - certFile?: string; - readonly exec?: any; - readonly keyData?: string; - keyFile?: string; - readonly authProvider?: any; - readonly token?: string; - readonly username?: string; - readonly password?: string; - readonly "token-file"?: string; + readonly name: string; + readonly certData?: string; + certFile?: string; + readonly exec?: any; + readonly keyData?: string; + keyFile?: string; + readonly authProvider?: any; + readonly token?: string; + readonly username?: string; + readonly password?: string; + readonly 'token-file'?: string; } export function newUsers(a: any, opts?: Partial): User[] { - if (!Array.isArray(a)) { - return []; - } + if (!Array.isArray(a)) { + return []; + } - const options = Object.assign(defaultNewConfigOptions(), opts || {}); + const options = Object.assign(defaultNewConfigOptions(), opts || {}); - return a.map(userIterator(options.onInvalidEntry)).filter(Boolean) as User[]; + return a.map(userIterator(options.onInvalidEntry)).filter(Boolean) as User[]; } export function exportUser(user: User): any { - return { - name: user.name, - user: { - 'auth-provider': user.authProvider, - 'client-certificate-data': user.certData, - 'client-certificate': user.certFile, - exec: user.exec, - 'client-key-data': user.keyData, - 'client-key': user.keyFile, - token: user.token, - password: user.password, - username: user.username, - }, - }; + return { + name: user.name, + user: { + 'auth-provider': user.authProvider, + 'client-certificate-data': user.certData, + 'client-certificate': user.certFile, + exec: user.exec, + 'client-key-data': user.keyData, + 'client-key': user.keyFile, + token: user.token, + password: user.password, + username: user.username, + }, + }; } function userIterator(onInvalidEntry: ActionOnInvalid): (elt: any, i: number, list: any[]) => User | null { - return (elt: any, i: number): 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 (elt: any, i: number): 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; + } + } + }; } function findToken(user: User | undefined): string | undefined { - if (user) { - if (user.token) { - return user.token; - } - if (user['token-file']) { - return fs.readFileSync(user['token-file']).toString(); - } + if (user) { + if (user.token) { + return user.token; } + if (user['token-file']) { + return fs.readFileSync(user['token-file']).toString(); + } + } } export interface Context { - readonly cluster: string; - readonly user: string; - readonly name: string; - readonly namespace?: string; + readonly cluster: string; + readonly user: string; + readonly name: string; + readonly namespace?: string; } export function newContexts(a: any, opts?: Partial): Context[] { - if (!Array.isArray(a)) { - return []; - } + if (!Array.isArray(a)) { + return []; + } - const options = Object.assign(defaultNewConfigOptions(), opts || {}); + const options = Object.assign(defaultNewConfigOptions(), opts || {}); - return a.map(contextIterator(options.onInvalidEntry)).filter(Boolean) as Context[]; + return a.map(contextIterator(options.onInvalidEntry)).filter(Boolean) as Context[]; } export function exportContext(ctx: Context): any { - return { - name: ctx.name, - context: ctx, - }; -} - -function contextIterator( - onInvalidEntry: ActionOnInvalid, -): (elt: any, i: number, list: any[]) => Context | null { - return (elt: any, i: number, 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 { + name: ctx.name, + context: ctx, + }; +} + +function contextIterator(onInvalidEntry: ActionOnInvalid): (elt: any, i: number, list: any[]) => Context | null { + return (elt: any, i: number, 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; + } + } + }; } diff --git a/packages/kubekit-client/src/lib/config/exec_auth.ts b/packages/kubekit-client/src/lib/config/exec_auth.ts index b8b5ee4e..ba31a074 100644 --- a/packages/kubekit-client/src/lib/config/exec_auth.ts +++ b/packages/kubekit-client/src/lib/config/exec_auth.ts @@ -6,109 +6,104 @@ import { Authenticator } from './auth'; import { User } from './config_types'; export interface CredentialStatus { - readonly token: string; - readonly clientCertificateData: string; - readonly clientKeyData: string; - readonly expirationTimestamp: string; + readonly token: string; + readonly clientCertificateData: string; + readonly clientKeyData: string; + readonly expirationTimestamp: string; } export interface Credential { - readonly status: CredentialStatus; + readonly status: CredentialStatus; } export class ExecAuth implements Authenticator { - private readonly tokenCache: { [key: string]: Credential | null } = {}; - private execFn: ( - cmd: string, - args: string[], - opts: child_process.SpawnOptions, - ) => child_process.SpawnSyncReturns = child_process.spawnSync; + private readonly tokenCache: { [key: string]: Credential | null } = {}; + private execFn: ( + cmd: string, + args: string[], + opts: child_process.SpawnOptions + ) => child_process.SpawnSyncReturns = child_process.spawnSync; - public isAuthProvider(user: User): boolean { - if (!user) { - return false; - } - if (user.exec) { - return true; - } - if (!user.authProvider) { - return false; - } - return ( - user.authProvider.name === 'exec' || !!(user.authProvider.config && user.authProvider.config.exec) - ); + public isAuthProvider(user: User): boolean { + if (!user) { + return false; } + if (user.exec) { + return true; + } + if (!user.authProvider) { + return false; + } + return user.authProvider.name === 'exec' || !!(user.authProvider.config && user.authProvider.config.exec); + } - public async applyAuthentication( - user: User, - opts: request.Options | https.RequestOptions, - ): Promise { - const credential = this.getCredential(user); - if (!credential) { - return; - } - if (credential.status.clientCertificateData) { - opts.cert = credential.status.clientCertificateData; - } - if (credential.status.clientKeyData) { - opts.key = credential.status.clientKeyData; - } - const token = this.getToken(credential); - if (token) { - if (!opts.headers) { - opts.headers = []; - } - opts.headers!.Authorization = `Bearer ${token}`; - } + public async applyAuthentication(user: User, opts: request.Options | https.RequestOptions): Promise { + const credential = this.getCredential(user); + if (!credential) { + return; + } + if (credential.status.clientCertificateData) { + opts.cert = credential.status.clientCertificateData; } + if (credential.status.clientKeyData) { + opts.key = credential.status.clientKeyData; + } + const token = this.getToken(credential); + if (token) { + if (!opts.headers) { + opts.headers = []; + } + opts.headers!.Authorization = `Bearer ${token}`; + } + } - private getToken(credential: Credential): string | null { - if (!credential) { - return null; - } - if (credential.status.token) { - return credential.status.token; - } - return null; + private getToken(credential: Credential): string | null { + if (!credential) { + return null; + } + if (credential.status.token) { + return credential.status.token; } + return null; + } - private getCredential(user: User): Credential | null { - // TODO: Add a unit test for token caching. - const cachedToken = this.tokenCache[user.name]; - if (cachedToken) { - const date = Date.parse(cachedToken.status.expirationTimestamp); - if (date > Date.now()) { - return cachedToken; - } - this.tokenCache[user.name] = null; - } - let exec: any = null; - if (user.authProvider && user.authProvider.config) { - exec = user.authProvider.config.exec; - } - if (user.exec) { - exec = user.exec; - } - if (!exec) { - return null; - } - if (!exec.command) { - throw new Error('No command was specified for exec authProvider!'); - } - let opts = {}; - if (exec.env) { - const env = { - ...process.env, - }; - exec.env.forEach((elt: { name: string | number; value?: string }) => (env[elt.name] = elt.value)); - opts = { ...opts, env }; - } - const result = this.execFn(exec.command, exec.args, opts); - if (result.status === 0) { - const obj = JSON.parse(result.stdout.toString('utf8')) as Credential; - this.tokenCache[user.name] = obj; - return obj; - } - throw new Error(result.stderr.toString('utf8')); + private getCredential(user: User): Credential | null { + // TODO: Add a unit test for token caching. + const cachedToken = this.tokenCache[user.name]; + if (cachedToken) { + const date = Date.parse(cachedToken.status.expirationTimestamp); + if (date > Date.now()) { + return cachedToken; + } + this.tokenCache[user.name] = null; + } + let exec: any = null; + if (user.authProvider && user.authProvider.config) { + exec = user.authProvider.config.exec; + } + if (user.exec) { + exec = user.exec; + } + if (!exec) { + return null; + } + if (!exec.command) { + throw new Error('No command was specified for exec authProvider!'); + } + let opts = {}; + if (exec.env) { + const env = { + ...process.env, + }; + exec.env.forEach((elt: { name: string | number; value?: string }) => (env[elt.name] = elt.value)); + opts = { ...opts, env }; + } + const result = this.execFn(exec.command, exec.args, opts); + if (result.status === 0) { + const obj = JSON.parse(result.stdout.toString('utf8')) as Credential; + this.tokenCache[user.name] = obj; + return obj; } + throw new Error(result.stderr.toString('utf8')); + } } diff --git a/packages/kubekit-client/src/lib/config/file_auth.ts b/packages/kubekit-client/src/lib/config/file_auth.ts index bd3ab9e0..33221f8f 100644 --- a/packages/kubekit-client/src/lib/config/file_auth.ts +++ b/packages/kubekit-client/src/lib/config/file_auth.ts @@ -6,44 +6,41 @@ import { Authenticator } from './auth'; import { User } from './config_types'; export class FileAuth implements Authenticator { - private token: string | null = null; - private lastRead: Date | null = null; + private token: string | null = null; + private lastRead: Date | null = null; - public isAuthProvider(user: User): boolean { - return user.authProvider && user.authProvider.config && user.authProvider.config.tokenFile; - } + public isAuthProvider(user: User): boolean { + return user.authProvider && user.authProvider.config && user.authProvider.config.tokenFile; + } - public async applyAuthentication( - user: User, - opts: request.Options | https.RequestOptions, - ): Promise { - if (this.token == null) { - this.refreshToken(user.authProvider.config.tokenFile); - } - if (this.isTokenExpired()) { - this.refreshToken(user.authProvider.config.tokenFile); - } - if (this.token) { - opts.headers!.Authorization = `Bearer ${this.token}`; - } + public async applyAuthentication(user: User, opts: request.Options | https.RequestOptions): Promise { + if (this.token == null) { + this.refreshToken(user.authProvider.config.tokenFile); } - - private refreshToken(filePath: string): void { - // TODO make this async? - this.token = fs.readFileSync(filePath).toString('utf8'); - this.lastRead = new Date(); + if (this.isTokenExpired()) { + this.refreshToken(user.authProvider.config.tokenFile); } + if (this.token) { + opts.headers!.Authorization = `Bearer ${this.token}`; + } + } + + private refreshToken(filePath: string): void { + // TODO make this async? + this.token = fs.readFileSync(filePath).toString('utf8'); + this.lastRead = new Date(); + } - private isTokenExpired(): boolean { - if (this.lastRead === null) { - return true; - } - const now = new Date(); - const delta = (now.getTime() - this.lastRead.getTime()) / 1000; - // For now just refresh every 60 seconds. This is imperfect since the token - // could be out of date for this time, but it is unlikely and it's also what - // the client-go library does. - // TODO: Use file notifications instead? - return delta > 60; + private isTokenExpired(): boolean { + if (this.lastRead === null) { + return true; } + const now = new Date(); + const delta = (now.getTime() - this.lastRead.getTime()) / 1000; + // For now just refresh every 60 seconds. This is imperfect since the token + // could be out of date for this time, but it is unlikely and it's also what + // the client-go library does. + // TODO: Use file notifications instead? + return delta > 60; + } } diff --git a/packages/kubekit-client/src/lib/config/gcp_auth.ts b/packages/kubekit-client/src/lib/config/gcp_auth.ts index a8277d84..3bb45ab6 100644 --- a/packages/kubekit-client/src/lib/config/gcp_auth.ts +++ b/packages/kubekit-client/src/lib/config/gcp_auth.ts @@ -11,86 +11,83 @@ Currently user.authProvider has `any` type and so we don't have a type for user. We therefore define its type here */ interface Config { - expiry: string; - ['cmd-args']?: string; - ['cmd-path']?: string; - ['token-key']: string; - ['expiry-key']: string; - ['access-token']?: string; + expiry: string; + ['cmd-args']?: string; + ['cmd-path']?: string; + ['token-key']: string; + ['expiry-key']: string; + ['access-token']?: string; } export class GoogleCloudPlatformAuth implements Authenticator { - public isAuthProvider(user: User): boolean { - if (!user || !user.authProvider) { - return false; - } - return user.authProvider.name === 'gcp'; + public isAuthProvider(user: User): boolean { + if (!user || !user.authProvider) { + return false; } + return user.authProvider.name === 'gcp'; + } - public async applyAuthentication( - user: User, - opts: request.Options | https.RequestOptions, - ): Promise { - const token = this.getToken(user); - if (token) { - opts.headers!.Authorization = `Bearer ${token}`; - } + public async applyAuthentication(user: User, opts: request.Options | https.RequestOptions): Promise { + const token = this.getToken(user); + if (token) { + opts.headers!.Authorization = `Bearer ${token}`; } + } - private getToken(user: User): string | null { - const config = user.authProvider.config; - if (this.isExpired(config)) { - this.updateAccessToken(config); - } - return config['access-token']; + private getToken(user: User): string | null { + const config = user.authProvider.config; + if (this.isExpired(config)) { + this.updateAccessToken(config); } + return config['access-token']; + } - private isExpired(config: Config): boolean { - const token = config['access-token']; - const expiry = config.expiry; - if (!token) { - return true; - } - if (!expiry) { - return false; - } + private isExpired(config: Config): boolean { + const token = config['access-token']; + const expiry = config.expiry; + if (!token) { + return true; + } + if (!expiry) { + return false; + } - const expiration = Date.parse(expiry); - if (expiration < Date.now()) { - return true; - } - return false; + const expiration = Date.parse(expiry); + if (expiration < Date.now()) { + return true; } + return false; + } - private updateAccessToken(config: Config): void { - let cmd = config['cmd-path']; - if (!cmd) { - throw new Error('Token is expired!'); - } - // Wrap cmd in quotes to make it cope with spaces in path - cmd = `"${cmd}"`; - const args = config['cmd-args']; - if (args) { - cmd = cmd + ' ' + args; - } - // TODO: Cache to file? - // TODO: do this asynchronously - let output: any; - try { - output = proc.execSync(cmd); - } catch (err) { - throw new Error('Failed to refresh token: ' + (err as Error).message); - } + private updateAccessToken(config: Config): void { + let cmd = config['cmd-path']; + if (!cmd) { + throw new Error('Token is expired!'); + } + // Wrap cmd in quotes to make it cope with spaces in path + cmd = `"${cmd}"`; + const args = config['cmd-args']; + if (args) { + cmd = cmd + ' ' + args; + } + // TODO: Cache to file? + // TODO: do this asynchronously + let output: any; + try { + output = proc.execSync(cmd); + } catch (err) { + throw new Error('Failed to refresh token: ' + (err as Error).message); + } - const resultObj = JSON.parse(output); + const resultObj = JSON.parse(output); - const tokenPathKeyInConfig = config['token-key']; - const expiryPathKeyInConfig = config['expiry-key']; + const tokenPathKeyInConfig = config['token-key']; + const expiryPathKeyInConfig = config['expiry-key']; - // Format in file is {}, so slice it out and add '$' - const tokenPathKey = '$' + tokenPathKeyInConfig.slice(1, -1); - const expiryPathKey = '$' + expiryPathKeyInConfig.slice(1, -1); + // Format in file is {}, so slice it out and add '$' + const tokenPathKey = '$' + tokenPathKeyInConfig.slice(1, -1); + const expiryPathKey = '$' + expiryPathKeyInConfig.slice(1, -1); - config['access-token'] = jsonpath(tokenPathKey, resultObj); - config.expiry = jsonpath(expiryPathKey, resultObj); - } + config['access-token'] = jsonpath(tokenPathKey, resultObj); + config.expiry = jsonpath(expiryPathKey, resultObj); + } } diff --git a/packages/kubekit-client/src/lib/config/index.ts b/packages/kubekit-client/src/lib/config/index.ts index 8d1cb40c..471bb972 100644 --- a/packages/kubekit-client/src/lib/config/index.ts +++ b/packages/kubekit-client/src/lib/config/index.ts @@ -11,16 +11,16 @@ import WebSocket from 'ws'; import { Authenticator } from './auth'; import { AzureAuth } from './azure_auth'; import { - Cluster, - ConfigOptions, - Context, - exportCluster, - exportContext, - exportUser, - newClusters, - newContexts, - newUsers, - User, + Cluster, + ConfigOptions, + Context, + exportCluster, + exportContext, + exportUser, + newClusters, + newContexts, + newUsers, + User, } from './config_types'; import { ExecAuth } from './exec_auth'; import { FileAuth } from './file_auth'; @@ -29,554 +29,543 @@ import { DelayedOpenIDConnectAuth } from './oidc_auth_delayed'; // fs.existsSync was removed in node 10 function fileExists(filepath: string): boolean { - try { - fs.accessSync(filepath); - return true; - } catch (ignore) { - return false; - } + try { + fs.accessSync(filepath); + return true; + } catch (ignore) { + return false; + } } export class KubeConfig { - private authenticators: Authenticator[] = [ - new AzureAuth(), - new GoogleCloudPlatformAuth(), - new ExecAuth(), - new FileAuth(), - new DelayedOpenIDConnectAuth(), - ]; - - /** - * The list of all known clusters - */ - public 'clusters': Cluster[]; - - /** - * The list of all known users - */ - public 'users': User[]; - - /** - * The list of all known contexts - */ - public 'contexts': Context[]; - - /** - * The name of the current context - */ - public 'currentContext': string; - - constructor() { - this.contexts = []; - this.clusters = []; - this.users = []; - } - - public getContexts(): Context[] { - return this.contexts; - } - - public getClusters(): Cluster[] { - return this.clusters; - } - - public getUsers(): User[] { - return this.users; - } - - public getCurrentContext(): string { - return this.currentContext; - } - - public setCurrentContext(context: string): void { - this.currentContext = context; - } - - public getContextObject(name: string): Context | null { - if (!this.contexts) { - return null; - } - return findObject(this.contexts, name, 'context'); - } - - public getCurrentCluster(): Cluster | null { - const context = this.getCurrentContextObject(); - if (!context) { - return null; - } - return this.getCluster(context.cluster); - } + private authenticators: Authenticator[] = [ + new AzureAuth(), + new GoogleCloudPlatformAuth(), + new ExecAuth(), + new FileAuth(), + new DelayedOpenIDConnectAuth(), + ]; + + /** + * The list of all known clusters + */ + public 'clusters': Cluster[]; + + /** + * The list of all known users + */ + public 'users': User[]; + + /** + * The list of all known contexts + */ + public 'contexts': Context[]; + + /** + * The name of the current context + */ + public 'currentContext': string; + + constructor() { + this.contexts = []; + this.clusters = []; + this.users = []; + } - public getCluster(name: string): Cluster | null { - return findObject(this.clusters, name, 'cluster'); - } + public getContexts(): Context[] { + return this.contexts; + } + + public getClusters(): Cluster[] { + return this.clusters; + } + + public getUsers(): User[] { + return this.users; + } + + public getCurrentContext(): string { + return this.currentContext; + } + + public setCurrentContext(context: string): void { + this.currentContext = context; + } + + public getContextObject(name: string): Context | null { + if (!this.contexts) { + return null; + } + return findObject(this.contexts, name, 'context'); + } + + public getCurrentCluster(): Cluster | null { + const context = this.getCurrentContextObject(); + if (!context) { + return null; + } + return this.getCluster(context.cluster); + } + + public getCluster(name: string): Cluster | null { + return findObject(this.clusters, name, 'cluster'); + } + + public getCurrentUser(): User | null { + const ctx = this.getCurrentContextObject(); + if (!ctx) { + return null; + } + return this.getUser(ctx.user); + } + + public getUser(name: string): User | null { + return findObject(this.users, name, 'user'); + } + + public loadFromFile(file: string, opts?: Partial): void { + const rootDirectory = path.dirname(file); + this.loadFromString(fs.readFileSync(file).toString('utf-8'), opts); + this.makePathsAbsolute(rootDirectory); + } + + public async applyToHTTPSOptions(opts: https.RequestOptions | WebSocket.ClientOptions): Promise { + await this.applyOptions(opts); + + const user = this.getCurrentUser(); + if (user && user.username) { + // The ws docs say that it accepts anything that https.RequestOptions accepts, + // but Typescript doesn't understand that idea (yet) probably could be fixed in + // the typings, but for now just cast to any + (opts as any).auth = `${user.username}:${user.password}`; + } + + const cluster = this.getCurrentCluster(); + if (cluster && cluster.tlsServerName) { + // The ws docs say that it accepts anything that https.RequestOptions accepts, + // but Typescript doesn't understand that idea (yet) probably could be fixed in + // the typings, but for now just cast to any + (opts as any).servername = cluster.tlsServerName; + } + } + + public async applyToRequest(opts: request.Options): Promise { + const cluster = this.getCurrentCluster(); + const user = this.getCurrentUser(); + + await this.applyOptions(opts); + + if (cluster && cluster.skipTLSVerify) { + opts.strictSSL = false; + } + + if (user && user.username) { + opts.auth = { + password: user.password, + username: user.username, + }; + } + + if (cluster && cluster.tlsServerName) { + opts.agentOptions = { servername: cluster.tlsServerName } as https.AgentOptions; + } + } + + public loadFromString(config: string, opts?: Partial): void { + const obj = yaml.load(config) as any; + this.clusters = newClusters(obj.clusters, opts); + this.contexts = newContexts(obj.contexts, opts); + this.users = newUsers(obj.users, opts); + this.currentContext = obj['current-context']; + } + + public loadFromOptions(options: { + clusters: Cluster[]; + contexts: Context[]; + currentContext: Context['name']; + users: User[]; + }): void { + this.clusters = options.clusters; + this.contexts = options.contexts; + this.users = options.users; + this.currentContext = options.currentContext; + } + + public loadFromClusterAndUser(cluster: Cluster, user: User): void { + this.clusters = [cluster]; + this.users = [user]; + this.currentContext = 'loaded-context'; + this.contexts = [ + { + cluster: cluster.name, + user: user.name, + name: this.currentContext, + } as Context, + ]; + } + + public loadFromCluster(pathPrefix: string = ''): void { + const host = process.env.KUBERNETES_SERVICE_HOST; + const port = process.env.KUBERNETES_SERVICE_PORT; + const clusterName = 'inCluster'; + const userName = 'inClusterUser'; + const contextName = 'inClusterContext'; + const tokenFile = process.env.TOKEN_FILE_PATH + ? process.env.TOKEN_FILE_PATH + : `${pathPrefix}${Config.SERVICEACCOUNT_TOKEN_PATH}`; + const caFile = process.env.KUBERNETES_CA_FILE_PATH + ? process.env.KUBERNETES_CA_FILE_PATH + : `${pathPrefix}${Config.SERVICEACCOUNT_CA_PATH}`; + + let scheme = 'https'; + if (port === '80' || port === '8080' || port === '8001') { + scheme = 'http'; + } + + // Wrap raw IPv6 addresses in brackets. + let serverHost = host; + if (host && net.isIPv6(host)) { + serverHost = `[${host}]`; + } + + this.clusters = [ + { + name: clusterName, + caFile, + server: `${scheme}://${serverHost}:${port}`, + skipTLSVerify: false, + }, + ]; + this.users = [ + { + name: userName, + authProvider: { + name: 'tokenFile', + config: { + tokenFile, + }, + }, + }, + ]; + const namespaceFile = `${pathPrefix}${Config.SERVICEACCOUNT_NAMESPACE_PATH}`; + let namespace: string | undefined; + if (fileExists(namespaceFile)) { + namespace = fs.readFileSync(namespaceFile).toString('utf-8'); + } + this.contexts = [ + { + cluster: clusterName, + name: contextName, + user: userName, + namespace, + }, + ]; + this.currentContext = contextName; + } - public getCurrentUser(): User | null { - const ctx = this.getCurrentContextObject(); - if (!ctx) { - return null; - } - return this.getUser(ctx.user); + public mergeConfig(config: KubeConfig, preserveContext: boolean = false): void { + if (!preserveContext && config.currentContext) { + this.currentContext = config.currentContext; } + config.clusters.forEach((cluster: Cluster) => { + this.addCluster(cluster); + }); + config.users.forEach((user: User) => { + this.addUser(user); + }); + config.contexts.forEach((ctx: Context) => { + this.addContext(ctx); + }); + } - public getUser(name: string): User | null { - return findObject(this.users, name, 'user'); + public addCluster(cluster: Cluster): void { + if (!this.clusters) { + this.clusters = []; } - public loadFromFile(file: string, opts?: Partial): void { - const rootDirectory = path.dirname(file); - this.loadFromString(fs.readFileSync(file).toString('utf-8'), opts); - this.makePathsAbsolute(rootDirectory); + if (this.clusters.some((c) => c.name === cluster.name)) { + throw new Error(`Duplicate cluster: ${cluster.name}`); } - public async applyToHTTPSOptions(opts: https.RequestOptions | WebSocket.ClientOptions): Promise { - await this.applyOptions(opts); - - const user = this.getCurrentUser(); - if (user && user.username) { - // The ws docs say that it accepts anything that https.RequestOptions accepts, - // but Typescript doesn't understand that idea (yet) probably could be fixed in - // the typings, but for now just cast to any - (opts as any).auth = `${user.username}:${user.password}`; - } + this.clusters.push(cluster); + } - const cluster = this.getCurrentCluster(); - if (cluster && cluster.tlsServerName) { - // The ws docs say that it accepts anything that https.RequestOptions accepts, - // but Typescript doesn't understand that idea (yet) probably could be fixed in - // the typings, but for now just cast to any - (opts as any).servername = cluster.tlsServerName; - } + public addUser(user: User): void { + if (!this.users) { + this.users = []; } - public async applyToRequest(opts: request.Options): Promise { - const cluster = this.getCurrentCluster(); - const user = this.getCurrentUser(); - - await this.applyOptions(opts); - - if (cluster && cluster.skipTLSVerify) { - opts.strictSSL = false; - } - - if (user && user.username) { - opts.auth = { - password: user.password, - username: user.username, - }; - } - - if (cluster && cluster.tlsServerName) { - opts.agentOptions = { servername: cluster.tlsServerName } as https.AgentOptions; - } + if (this.users.some((c) => c.name === user.name)) { + throw new Error(`Duplicate user: ${user.name}`); } - public loadFromString(config: string, opts?: Partial): void { - const obj = yaml.load(config) as any; - this.clusters = newClusters(obj.clusters, opts); - this.contexts = newContexts(obj.contexts, opts); - this.users = newUsers(obj.users, opts); - this.currentContext = obj['current-context']; - } - - public loadFromOptions(options: { - clusters: Cluster[]; - contexts: Context[]; - currentContext: Context['name']; - users: User[]; - }): void { - this.clusters = options.clusters; - this.contexts = options.contexts; - this.users = options.users; - this.currentContext = options.currentContext; - } - - public loadFromClusterAndUser(cluster: Cluster, user: User): void { - this.clusters = [cluster]; - this.users = [user]; - this.currentContext = 'loaded-context'; - this.contexts = [ - { - cluster: cluster.name, - user: user.name, - name: this.currentContext, - } as Context, - ]; - } - - public loadFromCluster(pathPrefix: string = ''): void { - const host = process.env.KUBERNETES_SERVICE_HOST; - const port = process.env.KUBERNETES_SERVICE_PORT; - const clusterName = 'inCluster'; - const userName = 'inClusterUser'; - const contextName = 'inClusterContext'; - const tokenFile = process.env.TOKEN_FILE_PATH - ? process.env.TOKEN_FILE_PATH - : `${pathPrefix}${Config.SERVICEACCOUNT_TOKEN_PATH}`; - const caFile = process.env.KUBERNETES_CA_FILE_PATH - ? process.env.KUBERNETES_CA_FILE_PATH - : `${pathPrefix}${Config.SERVICEACCOUNT_CA_PATH}`; - - let scheme = 'https'; - if (port === '80' || port === '8080' || port === '8001') { - scheme = 'http'; - } - - // Wrap raw IPv6 addresses in brackets. - let serverHost = host; - if (host && net.isIPv6(host)) { - serverHost = `[${host}]`; - } - - this.clusters = [ - { - name: clusterName, - caFile, - server: `${scheme}://${serverHost}:${port}`, - skipTLSVerify: false, - }, - ]; - this.users = [ - { - name: userName, - authProvider: { - name: 'tokenFile', - config: { - tokenFile, - }, - }, - }, - ]; - const namespaceFile = `${pathPrefix}${Config.SERVICEACCOUNT_NAMESPACE_PATH}`; - let namespace: string | undefined; - if (fileExists(namespaceFile)) { - namespace = fs.readFileSync(namespaceFile).toString('utf-8'); - } - this.contexts = [ - { - cluster: clusterName, - name: contextName, - user: userName, - namespace, - }, - ]; - this.currentContext = contextName; - } - - public mergeConfig(config: KubeConfig, preserveContext: boolean = false): void { - if (!preserveContext && config.currentContext) { - this.currentContext = config.currentContext; - } - config.clusters.forEach((cluster: Cluster) => { - this.addCluster(cluster); - }); - config.users.forEach((user: User) => { - this.addUser(user); - }); - config.contexts.forEach((ctx: Context) => { - this.addContext(ctx); - }); - } - - public addCluster(cluster: Cluster): void { - if (!this.clusters) { - this.clusters = []; - } - - if (this.clusters.some((c) => c.name === cluster.name)) { - throw new Error(`Duplicate cluster: ${cluster.name}`); - } + this.users.push(user); + } - this.clusters.push(cluster); + public addContext(ctx: Context): void { + if (!this.contexts) { + this.contexts = []; } - public addUser(user: User): void { - if (!this.users) { - this.users = []; - } - - if (this.users.some((c) => c.name === user.name)) { - throw new Error(`Duplicate user: ${user.name}`); - } - - this.users.push(user); + if (this.contexts.some((c) => c.name === ctx.name)) { + throw new Error(`Duplicate context: ${ctx.name}`); } - public addContext(ctx: Context): void { - if (!this.contexts) { - this.contexts = []; - } - - if (this.contexts.some((c) => c.name === ctx.name)) { - throw new Error(`Duplicate context: ${ctx.name}`); - } + this.contexts.push(ctx); + } - this.contexts.push(ctx); + public loadFromDefault(opts?: Partial, contextFromStartingConfig: boolean = false): void { + if (process.env.KUBECONFIG && process.env.KUBECONFIG.length > 0) { + const files = process.env.KUBECONFIG.split(path.delimiter).filter((filename: string) => filename); + this.loadFromFile(files[0], opts); + for (let i = 1; i < files.length; i++) { + const kc = new KubeConfig(); + kc.loadFromFile(files[i], opts); + this.mergeConfig(kc, contextFromStartingConfig); + } + return; } - - public loadFromDefault(opts?: Partial, contextFromStartingConfig: boolean = false): void { - if (process.env.KUBECONFIG && process.env.KUBECONFIG.length > 0) { - const files = process.env.KUBECONFIG.split(path.delimiter).filter((filename: string) => filename); - this.loadFromFile(files[0], opts); - for (let i = 1; i < files.length; i++) { - const kc = new KubeConfig(); - kc.loadFromFile(files[i], opts); - this.mergeConfig(kc, contextFromStartingConfig); - } - return; - } - const home = findHomeDir(); - if (home) { - const config = path.join(home, '.kube', 'config'); - if (fileExists(config)) { - this.loadFromFile(config, opts); - return; - } - } - if (process.platform === 'win32') { - try { - const envKubeconfigPathResult = child_process.spawnSync('wsl.exe', [ - 'bash', - '-c', - 'printenv KUBECONFIG', - ]); - if (envKubeconfigPathResult.status === 0 && envKubeconfigPathResult.stdout.length > 0) { - const result = child_process.spawnSync('wsl.exe', [ - 'cat', - envKubeconfigPathResult.stdout.toString('utf8'), - ]); - if (result.status === 0) { - this.loadFromString(result.stdout.toString('utf8'), opts); - return; - } - } - } catch (err) { - // Falling back to default kubeconfig - } - try { - const configResult = child_process.spawnSync('wsl.exe', ['cat', '~/.kube/config']); - if (configResult.status === 0) { - this.loadFromString(configResult.stdout.toString('utf8'), opts); - const result = child_process.spawnSync('wsl.exe', ['wslpath', '-w', '~/.kube']); - if (result.status === 0) { - this.makePathsAbsolute(result.stdout.toString('utf8')); - } - return; - } - } catch (err) { - // Falling back to alternative auth - } - } - - if ( - fileExists(Config.SERVICEACCOUNT_TOKEN_PATH) || - (process.env.TOKEN_FILE_PATH !== undefined && process.env.TOKEN_FILE_PATH !== '') - ) { - this.loadFromCluster(); - return; - } - - this.loadFromClusterAndUser( - { name: 'cluster', server: 'http://localhost:8080' } as Cluster, - { name: 'user' } as User, - ); - } - - public makePathsAbsolute(rootDirectory: string): void { - this.clusters.forEach((cluster: Cluster) => { - if (cluster.caFile) { - cluster.caFile = makeAbsolutePath(rootDirectory, cluster.caFile); - } - }); - this.users.forEach((user: User) => { - if (user.certFile) { - user.certFile = makeAbsolutePath(rootDirectory, user.certFile); - } - if (user.keyFile) { - user.keyFile = makeAbsolutePath(rootDirectory, user.keyFile); - } - }); - } - - public exportConfig(): string { - const configObj = { - apiVersion: 'v1', - kind: 'Config', - clusters: this.clusters.map(exportCluster), - users: this.users.map(exportUser), - contexts: this.contexts.map(exportContext), - preferences: {}, - 'current-context': this.getCurrentContext(), - }; - - return JSON.stringify(configObj); - } - - private getCurrentContextObject(): Context | null { - return this.getContextObject(this.currentContext); - } - - private applyHTTPSOptions(opts: request.Options | https.RequestOptions | WebSocket.ClientOptions): void { - const cluster = this.getCurrentCluster(); - const user = this.getCurrentUser(); - if (!user) { - return; - } - - if (cluster != null && cluster.skipTLSVerify) { - opts.rejectUnauthorized = false; - } - const ca = cluster != null ? bufferFromFileOrString(cluster.caFile, cluster.caData) : null; - if (ca) { - opts.ca = ca; - } - const cert = bufferFromFileOrString(user.certFile, user.certData); - if (cert) { - opts.cert = cert; - } - const key = bufferFromFileOrString(user.keyFile, user.keyData); - if (key) { - opts.key = key; - } + const home = findHomeDir(); + if (home) { + const config = path.join(home, '.kube', 'config'); + if (fileExists(config)) { + this.loadFromFile(config, opts); + return; + } } - - private async applyAuthorizationHeader( - opts: request.Options | https.RequestOptions | WebSocket.ClientOptions, - ): Promise { - const user = this.getCurrentUser(); - if (!user) { + if (process.platform === 'win32') { + try { + const envKubeconfigPathResult = child_process.spawnSync('wsl.exe', ['bash', '-c', 'printenv KUBECONFIG']); + if (envKubeconfigPathResult.status === 0 && envKubeconfigPathResult.stdout.length > 0) { + const result = child_process.spawnSync('wsl.exe', ['cat', envKubeconfigPathResult.stdout.toString('utf8')]); + if (result.status === 0) { + this.loadFromString(result.stdout.toString('utf8'), opts); return; - } - const authenticator = this.authenticators.find((elt: Authenticator) => { - return elt.isAuthProvider(user); - }); - - if (!opts.headers) { - opts.headers = {}; - } - if (authenticator) { - await authenticator.applyAuthentication(user, opts); - } - - if (user.token) { - opts.headers.Authorization = `Bearer ${user.token}`; - } - } - - private async applyOptions( - opts: request.Options | https.RequestOptions | WebSocket.ClientOptions, - ): Promise { - this.applyHTTPSOptions(opts); - await this.applyAuthorizationHeader(opts); - } + } + } + } catch (err) { + // Falling back to default kubeconfig + } + try { + const configResult = child_process.spawnSync('wsl.exe', ['cat', '~/.kube/config']); + if (configResult.status === 0) { + this.loadFromString(configResult.stdout.toString('utf8'), opts); + const result = child_process.spawnSync('wsl.exe', ['wslpath', '-w', '~/.kube']); + if (result.status === 0) { + this.makePathsAbsolute(result.stdout.toString('utf8')); + } + return; + } + } catch (err) { + // Falling back to alternative auth + } + } + + if ( + fileExists(Config.SERVICEACCOUNT_TOKEN_PATH) || + (process.env.TOKEN_FILE_PATH !== undefined && process.env.TOKEN_FILE_PATH !== '') + ) { + this.loadFromCluster(); + return; + } + + this.loadFromClusterAndUser( + { name: 'cluster', server: 'http://localhost:8080' } as Cluster, + { name: 'user' } as User + ); + } + + public makePathsAbsolute(rootDirectory: string): void { + this.clusters.forEach((cluster: Cluster) => { + if (cluster.caFile) { + cluster.caFile = makeAbsolutePath(rootDirectory, cluster.caFile); + } + }); + this.users.forEach((user: User) => { + if (user.certFile) { + user.certFile = makeAbsolutePath(rootDirectory, user.certFile); + } + if (user.keyFile) { + user.keyFile = makeAbsolutePath(rootDirectory, user.keyFile); + } + }); + } + + public exportConfig(): string { + const configObj = { + apiVersion: 'v1', + kind: 'Config', + clusters: this.clusters.map(exportCluster), + users: this.users.map(exportUser), + contexts: this.contexts.map(exportContext), + preferences: {}, + 'current-context': this.getCurrentContext(), + }; + + return JSON.stringify(configObj); + } + + private getCurrentContextObject(): Context | null { + return this.getContextObject(this.currentContext); + } + + private applyHTTPSOptions(opts: request.Options | https.RequestOptions | WebSocket.ClientOptions): void { + const cluster = this.getCurrentCluster(); + const user = this.getCurrentUser(); + if (!user) { + return; + } + + if (cluster != null && cluster.skipTLSVerify) { + opts.rejectUnauthorized = false; + } + const ca = cluster != null ? bufferFromFileOrString(cluster.caFile, cluster.caData) : null; + if (ca) { + opts.ca = ca; + } + const cert = bufferFromFileOrString(user.certFile, user.certData); + if (cert) { + opts.cert = cert; + } + const key = bufferFromFileOrString(user.keyFile, user.keyData); + if (key) { + opts.key = key; + } + } + + private async applyAuthorizationHeader( + opts: request.Options | https.RequestOptions | WebSocket.ClientOptions + ): Promise { + const user = this.getCurrentUser(); + if (!user) { + return; + } + const authenticator = this.authenticators.find((elt: Authenticator) => { + return elt.isAuthProvider(user); + }); + + if (!opts.headers) { + opts.headers = {}; + } + if (authenticator) { + await authenticator.applyAuthentication(user, opts); + } + + if (user.token) { + opts.headers.Authorization = `Bearer ${user.token}`; + } + } + + private async applyOptions(opts: request.Options | https.RequestOptions | WebSocket.ClientOptions): Promise { + this.applyHTTPSOptions(opts); + await this.applyAuthorizationHeader(opts); + } } // This class is deprecated and will eventually be removed. export class Config { - public static SERVICEACCOUNT_ROOT: string = '/var/run/secrets/kubernetes.io/serviceaccount'; - public static SERVICEACCOUNT_CA_PATH: string = Config.SERVICEACCOUNT_ROOT + '/ca.crt'; - public static SERVICEACCOUNT_TOKEN_PATH: string = Config.SERVICEACCOUNT_ROOT + '/token'; - public static SERVICEACCOUNT_NAMESPACE_PATH: string = Config.SERVICEACCOUNT_ROOT + '/namespace'; + public static SERVICEACCOUNT_ROOT: string = '/var/run/secrets/kubernetes.io/serviceaccount'; + public static SERVICEACCOUNT_CA_PATH: string = Config.SERVICEACCOUNT_ROOT + '/ca.crt'; + public static SERVICEACCOUNT_TOKEN_PATH: string = Config.SERVICEACCOUNT_ROOT + '/token'; + public static SERVICEACCOUNT_NAMESPACE_PATH: string = Config.SERVICEACCOUNT_ROOT + '/namespace'; } export function makeAbsolutePath(root: string, file: string): string { - if (!root || path.isAbsolute(file)) { - return file; - } - return path.join(root, file); + if (!root || path.isAbsolute(file)) { + return file; + } + return path.join(root, file); } // This is public really only for testing. export function bufferFromFileOrString(file?: string, data?: string): Buffer | null { - if (file) { - return fs.readFileSync(file); - } - if (data) { - return Buffer.from(data, 'base64'); - } - return null; + if (file) { + return fs.readFileSync(file); + } + if (data) { + return Buffer.from(data, 'base64'); + } + return null; } function dropDuplicatesAndNils(a: string[]): string[] { - return a.reduce((acceptedValues, currentValue) => { - // Good-enough algorithm for reducing a small (3 items at this point) array into an ordered list - // of unique non-empty strings. - if (currentValue && !acceptedValues.includes(currentValue)) { - return acceptedValues.concat(currentValue); - } else { - return acceptedValues; - } - }, [] as string[]); + return a.reduce((acceptedValues, currentValue) => { + // Good-enough algorithm for reducing a small (3 items at this point) array into an ordered list + // of unique non-empty strings. + if (currentValue && !acceptedValues.includes(currentValue)) { + return acceptedValues.concat(currentValue); + } else { + return acceptedValues; + } + }, [] as string[]); } // Only public for testing. export function findHomeDir(): string | null { - if (process.platform !== 'win32') { - if (process.env.HOME) { - try { - fs.accessSync(process.env.HOME); - return process.env.HOME; - // tslint:disable-next-line:no-empty - } catch (ignore) {} - } - return null; - } - // $HOME is always favoured, but the k8s go-client prefers the other two env vars - // differently depending on whether .kube/config exists or not. - const homeDrivePath = - process.env.HOMEDRIVE && process.env.HOMEPATH - ? path.join(process.env.HOMEDRIVE, process.env.HOMEPATH) - : ''; - const homePath = process.env.HOME || ''; - const userProfile = process.env.USERPROFILE || ''; - const favourHomeDrivePathList: string[] = dropDuplicatesAndNils([homePath, homeDrivePath, userProfile]); - const favourUserProfileList: string[] = dropDuplicatesAndNils([homePath, userProfile, homeDrivePath]); - // 1. the first of %HOME%, %HOMEDRIVE%%HOMEPATH%, %USERPROFILE% containing a `.kube\config` file is returned. - for (const dir of favourHomeDrivePathList) { - try { - fs.accessSync(path.join(dir, '.kube', 'config')); - return dir; - // tslint:disable-next-line:no-empty - } catch (ignore) {} - } - // 2. ...the first of %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that exists and is writeable is returned - for (const dir of favourUserProfileList) { - try { - fs.accessSync(dir, fs.constants.W_OK); - return dir; - // tslint:disable-next-line:no-empty - } catch (ignore) {} - } - // 3. ...the first of %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that exists is returned. - for (const dir of favourUserProfileList) { - try { - fs.accessSync(dir); - return dir; - // tslint:disable-next-line:no-empty - } catch (ignore) {} - } - // 4. if none of those locations exists, the first of - // %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that is set is returned. - return favourUserProfileList[0] || null; + if (process.platform !== 'win32') { + if (process.env.HOME) { + try { + fs.accessSync(process.env.HOME); + return process.env.HOME; + // tslint:disable-next-line:no-empty + } catch (ignore) {} + } + return null; + } + // $HOME is always favoured, but the k8s go-client prefers the other two env vars + // differently depending on whether .kube/config exists or not. + const homeDrivePath = + process.env.HOMEDRIVE && process.env.HOMEPATH ? path.join(process.env.HOMEDRIVE, process.env.HOMEPATH) : ''; + const homePath = process.env.HOME || ''; + const userProfile = process.env.USERPROFILE || ''; + const favourHomeDrivePathList: string[] = dropDuplicatesAndNils([homePath, homeDrivePath, userProfile]); + const favourUserProfileList: string[] = dropDuplicatesAndNils([homePath, userProfile, homeDrivePath]); + // 1. the first of %HOME%, %HOMEDRIVE%%HOMEPATH%, %USERPROFILE% containing a `.kube\config` file is returned. + for (const dir of favourHomeDrivePathList) { + try { + fs.accessSync(path.join(dir, '.kube', 'config')); + return dir; + // tslint:disable-next-line:no-empty + } catch (ignore) {} + } + // 2. ...the first of %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that exists and is writeable is returned + for (const dir of favourUserProfileList) { + try { + fs.accessSync(dir, fs.constants.W_OK); + return dir; + // tslint:disable-next-line:no-empty + } catch (ignore) {} + } + // 3. ...the first of %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that exists is returned. + for (const dir of favourUserProfileList) { + try { + fs.accessSync(dir); + return dir; + // tslint:disable-next-line:no-empty + } catch (ignore) {} + } + // 4. if none of those locations exists, the first of + // %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that is set is returned. + return favourUserProfileList[0] || null; } export interface Named { - name: string; + name: string; } // Only really public for testing... export function findObject(list: T[], name: string, key: string): T | null { - if (!list) { - return null; - } - for (const obj of list) { - if (obj.name === name) { - if ((obj as any)[key]) { - (obj as any)[key].name = name; - return (obj as any)[key]; - } - return obj; - } - } + if (!list) { return null; + } + for (const obj of list) { + if (obj.name === name) { + if ((obj as any)[key]) { + (obj as any)[key].name = name; + return (obj as any)[key]; + } + return obj; + } + } + return null; } diff --git a/packages/kubekit-client/src/lib/config/json_path.ts b/packages/kubekit-client/src/lib/config/json_path.ts index 5a619235..6256d47c 100644 --- a/packages/kubekit-client/src/lib/config/json_path.ts +++ b/packages/kubekit-client/src/lib/config/json_path.ts @@ -1,8 +1,8 @@ import { JSONPath } from 'jsonpath-plus'; export function jsonpath(path: string, json: object): any { - return JSONPath({ - path, - json, - }); + return JSONPath({ + path, + json, + }); } diff --git a/packages/kubekit-client/src/lib/config/oidc_auth.ts b/packages/kubekit-client/src/lib/config/oidc_auth.ts index 29e23988..93956c0b 100644 --- a/packages/kubekit-client/src/lib/config/oidc_auth.ts +++ b/packages/kubekit-client/src/lib/config/oidc_auth.ts @@ -8,111 +8,109 @@ import { Authenticator } from './auth'; import { User } from './config_types'; interface JwtObj { - header: any; - payload: any; - signature: string; + header: any; + payload: any; + signature: string; } export class OpenIDConnectAuth implements Authenticator { - public static decodeJWT(token: string): JwtObj | null { - const parts = token.split('.'); - if (parts.length !== 3) { - return null; - } + public static decodeJWT(token: string): JwtObj | null { + const parts = token.split('.'); + if (parts.length !== 3) { + return null; + } - const header = JSON.parse(new TextDecoder().decode(base64url.parse(parts[0], { loose: true }))); - const payload = JSON.parse(new TextDecoder().decode(base64url.parse(parts[1], { loose: true }))); - const signature = parts[2]; + const header = JSON.parse(new TextDecoder().decode(base64url.parse(parts[0], { loose: true }))); + const payload = JSON.parse(new TextDecoder().decode(base64url.parse(parts[1], { loose: true }))); + const signature = parts[2]; - return { - header, - payload, - signature, - }; - } + return { + header, + payload, + signature, + }; + } - public static expirationFromToken(token: string): number { - const jwt = OpenIDConnectAuth.decodeJWT(token); - if (!jwt) { - return 0; - } - return jwt.payload.exp; + public static expirationFromToken(token: string): number { + const jwt = OpenIDConnectAuth.decodeJWT(token); + if (!jwt) { + return 0; } + return jwt.payload.exp; + } - // public for testing purposes. - private currentTokenExpiration: number = 0; - public isAuthProvider(user: User): boolean { - if (!user.authProvider) { - return false; - } - return user.authProvider.name === 'oidc'; + // public for testing purposes. + private currentTokenExpiration: number = 0; + public isAuthProvider(user: User): boolean { + if (!user.authProvider) { + return false; } + return user.authProvider.name === 'oidc'; + } - /** - * Setup the authentication header for oidc authed clients - * @param user user info - * @param opts request options - * @param overrideClient for testing, a preconfigured oidc client - */ - public async applyAuthentication( - user: User, - opts: request.Options | https.RequestOptions, - overrideClient?: any, - ): Promise { - const token = await this.getToken(user, overrideClient); - if (token) { - opts.headers!.Authorization = `Bearer ${token}`; - } + /** + * Setup the authentication header for oidc authed clients + * @param user user info + * @param opts request options + * @param overrideClient for testing, a preconfigured oidc client + */ + public async applyAuthentication( + user: User, + opts: request.Options | https.RequestOptions, + overrideClient?: any + ): Promise { + const token = await this.getToken(user, overrideClient); + if (token) { + opts.headers!.Authorization = `Bearer ${token}`; } + } - private async getToken(user: User, overrideClient?: Client): Promise { - if (!user.authProvider.config) { - return null; - } - if (!user.authProvider.config['client-secret']) { - user.authProvider.config['client-secret'] = ''; - } - if (!user.authProvider.config || !user.authProvider.config['id-token']) { - return null; - } - return this.refresh(user, overrideClient); + private async getToken(user: User, overrideClient?: Client): Promise { + if (!user.authProvider.config) { + return null; } + if (!user.authProvider.config['client-secret']) { + user.authProvider.config['client-secret'] = ''; + } + if (!user.authProvider.config || !user.authProvider.config['id-token']) { + return null; + } + return this.refresh(user, overrideClient); + } - private async refresh(user: User, overrideClient?: Client): Promise { - if (this.currentTokenExpiration === 0) { - this.currentTokenExpiration = OpenIDConnectAuth.expirationFromToken( - user.authProvider.config['id-token'], - ); - } - if (Date.now() / 1000 > this.currentTokenExpiration) { - if ( - !user.authProvider.config['client-id'] || - !user.authProvider.config['refresh-token'] || - !user.authProvider.config['idp-issuer-url'] - ) { - return null; - } - - const client = overrideClient ? overrideClient : await this.getClient(user); - const newToken = await client.refresh(user.authProvider.config['refresh-token']); - user.authProvider.config['id-token'] = newToken.id_token; - user.authProvider.config['refresh-token'] = newToken.refresh_token; - this.currentTokenExpiration = newToken.expires_at || 0; - } - return user.authProvider.config['id-token']; + private async refresh(user: User, overrideClient?: Client): Promise { + if (this.currentTokenExpiration === 0) { + this.currentTokenExpiration = OpenIDConnectAuth.expirationFromToken(user.authProvider.config['id-token']); } + if (Date.now() / 1000 > this.currentTokenExpiration) { + if ( + !user.authProvider.config['client-id'] || + !user.authProvider.config['refresh-token'] || + !user.authProvider.config['idp-issuer-url'] + ) { + return null; + } - private async getClient(user: User): Promise { - const oidcIssuer = await Issuer.discover(user.authProvider.config['idp-issuer-url']); - const metadata: ClientMetadata = { - client_id: user.authProvider.config['client-id'], - client_secret: user.authProvider.config['client-secret'], - }; + const client = overrideClient ? overrideClient : await this.getClient(user); + const newToken = await client.refresh(user.authProvider.config['refresh-token']); + user.authProvider.config['id-token'] = newToken.id_token; + user.authProvider.config['refresh-token'] = newToken.refresh_token; + this.currentTokenExpiration = newToken.expires_at || 0; + } + return user.authProvider.config['id-token']; + } - if (!user.authProvider.config['client-secret']) { - metadata.token_endpoint_auth_method = 'none'; - } + private async getClient(user: User): Promise { + const oidcIssuer = await Issuer.discover(user.authProvider.config['idp-issuer-url']); + const metadata: ClientMetadata = { + client_id: user.authProvider.config['client-id'], + client_secret: user.authProvider.config['client-secret'], + }; - return new oidcIssuer.Client(metadata); + if (!user.authProvider.config['client-secret']) { + metadata.token_endpoint_auth_method = 'none'; } + + return new oidcIssuer.Client(metadata); + } } diff --git a/packages/kubekit-client/src/lib/config/oidc_auth_delayed.ts b/packages/kubekit-client/src/lib/config/oidc_auth_delayed.ts index 4ed4198c..2f6f0600 100644 --- a/packages/kubekit-client/src/lib/config/oidc_auth_delayed.ts +++ b/packages/kubekit-client/src/lib/config/oidc_auth_delayed.ts @@ -5,25 +5,25 @@ import { Authenticator } from './auth'; import { User } from './config_types'; export class DelayedOpenIDConnectAuth implements Authenticator { - public isAuthProvider(user: User): boolean { - if (!user.authProvider) { - return false; - } - return user.authProvider.name === 'oidc'; + public isAuthProvider(user: User): boolean { + if (!user.authProvider) { + return false; } + return user.authProvider.name === 'oidc'; + } - /** - * Setup the authentication header for oidc authed clients - * @param user user info - * @param opts request options - * @param overrideClient for testing, a preconfigured oidc client - */ - public async applyAuthentication( - user: User, - opts: request.Options | https.RequestOptions, - overrideClient?: any, - ): Promise { - const oidc = await import('./oidc_auth'); - return new oidc.OpenIDConnectAuth().applyAuthentication(user, opts, overrideClient); - } + /** + * Setup the authentication header for oidc authed clients + * @param user user info + * @param opts request options + * @param overrideClient for testing, a preconfigured oidc client + */ + public async applyAuthentication( + user: User, + opts: request.Options | https.RequestOptions, + overrideClient?: any + ): Promise { + const oidc = await import('./oidc_auth'); + return new oidc.OpenIDConnectAuth().applyAuthentication(user, opts, overrideClient); + } } diff --git a/packages/kubekit-client/src/lib/task_manager.ts b/packages/kubekit-client/src/lib/task_manager.ts index 3004f44f..7af85c22 100644 --- a/packages/kubekit-client/src/lib/task_manager.ts +++ b/packages/kubekit-client/src/lib/task_manager.ts @@ -22,12 +22,7 @@ export class TaskManager { private wait: number; private maxWait: number; - constructor({ - concurrency = Infinity, - isPaused = false, - wait = 0, - maxWait = 0, - }: TaskManagerOptions = {}) { + constructor({ concurrency = Infinity, isPaused = false, wait = 0, maxWait = 0 }: TaskManagerOptions = {}) { this.concurrency = concurrency; this.currentlyRunning = 0; this.queue = {}; diff --git a/packages/kubekit-client/test/task_manager.test.ts b/packages/kubekit-client/test/task_manager.test.ts index e0c2d54b..dbbf39df 100644 --- a/packages/kubekit-client/test/task_manager.test.ts +++ b/packages/kubekit-client/test/task_manager.test.ts @@ -21,7 +21,7 @@ describe('TaskManager', () => { it('should execute tasks in parallel up to the concurrency limit and complete all tasks', async () => { const concurrency = 2; const taskManager = new TaskManager({ - concurrency + concurrency, }); const taskCallbacks = Array.from({ length: 100 }, () => vi.fn()); const num = 100; @@ -181,7 +181,7 @@ describe('TaskManager', () => { it('should debounce tasks for the same namespace/name', async () => { const taskManager = new TaskManager({ - wait: 10 + wait: 10, }); const taskCallback = vi.fn();