From ed47d43bc5c5da44878d710b05b74721843b62fb Mon Sep 17 00:00:00 2001 From: Daniel Gempesaw Date: Tue, 25 Sep 2018 00:23:59 -0400 Subject: [PATCH] feat(pod-exec): add initial support for command execution (#329) --- examples/pod-exec.js | 39 +++++++++++++++++++++++++++ lib/request.js | 63 ++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 30 +++++++++++++-------- package.json | 4 ++- 4 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 examples/pod-exec.js diff --git a/examples/pod-exec.js b/examples/pod-exec.js new file mode 100644 index 00000000..9df7f5a1 --- /dev/null +++ b/examples/pod-exec.js @@ -0,0 +1,39 @@ +// +// Execute commands non-interactively in a pod +// +const Client = require('kubernetes-client').Client; +const config = require('kubernetes-client').config; + +async function main() { + try { + + const client = new Client({ config: config.fromKubeconfig(), version: '1.9' }); + + // Pod with single container + let res = await client.api.v1.namespaces('namespace_name').pods('pod_name').exec.post({ + qs: { + command: ['ls', '-al'], + stdout: true, + stderr: true + } + }); + console.log(res.body); + console.log(res.messages); + + // Pod with multiple containers /must/ specify a container + res = await client.api.v1.namespaces('namespace_name').pods('pod_name').exec.post({ + qs: { + command: ['ls', '-al'], + container: 'container_name', + stdout: true, + stderr: true + } + }); + console.log(res.body); + + } catch (err) { + console.error('Error: ', err); + } +} + +main(); diff --git a/lib/request.js b/lib/request.js index 9be4335d..63b7733f 100644 --- a/lib/request.js +++ b/lib/request.js @@ -1,6 +1,8 @@ 'use strict'; const request = require('request'); +const qs = require('qs'); +const WebSocket = require('ws'); /** * Refresh whatever authentication {type} is. @@ -23,6 +25,63 @@ function refreshAuth(type, config) { }); } +const execChannels = [ + 'stdin', + 'stdout', + 'stderr', + 'error', + 'resize' +]; + +/** + * Determine whether a failed Kubernetes API response is asking for an upgrade + * @param {object} body - response body object from Kubernetes + * @property {string} status - request status + * @property {number} code - previous request's response code + * @property {message} message - previous request response message + * @returns {boolean} Upgrade the request + */ + +function isUpgradeRequired(body) { + return body.status === 'Failure' + && body.code === 400 + && body.message === 'Upgrade request required'; +} + +/** + * Upgrade a request into a Websocket transaction & process the result + * @param {ApiRequestOptions} options - Options object + * @param {callback} cb - The callback that handles the response + */ + +function upgradeRequest(options, cb) { + const queryParams = qs.stringify(options.qs, { indices: false }); + const wsUrl = `${options.baseUrl}/${options.uri}?${queryParams}`; + const protocol = 'base64.channel.k8s.io'; + const ws = new WebSocket(wsUrl, protocol, options); + + const messages = []; + ws.on('message', (msg) => { + const channel = execChannels[msg.slice(0, 1)]; + const message = Buffer.from(msg.slice(1), 'base64').toString('ascii'); + messages.push({ channel, message }); + }); + + ws.on('error', (err) => { + err.messages = messages; + cb(err, messages); + }); + + ws.on('close', (code, reason) => cb(null, { + messages, + body: messages.map(({ message }) => message).join(''), + code, + reason + })); + + return ws; +} + class Request { /** @@ -92,6 +151,10 @@ class Request { return request(requestOptions, (err, res, body) => { if (err) return cb(err); + if (isUpgradeRequired(body)) { + return upgradeRequest(requestOptions, cb); + } + // Refresh auth if 401 if (res.statusCode === 401 && auth.type) { return refreshAuth(auth.type, auth.config) diff --git a/package-lock.json b/package-lock.json index 75108bbf..6fd344b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,6 @@ "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", "dev": true, - "optional": true, "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -153,6 +152,11 @@ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2012,8 +2016,7 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true, - "optional": true + "dev": true }, "is-builtin-module": { "version": "1.0.0", @@ -2414,7 +2417,6 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, - "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -2591,8 +2593,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true, - "optional": true + "dev": true }, "loud-rejection": { "version": "1.6.0", @@ -3323,9 +3324,9 @@ "dev": true }, "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "query-string": { "version": "5.1.1", @@ -3440,8 +3441,7 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true, - "optional": true + "dev": true }, "repeating": { "version": "2.0.1", @@ -4483,6 +4483,14 @@ "signal-exit": "^3.0.2" } }, + "ws": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.0.0.tgz", + "integrity": "sha512-c2UlYcAZp1VS8AORtpq6y4RJIkJ9dQz18W32SpR/qXGfLDZ2jU4y4wKvvZwqbi7U6gxFQTeE+urMbXU/tsDy4w==", + "requires": { + "async-limiter": "~1.0.0" + } + }, "xdg-basedir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", diff --git a/package.json b/package.json index d49f0b96..9193e3f9 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,9 @@ "fluent-openapi": "0.1.1", "js-yaml": "^3.10.0", "openid-client": "^2.0.0", - "request": "^2.83.0" + "qs": "^6.5.2", + "request": "^2.83.0", + "ws": "^6.0.0" }, "devDependencies": { "@types/node": "^10.3.5",