mirror of https://github.com/portainer/portainer
				
				
				
			feat(global): add container exec support (#97)
							parent
							
								
									b0ebbdf68c
								
							
						
					
					
						commit
						1aaa5acbef
					
				
							
								
								
									
										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