From 53e6078cc50db8ca360ac8aba09b1cccb66aaabf Mon Sep 17 00:00:00 2001 From: "Jonathan Leibiusky @xetorthio" Date: Tue, 25 Jul 2017 16:36:10 -0300 Subject: [PATCH] WIP --- config/config.go | 9 ++-- docker-compose.yml | 46 ++++++------------- handlers/bootstrap.go | 14 ++---- handlers/new_session.go | 5 ++- haproxy/haproxy.cfg | 26 +++-------- provider/local_session_provider.go | 36 +++++++++++++++ provider/provider.go | 10 +++++ pwd/client_test.go | 15 ++++--- pwd/collect_stats_task.go | 7 +-- pwd/instance.go | 18 ++++---- pwd/instance_test.go | 25 ++++++++--- pwd/pwd.go | 23 +++++++--- pwd/session.go | 14 +++--- pwd/session_provider_mock_test.go | 11 +++++ pwd/session_test.go | 14 ++++-- pwd/tasks.go | 5 ++- pwd/types/instance.go | 1 + router/host.go | 71 ++++++++++++++++++++++++++++++ router/host_test.go | 45 +++++++++++++++++++ router/l2/l2.go | 33 +++++--------- router/l2/l2_test.go | 21 ++++----- www/assets/app.js | 2 +- 22 files changed, 307 insertions(+), 144 deletions(-) create mode 100644 provider/local_session_provider.go create mode 100644 provider/provider.go create mode 100644 pwd/session_provider_mock_test.go create mode 100644 router/host.go create mode 100644 router/host_test.go diff --git a/config/config.go b/config/config.go index bbeecdd..b43bc82 100644 --- a/config/config.go +++ b/config/config.go @@ -2,7 +2,6 @@ package config import ( "flag" - "log" "os" "regexp" "time" @@ -21,7 +20,7 @@ const ( var NameFilter = regexp.MustCompile(PWDHostPortGroupRegex) var AliasFilter = regexp.MustCompile(AliasPortGroupRegex) -var SSLPortNumber, PortNumber, Key, Cert, SessionsFile, PWDContainerName, PWDCName, HashKey, SSHKeyPath string +var SSLPortNumber, PortNumber, Key, Cert, SessionsFile, PWDContainerName, L2ContainerName, L2Subdomain, PWDCName, HashKey, SSHKeyPath string var MaxLoadAvg float64 func ParseFlags() { @@ -31,13 +30,13 @@ func ParseFlags() { flag.StringVar(&Cert, "cert", "./pwd/server.pem", "Give a SSL cert") flag.StringVar(&SessionsFile, "save", "./pwd/sessions", "Tell where to store sessions file") flag.StringVar(&PWDContainerName, "name", "pwd", "Container name used to run PWD (used to be able to connect it to the networks it creates)") - flag.StringVar(&PWDCName, "cname", "host1", "CNAME given to this host") + flag.StringVar(&L2ContainerName, "l2", "l2", "Container name used to run L2 Router") + flag.StringVar(&L2Subdomain, "l2-subdomain", "direct", "Subdomain to the L2 Router") + flag.StringVar(&PWDCName, "cname", "", "CNAME given to this host") flag.StringVar(&HashKey, "hash_key", "salmonrosado", "Hash key to use for cookies") flag.Float64Var(&MaxLoadAvg, "maxload", 100, "Maximum allowed load average before failing ping requests") flag.StringVar(&SSHKeyPath, "ssh_key_path", "", "SSH Private Key to use") flag.Parse() - - log.Println("*****************************", SSHKeyPath) } func GetDindImageName() string { dindImage := os.Getenv("DIND_IMAGE") diff --git a/docker-compose.yml b/docker-compose.yml index 8bd8bf3..7cfc124 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,21 +1,20 @@ version: '3.2' services: haproxy: - container_name: proxy + container_name: haproxy image: haproxy ports: - "80:8080" - - "443:8443" - volumes: - ./haproxy:/usr/local/etc/haproxy - pwd1: + + pwd: # pwd daemon container always needs to be named this way - container_name: pwd1 + container_name: pwd # use the latest golang image image: golang # go to the right place and starts the app - command: /bin/sh -c 'ssh-keygen -N "" -t rsa -f /etc/ssh/ssh_host_rsa_key >/dev/null; cd /go/src/github.com/play-with-docker/play-with-docker; go run api.go -save /pwd/sessions1 -name pwd1 -cname host1' + command: /bin/sh -c 'ssh-keygen -N "" -t rsa -f /etc/ssh/ssh_host_rsa_key >/dev/null; cd /go/src/github.com/play-with-docker/play-with-docker; go run api.go -save /pwd/sessions -name l2' volumes: # since this app creates networks and launches containers, we need to talk to docker daemon - /var/run/docker.sock:/var/run/docker.sock @@ -24,37 +23,20 @@ services: - sessions:/pwd environment: GOOGLE_RECAPTCHA_DISABLED: "true" - ports: - - "1022:1022" - pwd2: - # pwd daemon container always needs to be named this way - container_name: pwd2 + l2: + container_name: l2 # use the latest golang image image: golang # go to the right place and starts the app - command: /bin/sh -c 'ssh-keygen -N "" -t rsa -f /etc/ssh/ssh_host_rsa_key >/dev/null; cd /go/src/github.com/play-with-docker/play-with-docker; go run api.go -save /pwd/sessions2 -name pwd2 -cname host2' + command: /bin/sh -c 'ssh-keygen -N "" -t rsa -f /etc/ssh/ssh_host_rsa_key >/dev/null; cd /go/src/github.com/play-with-docker/play-with-docker/router/l2; go run l2.go -ssh_key_path /etc/ssh/ssh_host_rsa_key -name l2 -save /pwd/networks' volumes: - # since this app creates networks and launches containers, we need to talk to docker daemon - - /var/run/docker.sock:/var/run/docker.sock - # mount the box mounted shared folder to the container - $GOPATH/src:/go/src - - sessions:/pwd - environment: - GOOGLE_RECAPTCHA_DISABLED: "true" + - /var/run/docker.sock:/var/run/docker.sock + - networks:/pwd ports: - - "1023:1022" - prometheus: - container_name: prometheus - image: prom/prometheus - volumes: - - ./prometheus.yml:/etc/prometheus/prometheus.yml - grafana: - container_name: grafana - image: grafana/grafana - ports: - - "3000:3000" - volumes: - - grafana:/var/lib/grafana + - "8022:22" + - "8053:53" + - "443:443" volumes: sessions: - grafana: + networks: diff --git a/handlers/bootstrap.go b/handlers/bootstrap.go index 1342b6c..be32d59 100644 --- a/handlers/bootstrap.go +++ b/handlers/bootstrap.go @@ -4,11 +4,10 @@ import ( "log" "os" - "github.com/docker/docker/client" "github.com/googollee/go-socket.io" "github.com/play-with-docker/play-with-docker/config" - "github.com/play-with-docker/play-with-docker/docker" "github.com/play-with-docker/play-with-docker/event" + "github.com/play-with-docker/play-with-docker/provider" "github.com/play-with-docker/play-with-docker/pwd" "github.com/play-with-docker/play-with-docker/storage" ) @@ -18,23 +17,18 @@ var e event.EventApi var ws *socketio.Server func Bootstrap() { - c, err := client.NewEnvClient() - if err != nil { - log.Fatal(err) - } - - d := docker.NewDocker(c) + sp := provider.NewLocalSessionProvider() e = event.NewLocalBroker() - t := pwd.NewScheduler(e, d) + t := pwd.NewScheduler(e, sp) s, err := storage.NewFileStorage(config.SessionsFile) if err != nil && !os.IsNotExist(err) { log.Fatal("Error initializing StorageAPI: ", err) } - core = pwd.NewPWD(d, t, e, s) + core = pwd.NewPWD(sp, t, e, s) } diff --git a/handlers/new_session.go b/handlers/new_session.go index b0a98bd..161ba24 100644 --- a/handlers/new_session.go +++ b/handlers/new_session.go @@ -49,7 +49,10 @@ func NewSession(rw http.ResponseWriter, req *http.Request) { log.Println(err) //TODO: Return some error code } else { - hostname := fmt.Sprintf("%s.%s", config.PWDCName, req.Host) + hostname := req.Host + if config.PWDCName != "" { + hostname = fmt.Sprintf("%s.%s", config.PWDCName, req.Host) + } // If request is not a form, return sessionId in the body if req.Header.Get("X-Requested-With") == "XMLHttpRequest" { resp := NewSessionResponse{SessionId: s.Id, Hostname: hostname} diff --git a/haproxy/haproxy.cfg b/haproxy/haproxy.cfg index 481c651..e92965b 100644 --- a/haproxy/haproxy.cfg +++ b/haproxy/haproxy.cfg @@ -8,25 +8,13 @@ frontend http-in bind *:8080 acl host_localhost hdr(host) localhost - acl host_pwd1 hdr_reg(host) -i ^.*\.?host1\.localhost?:?.*$ - acl host_pwd2 hdr_reg(host) -i ^.*\.?host2\.localhost?:?.*$ + acl host_direct_localhost hdr_reg(host) -i ^.*\.direct\.localhost?:?.*$ - use_backend all if host_localhost - use_backend pwd1 if host_pwd1 - use_backend pwd2 if host_pwd2 + use_backend pwd if host_localhost + use_backend l2 if host_direct_localhost -backend all - balance roundrobin +backend pwd + server node1 pwd:3000 - option httpchk GET /ping HTTP/1.0 - http-check expect rstatus 200 - default-server inter 3s fall 3 rise 2 - - server node1 pwd1:3000 check - server node2 pwd2:3000 check - -backend pwd1 - server node1 pwd1:3000 - -backend pwd2 - server node2 pwd2:3000 +backend l2 + server node2 l2:443 diff --git a/provider/local_session_provider.go b/provider/local_session_provider.go new file mode 100644 index 0000000..bf1936f --- /dev/null +++ b/provider/local_session_provider.go @@ -0,0 +1,36 @@ +package provider + +import ( + "sync" + + "github.com/docker/docker/client" + "github.com/play-with-docker/play-with-docker/docker" +) + +type localSessionProvider struct { + rw sync.Mutex + + docker docker.DockerApi +} + +func (p *localSessionProvider) GetDocker(sessionId string) (docker.DockerApi, error) { + p.rw.Lock() + defer p.rw.Unlock() + + if p.docker != nil { + return p.docker, nil + } + + c, err := client.NewEnvClient() + if err != nil { + return nil, err + } + d := docker.NewDocker(c) + + p.docker = d + return d, nil +} + +func NewLocalSessionProvider() *localSessionProvider { + return &localSessionProvider{} +} diff --git a/provider/provider.go b/provider/provider.go new file mode 100644 index 0000000..7c99152 --- /dev/null +++ b/provider/provider.go @@ -0,0 +1,10 @@ +package provider + +import "github.com/play-with-docker/play-with-docker/docker" + +type InstanceProvider interface { +} + +type SessionProvider interface { + GetDocker(sessionId string) (docker.DockerApi, error) +} diff --git a/pwd/client_test.go b/pwd/client_test.go index c0cffd1..cf687dd 100644 --- a/pwd/client_test.go +++ b/pwd/client_test.go @@ -11,12 +11,13 @@ import ( ) func TestClientNew(t *testing.T) { - docker := &mockDocker{} + d := &mockDocker{} tasks := &mockTasks{} e := event.NewLocalBroker() storage := &mockStorage{} + sp := &mockSessionProvider{docker: d} - p := NewPWD(docker, tasks, e, storage) + p := NewPWD(sp, tasks, e, storage) session, err := p.SessionNew(time.Hour, "", "", "") assert.Nil(t, err) @@ -27,12 +28,13 @@ func TestClientNew(t *testing.T) { assert.Contains(t, session.Clients, client) } func TestClientCount(t *testing.T) { - docker := &mockDocker{} + d := &mockDocker{} tasks := &mockTasks{} e := event.NewLocalBroker() storage := &mockStorage{} + sp := &mockSessionProvider{docker: d} - p := NewPWD(docker, tasks, e, storage) + p := NewPWD(sp, tasks, e, storage) session, err := p.SessionNew(time.Hour, "", "", "") assert.Nil(t, err) @@ -45,9 +47,10 @@ func TestClientCount(t *testing.T) { func TestClientResizeViewPort(t *testing.T) { wg := sync.WaitGroup{} wg.Add(1) - docker := &mockDocker{} + d := &mockDocker{} tasks := &mockTasks{} e := event.NewLocalBroker() + sp := &mockSessionProvider{docker: d} broadcastedSessionId := "" broadcastedArgs := []interface{}{} @@ -60,7 +63,7 @@ func TestClientResizeViewPort(t *testing.T) { storage := &mockStorage{} - p := NewPWD(docker, tasks, e, storage) + p := NewPWD(sp, tasks, e, storage) session, err := p.SessionNew(time.Hour, "", "", "") assert.Nil(t, err) diff --git a/pwd/collect_stats_task.go b/pwd/collect_stats_task.go index 828d190..4eb36d8 100644 --- a/pwd/collect_stats_task.go +++ b/pwd/collect_stats_task.go @@ -7,7 +7,7 @@ import ( dockerTypes "github.com/docker/docker/api/types" units "github.com/docker/go-units" - "github.com/play-with-docker/play-with-docker/docker" + "github.com/play-with-docker/play-with-docker/provider" "github.com/play-with-docker/play-with-docker/pwd/types" ) @@ -20,11 +20,12 @@ type collectStatsTask struct { previousCPU uint64 previousSystem uint64 - docker docker.DockerApi + sessionProvider provider.SessionProvider } func (c collectStatsTask) Run(i *types.Instance) error { - reader, err := c.docker.GetContainerStats(i.Name) + docker, _ := c.sessionProvider.GetDocker(i.SessionId) + reader, err := docker.GetContainerStats(i.Name) if err != nil { log.Println("Error while trying to collect instance stats", err) return err diff --git a/pwd/instance.go b/pwd/instance.go index dbd1d9e..edebd0e 100644 --- a/pwd/instance.go +++ b/pwd/instance.go @@ -15,6 +15,7 @@ import ( "github.com/play-with-docker/play-with-docker/docker" "github.com/play-with-docker/play-with-docker/event" "github.com/play-with-docker/play-with-docker/pwd/types" + "github.com/play-with-docker/play-with-docker/router" "golang.org/x/text/encoding" ) @@ -46,7 +47,7 @@ type InstanceConfig struct { func (p *pwd) InstanceResizeTerminal(instance *types.Instance, rows, cols uint) error { defer observeAction("InstanceResizeTerminal", time.Now()) - return p.docker.ContainerResize(instance.Name, rows, cols) + return p.docker(instance.SessionId).ContainerResize(instance.Name, rows, cols) } func (p *pwd) InstanceAttachTerminal(instance *types.Instance) error { @@ -54,7 +55,7 @@ func (p *pwd) InstanceAttachTerminal(instance *types.Instance) error { if getInstanceTermConn(instance.SessionId, instance.Name) != nil { return nil } - conn, err := p.docker.CreateAttachConnection(instance.Name) + conn, err := p.docker(instance.SessionId).CreateAttachConnection(instance.Name) if err != nil { return err @@ -84,7 +85,7 @@ func (p *pwd) InstanceUploadFromUrl(instance *types.Instance, fileName, dest str return fmt.Errorf("Could not download file [%s]. Status code: %d\n", url, resp.StatusCode) } - copyErr := p.docker.CopyToContainer(instance.Name, dest, fileName, resp.Body) + copyErr := p.docker(instance.SessionId).CopyToContainer(instance.Name, dest, fileName, resp.Body) if copyErr != nil { return fmt.Errorf("Error while downloading file [%s]. Error: %s\n", url, copyErr) @@ -96,7 +97,7 @@ func (p *pwd) InstanceUploadFromUrl(instance *types.Instance, fileName, dest str func (p *pwd) getInstanceCWD(instance *types.Instance) (string, error) { b := bytes.NewBufferString("") - if c, err := p.docker.ExecAttach(instance.Name, []string{"bash", "-c", `pwdx $( 0 { + if c, err := p.docker(instance.SessionId).ExecAttach(instance.Name, []string{"bash", "-c", `pwdx $( 0 { log.Println(b.String()) return "", fmt.Errorf("Error %d trying to get CWD", c) } else if err != nil { @@ -122,7 +123,7 @@ func (p *pwd) InstanceUploadFromReader(instance *types.Instance, fileName, dest } } - copyErr := p.docker.CopyToContainer(instance.Name, finalDest, fileName, reader) + copyErr := p.docker(instance.SessionId).CopyToContainer(instance.Name, finalDest, fileName, reader) if copyErr != nil { return fmt.Errorf("Error while uploading file [%s]. Error: %s\n", fileName, copyErr) @@ -171,7 +172,7 @@ func (p *pwd) InstanceDelete(session *types.Session, instance *types.Instance) e if conn != nil { conn.Close() } - err := p.docker.DeleteContainer(instance.Name) + err := p.docker(session.Id).DeleteContainer(instance.Name) if err != nil && !strings.Contains(err.Error(), "No such container") { log.Println(err) return err @@ -243,7 +244,7 @@ func (p *pwd) InstanceNew(session *types.Session, conf InstanceConfig) (*types.I } } - ip, err := p.docker.CreateContainer(opts) + ip, err := p.docker(session.Id).CreateContainer(opts) if err != nil { return nil, err } @@ -261,6 +262,7 @@ func (p *pwd) InstanceNew(session *types.Session, conf InstanceConfig) (*types.I instance.ServerKey = conf.ServerKey instance.CACert = conf.CACert instance.Session = session + instance.Proxy = router.EncodeHost(session.Id, ip, router.HostOpts{}) // For now this condition holds through. In the future we might need a more complex logic. instance.IsDockerHost = opts.Privileged @@ -304,7 +306,7 @@ func (p *pwd) InstanceAllowedImages() []string { func (p *pwd) InstanceExec(instance *types.Instance, cmd []string) (int, error) { defer observeAction("InstanceExec", time.Now()) - return p.docker.Exec(instance.Name, cmd) + return p.docker(instance.SessionId).Exec(instance.Name, cmd) } func getInstanceTermConn(sessionId, instanceName string) net.Conn { diff --git a/pwd/instance_test.go b/pwd/instance_test.go index 308ea5a..804ac49 100644 --- a/pwd/instance_test.go +++ b/pwd/instance_test.go @@ -12,6 +12,7 @@ import ( "github.com/play-with-docker/play-with-docker/docker" "github.com/play-with-docker/play-with-docker/event" "github.com/play-with-docker/play-with-docker/pwd/types" + "github.com/play-with-docker/play-with-docker/router" "github.com/stretchr/testify/assert" ) @@ -28,12 +29,13 @@ func TestInstanceResizeTerminal(t *testing.T) { return nil } + sp := &mockSessionProvider{docker: docker} tasks := &mockTasks{} e := event.NewLocalBroker() storage := &mockStorage{} - p := NewPWD(docker, tasks, e, storage) + p := NewPWD(sp, tasks, e, storage) err := p.InstanceResizeTerminal(&types.Instance{Name: "foobar"}, 24, 80) @@ -50,12 +52,13 @@ func TestInstanceNew(t *testing.T) { containerOpts = opts return "10.0.0.1", nil } + sp := &mockSessionProvider{docker: dock} tasks := &mockTasks{} e := event.NewLocalBroker() storage := &mockStorage{} - p := NewPWD(dock, tasks, e, storage) + p := NewPWD(sp, tasks, e, storage) session, err := p.SessionNew(time.Hour, "", "", "") @@ -74,6 +77,7 @@ func TestInstanceNew(t *testing.T) { IsDockerHost: true, SessionId: session.Id, Session: session, + Proxy: router.EncodeHost(session.Id, "10.0.0.1", router.HostOpts{}), } assert.Equal(t, expectedInstance, *instance) @@ -101,12 +105,13 @@ func TestInstanceNew_Concurrency(t *testing.T) { i++ return fmt.Sprintf("10.0.0.%d", i), nil } + sp := &mockSessionProvider{docker: dock} tasks := &mockTasks{} e := event.NewLocalBroker() storage := &mockStorage{} - p := NewPWD(dock, tasks, e, storage) + p := NewPWD(sp, tasks, e, storage) session, err := p.SessionNew(time.Hour, "", "", "") @@ -142,12 +147,13 @@ func TestInstanceNew_WithNotAllowedImage(t *testing.T) { containerOpts = opts return "10.0.0.1", nil } + sp := &mockSessionProvider{docker: dock} tasks := &mockTasks{} e := event.NewLocalBroker() storage := &mockStorage{} - p := NewPWD(dock, tasks, e, storage) + p := NewPWD(sp, tasks, e, storage) session, err := p.SessionNew(time.Hour, "", "", "") @@ -166,6 +172,7 @@ func TestInstanceNew_WithNotAllowedImage(t *testing.T) { SessionId: session.Id, IsDockerHost: false, Session: session, + Proxy: instance.Proxy, } assert.Equal(t, expectedInstance, *instance) @@ -191,12 +198,13 @@ func TestInstanceNew_WithCustomHostname(t *testing.T) { containerOpts = opts return "10.0.0.1", nil } + sp := &mockSessionProvider{docker: dock} tasks := &mockTasks{} e := event.NewLocalBroker() storage := &mockStorage{} - p := NewPWD(dock, tasks, e, storage) + p := NewPWD(sp, tasks, e, storage) session, err := p.SessionNew(time.Hour, "", "", "") @@ -215,6 +223,7 @@ func TestInstanceNew_WithCustomHostname(t *testing.T) { IsDockerHost: false, Session: session, SessionId: session.Id, + Proxy: instance.Proxy, } assert.Equal(t, expectedInstance, *instance) @@ -238,8 +247,9 @@ func TestInstanceAllowedImages(t *testing.T) { tasks := &mockTasks{} e := event.NewLocalBroker() storage := &mockStorage{} + sp := &mockSessionProvider{docker: dock} - p := NewPWD(dock, tasks, e, storage) + p := NewPWD(sp, tasks, e, storage) expectedImages := []string{config.GetDindImageName(), "franela/dind:overlay2-dev", "franela/ucp:2.4.1"} @@ -264,8 +274,9 @@ func TestTermConnAssignment(t *testing.T) { // return error connection to unlock the goroutine return errConn{}, nil } + sp := &mockSessionProvider{docker: dock} - p := NewPWD(dock, tasks, e, storage) + p := NewPWD(sp, tasks, e, storage) session, _ := p.SessionNew(time.Hour, "", "", "") mockInstance := &types.Instance{ Name: fmt.Sprintf("%s_redis-master", session.Id[:8]), diff --git a/pwd/pwd.go b/pwd/pwd.go index 8300c9a..d2b3842 100644 --- a/pwd/pwd.go +++ b/pwd/pwd.go @@ -6,6 +6,7 @@ import ( "github.com/play-with-docker/play-with-docker/docker" "github.com/play-with-docker/play-with-docker/event" + "github.com/play-with-docker/play-with-docker/provider" "github.com/play-with-docker/play-with-docker/pwd/types" "github.com/play-with-docker/play-with-docker/storage" "github.com/prometheus/client_golang/prometheus" @@ -44,11 +45,11 @@ func init() { } type pwd struct { - docker docker.DockerApi - tasks SchedulerApi - event event.EventApi - storage storage.StorageApi - clientCount int32 + sessionProvider provider.SessionProvider + tasks SchedulerApi + event event.EventApi + storage storage.StorageApi + clientCount int32 } type PWDApi interface { @@ -80,8 +81,16 @@ type PWDApi interface { ClientCount() int } -func NewPWD(d docker.DockerApi, t SchedulerApi, e event.EventApi, s storage.StorageApi) *pwd { - return &pwd{docker: d, tasks: t, event: e, storage: s} +func NewPWD(sp provider.SessionProvider, t SchedulerApi, e event.EventApi, s storage.StorageApi) *pwd { + return &pwd{sessionProvider: sp, tasks: t, event: e, storage: s} +} + +func (p *pwd) docker(sessionId string) docker.DockerApi { + d, err := p.sessionProvider.GetDocker(sessionId) + if err != nil { + panic("Should not have got here. Session always need to be validated before calling this.") + } + return d } func (p *pwd) setGauges() { diff --git a/pwd/session.go b/pwd/session.go index 09068e7..65d26fa 100644 --- a/pwd/session.go +++ b/pwd/session.go @@ -62,7 +62,7 @@ func (p *pwd) SessionNew(duration time.Duration, stack, stackName, imageName str log.Printf("NewSession id=[%s]\n", s.Id) - if err := p.docker.CreateNetwork(s.Id); err != nil { + if err := p.docker(s.Id).CreateNetwork(s.Id); err != nil { log.Println("ERROR NETWORKING") return nil, err } @@ -101,14 +101,14 @@ func (p *pwd) SessionClose(s *types.Session) error { } } // Disconnect PWD daemon from the network - if err := p.docker.DisconnectNetwork(config.PWDContainerName, s.Id); err != nil { + if err := p.docker(s.Id).DisconnectNetwork(config.PWDContainerName, s.Id); err != nil { if !strings.Contains(err.Error(), "is not connected to the network") { log.Println("ERROR NETWORKING") return err } } log.Printf("Disconnected pwd from network [%s]\n", s.Id) - if err := p.docker.DeleteNetwork(s.Id); err != nil { + if err := p.docker(s.Id).DeleteNetwork(s.Id); err != nil { if !strings.Contains(err.Error(), "not found") { log.Println(err) return err @@ -168,7 +168,7 @@ func (p *pwd) SessionDeployStack(s *types.Session) error { cmd := fmt.Sprintf("docker swarm init --advertise-addr eth0 && docker-compose -f %s pull && docker stack deploy -c %s %s", file, file, s.StackName) w := sessionBuilderWriter{sessionId: s.Id, event: p.event} - code, err := p.docker.ExecAttach(i.Name, []string{"sh", "-c", cmd}, &w) + code, err := p.docker(s.Id).ExecAttach(i.Name, []string{"sh", "-c", cmd}, &w) if err != nil { log.Printf("Error executing stack [%s]: %s\n", s.Stack, err) return err @@ -218,7 +218,7 @@ func (p *pwd) SessionSetup(session *types.Session, conf SessionSetupConf) error return err } if i.Docker == nil { - dock, err := p.docker.New(i.IP, i.Cert, i.Key) + dock, err := p.docker(session.Id).New(i.IP, i.Cert, i.Key) if err != nil { return err } @@ -254,7 +254,7 @@ func (p *pwd) SessionSetup(session *types.Session, conf SessionSetupConf) error if c.IsSwarmManager || c.IsSwarmWorker { // check if we have connection to the daemon, if not, create it if i.Docker == nil { - dock, err := p.docker.New(i.IP, i.Cert, i.Key) + dock, err := p.docker(session.Id).New(i.IP, i.Cert, i.Key) if err != nil { log.Println(err) return @@ -334,7 +334,7 @@ func (p *pwd) scheduleSessionClose(s *types.Session) { } func (p *pwd) connectToNetwork(s *types.Session) error { - ip, err := p.docker.ConnectNetwork(config.PWDContainerName, s.Id, s.PwdIpAddress) + ip, err := p.docker(s.Id).ConnectNetwork(config.PWDContainerName, s.Id, s.PwdIpAddress) if err != nil { log.Println("ERROR NETWORKING") return err diff --git a/pwd/session_provider_mock_test.go b/pwd/session_provider_mock_test.go new file mode 100644 index 0000000..04a2c14 --- /dev/null +++ b/pwd/session_provider_mock_test.go @@ -0,0 +1,11 @@ +package pwd + +import "github.com/play-with-docker/play-with-docker/docker" + +type mockSessionProvider struct { + docker docker.DockerApi +} + +func (p *mockSessionProvider) GetDocker(sessionId string) (docker.DockerApi, error) { + return p.docker, nil +} diff --git a/pwd/session_test.go b/pwd/session_test.go index fb0f22a..cb8e6d3 100644 --- a/pwd/session_test.go +++ b/pwd/session_test.go @@ -30,6 +30,7 @@ func TestSessionNew(t *testing.T) { connectIP = ip return "10.0.0.1", nil } + sp := &mockSessionProvider{docker: docker} var scheduledSession *types.Session tasks := &mockTasks{} @@ -44,7 +45,7 @@ func TestSessionNew(t *testing.T) { return nil } - p := NewPWD(docker, tasks, ev, storage) + p := NewPWD(sp, tasks, ev, storage) before := time.Now() @@ -166,11 +167,12 @@ func TestSessionSetup(t *testing.T) { assert.Fail(t, "Shouldn't have reached here.") return nil, nil } + sp := &mockSessionProvider{docker: dock} tasks := &mockTasks{} ev := event.NewLocalBroker() storage := &mockStorage{} - p := NewPWD(dock, tasks, ev, storage) + p := NewPWD(sp, tasks, ev, storage) s, e := p.SessionNew(time.Hour, "", "", "") assert.Nil(t, e) @@ -215,6 +217,7 @@ func TestSessionSetup(t *testing.T) { IsDockerHost: true, Session: s, Docker: manager1Received.Docker, + Proxy: manager1Received.Proxy, }, manager1Received) manager2 := fmt.Sprintf("%s_manager2", s.Id[:8]) @@ -229,6 +232,7 @@ func TestSessionSetup(t *testing.T) { SessionId: s.Id, Session: s, Docker: manager2Received.Docker, + Proxy: manager2Received.Proxy, }, manager2Received) manager3 := fmt.Sprintf("%s_manager3", s.Id[:8]) @@ -243,6 +247,7 @@ func TestSessionSetup(t *testing.T) { IsDockerHost: true, Session: s, Docker: manager3Received.Docker, + Proxy: manager3Received.Proxy, }, manager3Received) worker1 := fmt.Sprintf("%s_worker1", s.Id[:8]) @@ -257,6 +262,7 @@ func TestSessionSetup(t *testing.T) { IsDockerHost: true, Session: s, Docker: worker1Received.Docker, + Proxy: worker1Received.Proxy, }, worker1Received) other := fmt.Sprintf("%s_other", s.Id[:8]) @@ -271,6 +277,7 @@ func TestSessionSetup(t *testing.T) { IsDockerHost: true, Session: s, Docker: otherReceived.Docker, + Proxy: otherReceived.Proxy, }, otherReceived) assert.True(t, swarmInitOnMaster1) @@ -284,8 +291,9 @@ func TestSessionPrepareOnce(t *testing.T) { tasks := &mockTasks{} ev := event.NewLocalBroker() storage := &mockStorage{} + sp := &mockSessionProvider{docker: dock} - p := NewPWD(dock, tasks, ev, storage) + p := NewPWD(sp, tasks, ev, storage) session := &types.Session{Id: "1234"} prepared, err := p.prepareSession(session) assert.True(t, preparedSessions[session.Id]) diff --git a/pwd/tasks.go b/pwd/tasks.go index 412c1f2..7d23b5b 100644 --- a/pwd/tasks.go +++ b/pwd/tasks.go @@ -16,6 +16,7 @@ import ( "github.com/docker/go-connections/tlsconfig" "github.com/play-with-docker/play-with-docker/docker" "github.com/play-with-docker/play-with-docker/event" + "github.com/play-with-docker/play-with-docker/provider" "github.com/play-with-docker/play-with-docker/pwd/types" ) @@ -112,8 +113,8 @@ func (sch *scheduler) Schedule(s *types.Session) { func (sch *scheduler) Unschedule(s *types.Session) { } -func NewScheduler(e event.EventApi, d docker.DockerApi) *scheduler { +func NewScheduler(e event.EventApi, sp provider.SessionProvider) *scheduler { s := &scheduler{event: e} - s.periodicTasks = []periodicTask{&collectStatsTask{docker: d}, &checkSwarmStatusTask{}, &checkUsedPortsTask{}, &checkSwarmUsedPortsTask{}} + s.periodicTasks = []periodicTask{&collectStatsTask{sessionProvider: sp}, &checkSwarmStatusTask{}, &checkUsedPortsTask{}, &checkSwarmUsedPortsTask{}} return s } diff --git a/pwd/types/instance.go b/pwd/types/instance.go index e9f40ed..b87a10c 100644 --- a/pwd/types/instance.go +++ b/pwd/types/instance.go @@ -30,6 +30,7 @@ type Instance struct { IsDockerHost bool `json:"is_docker_host" bson:"is_docker_host"` SessionId string `json:"session_id" bson:"session_id"` SessionPrefix string `json:"session_prefix" bson:"session_prefix"` + Proxy string `json:"proxy" bson:"proxy"` Docker docker.DockerApi `json:"-"` Session *Session `json:"-" bson:"-"` ctx context.Context `json:"-" bson:"-"` diff --git a/router/host.go b/router/host.go new file mode 100644 index 0000000..4cc079c --- /dev/null +++ b/router/host.go @@ -0,0 +1,71 @@ +package router + +import ( + "fmt" + "regexp" + "strconv" + "strings" +) + +const hostPattern = "^.*ip([0-9]{1,3}-[0-9]{1,3}-[0-9]{1,3}-[0-9]{1,3})-([0-9|a-z]+)(?:-?([0-9]{1,5}))?(?:\\.([a-z|A-Z|0-9|_|\\-\\.]+))?(?:\\:([0-9]{1,5}))?$" + +var hostRegex *regexp.Regexp + +func init() { + hostRegex = regexp.MustCompile(hostPattern) +} + +type HostOpts struct { + TLD string + EncodedPort int + Port int +} + +type HostInfo struct { + SessionId string + InstanceIP string + TLD string + EncodedPort int + Port int +} + +func EncodeHost(sessionId, instanceIP string, opts HostOpts) string { + encodedIP := strings.Replace(instanceIP, ".", "-", -1) + + sub := fmt.Sprintf("ip%s-%s", encodedIP, sessionId) + if opts.EncodedPort > 0 { + sub = fmt.Sprintf("%s-%d", sub, opts.EncodedPort) + } + if opts.TLD != "" { + sub = fmt.Sprintf("%s.%s", sub, opts.TLD) + } + if opts.Port > 0 { + sub = fmt.Sprintf("%s:%d", sub, opts.Port) + } + + return sub +} + +func DecodeHost(host string) (HostInfo, error) { + info := HostInfo{} + + matches := hostRegex.FindStringSubmatch(host) + if len(matches) != 6 { + return HostInfo{}, fmt.Errorf("Couldn't find host in string") + } + + info.InstanceIP = strings.Replace(matches[1], "-", ".", -1) + info.SessionId = matches[2] + info.TLD = matches[4] + + if matches[3] != "" { + i, _ := strconv.Atoi(matches[3]) + info.EncodedPort = i + } + if matches[5] != "" { + i, _ := strconv.Atoi(matches[5]) + info.Port = i + } + + return info, nil +} diff --git a/router/host_test.go b/router/host_test.go new file mode 100644 index 0000000..6deea3e --- /dev/null +++ b/router/host_test.go @@ -0,0 +1,45 @@ +package router + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEncodeHostInfo(t *testing.T) { + host := EncodeHost("aaabbbcccddd", "10.0.0.1", HostOpts{}) + assert.Equal(t, "ip10-0-0-1-aaabbbcccddd", host) + + opts := HostOpts{EncodedPort: 8080} + host = EncodeHost("aaabbbcccddd", "10.0.0.1", opts) + assert.Equal(t, "ip10-0-0-1-aaabbbcccddd-8080", host) + + opts = HostOpts{TLD: "foo.bar"} + host = EncodeHost("aaabbbcccddd", "10.0.0.1", opts) + assert.Equal(t, "ip10-0-0-1-aaabbbcccddd.foo.bar", host) + + opts = HostOpts{TLD: "foo.bar", EncodedPort: 8080, Port: 443} + host = EncodeHost("aaabbbcccddd", "10.0.0.1", opts) + assert.Equal(t, "ip10-0-0-1-aaabbbcccddd-8080.foo.bar:443", host) +} + +func TestDecodeHostInfo(t *testing.T) { + info, err := DecodeHost("ip10-0-0-1-aaabbbcccddd") + assert.Nil(t, err) + assert.Equal(t, HostInfo{InstanceIP: "10.0.0.1", SessionId: "aaabbbcccddd"}, info) + + info, err = DecodeHost("ip10-0-0-1-aaabbbcccddd-8080") + assert.Nil(t, err) + assert.Equal(t, HostInfo{InstanceIP: "10.0.0.1", SessionId: "aaabbbcccddd", EncodedPort: 8080}, info) + + info, err = DecodeHost("ip10-0-0-1-aaabbbcccddd-8080.foo.bar") + assert.Nil(t, err) + assert.Equal(t, HostInfo{InstanceIP: "10.0.0.1", SessionId: "aaabbbcccddd", EncodedPort: 8080, TLD: "foo.bar"}, info) + + info, err = DecodeHost("ip10-0-0-1-aaabbbcccddd-8080.foo.bar:443") + assert.Nil(t, err) + assert.Equal(t, HostInfo{InstanceIP: "10.0.0.1", SessionId: "aaabbbcccddd", EncodedPort: 8080, TLD: "foo.bar", Port: 443}, info) + + _, err = DecodeHost("ip10-0-0-1") + assert.NotNil(t, err) +} diff --git a/router/l2/l2.go b/router/l2/l2.go index 1cce468..416c095 100644 --- a/router/l2/l2.go +++ b/router/l2/l2.go @@ -7,7 +7,6 @@ import ( "log" "net" "os" - "strings" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" @@ -18,31 +17,23 @@ import ( ) func director(host string) (*net.TCPAddr, error) { - chunks := strings.Split(host, ":") - matches := config.NameFilter.FindStringSubmatch(chunks[0]) - - var rawHost, port string - - if len(matches) == 3 { - rawHost = matches[1] - port = matches[2] - } else if len(matches) == 2 { - rawHost = matches[1] - } else { - return nil, fmt.Errorf("Couldn't find host in string") + info, err := router.DecodeHost(host) + if err != nil { + return nil, err } - if port == "" { - if len(chunks) == 2 { - port = chunks[1] - } else { - port = "80" - } + port := info.Port + + if info.EncodedPort > 0 { + port = info.EncodedPort } - dstHost := strings.Replace(rawHost, "-", ".", -1) + if port == 0 { + // TODO: Should default depending on the protocol + port = 80 + } - t, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%s:%s", dstHost, port)) + t, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%s:%d", info.InstanceIP, port)) if err != nil { return nil, err } diff --git a/router/l2/l2_test.go b/router/l2/l2_test.go index 70f3fcf..5b31040 100644 --- a/router/l2/l2_test.go +++ b/router/l2/l2_test.go @@ -7,41 +7,38 @@ import ( ) func TestDirector(t *testing.T) { - addr, err := director("ip10-0-0-1-8080.foo.bar") + addr, err := director("ip10-0-0-1-aabb-8080.foo.bar") assert.Nil(t, err) assert.Equal(t, "10.0.0.1:8080", addr.String()) - addr, err = director("ip10-0-0-1.foo.bar") + addr, err = director("ip10-0-0-1-aabb.foo.bar") assert.Nil(t, err) assert.Equal(t, "10.0.0.1:80", addr.String()) - addr, err = director("ip10-0-0-1.foo.bar:9090") + addr, err = director("ip10-0-0-1-aabb.foo.bar:9090") assert.Nil(t, err) assert.Equal(t, "10.0.0.1:9090", addr.String()) - addr, err = director("ip10-0-0-1-2222.foo.bar:9090") + addr, err = director("ip10-0-0-1-aabb-2222.foo.bar:9090") assert.Nil(t, err) assert.Equal(t, "10.0.0.1:2222", addr.String()) - addr, err = director("lala.ip10-0-0-1-2222.foo.bar") + addr, err = director("lala.ip10-0-0-1-aabb-2222.foo.bar") assert.Nil(t, err) assert.Equal(t, "10.0.0.1:2222", addr.String()) - addr, err = director("lala.ip10-0-0-1-2222") + addr, err = director("lala.ip10-0-0-1-aabb-2222") assert.Nil(t, err) assert.Equal(t, "10.0.0.1:2222", addr.String()) - addr, err = director("ip10-0-0-1-2222") + addr, err = director("ip10-0-0-1-aabb-2222") assert.Nil(t, err) assert.Equal(t, "10.0.0.1:2222", addr.String()) - addr, err = director("ip10-0-0-1") + addr, err = director("ip10-0-0-1-aabb") assert.Nil(t, err) assert.Equal(t, "10.0.0.1:80", addr.String()) - _, err = director("lala10-0-0-1.foo.bar") - assert.NotNil(t, err) - - _, err = director("ip10-0-0-1-10-20") + _, err = director("lala10-0-0-1-aabb.foo.bar") assert.NotNil(t, err) } diff --git a/www/assets/app.js b/www/assets/app.js index 0f750ef..418f9fa 100644 --- a/www/assets/app.js +++ b/www/assets/app.js @@ -241,7 +241,7 @@ } $scope.getProxyUrl = function(instance, port) { - var url = window.location.protocol + '//pwd' + instance.ip.replace(/\./g, '-') + '-' + port + '.' + window.location.host; + var url = window.location.protocol + '//' + instance.proxy + '-' + port + '.' + window.location.host; return url; }