mirror of
https://github.com/cuigh/swirl
synced 2024-12-28 14:51:57 +00:00
Support executing command in containers
This feature is only valid on single node since container API is not Swarmable
This commit is contained in:
parent
e025057893
commit
999b9ec5a1
28
Gopkg.lock
generated
28
Gopkg.lock
generated
@ -138,6 +138,32 @@
|
||||
revision = "bb7a9ca6e4fbc2129e3db588a34bc970ffe811a9"
|
||||
version = "v2.5.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/gobwas/httphead"
|
||||
packages = ["."]
|
||||
revision = "2c6c146eadee0b69f856f87e3e9f1d0cd6aad2f5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/gobwas/pool"
|
||||
packages = [
|
||||
".",
|
||||
"internal/pmath",
|
||||
"pbufio",
|
||||
"pbytes"
|
||||
]
|
||||
revision = "fa3125c39d7eca32e1387bb69b1b38dcb31b1e0b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/gobwas/ws"
|
||||
packages = [
|
||||
".",
|
||||
"wsutil"
|
||||
]
|
||||
revision = "b93773f50025fc1c14bbd7e97a3b170aae9a0977"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
packages = ["proto"]
|
||||
@ -236,6 +262,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "fe9c254e0c7e739370b6380ed53816426b8245a1610db2cfc612aec63aa01701"
|
||||
inputs-digest = "d650099fe870caa619261ca2780130300821767813a6fb79199aaae1cc6e69f1"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
@ -1,5 +1,9 @@
|
||||
required = ["github.com/docker/distribution"]
|
||||
|
||||
[[override]]
|
||||
branch = "master"
|
||||
name = "github.com/gobwas/pool"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/cuigh/auxo"
|
||||
|
@ -2837,45 +2837,64 @@ var Swirl;
|
||||
})(Swirl || (Swirl = {}));
|
||||
var Swirl;
|
||||
(function (Swirl) {
|
||||
var Service;
|
||||
(function (Service) {
|
||||
class LogsPage {
|
||||
var Container;
|
||||
(function (Container) {
|
||||
class ExecPage {
|
||||
constructor() {
|
||||
this.refreshInterval = 3000;
|
||||
this.$line = $("#txt-line");
|
||||
this.$timestamps = $("#cb-timestamps");
|
||||
this.$refresh = $("#cb-refresh");
|
||||
this.$stdout = $("#txt-stdout");
|
||||
this.$stderr = $("#txt-stderr");
|
||||
this.$refresh.change(e => {
|
||||
let elem = (e.target);
|
||||
if (elem.checked) {
|
||||
this.refreshData();
|
||||
}
|
||||
else if (this.timer > 0) {
|
||||
window.clearTimeout(this.timer);
|
||||
this.timer = 0;
|
||||
}
|
||||
});
|
||||
this.refreshData();
|
||||
this.$cmd = $("#txt-cmd");
|
||||
this.$connect = $("#btn-connect");
|
||||
this.$disconnect = $("#btn-disconnect");
|
||||
this.$connect.click(this.connect.bind(this));
|
||||
this.$disconnect.click(this.disconnect.bind(this));
|
||||
Terminal.applyAddon(fit);
|
||||
}
|
||||
refreshData() {
|
||||
let args = {
|
||||
line: this.$line.val(),
|
||||
timestamps: this.$timestamps.prop("checked"),
|
||||
connect(e) {
|
||||
this.$connect.hide();
|
||||
this.$disconnect.show();
|
||||
let url = location.host + location.pathname.substring(0, location.pathname.lastIndexOf("/")) + "/connect?cmd=" + encodeURIComponent(this.$cmd.val());
|
||||
let ws = new WebSocket("ws://" + url);
|
||||
ws.onopen = () => {
|
||||
this.term = new Terminal();
|
||||
this.term.on('data', (data) => {
|
||||
if (ws.readyState == WebSocket.OPEN) {
|
||||
ws.send(data);
|
||||
}
|
||||
});
|
||||
this.term.open(document.getElementById('terminal-container'));
|
||||
this.term.focus();
|
||||
let width = Math.floor(($('#terminal-container').width() - 20) / 8.39);
|
||||
let height = 30;
|
||||
this.term.resize(width, height);
|
||||
this.term.setOption('cursorBlink', true);
|
||||
this.term.fit();
|
||||
window.onresize = () => {
|
||||
this.term.fit();
|
||||
};
|
||||
ws.onmessage = (e) => {
|
||||
this.term.write(e.data);
|
||||
};
|
||||
ws.onerror = function (error) {
|
||||
console.log("error: " + error);
|
||||
};
|
||||
ws.onclose = () => {
|
||||
console.log("close");
|
||||
};
|
||||
};
|
||||
$ajax.get('fetch_logs', args).json((r) => {
|
||||
this.$stdout.val(r.stdout);
|
||||
this.$stderr.val(r.stderr);
|
||||
this.$stdout.get(0).scrollTop = this.$stdout.get(0).scrollHeight;
|
||||
this.$stderr.get(0).scrollTop = this.$stderr.get(0).scrollHeight;
|
||||
});
|
||||
if (this.$refresh.prop("checked")) {
|
||||
this.timer = setTimeout(this.refreshData.bind(this), this.refreshInterval);
|
||||
this.ws = ws;
|
||||
}
|
||||
disconnect(e) {
|
||||
if (this.ws && this.ws.readyState != WebSocket.CLOSED) {
|
||||
this.ws.close();
|
||||
}
|
||||
if (this.term) {
|
||||
this.term.destroy();
|
||||
this.term = null;
|
||||
}
|
||||
this.$connect.show();
|
||||
this.$disconnect.hide();
|
||||
}
|
||||
}
|
||||
Service.LogsPage = LogsPage;
|
||||
})(Service = Swirl.Service || (Swirl.Service = {}));
|
||||
Container.ExecPage = ExecPage;
|
||||
})(Container = Swirl.Container || (Swirl.Container = {}));
|
||||
})(Swirl || (Swirl = {}));
|
||||
//# sourceMappingURL=swirl.js.map
|
File diff suppressed because one or more lines are too long
70
assets/swirl/ts/container/exec.ts
Normal file
70
assets/swirl/ts/container/exec.ts
Normal file
@ -0,0 +1,70 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Container {
|
||||
export class ExecPage {
|
||||
private $cmd: JQuery;
|
||||
private $connect: JQuery;
|
||||
private $disconnect: JQuery;
|
||||
private ws: WebSocket;
|
||||
private term: Terminal;
|
||||
|
||||
constructor() {
|
||||
this.$cmd = $("#txt-cmd");
|
||||
this.$connect = $("#btn-connect");
|
||||
this.$disconnect = $("#btn-disconnect");
|
||||
|
||||
this.$connect.click(this.connect.bind(this));
|
||||
this.$disconnect.click(this.disconnect.bind(this));
|
||||
|
||||
Terminal.applyAddon(fit);
|
||||
}
|
||||
|
||||
private connect(e: JQueryEventObject) {
|
||||
this.$connect.hide();
|
||||
this.$disconnect.show();
|
||||
|
||||
let url = location.host + location.pathname.substring(0, location.pathname.lastIndexOf("/")) + "/connect?cmd=" + encodeURIComponent(this.$cmd.val());
|
||||
let ws = new WebSocket("ws://" + url);
|
||||
ws.onopen = () => {
|
||||
this.term = new Terminal();
|
||||
this.term.on('data', (data: any) => {
|
||||
if (ws.readyState == WebSocket.OPEN) {
|
||||
ws.send(data);
|
||||
}
|
||||
});
|
||||
this.term.open(document.getElementById('terminal-container'));
|
||||
this.term.focus();
|
||||
let width = Math.floor(($('#terminal-container').width() - 20) / 8.39);
|
||||
let height = 30;
|
||||
this.term.resize(width, height);
|
||||
this.term.setOption('cursorBlink', true);
|
||||
this.term.fit();
|
||||
|
||||
window.onresize = () => {
|
||||
this.term.fit();
|
||||
};
|
||||
ws.onmessage = (e) => {
|
||||
this.term.write(e.data);
|
||||
};
|
||||
ws.onerror = function (error) {
|
||||
console.log("error: " + error);
|
||||
};
|
||||
ws.onclose = () => {
|
||||
console.log("close");
|
||||
};
|
||||
};
|
||||
this.ws = ws;
|
||||
}
|
||||
|
||||
private disconnect(e: JQueryEventObject) {
|
||||
if (this.ws && this.ws.readyState != WebSocket.CLOSED) {
|
||||
this.ws.close();
|
||||
}
|
||||
if (this.term) {
|
||||
this.term.destroy();
|
||||
this.term = null;
|
||||
}
|
||||
this.$connect.show();
|
||||
this.$disconnect.hide();
|
||||
}
|
||||
}
|
||||
}
|
53
assets/xterm/fit/fit.js
Normal file
53
assets/xterm/fit/fit.js
Normal file
@ -0,0 +1,53 @@
|
||||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.fit = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
function proposeGeometry(term) {
|
||||
if (!term.element.parentElement) {
|
||||
return null;
|
||||
}
|
||||
var parentElementStyle = window.getComputedStyle(term.element.parentElement);
|
||||
var parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||
var parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
|
||||
var elementStyle = window.getComputedStyle(term.element);
|
||||
var elementPadding = {
|
||||
top: parseInt(elementStyle.getPropertyValue('padding-top')),
|
||||
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
|
||||
right: parseInt(elementStyle.getPropertyValue('padding-right')),
|
||||
left: parseInt(elementStyle.getPropertyValue('padding-left'))
|
||||
};
|
||||
var elementPaddingVer = elementPadding.top + elementPadding.bottom;
|
||||
var elementPaddingHor = elementPadding.right + elementPadding.left;
|
||||
var availableHeight = parentElementHeight - elementPaddingVer;
|
||||
var availableWidth = parentElementWidth - elementPaddingHor - term.viewport.scrollBarWidth;
|
||||
var geometry = {
|
||||
cols: Math.floor(availableWidth / term.renderer.dimensions.actualCellWidth),
|
||||
rows: Math.floor(availableHeight / term.renderer.dimensions.actualCellHeight)
|
||||
};
|
||||
return geometry;
|
||||
}
|
||||
exports.proposeGeometry = proposeGeometry;
|
||||
function fit(term) {
|
||||
var geometry = proposeGeometry(term);
|
||||
if (geometry) {
|
||||
if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
|
||||
term.renderer.clear();
|
||||
term.resize(geometry.cols, geometry.rows);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.fit = fit;
|
||||
function apply(terminalConstructor) {
|
||||
terminalConstructor.prototype.proposeGeometry = function () {
|
||||
return proposeGeometry(this);
|
||||
};
|
||||
terminalConstructor.prototype.fit = function () {
|
||||
fit(this);
|
||||
};
|
||||
}
|
||||
exports.apply = apply;
|
||||
|
||||
|
||||
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
//# sourceMappingURL=fit.js.map
|
159
assets/xterm/xterm.css
Normal file
159
assets/xterm/xterm.css
Normal file
@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
||||
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
* https://github.com/chjj/term.js
|
||||
* @license MIT
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* Originally forked from (with the author's permission):
|
||||
* Fabrice Bellard's javascript vt100 for jslinux:
|
||||
* http://bellard.org/jslinux/
|
||||
* Copyright (c) 2011 Fabrice Bellard
|
||||
* The original design remains. The terminal itself
|
||||
* has been extended to include xterm CSI codes, among
|
||||
* other features.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default styles for xterm.js
|
||||
*/
|
||||
|
||||
.xterm {
|
||||
font-family: courier-new, courier, monospace;
|
||||
font-feature-settings: "liga" 0;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.xterm.focus,
|
||||
.xterm:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.xterm .xterm-helpers {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
/**
|
||||
* The z-index of the helpers must be higher than the canvases in order for
|
||||
* IMEs to appear on top.
|
||||
*/
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.xterm .xterm-helper-textarea {
|
||||
/*
|
||||
* HACK: to fix IE's blinking cursor
|
||||
* Move textarea out of the screen to the far left, so that the cursor is not visible.
|
||||
*/
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
left: -9999em;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
z-index: -10;
|
||||
/** Prevent wrapping so the IME appears against the textarea at the correct position */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.xterm .composition-view {
|
||||
/* TODO: Composition position got messed up somewhere */
|
||||
background: #000;
|
||||
color: #FFF;
|
||||
display: none;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.xterm .composition-view.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.xterm .xterm-viewport {
|
||||
/* On OS X this is required in order for the scroll bar to appear fully opaque */
|
||||
background-color: #000;
|
||||
overflow-y: scroll;
|
||||
cursor: default;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.xterm .xterm-scroll-area {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.xterm-char-measure-element {
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -9999em;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.xterm {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.xterm.enable-mouse-events {
|
||||
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.xterm.xterm-cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility,
|
||||
.xterm .xterm-message {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.xterm .live-region {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
7071
assets/xterm/xterm.js
Normal file
7071
assets/xterm/xterm.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -4,8 +4,8 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cuigh/swirl/misc"
|
||||
"github.com/cuigh/swirl/model"
|
||||
@ -111,3 +111,45 @@ func ContainerLogs(id string, line int, timestamps bool) (stdout, stderr *bytes.
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ContainerExecCreate creates an exec instance.
|
||||
func ContainerExecCreate(id string, cmd string) (resp types.IDResponse, err error) {
|
||||
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
|
||||
opts := types.ExecConfig{
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
//User: "root",
|
||||
Cmd: strings.Split(cmd, " "),
|
||||
}
|
||||
//cli.DialSession()
|
||||
resp, err = cli.ContainerExecCreate(ctx, id, opts)
|
||||
return
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ContainerExecAttach attaches a connection to an exec process in the server.
|
||||
func ContainerExecAttach(id string) (resp types.HijackedResponse, err error) {
|
||||
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
|
||||
opts := types.ExecStartCheck{
|
||||
Detach: false,
|
||||
Tty: true,
|
||||
}
|
||||
resp, err = cli.ContainerExecAttach(ctx, id, opts)
|
||||
return err
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ContainerExecStart starts an exec instance.
|
||||
func ContainerExecStart(id string) error {
|
||||
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
|
||||
opts := types.ExecStartCheck{
|
||||
Detach: false,
|
||||
Tty: true,
|
||||
}
|
||||
return cli.ContainerExecStart(ctx, id, opts)
|
||||
})
|
||||
}
|
||||
|
@ -27,6 +27,8 @@ button.next: Next
|
||||
button.import: Import
|
||||
button.export: Export
|
||||
button.more: More
|
||||
button.connect: Connect
|
||||
button.disconnect: Disconnect
|
||||
|
||||
# field
|
||||
field.name: Name
|
||||
@ -92,6 +94,7 @@ menu.edit: Edit
|
||||
menu.log: Logs
|
||||
menu.perm: Permission
|
||||
menu.stats: Stats
|
||||
menu.exec: Exec
|
||||
|
||||
# login page
|
||||
login.title: Sign in to Swirl
|
||||
|
@ -27,6 +27,8 @@ button.next: 后一页
|
||||
button.import: 导入
|
||||
button.export: 导出
|
||||
button.more: 更多
|
||||
button.connect: 连接
|
||||
button.disconnect: 断开
|
||||
|
||||
# field
|
||||
field.name: 名称
|
||||
@ -92,6 +94,7 @@ menu.edit: 编辑
|
||||
menu.log: 日志
|
||||
menu.perm: 权限
|
||||
menu.stats: 状态
|
||||
menu.exec: 执行
|
||||
|
||||
# login page
|
||||
login.title: 登录到 Swirl
|
||||
|
@ -1,14 +1,18 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/log"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/auxo/util/cast"
|
||||
"github.com/cuigh/swirl/biz/docker"
|
||||
"github.com/cuigh/swirl/misc"
|
||||
"github.com/cuigh/swirl/model"
|
||||
"github.com/gobwas/ws"
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
)
|
||||
|
||||
// ContainerController is a controller of docker container
|
||||
@ -19,6 +23,8 @@ type ContainerController struct {
|
||||
Logs web.HandlerFunc `path:"/:id/logs" name:"container.logs" authorize:"!" desc:"container logs page"`
|
||||
FetchLogs web.HandlerFunc `path:"/:id/fetch_logs" name:"container.fetch_logs" authorize:"?" desc:"fetch container logs"`
|
||||
Delete web.HandlerFunc `path:"/delete" method:"post" name:"container.delete" authorize:"!" desc:"delete container"`
|
||||
Exec web.HandlerFunc `path:"/:id/exec" name:"container.exec" authorize:"!" desc:"run a command in a running container"`
|
||||
Connect web.HandlerFunc `path:"/:id/connect" name:"container.connect" authorize:"!" desc:"connect to a running container"`
|
||||
}
|
||||
|
||||
// Container creates an instance of ContainerController
|
||||
@ -28,8 +34,10 @@ func Container() (c *ContainerController) {
|
||||
Detail: containerDetail,
|
||||
Raw: containerRaw,
|
||||
Logs: containerLogs,
|
||||
Delete: containerDelete,
|
||||
FetchLogs: containerFetchLogs,
|
||||
Delete: containerDelete,
|
||||
Exec: containerExec,
|
||||
Connect: containerConnect,
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,3 +126,103 @@ func containerDelete(ctx web.Context) error {
|
||||
}
|
||||
return ajaxSuccess(ctx, nil)
|
||||
}
|
||||
|
||||
func containerExec(ctx web.Context) error {
|
||||
id := ctx.P("id")
|
||||
container, _, err := docker.ContainerInspectRaw(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m := newModel(ctx).Set("Container", container)
|
||||
return ctx.Render("container/exec", m)
|
||||
}
|
||||
|
||||
func containerConnect(ctx web.Context) error {
|
||||
id := ctx.P("id")
|
||||
_, _, err := docker.ContainerInspectRaw(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn, _, _, err := ws.UpgradeHTTP(ctx.Request(), ctx.Response(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := ctx.Q("cmd")
|
||||
idResp, err := docker.ContainerExecCreate(id, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := docker.ContainerExecAttach(idResp.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = docker.ContainerExecStart(idResp.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
closed = false
|
||||
logger = log.Get("exec")
|
||||
disposer = func() {
|
||||
if !closed {
|
||||
closed = true
|
||||
conn.Close()
|
||||
resp.Close()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// input
|
||||
go func() {
|
||||
defer disposer()
|
||||
|
||||
for {
|
||||
msg, op, err := wsutil.ReadClientData(conn)
|
||||
if err != nil {
|
||||
if !closed {
|
||||
logger.Error("Failed to read data from client: ", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if op == ws.OpClose {
|
||||
break
|
||||
}
|
||||
|
||||
_, err = resp.Conn.Write(msg)
|
||||
if err != nil {
|
||||
logger.Error("Failed to write data to container: ", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// output
|
||||
go func() {
|
||||
defer disposer()
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := resp.Reader.Read(buf)
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
logger.Error("Failed to read data from container: ", err)
|
||||
break
|
||||
}
|
||||
|
||||
err = wsutil.WriteServerMessage(conn, ws.OpText, buf[:n])
|
||||
if err != nil {
|
||||
logger.Error("Failed to write data to client: ", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
@ -37,6 +37,7 @@
|
||||
<a class="navbar-item is-tab is-active" href="/container/{{.Container.ContainerJSONBase.ID}}/detail">{{ i18n("menu.detail") }}</a>
|
||||
<a class="navbar-item is-tab" href="/container/{{.Container.ContainerJSONBase.ID}}/raw">{{ i18n("menu.raw") }}</a>
|
||||
<a class="navbar-item is-tab" href="/container/{{.Container.ContainerJSONBase.ID}}/logs">{{ i18n("menu.log") }}</a>
|
||||
<a class="navbar-item is-tab" href="/container/{{.Container.ContainerJSONBase.ID}}/exec">{{ i18n("menu.exec") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
71
views/container/exec.jet
Normal file
71
views/container/exec.jet
Normal file
@ -0,0 +1,71 @@
|
||||
{{ extends "../_layouts/default" }}
|
||||
|
||||
{{ block style() }}
|
||||
<link rel="stylesheet" href="/assets/xterm/xterm.css?v=3.3.0">
|
||||
{{ end }}
|
||||
|
||||
{{ block script() }}
|
||||
<script src="/assets/xterm/xterm.js?v=3.3.0"></script>
|
||||
<script src="/assets/xterm/fit/fit.js?v=3.3.0"></script>
|
||||
<script>$(() => new Swirl.Container.ExecPage())</script>
|
||||
{{ end }}
|
||||
|
||||
{{ block body() }}
|
||||
<section class="hero is-info">
|
||||
<div class="hero-body">
|
||||
<div class="container has-text-centered">
|
||||
<h1 class="title is-2 is-uppercase">{{ i18n("container.title") }}</h1>
|
||||
<h2 class="subtitle is-5">{{ i18n("container.description") }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="container">
|
||||
<nav class="breadcrumb has-succeeds-separator is-small is-marginless" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="/">{{ i18n("menu.home") }}</a></li>
|
||||
<li><a href="/container/">{{ i18n("menu.container") }}</a></li>
|
||||
<li><a>{{ i18n("menu.exec") }}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<section class="hero is-small is-light">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h2 class="title is-2">
|
||||
{{ .Container.ContainerJSONBase.Name }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<nav class="navbar has-shadow">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item is-tab" href="/container/{{.Container.ContainerJSONBase.ID}}/detail">{{ i18n("menu.detail") }}</a>
|
||||
<a class="navbar-item is-tab" href="/container/{{.Container.ContainerJSONBase.ID}}/raw">{{ i18n("menu.raw") }}</a>
|
||||
<a class="navbar-item is-tab" href="/container/{{.Container.ContainerJSONBase.ID}}/logs">{{ i18n("menu.log") }}</a>
|
||||
<a class="navbar-item is-tab is-active" href="/container/{{.Container.ContainerJSONBase.ID}}/exec">{{ i18n("menu.exec") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="field has-addons">
|
||||
<p class="control">
|
||||
<a class="button is-static">Command</a>
|
||||
</p>
|
||||
<p class="control is-expanded">
|
||||
<input id="txt-cmd" name="cmd" value="/bin/sh" class="input">
|
||||
</p>
|
||||
<p class="control">
|
||||
<button id="btn-connect" class="button is-primary">{{ i18n("button.connect") }}</button>
|
||||
<button id="btn-disconnect" class="button is-danger" style="display: none">{{ i18n("button.disconnect") }}</button>
|
||||
</p>
|
||||
</div>
|
||||
<div id="terminal-container"></div>
|
||||
</div>
|
||||
</section>
|
||||
{{ end }}
|
@ -40,6 +40,7 @@
|
||||
<a class="navbar-item is-tab" href="/container/{{.Container.ContainerJSONBase.ID}}/detail">{{ i18n("menu.detail") }}</a>
|
||||
<a class="navbar-item is-tab" href="/container/{{.Container.ContainerJSONBase.ID}}/raw">{{ i18n("menu.raw") }}</a>
|
||||
<a class="navbar-item is-tab is-active" href="/container/{{.Container.ContainerJSONBase.ID}}/logs">{{ i18n("menu.log") }}</a>
|
||||
<a class="navbar-item is-tab" href="/container/{{.Container.ContainerJSONBase.ID}}/exec">{{ i18n("menu.exec") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
@ -42,6 +42,7 @@
|
||||
<a class="navbar-item is-tab" href="/container/{{.Container.ContainerJSONBase.ID}}/detail">{{ i18n("menu.detail") }}</a>
|
||||
<a class="navbar-item is-tab is-active" href="/container/{{.Container.ContainerJSONBase.ID}}/raw">{{ i18n("menu.raw") }}</a>
|
||||
<a class="navbar-item is-tab" href="/container/{{.Container.ContainerJSONBase.ID}}/logs">{{ i18n("menu.log") }}</a>
|
||||
<a class="navbar-item is-tab" href="/container/{{.Container.ContainerJSONBase.ID}}/exec">{{ i18n("menu.exec") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
Loading…
Reference in New Issue
Block a user