auto-kubernetes-client
Advanced tools
Comparing version 0.4.3 to 0.5.0
@@ -5,4 +5,3 @@ { | ||
"es6": true, | ||
"node": true, | ||
"mocha": true | ||
"node": true | ||
}, | ||
@@ -15,14 +14,21 @@ "parserOptions": { | ||
}, | ||
"rules": { | ||
"no-const-assign": "error", | ||
"no-this-before-super": "warn", | ||
"no-undef": "error", | ||
"no-unreachable": "error", | ||
"no-unused-vars": "warn", | ||
"constructor-super": "warn", | ||
"valid-typeof": "warn", | ||
"indent": ["warn", "tab"], | ||
"quotes": ["error", "single"], | ||
"no-extra-parens": "warn" | ||
} | ||
"extends": "eslint-config-collaborne", | ||
"overrides": [ | ||
{ | ||
"files": "examples/*/index.js", | ||
"parserOptions": { | ||
"sourceType": "script" | ||
} | ||
}, | ||
{ | ||
"files": "src/index.js", | ||
"rules": { | ||
"sort-keys": "warn" | ||
} | ||
} | ||
] | ||
} |
@@ -10,9 +10,9 @@ 'use strict'; | ||
const config = { | ||
url: 'https://192.168.99.100:8443', | ||
ca: fs.readFileSync(path.resolve(userDir, '.minikube/ca.crt'), 'UTF-8'), | ||
cert: fs.readFileSync(path.resolve(userDir, '.minikube/apiserver.crt'), 'UTF-8'), | ||
key: fs.readFileSync(path.resolve(userDir, '.minikube/apiserver.key'), 'UTF-8') | ||
key: fs.readFileSync(path.resolve(userDir, '.minikube/apiserver.key'), 'UTF-8'), | ||
url: 'https://192.168.99.100:8443', | ||
}; | ||
K8sClient(config).then(function(client) { | ||
K8sClient(config).then(client => { | ||
return client.namespaces.list() | ||
@@ -23,4 +23,4 @@ .then(nsList => nsList.items.map(ns => client.ns(ns.metadata.name).pods.list())) | ||
.then(pods => pods.forEach(pod => console.log(`Discovered pod ${pod.metadata.namespace}/${pod.metadata.name}`))); | ||
}).catch(function(err) { | ||
}).catch(err => { | ||
console.error(`Error: ${err.message}`); | ||
}); |
@@ -10,15 +10,15 @@ 'use strict'; | ||
const config = { | ||
url: 'https://192.168.99.100:8443', | ||
ca: fs.readFileSync(path.resolve(userDir, '.minikube/ca.crt'), 'UTF-8'), | ||
cert: fs.readFileSync(path.resolve(userDir, '.minikube/apiserver.crt'), 'UTF-8'), | ||
key: fs.readFileSync(path.resolve(userDir, '.minikube/apiserver.key'), 'UTF-8') | ||
key: fs.readFileSync(path.resolve(userDir, '.minikube/apiserver.key'), 'UTF-8'), | ||
url: 'https://192.168.99.100:8443', | ||
}; | ||
K8sClient(config).then(function(client) { | ||
K8sClient(config).then(client => { | ||
const watchPipe = client.ns('master').pods.watch(); | ||
watchPipe.on('data', function(event) { | ||
watchPipe.on('data', event => { | ||
console.log(`${event.type} ${event.object.metadata.name}`); | ||
}); | ||
}, function(err) { | ||
}, err => { | ||
console.error(`Cannot connect to cluster: ${err.message}`); | ||
}); |
{ | ||
"name": "auto-kubernetes-client", | ||
"version": "0.4.3", | ||
"description": "NodeJS Kubernetes Client with automatic API discoveryEdit", | ||
"version": "0.5.0", | ||
"description": "NodeJS Kubernetes Client with automatic API discovery", | ||
"main": "src/index.js", | ||
"scripts": { | ||
"install": "check-node-version --package", | ||
"prepublish": "eslint src examples", | ||
"lint": "eslint src examples", | ||
"prepublish": "npm run lint", | ||
"test": "echo 'No tests yet'" | ||
@@ -25,7 +26,8 @@ }, | ||
"devDependencies": { | ||
"eslint": "^3.18.0" | ||
"eslint": "^4.0.0", | ||
"eslint-config-collaborne": "^1.1.1" | ||
}, | ||
"dependencies": { | ||
"check-node-version": "^2.0.1", | ||
"deepmerge": "^1.3.2", | ||
"check-node-version": "^3.0.0", | ||
"deepmerge": "^2.0.0", | ||
"flatmap": "0.0.3", | ||
@@ -32,0 +34,0 @@ "request": "^2.81.0", |
285
src/index.js
@@ -1,3 +0,1 @@ | ||
'use strict'; | ||
const request = require('request'); | ||
@@ -14,2 +12,3 @@ const url = require('url'); | ||
* @property {string} url | ||
* @property {string} [version='v1'] requested API version for the "core" group | ||
* @property {boolean} [insecureSkipTlsVerify] | ||
@@ -39,8 +38,10 @@ * @property {string} [ca] | ||
* | ||
* @param {Configuration} config | ||
* @return {Promise<Client>} | ||
* @param {Configuration} config client configuration | ||
* @return {Promise<Client>} a promise that resolves to a connected client | ||
*/ | ||
module.exports = function connect(config) { | ||
function connect(config) { | ||
// Ensure that the config.url ends with a '/' | ||
const configOptions = Object.assign({}, config, { url: config.url.endsWith('/') ? config.url : config.url + '/' }); | ||
const configOptions = Object.assign({}, config, { | ||
url: config.url.endsWith('/') ? config.url : `${config.url}/` | ||
}); | ||
@@ -50,5 +51,5 @@ /** | ||
* | ||
* @param {string} path | ||
* @param {Object} [extraOptions={}] | ||
* @return {Stream} | ||
* @param {string} path path to query, relative to the API server URL | ||
* @param {Object} [extraOptions={}] additional options for use with `request` | ||
* @return {Request} a stream of the query results | ||
*/ | ||
@@ -67,42 +68,42 @@ function streamK8sRequest(path, extraOptions = {}) { | ||
* | ||
* @param {any} path | ||
* @param {any} [extraOptions={}] | ||
* @returns {Promise<>} | ||
* @param {string} path path to query, relative to the API server URL | ||
* @param {Object} [extraOptions={}] additional options for use with `request` | ||
* @returns {Promise<Object>} a promise that resolves to the result of the query | ||
*/ | ||
function k8sRequest(path, extraOptions = {}) { | ||
const cooked = !extraOptions.rawResponse; | ||
const options = Object.assign({}, configOptions, { json: true }, extraOptions); | ||
const options = Object.assign({}, configOptions, {json: true}, extraOptions); | ||
return new Promise(function(resolve, reject) { | ||
return request(url.resolve(configOptions.url, path), options, function(err, response, data) { | ||
return new Promise((resolve, reject) => { | ||
return request(url.resolve(configOptions.url, path), options, (err, response, data) => { | ||
if (err) { | ||
return reject(err); | ||
} else { | ||
if (cooked) { | ||
let maybeStatus; | ||
if (typeof data === 'string') { | ||
// The server doesn't (always?) produce a Status for 401/403 errors it seems. | ||
// It should according to https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#http-status-codes, | ||
// but likely the authentication/authorization layer prevents that. | ||
// See https://github.com/kubernetes/kubernetes/issues/45970 | ||
maybeStatus = { | ||
kind: 'Status', | ||
apiVersion: 'v1', | ||
metadata: {}, | ||
status: 'Failure', | ||
message: data, | ||
reason: response.statusMessage, | ||
code: response.statusCode, | ||
}; | ||
} else { | ||
maybeStatus = data; | ||
} | ||
} | ||
if (maybeStatus.kind === 'Status' && maybeStatus.status === 'Failure') { | ||
// Synthesize an error from the status | ||
return reject(Object.assign(maybeStatus, new Error(maybeStatus.message))); | ||
} | ||
if (cooked) { | ||
let maybeStatus; | ||
if (typeof data === 'string') { | ||
// The server doesn't (always?) produce a Status for 401/403 errors it seems. | ||
// It should according to https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#http-status-codes, | ||
// but likely the authentication/authorization layer prevents that. | ||
// See https://github.com/kubernetes/kubernetes/issues/45970 | ||
maybeStatus = { | ||
apiVersion: 'v1', | ||
code: response.statusCode, | ||
kind: 'Status', | ||
message: data, | ||
metadata: {}, | ||
reason: response.statusMessage, | ||
status: 'Failure', | ||
}; | ||
} else { | ||
maybeStatus = data; | ||
} | ||
return resolve(data); | ||
if (maybeStatus.kind === 'Status' && maybeStatus.status === 'Failure') { | ||
// Synthesize an error from the status | ||
return reject(Object.assign(maybeStatus, new Error(maybeStatus.message))); | ||
} | ||
} | ||
return resolve(data); | ||
}); | ||
@@ -112,5 +113,5 @@ }); | ||
function createApi(name, groupPath, version, preferred) { | ||
function createApi(apiName, groupPath, version, preferred) { // eslint-disable-line max-params | ||
// Query that API for all possible operations, and map them. | ||
return k8sRequest(groupPath, {}).then(function(apiResources) { | ||
function createApiFromResources(apiResources) { | ||
// TODO: Transform the API information (APIResourceList) into functions. | ||
@@ -124,5 +125,5 @@ // Basically we have resources[] with each | ||
function createResourceCollection(resource, pathPrefix = '', extraOptions = {}) { | ||
let resourcePath = groupPath + '/'; | ||
let resourcePath = `${groupPath}/`; | ||
if (pathPrefix) { | ||
resourcePath += pathPrefix + '/'; | ||
resourcePath += `${pathPrefix}/`; | ||
} | ||
@@ -132,11 +133,11 @@ resourcePath += resource.name; | ||
return { | ||
options: function(options) { | ||
options(options) { | ||
return createResourceCollection(resource, pathPrefix, Object.assign({}, extraOptions, options)); | ||
}, | ||
watch: function(resourceVersion = '', qs = {}) { | ||
watch(resourceVersion = '', qs = {}) { | ||
let buffer = Buffer.alloc(0); | ||
let bufferLength = 0; | ||
const parseJSONStream = through2.obj(function(chunk, enc, callback) { | ||
const parseJSONStream = through2.obj((chunk, enc, callback) => { | ||
// Find a newline in the buffer: everything up to it together with the current buffer contents is for the callback, | ||
@@ -150,3 +151,3 @@ // and the rest forms the new buffer. | ||
chunk.copy(contents, bufferLength, startIndex, newlineIndex); | ||
this.push(JSON.parse(contents.toString('UTF-8'))); | ||
parseJSONStream.push(JSON.parse(contents.toString('UTF-8'))); | ||
@@ -171,5 +172,5 @@ // Clear the buffer if we used it. | ||
return callback(); | ||
}, function(callback) { | ||
}, callback => { | ||
if (bufferLength > 0) { | ||
this.push(JSON.parse(buffer.toString('UTF-8', 0, bufferLength))); | ||
parseJSONStream.push(JSON.parse(buffer.toString('UTF-8', 0, bufferLength))); | ||
bufferLength = 0; | ||
@@ -181,53 +182,81 @@ } | ||
return streamK8sRequest(resourcePath, Object.assign({}, extraOptions, { method: 'GET', json: false, qs: Object.assign({}, qs, { watch: 'true', resourceVersion }) })) | ||
return streamK8sRequest(resourcePath, Object.assign({}, extraOptions, { | ||
json: false, | ||
method: 'GET', | ||
qs: Object.assign({}, qs, { | ||
resourceVersion, | ||
watch: 'true', | ||
}) | ||
})) | ||
.pipe(parseJSONStream); | ||
}, | ||
list: function(qs = {}) { | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { qs, method: 'GET' })); | ||
list(qs = {}) { | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { | ||
method: 'GET', | ||
qs, | ||
})); | ||
}, | ||
create: function(object, qs = {}) { | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { qs, method: 'POST', body: object })); | ||
create(object, qs = {}) { | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { | ||
body: object, | ||
method: 'POST', | ||
qs, | ||
})); | ||
}, | ||
deletecollection: function(qs = {}) { | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { qs, method: 'DELETE' })); | ||
deletecollection(qs = {}) { | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { | ||
method: 'DELETE', | ||
qs, | ||
})); | ||
}, | ||
} | ||
}; | ||
} | ||
function createResource(resource, name, pathPrefix = '', extraOptions = {}) { | ||
let resourcePath = groupPath + '/'; | ||
function createResource(resource, name, pathPrefix = '', extraOptions = {}) { // eslint-disable-line max-params | ||
let resourcePath = `${groupPath}/`; | ||
if (pathPrefix) { | ||
resourcePath += pathPrefix + '/'; | ||
resourcePath += `${pathPrefix}/`; | ||
} | ||
resourcePath += resource.name + '/'; | ||
resourcePath += `${resource.name}/`; | ||
resourcePath += name; | ||
return { | ||
options: function(options) { | ||
options(options) { | ||
return createResource(resource, name, pathPrefix, Object.assign({}, extraOptions, options)); | ||
}, | ||
get: function(qs = {}) { | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { qs, method: 'GET' })); | ||
get(qs = {}) { | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { | ||
method: 'GET', | ||
qs, | ||
})); | ||
}, | ||
create: function(object, qs = {}) { | ||
const createObject = deepMerge({ metadata: { name }}, object); | ||
create(object, qs = {}) { | ||
const createObject = deepMerge({metadata: {name}}, object); | ||
// Creating happens by posting to the list: | ||
let listPath = groupPath + '/'; | ||
let listPath = `${groupPath}/`; | ||
if (pathPrefix) { | ||
listPath += pathPrefix + '/'; | ||
listPath += `${pathPrefix}/`; | ||
} | ||
listPath += resource.name; | ||
return k8sRequest(listPath, Object.assign({}, extraOptions, { qs, method: 'POST', body: createObject })); | ||
return k8sRequest(listPath, Object.assign({}, extraOptions, { | ||
body: createObject, | ||
method: 'POST', | ||
qs, | ||
})); | ||
}, | ||
update: function(object, qs = {}) { | ||
const updateObject = deepMerge({ metadata: { name }}, object); | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { qs, method: 'PUT', body: updateObject })); | ||
update(object, qs = {}) { | ||
const updateObject = deepMerge({metadata: {name}}, object); | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { | ||
body: updateObject, | ||
method: 'PUT', | ||
qs, | ||
})); | ||
}, | ||
@@ -248,15 +277,29 @@ | ||
* @param {Object} [qs] additional query parameters | ||
* @return {Promise<Object>} promise that resolves to the response of the request | ||
*/ | ||
patch: function(object, contentType = 'application/strategic-merge-patch+json', qs = {}) { | ||
patch(object, contentType = 'application/strategic-merge-patch+json', qs = {}) { | ||
// Handle cases where qs is given but not contentType | ||
let realQS; | ||
let realContentType; | ||
if (typeof contentType === 'object') { | ||
qs = contentType; | ||
contentType = 'application/strategic-merge-patch+json'; | ||
realQS = contentType; | ||
realContentType = 'application/strategic-merge-patch+json'; | ||
} else { | ||
realQS = qs; | ||
realContentType = contentType; | ||
} | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { qs, method: 'PATCH', headers: { 'content-type': contentType }, body: object })); | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { | ||
body: object, | ||
headers: {'content-type': realContentType}, | ||
method: 'PATCH', | ||
qs: realQS, | ||
})); | ||
}, | ||
delete: function(qs = {}) { | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { qs, method: 'DELETE' })); | ||
delete(qs = {}) { | ||
return k8sRequest(resourcePath, Object.assign({}, extraOptions, { | ||
method: 'DELETE', | ||
qs, | ||
})); | ||
}, | ||
@@ -269,6 +312,4 @@ }; | ||
[resource.name.toLowerCase()]: createResourceCollection(resource, pathPrefix), | ||
[resource.kind.toLowerCase()]: function(name) { | ||
return createResource(resource, name, pathPrefix) | ||
} | ||
} | ||
[resource.kind.toLowerCase()]: resourceName => createResource(resource, resourceName, pathPrefix) | ||
}; | ||
} | ||
@@ -278,25 +319,27 @@ | ||
const api = { | ||
name, | ||
apiName, | ||
version: version.version, | ||
preferred, | ||
ns: function(namespace) { | ||
// Return adapted nsResources for this namespace | ||
return Object.keys(nsResources).reduce(function(result, resourceKey) { | ||
return Object.assign(result, createResourceAPI(nsResources[resourceKey], `namespaces/${namespace}`)); | ||
}, {}); | ||
}, | ||
/** | ||
* 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); | ||
}, | ||
// Other properties here represent non-namespaced resources | ||
}; | ||
// other properties here represent non-namespaced resources | ||
api.ns = namespace => { | ||
// Return adapted nsResources for this namespace | ||
return Object.keys(nsResources).reduce((result, resourceKey) => { | ||
return Object.assign(result, createResourceAPI(nsResources[resourceKey], `namespaces/${namespace}`)); | ||
}, {}); | ||
}; | ||
return apiResources.resources.reduce(function(api, resource) { | ||
/** | ||
* 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? | ||
api.resource = kind => { | ||
return apiResources.resources.find(resource => kind === resource.kind); | ||
}; | ||
return apiResources.resources.reduce((targetApi, resource) => { | ||
const slashIndex = resource.name.indexOf('/'); | ||
@@ -314,7 +357,9 @@ if (slashIndex !== -1) { | ||
} else { | ||
Object.assign(api, createResourceAPI(resource)); | ||
Object.assign(targetApi, createResourceAPI(resource)); | ||
} | ||
return api; | ||
return targetApi; | ||
}, api); | ||
}); | ||
} | ||
return k8sRequest(groupPath, {}).then(createApiFromResources); | ||
} | ||
@@ -324,11 +369,12 @@ | ||
return k8sRequest('apis').then(function(apiGroups) { | ||
return k8sRequest('apis').then(apiGroups => { | ||
// Initialize the APIs | ||
const apiPromises = flatMap(apiGroups.groups, function(group) { | ||
const apiPromises = flatMap(apiGroups.groups, group => { | ||
return group.versions.map(version => createApi(group.name, `apis/${version.groupVersion}`, version, version.version === group.preferredVersion.version)); | ||
}); | ||
apiPromises.push(createApi('', `api/${coreVersion}`, { groupVersion: coreVersion, version: coreVersion }, true)); | ||
apiPromises.push(createApi('', `api/${coreVersion}`, {groupVersion: coreVersion, | ||
version: coreVersion}, true)); | ||
return Promise.all(apiPromises); | ||
}).then(function(apis) { | ||
return apis.reduce(function(result, api) { | ||
}).then(apis => { | ||
return apis.reduce((result, api) => { | ||
// Build a compatible name for this API. Note that both api.name and api.version can end up empty here. | ||
@@ -341,16 +387,19 @@ const apiNamePrefix = api.name ? `${api.name}/` : ''; | ||
return result; | ||
}, {}) | ||
}).then(function(apis) { | ||
}, {}); | ||
}).then(apis => { | ||
const coreApi = Object.assign({}, apis[coreVersion]); | ||
// Remove the 'name' field from the root object | ||
delete coreApi.name; | ||
delete coreApi.name; | ||
return Object.assign({}, coreApi, { | ||
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 | ||
* @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 | ||
* @return {Object} an API for the given group | ||
* @throws {Error} when no API group is available with that name (and version) | ||
*/ | ||
group: function(groupName, versionName) { | ||
group(groupName, versionName) { | ||
// Calculate a full API name from groupName and version name. | ||
@@ -380,1 +429,3 @@ let apiName; | ||
} | ||
module.exports = connect; |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
33204
435
2
8
+ Addedansi-styles@3.2.1(transitive)
+ Addedchalk@2.4.2(transitive)
+ Addedcheck-node-version@3.3.0(transitive)
+ Addedcolor-convert@1.9.3(transitive)
+ Addedcolor-name@1.1.3(transitive)
+ Addeddeepmerge@2.2.1(transitive)
+ Addedescape-string-regexp@1.0.5(transitive)
+ Addedhas-flag@3.0.0(transitive)
+ Addedsupports-color@5.5.0(transitive)
- Removedcheck-node-version@2.1.0(transitive)
- Removeddeepmerge@1.5.2(transitive)
Updatedcheck-node-version@^3.0.0
Updateddeepmerge@^2.0.0