Skip to content

Commit

Permalink
feat(backends): @kubernetes/client-node (#394)
Browse files Browse the repository at this point in the history
Experimental support for a @kubernetes/client-node based backend.
  • Loading branch information
Silas Boyd-Wickizer committed May 19, 2019
1 parent ec6cca3 commit 7faef3e
Show file tree
Hide file tree
Showing 5 changed files with 546 additions and 33 deletions.
82 changes: 82 additions & 0 deletions examples/kubernetes-client-node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* eslint no-console:0 */
//
// Demonstrate how to use @kubernetes/client-node as a backend.
//
const k8s = require('@kubernetes/client-node')

const Client = require('..').Client
const ClientNodeBackend = require('../lib/backends/kubernetes-client-node')

const deploymentManifest = require('./nginx-deployment.json')

async function main () {
try {
const kubeconfig = new k8s.KubeConfig()
kubeconfig.loadFromDefault()

const backend = new ClientNodeBackend({ client: k8s, kubeconfig })
const client = new Client({ backend, version: '1.10' })

//
// Get all the Namespaces.
//
const namespaces = (await client.api.v1.namespaces.get()).body.items.map(namespace => ({
name: namespace.metadata.name,
status: namespace.status
}))
console.log('Namespaces:', JSON.stringify(namespaces, null, 2))

//
// Create a new Deployment.
//
const create = await client.apis.apps.v1.namespaces('default').deployments.post({ body: deploymentManifest })
console.log('Create:', create.body)

//
// Fetch the Deployment we just created.
//
const deployment = await client.apis.apps.v1.namespaces('default').deployments(deploymentManifest.metadata.name).get()
console.log('Deployment: ', deployment.body)

//
// Change the Deployment Replica count to 10
//

const replica = {
spec: {
replicas: 10
}
}

const replicaModify = await client.apis.apps.v1.namespaces('default').deployments(deploymentManifest.metadata.name).patch({ body: replica })
console.log('Replica Modification: ', replicaModify)

//
// Modify the image tag
//
const newImage = {
spec: {
template: {
spec: {
containers: [{
name: 'nginx',
image: 'nginx:1.8.1'
}]
}
}
}
}
const imageSet = await client.apis.apps.v1.namespaces('default').deployments(deploymentManifest.metadata.name).patch({ body: newImage })
console.log('New Image: ', imageSet)

//
// Remove the Deployment we created.
//
const removed = await client.apis.apps.v1.namespaces('default').deployments(deploymentManifest.metadata.name).delete()
console.log('Removed: ', removed)
} catch (err) {
console.error('Error:', err.message)
}
}

main()
93 changes: 93 additions & 0 deletions lib/backends/kubernetes-client-node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use strict'

const pascalcase = require('pascalcase')

//
// https://github.com/kubernetes-client/javascript
//
class ClientNodeBackend {
constructor (options) {
this.client = options.client
this.kubeconfig = options.kubeconfig
this.apiClients = { }
}

_getApiClient (tag) {
//
// API type is a snake_case CamelCase amalgamation. E.g., Core_v1Api
//
const apiType = tag.charAt(0).toUpperCase() + tag.slice(1) + 'Api'
if (!(apiType in this.apiClients)) {
this.apiClients[apiType] = this.kubeconfig.makeApiClient(this.client[apiType])
}
return this.apiClients[apiType]
}

http (options) {
const pathItemObject = options.pathItemObject
const operationObject = pathItemObject[options.method.toLowerCase()]
const tag = operationObject.tags[0]

const apiClient = this._getApiClient(tag)

//
// In older Kubernetes API OpenAPI specifications the Operation IDs include
// the tag, but in newer versions (including the ones used to generate
// @kubernetes/client-node), the tag is absent.
//
// Support older versions of the Swagger specifications by removing the tag
// part.
//
const method = operationObject.operationId.replace(pascalcase(tag), '')

//
// @kubernetes/client-node methods take parameters in the order the OpenAPI
// specification declares them.
//
const parameterObjects = (pathItemObject.parameters || []).concat(operationObject.parameters || [])
const orderedParameterObjects = parameterObjects
.filter(parameterObject => parameterObject.required)
.concat(parameterObjects
.filter(parameterObject => !parameterObject.required))

//
// Older versions of the Kubernetes API OpenAPI specifications requires body
// for _some_ delete operations (e.g., deleteNamespacedDeployment). The API
// does not actually require it and newer specifications remove the
// requirement. Try to Workaround this issue by adding an empty body to
// @kubernetes/client-node calls.
//
let body = options.body
if (options.method.toLowerCase() === 'delete' && !body) {
body = {}
}

const parameters = Object.assign(
{ body },
options.pathnameParameters,
options.qs,
options.parameters)
const args = orderedParameterObjects.reduce((acc, operationParameter) => {
const name = operationParameter.name
if (name in parameters) acc.push(parameters[name])
else acc.push(undefined)
return acc
}, [])

return apiClient[method].apply(apiClient, args)
.then(res => {
res.statusCode = res.response.statusCode
return res
})
.catch(err => {
if (!err.body) throw err
const error = new Error(err.body.message)
// .code is backwards compatible with pre-5.0.0 code.
error.code = err.response.statusCode
error.statusCode = err.response.statusCode
throw error
})
}
}

module.exports = ClientNodeBackend
61 changes: 61 additions & 0 deletions lib/backends/kubernetes-client-node.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use strict'
/* eslint-env mocha */

const { expect } = require('chai')
const sinon = require('sinon')

const ClientNodeBackend = require('./kubernetes-client-node')

describe('lib.backends.kubernetes-client-node', () => {
it('calls the expected method', done => {
const fakeK8sClient = { Core_v1Api: 'foo' }
const fakeKubeconfig = {
makeApiClient: sinon.spy(() => {
return {
getStuff: (bar, foo) => {
return new Promise((resolve, reject) => {
expect(bar).to.equal('bar')
expect(foo).to.equal('foo')
resolve({
response: {
statusCode: 200
}
})
})
}
}
})
}

const options = {
method: 'GET',
pathnameParameters: {
foo: 'foo'
},
parameters: {
bar: 'bar'
},
pathItemObject: {
get: {
operationId: 'getStuff',
parameters: [{
name: 'foo'
}],
tags: ['core_v1']
},
parameters: [{
name: 'bar'
}]
}
}

const client = new ClientNodeBackend({
client: fakeK8sClient,
kubeconfig: fakeKubeconfig
})

client.http(options)
.then(res => done())
.catch(done)
})
})
Loading

0 comments on commit 7faef3e

Please sign in to comment.