Skip to content

Commit

Permalink
feat(Promise API): Experimental support for a promise-based API (#133)
Browse files Browse the repository at this point in the history
This preserves the original callback-based "core" implementation and layers the
promised-based API on top of that core implementation. The goal is start using
and iterating on the promised-based API even if we expect the underlying
implementation to evolve (e.g., we could consider replacing the core
callback-based implementation with a promise one).  See
#37 for discussion.

Example usage:

```js
const core = new api.Core({
  url: 'http://my-k8s-api-server.com',
  promises: true
});

async function main() {
  try {
    const pods = await core.ns('kube-system').po.get();
    console.log(`Watching: ${ JSON.stringify(pods, null, 2) }`);
    const stream = core.ns('kube-system').po.getStream({ qs: { watch: true }});
    stream.on('data', data => { console.log(data.toString()); });
    stream.on('error', err => { throw err; });
    stream.on('end', () => { console.log('end'); });
  } catch (error) {
    console.log(error);
  }
}
main();
```
  • Loading branch information
silasbw committed Sep 28, 2017
1 parent 3ff438f commit b5f5e10
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 18 deletions.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,43 @@ const Api = require('kubernetes-client');
const core = new Api.Core(Api.config.fromKubeconfig());
```

### **Experimental** support for promises and async/await

kubernetes-client exposes **experimental** support for promises via
the `promises` option passed to API group constructors. The API is the
same, except for the functions that previously took a callback
(*e.g.*, `.get`). Those functions now return a promise.

```js
// Notice the promises: true
const core = new Api.Core({
url: 'http://my-k8s-api-server.com',
version: 'v1', // Defaults to 'v1'
promises: true, // Enable promises
namespace: 'my-project' // Defaults to 'default'
});
```

and then:

```js
core.namespaces.replicationcontrollers('http-rc').get()
.then(result => print(null, result));
```

or with `async/await`:

```js
print(null, await core.namespaces.replicationcontrollers('http-rc').get());
```

You can invoke promise-based and callback-based functions explictly:

```js
print(null, await core.namespaces.replicationcontrollers('http-rc').getPromise());
core.namespaces.replicationcontrollers('http-rc').getCb(print);
```

### Creating and updating

kubernetes-client objects expose `.post`, `.patch`, and `.put`
Expand Down
4 changes: 4 additions & 0 deletions lib/api-group.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ class ApiGroup {
this.requestOptions.auth = options.auth;
}

this.resourceConfig = {
promises: options.promises
};

//
// Create the default namespace so we have it directly on the API
//
Expand Down
22 changes: 16 additions & 6 deletions lib/base.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict';

const promisify = require('util.promisify');

const matchExpression = require('./match-expression');

function cb200(cb) {
Expand Down Expand Up @@ -71,14 +73,22 @@ class BaseObject extends CallableObject {
this.qs = options.qs || {};

this.path = path;

const apiFunctions = ['delete', 'get', 'patch', 'post', 'put'];
apiFunctions.forEach(func => {
this[`${ func }Promise`] = promisify(this[`_${ func }`].bind(this));
this[`${ func }Cb`] = this[`_${ func }`].bind(this);
if (this.api.resourceConfig.promises) this[func] = this[`${ func }Promise`];
else this[func] = this[`${ func }Cb`];
});
}

/**
* Delete a Kubernetes resource
* @param {RequestOptions|string} options - DELETE options, or resource name
* @param {callback} cb - The callback that handles the response
*/
delete(options, cb) {
_delete(options, cb) {
if (typeof options === 'function') {
cb = options;
options = {};
Expand All @@ -99,7 +109,7 @@ class BaseObject extends CallableObject {
* @param {callback} cb - The callback that handles the response
* @returns {Stream} If cb is falsy, return a stream
*/
get(options, cb) {
_get(options, cb) {
if (typeof options === 'function') {
cb = options;
options = {};
Expand All @@ -121,14 +131,14 @@ class BaseObject extends CallableObject {
* @param {RequestOptions|string} options - GET options, or resource name
* @returns {Stream} Result stream
*/
getStream(options) { return this.get(options); }
getStream(options) { return this._get(options); }

/**
* Patch a Kubernetes resource
* @param {RequestOptions} options - PATCH options
* @param {callback} cb - The callback that handle the response
*/
patch(options, cb) {
_patch(options, cb) {
const patchOptions = {
path: this._path(options),
body: options.body
Expand All @@ -142,7 +152,7 @@ class BaseObject extends CallableObject {
* @param {RequestOptions} options - POST options
* @param {callback} cb - The callback that handle the response
*/
post(options, cb) {
_post(options, cb) {
this.api.post({ path: this._path(options), body: options.body },
cb200(cb));
}
Expand All @@ -152,7 +162,7 @@ class BaseObject extends CallableObject {
* @param {RequestOptions} options - PUT options
* @param {callback} cb - The callback that handle the response
*/
put(options, cb) {
_put(options, cb) {
this.api.put({ path: this._path(options), body: options.body }, cb200(cb));
}

Expand Down
9 changes: 5 additions & 4 deletions lib/replicationcontrollers.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,20 @@ class ReplicationControllerPods extends BaseObject {
* @param {RequestOptions|string} options - GET options, or resource name
* @param {callback} cb - The callback that handles the response
*/
get(options, cb) {
_get(options, cb) {
if (arguments.length < 2) {
throw new Error(
'GETing ReplicationController Pods requires options and cb arguments. ' +
'Use api.namsepaces.pods.get if you want to get all Pods in a Namespace.'
);
}
this.rc.get(options, (rcErr, rc) => {

this.rc._get(options, (rcErr, rc) => {
if (rcErr) return cb(rcErr);

const selector = Object.keys(rc.spec.selector).map(
key => `${ key }=${ rc.spec.selector[key] }`).join(',');
super.get({
super._get({
path: this.path,
qs: { labelSelector: selector }
}, (podsErr, pods) => {
Expand Down Expand Up @@ -80,7 +81,7 @@ class ReplicationControllers extends BaseObject {
* @param {boolean} options.preservePods - If true, do not delete the Pods
* @param {callback} cb - The callback that handles the response
*/
delete(options, cb) {
_delete(options, cb) {
if (typeof options === 'string') {
options = { name: options };
}
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"assign-deep": "^0.4.5",
"async": "^2.4.1",
"js-yaml": "^3.7.0",
"request": "^2.79.0"
"request": "^2.79.0",
"util.promisify": "^1.0.0"
},
"devDependencies": {
"assume": "^1.4.1",
Expand Down
3 changes: 3 additions & 0 deletions test/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ function injectApis(options) {
apiGroup: { Constructor: Api },
apps: { Constructor: Apps },
batch: { Constructor: Batch },
core: { Constructor: Core },
extensions: { Constructor: Extensions },
rbac: { Constructor: Rbac },
thirdPartyResources: {
Expand All @@ -80,6 +81,8 @@ function injectApis(options) {
Object.keys(apis).forEach(apiName => {
const api = apis[apiName];
module.exports[apiName] = new (api.Constructor)(Object.assign({}, options, api.options));
module.exports[`${ apiName }Promise`] =
new (api.Constructor)(Object.assign({ promises: true }, options, api.options));
});
}

Expand Down
9 changes: 5 additions & 4 deletions test/container-base-object.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,27 @@
const assume = require('assume');

const ContainerBaseObject = require('../lib/container-base-object');
const api = { resourceConfig: {}};

describe('lib.container-base-object', () => {
describe('.ContainerBaseObject', () => {
it('adds resources specified in the constructor', () => {
const fake = new ContainerBaseObject({ resources: ['foo'] });
const fake = new ContainerBaseObject({ api, resources: ['foo'] });
assume(fake.foo).is.a('function');
});

it('throws an error if missing resource name', () => {
const fn = () => new ContainerBaseObject({ resources: [{ Constructor: 'fake' }] });
const fn = () => new ContainerBaseObject({ api, resources: [{ Constructor: 'fake' }] });
assume(fn).throws();
});

it('throws an error if missing resource Constructor', () => {
const fn = () => new ContainerBaseObject({ resources: [{ name: 'fake' }] });
const fn = () => new ContainerBaseObject({ api, resources: [{ name: 'fake' }] });
assume(fn).throws();
});

it('throws an error for adding the resource', () => {
const fake = new ContainerBaseObject({ resources: ['foo'] });
const fake = new ContainerBaseObject({ api, resources: ['foo'] });
const fn = () => fake.addResource('foo');
assume(fn).throws();
});
Expand Down
4 changes: 2 additions & 2 deletions test/namespaces.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ const assume = require('assume');

const Namespaces = require('../lib/namespaces');

const api = { resourceConfig: {}};

describe('lib.namespaces', () => {
describe('.addResource', () => {
it('adds a new resource object', () => {
const api = {};
const parentPath = '/apis/foo.com/v1';
const namespace = 'notdefault';
const namespaces = new Namespaces({ api, parentPath, namespace, resources: [] });
Expand All @@ -19,7 +20,6 @@ describe('lib.namespaces', () => {
});

it('ensures named namespaces inherit resources', () => {
const api = {};
const parentPath = '/apis/foo.com/v1';
const namespace = 'notdefault';
const namespaces = new Namespaces({ api, parentPath, namespace, resources: [] });
Expand Down
2 changes: 1 addition & 1 deletion test/objects.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@ describe('objects', function () {
only('unit', 'GETs PodList', function (done) {
nock200();
rcs().po.get({ name: 'foo' }, (err, results) => {
assume(err).is.falsy();
const rc = results.rc;
const podList = results.podList;
assume(err).is.falsy();
assume(rc.kind).is.equal('replicationcontroller');
assume(podList.kind).is.equal('podlist');
done();
Expand Down
41 changes: 41 additions & 0 deletions test/promise.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict';

const assume = require('assume');
const nock = require('nock');

const common = require('./common');
const only = common.only;
const beforeTestingEach = common.beforeTestingEach;

describe('lib.promise', () => {
describe('Core', () => {
beforeTestingEach('unit', () => {
nock(common.api.url)
.get(`/api/v1/namespaces/${ common.currentName }/pods/test-pod`)
.reply(200, {
kind: 'Pod',
metadata: { name: 'test-pod' }
});
});

only('unit', '.get returns a Pod via a promise', done => {
const pods = common.corePromise.ns.po('test-pod').get();
pods.then(object => {
assume(object.kind).is.equal('Pod');
assume(object.metadata.name).is.equal('test-pod');
done();
});
});
only('unit', '.getStream returns the Pod via a stream', done => {
const stream = common.corePromise.ns.po('test-pod').getStream();
const pieces = [];
stream.on('data', data => pieces.push(data.toString()));
stream.on('error', err => assume(err).is.falsy());
stream.on('end', () => {
const object = JSON.parse(pieces.join(''));
assume(object.kind).is.equal('Pod');
done();
});
});
});
});

0 comments on commit b5f5e10

Please sign in to comment.