mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 13:55:21 +02:00
Merge branch 'feat34-container-exec' into internal
This commit is contained in:
commit
bab57e0402
16 changed files with 440 additions and 76 deletions
69
api/api.go
69
api/api.go
|
@ -1,34 +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'))
|
||||
registries = pairs(kingpin.Flag("registries", "Supported Docker registries").Short('r'))
|
||||
)
|
||||
kingpin.Parse()
|
||||
type (
|
||||
api struct {
|
||||
endpoint *url.URL
|
||||
bindAddress string
|
||||
assetPath string
|
||||
dataPath string
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
configuration := newConfig(*swarm, *labels, *registries)
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,6 @@ func newConfig(swarm bool, labels, registries 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
24
api/exec.go
Normal 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
|
||||
}
|
||||
}
|
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
|
||||
}
|
||||
|
|
123
api/hijack.go
Normal file
123
api/hijack.go
Normal 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
|
||||
}
|
45
api/main.go
Normal file
45
api/main.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package main // import "github.com/cloudinovasi/ui-for-docker"
|
||||
|
||||
import (
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
// main is the entry point of the program
|
||||
func main() {
|
||||
kingpin.Version("1.5.0")
|
||||
var (
|
||||
endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String()
|
||||
addr = kingpin.Flag("bind", "Address and port to serve UI For Docker").Default(":9000").Short('p').String()
|
||||
assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String()
|
||||
data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').String()
|
||||
tlsverify = kingpin.Flag("tlsverify", "TLS support").Default("false").Bool()
|
||||
tlscacert = kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String()
|
||||
tlscert = kingpin.Flag("tlscert", "Path to the TLS certificate file").Default("/certs/cert.pem").String()
|
||||
tlskey = kingpin.Flag("tlskey", "Path to the TLS key").Default("/certs/key.pem").String()
|
||||
swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool()
|
||||
labels = pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
|
||||
registries = pairs(kingpin.Flag("registries", "Supported Docker registries").Short('r'))
|
||||
)
|
||||
kingpin.Parse()
|
||||
|
||||
apiConfig := apiConfig{
|
||||
Endpoint: *endpoint,
|
||||
BindAddress: *addr,
|
||||
AssetPath: *assets,
|
||||
DataPath: *data,
|
||||
SwarmSupport: *swarm,
|
||||
TLSEnabled: *tlsverify,
|
||||
TLSCACertPath: *tlscacert,
|
||||
TLSCertPath: *tlscert,
|
||||
TLSKeyPath: *tlskey,
|
||||
}
|
||||
|
||||
configuration := &Config{
|
||||
Swarm: *swarm,
|
||||
HiddenLabels: *labels,
|
||||
Registries: *registries,
|
||||
}
|
||||
|
||||
api := newAPI(apiConfig)
|
||||
api.run(configuration)
|
||||
}
|
|
@ -7,13 +7,13 @@ import (
|
|||
"log"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue