diff --git a/api.go b/api.go
index a55b0bd..907314e 100644
--- a/api.go
+++ b/api.go
@@ -61,6 +61,7 @@ func main() {
r.HandleFunc("/ping", handlers.Ping).Methods("GET")
corsRouter.HandleFunc("/instances/images", handlers.GetInstanceImages).Methods("GET")
corsRouter.HandleFunc("/sessions/{sessionId}", handlers.GetSession).Methods("GET")
+ corsRouter.HandleFunc("/sessions/{sessionId}/setup", handlers.SessionSetup).Methods("POST")
corsRouter.HandleFunc("/sessions/{sessionId}/instances", handlers.NewInstance).Methods("POST")
corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/uploads", handlers.FileUpload).Methods("POST")
corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}", handlers.DeleteInstance).Methods("DELETE")
diff --git a/docker/docker.go b/docker/docker.go
index a619527..1bf793d 100644
--- a/docker/docker.go
+++ b/docker/docker.go
@@ -4,22 +4,27 @@ import (
"archive/tar"
"bytes"
"context"
+ "crypto/tls"
"fmt"
"io"
"io/ioutil"
"log"
"net"
+ "net/http"
"os"
"strconv"
"strings"
"time"
"github.com/docker/distribution/reference"
+ "github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
+ "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/jsonmessage"
+ "github.com/docker/go-connections/tlsconfig"
)
const (
@@ -44,6 +49,14 @@ type DockerApi interface {
DisconnectNetwork(containerId, networkId string) error
DeleteNetwork(id string) error
Exec(instanceName string, command []string) (int, error)
+ New(ip string, cert, key []byte) (DockerApi, error)
+ SwarmInit() (*SwarmTokens, error)
+ SwarmJoin(addr, token string) error
+}
+
+type SwarmTokens struct {
+ Manager string
+ Worker string
}
type docker struct {
@@ -396,6 +409,73 @@ func (d *docker) DeleteNetwork(id string) error {
return nil
}
+func (d *docker) New(ip string, cert, key []byte) (DockerApi, error) {
+ // We check if the client needs to use TLS
+ var tlsConfig *tls.Config
+ if len(cert) > 0 && len(key) > 0 {
+ tlsConfig = tlsconfig.ClientDefault()
+ tlsConfig.InsecureSkipVerify = true
+ tlsCert, err := tls.X509KeyPair(cert, key)
+ if err != nil {
+ return nil, err
+ }
+ tlsConfig.Certificates = []tls.Certificate{tlsCert}
+ }
+
+ transport := &http.Transport{
+ DialContext: (&net.Dialer{
+ Timeout: 1 * time.Second,
+ KeepAlive: 30 * time.Second,
+ }).DialContext}
+ if tlsConfig != nil {
+ transport.TLSClientConfig = tlsConfig
+ }
+ cli := &http.Client{
+ Transport: transport,
+ }
+ c, err := client.NewClient(fmt.Sprintf("http://%s:2375", ip), api.DefaultVersion, cli, nil)
+ if err != nil {
+ return nil, fmt.Errorf("Could not connect to DinD docker daemon. %s", err)
+ }
+ // try to connect up to 5 times and then give up
+ for i := 0; i < 5; i++ {
+ _, err := c.Ping(context.Background())
+ if err != nil {
+ if client.IsErrConnectionFailed(err) {
+ // connection has failed, maybe instance is not ready yet, sleep and retry
+ log.Printf("Connection to [%s] has failed, maybe instance is not ready yet, sleeping and retrying in 1 second. Try #%d\n", fmt.Sprintf("http://%s:2375", ip), i+1)
+ time.Sleep(time.Second)
+ continue
+ }
+ return nil, err
+ }
+ }
+ return NewDocker(c), nil
+}
+
+func (d *docker) SwarmInit() (*SwarmTokens, error) {
+ req := swarm.InitRequest{AdvertiseAddr: "eth0", ListenAddr: "0.0.0.0:2377"}
+ _, err := d.c.SwarmInit(context.Background(), req)
+
+ if err != nil {
+ return nil, err
+ }
+
+ swarmInfo, err := d.c.SwarmInspect(context.Background())
+ if err != nil {
+ return nil, err
+ }
+
+ return &SwarmTokens{
+ Worker: swarmInfo.JoinTokens.Worker,
+ Manager: swarmInfo.JoinTokens.Manager,
+ }, nil
+}
+func (d *docker) SwarmJoin(addr, token string) error {
+ req := swarm.JoinRequest{RemoteAddrs: []string{addr}, JoinToken: token, ListenAddr: "0.0.0.0:2377", AdvertiseAddr: "eth0"}
+ return d.c.SwarmJoin(context.Background(), req)
+}
+
func NewDocker(c *client.Client) *docker {
return &docker{c: c}
}
diff --git a/handlers/session_setup.go b/handlers/session_setup.go
new file mode 100644
index 0000000..a72dba5
--- /dev/null
+++ b/handlers/session_setup.go
@@ -0,0 +1,35 @@
+package handlers
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+
+ "github.com/gorilla/mux"
+ "github.com/play-with-docker/play-with-docker/pwd"
+)
+
+func SessionSetup(rw http.ResponseWriter, req *http.Request) {
+ vars := mux.Vars(req)
+ sessionId := vars["sessionId"]
+
+ body := pwd.SessionSetupConf{}
+
+ json.NewDecoder(req.Body).Decode(&body)
+
+ s := core.SessionGet(sessionId)
+
+ if len(s.Instances) > 0 {
+ log.Println("Cannot setup a session that contains instances")
+ rw.WriteHeader(http.StatusConflict)
+ rw.Write([]byte("Cannot setup a session that contains instances"))
+ return
+ }
+
+ err := core.SessionSetup(s, body)
+ if err != nil {
+ log.Println(err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+}
diff --git a/pwd/docker_mock_test.go b/pwd/docker_mock_test.go
index c84d638..faa05ff 100644
--- a/pwd/docker_mock_test.go
+++ b/pwd/docker_mock_test.go
@@ -14,6 +14,10 @@ type mockDocker struct {
connectNetwork func(container, network, ip string) (string, error)
containerResize func(string, uint, uint) error
createContainer func(opts docker.CreateContainerOpts) (string, error)
+ execAttach func(instanceName string, command []string, out io.Writer) (int, error)
+ new func(ip string, cert, key []byte) (docker.DockerApi, error)
+ swarmInit func() (*docker.SwarmTokens, error)
+ swarmJoin func(addr, token string) error
}
func (m *mockDocker) CreateNetwork(id string) error {
@@ -64,6 +68,9 @@ func (m *mockDocker) CreateContainer(opts docker.CreateContainerOpts) (string, e
return "10.0.0.1", nil
}
func (m *mockDocker) ExecAttach(instanceName string, command []string, out io.Writer) (int, error) {
+ if m.execAttach != nil {
+ return m.execAttach(instanceName, command, out)
+ }
return 0, nil
}
func (m *mockDocker) DisconnectNetwork(containerId, networkId string) error {
@@ -75,6 +82,24 @@ func (m *mockDocker) DeleteNetwork(id string) error {
func (m *mockDocker) Exec(instanceName string, command []string) (int, error) {
return 0, nil
}
+func (m *mockDocker) New(ip string, cert, key []byte) (docker.DockerApi, error) {
+ if m.new != nil {
+ return m.new(ip, cert, key)
+ }
+ return nil, nil
+}
+func (m *mockDocker) SwarmInit() (*docker.SwarmTokens, error) {
+ if m.swarmInit != nil {
+ return m.swarmInit()
+ }
+ return nil, nil
+}
+func (m *mockDocker) SwarmJoin(addr, token string) error {
+ if m.swarmJoin != nil {
+ return m.swarmJoin(addr, token)
+ }
+ return nil
+}
type mockConn struct {
}
diff --git a/pwd/pwd.go b/pwd/pwd.go
index 04412a8..99d5fde 100644
--- a/pwd/pwd.go
+++ b/pwd/pwd.go
@@ -46,6 +46,7 @@ type PWDApi interface {
SessionDeployStack(session *Session) error
SessionGet(id string) *Session
SessionLoadAndPrepare() error
+ SessionSetup(session *Session, conf SessionSetupConf) error
InstanceNew(session *Session, conf InstanceConfig) (*Instance, error)
InstanceResizeTerminal(instance *Instance, cols, rows uint) error
diff --git a/pwd/session.go b/pwd/session.go
index ef5aaa5..e64cb28 100644
--- a/pwd/session.go
+++ b/pwd/session.go
@@ -10,6 +10,7 @@ import (
"time"
"github.com/play-with-docker/play-with-docker/config"
+ "github.com/play-with-docker/play-with-docker/docker"
"github.com/twinj/uuid"
)
@@ -23,6 +24,17 @@ func (s *sessionBuilderWriter) Write(p []byte) (n int, err error) {
return len(p), nil
}
+type SessionSetupConf struct {
+ Instances []SessionSetupInstanceConf `json:"instances"`
+}
+
+type SessionSetupInstanceConf struct {
+ Image string `json:"image"`
+ Hostname string `json:"hostname"`
+ IsSwarmManager bool `json:"is_swarm_manager"`
+ IsSwarmWorker bool `json:"is_swarm_worker"`
+}
+
type Session struct {
rw sync.Mutex
Id string `json:"id"`
@@ -209,6 +221,94 @@ func (p *pwd) SessionLoadAndPrepare() error {
return nil
}
+func (p *pwd) SessionSetup(session *Session, conf SessionSetupConf) error {
+ var tokens *docker.SwarmTokens = nil
+ var firstSwarmManager *Instance = nil
+
+ // first look for a swarm manager and create it
+ for _, conf := range conf.Instances {
+ if conf.IsSwarmManager {
+ instanceConf := InstanceConfig{
+ ImageName: conf.Image,
+ Hostname: conf.Hostname,
+ }
+ i, err := p.InstanceNew(session, instanceConf)
+ if err != nil {
+ return err
+ }
+ if i.docker == nil {
+ dock, err := p.docker.New(i.IP, i.Cert, i.Key)
+ if err != nil {
+ return err
+ }
+ i.docker = dock
+ }
+ tkns, err := i.docker.SwarmInit()
+ if err != nil {
+ return err
+ }
+ tokens = tkns
+ firstSwarmManager = i
+ break
+ }
+ }
+
+ // now create the rest in parallel
+
+ wg := sync.WaitGroup{}
+ for _, c := range conf.Instances {
+ if firstSwarmManager != nil && c.Hostname != firstSwarmManager.Hostname {
+ wg.Add(1)
+ go func(c SessionSetupInstanceConf) {
+ defer wg.Done()
+ instanceConf := InstanceConfig{
+ ImageName: c.Image,
+ Hostname: c.Hostname,
+ }
+ i, err := p.InstanceNew(session, instanceConf)
+ if err != nil {
+ log.Println(err)
+ return
+ }
+ 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)
+ if err != nil {
+ log.Println(err)
+ return
+ }
+ i.docker = dock
+ }
+ }
+
+ if firstSwarmManager != nil {
+ if c.IsSwarmManager {
+ // this is a swarm manager
+ // cluster has already been initiated, join as manager
+ err := i.docker.SwarmJoin(fmt.Sprintf("%s:2377", firstSwarmManager.IP), tokens.Manager)
+ if err != nil {
+ log.Println(err)
+ return
+ }
+ }
+ if c.IsSwarmWorker {
+ // this is a swarm worker
+ err := i.docker.SwarmJoin(fmt.Sprintf("%s:2377", firstSwarmManager.IP), tokens.Worker)
+ if err != nil {
+ log.Println(err)
+ return
+ }
+ }
+ }
+ }(c)
+ }
+ }
+ wg.Wait()
+
+ return nil
+}
+
// This function should be called any time a session needs to be prepared:
// 1. Like when it is created
// 2. When it was loaded from storage
diff --git a/pwd/session_test.go b/pwd/session_test.go
index aa4ccfd..9533e2c 100644
--- a/pwd/session_test.go
+++ b/pwd/session_test.go
@@ -1,10 +1,12 @@
package pwd
import (
+ "fmt"
"testing"
"time"
"github.com/play-with-docker/play-with-docker/config"
+ "github.com/play-with-docker/play-with-docker/docker"
"github.com/stretchr/testify/assert"
)
@@ -80,3 +82,199 @@ func TestSessionNew(t *testing.T) {
assert.Equal(t, expectedSessions, sessions)
assert.True(t, saveCalled)
}
+
+func TestSessionSetup(t *testing.T) {
+ swarmInitOnMaster1 := false
+ manager2JoinedHasManager := false
+ manager3JoinedHasManager := false
+ worker1JoinedHasWorker := false
+
+ dock := &mockDocker{}
+ dock.createContainer = func(opts docker.CreateContainerOpts) (string, error) {
+ if opts.Hostname == "manager1" {
+ return "10.0.0.1", nil
+ } else if opts.Hostname == "manager2" {
+ return "10.0.0.2", nil
+ } else if opts.Hostname == "manager3" {
+ return "10.0.0.3", nil
+ } else if opts.Hostname == "worker1" {
+ return "10.0.0.4", nil
+ } else if opts.Hostname == "other" {
+ return "10.0.0.5", nil
+ } else {
+ assert.Fail(t, "Should not have reached here")
+ }
+ return "", nil
+ }
+ dock.new = func(ip string, cert, key []byte) (docker.DockerApi, error) {
+ if ip == "10.0.0.1" {
+ return &mockDocker{
+ swarmInit: func() (*docker.SwarmTokens, error) {
+ swarmInitOnMaster1 = true
+ return &docker.SwarmTokens{Worker: "worker-join-token", Manager: "manager-join-token"}, nil
+ },
+ }, nil
+ }
+ if ip == "10.0.0.2" {
+ return &mockDocker{
+ swarmInit: func() (*docker.SwarmTokens, error) {
+ assert.Fail(t, "Shouldn't have reached here.")
+ return nil, nil
+ },
+ swarmJoin: func(addr, token string) error {
+ if addr == "10.0.0.1:2377" && token == "manager-join-token" {
+ manager2JoinedHasManager = true
+ return nil
+ }
+ assert.Fail(t, "Shouldn't have reached here.")
+ return nil
+ },
+ }, nil
+ }
+ if ip == "10.0.0.3" {
+ return &mockDocker{
+ swarmInit: func() (*docker.SwarmTokens, error) {
+ assert.Fail(t, "Shouldn't have reached here.")
+ return nil, nil
+ },
+ swarmJoin: func(addr, token string) error {
+ if addr == "10.0.0.1:2377" && token == "manager-join-token" {
+ manager3JoinedHasManager = true
+ return nil
+ }
+ assert.Fail(t, "Shouldn't have reached here.")
+ return nil
+ },
+ }, nil
+ }
+ if ip == "10.0.0.4" {
+ return &mockDocker{
+ swarmInit: func() (*docker.SwarmTokens, error) {
+ assert.Fail(t, "Shouldn't have reached here.")
+ return nil, nil
+ },
+ swarmJoin: func(addr, token string) error {
+ if addr == "10.0.0.1:2377" && token == "worker-join-token" {
+ worker1JoinedHasWorker = true
+ return nil
+ }
+ assert.Fail(t, "Shouldn't have reached here.")
+ return nil
+ },
+ }, nil
+ }
+ assert.Fail(t, "Shouldn't have reached here.")
+ return nil, nil
+ }
+ tasks := &mockTasks{}
+ broadcast := &mockBroadcast{}
+ storage := &mockStorage{}
+
+ p := NewPWD(dock, tasks, broadcast, storage)
+ s, e := p.SessionNew(time.Hour, "", "")
+ assert.Nil(t, e)
+
+ err := p.SessionSetup(s, SessionSetupConf{
+ Instances: []SessionSetupInstanceConf{
+ {
+ Image: "franela/dind",
+ IsSwarmManager: true,
+ Hostname: "manager1",
+ },
+ {
+ IsSwarmManager: true,
+ Hostname: "manager2",
+ },
+ {
+ Image: "franela/dind:overlay2-dev",
+ IsSwarmManager: true,
+ Hostname: "manager3",
+ },
+ {
+ IsSwarmWorker: true,
+ Hostname: "worker1",
+ },
+ {
+ Hostname: "other",
+ },
+ },
+ })
+ assert.Nil(t, err)
+
+ assert.Equal(t, 5, len(s.Instances))
+
+ manager1 := fmt.Sprintf("%s_manager1", s.Id[:8])
+ manager1Received := *s.Instances[manager1]
+ assert.Equal(t, Instance{
+ Name: manager1,
+ Image: "franela/dind",
+ Hostname: "manager1",
+ IP: "10.0.0.1",
+ Alias: "",
+ IsDockerHost: true,
+ session: s,
+ conn: manager1Received.conn,
+ docker: manager1Received.docker,
+ }, manager1Received)
+
+ manager2 := fmt.Sprintf("%s_manager2", s.Id[:8])
+ manager2Received := *s.Instances[manager2]
+ assert.Equal(t, Instance{
+ Name: manager2,
+ Image: "franela/dind",
+ Hostname: "manager2",
+ IP: "10.0.0.2",
+ Alias: "",
+ IsDockerHost: true,
+ session: s,
+ conn: manager2Received.conn,
+ docker: manager2Received.docker,
+ }, manager2Received)
+
+ manager3 := fmt.Sprintf("%s_manager3", s.Id[:8])
+ manager3Received := *s.Instances[manager3]
+ assert.Equal(t, Instance{
+ Name: manager3,
+ Image: "franela/dind:overlay2-dev",
+ Hostname: "manager3",
+ IP: "10.0.0.3",
+ Alias: "",
+ IsDockerHost: true,
+ session: s,
+ conn: manager3Received.conn,
+ docker: manager3Received.docker,
+ }, manager3Received)
+
+ worker1 := fmt.Sprintf("%s_worker1", s.Id[:8])
+ worker1Received := *s.Instances[worker1]
+ assert.Equal(t, Instance{
+ Name: worker1,
+ Image: "franela/dind",
+ Hostname: "worker1",
+ IP: "10.0.0.4",
+ Alias: "",
+ IsDockerHost: true,
+ session: s,
+ conn: worker1Received.conn,
+ docker: worker1Received.docker,
+ }, worker1Received)
+
+ other := fmt.Sprintf("%s_other", s.Id[:8])
+ otherReceived := *s.Instances[other]
+ assert.Equal(t, Instance{
+ Name: other,
+ Image: "franela/dind",
+ Hostname: "other",
+ IP: "10.0.0.5",
+ Alias: "",
+ IsDockerHost: true,
+ session: s,
+ conn: otherReceived.conn,
+ docker: otherReceived.docker,
+ }, otherReceived)
+
+ assert.True(t, swarmInitOnMaster1)
+ assert.True(t, manager2JoinedHasManager)
+ assert.True(t, manager3JoinedHasManager)
+ assert.True(t, worker1JoinedHasWorker)
+}
diff --git a/www/assets/app.js b/www/assets/app.js
index b23e516..75d735b 100644
--- a/www/assets/app.js
+++ b/www/assets/app.js
@@ -19,8 +19,8 @@
}
}
- app.controller('PlayController', ['$scope', '$log', '$http', '$location', '$timeout', '$mdDialog', '$window', 'TerminalService', 'KeyboardShortcutService', 'InstanceService', function($scope, $log, $http, $location, $timeout, $mdDialog, $window, TerminalService, KeyboardShortcutService, InstanceService) {
- $scope.sessionId = window.location.pathname.replace('/p/', '');
+ app.controller('PlayController', ['$scope', '$log', '$http', '$location', '$timeout', '$mdDialog', '$window', 'TerminalService', 'KeyboardShortcutService', 'InstanceService', 'SessionService', function($scope, $log, $http, $location, $timeout, $mdDialog, $window, TerminalService, KeyboardShortcutService, InstanceService, SessionService) {
+ $scope.sessionId = SessionService.getCurrentSessionId();
$scope.instances = [];
$scope.idx = {};
$scope.selectedInstance = null;
@@ -363,7 +363,7 @@
$mdIconProvider.defaultIconSet('../assets/social-icons.svg', 24);
}])
.component('settingsIcon', {
- template : "