auto-kubernetes-client
Advanced tools
Comparing version 0.3.1 to 0.4.0
{ | ||
"name": "auto-kubernetes-client", | ||
"version": "0.3.1", | ||
"version": "0.4.0", | ||
"description": "NodeJS Kubernetes Client with automatic API discoveryEdit", | ||
@@ -29,2 +29,3 @@ "main": "src/index.js", | ||
"check-node-version": "^2.0.1", | ||
"deepmerge": "^1.3.2", | ||
"flatmap": "0.0.3", | ||
@@ -31,0 +32,0 @@ "request": "^2.81.0", |
# auto-kubernetes-client [](https://travis-ci.org/Collaborne/auto-kubernetes-client) [](https://greenkeeper.io/) | ||
NodeJS Kubernetes Client with automatic API discovery | ||
NodeJS Kubernetes Client with automatic API discovery. | ||
See this [blog post](https://medium.com/collaborne-engineering/keep-pace-with-kubernetes-nodejs-client-b87a8b175b7b) for further information. | ||
## Installation | ||
@@ -50,7 +52,10 @@ | ||
represents the "pod" resources for the "pod1" pod. | ||
Single resources offer resource methods `get`, `update`, `patch` and `delete`. | ||
Single resources offer resource methods `get`, `create`, `update`, `patch` and `delete`. | ||
- Resource methods typically have the signature `method([qs])`, where `qs` is a hash for additional query parameters, | ||
and return a promise for the parsed response entity. | ||
- The `watch` resource method has the signature `watch([resourceVersion[, qs]])`, and returns an object stream for the observed changes. | ||
Each object has a `type` field ('ADDED', 'DELETED', 'MODIFIED'), and the actual object that was modified. | ||
Each object has a `type` field ('ADDED', 'DELETED', 'MODIFIED', 'ERROR'), and the actual object that was modified. | ||
- By default the client interprets 'Status' responses from the server with a 'Failure' status as error responses, and translates | ||
them into actual promise rejections. This can be disabled by using '.options({ rawResponse: true}).resourceMethod(...)' on the resource collection | ||
or resource. | ||
@@ -62,2 +67,3 @@ ## Examples | ||
|[examples/list-pods](./examples/list-pods)|List all pods in the cluster | ||
|[examples/watch-pods](./examples/watch-pods)|Watch all pods in a specific namespace | ||
@@ -64,0 +70,0 @@ ## License |
115
src/index.js
@@ -7,2 +7,3 @@ 'use strict'; | ||
const through2 = require('through2'); | ||
const deepMerge = require('deepmerge'); | ||
@@ -69,2 +70,3 @@ /** | ||
function k8sRequest(path, extraOptions = {}) { | ||
const cooked = !extraOptions.rawResponse; | ||
const options = Object.assign({}, configOptions, { json: true }, extraOptions); | ||
@@ -77,2 +79,6 @@ | ||
} else { | ||
if (cooked && data.kind === 'Status' && data.status === 'Failure') { | ||
// Synthesize an error from the status | ||
return reject(Object.assign(data, new Error(data.message))); | ||
} | ||
return resolve(data); | ||
@@ -94,3 +100,3 @@ } | ||
// set on the thing. | ||
function createResourceCollection(resource, pathPrefix = '') { | ||
function createResourceCollection(resource, pathPrefix = '', extraOptions = {}) { | ||
let resourcePath = groupPath + '/'; | ||
@@ -103,2 +109,6 @@ if (pathPrefix) { | ||
return { | ||
options: function(options) { | ||
return createResourceCollection(resource, pathPrefix, Object.assign({}, extraOptions, options)); | ||
}, | ||
watch: function(resourceVersion = '', qs = {}) { | ||
@@ -146,3 +156,3 @@ let buffer = Buffer.alloc(0); | ||
return streamK8sRequest(resourcePath, { method: 'GET', json: false, qs: Object.assign({}, qs, { watch: 'true', resourceVersion }) }) | ||
return streamK8sRequest(resourcePath, Object.assign({}, extraOptions, { method: 'GET', json: false, qs: Object.assign({}, qs, { watch: 'true', resourceVersion }) })) | ||
.pipe(parseJSONStream); | ||
@@ -152,11 +162,11 @@ }, | ||
list: function(qs = {}) { | ||
return k8sRequest(resourcePath, { qs, method: 'GET' }); | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { qs, method: 'GET' })); | ||
}, | ||
create: function(object, qs = {}) { | ||
return k8sRequest(resourcePath, { qs, method: 'POST', body: object }); | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { qs, method: 'POST', body: object })); | ||
}, | ||
deletecollection: function(qs = {}) { | ||
return k8sRequest(resourcePath, { qs, method: 'DELETE' }); | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { qs, method: 'DELETE' })); | ||
}, | ||
@@ -166,3 +176,3 @@ } | ||
function createResource(resource, name, pathPrefix = '') { | ||
function createResource(resource, name, pathPrefix = '', extraOptions = {}) { | ||
let resourcePath = groupPath + '/'; | ||
@@ -176,17 +186,54 @@ if (pathPrefix) { | ||
return { | ||
options: function(options) { | ||
return createResource(resource, name, pathPrefix, Object.assign({}, extraOptions, options)); | ||
}, | ||
get: function(qs = {}) { | ||
return k8sRequest(resourcePath, { qs, method: 'GET' }); | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { qs, method: 'GET' })); | ||
}, | ||
create: function(object, qs = {}) { | ||
const createObject = deepMerge({ metadata: { name }}, object); | ||
// Creating happens by posting to the list: | ||
let listPath = groupPath + '/'; | ||
if (pathPrefix) { | ||
listPath += pathPrefix + '/'; | ||
} | ||
listPath += resource.name; | ||
return k8sRequest(listPath, Object.assign({}, extraOptions, { qs, method: 'POST', body: createObject })); | ||
}, | ||
update: function(object, qs = {}) { | ||
const updateObject = Object.assign({ metadata: { name }}, object); | ||
return k8sRequest(resourcePath, { qs, method: 'PUT', body: updateObject }); | ||
const updateObject = deepMerge({ metadata: { name }}, object); | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { qs, method: 'PUT', body: updateObject })); | ||
}, | ||
patch: function(object, qs = {}) { | ||
return k8sRequest(resourcePath, { qs, method: 'PATCH', body: object }); | ||
/** | ||
* Patch the resource. | ||
* | ||
* The 'contentType' parameter describes how to process the given object: | ||
* 'application/strategic-merge-patch+json': (default) object is a partial representation | ||
* 'application/merge-patch+json': RFC7386 "Merge Patch" | ||
* 'application/json-patch+json': RFC6902 "JSON Patch" (object is an array of operations to apply) | ||
* | ||
* See https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#patch-operations for details. | ||
* | ||
* @param {any} object the patch to apply | ||
* @param {String} [contentType] the content type | ||
* @param {Object} [qs] additional query parameters | ||
*/ | ||
patch: function(object, contentType = 'application/strategic-merge-patch+json', qs = {}) { | ||
// Handle cases where qs is given but not contentType | ||
if (typeof contentType === 'object') { | ||
qs = contentType; | ||
contentType = 'application/strategic-merge-patch+json'; | ||
} | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { qs, method: 'PATCH', headers: { 'content-type': contentType }, body: object })); | ||
}, | ||
delete: function(qs = {}) { | ||
return k8sRequest(resourcePath, { qs, method: 'DELETE' }); | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { qs, method: 'DELETE' })); | ||
}, | ||
@@ -216,2 +263,11 @@ }; | ||
}, | ||
/** | ||
* Get information about the resource with the given kind | ||
* | ||
* @param {String} kind | ||
*/ | ||
// XXX: Should this instead exist on the collection or on the single resource via an 'info'/'explain' method? | ||
resource: function(kind) { | ||
return apiResources.resources.find(resource => kind === resource.kind); | ||
}, | ||
@@ -253,6 +309,7 @@ // other properties here represent non-namespaced resources | ||
return apis.reduce(function(result, api) { | ||
result[api.name] = result[api.name] || {}; | ||
result[api.name][api.version] = api; | ||
// Build a compatible name for this API. Note that both api.name and api.version can end up empty here. | ||
const apiNamePrefix = api.name ? `${api.name}/` : ''; | ||
result[`${apiNamePrefix}${api.version}`] = api; | ||
if (api.preferred) { | ||
result[api.name][''] = api; | ||
result[api.name] = api; | ||
} | ||
@@ -262,15 +319,31 @@ return result; | ||
}).then(function(apis) { | ||
const coreApi = Object.assign({}, apis[''][coreVersion]); | ||
const coreApi = Object.assign({}, apis[coreVersion]); | ||
// Remove the 'name' field from the root object | ||
delete coreApi.name; | ||
return Object.assign({}, coreApi, { | ||
/** | ||
* Get the API group with the given name and version | ||
* | ||
* @param {String} groupName name of the group, may optionally contain a '/version' specification | ||
* @param {String} [versionName] version of the group, if not given defaults to the "preferred" version as reported by the server | ||
*/ | ||
group: function(groupName, versionName) { | ||
const apiGroup = apis[groupName]; | ||
if (!apiGroup) { | ||
throw new Error(`No API group ${groupName} available`); | ||
// Calculate a full API name from groupName and version name. | ||
let apiName; | ||
const slashIndex = groupName.indexOf('/'); | ||
if (slashIndex === -1) { | ||
apiName = versionName ? `${groupName}/${versionName}` : groupName; | ||
} else if (versionName) { | ||
// Version given in both the groupName and as parameters, use the one from the parameter. | ||
const realGroupName = groupName.substring(0, slashIndex); | ||
apiName = `${realGroupName}/${versionName}`; | ||
} else { | ||
apiName = groupName; | ||
} | ||
const api = apiGroup[versionName || '']; | ||
const api = apis[apiName]; | ||
if (!api) { | ||
throw new Error(`No version ${versionName} for API group ${groupName} available`); | ||
// FIXME: APIs might appear later on, we should do a query again here to check | ||
throw new Error(`No API group ${apiName} available`); | ||
} | ||
@@ -277,0 +350,0 @@ |
32222
368
85
5
+ Addeddeepmerge@^1.3.2
+ Addeddeepmerge@1.5.2(transitive)