diff --git a/lib/api-extensions.js b/lib/api-extensions.js new file mode 100644 index 00000000..5c04d73f --- /dev/null +++ b/lib/api-extensions.js @@ -0,0 +1,22 @@ +'use strict'; + +const ApiGroup = require('./api-group'); +const CustomResourceDefiniton = require('./custom-resource-definition'); + +class ApiExtensions extends ApiGroup { + constructor(options) { + options = Object.assign({}, options, { + path: 'apis/apiextensions.k8s.io', + version: options.version || 'v1beta1', + groupResources: [ + 'customresourcedefinitions' + ], + namespaceResources: [ + { name: 'customresourcedefinitions', Constructor: CustomResourceDefiniton } + ] + }); + super(options); + } +} + +module.exports = ApiExtensions; diff --git a/lib/api.js b/lib/api.js index 393c1b5c..d203d912 100644 --- a/lib/api.js +++ b/lib/api.js @@ -5,12 +5,14 @@ const Extensions = require('./extensions'); const Apps = require('./apps'); const Batch = require('./batch'); const Rbac = require('./rbac'); +const ApiExtensions = require('./api-extensions'); const groups = { 'extensions': Extensions, 'apps': Apps, 'batch': Batch, - 'rbac.authorization.k8s.io': Rbac + 'rbac.authorization.k8s.io': Rbac, + 'apiextensions.k8s.io': ApiExtensions }; class Api { @@ -22,6 +24,7 @@ class Api { * @param {object} options.apps - Optional default Apps client * @param {object} options.batch - Optional default Batch client * @param {object} options.rbac - Optional default RBAC client + * @param {object} options.apiExtensions - Optional default ApiExtensions client */ constructor(options) { this.options = options; @@ -30,6 +33,7 @@ class Api { this.apps = options.apps || new Apps(options); this.batch = options.batch || new Batch(options); this.rbac = options.rbac || new Rbac(options); + this.apiExtensions = options.apiExtensions || new ApiExtensions(options); } /** diff --git a/lib/common.js b/lib/common.js index 98dc19fb..a9abc0b9 100644 --- a/lib/common.js +++ b/lib/common.js @@ -10,6 +10,7 @@ module.exports.aliasResources = function (resourceObject) { componentstatuses: ['cs'], configmaps: ['cm'], cronjobs: [], + customresourcedefinitions: ['crd'], daemonsets: ['ds'], deployments: ['deploy'], events: ['ev'], @@ -36,6 +37,7 @@ module.exports.aliasResources = function (resourceObject) { serviceaccounts: [], services: ['svc'], statefulsets: [], + // Deprecated name of customresourcedefinition in kubernetes 1.7 thirdpartyresources: [] }; diff --git a/lib/custom-resource-definition.js b/lib/custom-resource-definition.js new file mode 100644 index 00000000..449cfa1d --- /dev/null +++ b/lib/custom-resource-definition.js @@ -0,0 +1,27 @@ +'use strict'; + +const ApiGroup = require('./api-group'); + +class CustomResourceDefinition extends ApiGroup { + constructor(options) { + options = Object.assign({}, options, { + path: `apis/${ options.group }`, + version: options.version || 'v1', + groupResources: [], + namespaceResources: [] + }); + super(options); + + if (options.resources) { + options.resources.forEach(resource => this.addResource(resource)); + } + } + + addResource(resourceName) { + this.namespace.addResource(resourceName); + super.addResource(resourceName); + return this; + } +} + +module.exports = CustomResourceDefinition; diff --git a/lib/index.js b/lib/index.js index c315e8b2..4afb3d79 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,6 +5,7 @@ module.exports.Api = require('./api'); module.exports.Core = core; module.exports.Extensions = require('./extensions'); module.exports.ThirdPartyResources = require('./third-party-resources'); +module.exports.CustomResourceDefinitions = require('./custom-resource-definitions'); module.exports.config = require('./config'); module.exports.Apps = require('./apps'); module.exports.Batch = require('./batch'); diff --git a/package-lock.json b/package-lock.json index 3a1c32a4..1184802c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -768,6 +768,15 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "define-properties": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", + "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", + "requires": { + "foreach": "2.0.5", + "object-keys": "1.0.11" + } + }, "del": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", @@ -886,6 +895,28 @@ "is-arrayish": "0.2.1" } }, + "es-abstract": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.8.2.tgz", + "integrity": "sha512-dvhwFL3yjQxNNsOWx6exMlaDrRHCRGMQlnx5lsXDCZ/J7G/frgIIl94zhZSp/galVAYp7VzPi1OrAHta89/yGQ==", + "requires": { + "es-to-primitive": "1.1.1", + "function-bind": "1.1.1", + "has": "1.0.1", + "is-callable": "1.1.3", + "is-regex": "1.0.4" + } + }, + "es-to-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", + "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", + "requires": { + "is-callable": "1.1.3", + "is-date-object": "1.0.1", + "is-symbol": "1.0.1" + } + }, "es5-ext": { "version": "0.10.23", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.23.tgz", @@ -1226,6 +1257,11 @@ "integrity": "sha1-gBWtFJwQEaEWzbieukzBHZA5rdg=", "dev": true }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -1265,6 +1301,11 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, "generate-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", @@ -1467,6 +1508,14 @@ "har-schema": "1.0.5" } }, + "has": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", + "requires": { + "function-bind": "1.1.1" + } + }, "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", @@ -1649,6 +1698,16 @@ "builtin-modules": "1.1.1" } }, + "is-callable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", + "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=" + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" + }, "is-finite": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", @@ -1720,6 +1779,14 @@ "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", "dev": true }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "requires": { + "has": "1.0.1" + } + }, "is-resolvable": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", @@ -1741,6 +1808,11 @@ "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", "dev": true }, + "is-symbol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", + "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=" + }, "is-text-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", @@ -2437,6 +2509,20 @@ "integrity": "sha1-qXiFtVPldetACevAm92psc0hl5o=", "dev": true }, + "object-keys": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", + "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=" + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.8.2" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3469,6 +3555,15 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "requires": { + "define-properties": "1.1.2", + "object.getownpropertydescriptors": "2.0.3" + } + }, "uuid": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", diff --git a/test/api-extensions.test.js b/test/api-extensions.test.js new file mode 100644 index 00000000..1bfb9f29 --- /dev/null +++ b/test/api-extensions.test.js @@ -0,0 +1,58 @@ +'use strict'; + +const assume = require('assume'); +const async = require('async'); +const nock = require('nock'); + +const common = require('./common'); +const beforeTesting = common.beforeTesting; + +const testApiExtensions = { + apiVersion: 'apiextensions.k8s.io/v1beta1', + kind: 'CustomResourceDefinition', + metadata: { + name: 'test.example.com' + }, + spec: { + group: 'example.com', + version: 'v1', + scope: 'Namespaced', + names: { + plural: 'tests', + singular: 'test', + kind: 'Test', + shortNames: [ + 't' + ] + } + } +}; + +describe('lib.apiextensions', () => { + describe('.ApiExtensions', () => { + const testCustomResourceName = testApiExtensions.metadata.name; + + beforeTesting('unit', () => { + nock(common.apiExtensions.url) + .post(`${ common.apiExtensions.path }/customresourcedefinitions`) + .reply(201, testApiExtensions) + .get(`${ common.apiExtensions.path }/customresourcedefinitions/${ testCustomResourceName }`) + .reply(200, testApiExtensions); + }); + + // NOTE: Running only unit tests. Setting up CRD is more involved, and it + // makes it cumbersome to run the other integration tests. We need + // improvements to our integration test harness to make this work well. + common.only('unit', 'can POST and GET', done => { + async.series([ + next => common.apiExtensions.customresourcedefinitions.post({ body: testApiExtensions }, next), + next => common.apiExtensions.customresourcedefinitions.get(testCustomResourceName, next) + ], (err, results) => { + assume(err).is.falsy(); + const getResult = results[1]; + assume(getResult.metadata.name).is.equal(testCustomResourceName); + done(); + }); + }); + }); +}); diff --git a/test/common.js b/test/common.js index fb6f3780..2e2b998d 100644 --- a/test/common.js +++ b/test/common.js @@ -8,12 +8,14 @@ const path = require('path'); const yaml = require('js-yaml'); const Api = require('../lib/api'); +const ApiExtensions = require('../lib/api-extensions'); const Apps = require('../lib/apps'); const Batch = require('../lib/batch'); const Core = require('../lib/core'); const Extensions = require('../lib/extensions'); const Rbac = require('../lib/rbac'); const ThirdPartyResources = require('../lib/third-party-resources'); +const CustomResourceDefinition = require('../lib/custom-resource-definition'); const defaultName = process.env.NAMESPACE || 'integration-tests'; const defaultTimeout = process.env.TIMEOUT || 30000; @@ -68,6 +70,7 @@ function newName() { function injectApis(options) { const apis = { api: { Constructor: Core }, + apiExtensions: { Constructor: ApiExtensions }, apiGroup: { Constructor: Api }, apps: { Constructor: Apps }, batch: { Constructor: Batch }, @@ -76,6 +79,9 @@ function injectApis(options) { rbac: { Constructor: Rbac }, thirdPartyResources: { Constructor: ThirdPartyResources, options: { group: 'kubernetes-client.com' } + }, + customResourceDefinitions: { + Constructor: CustomResourceDefinition, options: { group: 'kubernetes-client.com' } } }; Object.keys(apis).forEach(apiName => { @@ -198,3 +204,4 @@ module.exports.beforeTesting = beforeTesting; module.exports.beforeTestingEach = beforeTestingEach; module.exports.only = only; module.exports.thirdPartyDomain = 'kubernetes-client.com'; +module.exports.customResourceDomain = 'kubernetes-client.com'; diff --git a/test/custom-resource-definition.test.js b/test/custom-resource-definition.test.js new file mode 100644 index 00000000..87c391d4 --- /dev/null +++ b/test/custom-resource-definition.test.js @@ -0,0 +1,126 @@ +/* eslint max-nested-callbacks:0 */ +'use strict'; + +const assume = require('assume'); +const async = require('async'); +const nock = require('nock'); + +const common = require('./common'); +const afterTesting = common.afterTesting; +const beforeTesting = common.beforeTesting; +const only = common.only; + +const newResource = { + apiVersion: 'apiextensions.k8s.io/v1beta1', + kind: 'CustomResourceDefinition', + metadata: { + name: `newresources.${ common.customResourceDomain }` + }, + spec: { + group: common.customResourceDomain, + version: 'v1', + scope: 'Namespaced', + names: { + plural: 'newresources', + singular: 'newresource', + kind: 'NewResource', + shortNames: [ + 'nr' + ] + } + } +}; + +const testManifest = { + apiVersion: `${ common.customResourceDomain }/v1`, + kind: 'NewResource', + metadata: { + name: 'test' + }, + testProperty: 'hello world' +}; + +function createNewResource(cb) { + common.customResourceDefinitions.addResource('newresources'); + common.apiExtensions.customresourcedefinition.delete(newResource.metadata.name, () => { + common.apiExtensions.customresourcedefinition.post({ body: newResource }, postErr => { + if (postErr) return cb(postErr); + // + // Creating the API endpoints for a 3rd party resource appears to be + // asynchronous with respect to creating the resource. + // + const times = common.defaultTimeout / 1000; + async.retry({ times: times, interval: 1000 }, next => { + common.customResourceDefinitions.ns(common.currentName).newresources.get(err => { + if (err) return next(err); + cb(); + }); + }); + }); + }); +} + +describe('lib.CustomResourceDefinition', () => { + + beforeTesting('int', common.changeName); + + describe('.addResource', () => { + only('unit', 'adds a BaseObject globally and to default namespace', () => { + common.customResourceDefinitions.addResource('newresources'); + assume(common.customResourceDefinitions.newresources).is.truthy(); + assume(common.customResourceDefinitions.namespace.newresources).is.truthy(); + }); + }); + + describe('.newresources', () => { + beforeTesting('int', done => { + createNewResource(done); + }); + afterTesting('int', done => { + common.apiExtensions.customresourcedefinition.delete(newResource.metadata.name, done); + }); + + describe('.get', () => { + beforeTesting('unit', () => { + nock(common.customResourceDefinitions.url) + .get(`${ common.customResourceDefinitions.path }/newresources`) + .reply(200, { kind: 'NewResourceList' }); + }); + + it('returns NewSourceList', done => { + common.customResourceDefinitions.newresources.get((err, results) => { + assume(err).is.falsy(); + assume(results.kind).is.equal('NewResourceList'); + done(); + }); + }); + }); + + describe('.post', () => { + beforeTesting('unit', () => { + nock(common.customResourceDefinitions.url) + .post(`/apis/${ common.customResourceDomain }/v1/namespaces/${ common.currentName }/newresources`) + .reply(200, {}) + .get(`/apis/${ common.customResourceDomain }/v1/namespaces/${ common.currentName }/newresources/test`) + .reply(200, { metadata: { name: 'test' }}); + }); + + it('creates a resources', done => { + common.customResourceDefinitions + .ns + .newresources + .post({ body: testManifest }, postErr => { + assume(postErr).is.falsy(); + common.customResourceDefinitions + .ns + .newresources + .get('test', (err, result) => { + assume(err).is.falsy(); + assume(result.metadata.name).is.equal('test'); + done(); + }); + }); + }); + }); + }); +});