From cb94f02918f36c1c77003d247dbe3cbf0d62f3fa Mon Sep 17 00:00:00 2001 From: Silas Boyd-Wickizer Date: Mon, 24 Jun 2019 22:55:13 -0700 Subject: [PATCH] feat(KubeConfig): switch to using `KubeConfig` for config handling (#506) --- README.md | 41 +- backends/request/client.js | 38 +- backends/request/client.test.js | 7 +- backends/request/config.js | 109 ++++ .../request/config.test/deprecated.test.js | 538 ++++++++++++++++++ backends/request/config.test/index.test.js | 157 ++--- examples/basic.js | 3 +- lib/config.js | 3 + lib/index.js | 3 +- lib/swagger-client.js | 5 +- lib/swagger-client.test.js | 20 +- merging-with-kubernetes.md | 27 + test-integration/env.js | 6 +- 13 files changed, 801 insertions(+), 156 deletions(-) create mode 100644 backends/request/config.test/deprecated.test.js create mode 100644 lib/config.js diff --git a/README.md b/README.md index 123ba3cd..e88907c3 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,9 @@ the cluster's kubeconfig file and that cluster's API specification. To create the config required to make a client, you can either: -let kubernetes-client load the file automatically through the `KUBECONFIG` -env +let kubernetes-client configure automatically by trying the `KUBECONFIG` +environment variable first, then `~/.kube/config`, then an in-cluster +service account, and lastly settling on a default proxy configuration: ```js const client = new Client({ version: '1.13' }) @@ -34,33 +35,41 @@ const client = new Client({ version: '1.13' }) provide your own path to a file: ```js +const { KubeConfig } = require('kubernetes-client') +const kubeconfig = new KubeConfig() +kubeconfig.loadFromFile('~/some/path') const Request = require('kubernetes-client/backends/request') -const backend = new Request(Request.config.fromKubeconfig('~/some/path')) + +const backend = new Request({ kubeconfig }) const client = new Client({ backend, version: '1.13' }) ``` -provide a kubeconfig object from memory: +provide a configuration object from memory: ```js // Should match the kubeconfig file format exactly -const kubeconfig = { - apiVersion: 'v1', - clusters: [], - contexts: [], - 'current-context': '', - kind: 'Config', - users: [] +const config = { + apiVersion: 'v1', + clusters: [], + contexts: [], + 'current-context': '', + kind: 'Config', + users: [] } +const { KubeConfig } = require('kubernetes-client') +const kubeconfig = new KubeConfig() +kubeconfig.loadFromString(JSON.stringify(config)) + const Request = require('kubernetes-client/backends/request') -const backend = new Request(Request.config.fromKubeconfig(kubeconfig)) +const backend = new Request({ kubeconfig }) const client = new Client({ backend, version: '1.13' }) ``` -and you can also specify the kubeconfig context by passing it as the -second argument to `fromKubeconfig()`: +and you can also specify the context by setting it in the `kubeconfig` +object: -``` -const config = Request.config.fromKubeconfig(null, 'dev') +```js +kubeconfig.setCurrentContext('dev') ``` You can also elide the `.version` and pass an OpenAPI specification: diff --git a/backends/request/client.js b/backends/request/client.js index 9913b010..c179b276 100644 --- a/backends/request/client.js +++ b/backends/request/client.js @@ -1,5 +1,7 @@ 'use strict' +const { convertKubeconfig } = require('./config') +const deprecate = require('depd')('kubernetes-client') const JSONStream = require('json-stream') const pump = require('pump') const qs = require('qs') @@ -100,26 +102,36 @@ class Request { */ constructor (options) { this.requestOptions = options.request || {} + + let convertedOptions + if (!options.kubeconfig) { + deprecate('Request() without a .kubeconfig option, see ' + + 'https://github.com/godaddy/kubernetes-client/blob/master/merging-with-kubernetes.md') + convertedOptions = options + } else { + convertedOptions = convertKubeconfig(options.kubeconfig) + } + this.requestOptions.qsStringifyOptions = { indices: false } - this.requestOptions.baseUrl = options.url - this.requestOptions.ca = options.ca - this.requestOptions.cert = options.cert - this.requestOptions.key = options.key - if ('insecureSkipTlsVerify' in options) { - this.requestOptions.strictSSL = !options.insecureSkipTlsVerify + this.requestOptions.baseUrl = convertedOptions.url + this.requestOptions.ca = convertedOptions.ca + this.requestOptions.cert = convertedOptions.cert + this.requestOptions.key = convertedOptions.key + if ('insecureSkipTlsVerify' in convertedOptions) { + this.requestOptions.strictSSL = !convertedOptions.insecureSkipTlsVerify } - if ('timeout' in options) { - this.requestOptions.timeout = options.timeout + if ('timeout' in convertedOptions) { + this.requestOptions.timeout = convertedOptions.timeout } this.authProvider = { type: null } - if (options.auth) { - this.requestOptions.auth = options.auth - if (options.auth.provider) { - this.requestOptions.auth = options.auth.request - this.authProvider = options.auth.provider + if (convertedOptions.auth) { + this.requestOptions.auth = convertedOptions.auth + if (convertedOptions.auth.provider) { + this.requestOptions.auth = convertedOptions.auth.request + this.authProvider = convertedOptions.auth.provider } } } diff --git a/backends/request/client.test.js b/backends/request/client.test.js index 069ea7a6..d7d92948 100644 --- a/backends/request/client.test.js +++ b/backends/request/client.test.js @@ -4,9 +4,14 @@ const { expect } = require('chai') const nock = require('nock') +const KubeConfig = require('../../lib/config') const Request = require('./client') const url = 'http://mock.kube.api' +const kubeconfig = new KubeConfig() +kubeconfig.loadFromClusterAndUser( + { name: 'cluster', server: url }, + { name: 'user' }) describe('lib.backends.request', () => { describe('Request', () => { @@ -15,7 +20,7 @@ describe('lib.backends.request', () => { .get('/foo') .reply(200) - const backend = new Request({ url }) + const backend = new Request({ kubeconfig }) backend.http({ method: 'GET', pathname: '/foo' diff --git a/backends/request/config.js b/backends/request/config.js index 0a8d135e..6b3a36da 100644 --- a/backends/request/config.js +++ b/backends/request/config.js @@ -53,6 +53,115 @@ function getInCluster () { module.exports.getInCluster = getInCluster +function convertKubeconfig (kubeconfig) { + const context = kubeconfig.getCurrentContext() + const cluster = kubeconfig.getCurrentCluster() + const user = kubeconfig.getCurrentUser() + const namespace = context.namespace + + let ca + let insecureSkipTlsVerify = false + if (cluster) { + if (cluster.caFile) { + ca = fs.readFileSync(path.normalize(cluster.caFile)) + } else if (cluster.caData) { + ca = Buffer.from(cluster.caData, 'base64').toString() + } + insecureSkipTlsVerify = cluster.skipTLSVerify + } + + let cert + let key + + let auth = {} + if (user) { + if (user.certFile) { + cert = fs.readFileSync(path.normalize(user.certFile)) + } else if (user.certData) { + cert = Buffer.from(user.certData, 'base64').toString() + } + + if (user.keyFile) { + key = fs.readFileSync(path.normalize(user.keyFile)) + } else if (user.keyData) { + key = Buffer.from(user.keyData, 'base64').toString() + } + + if (user.token) { + auth.bearer = user.token + } + + if (user.authProvider) { + const config = user.authProvider.config + + // if we can't determine the type, just fail later (or don't refresh). + let type = null + let token = null + if (config['cmd-path']) { + type = 'cmd' + token = config['access-token'] + } else if (config['idp-issuer-url']) { + type = 'openid' + token = config['id-token'] + } + + // If we have just an access-token, allow that... will expire later though. + if (config['access-token'] && !type) { + token = config['access-token'] + } + + auth = { + request: { + bearer: token + }, + provider: { + config, + type + } + } + } + + if (user.exec) { + const env = {} + if (user.exec.env) { + user.exec.env.forEach(variable => { + env[variable.name] = variable.value + }) + } + let args = '' + if (user.exec.args) { + args = user.exec.args.join(' ') + } + auth = { + provider: { + type: 'cmd', + config: { + 'cmd-args': args, + 'cmd-path': user.exec.command, + 'token-key': 'status.token', + 'cmd-env': env + } + } + } + } + + if (user.username) auth.user = user.username + if (user.password) auth.pass = user.password + } + + return { + url: cluster.server, + auth: Object.keys(auth).length ? auth : null, + ca, + insecureSkipTlsVerify, + namespace, + cert, + key + } +} + +module.exports.convertKubeconfig = convertKubeconfig + // // Accept a manually specified current-context to take precedence over // `current-context` diff --git a/backends/request/config.test/deprecated.test.js b/backends/request/config.test/deprecated.test.js new file mode 100644 index 00000000..4701ea5c --- /dev/null +++ b/backends/request/config.test/deprecated.test.js @@ -0,0 +1,538 @@ +/* eslint no-process-env: 0 */ +/* eslint-env mocha */ +'use strict' + +const expect = require('chai').expect +const fs = require('fs') +const sinon = require('sinon') +const yaml = require('js-yaml') + +const config = require('../config') + +describe('Config (deprecated)', () => { + let sandbox + + beforeEach(() => { + sandbox = sinon.createSandbox() + }) + + afterEach(() => { + sandbox.restore() + }) + + describe('getInCluster', () => { + beforeEach(() => { + process.env.KUBERNETES_SERVICE_HOST = 'myhost' + process.env.KUBERNETES_SERVICE_PORT = 443 + }) + + it('should return with in-cluster config', () => { + const fsReadFileSync = sandbox.stub(fs, 'readFileSync') + + fsReadFileSync + .withArgs('/var/run/secrets/kubernetes.io/serviceaccount/ca.crt') + .returns('my-ca') + + fsReadFileSync + .withArgs('/var/run/secrets/kubernetes.io/serviceaccount/token') + .returns('my-token') + + fsReadFileSync + .withArgs('/var/run/secrets/kubernetes.io/serviceaccount/namespace') + .returns('my-namespace') + + const configInCluster = config.getInCluster() + expect(configInCluster).eqls({ + auth: { bearer: 'my-token' }, + ca: 'my-ca', + namespace: 'my-namespace', + url: 'https://myhost:443' + }) + }) + + afterEach(() => { + delete process.env.KUBERNETES_SERVICE_HOST + delete process.env.KUBERNETES_SERVICE_PORT + }) + }) + + describe('.loadKubeconfig', () => { + const cfgPaths = [ + './backends/request/config.test/fixtures/kube-fixture.yml', + './backends/request/config.test/fixtures/kube-fixture-two.yml' + ] + + it('supports multiple config files', () => { + const args = config.loadKubeconfig(cfgPaths) + expect(args.contexts[0].name).equals('foo-context-1') + expect(args.contexts[1].name).equals('foo-ramp-up') + }) + + it('supports multiple config files in KUBECONFIG', () => { + const delimiter = process.platform === 'win32' ? ';' : ':' + process.env.KUBECONFIG = cfgPaths.join(delimiter) + + const args = config.loadKubeconfig() + expect(args.contexts[0].name).equals('foo-context-1') + expect(args.contexts[1].name).equals('foo-ramp-up') + + delete process.env.KUBECONFIG + }) + }) + + describe('.fromKubeconfig', () => { + it('handles username and password', () => { + const kubeconfig = { + 'apiVersion': 'v1', + 'kind': 'Config', + 'preferences': {}, + 'current-context': 'foo-context', + 'contexts': [ + { + name: 'foo-context', + context: { + cluster: 'foo-cluster', + user: 'foo-user' + } + } + ], + 'clusters': [ + { + name: 'foo-cluster', + cluster: { + server: 'https://192.168.42.121:8443' + } + } + ], + 'users': [ + { + name: 'foo-user', + user: { + password: 'foo-password', + username: 'foo-user' + } + } + ] + } + const args = config.fromKubeconfig(kubeconfig) + expect(args.auth.user).equals('foo-user') + expect(args.auth.pass).equals('foo-password') + }) + + it('handles base64 encoded certs and keys', () => { + const kubeconfig = { + 'apiVersion': 'v1', + 'kind': 'Config', + 'preferences': {}, + 'current-context': 'foo-context', + 'contexts': [ + { + name: 'foo-context', + context: { + cluster: 'foo-cluster', + user: 'foo-user' + } + } + ], + 'clusters': [ + { + name: 'foo-cluster', + cluster: { + 'certificate-authority-data': Buffer.from('certificate-authority-data').toString('base64'), + 'server': 'https://192.168.42.121:8443' + } + } + ], + 'users': [ + { + name: 'foo-user', + user: { + 'client-certificate-data': Buffer.from('client-certificate').toString('base64'), + 'client-key-data': Buffer.from('client-key').toString('base64') + } + } + ] + } + const args = config.fromKubeconfig(kubeconfig) + expect(args.ca).equals('certificate-authority-data') + expect(args.key).equals('client-key') + expect(args.cert).equals('client-certificate') + }) + + it('handles relative and absolute certs and keys', () => { + const kubeconfig = { + 'apiVersion': 'v1', + 'kind': 'Config', + 'preferences': {}, + 'current-context': 'foo-context', + 'contexts': [ + { + name: 'foo-context', + context: { + cluster: 'foo-cluster', + user: 'foo-user' + } + } + ], + 'clusters': [ + { + name: 'foo-cluster', + cluster: { + 'certificate-authority': 'ca.pem', + 'server': 'https://192.168.42.121:8443' + } + } + ], + 'users': [ + { + name: 'foo-user', + user: { + 'client-certificate': '/absolute/path/client.cert', + 'client-key': 'subdir/client.key' + } + } + ] + } + + const fsReadFileSync = sandbox.stub(fs, 'readFileSync') + const yamlSafeLoad = sandbox.stub(yaml, 'safeLoad') + + fsReadFileSync + .withArgs(sinon.match(/config$/)) + .returns('mock-config') + + fsReadFileSync + .withArgs(sinon.match('/.kube/ca.pem')) + .returns('certificate-authority-data') + + fsReadFileSync + .withArgs(sinon.match('/.kube/subdir/client.key')) + .returns('client-key-data') + + fsReadFileSync + .withArgs('/absolute/path/client.cert') + .returns('client-certificate-data') + + yamlSafeLoad + .withArgs('mock-config') + .returns(kubeconfig) + + const args = config.fromKubeconfig() + expect(args.ca).equals('certificate-authority-data') + expect(args.key).equals('client-key-data') + expect(args.cert).equals('client-certificate-data') + }) + + it('handles token', () => { + const kubeconfig = { + 'apiVersion': 'v1', + 'kind': 'Config', + 'preferences': {}, + 'current-context': 'foo-context', + 'contexts': [ + { + name: 'foo-context', + context: { + cluster: 'foo-cluster', + user: 'foo-user' + } + } + ], + 'clusters': [ + { + name: 'foo-cluster', + cluster: { + server: 'https://192.168.42.121:8443' + } + } + ], + 'users': [ + { + name: 'foo-user', + user: { + token: 'foo-token' + } + } + ] + } + const args = config.fromKubeconfig(kubeconfig) + expect(args.auth.bearer).equals('foo-token') + }) + + it('handles auth-provider.config.access-token', () => { + const kubeconfig = { + 'apiVersion': 'v1', + 'kind': 'Config', + 'preferences': {}, + 'current-context': 'foo-context', + 'contexts': [ + { + name: 'foo-context', + context: { + cluster: 'foo-cluster', + user: 'foo-user' + } + } + ], + 'clusters': [ + { + name: 'foo-cluster', + cluster: { + server: 'https://192.168.42.121:8443' + } + } + ], + 'users': [ + { + name: 'foo-user', + user: { + 'auth-provider': { + config: { + 'access-token': 'foo-token' + } + } + } + } + ] + } + const args = config.fromKubeconfig(kubeconfig) + expect(args.auth.request.bearer).equals('foo-token') + }) + + it('handles auth-provider.config.idp-issuer-url', () => { + const kubeconfig = { + 'apiVersion': 'v1', + 'kind': 'Config', + 'preferences': {}, + 'current-context': 'foo-context', + 'contexts': [ + { + name: 'foo-context', + context: { + cluster: 'foo-cluster', + user: 'foo-user' + } + } + ], + 'clusters': [ + { + name: 'foo-cluster', + cluster: { + server: 'https://192.168.42.121:8443' + } + } + ], + 'users': [ + { + name: 'foo-user', + user: { + 'auth-provider': { + config: { + 'idp-issuer-url': 'https://accounts.google.com' + } + } + } + } + ] + } + const args = config.fromKubeconfig(kubeconfig) + expect(args.auth.provider.type).equals('openid') + }) + + it('handles auth-provider.config.cmd-path', () => { + const kubeconfig = { + 'apiVersion': 'v1', + 'kind': 'Config', + 'preferences': {}, + 'current-context': 'foo-context', + 'contexts': [ + { + name: 'foo-context', + context: { + cluster: 'foo-cluster', + user: 'foo-user' + } + } + ], + 'clusters': [ + { + name: 'foo-cluster', + cluster: { + server: 'https://192.168.42.121:8443' + } + } + ], + 'users': [ + { + name: 'foo-user', + user: { + 'auth-provider': { + config: { + 'cmd-path': 'gcloud', + 'cmd-args': 'config config-helper --format=json' + } + } + } + } + ] + } + const args = config.fromKubeconfig(kubeconfig) + expect(args.auth.provider.type).equals('cmd') + }) + + it('handles user.exec', () => { + const command = 'foo-command' + const cmdArgs = ['arg1', 'arg2'] + const envKey = 'foo-env-key' + const envValue = 'foo-env-value' + const kubeconfig = { + 'apiVersion': 'v1', + 'kind': 'Config', + 'preferences': {}, + 'current-context': 'foo-context', + 'contexts': [ + { + name: 'foo-context', + context: { + cluster: 'foo-cluster', + user: 'foo-user' + } + } + ], + 'clusters': [ + { + name: 'foo-cluster', + cluster: { + server: 'https://192.168.42.121:8443' + } + } + ], + 'users': [ + { + name: 'foo-user', + user: { + exec: { + command, + args: cmdArgs, + env: [{ + name: envKey, + value: envValue + }] + } + } + } + ] + } + const args = config.fromKubeconfig(kubeconfig) + expect(args.auth.provider.type).equals('cmd') + expect(args.auth.provider.config['cmd-args']).equals(cmdArgs.join(' ')) + expect(args.auth.provider.config['cmd-path']).equals(command) + expect(args.auth.provider.config['cmd-env']).deep.equals({ [envKey]: envValue }) + }) + + it('handles user.exec without optional env and args', () => { + const command = 'foo-command' + const kubeconfig = { + 'apiVersion': 'v1', + 'kind': 'Config', + 'preferences': {}, + 'current-context': 'foo-context', + 'contexts': [ + { + name: 'foo-context', + context: { + cluster: 'foo-cluster', + user: 'foo-user' + } + } + ], + 'clusters': [ + { + name: 'foo-cluster', + cluster: { + server: 'https://192.168.42.121:8443' + } + } + ], + 'users': [ + { + name: 'foo-user', + user: { + exec: { + command + } + } + } + ] + } + const args = config.fromKubeconfig(kubeconfig) + expect(args.auth.provider.type).equals('cmd') + expect(args.auth.provider.config['cmd-args']).equals('') + expect(args.auth.provider.config['cmd-path']).equals(command) + expect(args.auth.provider.config['cmd-env']).deep.equals({}) + }) + + it('handles manually specified current-context', () => { + const kubeconfig = { + 'apiVersion': 'v1', + 'kind': 'Config', + 'preferences': {}, + 'current-context': 'foo-context-1', + 'contexts': [ + { + name: 'foo-context-1', + context: { + cluster: 'foo-cluster-1', + user: 'foo-user' + } + }, + { + name: 'foo-context-2', + context: { + cluster: 'foo-cluster-2', + user: 'foo-user' + } + } + ], + 'clusters': [ + { + name: 'foo-cluster-1', + cluster: { + server: 'https://192.168.42.121:8443' + } + }, + { + name: 'foo-cluster-2', + cluster: { + server: 'https://192.168.42.122:8443' + } + } + ], + 'users': [ + { + name: 'foo-user', + user: { + token: 'foo-token' + } + } + ] + } + const args = config.fromKubeconfig(kubeconfig, 'foo-context-2') + expect(args.url).equals('https://192.168.42.122:8443') + }) + + it('load kubeconfig from provided path', () => { + const args = config.fromKubeconfig('./backends/request/config.test/fixtures/kube-fixture.yml') + expect(args.url).equals('https://192.168.42.121:8443') + }) + + it('load kubeconfig from provided array of paths', () => { + const cfgPaths = [ + './backends/request/config.test/fixtures/kube-fixture.yml', + './backends/request/config.test/fixtures/kube-fixture-two.yml' + ] + const args = config.fromKubeconfig(cfgPaths) + expect(args.url).equals('https://192.168.42.121:8443') + }) + }) +}) diff --git a/backends/request/config.test/index.test.js b/backends/request/config.test/index.test.js index 6c86e51f..5d69b433 100644 --- a/backends/request/config.test/index.test.js +++ b/backends/request/config.test/index.test.js @@ -4,6 +4,7 @@ const expect = require('chai').expect const fs = require('fs') +const k8s = require('@kubernetes/client-node') const sinon = require('sinon') const yaml = require('js-yaml') @@ -20,72 +21,12 @@ describe('Config', () => { sandbox.restore() }) - describe('getInCluster', () => { - beforeEach(() => { - process.env.KUBERNETES_SERVICE_HOST = 'myhost' - process.env.KUBERNETES_SERVICE_PORT = 443 - }) - - it('should return with in-cluster config', () => { - const fsReadFileSync = sandbox.stub(fs, 'readFileSync') - - fsReadFileSync - .withArgs('/var/run/secrets/kubernetes.io/serviceaccount/ca.crt') - .returns('my-ca') - - fsReadFileSync - .withArgs('/var/run/secrets/kubernetes.io/serviceaccount/token') - .returns('my-token') - - fsReadFileSync - .withArgs('/var/run/secrets/kubernetes.io/serviceaccount/namespace') - .returns('my-namespace') - - const configInCluster = config.getInCluster() - expect(configInCluster).eqls({ - auth: { bearer: 'my-token' }, - ca: 'my-ca', - namespace: 'my-namespace', - url: 'https://myhost:443' - }) - }) - - afterEach(() => { - delete process.env.KUBERNETES_SERVICE_HOST - delete process.env.KUBERNETES_SERVICE_PORT - }) - }) - - describe('.loadKubeconfig', () => { - const cfgPaths = [ - './backends/request/config.test/fixtures/kube-fixture.yml', - './backends/request/config.test/fixtures/kube-fixture-two.yml' - ] - - it('supports multiple config files', () => { - const args = config.loadKubeconfig(cfgPaths) - expect(args.contexts[0].name).equals('foo-context-1') - expect(args.contexts[1].name).equals('foo-ramp-up') - }) - - it('supports multiple config files in KUBECONFIG', () => { - const delimiter = process.platform === 'win32' ? ';' : ':' - process.env.KUBECONFIG = cfgPaths.join(delimiter) - - const args = config.loadKubeconfig() - expect(args.contexts[0].name).equals('foo-context-1') - expect(args.contexts[1].name).equals('foo-ramp-up') - - delete process.env.KUBECONFIG - }) - }) - - describe('.fromKubeconfig', () => { - it('handles username and password', () => { - const kubeconfig = { + describe('.convertKubeconfig', () => { + it('handles basic KubeConfig conversion', () => { + const kubeconfig = new k8s.KubeConfig() + kubeconfig.loadFromString(JSON.stringify({ 'apiVersion': 'v1', 'kind': 'Config', - 'preferences': {}, 'current-context': 'foo-context', 'contexts': [ { @@ -113,14 +54,16 @@ describe('Config', () => { } } ] - } - const args = config.fromKubeconfig(kubeconfig) - expect(args.auth.user).equals('foo-user') - expect(args.auth.pass).equals('foo-password') + })) + const covertedOptions = config.convertKubeconfig(kubeconfig) + expect(covertedOptions.url).to.equal('https://192.168.42.121:8443') + expect(covertedOptions.auth.user).to.equal('foo-user') + expect(covertedOptions.auth.pass).to.equal('foo-password') }) it('handles base64 encoded certs and keys', () => { - const kubeconfig = { + const kubeconfig = new k8s.KubeConfig() + kubeconfig.loadFromString(JSON.stringify({ 'apiVersion': 'v1', 'kind': 'Config', 'preferences': {}, @@ -152,15 +95,16 @@ describe('Config', () => { } } ] - } - const args = config.fromKubeconfig(kubeconfig) + })) + const args = config.convertKubeconfig(kubeconfig) expect(args.ca).equals('certificate-authority-data') expect(args.key).equals('client-key') expect(args.cert).equals('client-certificate') }) it('handles relative and absolute certs and keys', () => { - const kubeconfig = { + const kubeconfig = new k8s.KubeConfig() + kubeconfig.loadFromString(JSON.stringify({ 'apiVersion': 'v1', 'kind': 'Config', 'preferences': {}, @@ -192,7 +136,8 @@ describe('Config', () => { } } ] - } + })) + kubeconfig.makePathsAbsolute('/.kube') const fsReadFileSync = sandbox.stub(fs, 'readFileSync') const yamlSafeLoad = sandbox.stub(yaml, 'safeLoad') @@ -217,14 +162,15 @@ describe('Config', () => { .withArgs('mock-config') .returns(kubeconfig) - const args = config.fromKubeconfig() + const args = config.convertKubeconfig(kubeconfig) expect(args.ca).equals('certificate-authority-data') expect(args.key).equals('client-key-data') expect(args.cert).equals('client-certificate-data') }) it('handles token', () => { - const kubeconfig = { + const kubeconfig = new k8s.KubeConfig() + kubeconfig.loadFromString(JSON.stringify({ 'apiVersion': 'v1', 'kind': 'Config', 'preferences': {}, @@ -254,13 +200,14 @@ describe('Config', () => { } } ] - } - const args = config.fromKubeconfig(kubeconfig) + })) + const args = config.convertKubeconfig(kubeconfig) expect(args.auth.bearer).equals('foo-token') }) it('handles auth-provider.config.access-token', () => { - const kubeconfig = { + const kubeconfig = new k8s.KubeConfig() + kubeconfig.loadFromString(JSON.stringify({ 'apiVersion': 'v1', 'kind': 'Config', 'preferences': {}, @@ -294,13 +241,14 @@ describe('Config', () => { } } ] - } - const args = config.fromKubeconfig(kubeconfig) + })) + const args = config.convertKubeconfig(kubeconfig) expect(args.auth.request.bearer).equals('foo-token') }) it('handles auth-provider.config.idp-issuer-url', () => { - const kubeconfig = { + const kubeconfig = new k8s.KubeConfig() + kubeconfig.loadFromString(JSON.stringify({ 'apiVersion': 'v1', 'kind': 'Config', 'preferences': {}, @@ -334,13 +282,14 @@ describe('Config', () => { } } ] - } - const args = config.fromKubeconfig(kubeconfig) + })) + const args = config.convertKubeconfig(kubeconfig) expect(args.auth.provider.type).equals('openid') }) it('handles auth-provider.config.cmd-path', () => { - const kubeconfig = { + const kubeconfig = new k8s.KubeConfig() + kubeconfig.loadFromString(JSON.stringify({ 'apiVersion': 'v1', 'kind': 'Config', 'preferences': {}, @@ -375,8 +324,8 @@ describe('Config', () => { } } ] - } - const args = config.fromKubeconfig(kubeconfig) + })) + const args = config.convertKubeconfig(kubeconfig) expect(args.auth.provider.type).equals('cmd') }) @@ -385,7 +334,8 @@ describe('Config', () => { const cmdArgs = ['arg1', 'arg2'] const envKey = 'foo-env-key' const envValue = 'foo-env-value' - const kubeconfig = { + const kubeconfig = new k8s.KubeConfig() + kubeconfig.loadFromString(JSON.stringify({ 'apiVersion': 'v1', 'kind': 'Config', 'preferences': {}, @@ -422,8 +372,8 @@ describe('Config', () => { } } ] - } - const args = config.fromKubeconfig(kubeconfig) + })) + const args = config.convertKubeconfig(kubeconfig) expect(args.auth.provider.type).equals('cmd') expect(args.auth.provider.config['cmd-args']).equals(cmdArgs.join(' ')) expect(args.auth.provider.config['cmd-path']).equals(command) @@ -432,7 +382,8 @@ describe('Config', () => { it('handles user.exec without optional env and args', () => { const command = 'foo-command' - const kubeconfig = { + const kubeconfig = new k8s.KubeConfig() + kubeconfig.loadFromString(JSON.stringify({ 'apiVersion': 'v1', 'kind': 'Config', 'preferences': {}, @@ -464,8 +415,8 @@ describe('Config', () => { } } ] - } - const args = config.fromKubeconfig(kubeconfig) + })) + const args = config.convertKubeconfig(kubeconfig) expect(args.auth.provider.type).equals('cmd') expect(args.auth.provider.config['cmd-args']).equals('') expect(args.auth.provider.config['cmd-path']).equals(command) @@ -473,7 +424,8 @@ describe('Config', () => { }) it('handles manually specified current-context', () => { - const kubeconfig = { + const kubeconfig = new k8s.KubeConfig() + kubeconfig.loadFromString(JSON.stringify({ 'apiVersion': 'v1', 'kind': 'Config', 'preferences': {}, @@ -516,23 +468,10 @@ describe('Config', () => { } } ] - } - const args = config.fromKubeconfig(kubeconfig, 'foo-context-2') + })) + kubeconfig.setCurrentContext('foo-context-2') + const args = config.convertKubeconfig(kubeconfig) expect(args.url).equals('https://192.168.42.122:8443') }) - - it('load kubeconfig from provided path', () => { - const args = config.fromKubeconfig('./backends/request/config.test/fixtures/kube-fixture.yml') - expect(args.url).equals('https://192.168.42.121:8443') - }) - - it('load kubeconfig from provided array of paths', () => { - const cfgPaths = [ - './backends/request/config.test/fixtures/kube-fixture.yml', - './backends/request/config.test/fixtures/kube-fixture-two.yml' - ] - const args = config.fromKubeconfig(cfgPaths) - expect(args.url).equals('https://192.168.42.121:8443') - }) }) }) diff --git a/examples/basic.js b/examples/basic.js index 5e9a0050..eda72909 100644 --- a/examples/basic.js +++ b/examples/basic.js @@ -3,13 +3,12 @@ // Demonstrate some of the basics. // const Client = require('kubernetes-client').Client -const config = require('kubernetes-client').config const deploymentManifest = require('./nginx-deployment.json') async function main () { try { - const client = new Client({ config: config.fromKubeconfig(), version: '1.9' }) + const client = new Client({ version: '1.9' }) // // Get all the Namespaces. diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 00000000..aed48229 --- /dev/null +++ b/lib/config.js @@ -0,0 +1,3 @@ +const k8s = require('@kubernetes/client-node') + +module.exports = k8s.KubeConfig diff --git a/lib/index.js b/lib/index.js index 4fbcf70e..8980cb26 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,7 +4,8 @@ module.exports = { Client: require('./swagger-client').Client, Client1_13: require('./swagger-client').Client1_13, alias: require('./alias'), - config: require('../backends/request/config') + config: require('../backends/request/config'), + KubeConfig: require('./config') } deprecate.property( diff --git a/lib/swagger-client.js b/lib/swagger-client.js index 67b73d03..35b32100 100644 --- a/lib/swagger-client.js +++ b/lib/swagger-client.js @@ -26,6 +26,7 @@ const Component = require('swagger-fluent').Component const deprecate = require('depd')('kubernetes-client') const fs = require('fs') +const KubeConfig = require('./config') const path = require('path') const zlib = require('zlib') @@ -197,7 +198,9 @@ class Client { if (options.config) { backend = new Request(options.config) } else { - backend = new Request(Request.config.fromKubeconfig()) + const kubeconfig = new KubeConfig() + kubeconfig.loadFromDefault() + backend = new Request({ kubeconfig }) } } diff --git a/lib/swagger-client.test.js b/lib/swagger-client.test.js index 8825ec89..28159458 100644 --- a/lib/swagger-client.test.js +++ b/lib/swagger-client.test.js @@ -6,10 +6,14 @@ const expect = require('chai').expect const nock = require('nock') const Client = require('./swagger-client').Client +const KubeConfig = require('./config') const Request = require('../backends/request') const url = 'http://mock.kube.api' -const config = { url } +const kubeconfig = new KubeConfig() +kubeconfig.loadFromClusterAndUser( + { name: 'cluster', server: url }, + { name: 'user' }) describe('lib.swagger-client', () => { describe('.Client', () => { @@ -30,7 +34,7 @@ describe('lib.swagger-client', () => { }) it('creates a dynamically generated client', done => { - const backend = new Request(config) + const backend = new Request({ kubeconfig }) const client = new Client({ backend }) client.loadSpec() .then(() => { @@ -61,7 +65,7 @@ describe('lib.swagger-client', () => { }) it('creates a dynamically generated client', (done) => { - const backend = new Request(config) + const backend = new Request({ kubeconfig }) const client = new Client({ backend }) client.loadSpec() .then(() => { @@ -92,7 +96,7 @@ describe('lib.swagger-client', () => { }) it('returns an error message with the status code', (done) => { - const backend = new Request(config) + const backend = new Request({ kubeconfig }) const client = new Client({ backend }) client.loadSpec() .then(() => { @@ -126,7 +130,7 @@ describe('lib.swagger-client', () => { }) it('returns an error message with the status code', (done) => { - const backend = new Request(config) + const backend = new Request({ kubeconfig }) const client = new Client({ backend }) client.loadSpec() .then(() => { @@ -147,7 +151,7 @@ describe('lib.swagger-client', () => { .get('/api/v1/namespaces/foo/pods/bar/log') .reply(200, 'hello') - const backend = new Request(config) + const backend = new Request({ kubeconfig }) const client = new Client({ backend, version: '1.9' }) const stream = await client.api.v1.namespaces('foo').pods('bar').log.getByteStream() return new Promise((resolve, reject) => { @@ -171,7 +175,7 @@ describe('lib.swagger-client', () => { type: 'ADDED' }) - const backend = new Request(config) + const backend = new Request({ kubeconfig }) const client = new Client({ backend, version: '1.9' }) const stream = await client.api.v1.watch.namespaces.getObjectStream() return new Promise((resolve, reject) => { @@ -189,7 +193,7 @@ describe('lib.swagger-client', () => { describe('.constructor', () => { it('creates a dynamically generated client synchronously based on version', () => { - const backend = new Request(config) + const backend = new Request({ kubeconfig }) const client = new Client({ backend, version: '1.9' }) expect(client.api.get).is.a('function') }) diff --git a/merging-with-kubernetes.md b/merging-with-kubernetes.md index ec255423..f56cc93d 100644 --- a/merging-with-kubernetes.md +++ b/merging-with-kubernetes.md @@ -70,6 +70,33 @@ const stream = await client.api.v1.namespaces(namespace).pods(manifest.metadata. We are going to remove support for streaming other endpoints. +### `Request({ kubeconfig })` + +You must construct a `Request` backend with a +[`@kubernetes/client-node` `KubeConfig`](https://github.com/kubernetes-client/javascript) object. + +```js +// Deprecated loading from kubeconfig file +const Request = require('kubernetes-client/backends/request') +const requestOptions = Request.config.fromKubeconfig(Request.config.loadKubeconfig()) +const backend = new Request(requestOptions) + +// New version of loading from kubeconfig file +const { KubeConfig } = require('kubernetes-client') +const kubeconfig = new KubeConfig() +kubeconfig.loadFromDefault() +const backend = new Request({ kubeconfig }) + +// Deprecated loading from in-cluster config +const requestOptions = Request.config.fromKubeconfig(Request.config.getInCluster()) +const backend = new Request(requestOptions) + +// New reversion of loading from in-cluster config +const kubeconfig = new KubeConfig() +kubeconfig.loadFromCluster() +const backend = new Request({ kubeconfig }) +``` + ## Why are you doing this? Because it will improve the quality of both JavaScript clients and diff --git a/test-integration/env.js b/test-integration/env.js index 0e676d7d..2af2b947 100644 --- a/test-integration/env.js +++ b/test-integration/env.js @@ -3,10 +3,7 @@ 'use strict' const k8s = require('@kubernetes/client-node') - const Client = require('../').Client -const Request = require('../backends/request') - const ClientNodeBackend = require('../backends/kubernetes-client-node') async function getClient () { @@ -17,8 +14,7 @@ async function getClient () { const client = new Client({ backend, version: '1.13' }) return client } else { - const backend = new Request(Request.config.fromKubeconfig()) - const client = new Client({ backend }) + const client = new Client({}) await client.loadSpec() return client }