mirror of https://github.com/portainer/portainer
commit
d3fa9736f4
|
@ -0,0 +1,57 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
api struct {
|
||||||
|
endpoint *url.URL
|
||||||
|
bindAddress string
|
||||||
|
assetPath string
|
||||||
|
dataPath string
|
||||||
|
tlsConfig *tls.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
apiConfig struct {
|
||||||
|
Endpoint string
|
||||||
|
BindAddress string
|
||||||
|
AssetPath string
|
||||||
|
DataPath string
|
||||||
|
SwarmSupport bool
|
||||||
|
TLSEnabled bool
|
||||||
|
TLSCACertPath string
|
||||||
|
TLSCertPath string
|
||||||
|
TLSKeyPath string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *api) run(settings *Settings) {
|
||||||
|
handler := a.newHandler(settings)
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gorilla/csrf"
|
||||||
|
"github.com/gorilla/securecookie"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const keyFile = "authKey.dat"
|
||||||
|
|
||||||
|
// newAuthKey reuses an existing CSRF authkey if present or generates a new one
|
||||||
|
func newAuthKey(path string) []byte {
|
||||||
|
var authKey []byte
|
||||||
|
authKeyPath := path + "/" + keyFile
|
||||||
|
data, err := ioutil.ReadFile(authKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Print("Unable to find an existing CSRF auth key. Generating a new key.")
|
||||||
|
authKey = securecookie.GenerateRandomKey(32)
|
||||||
|
err := ioutil.WriteFile(authKeyPath, authKey, 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Unable to persist CSRF auth key.")
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
authKey = data
|
||||||
|
}
|
||||||
|
return authKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// newCSRF initializes a new CSRF handler
|
||||||
|
func newCSRFHandler(keyPath string) func(h http.Handler) http.Handler {
|
||||||
|
authKey := newAuthKey(keyPath)
|
||||||
|
return csrf.Protect(
|
||||||
|
authKey,
|
||||||
|
csrf.HttpOnly(false),
|
||||||
|
csrf.Secure(false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newCSRFWrapper wraps a http.Handler to add the CSRF token
|
||||||
|
func newCSRFWrapper(h http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("X-CSRF-Token", csrf.Token(r))
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gopkg.in/alecthomas/kingpin.v2"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pair defines a key/value pair
|
||||||
|
type pair struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// pairList defines an array of Label
|
||||||
|
type pairList []pair
|
||||||
|
|
||||||
|
// Set implementation for Labels
|
||||||
|
func (l *pairList) Set(value string) error {
|
||||||
|
parts := strings.SplitN(value, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return fmt.Errorf("expected NAME=VALUE got '%s'", value)
|
||||||
|
}
|
||||||
|
p := new(pair)
|
||||||
|
p.Name = parts[0]
|
||||||
|
p.Value = parts[1]
|
||||||
|
*l = append(*l, *p)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implementation for Labels
|
||||||
|
func (l *pairList) String() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCumulative implementation for Labels
|
||||||
|
func (l *pairList) IsCumulative() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// LabelParser defines a custom parser for Labels flags
|
||||||
|
func pairs(s kingpin.Settings) (target *[]pair) {
|
||||||
|
target = new([]pair)
|
||||||
|
s.SetValue((*pairList)(target))
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newHandler creates a new http.Handler with CSRF protection
|
||||||
|
func (a *api) newHandler(settings *Settings) http.Handler {
|
||||||
|
var (
|
||||||
|
mux = http.NewServeMux()
|
||||||
|
fileHandler = http.FileServer(http.Dir(a.assetPath))
|
||||||
|
)
|
||||||
|
|
||||||
|
handler := a.newAPIHandler()
|
||||||
|
CSRFHandler := newCSRFHandler(a.dataPath)
|
||||||
|
|
||||||
|
mux.Handle("/", fileHandler)
|
||||||
|
mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", handler))
|
||||||
|
mux.Handle("/ws/exec", websocket.Handler(a.execContainer))
|
||||||
|
mux.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
settingsHandler(w, r, settings)
|
||||||
|
})
|
||||||
|
return CSRFHandler(newCSRFWrapper(mux))
|
||||||
|
}
|
||||||
|
|
||||||
|
// newAPIHandler initializes a new http.Handler based on the URL scheme
|
||||||
|
func (a *api) newAPIHandler() http.Handler {
|
||||||
|
var handler http.Handler
|
||||||
|
var endpoint = *a.endpoint
|
||||||
|
if endpoint.Scheme == "tcp" {
|
||||||
|
if a.tlsConfig != nil {
|
||||||
|
handler = a.newTCPHandlerWithTLS(&endpoint)
|
||||||
|
} else {
|
||||||
|
handler = a.newTCPHandler(&endpoint)
|
||||||
|
}
|
||||||
|
} 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 = a.newUnixHandler(socketPath)
|
||||||
|
} else {
|
||||||
|
log.Fatalf("Bad Docker enpoint: %v. Only unix:// and tcp:// are supported.", &endpoint)
|
||||||
|
}
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// newUnixHandler initializes a new UnixHandler
|
||||||
|
func (a *api) newUnixHandler(e string) http.Handler {
|
||||||
|
return &unixHandler{e}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTCPHandler initializes a HTTP reverse proxy
|
||||||
|
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 (a *api) newTCPHandlerWithTLS(u *url.URL) http.Handler {
|
||||||
|
u.Scheme = "https"
|
||||||
|
proxy := httputil.NewSingleHostReverseProxy(u)
|
||||||
|
proxy.Transport = &http.Transport{
|
||||||
|
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.6.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,
|
||||||
|
}
|
||||||
|
|
||||||
|
settings := &Settings{
|
||||||
|
Swarm: *swarm,
|
||||||
|
HiddenLabels: *labels,
|
||||||
|
}
|
||||||
|
|
||||||
|
api := newAPI(apiConfig)
|
||||||
|
api.run(settings)
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Settings defines the settings available under the /settings endpoint
|
||||||
|
type Settings struct {
|
||||||
|
Swarm bool `json:"swarm"`
|
||||||
|
HiddenLabels pairList `json:"hiddenLabels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// configurationHandler defines a handler function used to encode the configuration in JSON
|
||||||
|
func settingsHandler(w http.ResponseWriter, r *http.Request, s *Settings) {
|
||||||
|
json.NewEncoder(w).Encode(*s)
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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(caCertPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
caCertPool.AppendCertsFromPEM(caCert)
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
RootCAs: caCertPool,
|
||||||
|
}
|
||||||
|
return tlsConfig
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// unixHandler defines a handler holding the path to a socket under UNIX
|
||||||
|
type unixHandler struct {
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP implementation for unixHandler
|
||||||
|
func (h *unixHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := net.Dial("unix", h.path)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c := httputil.NewClientConn(conn, nil)
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
res, err := c.Do(r)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
copyHeader(w.Header(), res.Header)
|
||||||
|
if _, err := io.Copy(w, res.Body); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyHeader(dst, src http.Header) {
|
||||||
|
for k, vv := range src {
|
||||||
|
for _, v := range vv {
|
||||||
|
dst.Add(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
app/app.js
22
app/app.js
|
@ -5,16 +5,18 @@ angular.module('uifordocker', [
|
||||||
'ui.select',
|
'ui.select',
|
||||||
'ngCookies',
|
'ngCookies',
|
||||||
'ngSanitize',
|
'ngSanitize',
|
||||||
'dockerui.services',
|
'uifordocker.services',
|
||||||
'dockerui.filters',
|
'uifordocker.filters',
|
||||||
'dashboard',
|
'dashboard',
|
||||||
'container',
|
'container',
|
||||||
|
'containerConsole',
|
||||||
|
'containerLogs',
|
||||||
'containers',
|
'containers',
|
||||||
'createContainer',
|
'createContainer',
|
||||||
'docker',
|
'docker',
|
||||||
|
'events',
|
||||||
'images',
|
'images',
|
||||||
'image',
|
'image',
|
||||||
'containerLogs',
|
|
||||||
'stats',
|
'stats',
|
||||||
'swarm',
|
'swarm',
|
||||||
'network',
|
'network',
|
||||||
|
@ -56,6 +58,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",
|
||||||
|
@ -86,6 +93,11 @@ angular.module('uifordocker', [
|
||||||
templateUrl: 'app/components/docker/docker.html',
|
templateUrl: 'app/components/docker/docker.html',
|
||||||
controller: 'DockerController'
|
controller: 'DockerController'
|
||||||
})
|
})
|
||||||
|
.state('events', {
|
||||||
|
url: '/events/',
|
||||||
|
templateUrl: 'app/components/events/events.html',
|
||||||
|
controller: 'EventsController'
|
||||||
|
})
|
||||||
.state('images', {
|
.state('images', {
|
||||||
url: '/images/',
|
url: '/images/',
|
||||||
templateUrl: 'app/components/images/images.html',
|
templateUrl: 'app/components/images/images.html',
|
||||||
|
@ -143,5 +155,5 @@ angular.module('uifordocker', [
|
||||||
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
|
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
|
||||||
.constant('DOCKER_ENDPOINT', 'dockerapi')
|
.constant('DOCKER_ENDPOINT', 'dockerapi')
|
||||||
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is requred. If you have a port, prefix it with a ':' i.e. :4243
|
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is requred. If you have a port, prefix it with a ':' i.e. :4243
|
||||||
.constant('CONFIG_ENDPOINT', '/config')
|
.constant('CONFIG_ENDPOINT', 'settings')
|
||||||
.constant('UI_VERSION', 'v1.5.0');
|
.constant('UI_VERSION', 'v1.6.0');
|
||||||
|
|
|
@ -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();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}]);
|
|
@ -8,7 +8,7 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="!swarm">
|
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="!swarm">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-header icon="fa-cogs" title="Node info"></rd-widget-header>
|
<rd-widget-header icon="fa-tachometer" title="Node info"></rd-widget-header>
|
||||||
<rd-widget-body classes="no-padding">
|
<rd-widget-body classes="no-padding">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -35,7 +35,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="swarm">
|
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="swarm">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-header icon="fa-cogs" title="Cluster info"></rd-widget-header>
|
<rd-widget-header icon="fa-tachometer" title="Cluster info"></rd-widget-header>
|
||||||
<rd-widget-body classes="no-padding">
|
<rd-widget-body classes="no-padding">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
<rd-header>
|
||||||
|
<rd-header-title title="Event list">
|
||||||
|
<a data-toggle="tooltip" title="Refresh" ui-sref="events" ui-sref-opts="{reload: true}">
|
||||||
|
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</rd-header-title>
|
||||||
|
<rd-header-content>Events</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-history" title="Events">
|
||||||
|
<div class="pull-right">
|
||||||
|
<i id="loadEventsSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||||
|
</div>
|
||||||
|
</rd-widget-header>
|
||||||
|
<rd-widget-taskbar classes="col-lg-12">
|
||||||
|
<div class="pull-right">
|
||||||
|
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||||
|
</div>
|
||||||
|
</rd-widget-taskbar>
|
||||||
|
<rd-widget-body classes="no-padding">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<a ui-sref="events" ng-click="order('Time')">
|
||||||
|
Date
|
||||||
|
<span ng-show="sortType == 'Time' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
|
<span ng-show="sortType == 'Time' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<a ui-sref="events" ng-click="order('Type')">
|
||||||
|
Category
|
||||||
|
<span ng-show="sortType == 'Type' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
|
<span ng-show="sortType == 'Type' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<a ui-sref="events" ng-click="order('Details')">
|
||||||
|
Details
|
||||||
|
<span ng-show="sortType == 'Details' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
|
<span ng-show="sortType == 'Details' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="event in (events | filter:state.filter | orderBy:sortType:sortReverse)">
|
||||||
|
<td>{{ event.Time|getdatefromtimestamp }}</td>
|
||||||
|
<td>{{ event.Type }}</td>
|
||||||
|
<td>{{ event.Details }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</rd-widget-body>
|
||||||
|
<rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,27 @@
|
||||||
|
angular.module('events', [])
|
||||||
|
.controller('EventsController', ['$scope', 'Settings', 'Messages', 'Events',
|
||||||
|
function ($scope, Settings, Messages, Events) {
|
||||||
|
$scope.state = {};
|
||||||
|
$scope.sortType = 'Time';
|
||||||
|
$scope.sortReverse = true;
|
||||||
|
|
||||||
|
$scope.order = function(sortType) {
|
||||||
|
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||||
|
$scope.sortType = sortType;
|
||||||
|
};
|
||||||
|
|
||||||
|
var from = moment().subtract(24, 'hour').unix();
|
||||||
|
var to = moment().unix();
|
||||||
|
|
||||||
|
Events.query({since: from, until: to},
|
||||||
|
function(d) {
|
||||||
|
$scope.events = d.map(function (item) {
|
||||||
|
return new EventViewModel(item);
|
||||||
|
});
|
||||||
|
$('#loadEventsSpinner').hide();
|
||||||
|
},
|
||||||
|
function (e) {
|
||||||
|
Messages.error("Unable to load events", e.data);
|
||||||
|
$('#loadEventsSpinner').hide();
|
||||||
|
});
|
||||||
|
}]);
|
|
@ -45,9 +45,14 @@ function ($scope, Network, Messages, errorMsgFilter) {
|
||||||
if (network.Checked) {
|
if (network.Checked) {
|
||||||
counter = counter + 1;
|
counter = counter + 1;
|
||||||
Network.remove({id: network.Id}, function (d) {
|
Network.remove({id: network.Id}, function (d) {
|
||||||
|
var error = errorMsgFilter(d);
|
||||||
|
if (error) {
|
||||||
|
Messages.send("Error", "Unable to remove network with active endpoints");
|
||||||
|
} else {
|
||||||
Messages.send("Network deleted", network.Id);
|
Messages.send("Network deleted", network.Id);
|
||||||
var index = $scope.networks.indexOf(network);
|
var index = $scope.networks.indexOf(network);
|
||||||
$scope.networks.splice(index, 1);
|
$scope.networks.splice(index, 1);
|
||||||
|
}
|
||||||
complete();
|
complete();
|
||||||
}, function (e) {
|
}, function (e) {
|
||||||
Messages.error("Failure", e.data);
|
Messages.error("Failure", e.data);
|
||||||
|
|
|
@ -8,43 +8,7 @@
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-3 col-md-6 col-xs-12">
|
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-body>
|
|
||||||
<div class="widget-icon pull-left">
|
|
||||||
<i class="fa fa-code"></i>
|
|
||||||
</div>
|
|
||||||
<div class="title">{{ docker.Version }}</div>
|
|
||||||
<div class="comment">Swarm version</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-3 col-md-6 col-xs-12">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-body>
|
|
||||||
<div class="widget-icon pull-left">
|
|
||||||
<i class="fa fa-code"></i>
|
|
||||||
</div>
|
|
||||||
<div class="title">{{ docker.ApiVersion }}</div>
|
|
||||||
<div class="comment">API version</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-3 col-md-6 col-xs-12">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-body>
|
|
||||||
<div class="widget-icon pull-left">
|
|
||||||
<i class="fa fa-code"></i>
|
|
||||||
</div>
|
|
||||||
<div class="title">{{ docker.GoVersion }}</div>
|
|
||||||
<div class="comment">Go version</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-header icon="fa-object-group" title="Cluster status"></rd-widget-header>
|
<rd-widget-header icon="fa-object-group" title="Cluster status"></rd-widget-header>
|
||||||
<rd-widget-body classes="no-padding">
|
<rd-widget-body classes="no-padding">
|
||||||
|
@ -58,34 +22,48 @@
|
||||||
<td>Images</td>
|
<td>Images</td>
|
||||||
<td>{{ info.Images }}</td>
|
<td>{{ info.Images }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Swarm version</td>
|
||||||
|
<td>{{ docker.Version|swarmversion }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Docker API version</td>
|
||||||
|
<td>{{ docker.ApiVersion }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Strategy</td>
|
<td>Strategy</td>
|
||||||
<td>{{ swarm.Strategy }}</td>
|
<td>{{ swarm.Strategy }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>CPUs</td>
|
<td>Total CPU</td>
|
||||||
<td>{{ info.NCPU }}</td>
|
<td>{{ info.NCPU }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Total Memory</td>
|
<td>Total memory</td>
|
||||||
<td>{{ info.MemTotal|humansize }}</td>
|
<td>{{ info.MemTotal|humansize }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Operating System</td>
|
<td>Operating system</td>
|
||||||
<td>{{ info.OperatingSystem }}</td>
|
<td>{{ info.OperatingSystem }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Kernel Version</td>
|
<td>Kernel version</td>
|
||||||
<td>{{ info.KernelVersion }}</td>
|
<td>{{ info.KernelVersion }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Go version</td>
|
||||||
|
<td>{{ docker.GoVersion }}</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
</rd-widget>
|
</rd-widget>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-6">
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-header icon="fa-hdd-o" title="Nodes status"></rd-widget-header>
|
<rd-widget-header icon="fa-hdd-o" title="Node status"></rd-widget-header>
|
||||||
<rd-widget-body classes="no-padding">
|
<rd-widget-body classes="no-padding">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -97,6 +75,20 @@
|
||||||
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
|
<th>
|
||||||
|
<a ui-sref="swarm" ng-click="order('cpu')">
|
||||||
|
CPU
|
||||||
|
<span ng-show="sortType == 'cpu' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
|
<span ng-show="sortType == 'cpu' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<a ui-sref="swarm" ng-click="order('memory')">
|
||||||
|
Memory
|
||||||
|
<span ng-show="sortType == 'memory' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
|
<span ng-show="sortType == 'memory' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a ui-sref="swarm" ng-click="order('IP')">
|
<a ui-sref="swarm" ng-click="order('IP')">
|
||||||
IP
|
IP
|
||||||
|
@ -123,6 +115,8 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr ng-repeat="node in (state.filteredNodes = (swarm.Status | filter:state.filter | orderBy:sortType:sortReverse))">
|
<tr ng-repeat="node in (state.filteredNodes = (swarm.Status | filter:state.filter | orderBy:sortType:sortReverse))">
|
||||||
<td>{{ node.name }}</td>
|
<td>{{ node.name }}</td>
|
||||||
|
<td>{{ node.cpu }}</td>
|
||||||
|
<td>{{ node.memory }}</td>
|
||||||
<td>{{ node.ip }}</td>
|
<td>{{ node.ip }}</td>
|
||||||
<td>{{ node.version }}</td>
|
<td>{{ node.version }}</td>
|
||||||
<td><span class="label label-{{ node.status|nodestatusbadge }}">{{ node.status }}</span></td>
|
<td><span class="label label-{{ node.status|nodestatusbadge }}">{{ node.status }}</span></td>
|
||||||
|
|
|
@ -53,8 +53,8 @@ angular.module('swarm', [])
|
||||||
node.id = info[offset + 1][1];
|
node.id = info[offset + 1][1];
|
||||||
node.status = info[offset + 2][1];
|
node.status = info[offset + 2][1];
|
||||||
node.containers = info[offset + 3][1];
|
node.containers = info[offset + 3][1];
|
||||||
node.cpu = info[offset + 4][1];
|
node.cpu = info[offset + 4][1].split('/')[1];
|
||||||
node.memory = info[offset + 5][1];
|
node.memory = info[offset + 5][1].split('/')[1];
|
||||||
node.labels = info[offset + 6][1];
|
node.labels = info[offset + 6][1];
|
||||||
node.version = info[offset + 8][1];
|
node.version = info[offset + 8][1];
|
||||||
$scope.swarm.Status.push(node);
|
$scope.swarm.Status.push(node);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
angular.module('dockerui.filters', [])
|
angular.module('uifordocker.filters', [])
|
||||||
.filter('truncate', function () {
|
.filter('truncate', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (text, length, end) {
|
return function (text, length, end) {
|
||||||
|
@ -163,6 +163,12 @@ angular.module('dockerui.filters', [])
|
||||||
return date.toDateString();
|
return date.toDateString();
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
.filter('getdatefromtimestamp', function () {
|
||||||
|
'use strict';
|
||||||
|
return function (timestamp) {
|
||||||
|
return moment.unix(timestamp).format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
};
|
||||||
|
})
|
||||||
.filter('errorMsg', function () {
|
.filter('errorMsg', function () {
|
||||||
return function (object) {
|
return function (object) {
|
||||||
var idx = 0;
|
var idx = 0;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
angular.module('dockerui.services', ['ngResource', 'ngSanitize'])
|
angular.module('uifordocker.services', ['ngResource', 'ngSanitize'])
|
||||||
.factory('Container', ['$resource', 'Settings', function ContainerFactory($resource, Settings) {
|
.factory('Container', ['$resource', 'Settings', function ContainerFactory($resource, Settings) {
|
||||||
'use strict';
|
'use strict';
|
||||||
// Resource for interacting with the docker containers
|
// Resource for interacting with the docker containers
|
||||||
|
@ -18,7 +18,15 @@ angular.module('dockerui.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) {
|
||||||
|
@ -98,6 +106,16 @@ angular.module('dockerui.services', ['ngResource', 'ngSanitize'])
|
||||||
inspect: {method: 'GET', params: {id: '@id', action: 'json'}}
|
inspect: {method: 'GET', params: {id: '@id', action: 'json'}}
|
||||||
});
|
});
|
||||||
}])
|
}])
|
||||||
|
.factory('Events', ['$resource', 'Settings', function EventFactory($resource, Settings) {
|
||||||
|
'use strict';
|
||||||
|
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/monitor-docker-s-events
|
||||||
|
return $resource(Settings.url + '/events', {}, {
|
||||||
|
query: {method: 'GET', params: {since: '@since', until: '@until'}, isArray: true, transformResponse: [function f(data) {
|
||||||
|
var str = "[" + data.replace(/\n/g, " ").replace(/\}\s*\{/g, "}, {") + "]";
|
||||||
|
return angular.fromJson(str);
|
||||||
|
}]}
|
||||||
|
});
|
||||||
|
}])
|
||||||
.factory('Version', ['$resource', 'Settings', function VersionFactory($resource, Settings) {
|
.factory('Version', ['$resource', 'Settings', function VersionFactory($resource, Settings) {
|
||||||
'use strict';
|
'use strict';
|
||||||
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#show-the-docker-version-information
|
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#show-the-docker-version-information
|
||||||
|
@ -142,7 +160,7 @@ angular.module('dockerui.services', ['ngResource', 'ngSanitize'])
|
||||||
remove: {method: 'DELETE'}
|
remove: {method: 'DELETE'}
|
||||||
});
|
});
|
||||||
}])
|
}])
|
||||||
.factory('Config', ['$resource', 'CONFIG_ENDPOINT', function($resource, CONFIG_ENDPOINT) {
|
.factory('Config', ['$resource', 'CONFIG_ENDPOINT', function ConfigFactory($resource, CONFIG_ENDPOINT) {
|
||||||
return $resource(CONFIG_ENDPOINT).get();
|
return $resource(CONFIG_ENDPOINT).get();
|
||||||
}])
|
}])
|
||||||
.factory('Settings', ['DOCKER_ENDPOINT', 'DOCKER_PORT', 'UI_VERSION', function SettingsFactory(DOCKER_ENDPOINT, DOCKER_PORT, UI_VERSION) {
|
.factory('Settings', ['DOCKER_ENDPOINT', 'DOCKER_PORT', 'UI_VERSION', function SettingsFactory(DOCKER_ENDPOINT, DOCKER_PORT, UI_VERSION) {
|
||||||
|
|
|
@ -20,3 +20,115 @@ function ContainerViewModel(data) {
|
||||||
this.Command = data.Command;
|
this.Command = data.Command;
|
||||||
this.Checked = false;
|
this.Checked = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createEventDetails(event) {
|
||||||
|
var eventAttr = event.Actor.Attributes;
|
||||||
|
var details = '';
|
||||||
|
switch (event.Type) {
|
||||||
|
case 'container':
|
||||||
|
switch (event.Action) {
|
||||||
|
case 'stop':
|
||||||
|
details = 'Container ' + eventAttr.name + ' stopped';
|
||||||
|
break;
|
||||||
|
case 'destroy':
|
||||||
|
details = 'Container ' + eventAttr.name + ' deleted';
|
||||||
|
break;
|
||||||
|
case 'create':
|
||||||
|
details = 'Container ' + eventAttr.name + ' created';
|
||||||
|
break;
|
||||||
|
case 'start':
|
||||||
|
details = 'Container ' + eventAttr.name + ' started';
|
||||||
|
break;
|
||||||
|
case 'kill':
|
||||||
|
details = 'Container ' + eventAttr.name + ' killed';
|
||||||
|
break;
|
||||||
|
case 'die':
|
||||||
|
details = 'Container ' + eventAttr.name + ' exited with status code ' + eventAttr.exitCode;
|
||||||
|
break;
|
||||||
|
case 'commit':
|
||||||
|
details = 'Container ' + eventAttr.name + ' committed';
|
||||||
|
break;
|
||||||
|
case 'restart':
|
||||||
|
details = 'Container ' + eventAttr.name + ' restarted';
|
||||||
|
break;
|
||||||
|
case 'pause':
|
||||||
|
details = 'Container ' + eventAttr.name + ' paused';
|
||||||
|
break;
|
||||||
|
case 'unpause':
|
||||||
|
details = 'Container ' + eventAttr.name + ' unpaused';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (event.Action.indexOf('exec_create') === 0) {
|
||||||
|
details = 'Exec instance created';
|
||||||
|
} else if (event.Action.indexOf('exec_start') === 0) {
|
||||||
|
details = 'Exec instance started';
|
||||||
|
} else {
|
||||||
|
details = 'Unsupported event';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'image':
|
||||||
|
switch (event.Action) {
|
||||||
|
case 'delete':
|
||||||
|
details = 'Image deleted';
|
||||||
|
break;
|
||||||
|
case 'tag':
|
||||||
|
details = 'New tag created for ' + eventAttr.name;
|
||||||
|
break;
|
||||||
|
case 'untag':
|
||||||
|
details = 'Image untagged';
|
||||||
|
break;
|
||||||
|
case 'pull':
|
||||||
|
details = 'Image ' + event.Actor.ID + ' pulled';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
details = 'Unsupported event';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'network':
|
||||||
|
switch (event.Action) {
|
||||||
|
case 'create':
|
||||||
|
details = 'Network ' + eventAttr.name + ' created';
|
||||||
|
break;
|
||||||
|
case 'destroy':
|
||||||
|
details = 'Network ' + eventAttr.name + ' deleted';
|
||||||
|
break;
|
||||||
|
case 'connect':
|
||||||
|
details = 'Container connected to ' + eventAttr.name + ' network';
|
||||||
|
break;
|
||||||
|
case 'disconnect':
|
||||||
|
details = 'Container disconnected from ' + eventAttr.name + ' network';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
details = 'Unsupported event';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'volume':
|
||||||
|
switch (event.Action) {
|
||||||
|
case 'create':
|
||||||
|
details = 'Volume ' + event.Actor.ID + ' created';
|
||||||
|
break;
|
||||||
|
case 'destroy':
|
||||||
|
details = 'Volume ' + event.Actor.ID + ' deleted';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
details = 'Unsupported event';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
details = 'Unsupported event';
|
||||||
|
}
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EventViewModel(data) {
|
||||||
|
// Type, Action, Actor unavailable in Docker < 1.10
|
||||||
|
this.Time = data.time;
|
||||||
|
if (data.Type) {
|
||||||
|
this.Type = data.Type;
|
||||||
|
this.Details = createEventDetails(data);
|
||||||
|
} else {
|
||||||
|
this.Type = data.status;
|
||||||
|
this.Details = data.from;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -185,3 +185,8 @@ 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;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "uifordocker",
|
"name": "uifordocker",
|
||||||
"version": "1.5.0",
|
"version": "1.6.0",
|
||||||
"homepage": "https://github.com/kevana/ui-for-docker",
|
"homepage": "https://github.com/kevana/ui-for-docker",
|
||||||
"authors": [
|
"authors": [
|
||||||
"Michael Crosby <crosbymichael@gmail.com>",
|
"Michael Crosby <crosbymichael@gmail.com>",
|
||||||
|
@ -30,7 +30,6 @@
|
||||||
"angular-ui-router": "^0.2.15",
|
"angular-ui-router": "^0.2.15",
|
||||||
"angular-sanitize": "~1.5.0",
|
"angular-sanitize": "~1.5.0",
|
||||||
"angular-mocks": "~1.5.0",
|
"angular-mocks": "~1.5.0",
|
||||||
"angular-oboe": "*",
|
|
||||||
"angular-resource": "~1.5.0",
|
"angular-resource": "~1.5.0",
|
||||||
"angular-ui-select": "~0.17.1",
|
"angular-ui-select": "~0.17.1",
|
||||||
"bootstrap": "~3.3.6",
|
"bootstrap": "~3.3.6",
|
||||||
|
@ -39,6 +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",
|
||||||
|
"xterm.js": "~1.0.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"angular": "1.5.5"
|
"angular": "1.5.5"
|
||||||
|
|
BIN
dashboard.png
BIN
dashboard.png
Binary file not shown.
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 55 KiB |
243
dockerui.go
243
dockerui.go
|
@ -1,243 +0,0 @@
|
||||||
package main // import "github.com/cloudinovasi/ui-for-docker"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httputil"
|
|
||||||
"net/url"
|
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"github.com/gorilla/csrf"
|
|
||||||
"io/ioutil"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gorilla/securecookie"
|
|
||||||
"gopkg.in/alecthomas/kingpin.v2"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 = LabelParser(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
|
|
||||||
authKey []byte
|
|
||||||
authKeyFile = "authKey.dat"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UnixHandler struct {
|
|
||||||
path string
|
|
||||||
}
|
|
||||||
|
|
||||||
type TlsFlags struct {
|
|
||||||
tls bool
|
|
||||||
caPath string
|
|
||||||
certPath string
|
|
||||||
keyPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Swarm bool `json:"swarm"`
|
|
||||||
HiddenLabels Labels `json:"hiddenLabels"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Label struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Labels []Label
|
|
||||||
|
|
||||||
func (l *Labels) Set(value string) error {
|
|
||||||
parts := strings.SplitN(value, "=", 2)
|
|
||||||
if len(parts) != 2 {
|
|
||||||
return fmt.Errorf("expected HEADER=VALUE got '%s'", value)
|
|
||||||
}
|
|
||||||
label := new(Label)
|
|
||||||
label.Name = parts[0]
|
|
||||||
label.Value = parts[1]
|
|
||||||
*l = append(*l, *label)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Labels) String() string {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Labels) IsCumulative() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func LabelParser(s kingpin.Settings) (target *[]Label) {
|
|
||||||
target = new([]Label)
|
|
||||||
s.SetValue((*Labels)(target))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *UnixHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
conn, err := net.Dial("unix", h.path)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c := httputil.NewClientConn(conn, nil)
|
|
||||||
defer c.Close()
|
|
||||||
|
|
||||||
res, err := c.Do(r)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
copyHeader(w.Header(), res.Header)
|
|
||||||
if _, err := io.Copy(w, res.Body); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyHeader(dst, src http.Header) {
|
|
||||||
for k, vv := range src {
|
|
||||||
for _, v := range vv {
|
|
||||||
dst.Add(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func configurationHandler(w http.ResponseWriter, r *http.Request, c Config) {
|
|
||||||
json.NewEncoder(w).Encode(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createTcpHandler(u *url.URL) http.Handler {
|
|
||||||
u.Scheme = "http";
|
|
||||||
return httputil.NewSingleHostReverseProxy(u)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createTlsConfig(tlsFlags TlsFlags) *tls.Config {
|
|
||||||
cert, err := tls.LoadX509KeyPair(tlsFlags.certPath, tlsFlags.keyPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
caCert, err := ioutil.ReadFile(tlsFlags.caPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
caCertPool := x509.NewCertPool()
|
|
||||||
caCertPool.AppendCertsFromPEM(caCert)
|
|
||||||
tlsConfig := &tls.Config{
|
|
||||||
Certificates: []tls.Certificate{cert},
|
|
||||||
RootCAs: caCertPool,
|
|
||||||
}
|
|
||||||
return tlsConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
func createTcpHandlerWithTLS(u *url.URL, tlsFlags TlsFlags) http.Handler {
|
|
||||||
u.Scheme = "https";
|
|
||||||
var tlsConfig = createTlsConfig(tlsFlags)
|
|
||||||
proxy := httputil.NewSingleHostReverseProxy(u)
|
|
||||||
proxy.Transport = &http.Transport{
|
|
||||||
TLSClientConfig: tlsConfig,
|
|
||||||
}
|
|
||||||
return proxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
func createUnixHandler(e string) http.Handler {
|
|
||||||
return &UnixHandler{e}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createHandler(dir string, d string, e string, c Config, tlsFlags TlsFlags) http.Handler {
|
|
||||||
var (
|
|
||||||
mux = http.NewServeMux()
|
|
||||||
fileHandler = http.FileServer(http.Dir(dir))
|
|
||||||
h http.Handler
|
|
||||||
)
|
|
||||||
u, perr := url.Parse(e)
|
|
||||||
if perr != nil {
|
|
||||||
log.Fatal(perr)
|
|
||||||
}
|
|
||||||
if u.Scheme == "tcp" {
|
|
||||||
if tlsFlags.tls {
|
|
||||||
h = createTcpHandlerWithTLS(u, tlsFlags)
|
|
||||||
} else {
|
|
||||||
h = createTcpHandler(u)
|
|
||||||
}
|
|
||||||
} else if u.Scheme == "unix" {
|
|
||||||
var socketPath = u.Path
|
|
||||||
if _, err := os.Stat(socketPath); err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
log.Fatalf("unix socket %s does not exist", socketPath)
|
|
||||||
}
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
h = createUnixHandler(socketPath)
|
|
||||||
} else {
|
|
||||||
log.Fatalf("Bad Docker enpoint: %s. Only unix:// and tcp:// are supported.", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use existing csrf authKey if present or generate a new one.
|
|
||||||
var authKeyPath = d + "/" + authKeyFile
|
|
||||||
dat, err := ioutil.ReadFile(authKeyPath)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
authKey = securecookie.GenerateRandomKey(32)
|
|
||||||
err := ioutil.WriteFile(authKeyPath, authKey, 0644)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("unable to persist auth key", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
authKey = dat
|
|
||||||
}
|
|
||||||
|
|
||||||
CSRF := csrf.Protect(
|
|
||||||
authKey,
|
|
||||||
csrf.HttpOnly(false),
|
|
||||||
csrf.Secure(false),
|
|
||||||
)
|
|
||||||
|
|
||||||
mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", h))
|
|
||||||
mux.Handle("/", fileHandler)
|
|
||||||
mux.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
configurationHandler(w, r, c)
|
|
||||||
})
|
|
||||||
return CSRF(csrfWrapper(mux))
|
|
||||||
}
|
|
||||||
|
|
||||||
func csrfWrapper(h http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("X-CSRF-Token", csrf.Token(r))
|
|
||||||
h.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
kingpin.Version("1.5.0")
|
|
||||||
kingpin.Parse()
|
|
||||||
|
|
||||||
configuration := Config{
|
|
||||||
Swarm: *swarm,
|
|
||||||
HiddenLabels: *labels,
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsFlags := TlsFlags{
|
|
||||||
tls: *tlsverify,
|
|
||||||
caPath: *tlscacert,
|
|
||||||
certPath: *tlscert,
|
|
||||||
keyPath: *tlskey,
|
|
||||||
}
|
|
||||||
|
|
||||||
handler := createHandler(*assets, *data, *endpoint, configuration, tlsFlags)
|
|
||||||
if err := http.ListenAndServe(*addr, handler); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
15
gruntFile.js
15
gruntFile.js
|
@ -68,11 +68,12 @@ module.exports = function (grunt) {
|
||||||
jsTpl: ['<%= distdir %>/templates/**/*.js'],
|
jsTpl: ['<%= distdir %>/templates/**/*.js'],
|
||||||
jsVendor: [
|
jsVendor: [
|
||||||
'bower_components/jquery/dist/jquery.min.js',
|
'bower_components/jquery/dist/jquery.min.js',
|
||||||
'assets/js/jquery.gritter.js', // Using custom version to fix error in minified build due to "use strict"
|
|
||||||
'bower_components/bootstrap/dist/js/bootstrap.min.js',
|
'bower_components/bootstrap/dist/js/bootstrap.min.js',
|
||||||
'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/oboe/dist/oboe-browser.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
|
'assets/js/legend.js' // Not a bower package
|
||||||
],
|
],
|
||||||
specs: ['test/**/*.spec.js'],
|
specs: ['test/**/*.spec.js'],
|
||||||
|
@ -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: {
|
||||||
|
@ -156,7 +158,6 @@ module.exports = function (grunt) {
|
||||||
'bower_components/angular-ui-router/release/angular-ui-router.min.js',
|
'bower_components/angular-ui-router/release/angular-ui-router.min.js',
|
||||||
'bower_components/angular-resource/angular-resource.min.js',
|
'bower_components/angular-resource/angular-resource.min.js',
|
||||||
'bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js',
|
'bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js',
|
||||||
'bower_components/angular-oboe/dist/angular-oboe.min.js',
|
|
||||||
'bower_components/angular-ui-select/dist/select.min.js'],
|
'bower_components/angular-ui-select/dist/select.min.js'],
|
||||||
dest: '<%= distdir %>/js/angular.js'
|
dest: '<%= distdir %>/js/angular.js'
|
||||||
}
|
}
|
||||||
|
@ -255,10 +256,10 @@ module.exports = function (grunt) {
|
||||||
},
|
},
|
||||||
buildBinary: {
|
buildBinary: {
|
||||||
command: [
|
command: [
|
||||||
'docker run --rm -v $(pwd):/src centurylink/golang-builder',
|
'docker run --rm -v $(pwd)/api:/src centurylink/golang-builder',
|
||||||
'shasum ui-for-docker > ui-for-docker-checksum.txt',
|
'shasum api/ui-for-docker > ui-for-docker-checksum.txt',
|
||||||
'mkdir -p dist',
|
'mkdir -p dist',
|
||||||
'mv ui-for-docker dist/'
|
'mv api/ui-for-docker dist/'
|
||||||
].join(' && ')
|
].join(' && ')
|
||||||
},
|
},
|
||||||
run: {
|
run: {
|
||||||
|
|
|
@ -52,6 +52,9 @@
|
||||||
<li class="sidebar-list">
|
<li class="sidebar-list">
|
||||||
<a ui-sref="volumes">Volumes <span class="menu-icon fa fa-cubes"></span></a>
|
<a ui-sref="volumes">Volumes <span class="menu-icon fa fa-cubes"></span></a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="sidebar-list" ng-if="!config.swarm">
|
||||||
|
<a ui-sref="events">Events <span class="menu-icon fa fa-history"></span></a>
|
||||||
|
</li>
|
||||||
<li class="sidebar-list" ng-if="config.swarm">
|
<li class="sidebar-list" ng-if="config.swarm">
|
||||||
<a ui-sref="swarm">Swarm <span class="menu-icon fa fa-object-group"></span></a>
|
<a ui-sref="swarm">Swarm <span class="menu-icon fa fa-object-group"></span></a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"author": "Michael Crosby & Kevan Ahlquist",
|
"author": "Michael Crosby & Kevan Ahlquist",
|
||||||
"name": "uifordocker",
|
"name": "uifordocker",
|
||||||
"homepage": "https://github.com/kevana/ui-for-docker",
|
"homepage": "https://github.com/kevana/ui-for-docker",
|
||||||
"version": "1.5.0",
|
"version": "1.6.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git@github.com:kevana/ui-for-docker.git"
|
"url": "git@github.com:kevana/ui-for-docker.git"
|
||||||
|
|
Loading…
Reference in New Issue