diff --git a/Dockerfile b/Dockerfile index 1183ed9..a88c2b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,8 +12,15 @@ RUN go get -v -d ./... RUN CGO_ENABLED=0 go build -a -installsuffix nocgo -o /go/bin/play-with-docker . -# Set the workdir to be /go/bin which is where the binaries are built -WORKDIR /go/bin +FROM alpine -# Export the WORKDIR as a tar stream -CMD tar -cf - . +RUN apk --update add ca-certificates +RUN mkdir -p /app/pwd + +COPY --from=0 /go/bin/play-with-docker /app/play-with-docker +COPY ./www /app/www + +WORKDIR /app +CMD ["./play-with-docker"] + +EXPOSE 3000 diff --git a/Dockerfile.dind b/Dockerfile.dind index aa7dbf2..bba9a2d 100644 --- a/Dockerfile.dind +++ b/Dockerfile.dind @@ -1,14 +1,8 @@ -ARG VERSION=17.05.0-ce-dind -FROM docker:${VERSION} +ARG VERSION=docker:17.05.0-ce-dind +FROM ${VERSION} RUN apk add --no-cache git tmux py2-pip apache2-utils vim build-base gettext-dev curl bash-completion bash util-linux jq -ENV COMPOSE_VERSION=1.12.0 -ENV MACHINE_VERSION=v0.10.0 -# Install Compose and Machine -RUN pip install docker-compose==${COMPOSE_VERSION} -RUN curl -L https://github.com/docker/machine/releases/download/${MACHINE_VERSION}/docker-machine-Linux-x86_64 \ - -o /usr/bin/docker-machine && chmod +x /usr/bin/docker-machine # Compile and install httping # (used in orchestration workshop, and very useful anyway) @@ -18,6 +12,13 @@ RUN mkdir -p /opt && cd /opt && \ ./configure && make install LDFLAGS=-lintl && \ rm -rf httping-2.5 +ENV COMPOSE_VERSION=1.13.0 +ENV MACHINE_VERSION=v0.11.0 +# Install Compose and Machine +RUN pip install docker-compose==${COMPOSE_VERSION} +RUN curl -L https://github.com/docker/machine/releases/download/${MACHINE_VERSION}/docker-machine-Linux-x86_64 \ + -o /usr/bin/docker-machine && chmod +x /usr/bin/docker-machine + # Add bash completion RUN mkdir /etc/bash_completion.d && curl https://raw.githubusercontent.com/docker/docker/master/contrib/completion/bash/docker -o /etc/bash_completion.d/docker diff --git a/README.md b/README.md index cd0d4bf..6f92fe8 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ the daemon won't load it automatically. Run the following command for that purpo Start the Docker daemon on your machine and run `docker pull franela/dind`. -1) Install go 1.7.1 with `brew` on Mac or through a package manager. +1) Install go 1.7.1+ with `brew` on Mac or through a package manager. 2) `go get -v -d -t ./...` @@ -35,13 +35,27 @@ Notes: * There is a hard-coded limit to 5 Docker playgrounds per session. After 4 hours sessions are deleted. * If you want to override the DIND version or image then set the environmental variable i.e. `DIND_IMAGE=franela/docker-rc:dind`. Take into account that you can't use standard `dind` images, only [franela](https://hub.docker.com/r/franela/) ones work. + +### Port forwarding +In order for port forwarding to work correctly in development you need to make `*.localhost` to resolve to `127.0.0.1`. That way when you try to access to `pwd10-0-0-1-8080.host1.localhost`, then you're forwarded correctly to your local PWD server. + +You can achieve this by setting up a `dnsmasq` server (you can run it in a docker container also) and adding the following configuration: + +``` +address=/localhost/127.0.0.1 +``` + +Don't forget to change your computer default DNS to use the dnsmasq server to resolve. + +### Building the dind image myself. + +If you want to make changes to the `dind` image being used, make your changes to the `Dockerfile.dind` file and then build it using this command: `docker build --build-arg docker_storage_driver=vfs -f Dockerfile.dind -t franela/dind .` ## FAQ ### How can I connect to a published port from the outside world? -~~We're planning to setup a reverse proxy that handles redirection automatically, in the meantime you can use [ngrok](https://ngrok.com) within PWD running `docker run --name supergrok -d jpetazzo/supergrok` then `docker logs --follow supergrok` , it will give you a ngrok URL, now you can go to that URL and add the IP+port that you want to connect to… e.g. if your PWD instance is 10.0.42.3, you can go to http://xxxxxx.ngrok.io/10.0.42.3:8000 (where the xxxxxx is given to you in the supergrok logs).~~ If you need to access your services from outside, use the following URL pattern `http://pwd-..labs.play-with-docker.com` (i.e: http://pwd10_2_135_3-80.host3.labs.play-with-docker.com/). diff --git a/build.sh b/build.sh deleted file mode 100755 index 062ae61..0000000 --- a/build.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -docker build -t builder .&& docker run --rm builder | docker build -t franela/play-with-docker:latest - diff --git a/handlers/home.go b/handlers/home.go index fbe1bdd..d0af8a5 100644 --- a/handlers/home.go +++ b/handlers/home.go @@ -11,8 +11,14 @@ func Home(w http.ResponseWriter, r *http.Request) { sessionId := vars["sessionId"] s := core.SessionGet(sessionId) + if s == nil { + // Session doesn't exist (can happen if closing the sessions an reloading the page, or similar). + w.WriteHeader(http.StatusNotFound) + return + } if s.Stack != "" { go core.SessionDeployStack(s) } + http.ServeFile(w, r, "./www/index.html") } diff --git a/handlers/new_session.go b/handlers/new_session.go index 609ac7f..818dac2 100644 --- a/handlers/new_session.go +++ b/handlers/new_session.go @@ -5,6 +5,8 @@ import ( "fmt" "log" "net/http" + "path" + "strings" "github.com/play-with-docker/play-with-docker/config" "github.com/play-with-docker/play-with-docker/recaptcha" @@ -25,8 +27,10 @@ func NewSession(rw http.ResponseWriter, req *http.Request) { reqDur := req.Form.Get("session-duration") stack := req.Form.Get("stack") + stackName := req.Form.Get("stack_name") if stack != "" { + stack = formatStack(stack) if ok, err := stackExists(stack); err != nil { log.Printf("Error retrieving stack: %s", err) rw.WriteHeader(http.StatusInternalServerError) @@ -39,7 +43,7 @@ func NewSession(rw http.ResponseWriter, req *http.Request) { } duration := config.GetDuration(reqDur) - s, err := core.SessionNew(duration, stack, "") + s, err := core.SessionNew(duration, stack, stackName) if err != nil { log.Println(err) //TODO: Return some error code @@ -61,13 +65,24 @@ func NewSession(rw http.ResponseWriter, req *http.Request) { } } +func formatStack(stack string) string { + if !strings.HasSuffix(stack, ".yml") { + // If it doesn't end with ".yml", assume it hasn't been specified, then default to "stack.yml" + stack = path.Join(stack, "stack.yml") + } + if strings.HasPrefix(stack, "/") { + // The host is anonymous, then use our own stack repo. + stack = fmt.Sprintf("%s%s", "https://raw.githubusercontent.com/play-with-docker/stacks/master", stack) + } + return stack +} + func stackExists(stack string) (bool, error) { - resp, err := http.Head("https://raw.githubusercontent.com/play-with-docker/stacks/master" + stack) + resp, err := http.Head(stack) if err != nil { return false, err } defer resp.Body.Close() return resp.StatusCode == 200, nil - } diff --git a/pwd/session.go b/pwd/session.go index 47a94c3..ef5aaa5 100644 --- a/pwd/session.go +++ b/pwd/session.go @@ -47,11 +47,14 @@ func (p *pwd) SessionNew(duration time.Duration, stack, stackName string) (*Sess s.ExpiresAt = s.CreatedAt.Add(duration) s.Ready = true s.Stack = stack - s.StackName = stackName if s.Stack != "" { s.Ready = false } + if stackName == "" { + stackName = "pwd" + } + s.StackName = stackName log.Printf("NewSession id=[%s]\n", s.Id) @@ -142,32 +145,32 @@ func (p *pwd) SessionDeployStack(s *Session) error { } s.Ready = false - p.broadcast.BroadcastTo(s.Id, "session ready", s.Ready) - + p.broadcast.BroadcastTo(s.Id, "session ready", false) i, err := p.InstanceNew(s, InstanceConfig{}) if err != nil { log.Printf("Error creating instance for stack [%s]: %s\n", s.Stack, err) return err } - err = p.InstanceUploadFromUrl(i, "https://raw.githubusercontent.com/play-with-docker/stacks/master"+s.Stack) + err = p.InstanceUploadFromUrl(i, s.Stack) if err != nil { log.Printf("Error uploading stack file [%s]: %s\n", s.Stack, err) return err } - w := sessionBuilderWriter{sessionId: s.Id, broadcast: p.broadcast} fileName := path.Base(s.Stack) - code, err := p.docker.ExecAttach(i.Name, []string{"docker-compose", "-f", "/var/run/pwd/uploads/" + fileName, "up", "-d"}, &w) + file := fmt.Sprintf("/var/run/pwd/uploads/%s", fileName) + 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, broadcast: p.broadcast} + code, err := p.docker.ExecAttach(i.Name, []string{"sh", "-c", cmd}, &w) if err != nil { log.Printf("Error executing stack [%s]: %s\n", s.Stack, err) return err } log.Printf("Stack execution finished with code %d\n", code) - s.Ready = true - p.broadcast.BroadcastTo(s.Id, "session ready", s.Ready) - + p.broadcast.BroadcastTo(s.Id, "session ready", true) if err := p.storage.Save(); err != nil { return err } @@ -176,16 +179,6 @@ func (p *pwd) SessionDeployStack(s *Session) error { func (p *pwd) SessionGet(sessionId string) *Session { s := sessions[sessionId] - /* - if s != nil { - for _, instance := range s.Instances { - if !instance.IsConnected() { - instance.SetSession(s) - go instance.Attach() - } - } - - }*/ return s } diff --git a/pwd/session_test.go b/pwd/session_test.go index 825d9b0..8d0a003 100644 --- a/pwd/session_test.go +++ b/pwd/session_test.go @@ -43,6 +43,8 @@ func TestSessionNew(t *testing.T) { assert.Nil(t, e) assert.NotNil(t, s) + assert.Equal(t, "pwd", s.StackName) + assert.NotEmpty(t, s.Id) assert.WithinDuration(t, s.CreatedAt, before, time.Since(before)) assert.WithinDuration(t, s.ExpiresAt, before.Add(time.Hour), time.Second) diff --git a/www/assets/app.js b/www/assets/app.js index 71ba823..b23e516 100644 --- a/www/assets/app.js +++ b/www/assets/app.js @@ -63,6 +63,8 @@ KeyboardShortcutService.setResizeFunc($scope.resize); $scope.closeSession = function() { + // Remove alert before closing browser tab + window.onbeforeunload = null; $scope.socket.emit('session close'); } diff --git a/www/assets/style.css b/www/assets/style.css index 6bfdd36..a6cbefe 100644 --- a/www/assets/style.css +++ b/www/assets/style.css @@ -1,7 +1,7 @@ @import url('https://fonts.googleapis.com/css?family=Rationale'); .selected button { - background-color: rgba(158,158,158,0.2); + background-color: rgba(158,158,158,0.2); } md-card-content.terminal-container { @@ -49,3 +49,13 @@ md-input-container .md-errors-spacer { .stats { min-height: 230px; } + +::-webkit-scrollbar { + -webkit-appearance: none; + width: 7px; +} +::-webkit-scrollbar-thumb { + border-radius: 4px; + background-color: rgba(0,0,0,.5); + -webkit-box-shadow: 0 0 1px rgba(255,255,255,.5); +} diff --git a/www/bypass.html b/www/bypass.html index 9a47d4a..c33922f 100644 --- a/www/bypass.html +++ b/www/bypass.html @@ -12,6 +12,7 @@
+
@@ -37,5 +38,9 @@ if (stack) { document.getElementById('stack').value = stack; } + var stackName = getParameterByName('stack_name'); + if (stack) { + document.getElementById('stack_name').value = stackName; + } diff --git a/www/welcome.html b/www/welcome.html index b51b7fd..6effe5f 100644 --- a/www/welcome.html +++ b/www/welcome.html @@ -15,6 +15,7 @@
+ @@ -38,6 +39,10 @@ if (stack) { document.getElementById('stack').value = stack; } + var stackName = getParameterByName('stack_name'); + if (stackName) { + document.getElementById('stack_name').value = stackName; + } if (document.cookie.indexOf('session_id') > -1) { document.getElementById('create').style = ""; document.getElementById('recaptcha').style = "display:none;";