@kubernetes/client-node
Version:
NodeJS client for kubernetes
612 lines • 22 kB
JavaScript
import fs from 'node:fs';
import https from 'node:https';
import http from 'node:http';
import yaml from 'js-yaml';
import net from 'node:net';
import path from 'node:path';
import { Headers } from 'node-fetch';
import { AzureAuth } from './azure_auth.js';
import { exportCluster, exportContext, exportUser, newClusters, newContexts, newUsers, } from './config_types.js';
import { ExecAuth } from './exec_auth.js';
import { FileAuth } from './file_auth.js';
import { GoogleCloudPlatformAuth } from './gcp_auth.js';
import { createConfiguration, ServerConfiguration, } from './gen/index.js';
import { OpenIDConnectAuth } from './oidc_auth.js';
import child_process from 'node:child_process';
import { SocksProxyAgent } from 'socks-proxy-agent';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
const SERVICEACCOUNT_ROOT = '/var/run/secrets/kubernetes.io/serviceaccount';
const SERVICEACCOUNT_CA_PATH = SERVICEACCOUNT_ROOT + '/ca.crt';
const SERVICEACCOUNT_TOKEN_PATH = SERVICEACCOUNT_ROOT + '/token';
const SERVICEACCOUNT_NAMESPACE_PATH = SERVICEACCOUNT_ROOT + '/namespace';
// fs.existsSync was removed in node 10
function fileExists(filepath) {
try {
fs.accessSync(filepath);
return true;
}
catch {
// Ignore errors.
return false;
}
}
export class KubeConfig {
// Optionally add additional external authenticators, you must do this
// before you load a kubeconfig file that references them.
addAuthenticator(authenticator) {
this.custom_authenticators.push(authenticator);
}
constructor() {
// List of custom authenticators that can be added by the user
this.custom_authenticators = [];
this.contexts = [];
this.clusters = [];
this.users = [];
}
getContexts() {
return this.contexts;
}
getClusters() {
return this.clusters;
}
getUsers() {
return this.users;
}
getCurrentContext() {
return this.currentContext;
}
setCurrentContext(context) {
this.currentContext = context;
}
getContextObject(name) {
if (!this.contexts) {
return null;
}
return findObject(this.contexts, name, 'context');
}
getCurrentCluster() {
const context = this.getCurrentContextObject();
if (!context) {
return null;
}
return this.getCluster(context.cluster);
}
getCluster(name) {
return findObject(this.clusters, name, 'cluster');
}
getCurrentUser() {
const ctx = this.getCurrentContextObject();
if (!ctx) {
return null;
}
return this.getUser(ctx.user);
}
getUser(name) {
return findObject(this.users, name, 'user');
}
loadFromFile(file, opts) {
const rootDirectory = path.dirname(file);
this.loadFromString(fs.readFileSync(file).toString('utf-8'), opts);
this.makePathsAbsolute(rootDirectory);
}
async applyToFetchOptions(opts) {
await this.applyToHTTPSOptions(opts);
const headers = new Headers();
for (const [key, val] of Object.entries(opts.headers || {})) {
if (Array.isArray(val)) {
val.forEach((innerVal) => {
headers.append(key, innerVal);
});
}
else if (typeof val === 'number' || typeof val === 'string') {
headers.set(key, val.toString());
}
}
if (opts.auth) {
headers.set('Authorization', 'Basic ' + Buffer.from(opts.auth).toString('base64'));
}
return {
agent: opts.agent,
headers,
method: opts.method,
timeout: opts.timeout,
};
}
async applyToHTTPSOptions(opts) {
const user = this.getCurrentUser();
const cluster = this.getCurrentCluster();
await this.applyOptions(opts);
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.auth = `${user.username}:${user.password}`;
}
const agentOptions = {};
// Copy AgentOptions from RequestOptions
agentOptions.ca = opts.ca;
agentOptions.cert = opts.cert;
agentOptions.key = opts.key;
agentOptions.pfx = opts.pfx;
agentOptions.passphrase = opts.passphrase;
agentOptions.rejectUnauthorized = opts.rejectUnauthorized;
// 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
agentOptions.timeout = opts.timeout;
agentOptions.servername = opts.servername;
agentOptions.ciphers = opts.ciphers;
agentOptions.honorCipherOrder = opts.honorCipherOrder;
agentOptions.ecdhCurve = opts.ecdhCurve;
agentOptions.clientCertEngine = opts.clientCertEngine;
agentOptions.crl = opts.crl;
agentOptions.dhparam = opts.dhparam;
agentOptions.secureOptions = opts.secureOptions;
agentOptions.secureProtocol = opts.secureProtocol;
agentOptions.sessionIdContext = opts.sessionIdContext;
opts.agent = this.createAgent(cluster, agentOptions);
}
/**
* Applies SecurityAuthentication to RequestContext of an API Call from API Client
* @param context
*/
async applySecurityAuthentication(context) {
const cluster = this.getCurrentCluster();
const user = this.getCurrentUser();
const agentOptions = {};
const httpsOptions = {};
await this.applyOptions(httpsOptions);
if (cluster && cluster.skipTLSVerify) {
agentOptions.rejectUnauthorized = false;
}
if (cluster && cluster.tlsServerName) {
agentOptions.servername = cluster.tlsServerName;
}
if (user && user.username) {
const auth = Buffer.from(`${user.username}:${user.password}`).toString('base64');
context.setHeaderParam('Authorization', `Basic ${auth}`);
}
// Copy headers from httpsOptions to RequestContext
const headers = httpsOptions.headers || {};
Object.entries(headers).forEach(([key, value]) => {
context.setHeaderParam(key, `${value}`);
});
// Copy AgentOptions from RequestOptions
agentOptions.ca = httpsOptions.ca;
agentOptions.cert = httpsOptions.cert;
agentOptions.key = httpsOptions.key;
agentOptions.pfx = httpsOptions.pfx;
agentOptions.passphrase = httpsOptions.passphrase;
agentOptions.rejectUnauthorized = httpsOptions.rejectUnauthorized;
context.setAgent(this.createAgent(cluster, agentOptions));
}
/**
* Returns name of this security authentication method
* @returns string
*/
getName() {
return 'kubeconfig authentication';
}
loadFromString(config, opts) {
const obj = yaml.load(config);
this.clusters = newClusters(obj.clusters, opts);
this.contexts = newContexts(obj.contexts, opts);
this.users = newUsers(obj.users, opts);
this.currentContext = obj['current-context'];
}
loadFromOptions(options) {
this.clusters = options.clusters;
this.contexts = options.contexts;
this.users = options.users;
this.currentContext = options.currentContext;
}
loadFromClusterAndUser(cluster, user) {
this.clusters = [cluster];
this.users = [user];
this.currentContext = 'loaded-context';
this.contexts = [
{
cluster: cluster.name,
user: user.name,
name: this.currentContext,
},
];
}
loadFromCluster(pathPrefix = '') {
const host = process.env.KUBERNETES_SERVICE_HOST;
const port = process.env.KUBERNETES_SERVICE_PORT;
const clusterName = 'inCluster';
const userName = 'inClusterUser';
const contextName = 'inClusterContext';
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: `${pathPrefix}${SERVICEACCOUNT_CA_PATH}`,
server: `${scheme}://${serverHost}:${port}`,
skipTLSVerify: false,
},
];
this.users = [
{
name: userName,
authProvider: {
name: 'tokenFile',
config: {
tokenFile: `${pathPrefix}${SERVICEACCOUNT_TOKEN_PATH}`,
},
},
},
];
const namespaceFile = `${pathPrefix}${SERVICEACCOUNT_NAMESPACE_PATH}`;
let namespace;
if (fileExists(namespaceFile)) {
namespace = fs.readFileSync(namespaceFile).toString('utf-8');
}
this.contexts = [
{
cluster: clusterName,
name: contextName,
user: userName,
namespace,
},
];
this.currentContext = contextName;
}
mergeConfig(config, preserveContext = false) {
if (!preserveContext && config.currentContext) {
this.currentContext = config.currentContext;
}
config.clusters.forEach((cluster) => {
this.addCluster(cluster);
});
config.users.forEach((user) => {
this.addUser(user);
});
config.contexts.forEach((ctx) => {
this.addContext(ctx);
});
}
addCluster(cluster) {
if (!this.clusters) {
this.clusters = [];
}
this.clusters.forEach((c, ix) => {
if (c.name === cluster.name) {
throw new Error(`Duplicate cluster: ${c.name}`);
}
});
this.clusters.push(cluster);
}
addUser(user) {
if (!this.users) {
this.users = [];
}
this.users.forEach((c, ix) => {
if (c.name === user.name) {
throw new Error(`Duplicate user: ${c.name}`);
}
});
this.users.push(user);
}
addContext(ctx) {
if (!this.contexts) {
this.contexts = [];
}
this.contexts.forEach((c, ix) => {
if (c.name === ctx.name) {
throw new Error(`Duplicate context: ${c.name}`);
}
});
this.contexts.push(ctx);
}
loadFromDefault(opts, contextFromStartingConfig = false, platform = process.platform) {
if (process.env.KUBECONFIG && process.env.KUBECONFIG.length > 0) {
const files = process.env.KUBECONFIG.split(path.delimiter).filter((filename) => 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(platform);
if (home) {
const config = path.join(home, '.kube', 'config');
if (fileExists(config)) {
this.loadFromFile(config, opts);
return;
}
}
if (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 {
// 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 {
// Falling back to alternative auth
}
}
if (fileExists(SERVICEACCOUNT_TOKEN_PATH)) {
this.loadFromCluster();
return;
}
this.loadFromClusterAndUser({ name: 'cluster', server: 'http://localhost:8080' }, { name: 'user' });
}
makeApiClient(apiClientType) {
const cluster = this.getCurrentCluster();
if (!cluster) {
throw new Error('No active cluster!');
}
const authConfig = {
default: this,
};
const baseServerConfig = new ServerConfiguration(cluster.server, {});
const config = createConfiguration({
baseServer: baseServerConfig,
authMethods: authConfig,
});
const apiClient = new apiClientType(config);
return apiClient;
}
makePathsAbsolute(rootDirectory) {
this.clusters.forEach((cluster) => {
if (cluster.caFile) {
cluster.caFile = makeAbsolutePath(rootDirectory, cluster.caFile);
}
});
this.users.forEach((user) => {
if (user.certFile) {
user.certFile = makeAbsolutePath(rootDirectory, user.certFile);
}
if (user.keyFile) {
user.keyFile = makeAbsolutePath(rootDirectory, user.keyFile);
}
});
}
exportConfig() {
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);
}
getCurrentContextObject() {
return this.getContextObject(this.currentContext);
}
createAgent(cluster, agentOptions) {
var _a, _b;
let agent;
if (cluster && cluster.proxyUrl) {
if (cluster.proxyUrl.startsWith('socks')) {
agent = new SocksProxyAgent(cluster.proxyUrl, agentOptions);
}
else if (cluster.server.startsWith('https')) {
const httpsProxyAgentOptions = agentOptions;
httpsProxyAgentOptions.proxy = cluster.proxyUrl;
agent = new HttpsProxyAgent(httpsProxyAgentOptions);
}
else if (cluster.server.startsWith('http')) {
const httpProxyAgentOptions = agentOptions;
httpProxyAgentOptions.proxy = cluster.proxyUrl;
agent = new HttpProxyAgent(httpProxyAgentOptions);
}
else {
throw new Error('Unsupported proxy type');
}
}
else if (((_a = cluster === null || cluster === void 0 ? void 0 : cluster.server) === null || _a === void 0 ? void 0 : _a.startsWith('http:')) && cluster.skipTLSVerify) {
agent = new http.Agent(agentOptions);
}
else if (((_b = cluster === null || cluster === void 0 ? void 0 : cluster.server) === null || _b === void 0 ? void 0 : _b.startsWith('http:')) && !cluster.skipTLSVerify) {
throw new Error('HTTP protocol is not allowed when skipTLSVerify is not set or false');
}
else {
agent = new https.Agent(agentOptions);
}
return agent;
}
applyHTTPSOptions(opts) {
const cluster = this.getCurrentCluster();
const user = this.getCurrentUser();
if (!user) {
return;
}
if (cluster != null && cluster.skipTLSVerify) {
opts.rejectUnauthorized = false;
}
if (cluster != null && cluster.tlsServerName) {
// WebSocket.ClientOptions types are missing the servername
opts.servername = cluster.tlsServerName;
}
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;
}
}
async applyAuthorizationHeader(opts) {
const user = this.getCurrentUser();
if (!user) {
return;
}
let authenticator = KubeConfig.authenticators.find((elt) => {
return elt.isAuthProvider(user);
});
if (!authenticator) {
authenticator = this.custom_authenticators.find((elt) => {
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}`;
}
}
async applyOptions(opts) {
this.applyHTTPSOptions(opts);
await this.applyAuthorizationHeader(opts);
}
}
KubeConfig.authenticators = [
new AzureAuth(),
new GoogleCloudPlatformAuth(),
new ExecAuth(),
new FileAuth(),
new OpenIDConnectAuth(),
];
export function makeAbsolutePath(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, data) {
if (file) {
return fs.readFileSync(file);
}
if (data) {
return Buffer.from(data, 'base64');
}
return null;
}
function dropDuplicatesAndNils(a) {
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;
}
}, []);
}
// Only public for testing.
export function findHomeDir(platform = process.platform) {
if (platform !== 'win32') {
if (process.env.HOME) {
try {
fs.accessSync(process.env.HOME);
return process.env.HOME;
}
catch {
// Ignore errors.
}
}
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 = dropDuplicatesAndNils([homePath, homeDrivePath, userProfile]);
const favourUserProfileList = 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;
}
catch {
// Ignore errors.
}
}
// 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;
}
catch {
// Ignore errors.
}
}
// 3. ...the first of %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that exists is returned.
for (const dir of favourUserProfileList) {
try {
fs.accessSync(dir);
return dir;
}
catch {
// Ignore errors.
}
}
// 4. if none of those locations exists, the first of
// %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that is set is returned.
return favourUserProfileList[0] || null;
}
// Only really public for testing...
export function findObject(list, name, key) {
if (!list) {
return null;
}
for (const obj of list) {
if (obj.name === name) {
if (obj[key]) {
obj[key].name = name;
return obj[key];
}
return obj;
}
}
return null;
}
//# sourceMappingURL=config.js.map