auto-kubernetes-client
Advanced tools
Comparing version 0.2.0 to 0.3.0
@@ -6,5 +6,5 @@ 'use strict'; | ||
const K8sClient = require('.'); | ||
const K8sClient = require('../..'); | ||
const userDir = process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME']; | ||
const userDir = process.env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME']; | ||
const config = { | ||
@@ -17,22 +17,10 @@ url: 'https://192.168.99.100:8443', | ||
K8sClient(config, function(err, client) { | ||
if (err) { | ||
console.error(`Cannot connect to cluster: ${err.message}`); | ||
return; | ||
} | ||
client.namespaces.list(function(err, response, nsList) { | ||
if (err) { | ||
console.error(`Cannot list namespaces: ${err.message}`); | ||
return; | ||
} | ||
return nsList.items.forEach(function(ns) { | ||
return client.ns(ns.metadata.name).pods.list(function(err, response, podList) { | ||
podList.items.forEach(function(pod) { | ||
console.log(`Discovered pod ${pod.metadata.namespace}/${pod.metadata.name}`); | ||
}); | ||
}); | ||
}); | ||
}); | ||
K8sClient(config).then(function(client) { | ||
return client.namespaces.list() | ||
.then(nsList => nsList.items.map(ns => client.ns(ns.metadata.name).pods.list())) | ||
.then(podListPromises => Promise.all(podListPromises)) | ||
.then(podLists => podLists.reduce((result, podList) => result.concat(podList.items), [])) | ||
.then(pods => pods.forEach(pod => console.log(`Discovered pod ${pod.metadata.namespace}/${pod.metadata.name}`))); | ||
}).catch(function(err) { | ||
console.error(`Error: ${err.message}`); | ||
}); |
{ | ||
"name": "auto-kubernetes-client", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"description": "NodeJS Kubernetes Client with automatic API discoveryEdit", | ||
@@ -8,3 +8,3 @@ "main": "src/index.js", | ||
"install": "check-node-version --package", | ||
"prepublish": "eslint src", | ||
"prepublish": "eslint src examples", | ||
"test": "echo 'No tests yet'" | ||
@@ -31,3 +31,4 @@ }, | ||
"flatmap": "0.0.3", | ||
"request": "^2.81.0" | ||
"request": "^2.81.0", | ||
"through2": "^2.0.3" | ||
}, | ||
@@ -34,0 +35,0 @@ "engines": { |
@@ -51,6 +51,6 @@ # auto-kubernetes-client [](https://travis-ci.org/Collaborne/auto-kubernetes-client) [](https://greenkeeper.io/) | ||
Single resources offer resource methods `get`, `update`, `patch` and `delete`. | ||
- Resource methods typically have the signature `method(callback, qs = {})`, where `qs` is a hash for additional query parameters, | ||
and `callback` is a `function(err, response, data)`. `response` contains the full response as provided by the [request library](https://github.com/request/request), and `data` is the parsed response entity. | ||
- The `watch` resource method has the signature `watch(callback)`, and `callback` is a `function(err, change)`. If `change` is | ||
`null` then the watch was interrupted/ended, otherwise it will be an object with a `type` field ('ADDED', 'DELETED', 'MODIFIED'), and the actual object that was modified. | ||
- 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()`, 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. | ||
@@ -57,0 +57,0 @@ ## Examples |
431
src/index.js
@@ -6,250 +6,265 @@ 'use strict'; | ||
const flatMap = require('flatmap'); | ||
const through2 = require('through2'); | ||
/** | ||
* Query the kubernetes server | ||
* Cluster configuration | ||
* | ||
* @param {string} path | ||
* @param {Object} [extraOptions={}] | ||
* @param {Function} [callback=null] | ||
* @typedef Configuration | ||
* @property {string} url | ||
* @property {boolean} insecureSkipTlsVerify | ||
* @property {string} ca | ||
* @property {string} cert | ||
* @property {string} key | ||
* @property {ConfigurationAuth} auth | ||
*/ | ||
function doRequest(config, path, extraOptions = {}, callback = null) { | ||
const options = Object.assign({}, config, { | ||
json: true | ||
}, extraOptions) | ||
/** | ||
* Cluster user authentication | ||
* | ||
* @typedef ConfigurationAuth | ||
* @property {string} user | ||
* @property {string} password | ||
* @property {string} bearer | ||
*/ | ||
/** | ||
* Client | ||
* | ||
* @typedef Client | ||
*/ | ||
return request(url.resolve(config.url, path), options, callback); | ||
} | ||
function wrapCallback(callback) { | ||
return function(err, response, result) { | ||
if (err) { | ||
// Basic error | ||
return callback(err, response, result); | ||
} else if (result && result.apiVersion === 'v1' && result.kind === 'Status' && result.status === 'Failure') { | ||
// k8s error encoded as status | ||
return callback(new Error(result.message), response, result); | ||
} else { | ||
return callback(null, response, result); | ||
} | ||
} | ||
} | ||
module.exports = function connect(config, callback) { | ||
/** | ||
* Connect to the cluster | ||
* | ||
* @param {Configuration} config | ||
* @return {Promise<Client>} | ||
*/ | ||
module.exports = function connect(config) { | ||
// Ensure that the config.url ends with a '/' | ||
const k8sRequest = doRequest.bind(this, Object.assign({}, config, { url: config.url.endsWith('/') ? config.url : config.url + '/' })); | ||
const configOptions = Object.assign({}, config, { url: config.url.endsWith('/') ? config.url : config.url + '/' }); | ||
k8sRequest('apis', {}, function(err, response, apiGroups) { | ||
function _createApi(groupPath, version) { | ||
// Query that API for all possible operations, and map them. | ||
return new Promise(function(resolve, reject) { | ||
return k8sRequest(groupPath, {}, wrapCallback(function(err, response, apiResources) { | ||
if (err) { | ||
return reject(err); | ||
} | ||
/** | ||
* Query the kubernetes server and return a uncooked response stream | ||
* | ||
* @param {string} path | ||
* @param {Object} [extraOptions={}] | ||
* @return {Stream} | ||
*/ | ||
function streamK8sRequest(path, extraOptions = {}) { | ||
const options = Object.assign({}, configOptions, extraOptions); | ||
// TODO: Transform the API information (APIResourceList) into functions. | ||
// Basically we have resources[] with each | ||
// { kind: Bindings, name: bindings, namespaced: true/false } | ||
// For each of these we want to produce a list/watch function under that name, | ||
// and a function with that name that returns an object with get/... for the single thing. | ||
// If namespaced is set then this is appended to the ns() result, otherwise it is directly | ||
// set on the thing. | ||
function createResourceCollection(resource, pathPrefix = '') { | ||
let resourcePath = groupPath + '/'; | ||
if (pathPrefix) { | ||
resourcePath += pathPrefix + '/'; | ||
} | ||
resourcePath += resource.name; | ||
return request(url.resolve(configOptions.url, path), options); | ||
} | ||
return { | ||
watch: function(callback, resourceVersion = '', qs = {}) { | ||
// Watch calls the callback for each item, so we're switching off JSON mode, and instead | ||
// process each line of the response separately | ||
// Buffer contains usable data from 0..bufferLength | ||
let buffer = Buffer.alloc(0); | ||
let bufferLength = 0; | ||
return k8sRequest(resourcePath, { method: 'GET', json: false, qs: Object.assign({}, qs, { watch: 'true', resourceVersion }) }) | ||
.on('error', function(err) { | ||
return callback(err); | ||
}) | ||
.on('data', function(data) { | ||
// Find a newline in the buffer: everything up to it together with the current buffer contents is for the callback, | ||
// and the rest forms the new buffer. | ||
let newlineIndex; | ||
let startIndex = 0; | ||
while ((newlineIndex = data.indexOf('\n', startIndex)) !== -1) { | ||
const contents = Buffer.alloc(bufferLength + newlineIndex - startIndex); | ||
buffer.copy(contents, 0, 0, bufferLength); | ||
data.copy(contents, bufferLength, startIndex, newlineIndex); | ||
callback(null, JSON.parse(contents.toString('UTF-8'))); | ||
/** | ||
* Query the kubernetes server and return a promise for the result object | ||
* | ||
* Note that the promise does not reject when a 'Status' object is returned; the caller must apply suitable | ||
* checks on its own. | ||
* | ||
* @param {any} path | ||
* @param {any} [extraOptions={}] | ||
* @returns {Promise<>} | ||
*/ | ||
function k8sRequest(path, extraOptions = {}) { | ||
const options = Object.assign({}, configOptions, { json: true }, extraOptions); | ||
// Clear the buffer if we used it. | ||
if (bufferLength > 0) { | ||
bufferLength = 0; | ||
} | ||
return new Promise(function(resolve, reject) { | ||
return request(url.resolve(configOptions.url, path), options, function(err, response, data) { | ||
if (err) { | ||
return reject(err); | ||
} else { | ||
return resolve(data); | ||
} | ||
}); | ||
}); | ||
} | ||
startIndex = newlineIndex + 1; | ||
} | ||
function createApi(name, groupPath, version, preferred) { | ||
// Query that API for all possible operations, and map them. | ||
return k8sRequest(groupPath, {}).then(function(apiResources) { | ||
// TODO: Transform the API information (APIResourceList) into functions. | ||
// Basically we have resources[] with each | ||
// { kind: Bindings, name: bindings, namespaced: true/false } | ||
// For each of these we want to produce a list/watch function under that name, | ||
// and a function with that name that returns an object with get/... for the single thing. | ||
// If namespaced is set then this is appended to the ns() result, otherwise it is directly | ||
// set on the thing. | ||
function createResourceCollection(resource, pathPrefix = '') { | ||
let resourcePath = groupPath + '/'; | ||
if (pathPrefix) { | ||
resourcePath += pathPrefix + '/'; | ||
} | ||
resourcePath += resource.name; | ||
const restData = data.slice(startIndex); | ||
if (bufferLength + restData.length < buffer.length) { | ||
restData.copy(buffer, bufferLength); | ||
bufferLength += restData.length; | ||
} else { | ||
buffer = bufferLength === 0 ? restData : Buffer.concat([buffer.slice(0, bufferLength), restData]); | ||
bufferLength = buffer.length; | ||
} | ||
}) | ||
.on('end', function() { | ||
if (bufferLength > 0) { | ||
callback(JSON.parse(buffer.toString('UTF-8', 0, bufferLength))); | ||
} | ||
return { | ||
watch: function(resourceVersion = '', qs = {}) { | ||
let buffer = Buffer.alloc(0); | ||
let bufferLength = 0; | ||
return callback(null, null); | ||
}); | ||
}, | ||
const parseJSONStream = through2.obj(function(chunk, enc, callback) { | ||
// Find a newline in the buffer: everything up to it together with the current buffer contents is for the callback, | ||
// and the rest forms the new buffer. | ||
let newlineIndex; | ||
let startIndex = 0; | ||
while ((newlineIndex = chunk.indexOf('\n', startIndex)) !== -1) { | ||
const contents = Buffer.alloc(bufferLength + newlineIndex - startIndex); | ||
buffer.copy(contents, 0, 0, bufferLength); | ||
chunk.copy(contents, bufferLength, startIndex, newlineIndex); | ||
this.push(JSON.parse(contents.toString('UTF-8'))); | ||
list: function(callback, qs = {}) { | ||
return k8sRequest(resourcePath, { qs, method: 'GET' }, wrapCallback(callback)); | ||
}, | ||
// Clear the buffer if we used it. | ||
if (bufferLength > 0) { | ||
bufferLength = 0; | ||
} | ||
create: function(object, callback, qs = {}) { | ||
return k8sRequest(resourcePath, { qs, method: 'POST', body: object }, wrapCallback(callback)); | ||
}, | ||
startIndex = newlineIndex + 1; | ||
} | ||
deletecollection: function(callback, qs = {}) { | ||
return k8sRequest(resourcePath, { qs, method: 'DELETE' }, wrapCallback(callback)); | ||
}, | ||
} | ||
} | ||
const restData = chunk.slice(startIndex); | ||
if (bufferLength + restData.length < buffer.length) { | ||
restData.copy(buffer, bufferLength); | ||
bufferLength += restData.length; | ||
} else { | ||
buffer = bufferLength === 0 ? restData : Buffer.concat([buffer.slice(0, bufferLength), restData]); | ||
bufferLength = buffer.length; | ||
} | ||
function createResource(resource, name, pathPrefix = '') { | ||
let resourcePath = groupPath + '/'; | ||
if (pathPrefix) { | ||
resourcePath += pathPrefix + '/'; | ||
} | ||
resourcePath += resource.name + '/'; | ||
resourcePath += name; | ||
return callback(); | ||
}, function(callback) { | ||
if (bufferLength > 0) { | ||
this.push(JSON.parse(buffer.toString('UTF-8', 0, bufferLength))); | ||
bufferLength = 0; | ||
} | ||
return { | ||
get: function(callback, qs = {}) { | ||
return k8sRequest(resourcePath, { qs, method: 'GET' }, wrapCallback(callback)); | ||
}, | ||
return callback(); | ||
}); | ||
update: function(object, callback, qs = {}) { | ||
return k8sRequest(resourcePath, { qs, method: 'PUT', body: object }, wrapCallback(callback)); | ||
}, | ||
return streamK8sRequest(resourcePath, { method: 'GET', json: false, qs: Object.assign({}, qs, { watch: 'true', resourceVersion }) }) | ||
.pipe(parseJSONStream); | ||
}, | ||
patch: function(object, callback, qs = {}) { | ||
return k8sRequest(resourcePath, { qs, method: 'PATCH', body: object }, wrapCallback(callback)); | ||
}, | ||
list: function(qs = {}) { | ||
return k8sRequest(resourcePath, { qs, method: 'GET' }); | ||
}, | ||
delete: function(callback, qs = {}) { | ||
return k8sRequest(resourcePath, { qs, method: 'DELETE' }, wrapCallback(callback)); | ||
}, | ||
}; | ||
} | ||
create: function(object, qs = {}) { | ||
return k8sRequest(resourcePath, { qs, method: 'POST', body: object }); | ||
}, | ||
function createResourceAPI(resource, pathPrefix = '') { | ||
return { | ||
[resource.name.toLowerCase()]: createResourceCollection(resource, pathPrefix), | ||
[resource.kind.toLowerCase()]: function(name) { | ||
return createResource(resource, name, pathPrefix) | ||
} | ||
} | ||
} | ||
deletecollection: function(qs = {}) { | ||
return k8sRequest(resourcePath, { qs, method: 'DELETE' }); | ||
}, | ||
} | ||
} | ||
const nsResources = {}; | ||
const api = { | ||
name: version.groupVersion, | ||
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}`)); | ||
}, {}); | ||
}, | ||
function createResource(resource, name, pathPrefix = '') { | ||
let resourcePath = groupPath + '/'; | ||
if (pathPrefix) { | ||
resourcePath += pathPrefix + '/'; | ||
} | ||
resourcePath += resource.name + '/'; | ||
resourcePath += name; | ||
// other properties here represent non-namespaced resources | ||
}; | ||
return { | ||
get: function(qs = {}) { | ||
return k8sRequest(resourcePath, { qs, method: 'GET' }); | ||
}, | ||
apiResources.resources.forEach(function(resource) { | ||
const slashIndex = resource.name.indexOf('/'); | ||
if (slashIndex !== -1) { | ||
const subResource = resource.name.substring(slashIndex + 1); | ||
switch (subResource) { | ||
// TODO: Apply suitable additional methods on the resource when we understand the subresource. | ||
// TODO: support minimally 'status' and possibly 'proxy', 'exec'. | ||
default: | ||
// A unknown sub-resource, for now just ignore it. | ||
console.log(`Found unknown sub-resource ${subResource}, ignoring (${JSON.stringify(resource)})`); | ||
return; | ||
} | ||
} | ||
if (resource.namespaced) { | ||
nsResources[resource.name] = resource; | ||
} else { | ||
Object.assign(api, createResourceAPI(resource)); | ||
} | ||
}); | ||
update: function(object, qs = {}) { | ||
return k8sRequest(resourcePath, { qs, method: 'PUT', body: object }); | ||
}, | ||
return resolve(api); | ||
})); | ||
}); | ||
} | ||
patch: function(object, qs = {}) { | ||
return k8sRequest(resourcePath, { qs, method: 'PATCH', body: object }); | ||
}, | ||
function _resolveVersion(groupName, versionName) { | ||
const group = apiGroups.groups.find(group => group.name === groupName); | ||
if (!group) { | ||
throw new Error(`No API group ${groupName} available`); | ||
delete: function(qs = {}) { | ||
return k8sRequest(resourcePath, { qs, method: 'DELETE' }); | ||
}, | ||
}; | ||
} | ||
const version = versionName ? group.versions.find(version => version.version === versionName) : group.preferredVersion; | ||
if (!version) { | ||
throw new Error(`No version ${versionName} for API group ${groupName} available`); | ||
function createResourceAPI(resource, pathPrefix = '') { | ||
return { | ||
[resource.name.toLowerCase()]: createResourceCollection(resource, pathPrefix), | ||
[resource.kind.toLowerCase()]: function(name) { | ||
return createResource(resource, name, pathPrefix) | ||
} | ||
} | ||
} | ||
return version; | ||
} | ||
const nsResources = {}; | ||
const api = { | ||
name, | ||
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}`)); | ||
}, {}); | ||
}, | ||
if (err) { | ||
return callback(err, null); | ||
} | ||
// other properties here represent non-namespaced resources | ||
}; | ||
// Initialize the APIs | ||
const apis = {}; | ||
function _loadApi(groupPath, version) { | ||
return _createApi(groupPath, version).then(function(api) { | ||
apis[api.name || ''] = api; | ||
return apiResources.resources.reduce(function(api, resource) { | ||
const slashIndex = resource.name.indexOf('/'); | ||
if (slashIndex !== -1) { | ||
const subResource = resource.name.substring(slashIndex + 1); | ||
switch (subResource) { | ||
// TODO: Apply suitable additional methods on the resource when we understand the subresource. | ||
// TODO: support minimally 'status' and possibly 'proxy', 'exec'. | ||
default: | ||
// A unknown sub-resource, for now just ignore it. | ||
console.log(`Found unknown sub-resource ${subResource}, ignoring (${JSON.stringify(resource)})`); | ||
} | ||
} else if (resource.namespaced) { | ||
nsResources[resource.name] = resource; | ||
} else { | ||
Object.assign(api, createResourceAPI(resource)); | ||
} | ||
return api; | ||
}); | ||
} | ||
}, api); | ||
}); | ||
} | ||
const _createPromises = flatMap(apiGroups.groups, function(group) { | ||
return group.versions.map(version => _loadApi(`apis/${version.groupVersion}`, version)); | ||
const coreVersion = config.version || 'v1'; | ||
return k8sRequest('apis').then(function(apiGroups) { | ||
// Initialize the APIs | ||
const apiPromises = flatMap(apiGroups.groups, function(group) { | ||
return group.versions.map(version => createApi(group.name, `apis/${version.groupVersion}`, version, version.version === group.preferredVersion.version)); | ||
}); | ||
const coreVersion = config.version || 'v1'; | ||
const corePath = `api/${coreVersion}`; | ||
_createPromises.push(_loadApi(corePath, { groupVersion: coreVersion, version: coreVersion })); | ||
apiPromises.push(createApi('', `api/${coreVersion}`, { groupVersion: coreVersion, version: coreVersion }, true)); | ||
return Promise.all(apiPromises); | ||
}).then(function(apis) { | ||
return apis.reduce(function(result, api) { | ||
result[api.name] = result[api.name] || {}; | ||
result[api.name][api.version] = api; | ||
if (api.preferred) { | ||
result[api.name][''] = api; | ||
} | ||
return result; | ||
}, {}) | ||
}).then(function(apis) { | ||
const coreApi = Object.assign({}, apis[''][coreVersion]); | ||
delete coreApi.name; | ||
return Promise.all(_createPromises).then(function() { | ||
const coreApi = Object.assign({}, apis[coreVersion]); | ||
delete coreApi.name; | ||
return Object.assign({}, coreApi, { | ||
group: function(groupName, versionName) { | ||
const apiGroup = apis[groupName]; | ||
if (!apiGroup) { | ||
throw new Error(`No API group ${groupName} available`); | ||
} | ||
return callback(null, Object.assign({}, coreApi, { | ||
_request: k8sRequest, | ||
_apiGroups: apiGroups, | ||
group: function(groupName, versionName) { | ||
const version = _resolveVersion(groupName, versionName); | ||
const api = apiGroup[versionName || '']; | ||
if (!api) { | ||
throw new Error(`No version ${versionName} for API group ${groupName} available`); | ||
} | ||
const api = apis[version.groupVersion]; | ||
if (!api) { | ||
throw new Error(`Unknown group ${groupName}/${versionName}`); | ||
} | ||
return api; | ||
}, | ||
})); | ||
}, function(error) { | ||
return callback(error); | ||
}) | ||
return api; | ||
}, | ||
}); | ||
}); | ||
} |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
28001
9
300
4
4
+ Addedthrough2@^2.0.3
+ Addedcore-util-is@1.0.3(transitive)
+ Addedinherits@2.0.4(transitive)
+ Addedisarray@1.0.0(transitive)
+ Addedprocess-nextick-args@2.0.1(transitive)
+ Addedreadable-stream@2.3.8(transitive)
+ Addedsafe-buffer@5.1.2(transitive)
+ Addedstring_decoder@1.1.1(transitive)
+ Addedthrough2@2.0.5(transitive)
+ Addedutil-deprecate@1.0.2(transitive)
+ Addedxtend@4.0.2(transitive)