Comparing version 0.0.5 to 0.0.7
{ | ||
// Use IntelliSense to learn about possible Node.js debug attributes. | ||
// Hover to view descriptions of existing attributes. | ||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 | ||
// Use IntelliSense to find out which attributes exist for node debugging | ||
// Use hover for the description of the existing attributes | ||
// For further information visit https://go.microsoft.com/fwlink/?linkid=830387 | ||
"version": "0.2.0", | ||
"configurations": [ | ||
{ | ||
"type": "chrome", | ||
"name": "Node (launch)", | ||
"type": "node", | ||
"request": "launch", | ||
"name": "Launch Chrome", | ||
"file": "${workspaceRoot}/docs/index.html" | ||
"program": "${workspaceRoot}/index.js", | ||
"cwd": "${workspaceRoot}", | ||
"console": "integratedTerminal" | ||
}, | ||
{ | ||
"name": "Kukebox", | ||
"name": "Node (attach)", | ||
"type": "node", | ||
"request": "attach", | ||
"port": 5858 | ||
}, | ||
{ | ||
"name": "Chrome", | ||
"type": "chrome", | ||
"request": "launch", | ||
"url": "http://localhost:8080/index.html", | ||
"webRoot": "${workspaceRoot}/", | ||
"sourceMaps": true | ||
} | ||
] | ||
} |
@@ -10,4 +10,14 @@ 'use strict'; | ||
this.master_api = master_api; | ||
this.apis = []; | ||
} | ||
get master_api() { | ||
return this._master_api; | ||
} | ||
set master_api(master_api) { | ||
this.apis = []; | ||
this._master_api = master_api; | ||
} | ||
get headers() { | ||
@@ -21,2 +31,6 @@ return this.master_api.headers; | ||
get openshift() { | ||
return this.apis.some(path => path === '/oapi' || path === '/oapi/v1'); | ||
} | ||
get_apis() { | ||
@@ -23,0 +37,0 @@ return Object.assign({ |
'use strict'; | ||
const http = require('http'), | ||
https = require('https'); | ||
https = require('https'), | ||
os = require('os'), | ||
URI = require('urijs'); | ||
module.exports.get = function (options, generator, async = true) { | ||
return generator ? getStream(options, generator, async) : getBody(options); | ||
if (generator) { | ||
if (os.platform() === 'browser' && WebSocket) { | ||
return getWebSocketStream(options, generator, async); | ||
} else { | ||
return getStream(options, generator, async); | ||
} | ||
} else { | ||
return getBody(options); | ||
} | ||
}; | ||
@@ -35,8 +45,68 @@ | ||
function getWebSocketStream(options, generator, async = true) { | ||
let cancellation = Function.prototype; | ||
const promise = new Promise((resolve, reject) => { | ||
const url = new URI(options.path) | ||
.protocol((options.protocol || 'http').startsWith('https') ? 'wss' : 'ws') | ||
.hostname(options.hostname) | ||
.port(options.port); | ||
if (options.headers['Authorization']) { | ||
url.addQuery('access_token', options.headers['Authorization'].substring(7)); | ||
} | ||
const socket = new WebSocket(url.toString(), ['binary.k8s.io']); | ||
socket.binaryType = 'arraybuffer'; | ||
socket.addEventListener('error', event => { | ||
reject(Error(`WebSocket connection failed to ${event.target.url}`)); | ||
}); | ||
socket.addEventListener('open', event => { | ||
let clientAbort; | ||
cancellation = () => { | ||
clientAbort = true; | ||
socket.close(); | ||
}; | ||
const gen = generator(); | ||
gen.next(); | ||
socket.addEventListener('message', event => { | ||
const res = gen.next(new Buffer(event.data, 'binary')); | ||
if (res.done) { | ||
socket.close(); | ||
event.body = res.value; | ||
// ignored for async as it's already been resolved | ||
resolve(event); | ||
} | ||
}); | ||
socket.addEventListener('close', event => { | ||
if (!clientAbort) { | ||
const res = gen.next(); | ||
// the generator may have already returned from the 'data' event | ||
if (!async && !res.done) { | ||
event.body = res.value; | ||
resolve(event); | ||
} | ||
} | ||
// ignored if the generator is done already | ||
gen.return(); | ||
}); | ||
if (async) { | ||
resolve(event); | ||
} | ||
}); | ||
}); | ||
return { promise, cancellation: () => cancellation() }; | ||
} | ||
// TODO: add wrapper method getStreamAsync instead of a boolean flag | ||
function getStream(options, generator, async = true) { | ||
let cancellation = Function.prototype; | ||
const promise = new Promise((resolve, reject) => { | ||
const promise = new Promise((resolve, reject) => { | ||
let clientAbort, serverAbort; | ||
const client = (options.protocol || 'http').startsWith('https') ? https : http; | ||
const client = (options.protocol || 'http').startsWith('https') ? https : http; | ||
const request = client.get(options) | ||
@@ -51,3 +121,3 @@ .on('error', error => { | ||
if (response.statusCode >= 400) { | ||
const error = new Error(`Failed to get resource ${options.path}, status code: ${response.statusCode}`); | ||
const error = new Error(`Failed to get resource ${options.path}, status code: ${response.statusCode}`); | ||
// standard promises don't handle multi-parameters reject callbacks | ||
@@ -110,3 +180,3 @@ error.response = response; | ||
if (response.statusCode !== 101) { | ||
const error = new Error(`Failed to upgrade resource ${options.path}, status code: ${response.statusCode}`); | ||
const error = new Error(`Failed to upgrade resource ${options.path}, status code: ${response.statusCode}`); | ||
// standard promises don't handle multi-parameters reject callbacks | ||
@@ -157,3 +227,3 @@ error.response = response; | ||
}); | ||
return {promise, cancellation: () => cancellation()}; | ||
return { promise, cancellation: () => cancellation() }; | ||
} | ||
@@ -211,3 +281,3 @@ | ||
if (opcode === 0x8) { | ||
return {opcode}; | ||
return { opcode }; | ||
} | ||
@@ -236,3 +306,3 @@ | ||
} | ||
return {FIN, opcode, length, payload}; | ||
return { FIN, opcode, length, payload }; | ||
} |
@@ -6,222 +6,55 @@ 'use strict'; | ||
const blessed = require('blessed'), | ||
Client = require('./client'), | ||
contrib = require('blessed-contrib'), | ||
duration = require('moment-duration-format'), | ||
fs = require('fs'), | ||
get = require('./http-then').get, | ||
moment = require('moment'), | ||
os = require('os'), | ||
path = require('path'), | ||
task = require('./task'), | ||
URI = require('urijs'), | ||
util = require('./util'), | ||
yaml = require('js-yaml'); | ||
const Client = require('./client'), | ||
contrib = require('blessed-contrib'), | ||
get = require('./http-then').get, | ||
os = require('os'), | ||
URI = require('urijs'); | ||
const { call, delay, wait } = require('./promise'); | ||
const KubeConfig = require('./config/manager'); | ||
const login = require('./ui/login'), | ||
const Dashboard = require('./ui/dashboard'), | ||
login = require('./ui/login'), | ||
namespaces = require('./ui/namespaces'); | ||
const { isNotEmpty } = require('./util'); | ||
const { call, wait } = require('./promise'); | ||
class Kubebox { | ||
constructor(screen) { | ||
// TODO: better management of the current Kube config | ||
const session = { | ||
apis : [], | ||
cancellations : new task.Cancellations(), | ||
namespace : null, | ||
pod : null, | ||
pods : {}, | ||
get openshift() { | ||
return this.apis.some(path => path === '/oapi' || path === '/oapi/v1'); | ||
} | ||
}; | ||
let current_namespace; | ||
const { debug, log } = require('./ui/debug'); | ||
const kube_config = os.platform() !== 'browser' | ||
? getKubeConfig(process.argv[2] || process.env.KUBERNETES_MASTER) | ||
: []; | ||
const kube_config = new KubeConfig({ debug }); | ||
const client = new Client(); | ||
if (kube_config.length) { | ||
client.master_api = getMasterApi(kube_config[0]); | ||
session.namespace = kube_config[0].context.namespace; | ||
} | ||
client.master_api = kube_config.current_context.getMasterApi(); | ||
current_namespace = kube_config.current_context.namespace.name; | ||
screen.key(['l', 'C-l'], (ch, key) => reauthenticate() | ||
.then(dashboard) | ||
.catch(error => console.error(error.stack)) | ||
); | ||
const dashboard = new Dashboard(screen, client, debug); | ||
const grid = new contrib.grid({ rows: 12, cols: 12, screen: screen }); | ||
const pods_table = grid.set(0, 0, 6, 6, blessed.listtable, { | ||
border : 'line', | ||
align : 'left', | ||
keys : true, | ||
tags : true, | ||
shrink : false, | ||
noCellBorders : true, | ||
// FIXME: margin isn't incremented for child list in scrollable list table | ||
scrollbar : { | ||
ch : ' ', | ||
style : { bg: 'white' }, | ||
track : { | ||
style : { bg: 'black' } | ||
} | ||
}, | ||
style : { | ||
border : { fg: 'white' }, | ||
header : { fg: 'blue', bold: true }, | ||
cell : { fg: 'white', selected: { bg: 'blue' } } | ||
} | ||
}); | ||
pods_table.on('select', (item, i) => { | ||
// empty table! | ||
if (i === 0) return; | ||
// FIXME: logs resources are not available for pods in non running state | ||
const name = session.pods.items[i - 1].metadata.name; | ||
if (name === session.pod) | ||
return; | ||
session.cancellations.run('dashboard.logs'); | ||
session.pod = name; | ||
// just to update the table with the new selection | ||
setTableData(session.pods); | ||
// and reset the logs widget label until the log request succeeds | ||
pod_log.setLabel('Logs'); | ||
pod_log.logLines = []; | ||
pod_log.setItems([]); | ||
screen.render(); | ||
const logger = function*(sinceTime) { | ||
let log, timestamp; | ||
try { | ||
while (log = yield) { | ||
// skip empty data frame payload on connect! | ||
if (log.length === 0) continue; | ||
log = log.toString('utf8'); | ||
const i = log.indexOf(' '); | ||
timestamp = log.substring(0, i); | ||
const msg = log.substring(i + 1); | ||
// avoid scanning the whole buffer if the timestamp differs from the since time | ||
if (!timestamp.startsWith(sinceTime) || !pod_log.logLines.includes(msg)) | ||
pod_log.log(msg); | ||
} | ||
} catch (e) { | ||
// HTTP chunked transfer-encoding / streaming requests abort on timeout instead of being ended. | ||
// WebSocket upgraded requests end when timed out on OpenShift. | ||
} | ||
// wait 1s and retry the pod log follow request from the latest timestamp if any | ||
delay(1000) | ||
.then(() => get(client.get_pod(session.namespace, name))) | ||
.then(response => JSON.parse(response.body.toString('utf8'))) | ||
.then(pod => { | ||
// TODO: checks should be done at the level of the container (like CrashLoopBackOff) | ||
// check if the pod is not terminated (otherwise the connection closes) | ||
if (pod.status.phase !== 'Running') return; | ||
// check if the pod is not terminating | ||
if (pod.metadata.deletionTimestamp) { | ||
pod_log.setLabel(`Logs {grey-fg}[${name}]{/grey-fg} {red-fg}TERMINATING{/red-fg}`); | ||
} else { | ||
// TODO: max number of retries window | ||
// re-follow log from the latest timestamp received | ||
const { promise, cancellation } = get(client.follow_log(session.namespace, name, timestamp), timestamp | ||
? function*() { | ||
// sub-second info from the 'sinceTime' parameter are not taken into account | ||
// so just strip the info and add a 'startsWith' check to avoid duplicates | ||
yield* logger(timestamp.substring(0, timestamp.indexOf('.'))); | ||
} | ||
: logger); | ||
session.cancellations.add('dashboard.logs', cancellation); | ||
return promise.then(() => debug.log(`Following log for pod ${session.pod} ...`)); | ||
} | ||
}) | ||
.catch(error => { | ||
// the pod might have already been deleted? | ||
if (!error.response || error.response.statusCode !== 404) | ||
console.error(error.stack); | ||
}); | ||
}; | ||
// FIXME: deal with multi-containers pod | ||
const { promise, cancellation } = get(client.follow_log(session.namespace, name), logger); | ||
session.cancellations.add('dashboard.logs', cancellation); | ||
promise | ||
.then(() => debug.log(`Following log for pod ${session.pod} ...`)) | ||
.then(() => pod_log.setLabel(`Logs {grey-fg}[${name}]{/grey-fg}`)) | ||
.then(() => screen.render()) | ||
.catch(error => console.error(error.stack)); | ||
}); | ||
// work-around for https://github.com/chjj/blessed/issues/175 | ||
pods_table.on('remove', () => pods_table.removeLabel()); | ||
pods_table.on('prerender', () => pods_table.setLabel('Pods')); | ||
function setTableData(pods) { | ||
const selected = pods_table.selected; | ||
pods_table.setData(pods.items.reduce((data, pod) => { | ||
data.push([ | ||
pod.metadata.name === session.pod ? `{blue-fg}${pod.metadata.name}{/blue-fg}` : pod.metadata.name, | ||
// TODO: be more fine grained for the status | ||
// TODO: add a visual hint depending on the status | ||
pod.status.phase, | ||
// FIXME: negative duration is displayed when pod starts as clocks may not be synced | ||
util.formatDuration(moment.duration(moment().diff(moment(pod.status.startTime)))) | ||
]); | ||
return data; | ||
}, [['NAME', 'STATUS', 'AGE']])); | ||
pods_table.select(selected); | ||
} | ||
// TODO: enable user scrolling | ||
const pod_log = grid.set(6, 0, 6, 12, contrib.log, { | ||
border : 'line', | ||
align : 'left', | ||
label : 'Logs', | ||
tags : true, | ||
style : { | ||
border : { fg: 'white' } | ||
}, | ||
bufferLength: 50 | ||
}); | ||
// TODO: enable user scrolling and add timestamps | ||
const debug = grid.set(0, 0, 12, 12, contrib.log, { | ||
label : 'Logs', | ||
style : { | ||
fg : 'white', | ||
border : { fg: 'white' } | ||
}, | ||
bufferLength: 100 | ||
}); | ||
const log = message => new Promise(resolve => { | ||
debug.log(message); | ||
resolve(); | ||
}); | ||
// FIXME: the namespace selection handle should only be active | ||
// when the connection is established to the cluster | ||
screen.key(['n'], () => { | ||
namespaces.prompt(screen, session, client) | ||
namespaces.prompt(screen, client, { current_namespace }) | ||
.then(namespace => { | ||
if (namespace === session.namespace) | ||
return; | ||
resetDashboard(); | ||
if (namespace === current_namespace) return; | ||
dashboard.reset(); | ||
// switch dashboard to new namespace | ||
session.namespace = namespace; | ||
session.pod = null; | ||
debug.log(`Switching to namespace ${session.namespace}`); | ||
current_namespace = namespace; | ||
debug.log(`Switching to namespace ${current_namespace}`); | ||
screen.render(); | ||
return dashboard().catch(error => console.error(error.stack)); | ||
}); | ||
return dashboard.run(current_namespace); | ||
}) | ||
.catch(error => console.error(error.stack)); | ||
}); | ||
screen.key(['l', 'C-l'], (ch, key) => | ||
logging().catch(error => console.error(error.stack)) | ||
); | ||
const carousel = new contrib.carousel( | ||
[ | ||
screen => { | ||
// TODO: restore selection if any | ||
screen.append(pods_table); | ||
screen.append(pod_log); | ||
pod_log.setScrollPerc(100); | ||
pods_table.focus(); | ||
dashboard.render(); | ||
}, | ||
@@ -241,9 +74,7 @@ screen => { | ||
if (client.master_api) { | ||
// TODO: display login prompt with message on error | ||
if (isNotEmpty(client.master_api)) { | ||
connect().catch(error => console.error(error.stack)); | ||
} else { | ||
login.prompt(screen, kube_config) | ||
.then(updateSessionAfterLogin) | ||
.then(connect) | ||
.catch(error => console.error(error.stack)); | ||
logging().catch(error => console.error(error.stack)); | ||
} | ||
@@ -254,42 +85,47 @@ | ||
return get(client.get_apis()) | ||
.then(response => session.apis = JSON.parse(response.body.toString('utf8')).paths) | ||
.then(response => client.apis = JSON.parse(response.body.toString('utf8')).paths) | ||
.catch(error => debug.log(`Unable to retrieve available APIs: ${error.message}`)) | ||
.then(dashboard) | ||
.then(_ => current_namespace | ||
? Promise.resolve(current_namespace) | ||
: namespaces.prompt(screen, client, { promptAfterRequest : true }) | ||
.then(namespace => current_namespace = namespace)) | ||
.then(dashboard.run) | ||
.catch(error => error.response && [401, 403].includes(error.response.statusCode) | ||
? log(`Authentication required for ${client.url} (openshift)`) | ||
.then(_ => authenticate(login)) | ||
.then(_ => connect()) | ||
.then(_ => login ? authenticate(login).then(_ => connect()) : logging()) | ||
: Promise.reject(error)); | ||
} | ||
function authenticate(credentials) { | ||
if (!session.openshift) | ||
throw Error(`No authentication available for: ${client.url}`); | ||
function logging() { | ||
return login.prompt(screen, kube_config) | ||
// it may be better to reset the dashboard when authentication has succeeded | ||
.then(call(dashboard.reset)) | ||
.then(updateSessionAfterLogin) | ||
.then(connect); | ||
} | ||
// TODO: display an error message in the login prompt when authentication has failed | ||
return (credentials ? Promise.resolve(credentials) | ||
// TODO: it may be better to reset the dashboard when authentication has succeeded | ||
: login.prompt(screen, kube_config).then(call(resetDashboard))) | ||
.then(updateSessionAfterLogin) | ||
.then(credentials => util.isEmpty(credentials.token) | ||
// try retrieving an OAuth access token from the OpenShift OAuth server | ||
? os.platform() === 'browser' | ||
? get(client.oauth_authorize_web(credentials)) | ||
.then(response => { | ||
const path = URI.parse(response.url).path; | ||
if (response.statusCode === 200 && path === '/oauth/token/display') { | ||
return response.body.toString('utf8').match(/<code>(.*)<\/code>/)[1]; | ||
} else if (path === '/login') { | ||
const error = Error('Authentication failed!'); | ||
// fake authentication error to emulate the implicit grant flow | ||
response.statusCode = 401; | ||
error.response = response; | ||
throw error; | ||
} else { | ||
throw Error('Unsupported authentication!'); | ||
} | ||
}) | ||
: get(client.oauth_authorize(credentials)) | ||
.then(response => response.headers.location.match(/access_token=([^&]+)/)[1]) | ||
: credentials.token) | ||
function authenticate(login) { | ||
if (!client.openshift) | ||
return Promise.reject(Error(`No authentication available for: ${client.url}`)); | ||
return (isNotEmpty(login.token) ? Promise.resolve(login.token) | ||
// try retrieving an OAuth access token from the OpenShift OAuth server | ||
: os.platform() === 'browser' | ||
? get(client.oauth_authorize_web(login)) | ||
.then(response => { | ||
const path = URI.parse(response.url).path; | ||
if (response.statusCode === 200 && path === '/oauth/token/display') { | ||
return response.body.toString('utf8').match(/<code>(.*)<\/code>/)[1]; | ||
} else if (path === '/login') { | ||
const error = Error('Authentication failed!'); | ||
// fake authentication error to emulate the implicit grant flow | ||
response.statusCode = 401; | ||
error.response = response; | ||
throw error; | ||
} else { | ||
throw Error('Unsupported authentication!'); | ||
} | ||
}) | ||
: get(client.oauth_authorize(login)) | ||
.then(response => response.headers.location.match(/access_token=([^&]+)/)[1])) | ||
// test it authenticates ok | ||
@@ -306,107 +142,10 @@ .then(token => get(client.get_user(token)) | ||
.then(wait(1000)) | ||
.then(reauthenticate) | ||
.then(logging) | ||
: Promise.reject(error)); | ||
} | ||
function reauthenticate() { | ||
delete session.namespace; | ||
return authenticate(); | ||
} | ||
function dashboard() { | ||
return (session.namespace ? Promise.resolve(session.namespace) | ||
: namespaces.prompt(screen, session, client, { promptAfterRequest : true }) | ||
.then(namespace => session.namespace = namespace)) | ||
.then(_ => get(client.get_pods(session.namespace))) | ||
.then(response => { | ||
session.pods = JSON.parse(response.body.toString('utf8')); | ||
session.pods.items = session.pods.items || []; | ||
}) | ||
.then(() => setTableData(session.pods)) | ||
.then(() => debug.log(`Watching for pods changes in namespace ${session.namespace} ...`)) | ||
.then(() => screen.render()) | ||
.then(() => { | ||
const id = setInterval(refreshPodAges, 1000); | ||
session.cancellations.add('dashboard.refreshPodAges', () => clearInterval(id)); | ||
}) | ||
.then(() => { | ||
const { promise, cancellation } = get(client.watch_pods(session.namespace,session.pods.metadata.resourceVersion), updatePodTable); | ||
session.cancellations.add('dashboard', cancellation); | ||
return promise; | ||
}); | ||
} | ||
function resetDashboard() { | ||
// cancel current running tasks and open requests | ||
session.cancellations.run('dashboard'); | ||
// reset dashboard widgets | ||
pods_table.clearItems(); | ||
pod_log.setLabel('Logs'); | ||
pod_log.logLines = []; | ||
pod_log.setItems([]); | ||
screen.render(); | ||
} | ||
function* updatePodTable() { | ||
const index = object => session.pods.items.findIndex(pod => pod.metadata.uid === object.metadata.uid); | ||
let change; | ||
try { | ||
while (change = yield) { | ||
change = JSON.parse(change); | ||
switch (change.type) { | ||
case 'ADDED': | ||
session.pods.items.push(change.object); | ||
break; | ||
case 'MODIFIED': | ||
session.pods.items[index(change.object)] = change.object; | ||
break; | ||
case 'DELETED': | ||
session.pods.items.splice(index(change.object), 1); | ||
if (change.object.metadata.name === session.pod) { | ||
// check if that's the selected pod and clean the selection | ||
pod_log.setLabel(`Logs {grey-fg}[${session.pod}]{/grey-fg} {red-fg}DELETED{/red-fg}`); | ||
session.pod = null; | ||
} | ||
break; | ||
} | ||
setTableData(session.pods); | ||
screen.render(); | ||
} | ||
} catch (e) { | ||
// HTTP chunked transfer-encoding / streaming watch requests abort on timeout when the 'timeoutSeconds' | ||
// request parameter is greater than the '--min-request-timeout' server API option, | ||
// otherwise the connections just end normally (http://kubernetes.io/docs/admin/kube-apiserver/). | ||
// WebSocket upgraded watch requests (idle?) end when timed out on Kubernetes. | ||
} | ||
// retry the pods list watch request | ||
session.cancellations.run('dashboard.refreshPodAges'); | ||
dashboard().catch(error => console.error(error.stack)); | ||
} | ||
function refreshPodAges() { | ||
session.pods.items.forEach(pod => moment(pod.status.startTime).add(1, 's').toISOString()); | ||
// we may want to avoid recreating the whole table data | ||
setTableData(session.pods); | ||
screen.render(); | ||
} | ||
function updateSessionAfterLogin(login) { | ||
const config = findClusterByUrl(kube_config, login.cluster); | ||
if (!config) { | ||
client.master_api = getBaseMasterApi(login.cluster); | ||
} else { | ||
// override token, server URL and user but keep the rest of the config | ||
if (!util.isEmpty(login.token)) { | ||
config.user.token = login.token; | ||
} | ||
config.cluster.server = login.cluster; | ||
// use new master_api | ||
const master_api = getMasterApi(config); | ||
client.master_api = master_api; | ||
// TODO: if only the token is defined, get username from token and replace here | ||
if (!util.isEmpty(login.username)) { | ||
config.context.user = login.username + '/' + master_api.hostname + ':' + master_api.port; | ||
} | ||
session.namespace = config.context.namespace; | ||
} | ||
kube_config.updateOrInsertContext(login); | ||
client.master_api = kube_config.current_context.getMasterApi(); | ||
current_namespace = kube_config.current_context.namespace.name; | ||
return login; | ||
@@ -417,109 +156,2 @@ } | ||
// TODO: support client access information provided as CLI options | ||
// CLI option -> Kube config context -> prompt user | ||
// TODO: better context disambiguation workflow | ||
// see: | ||
// - http://kubernetes.io/docs/user-guide/accessing-the-cluster/ | ||
// - http://kubernetes.io/docs/user-guide/kubeconfig-file/ | ||
function getKubeConfig(master) { | ||
// TODO: check if the file exists and can be read first | ||
const kube = yaml.safeLoad(fs.readFileSync(path.join(os.homedir(), '.kube/config'), 'utf8')); | ||
const configs = []; | ||
if (!master) { | ||
const current = kube['current-context']; | ||
if (current) { | ||
const context = kube.contexts.find(item => item.name === current).context; | ||
configs.push({ | ||
context : context, | ||
cluster : kube.clusters.find(item => item.name === context.cluster).cluster | ||
}); | ||
} | ||
// TODO: better deal with the case no current context is set | ||
// - in case the master passed as argument matches the current context | ||
} else { | ||
const cluster = findClusterByUrl(kube.clusters, master); | ||
if (cluster) { | ||
configs.push({ | ||
// FIXME: there can be multiple contexts for the same cluster | ||
// that could be disambiguated in updateSessionAfterLogin | ||
context : (kube.contexts.find(item => item.context.cluster === cluster.name) || {}).context || {}, | ||
cluster : cluster.cluster | ||
}); | ||
} else { | ||
configs.push({ | ||
context : {}, | ||
cluster : { server: master } | ||
}); | ||
} | ||
} | ||
kube.clusters.filter(cluster => cluster.cluster !== configs[0].cluster) | ||
.forEach(cluster => configs.push({ | ||
cluster : cluster.cluster, | ||
context : (kube.contexts.find(item => item.context.cluster === cluster.name) || {}).context || {}, | ||
})); | ||
configs.forEach(config => config.user = (kube.users.find(user => user.name === config.context.user) || {}).user || {}); | ||
return configs; | ||
} | ||
function findClusterByUrl(clusters, master) { | ||
const uri = URI(master); | ||
let matches = clusters.filter(item => URI(item.cluster.server).hostname() === uri.hostname()); | ||
if (matches.length > 1) { | ||
matches = matches.filter(item => { | ||
const server = URI(item.cluster.server); | ||
return server.protocol() === uri.protocol() && server.port() === uri.port(); | ||
}); | ||
} | ||
if (matches.length > 1) | ||
throw Error(`Multiple clusters found for server: ${master}!`); | ||
return matches.length === 1 ? matches[0] : null; | ||
} | ||
function getMasterApi({ cluster, user }) { | ||
const api = getBaseMasterApi(cluster.server); | ||
if (user['client-certificate']) { | ||
api.cert = fs.readFileSync(user['client-certificate']); | ||
} | ||
if (user['client-certificate-data']) { | ||
api.cert = Buffer.from(user['client-certificate-data'], 'base64'); | ||
} | ||
if (user['client-key']) { | ||
api.key = fs.readFileSync(user['client-key']); | ||
} | ||
if (user['client-key-data']) { | ||
api.key = Buffer.from(user['client-key-data'], 'base64'); | ||
} | ||
if (user.token) { | ||
api.headers['Authorization'] = `Bearer ${user.token}`; | ||
} | ||
if (cluster['insecure-skip-tls-verify']) { | ||
api.rejectUnauthorized = false; | ||
} | ||
if (cluster['certificate-authority']) { | ||
api.ca = fs.readFileSync(cluster['certificate-authority']); | ||
} | ||
if (cluster['certificate-authority-data']) { | ||
api.ca = Buffer.from(cluster['certificate-authority-data'], 'base64'); | ||
} | ||
return api; | ||
} | ||
function getBaseMasterApi(url) { | ||
const { protocol, hostname, port } = URI.parse(url); | ||
const api = { | ||
protocol : protocol + ':', hostname, port, | ||
headers : { | ||
'Accept' : 'application/json, text/plain, */*' | ||
}, | ||
get url() { | ||
return this.protocol + '//' + this.hostname + (this.port ? ':' + this.port : ''); | ||
} | ||
} | ||
return api; | ||
} | ||
module.exports = Kubebox; |
@@ -6,4 +6,6 @@ 'use strict'; | ||
function login_form(kube_config) { | ||
function login_form(kube_config, screen) { | ||
const form = blessed.form({ | ||
parent : screen, | ||
name : 'form', | ||
keys : true, | ||
@@ -42,4 +44,5 @@ mouse : true, | ||
const cluster = blessed.textbox({ | ||
const url = blessed.textbox({ | ||
parent : form, | ||
name : 'url', | ||
inputOnFocus : true, | ||
@@ -52,7 +55,6 @@ mouse : true, | ||
top : 0, | ||
// FIXME: use the current config | ||
value : kube_config.length ? kube_config[0].cluster.server : '' | ||
value : kube_config.current_context.cluster.server | ||
}); | ||
// retain key grabbing as text areas reset it after input reading | ||
cluster.on('blur', () => form.screen.grabKeys = true); | ||
url.on('blur', () => form.screen.grabKeys = true); | ||
@@ -69,2 +71,3 @@ blessed.text({ | ||
parent : form, | ||
name : 'username', | ||
inputOnFocus : true, | ||
@@ -77,4 +80,3 @@ mouse : true, | ||
top : 2, | ||
// FIXME: use the current config | ||
value : kube_config.length ? kube_config[0].context.user.split('/')[0] : '' | ||
value : kube_config.current_context.user.username | ||
}); | ||
@@ -86,4 +88,2 @@ // retain key grabbing as text areas reset it after input reading | ||
parent : form, | ||
mouse : true, | ||
keys : true, | ||
left : 1, | ||
@@ -97,2 +97,3 @@ top : 3, | ||
parent : form, | ||
name : 'password', | ||
inputOnFocus : true, | ||
@@ -112,4 +113,2 @@ mouse : true, | ||
parent : form, | ||
mouse : true, | ||
keys : true, | ||
left : 1, | ||
@@ -123,2 +122,3 @@ top : 4, | ||
parent : form, | ||
name : 'token', | ||
inputOnFocus : true, | ||
@@ -131,4 +131,3 @@ mouse : true, | ||
top : 4, | ||
// FIXME: use the current config | ||
value : kube_config.length ? kube_config[0].user.token : '' | ||
value : kube_config.current_context.user.token | ||
}); | ||
@@ -158,2 +157,16 @@ // retain key grabbing as text areas reset it after input reading | ||
// Reset the focus stack when clicking on a form element | ||
const focusOnclick = element => element.on('click', () => form._selected = element); | ||
focusOnclick(username); | ||
focusOnclick(password); | ||
focusOnclick(token); | ||
focusOnclick(url); | ||
// This is a hack to not 'rewind' the focus stack on 'blur' | ||
username.options.inputOnFocus = false; | ||
password.options.inputOnFocus = false; | ||
token.options.inputOnFocus = false; | ||
url.options.inputOnFocus = false; | ||
return { | ||
@@ -164,3 +177,3 @@ form, | ||
token : () => token.value, | ||
cluster : () => cluster.value | ||
url : () => url.value | ||
}; | ||
@@ -173,3 +186,3 @@ } | ||
screen.grabKeys = true; | ||
const { form, username, password, token, cluster } = login_form(kube_config); | ||
const { form, username, password, token, url } = login_form(kube_config, screen); | ||
screen.append(form); | ||
@@ -185,3 +198,3 @@ form.focusNext(); | ||
fulfill({ | ||
cluster : cluster(), | ||
url : url(), | ||
username : username(), | ||
@@ -188,0 +201,0 @@ password : password(), |
@@ -17,2 +17,3 @@ 'use strict'; | ||
tags : true, | ||
mouse : true, | ||
border : { type: 'line' }, | ||
@@ -36,3 +37,3 @@ scrollbar : { | ||
function prompt(screen, session, client, { promptAfterRequest } = { promptAfterRequest : false }) { | ||
function prompt(screen, client, { current_namespace, promptAfterRequest } = { promptAfterRequest : false }) { | ||
return new Promise(function(fulfill, reject) { | ||
@@ -42,5 +43,5 @@ const list = namespaces_list(); | ||
// TODO: watch for namespace changes when the selection list is open | ||
// TODO: watch for namespaces changes when the selection list is open | ||
function request_namespaces() { | ||
return get(session.openshift ? client.get_projects() : client.get_namespaces()) | ||
return get(client.openshift ? client.get_projects() : client.get_namespaces()) | ||
.then(response => JSON.parse(response.body.toString('utf8'))) | ||
@@ -50,3 +51,3 @@ // TODO: display a message in case the user has access to no namespaces | ||
.then(namespaces => list.setItems(namespaces.items.reduce((data, namespace) => { | ||
data.push(namespace.metadata.name === session.namespace | ||
data.push(namespace.metadata.name === current_namespace | ||
? `{blue-fg}${namespace.metadata.name}{/blue-fg}` | ||
@@ -89,3 +90,3 @@ : namespace.metadata.name); | ||
// Force the user to select a namespace | ||
if (item || session.namespace) { | ||
if (item || current_namespace) { | ||
close_namespaces_list(); | ||
@@ -96,4 +97,4 @@ } | ||
list.on('cancel', () => { | ||
if (session.namespace) { | ||
fulfill(session.namespace); | ||
if (current_namespace) { | ||
fulfill(current_namespace); | ||
} | ||
@@ -100,0 +101,0 @@ }); |
@@ -5,3 +5,5 @@ 'use strict'; | ||
module.exports.formatDuration = function(duration) { | ||
module.exports.isNotEmpty = str => str && str.length > 0; | ||
module.exports.formatDuration = function (duration) { | ||
if (duration.years() > 0) | ||
@@ -8,0 +10,0 @@ return duration.format('y[y] M[M]'); |
@@ -5,3 +5,3 @@ { | ||
"author": "Antonin Stefanutti", | ||
"version": "0.0.5", | ||
"version": "0.0.7", | ||
"license": "MIT", | ||
@@ -11,8 +11,14 @@ "homepage": "https://github.com/astefanutti/kubebox", | ||
"bin": { | ||
"kubebox": "./bin/kubebox" | ||
"kubebox": "index.js" | ||
}, | ||
"scripts": { | ||
"start": "./bin/kubebox" | ||
"start": "node index.js", | ||
"browserify": "browserify -r ./lib/kubebox.js:kubebox -r blessed -i pty.js -i term.js -i map-canvas -i marked-terminal -i picture-tube -o docs/kubebox.js", | ||
"browserify-debug": "browserify --debug -r ./lib/kubebox.js:kubebox -r blessed -i pty.js -i term.js -i map-canvas -i marked-terminal -i picture-tube | exorcist docs/kubebox.js.map > docs/kubebox.js", | ||
"bundle": "browserify index.js -o bundle.js -i pty.js -i term.js -i map-canvas -i marked-terminal -i picture-tube --bare", | ||
"executable": "nexe package.json" | ||
}, | ||
"preferGlobal": true, | ||
"pkg": { | ||
"scripts": "node_modules/blessed/lib/widgets/*.js" | ||
}, | ||
"repository": { | ||
@@ -23,4 +29,6 @@ "type": "git", | ||
"devDependencies": { | ||
"xterm": "~2.8.1", | ||
"browserify": "~14.4.0" | ||
"browserify": "~14.4.0", | ||
"exorcist": "^0.4.0", | ||
"nexe": "~1.1.3", | ||
"xterm": "~2.9.0" | ||
}, | ||
@@ -37,3 +45,27 @@ "dependencies": { | ||
"node": ">=6.0.0" | ||
}, | ||
"nexe": { | ||
"input": "./index.js", | ||
"output": "kubebox^$", | ||
"temp": ".nexe", | ||
"browserify": { | ||
"requires": [], | ||
"excludes": [ | ||
"pty.js", | ||
"term.js", | ||
"map-canvas", | ||
"marked-terminal", | ||
"picture-tube" | ||
], | ||
"paths": [] | ||
}, | ||
"runtime": { | ||
"nodeConfigureOpts": [ | ||
"--fully-static" | ||
], | ||
"framework": "node", | ||
"version": "7.0.0", | ||
"ignoreFlags": true | ||
} | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
54015
21
1438
135
2
4
3