-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This connects Xterm.js to a Nomad exec websocket so people can interact on clients via live sessions. There are buttons on job, allocation, task group, and task detail pages that open a popup that lets them edit their shell command and start a session. More is to come, as recorded in issues.
- Loading branch information
Showing
50 changed files
with
1,633 additions
and
74 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import Component from '@ember/component'; | ||
import { FitAddon } from 'xterm-addon-fit'; | ||
import WindowResizable from '../mixins/window-resizable'; | ||
|
||
export default Component.extend(WindowResizable, { | ||
classNames: ['terminal-container'], | ||
|
||
didInsertElement() { | ||
let fitAddon = new FitAddon(); | ||
this.fitAddon = fitAddon; | ||
this.terminal.loadAddon(fitAddon); | ||
|
||
this.terminal.open(this.element.querySelector('.terminal')); | ||
|
||
fitAddon.fit(); | ||
}, | ||
|
||
windowResizeHandler(e) { | ||
this.fitAddon.fit(); | ||
if (this.terminal.resized) { | ||
this.terminal.resized(e); | ||
} | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import Component from '@ember/component'; | ||
import { inject as service } from '@ember/service'; | ||
import generateExecUrl from 'nomad-ui/utils/generate-exec-url'; | ||
import openExecUrl from 'nomad-ui/utils/open-exec-url'; | ||
|
||
export default Component.extend({ | ||
tagName: '', | ||
|
||
router: service(), | ||
|
||
actions: { | ||
open() { | ||
openExecUrl(this.generateUrl()); | ||
}, | ||
}, | ||
|
||
generateUrl() { | ||
let urlSegments = { | ||
job: this.job.get('name'), | ||
}; | ||
|
||
if (this.taskGroup) { | ||
urlSegments.taskGroup = this.taskGroup.get('name'); | ||
} | ||
|
||
if (this.task) { | ||
urlSegments.task = this.task.get('name'); | ||
} | ||
|
||
if (this.allocation) { | ||
urlSegments.allocation = this.allocation.get('shortId'); | ||
} | ||
|
||
return generateExecUrl(this.router, urlSegments); | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import Component from '@ember/component'; | ||
|
||
export default Component.extend({ | ||
tagName: '', | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import Component from '@ember/component'; | ||
import { inject as service } from '@ember/service'; | ||
import { computed } from '@ember/object'; | ||
import { or } from '@ember/object/computed'; | ||
import generateExecUrl from 'nomad-ui/utils/generate-exec-url'; | ||
import openExecUrl from 'nomad-ui/utils/open-exec-url'; | ||
|
||
export default Component.extend({ | ||
router: service(), | ||
|
||
isOpen: or('clickedOpen', 'currentRouteIsThisTaskGroup'), | ||
|
||
currentRouteIsThisTaskGroup: computed('router.currentRoute', function() { | ||
const route = this.router.currentRoute; | ||
|
||
if (route.name.includes('task-group')) { | ||
const taskGroupRoute = route.parent; | ||
const execRoute = taskGroupRoute.parent; | ||
|
||
return ( | ||
execRoute.params.job_name === this.taskGroup.job.name && | ||
taskGroupRoute.params.task_group_name === this.taskGroup.name | ||
); | ||
} else { | ||
return false; | ||
} | ||
}), | ||
|
||
tasksWithRunningStates: computed('taskGroup', function() { | ||
const activeStateTaskNames = this.taskGroup.allocations.reduce( | ||
(activeStateTaskNames, allocation) => { | ||
activeStateTaskNames = activeStateTaskNames.concat( | ||
allocation.states | ||
.filter( | ||
taskState => | ||
taskState.isActive && taskState.task.taskGroup.name === this.taskGroup.name | ||
) | ||
.mapBy('name') | ||
); | ||
|
||
return activeStateTaskNames; | ||
}, | ||
[] | ||
); | ||
|
||
return this.taskGroup.tasks.filter(task => activeStateTaskNames.includes(task.name)); | ||
}), | ||
|
||
clickedOpen: false, | ||
|
||
actions: { | ||
toggleOpen() { | ||
this.toggleProperty('clickedOpen'); | ||
}, | ||
|
||
openInNewWindow(job, taskGroup, task) { | ||
let url = generateExecUrl(this.router, { | ||
job: job.name, | ||
taskGroup: taskGroup.name, | ||
task: task.name, | ||
}); | ||
|
||
openExecUrl(url); | ||
}, | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { inject as service } from '@ember/service'; | ||
import Controller from '@ember/controller'; | ||
import { filterBy, mapBy, uniq } from '@ember/object/computed'; | ||
import escapeTaskName from 'nomad-ui/utils/escape-task-name'; | ||
import ExecCommandEditorXtermAdapter from 'nomad-ui/utils/classes/exec-command-editor-xterm-adapter'; | ||
import ExecSocketXtermAdapter from 'nomad-ui/utils/classes/exec-socket-xterm-adapter'; | ||
|
||
import { Terminal } from 'xterm-vendor'; | ||
|
||
const ANSI_UI_GRAY_400 = '\x1b[38;2;142;150;163m'; | ||
const ANSI_WHITE = '\x1b[0m'; | ||
|
||
export default Controller.extend({ | ||
sockets: service(), | ||
system: service(), | ||
|
||
queryParams: ['allocation'], | ||
|
||
command: '/bin/bash', // Issue to improve: https://github.com/hashicorp/nomad/issues/7469 | ||
socketOpen: false, | ||
taskState: null, | ||
|
||
runningAllocations: filterBy('model.allocations', 'isRunning'), | ||
runningTaskGroups: mapBy('runningAllocations', 'taskGroup'), | ||
uniqueRunningTaskGroups: uniq('runningTaskGroups'), | ||
|
||
init() { | ||
this._super(...arguments); | ||
|
||
this.terminal = new Terminal({ fontFamily: 'monospace', fontWeight: '400' }); | ||
window.execTerminal = this.terminal; // Issue to improve: https://github.com/hashicorp/nomad/issues/7457 | ||
|
||
this.terminal.write(ANSI_UI_GRAY_400); | ||
this.terminal.writeln('Select a task to start your session.'); | ||
}, | ||
|
||
actions: { | ||
setTaskState({ allocationSpecified, taskState }) { | ||
this.set('taskState', taskState); | ||
|
||
this.terminal.write(ANSI_UI_GRAY_400); | ||
this.terminal.writeln(''); | ||
|
||
if (!allocationSpecified) { | ||
this.terminal.writeln( | ||
'Multiple instances of this task are running. The allocation below was selected by random draw.' | ||
); | ||
this.terminal.writeln(''); | ||
} | ||
|
||
this.terminal.writeln('Customize your command, then hit ‘return’ to run.'); | ||
this.terminal.writeln(''); | ||
this.terminal.write( | ||
`$ nomad alloc exec -i -t -task ${escapeTaskName(taskState.name)} ${ | ||
taskState.allocation.shortId | ||
} ` | ||
); | ||
|
||
this.terminal.write(ANSI_WHITE); | ||
|
||
this.terminal.write(this.command); | ||
|
||
if (this.commandEditorAdapter) { | ||
this.commandEditorAdapter.destroy(); | ||
} | ||
|
||
this.commandEditorAdapter = new ExecCommandEditorXtermAdapter( | ||
this.terminal, | ||
this.openAndConnectSocket.bind(this), | ||
this.command | ||
); | ||
}, | ||
}, | ||
|
||
openAndConnectSocket(command) { | ||
this.set('socketOpen', true); | ||
this.socket = this.sockets.getTaskStateSocket(this.taskState, command); | ||
|
||
new ExecSocketXtermAdapter(this.terminal, this.socket); | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { inject as service } from '@ember/service'; | ||
import Route from '@ember/routing/route'; | ||
import notifyError from 'nomad-ui/utils/notify-error'; | ||
|
||
// copied from jobs/job, issue to improve: https://github.com/hashicorp/nomad/issues/7458 | ||
|
||
export default Route.extend({ | ||
store: service(), | ||
token: service(), | ||
|
||
serialize(model) { | ||
return { job_name: model.get('plainId') }; | ||
}, | ||
|
||
model(params, transition) { | ||
const namespace = transition.to.queryParams.namespace || this.get('system.activeNamespace.id'); | ||
const name = params.job_name; | ||
const fullId = JSON.stringify([name, namespace || 'default']); | ||
return this.store | ||
.findRecord('job', fullId, { reload: true }) | ||
.then(job => { | ||
return job.get('allocations').then(() => job); | ||
}) | ||
.catch(notifyError(this)); | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { inject as service } from '@ember/service'; | ||
import Route from '@ember/routing/route'; | ||
|
||
export default Route.extend({ | ||
store: service(), | ||
|
||
model({ task_name }) { | ||
const allocationQueryParam = this.paramsFor('exec').allocation; | ||
|
||
return this.modelFor('exec').allocations.then(allocations => { | ||
let allocation; | ||
|
||
if (allocationQueryParam) { | ||
allocation = allocations.findBy('shortId', allocationQueryParam); | ||
} else { | ||
allocation = allocations.find(allocation => | ||
allocation.states | ||
.filterBy('isActive') | ||
.mapBy('name') | ||
.includes(task_name) | ||
); | ||
} | ||
|
||
return { | ||
allocation, | ||
allocationSpecified: allocationQueryParam ? true : false, | ||
taskState: allocation.states.find(state => state.name === task_name), | ||
}; | ||
}); | ||
}, | ||
|
||
afterModel(model) { | ||
this.controllerFor('exec').send('setTaskState', model); | ||
}, | ||
|
||
setupController(controller, { allocation, taskState }) { | ||
controller.setProperties({ allocation, taskState }); | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import Service from '@ember/service'; | ||
import config from 'nomad-ui/config/environment'; | ||
import { getOwner } from '@ember/application'; | ||
|
||
export default Service.extend({ | ||
getTaskStateSocket(taskState, command) { | ||
const mirageEnabled = | ||
config['ember-cli-mirage'] && config['ember-cli-mirage'].enabled !== false; | ||
|
||
if (mirageEnabled) { | ||
return new Object({ | ||
messageDisplayed: false, | ||
|
||
send(e) { | ||
if (!this.messageDisplayed) { | ||
this.messageDisplayed = true; | ||
this.onmessage({ data: `{"stdout":{"data":"${btoa('unsupported in Mirage\n\r')}"}}` }); | ||
} else { | ||
this.onmessage({ data: e.replace('stdin', 'stdout') }); | ||
} | ||
}, | ||
}); | ||
} else { | ||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | ||
const applicationAdapter = getOwner(this).lookup('adapter:application'); | ||
const prefix = `${applicationAdapter.host || | ||
window.location.host}/${applicationAdapter.urlPrefix()}`; | ||
|
||
return new WebSocket( | ||
`${protocol}//${prefix}/client/allocation/${taskState.allocation.id}` + | ||
`/exec?task=${taskState.name}&tty=true` + | ||
`&command=${encodeURIComponent(`["${command}"]`)}` | ||
); | ||
} | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.