1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-05 05:45:22 +02:00

feat(global): add container exec support (#97)

This commit is contained in:
Anthony Lapenna 2016-08-03 15:12:53 +12:00 committed by GitHub
parent b0ebbdf68c
commit 1aaa5acbef
16 changed files with 438 additions and 75 deletions

View file

@ -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,
}
}

View file

@ -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)
}

24
api/exec.go Normal file
View file

@ -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
}
}

View file

@ -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,
}
}

View file

@ -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
}

123
api/hijack.go Normal file
View file

@ -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
}

43
api/main.go Normal file
View file

@ -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)
}

View file

@ -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)
}