mirror of https://github.com/portainer/portainer
feat(global): add container exec support
parent
b0ebbdf68c
commit
5878eed7ec
68
api/api.go
68
api/api.go
|
@ -1,33 +1,57 @@
|
|||
package main // import "github.com/cloudinovasi/ui-for-docker"
|
||||
package main
|
||||
|
||||
import (
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// main is the entry point of the program
|
||||
func main() {
|
||||
kingpin.Version("1.5.0")
|
||||
var (
|
||||
endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String()
|
||||
addr = kingpin.Flag("bind", "Address and port to serve UI For Docker").Default(":9000").Short('p').String()
|
||||
assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String()
|
||||
data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').String()
|
||||
swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool()
|
||||
tlsverify = kingpin.Flag("tlsverify", "TLS support").Default("false").Bool()
|
||||
tlscacert = kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String()
|
||||
tlscert = kingpin.Flag("tlscert", "Path to the TLS certificate file").Default("/certs/cert.pem").String()
|
||||
tlskey = kingpin.Flag("tlskey", "Path to the TLS key").Default("/certs/key.pem").String()
|
||||
labels = pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
|
||||
)
|
||||
kingpin.Parse()
|
||||
type (
|
||||
api struct {
|
||||
endpoint *url.URL
|
||||
bindAddress string
|
||||
assetPath string
|
||||
dataPath string
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
configuration := newConfig(*swarm, *labels)
|
||||
tlsFlags := newTLSFlags(*tlsverify, *tlscacert, *tlscert, *tlskey)
|
||||
apiConfig struct {
|
||||
Endpoint string
|
||||
BindAddress string
|
||||
AssetPath string
|
||||
DataPath string
|
||||
SwarmSupport bool
|
||||
TLSEnabled bool
|
||||
TLSCACertPath string
|
||||
TLSCertPath string
|
||||
TLSKeyPath string
|
||||
}
|
||||
)
|
||||
|
||||
handler := newHandler(*assets, *data, *endpoint, configuration, tlsFlags)
|
||||
if err := http.ListenAndServe(*addr, handler); err != nil {
|
||||
func (a *api) run(configuration *Config) {
|
||||
handler := a.newHandler(configuration)
|
||||
if err := http.ListenAndServe(a.bindAddress, handler); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func newAPI(apiConfig apiConfig) *api {
|
||||
endpointURL, err := url.Parse(apiConfig.Endpoint)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
if apiConfig.TLSEnabled {
|
||||
tlsConfig = newTLSConfig(apiConfig.TLSCACertPath, apiConfig.TLSCertPath, apiConfig.TLSKeyPath)
|
||||
}
|
||||
|
||||
return &api{
|
||||
endpoint: endpointURL,
|
||||
bindAddress: apiConfig.BindAddress,
|
||||
assetPath: apiConfig.AssetPath,
|
||||
dataPath: apiConfig.DataPath,
|
||||
tlsConfig: tlsConfig,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,6 @@ func newConfig(swarm bool, labels pairList) Config {
|
|||
}
|
||||
|
||||
// configurationHandler defines a handler function used to encode the configuration in JSON
|
||||
func configurationHandler(w http.ResponseWriter, r *http.Request, c Config) {
|
||||
json.NewEncoder(w).Encode(c)
|
||||
func configurationHandler(w http.ResponseWriter, r *http.Request, c *Config) {
|
||||
json.NewEncoder(w).Encode(*c)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/net/websocket"
|
||||
"log"
|
||||
)
|
||||
|
||||
// execContainer is used to create a websocket communication with an exec instance
|
||||
func (a *api) execContainer(ws *websocket.Conn) {
|
||||
qry := ws.Request().URL.Query()
|
||||
execID := qry.Get("id")
|
||||
|
||||
var host string
|
||||
if a.endpoint.Scheme == "tcp" {
|
||||
host = a.endpoint.Host
|
||||
} else if a.endpoint.Scheme == "unix" {
|
||||
host = a.endpoint.Path
|
||||
}
|
||||
|
||||
if err := hijack(host, a.endpoint.Scheme, "POST", "/exec/"+execID+"/start", a.tlsConfig, true, ws, ws, ws, nil, nil); err != nil {
|
||||
log.Fatalf("error during hijack: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
18
api/flags.go
18
api/flags.go
|
@ -6,14 +6,6 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// TLSFlags defines all the flags associated to the SSL configuration
|
||||
type TLSFlags struct {
|
||||
tls bool
|
||||
caPath string
|
||||
certPath string
|
||||
keyPath string
|
||||
}
|
||||
|
||||
// pair defines a key/value pair
|
||||
type pair struct {
|
||||
Name string `json:"name"`
|
||||
|
@ -52,13 +44,3 @@ func pairs(s kingpin.Settings) (target *[]pair) {
|
|||
s.SetValue((*pairList)(target))
|
||||
return
|
||||
}
|
||||
|
||||
// newTLSFlags creates a new TLSFlags from command flags
|
||||
func newTLSFlags(tls bool, cacert string, cert string, key string) TLSFlags {
|
||||
return TLSFlags{
|
||||
tls: tls,
|
||||
caPath: cacert,
|
||||
certPath: cert,
|
||||
keyPath: key,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/net/websocket"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
|
@ -9,22 +10,18 @@ import (
|
|||
)
|
||||
|
||||
// newHandler creates a new http.Handler with CSRF protection
|
||||
func newHandler(dir string, d string, e string, c Config, tlsFlags TLSFlags) http.Handler {
|
||||
func (a *api) newHandler(c *Config) http.Handler {
|
||||
var (
|
||||
mux = http.NewServeMux()
|
||||
fileHandler = http.FileServer(http.Dir(dir))
|
||||
fileHandler = http.FileServer(http.Dir(a.assetPath))
|
||||
)
|
||||
|
||||
u, perr := url.Parse(e)
|
||||
if perr != nil {
|
||||
log.Fatal(perr)
|
||||
}
|
||||
handler := a.newAPIHandler()
|
||||
CSRFHandler := newCSRFHandler(a.dataPath)
|
||||
|
||||
handler := newAPIHandler(u, tlsFlags)
|
||||
CSRFHandler := newCSRFHandler(d)
|
||||
|
||||
mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", handler))
|
||||
mux.Handle("/", fileHandler)
|
||||
mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", handler))
|
||||
mux.Handle("/ws/exec", websocket.Handler(a.execContainer))
|
||||
mux.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) {
|
||||
configurationHandler(w, r, c)
|
||||
})
|
||||
|
@ -32,47 +29,47 @@ func newHandler(dir string, d string, e string, c Config, tlsFlags TLSFlags) htt
|
|||
}
|
||||
|
||||
// newAPIHandler initializes a new http.Handler based on the URL scheme
|
||||
func newAPIHandler(u *url.URL, tlsFlags TLSFlags) http.Handler {
|
||||
func (a *api) newAPIHandler() http.Handler {
|
||||
var handler http.Handler
|
||||
if u.Scheme == "tcp" {
|
||||
if tlsFlags.tls {
|
||||
handler = newTCPHandlerWithTLS(u, tlsFlags)
|
||||
var endpoint = *a.endpoint
|
||||
if endpoint.Scheme == "tcp" {
|
||||
if a.tlsConfig != nil {
|
||||
handler = a.newTCPHandlerWithTLS(&endpoint)
|
||||
} else {
|
||||
handler = newTCPHandler(u)
|
||||
handler = a.newTCPHandler(&endpoint)
|
||||
}
|
||||
} else if u.Scheme == "unix" {
|
||||
socketPath := u.Path
|
||||
} else if endpoint.Scheme == "unix" {
|
||||
socketPath := endpoint.Path
|
||||
if _, err := os.Stat(socketPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
log.Fatalf("Unix socket %s does not exist", socketPath)
|
||||
}
|
||||
log.Fatal(err)
|
||||
}
|
||||
handler = newUnixHandler(socketPath)
|
||||
handler = a.newUnixHandler(socketPath)
|
||||
} else {
|
||||
log.Fatalf("Bad Docker enpoint: %s. Only unix:// and tcp:// are supported.", u)
|
||||
log.Fatalf("Bad Docker enpoint: %v. Only unix:// and tcp:// are supported.", &endpoint)
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
// newUnixHandler initializes a new UnixHandler
|
||||
func newUnixHandler(e string) http.Handler {
|
||||
func (a *api) newUnixHandler(e string) http.Handler {
|
||||
return &unixHandler{e}
|
||||
}
|
||||
|
||||
// newTCPHandler initializes a HTTP reverse proxy
|
||||
func newTCPHandler(u *url.URL) http.Handler {
|
||||
func (a *api) newTCPHandler(u *url.URL) http.Handler {
|
||||
u.Scheme = "http"
|
||||
return httputil.NewSingleHostReverseProxy(u)
|
||||
}
|
||||
|
||||
// newTCPHandlerWithL initializes a HTTPS reverse proxy with a TLS configuration
|
||||
func newTCPHandlerWithTLS(u *url.URL, tlsFlags TLSFlags) http.Handler {
|
||||
func (a *api) newTCPHandlerWithTLS(u *url.URL) http.Handler {
|
||||
u.Scheme = "https"
|
||||
var tlsConfig = newTLSConfig(tlsFlags)
|
||||
proxy := httputil.NewSingleHostReverseProxy(u)
|
||||
proxy.Transport = &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
TLSClientConfig: a.tlsConfig,
|
||||
}
|
||||
return proxy
|
||||
}
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"time"
|
||||
)
|
||||
|
||||
type execConfig struct {
|
||||
Tty bool
|
||||
Detach bool
|
||||
}
|
||||
|
||||
// hijack allows to upgrade an HTTP connection to a TCP connection
|
||||
// It redirects IO streams for stdin, stdout and stderr to a websocket
|
||||
func hijack(addr, scheme, method, path string, tlsConfig *tls.Config, setRawTerminal bool, in io.ReadCloser, stdout, stderr io.Writer, started chan io.Closer, data interface{}) error {
|
||||
execConfig := &execConfig{
|
||||
Tty: true,
|
||||
Detach: false,
|
||||
}
|
||||
|
||||
buf, err := json.Marshal(execConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error marshaling exec config: %s", err)
|
||||
}
|
||||
|
||||
rdr := bytes.NewReader(buf)
|
||||
|
||||
req, err := http.NewRequest(method, path, rdr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error during hijack request: %s", err)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Docker-Client")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
req.Header.Set("Upgrade", "tcp")
|
||||
req.Host = addr
|
||||
|
||||
var (
|
||||
dial net.Conn
|
||||
dialErr error
|
||||
)
|
||||
|
||||
if tlsConfig == nil {
|
||||
dial, dialErr = net.Dial(scheme, addr)
|
||||
} else {
|
||||
dial, dialErr = tls.Dial(scheme, addr, tlsConfig)
|
||||
}
|
||||
|
||||
if dialErr != nil {
|
||||
return dialErr
|
||||
}
|
||||
|
||||
// When we set up a TCP connection for hijack, there could be long periods
|
||||
// of inactivity (a long running command with no output) that in certain
|
||||
// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
|
||||
// state. Setting TCP KeepAlive on the socket connection will prohibit
|
||||
// ECONNTIMEOUT unless the socket connection truly is broken
|
||||
if tcpConn, ok := dial.(*net.TCPConn); ok {
|
||||
tcpConn.SetKeepAlive(true)
|
||||
tcpConn.SetKeepAlivePeriod(30 * time.Second)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clientconn := httputil.NewClientConn(dial, nil)
|
||||
defer clientconn.Close()
|
||||
|
||||
// Server hijacks the connection, error 'connection closed' expected
|
||||
clientconn.Do(req)
|
||||
|
||||
rwc, br := clientconn.Hijack()
|
||||
defer rwc.Close()
|
||||
|
||||
if started != nil {
|
||||
started <- rwc
|
||||
}
|
||||
|
||||
var receiveStdout chan error
|
||||
|
||||
if stdout != nil || stderr != nil {
|
||||
go func() (err error) {
|
||||
if setRawTerminal && stdout != nil {
|
||||
_, err = io.Copy(stdout, br)
|
||||
}
|
||||
return err
|
||||
}()
|
||||
}
|
||||
|
||||
go func() error {
|
||||
if in != nil {
|
||||
io.Copy(rwc, in)
|
||||
}
|
||||
|
||||
if conn, ok := rwc.(interface {
|
||||
CloseWrite() error
|
||||
}); ok {
|
||||
if err := conn.CloseWrite(); err != nil {
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
if stdout != nil || stderr != nil {
|
||||
if err := <-receiveStdout; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
fmt.Println(br)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package main // import "github.com/cloudinovasi/ui-for-docker"
|
||||
|
||||
import (
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
// main is the entry point of the program
|
||||
func main() {
|
||||
kingpin.Version("1.5.0")
|
||||
var (
|
||||
endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String()
|
||||
addr = kingpin.Flag("bind", "Address and port to serve UI For Docker").Default(":9000").Short('p').String()
|
||||
assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String()
|
||||
data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').String()
|
||||
tlsverify = kingpin.Flag("tlsverify", "TLS support").Default("false").Bool()
|
||||
tlscacert = kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String()
|
||||
tlscert = kingpin.Flag("tlscert", "Path to the TLS certificate file").Default("/certs/cert.pem").String()
|
||||
tlskey = kingpin.Flag("tlskey", "Path to the TLS key").Default("/certs/key.pem").String()
|
||||
swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool()
|
||||
labels = pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
|
||||
)
|
||||
kingpin.Parse()
|
||||
|
||||
apiConfig := apiConfig{
|
||||
Endpoint: *endpoint,
|
||||
BindAddress: *addr,
|
||||
AssetPath: *assets,
|
||||
DataPath: *data,
|
||||
SwarmSupport: *swarm,
|
||||
TLSEnabled: *tlsverify,
|
||||
TLSCACertPath: *tlscacert,
|
||||
TLSCertPath: *tlscert,
|
||||
TLSKeyPath: *tlskey,
|
||||
}
|
||||
|
||||
configuration := &Config{
|
||||
Swarm: *swarm,
|
||||
HiddenLabels: *labels,
|
||||
}
|
||||
|
||||
api := newAPI(apiConfig)
|
||||
api.run(configuration)
|
||||
}
|
|
@ -7,13 +7,13 @@ import (
|
|||
"log"
|
||||
)
|
||||
|
||||
// newTLSConfig initializes a tls.Config from the TLS flags
|
||||
func newTLSConfig(tlsFlags TLSFlags) *tls.Config {
|
||||
cert, err := tls.LoadX509KeyPair(tlsFlags.certPath, tlsFlags.keyPath)
|
||||
// newTLSConfig initializes a tls.Config using a CA certificate, a certificate and a key
|
||||
func newTLSConfig(caCertPath, certPath, keyPath string) *tls.Config {
|
||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
caCert, err := ioutil.ReadFile(tlsFlags.caPath)
|
||||
caCert, err := ioutil.ReadFile(caCertPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -9,13 +9,14 @@ angular.module('uifordocker', [
|
|||
'uifordocker.filters',
|
||||
'dashboard',
|
||||
'container',
|
||||
'containerConsole',
|
||||
'containerLogs',
|
||||
'containers',
|
||||
'createContainer',
|
||||
'docker',
|
||||
'events',
|
||||
'images',
|
||||
'image',
|
||||
'containerLogs',
|
||||
'stats',
|
||||
'swarm',
|
||||
'network',
|
||||
|
@ -57,6 +58,11 @@ angular.module('uifordocker', [
|
|||
templateUrl: 'app/components/containerLogs/containerlogs.html',
|
||||
controller: 'ContainerLogsController'
|
||||
})
|
||||
.state('console', {
|
||||
url: "^/containers/:id/console",
|
||||
templateUrl: 'app/components/containerConsole/containerConsole.html',
|
||||
controller: 'ContainerConsoleController'
|
||||
})
|
||||
.state('actions', {
|
||||
abstract: true,
|
||||
url: "/actions",
|
||||
|
|
|
@ -64,6 +64,7 @@
|
|||
<div class="btn-group" role="group" aria-label="...">
|
||||
<a class="btn btn-default" type="button" ui-sref="stats({id: container.Id})">Stats</a>
|
||||
<a class="btn btn-default" type="button" ui-sref="logs({id: container.Id})">Logs</a>
|
||||
<a class="btn btn-default" type="button" ui-sref="console({id: container.Id})">Console</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment">
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Container console">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Console
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-terminal" title="Console">
|
||||
<div class="pull-right">
|
||||
<i id="loadConsoleSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px; display: none;"></i>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<!-- command-list -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-3">
|
||||
<select class="selectpicker form-control" ng-model="state.command">
|
||||
<option value="bash">/bin/bash</option>
|
||||
<option value="sh">/bin/sh</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-9 pull-left">
|
||||
<button type="button" class="btn btn-primary" ng-click="connect()" ng-disabled="connected">Connect</button>
|
||||
<button type="button" class="btn btn-default" ng-click="disconnect()" ng-disabled="!connected">Disconnect</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<div id="terminal-container" class="terminal-container"></div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,104 @@
|
|||
angular.module('containerConsole', [])
|
||||
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Exec', '$timeout', 'Messages', 'errorMsgFilter',
|
||||
function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages, errorMsgFilter) {
|
||||
$scope.state = {};
|
||||
$scope.state.command = "bash";
|
||||
$scope.connected = false;
|
||||
|
||||
var socket, term;
|
||||
|
||||
// Ensure the socket is closed before leaving the view
|
||||
$scope.$on('$stateChangeStart', function (event, next, current) {
|
||||
if (socket !== null) {
|
||||
socket.close();
|
||||
}
|
||||
});
|
||||
|
||||
Container.get({id: $stateParams.id}, function(d) {
|
||||
$scope.container = d;
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
|
||||
$scope.connect = function() {
|
||||
$('#loadConsoleSpinner').show();
|
||||
var termWidth = Math.round($('#terminal-container').width() / 8.2);
|
||||
var termHeight = 30;
|
||||
var execConfig = {
|
||||
id: $stateParams.id,
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
Cmd: $scope.state.command.replace(" ", ",").split(",")
|
||||
};
|
||||
|
||||
Container.exec(execConfig, function(d) {
|
||||
if (d.Id) {
|
||||
var execId = d.Id;
|
||||
resizeTTY(execId, termHeight, termWidth);
|
||||
var url = window.location.href.split('#')[0].replace('http://', 'ws://') + 'ws/exec?id=' + execId;
|
||||
initTerm(url, termHeight, termWidth);
|
||||
} else {
|
||||
$('#loadConsoleSpinner').hide();
|
||||
Messages.error('Error', errorMsgFilter(d));
|
||||
}
|
||||
}, function (e) {
|
||||
$('#loadConsoleSpinner').hide();
|
||||
Messages.error("Failure", e.data);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.disconnect = function() {
|
||||
$scope.connected = false;
|
||||
if (socket !== null) {
|
||||
socket.close();
|
||||
}
|
||||
if (term !== null) {
|
||||
term.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
function resizeTTY(execId, height, width) {
|
||||
$timeout(function() {
|
||||
Exec.resize({id: execId, height: height, width: width}, function (d) {
|
||||
var error = errorMsgFilter(d);
|
||||
if (error) {
|
||||
Messages.error('Error', 'Unable to resize TTY');
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
}
|
||||
|
||||
function initTerm(url, height, width) {
|
||||
socket = new WebSocket(url);
|
||||
|
||||
$scope.connected = true;
|
||||
socket.onopen = function(evt) {
|
||||
$('#loadConsoleSpinner').hide();
|
||||
term = new Terminal({
|
||||
cols: width,
|
||||
rows: height,
|
||||
cursorBlink: true
|
||||
});
|
||||
|
||||
term.on('data', function (data) {
|
||||
socket.send(data);
|
||||
});
|
||||
term.open(document.getElementById('terminal-container'));
|
||||
|
||||
socket.onmessage = function (e) {
|
||||
term.write(e.data);
|
||||
};
|
||||
socket.onerror = function (error) {
|
||||
$scope.connected = false;
|
||||
|
||||
};
|
||||
socket.onclose = function(evt) {
|
||||
$scope.connected = false;
|
||||
// term.write("Session terminated");
|
||||
// term.destroy();
|
||||
};
|
||||
};
|
||||
}
|
||||
}]);
|
|
@ -18,9 +18,17 @@ angular.module('uifordocker.services', ['ngResource', 'ngSanitize'])
|
|||
create: {method: 'POST', params: {action: 'create'}},
|
||||
remove: {method: 'DELETE', params: {id: '@id', v: 0}},
|
||||
rename: {method: 'POST', params: {id: '@id', action: 'rename'}, isArray: false},
|
||||
stats: {method: 'GET', params: {id: '@id', stream: false, action: 'stats'}, timeout: 5000}
|
||||
stats: {method: 'GET', params: {id: '@id', stream: false, action: 'stats'}, timeout: 5000},
|
||||
exec: {method: 'POST', params: {id: '@id', action: 'exec'}}
|
||||
});
|
||||
}])
|
||||
.factory('Exec', ['$resource', 'Settings', function ExecFactory($resource, Settings) {
|
||||
'use strict';
|
||||
// https://docs.docker.com/engine/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/exec-resize
|
||||
return $resource(Settings.url + '/exec/:id/:action', {}, {
|
||||
resize: {method: 'POST', params: {id: '@id', action: 'resize', h: '@height', w: '@width'}}
|
||||
});
|
||||
}])
|
||||
.factory('ContainerCommit', ['$resource', '$http', 'Settings', function ContainerCommitFactory($resource, $http, Settings) {
|
||||
'use strict';
|
||||
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#create-a-new-image-from-a-container-s-changes
|
||||
|
|
|
@ -183,5 +183,10 @@ input[type="radio"] {
|
|||
}
|
||||
|
||||
.widget .widget-body table tbody .image-tag {
|
||||
font-size: 90% !important;
|
||||
font-size: 90% !important;
|
||||
}
|
||||
|
||||
.terminal-container {
|
||||
width: 100%;
|
||||
padding: 10px 5px;
|
||||
}
|
||||
|
|
|
@ -38,7 +38,8 @@
|
|||
"jquery.gritter": "1.7.4",
|
||||
"lodash": "4.12.0",
|
||||
"rdash-ui": "1.0.*",
|
||||
"moment": "~2.14.1"
|
||||
"moment": "~2.14.1",
|
||||
"xterm.js": "~1.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"angular": "1.5.5"
|
||||
|
|
|
@ -72,6 +72,7 @@ module.exports = function (grunt) {
|
|||
'bower_components/Chart.js/Chart.min.js',
|
||||
'bower_components/lodash/dist/lodash.min.js',
|
||||
'bower_components/moment/min/moment.min.js',
|
||||
'bower_components/xterm.js/src/xterm.js',
|
||||
'assets/js/jquery.gritter.js', // Using custom version to fix error in minified build due to "use strict"
|
||||
'assets/js/legend.js' // Not a bower package
|
||||
],
|
||||
|
@ -85,7 +86,8 @@ module.exports = function (grunt) {
|
|||
'bower_components/jquery.gritter/css/jquery.gritter.css',
|
||||
'bower_components/font-awesome/css/font-awesome.min.css',
|
||||
'bower_components/rdash-ui/dist/css/rdash.min.css',
|
||||
'bower_components/angular-ui-select/dist/select.min.css'
|
||||
'bower_components/angular-ui-select/dist/select.min.css',
|
||||
'bower_components/xterm.js/src/xterm.css'
|
||||
]
|
||||
},
|
||||
clean: {
|
||||
|
|
Loading…
Reference in New Issue