mirror of https://github.com/portainer/portainer
Merge branch 'feat34-container-exec' into internal
commit
bab57e0402
69
api/api.go
69
api/api.go
|
@ -1,34 +1,57 @@
|
||||||
package main // import "github.com/cloudinovasi/ui-for-docker"
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gopkg.in/alecthomas/kingpin.v2"
|
"crypto/tls"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
// main is the entry point of the program
|
type (
|
||||||
func main() {
|
api struct {
|
||||||
kingpin.Version("1.5.0")
|
endpoint *url.URL
|
||||||
var (
|
bindAddress string
|
||||||
endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String()
|
assetPath string
|
||||||
addr = kingpin.Flag("bind", "Address and port to serve UI For Docker").Default(":9000").Short('p').String()
|
dataPath string
|
||||||
assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String()
|
tlsConfig *tls.Config
|
||||||
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'))
|
|
||||||
registries = pairs(kingpin.Flag("registries", "Supported Docker registries").Short('r'))
|
|
||||||
)
|
|
||||||
kingpin.Parse()
|
|
||||||
|
|
||||||
configuration := newConfig(*swarm, *labels, *registries)
|
apiConfig struct {
|
||||||
tlsFlags := newTLSFlags(*tlsverify, *tlscacert, *tlscert, *tlskey)
|
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)
|
func (a *api) run(configuration *Config) {
|
||||||
if err := http.ListenAndServe(*addr, handler); err != nil {
|
handler := a.newHandler(configuration)
|
||||||
|
if err := http.ListenAndServe(a.bindAddress, handler); err != nil {
|
||||||
log.Fatal(err)
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -22,6 +22,6 @@ func newConfig(swarm bool, labels, registries pairList) Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
// configurationHandler defines a handler function used to encode the configuration in JSON
|
// configurationHandler defines a handler function used to encode the configuration in JSON
|
||||||
func configurationHandler(w http.ResponseWriter, r *http.Request, c Config) {
|
func configurationHandler(w http.ResponseWriter, r *http.Request, c *Config) {
|
||||||
json.NewEncoder(w).Encode(c)
|
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"
|
"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
|
// pair defines a key/value pair
|
||||||
type pair struct {
|
type pair struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
@ -52,13 +44,3 @@ func pairs(s kingpin.Settings) (target *[]pair) {
|
||||||
s.SetValue((*pairList)(target))
|
s.SetValue((*pairList)(target))
|
||||||
return
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
|
@ -9,22 +10,18 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// newHandler creates a new http.Handler with CSRF protection
|
// 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 (
|
var (
|
||||||
mux = http.NewServeMux()
|
mux = http.NewServeMux()
|
||||||
fileHandler = http.FileServer(http.Dir(dir))
|
fileHandler = http.FileServer(http.Dir(a.assetPath))
|
||||||
)
|
)
|
||||||
|
|
||||||
u, perr := url.Parse(e)
|
handler := a.newAPIHandler()
|
||||||
if perr != nil {
|
CSRFHandler := newCSRFHandler(a.dataPath)
|
||||||
log.Fatal(perr)
|
|
||||||
}
|
|
||||||
|
|
||||||
handler := newAPIHandler(u, tlsFlags)
|
|
||||||
CSRFHandler := newCSRFHandler(d)
|
|
||||||
|
|
||||||
mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", handler))
|
|
||||||
mux.Handle("/", fileHandler)
|
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) {
|
mux.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) {
|
||||||
configurationHandler(w, r, c)
|
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
|
// 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
|
var handler http.Handler
|
||||||
if u.Scheme == "tcp" {
|
var endpoint = *a.endpoint
|
||||||
if tlsFlags.tls {
|
if endpoint.Scheme == "tcp" {
|
||||||
handler = newTCPHandlerWithTLS(u, tlsFlags)
|
if a.tlsConfig != nil {
|
||||||
|
handler = a.newTCPHandlerWithTLS(&endpoint)
|
||||||
} else {
|
} else {
|
||||||
handler = newTCPHandler(u)
|
handler = a.newTCPHandler(&endpoint)
|
||||||
}
|
}
|
||||||
} else if u.Scheme == "unix" {
|
} else if endpoint.Scheme == "unix" {
|
||||||
socketPath := u.Path
|
socketPath := endpoint.Path
|
||||||
if _, err := os.Stat(socketPath); err != nil {
|
if _, err := os.Stat(socketPath); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
log.Fatalf("Unix socket %s does not exist", socketPath)
|
log.Fatalf("Unix socket %s does not exist", socketPath)
|
||||||
}
|
}
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
handler = newUnixHandler(socketPath)
|
handler = a.newUnixHandler(socketPath)
|
||||||
} else {
|
} 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
|
return handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// newUnixHandler initializes a new UnixHandler
|
// newUnixHandler initializes a new UnixHandler
|
||||||
func newUnixHandler(e string) http.Handler {
|
func (a *api) newUnixHandler(e string) http.Handler {
|
||||||
return &unixHandler{e}
|
return &unixHandler{e}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newTCPHandler initializes a HTTP reverse proxy
|
// 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"
|
u.Scheme = "http"
|
||||||
return httputil.NewSingleHostReverseProxy(u)
|
return httputil.NewSingleHostReverseProxy(u)
|
||||||
}
|
}
|
||||||
|
|
||||||
// newTCPHandlerWithL initializes a HTTPS reverse proxy with a TLS configuration
|
// 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"
|
u.Scheme = "https"
|
||||||
var tlsConfig = newTLSConfig(tlsFlags)
|
|
||||||
proxy := httputil.NewSingleHostReverseProxy(u)
|
proxy := httputil.NewSingleHostReverseProxy(u)
|
||||||
proxy.Transport = &http.Transport{
|
proxy.Transport = &http.Transport{
|
||||||
TLSClientConfig: tlsConfig,
|
TLSClientConfig: a.tlsConfig,
|
||||||
}
|
}
|
||||||
return proxy
|
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,45 @@
|
||||||
|
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'))
|
||||||
|
registries = pairs(kingpin.Flag("registries", "Supported Docker registries").Short('r'))
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
Registries: *registries,
|
||||||
|
}
|
||||||
|
|
||||||
|
api := newAPI(apiConfig)
|
||||||
|
api.run(configuration)
|
||||||
|
}
|
|
@ -7,13 +7,13 @@ import (
|
||||||
"log"
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newTLSConfig initializes a tls.Config from the TLS flags
|
// newTLSConfig initializes a tls.Config using a CA certificate, a certificate and a key
|
||||||
func newTLSConfig(tlsFlags TLSFlags) *tls.Config {
|
func newTLSConfig(caCertPath, certPath, keyPath string) *tls.Config {
|
||||||
cert, err := tls.LoadX509KeyPair(tlsFlags.certPath, tlsFlags.keyPath)
|
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
caCert, err := ioutil.ReadFile(tlsFlags.caPath)
|
caCert, err := ioutil.ReadFile(caCertPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,13 +9,14 @@ angular.module('uifordocker', [
|
||||||
'uifordocker.filters',
|
'uifordocker.filters',
|
||||||
'dashboard',
|
'dashboard',
|
||||||
'container',
|
'container',
|
||||||
|
'containerConsole',
|
||||||
|
'containerLogs',
|
||||||
'containers',
|
'containers',
|
||||||
'createContainer',
|
'createContainer',
|
||||||
'docker',
|
'docker',
|
||||||
'events',
|
'events',
|
||||||
'images',
|
'images',
|
||||||
'image',
|
'image',
|
||||||
'containerLogs',
|
|
||||||
'stats',
|
'stats',
|
||||||
'swarm',
|
'swarm',
|
||||||
'network',
|
'network',
|
||||||
|
@ -55,6 +56,11 @@ angular.module('uifordocker', [
|
||||||
templateUrl: 'app/components/containerLogs/containerlogs.html',
|
templateUrl: 'app/components/containerLogs/containerlogs.html',
|
||||||
controller: 'ContainerLogsController'
|
controller: 'ContainerLogsController'
|
||||||
})
|
})
|
||||||
|
.state('console', {
|
||||||
|
url: "^/containers/:id/console",
|
||||||
|
templateUrl: 'app/components/containerConsole/containerConsole.html',
|
||||||
|
controller: 'ContainerConsoleController'
|
||||||
|
})
|
||||||
.state('actions', {
|
.state('actions', {
|
||||||
abstract: true,
|
abstract: true,
|
||||||
url: "/actions",
|
url: "/actions",
|
||||||
|
|
|
@ -64,6 +64,7 @@
|
||||||
<div class="btn-group" role="group" aria-label="...">
|
<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="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="logs({id: container.Id})">Logs</a>
|
||||||
|
<a class="btn btn-default" type="button" ui-sref="console({id: container.Id})">Console</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="comment">
|
<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'}},
|
create: {method: 'POST', params: {action: 'create'}},
|
||||||
remove: {method: 'DELETE', params: {id: '@id', v: 0}},
|
remove: {method: 'DELETE', params: {id: '@id', v: 0}},
|
||||||
rename: {method: 'POST', params: {id: '@id', action: 'rename'}, isArray: false},
|
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) {
|
.factory('ContainerCommit', ['$resource', '$http', 'Settings', function ContainerCommitFactory($resource, $http, Settings) {
|
||||||
'use strict';
|
'use strict';
|
||||||
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#create-a-new-image-from-a-container-s-changes
|
// 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 {
|
.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",
|
"jquery.gritter": "1.7.4",
|
||||||
"lodash": "4.12.0",
|
"lodash": "4.12.0",
|
||||||
"rdash-ui": "1.0.*",
|
"rdash-ui": "1.0.*",
|
||||||
"moment": "~2.14.1"
|
"moment": "~2.14.1",
|
||||||
|
"xterm.js": "~1.0.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"angular": "1.5.5"
|
"angular": "1.5.5"
|
||||||
|
|
|
@ -72,6 +72,7 @@ module.exports = function (grunt) {
|
||||||
'bower_components/Chart.js/Chart.min.js',
|
'bower_components/Chart.js/Chart.min.js',
|
||||||
'bower_components/lodash/dist/lodash.min.js',
|
'bower_components/lodash/dist/lodash.min.js',
|
||||||
'bower_components/moment/min/moment.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/jquery.gritter.js', // Using custom version to fix error in minified build due to "use strict"
|
||||||
'assets/js/legend.js' // Not a bower package
|
'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/jquery.gritter/css/jquery.gritter.css',
|
||||||
'bower_components/font-awesome/css/font-awesome.min.css',
|
'bower_components/font-awesome/css/font-awesome.min.css',
|
||||||
'bower_components/rdash-ui/dist/css/rdash.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: {
|
clean: {
|
||||||
|
|
Loading…
Reference in New Issue