diff --git a/.gitattributes b/.gitattributes
index a6344aac8c09253b3b630fb776ae94478aa0275b..a57ac66f917f096729d52d0c3a4d8fc281b73c3d 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,35 +1,2 @@
-*.7z filter=lfs diff=lfs merge=lfs -text
-*.arrow filter=lfs diff=lfs merge=lfs -text
-*.bin filter=lfs diff=lfs merge=lfs -text
-*.bz2 filter=lfs diff=lfs merge=lfs -text
-*.ckpt filter=lfs diff=lfs merge=lfs -text
-*.ftz filter=lfs diff=lfs merge=lfs -text
-*.gz filter=lfs diff=lfs merge=lfs -text
-*.h5 filter=lfs diff=lfs merge=lfs -text
-*.joblib filter=lfs diff=lfs merge=lfs -text
-*.lfs.* filter=lfs diff=lfs merge=lfs -text
-*.mlmodel filter=lfs diff=lfs merge=lfs -text
-*.model filter=lfs diff=lfs merge=lfs -text
-*.msgpack filter=lfs diff=lfs merge=lfs -text
-*.npy filter=lfs diff=lfs merge=lfs -text
-*.npz filter=lfs diff=lfs merge=lfs -text
-*.onnx filter=lfs diff=lfs merge=lfs -text
-*.ot filter=lfs diff=lfs merge=lfs -text
-*.parquet filter=lfs diff=lfs merge=lfs -text
-*.pb filter=lfs diff=lfs merge=lfs -text
-*.pickle filter=lfs diff=lfs merge=lfs -text
-*.pkl filter=lfs diff=lfs merge=lfs -text
-*.pt filter=lfs diff=lfs merge=lfs -text
-*.pth filter=lfs diff=lfs merge=lfs -text
-*.rar filter=lfs diff=lfs merge=lfs -text
-*.safetensors filter=lfs diff=lfs merge=lfs -text
-saved_model/**/* filter=lfs diff=lfs merge=lfs -text
-*.tar.* filter=lfs diff=lfs merge=lfs -text
-*.tar filter=lfs diff=lfs merge=lfs -text
-*.tflite filter=lfs diff=lfs merge=lfs -text
-*.tgz filter=lfs diff=lfs merge=lfs -text
-*.wasm filter=lfs diff=lfs merge=lfs -text
-*.xz filter=lfs diff=lfs merge=lfs -text
-*.zip filter=lfs diff=lfs merge=lfs -text
-*.zst filter=lfs diff=lfs merge=lfs -text
-*tfevents* filter=lfs diff=lfs merge=lfs -text
+config/gen_bindata.go linguist-generated
+handlers/www/assets/swarm.png filter=lfs diff=lfs merge=lfs -text
diff --git a/.github/SECURITY.md b/.github/SECURITY.md
new file mode 100644
index 0000000000000000000000000000000000000000..ef2f0e6664554e503d5eaa752795709d5e64eb1f
--- /dev/null
+++ b/.github/SECURITY.md
@@ -0,0 +1,16 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+If there are any vulnerabilities in this project or in [the playground](https://labs.play-with-docker.com), don't hesitate to _report them_.
+
+1. Write an email to marcosnils (at) gmail.com
+2. Describe the vulnerability.
+
+ If you have a fix, that is most welcome -- please attach or summarize it in your message!
+
+3. We will evaluate the vulnerability and, if necessary, release a fix or mitigating steps to address it. We will contact you to let you know the outcome, and will credit you in the report.
+
+ Please **do not disclose the vulnerability publicly** until a fix is released!
+
+4. Once we have either a) published a fix, or b) declined to address the vulnerability for whatever reason, you are free to publicly disclose it.
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e9831926f1b694bf1e3de657a0c232da0eeb494c
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,30 @@
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ branches:
+ - '**'
+
+name: Go
+jobs:
+ test:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest]
+ go_version: ["1.16.0"]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+ - name: Install Go
+ uses: actions/setup-go@v2
+ with:
+ go-version: ${{ matrix.go_version }}
+ - name: Generate
+ run: go generate ./...
+ - name: Test
+ run: go test ./...
+ - name: Verify clean commit
+ run: test -z "$(git status --porcelain)" || (git status; git diff; false)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..5235eda96db431f511eaa57db7e7f9316f3f62af
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+play-with-docker
+node_modules
+/vendor
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..16b8281b6c54d49d771be3ac9d661fd2b48ca13a
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,23 @@
+FROM golang:1.16
+
+COPY . /go/src/github.com/play-with-docker/play-with-docker
+
+WORKDIR /go/src/github.com/play-with-docker/play-with-docker
+
+RUN ssh-keygen -N "" -t rsa -f /etc/ssh/ssh_host_rsa_key >/dev/null
+
+RUN CGO_ENABLED=0 go build -a -installsuffix nocgo -o /go/bin/play-with-docker .
+
+
+FROM alpine
+
+RUN apk --update add ca-certificates
+RUN mkdir -p /app/pwd
+
+COPY --from=0 /go/bin/play-with-docker /app/play-with-docker
+COPY --from=0 /etc/ssh/ssh_host_rsa_key /etc/ssh/ssh_host_rsa_key
+
+WORKDIR /app
+CMD ["./play-with-docker"]
+
+EXPOSE 3000
diff --git a/Dockerfile.l2 b/Dockerfile.l2
new file mode 100644
index 0000000000000000000000000000000000000000..134ebecb2851dfca75266c4ae1ce3ce6e71c431c
--- /dev/null
+++ b/Dockerfile.l2
@@ -0,0 +1,27 @@
+FROM golang:1.9
+
+# Copy the runtime dockerfile into the context as Dockerfile
+COPY . /go/src/github.com/play-with-docker/play-with-docker
+
+WORKDIR /go/src/github.com/play-with-docker/play-with-docker
+
+
+RUN ssh-keygen -N "" -t rsa -f /etc/ssh/ssh_host_rsa_key >/dev/null
+
+WORKDIR /go/src/github.com/play-with-docker/play-with-docker/router/l2
+
+RUN CGO_ENABLED=0 go build -a -installsuffix nocgo -o /go/bin/play-with-docker-l2 .
+
+
+FROM alpine
+
+RUN apk --update add ca-certificates
+RUN mkdir /app
+
+COPY --from=0 /go/bin/play-with-docker-l2 /app/play-with-docker-l2
+COPY --from=0 /etc/ssh/ssh_host_rsa_key /etc/ssh/ssh_host_rsa_key
+
+WORKDIR /app
+CMD ["./play-with-docker-l2", "-ssh_key_path", "/etc/ssh/ssh_host_rsa_key"]
+
+EXPOSE 22 53 443 8080
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..bfcd944250c5b1e9dcf133c257b1ac287cf588b4
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2016 Marcos Lilljedhal and Jonathan Leibiusky
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/api.go b/api.go
new file mode 100644
index 0000000000000000000000000000000000000000..e747a9b2f91c25c7b9180970f73e474c4f68f3a5
--- /dev/null
+++ b/api.go
@@ -0,0 +1,82 @@
+package main
+
+import (
+ "log"
+ "os"
+ "time"
+
+ "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/handlers"
+ "github.com/play-with-docker/play-with-docker/id"
+ "github.com/play-with-docker/play-with-docker/k8s"
+ "github.com/play-with-docker/play-with-docker/provisioner"
+ "github.com/play-with-docker/play-with-docker/pwd"
+ "github.com/play-with-docker/play-with-docker/pwd/types"
+ "github.com/play-with-docker/play-with-docker/scheduler"
+ "github.com/play-with-docker/play-with-docker/scheduler/task"
+ "github.com/play-with-docker/play-with-docker/storage"
+)
+
+func main() {
+ config.ParseFlags()
+
+ e := initEvent()
+ s := initStorage()
+ df := initDockerFactory(s)
+ kf := initK8sFactory(s)
+
+ ipf := provisioner.NewInstanceProvisionerFactory(provisioner.NewWindowsASG(df, s), provisioner.NewDinD(id.XIDGenerator{}, df, s))
+ sp := provisioner.NewOverlaySessionProvisioner(df)
+
+ core := pwd.NewPWD(df, e, s, sp, ipf)
+
+ tasks := []scheduler.Task{
+ task.NewCheckPorts(e, df),
+ task.NewCheckSwarmPorts(e, df),
+ task.NewCheckSwarmStatus(e, df),
+ task.NewCollectStats(e, df, s),
+ task.NewCheckK8sClusterStatus(e, kf),
+ task.NewCheckK8sClusterExposedPorts(e, kf),
+ }
+ sch, err := scheduler.NewScheduler(tasks, s, e, core)
+ if err != nil {
+ log.Fatal("Error initializing the scheduler: ", err)
+ }
+
+ sch.Start()
+
+ d, err := time.ParseDuration("2h")
+ if err != nil {
+ log.Fatalf("Cannot parse duration Got: %v", err)
+ }
+
+ playground := types.Playground{Domain: config.PlaygroundDomain, DefaultDinDInstanceImage: "franela/dind", AvailableDinDInstanceImages: []string{"franela/dind"}, AllowWindowsInstances: config.NoWindows, DefaultSessionDuration: d, Extras: map[string]interface{}{"LoginRedirect": "http://localhost:3000"}, Privileged: true}
+ if _, err := core.PlaygroundNew(playground); err != nil {
+ log.Fatalf("Cannot create default playground. Got: %v", err)
+ }
+
+ handlers.Bootstrap(core, e)
+ handlers.Register(nil)
+}
+
+func initStorage() storage.StorageApi {
+ s, err := storage.NewFileStorage(config.SessionsFile)
+ if err != nil && !os.IsNotExist(err) {
+ log.Fatal("Error initializing StorageAPI: ", err)
+ }
+ return s
+}
+
+func initEvent() event.EventApi {
+ return event.NewLocalBroker()
+}
+
+func initDockerFactory(s storage.StorageApi) docker.FactoryApi {
+ return docker.NewLocalCachedFactory(s)
+}
+
+func initK8sFactory(s storage.StorageApi) k8s.FactoryApi {
+ return k8s.NewLocalCachedFactory(s)
+}
diff --git a/config/config.go b/config/config.go
new file mode 100644
index 0000000000000000000000000000000000000000..623d7637bdce7df7a0d745d8580b774007b58d7e
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,78 @@
+package config
+
+import (
+ "flag"
+ "os"
+ "regexp"
+
+ "github.com/gorilla/securecookie"
+
+ "golang.org/x/oauth2"
+)
+
+const (
+ PWDHostnameRegex = "[0-9]{1,3}-[0-9]{1,3}-[0-9]{1,3}-[0-9]{1,3}"
+ PortRegex = "[0-9]{1,5}"
+ AliasnameRegex = "[0-9|a-z|A-Z|-]*"
+ AliasSessionRegex = "[0-9|a-z|A-Z]{8}"
+ AliasGroupRegex = "(" + AliasnameRegex + ")-(" + AliasSessionRegex + ")"
+ PWDHostPortGroupRegex = "^.*ip(" + PWDHostnameRegex + ")(?:-?(" + PortRegex + "))?(?:\\..*)?$"
+ AliasPortGroupRegex = "^.*pwd" + AliasGroupRegex + "(?:-?(" + PortRegex + "))?\\..*$"
+)
+
+var (
+ NameFilter = regexp.MustCompile(PWDHostPortGroupRegex)
+ AliasFilter = regexp.MustCompile(AliasPortGroupRegex)
+)
+
+var (
+ PortNumber, SessionsFile, PWDContainerName, L2ContainerName, L2Subdomain, HashKey, SSHKeyPath, L2RouterIP, CookieHashKey, CookieBlockKey string
+ UseLetsEncrypt, ExternalDindVolume, NoWindows bool
+ LetsEncryptCertsDir string
+ MaxLoadAvg float64
+ ForceTLS bool
+ SecureCookie *securecookie.SecureCookie
+ AdminToken string
+)
+
+// Unsafe enables a number of unsafe features when set. It is principally
+// intended to be used in development. For example, it allows the caller to
+// specify the Docker networks to join.
+var Unsafe bool
+
+var PlaygroundDomain string
+
+var SegmentId string
+
+// TODO move this to a sync map so it can be updated on demand when the configuration for a playground changes
+var Providers = map[string]map[string]*oauth2.Config{}
+
+func ParseFlags() {
+ flag.StringVar(&LetsEncryptCertsDir, "letsencrypt-certs-dir", "/certs", "Path where let's encrypt certs will be stored")
+ flag.BoolVar(&UseLetsEncrypt, "letsencrypt-enable", false, "Enabled let's encrypt tls certificates")
+ flag.BoolVar(&ForceTLS, "tls", false, "Use TLS to connect to docker daemons")
+ flag.StringVar(&PortNumber, "port", "3000", "Port number")
+ 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(&L2ContainerName, "l2", "l2", "Container name used to run L2 Router")
+ flag.StringVar(&L2RouterIP, "l2-ip", "", "Host IP address for L2 router ping response")
+ flag.StringVar(&L2Subdomain, "l2-subdomain", "direct", "Subdomain to the L2 Router")
+ flag.StringVar(&HashKey, "hash_key", "salmonrosado", "Hash key to use for cookies")
+ flag.BoolVar(&NoWindows, "win-disable", false, "Disable windows instances")
+ flag.BoolVar(&ExternalDindVolume, "dind-external-volume", false, "Use external dind volume though XFS volume driver")
+ 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.StringVar(&CookieHashKey, "cookie-hash-key", "", "Hash key to use to validate cookies")
+ flag.StringVar(&CookieBlockKey, "cookie-block-key", "", "Block key to use to encrypt cookies")
+
+ flag.StringVar(&PlaygroundDomain, "playground-domain", "localhost", "Domain to use for the playground")
+ flag.StringVar(&AdminToken, "admin-token", "", "Token to validate admin user for admin endpoints")
+
+ flag.StringVar(&SegmentId, "segment-id", "", "Segment id to post metrics")
+
+ flag.BoolVar(&Unsafe, "unsafe", os.Getenv("PWD_UNSAFE") == "true", "Operate in unsafe mode")
+
+ flag.Parse()
+
+ SecureCookie = securecookie.New([]byte(CookieHashKey), []byte(CookieBlockKey))
+}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..664d7f2f03e243bb40585a8ec5eddf43c5da9405
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,42 @@
+version: '3.2'
+services:
+ haproxy:
+ container_name: haproxy
+ image: haproxy
+ ports:
+ - "80:8080"
+ volumes:
+ - ./haproxy:/usr/local/etc/haproxy
+
+ pwd:
+ # pwd daemon container always needs to be named this way
+ 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/; if [ -e /runbin/pwd ]; then /runbin/pwd -save /pwd/sessions -name l2; else go run api.go -save /pwd/sessions -name l2; fi'
+ environment:
+ - APPARMOR_PROFILE=docker-dind
+ 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
+ - $PWD:/go/src
+ - sessions:/pwd
+ 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/router/l2; if [ -e /runbin/l2 ]; then /runbin/l2 -ssh_key_path /etc/ssh/ssh_host_rsa_key -name l2 -save /pwd/networks; else go run l2.go -ssh_key_path /etc/ssh/ssh_host_rsa_key -name l2 -save /pwd/networks; fi'
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ - $PWD:/go/src
+ - networks:/pwd
+ ports:
+ - "8022:22"
+ - "8053:53"
+ - "443:443"
+volumes:
+ sessions:
+ networks:
diff --git a/docker/docker.go b/docker/docker.go
new file mode 100644
index 0000000000000000000000000000000000000000..e36125d5d29722af15d3a8b949a926a68156dc6a
--- /dev/null
+++ b/docker/docker.go
@@ -0,0 +1,546 @@
+package docker
+
+import (
+ "archive/tar"
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "net"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/containerd/containerd/reference"
+ "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/api/types/volume"
+ "github.com/docker/docker/client"
+ "github.com/play-with-docker/play-with-docker/config"
+)
+
+const (
+ Byte = 1
+ Kilobyte = 1024 * Byte
+ Megabyte = 1024 * Kilobyte
+)
+
+type DockerApi interface {
+ GetClient() *client.Client
+
+ NetworkCreate(id string, opts types.NetworkCreate) error
+ NetworkConnect(container, network, ip string) (string, error)
+ NetworkInspect(id string) (types.NetworkResource, error)
+ NetworkDelete(id string) error
+ NetworkDisconnect(containerId, networkId string) error
+
+ DaemonInfo() (types.Info, error)
+ DaemonHost() string
+
+ GetSwarmPorts() ([]string, []uint16, error)
+ GetPorts() ([]uint16, error)
+
+ ContainerStats(name string) (io.ReadCloser, error)
+ ContainerResize(name string, rows, cols uint) error
+ ContainerRename(old, new string) error
+ ContainerDelete(name string) error
+ ContainerCreate(opts CreateContainerOpts) error
+ ContainerIPs(id string) (map[string]string, error)
+ ExecAttach(instanceName string, command []string, out io.Writer) (int, error)
+ Exec(instanceName string, command []string) (int, error)
+
+ CreateAttachConnection(name string) (net.Conn, error)
+ CopyToContainer(containerName, destination, fileName string, content io.Reader) error
+ CopyFromContainer(containerName, filePath string) (io.Reader, error)
+ SwarmInit(advertiseAddr string) (*SwarmTokens, error)
+ SwarmJoin(addr, token string) error
+
+ ConfigCreate(name string, labels map[string]string, data []byte) error
+ ConfigDelete(name string) error
+}
+
+type SwarmTokens struct {
+ Manager string
+ Worker string
+}
+
+type docker struct {
+ c *client.Client
+}
+
+func (d *docker) GetClient() *client.Client {
+ return d.c
+}
+
+func (d *docker) ConfigCreate(name string, labels map[string]string, data []byte) error {
+ config := swarm.ConfigSpec{}
+ config.Name = name
+ config.Labels = labels
+ config.Data = data
+ _, err := d.c.ConfigCreate(context.Background(), config)
+ return err
+}
+func (d *docker) ConfigDelete(name string) error {
+ return d.c.ConfigRemove(context.Background(), name)
+}
+
+func (d *docker) NetworkCreate(id string, opts types.NetworkCreate) error {
+ _, err := d.c.NetworkCreate(context.Background(), id, opts)
+
+ if err != nil {
+ log.Printf("Starting session err [%s]\n", err)
+
+ return err
+ }
+
+ return nil
+}
+
+func (d *docker) NetworkConnect(containerId, networkId, ip string) (string, error) {
+ settings := &network.EndpointSettings{}
+ if ip != "" {
+ settings.IPAddress = ip
+ }
+ err := d.c.NetworkConnect(context.Background(), networkId, containerId, settings)
+
+ if err != nil && !strings.Contains(err.Error(), "already exists") {
+ log.Printf("Connection container to network err [%s]\n", err)
+
+ return "", err
+ }
+
+ // Obtain the IP of the PWD container in this network
+ container, err := d.c.ContainerInspect(context.Background(), containerId)
+ if err != nil {
+ return "", err
+ }
+
+ n, found := container.NetworkSettings.Networks[networkId]
+ if !found {
+ return "", fmt.Errorf("Container [%s] connected to the network [%s] but couldn't obtain it's IP address", containerId, networkId)
+ }
+
+ return n.IPAddress, nil
+}
+
+func (d *docker) NetworkInspect(id string) (types.NetworkResource, error) {
+ return d.c.NetworkInspect(context.Background(), id, types.NetworkInspectOptions{})
+}
+
+func (d *docker) DaemonInfo() (types.Info, error) {
+ return d.c.Info(context.Background())
+}
+
+func (d *docker) DaemonHost() string {
+ return d.c.DaemonHost()
+}
+
+func (d *docker) GetSwarmPorts() ([]string, []uint16, error) {
+ hosts := []string{}
+ ports := []uint16{}
+
+ nodesIdx := map[string]string{}
+ nodes, nodesErr := d.c.NodeList(context.Background(), types.NodeListOptions{})
+ if nodesErr != nil {
+ return nil, nil, nodesErr
+ }
+ for _, n := range nodes {
+ nodesIdx[n.ID] = n.Description.Hostname
+ hosts = append(hosts, n.Description.Hostname)
+ }
+
+ services, err := d.c.ServiceList(context.Background(), types.ServiceListOptions{})
+ if err != nil {
+ return nil, nil, err
+ }
+ for _, service := range services {
+ for _, p := range service.Endpoint.Ports {
+ ports = append(ports, uint16(p.PublishedPort))
+ }
+ }
+
+ return hosts, ports, nil
+}
+
+func (d *docker) GetPorts() ([]uint16, error) {
+ opts := types.ContainerListOptions{}
+ containers, err := d.c.ContainerList(context.Background(), opts)
+ if err != nil {
+ return nil, err
+ }
+
+ openPorts := []uint16{}
+ for _, c := range containers {
+ for _, p := range c.Ports {
+ // When port is not published on the host docker return public port as 0, so we need to avoid it
+ if p.PublicPort != 0 {
+ openPorts = append(openPorts, p.PublicPort)
+ }
+ }
+ }
+
+ return openPorts, nil
+}
+
+func (d *docker) ContainerStats(name string) (io.ReadCloser, error) {
+ stats, err := d.c.ContainerStats(context.Background(), name, false)
+
+ return stats.Body, err
+}
+
+func (d *docker) ContainerResize(name string, rows, cols uint) error {
+ return d.c.ContainerResize(context.Background(), name, types.ResizeOptions{Height: rows, Width: cols})
+}
+
+func (d *docker) ContainerRename(old, new string) error {
+ return d.c.ContainerRename(context.Background(), old, new)
+}
+
+func (d *docker) CreateAttachConnection(name string) (net.Conn, error) {
+ ctx := context.Background()
+
+ conf := types.ContainerAttachOptions{true, true, true, true, "ctrl-^,ctrl-^", true}
+ conn, err := d.c.ContainerAttach(ctx, name, conf)
+ if err != nil {
+ return nil, err
+ }
+
+ return conn.Conn, nil
+}
+
+func (d *docker) CopyToContainer(containerName, destination, fileName string, content io.Reader) error {
+ contents, err := ioutil.ReadAll(content)
+ if err != nil {
+ return err
+ }
+ var buf bytes.Buffer
+ t := tar.NewWriter(&buf)
+ if err := t.WriteHeader(&tar.Header{Name: fileName, Mode: 0600, Size: int64(len(contents)), ModTime: time.Now()}); err != nil {
+ return err
+ }
+ if _, err := t.Write(contents); err != nil {
+ return err
+ }
+ if err := t.Close(); err != nil {
+ return err
+ }
+ return d.c.CopyToContainer(context.Background(), containerName, destination, &buf, types.CopyToContainerOptions{AllowOverwriteDirWithFile: true, CopyUIDGID: true})
+}
+
+func (d *docker) CopyFromContainer(containerName, filePath string) (io.Reader, error) {
+ rc, stat, err := d.c.CopyFromContainer(context.Background(), containerName, filePath)
+ if err != nil {
+ return nil, err
+ }
+ if stat.Mode.IsDir() {
+ return nil, fmt.Errorf("Copying directories is not supported")
+ }
+ tr := tar.NewReader(rc)
+ // advance to the only possible file in the tar archive
+ tr.Next()
+ return tr, nil
+}
+
+func (d *docker) ContainerDelete(name string) error {
+ err := d.c.ContainerRemove(context.Background(), name, types.ContainerRemoveOptions{Force: true, RemoveVolumes: true})
+ d.c.VolumeRemove(context.Background(), name, true)
+ return err
+}
+
+type CreateContainerOpts struct {
+ Image string
+ SessionId string
+ ContainerName string
+ Hostname string
+ ServerCert []byte
+ ServerKey []byte
+ CACert []byte
+ Privileged bool
+ HostFQDN string
+ Labels map[string]string
+ Networks []string
+ DindVolumeSize string
+ Envs []string
+}
+
+func (d *docker) ContainerCreate(opts CreateContainerOpts) (err error) {
+ // Make sure directories are available for the new instance container
+ containerDir := "/opt/pwd"
+ containerCertDir := fmt.Sprintf("%s/certs", containerDir)
+
+ env := append(opts.Envs, fmt.Sprintf("SESSION_ID=%s", opts.SessionId))
+
+ // Write certs to container cert dir
+ if len(opts.ServerCert) > 0 {
+ env = append(env, `DOCKER_TLSCERT=\/opt\/pwd\/certs\/cert.pem`)
+ }
+ if len(opts.ServerKey) > 0 {
+ env = append(env, `DOCKER_TLSKEY=\/opt\/pwd\/certs\/key.pem`)
+ }
+ if len(opts.CACert) > 0 {
+ // if ca cert is specified, verify that clients that connects present a certificate signed by the CA
+ env = append(env, `DOCKER_TLSCACERT=\/opt\/pwd\/certs\/ca.pem`)
+ }
+ if len(opts.ServerCert) > 0 || len(opts.ServerKey) > 0 || len(opts.CACert) > 0 {
+ // if any of the certs is specified, enable TLS
+ env = append(env, "DOCKER_TLSENABLE=true")
+ } else {
+ env = append(env, "DOCKER_TLSENABLE=false")
+ }
+
+ h := &container.HostConfig{
+ NetworkMode: container.NetworkMode(opts.SessionId),
+ Privileged: opts.Privileged,
+ AutoRemove: true,
+ LogConfig: container.LogConfig{Config: map[string]string{"max-size": "10m", "max-file": "1"}},
+ }
+
+ if os.Getenv("APPARMOR_PROFILE") != "" {
+ h.SecurityOpt = []string{fmt.Sprintf("apparmor=%s", os.Getenv("APPARMOR_PROFILE"))}
+ }
+
+ if os.Getenv("STORAGE_SIZE") != "" {
+ // assing 10GB size FS for each container
+ h.StorageOpt = map[string]string{"size": os.Getenv("STORAGE_SIZE")}
+ }
+
+ var pidsLimit = int64(1000)
+ if envLimit := os.Getenv("MAX_PROCESSES"); envLimit != "" {
+ if i, err := strconv.Atoi(envLimit); err == nil {
+ pidsLimit = int64(i)
+ }
+ }
+ h.Resources.PidsLimit = &pidsLimit
+
+ if memLimit := os.Getenv("MAX_MEMORY_MB"); memLimit != "" {
+ if i, err := strconv.Atoi(memLimit); err == nil {
+ h.Resources.Memory = int64(i) * Megabyte
+ }
+ }
+
+ t := true
+ h.Resources.OomKillDisable = &t
+
+ env = append(env, fmt.Sprintf("PWD_HOST_FQDN=%s", opts.HostFQDN))
+ cf := &container.Config{
+ Hostname: opts.Hostname,
+ Image: opts.Image,
+ Tty: true,
+ OpenStdin: true,
+ AttachStdin: true,
+ AttachStdout: true,
+ AttachStderr: true,
+ Env: env,
+ Labels: opts.Labels,
+ }
+
+ networkConf := &network.NetworkingConfig{
+ EndpointsConfig: map[string]*network.EndpointSettings{opts.Networks[0]: &network.EndpointSettings{}},
+ }
+
+ if config.ExternalDindVolume {
+ _, err = d.c.VolumeCreate(context.Background(), volume.VolumeCreateBody{
+ Driver: "xfsvol",
+ DriverOpts: map[string]string{
+ "size": opts.DindVolumeSize,
+ },
+ Name: opts.ContainerName,
+ })
+ if err != nil {
+ return
+ }
+ h.Binds = []string{fmt.Sprintf("%s:/var/lib/docker", opts.ContainerName)}
+
+ defer func() {
+ if err != nil {
+ d.c.VolumeRemove(context.Background(), opts.SessionId, true)
+ }
+ }()
+ }
+
+ container, err := d.c.ContainerCreate(context.Background(), cf, h, networkConf, opts.ContainerName)
+
+ if err != nil {
+ //if client.IsErrImageNotFound(err) {
+ //log.Printf("Unable to find image '%s' locally\n", opts.Image)
+ //if err = d.pullImage(context.Background(), opts.Image); err != nil {
+ //return "", err
+ //}
+ //container, err = d.c.ContainerCreate(context.Background(), cf, h, networkConf, opts.ContainerName)
+ //if err != nil {
+ //return "", err
+ //}
+ //} else {
+ return err
+ //}
+ }
+
+ //connect remaining networks if there are any
+ if len(opts.Networks) > 1 {
+ for _, nid := range opts.Networks {
+ err = d.c.NetworkConnect(context.Background(), nid, container.ID, &network.EndpointSettings{})
+ if err != nil {
+ return
+ }
+ }
+ }
+
+ if err = d.copyIfSet(opts.ServerCert, "cert.pem", containerCertDir, opts.ContainerName); err != nil {
+ return
+ }
+ if err = d.copyIfSet(opts.ServerKey, "key.pem", containerCertDir, opts.ContainerName); err != nil {
+ return
+ }
+ if err = d.copyIfSet(opts.CACert, "ca.pem", containerCertDir, opts.ContainerName); err != nil {
+ return
+ }
+
+ err = d.c.ContainerStart(context.Background(), container.ID, types.ContainerStartOptions{})
+ if err != nil {
+ return
+ }
+
+ return
+}
+
+func (d *docker) ContainerIPs(id string) (map[string]string, error) {
+ cinfo, err := d.c.ContainerInspect(context.Background(), id)
+ if err != nil {
+ return nil, err
+ }
+
+ ips := map[string]string{}
+ for networkId, conf := range cinfo.NetworkSettings.Networks {
+ ips[networkId] = conf.IPAddress
+ }
+ return ips, nil
+
+}
+
+func (d *docker) pullImage(ctx context.Context, image string) error {
+ _, err := reference.Parse(image)
+ if err != nil {
+ return err
+ }
+
+ options := types.ImageCreateOptions{}
+
+ responseBody, err := d.c.ImageCreate(ctx, image, options)
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(ioutil.Discard, responseBody)
+
+ return err
+}
+
+func (d *docker) copyIfSet(content []byte, fileName, path, containerName string) error {
+ if len(content) > 0 {
+ return d.CopyToContainer(containerName, path, fileName, bytes.NewReader(content))
+ }
+ return nil
+}
+
+func (d *docker) ExecAttach(instanceName string, command []string, out io.Writer) (int, error) {
+ e, err := d.c.ContainerExecCreate(context.Background(), instanceName, types.ExecConfig{Cmd: command, AttachStdout: true, AttachStderr: true, Tty: true})
+ if err != nil {
+ return 0, err
+ }
+ resp, err := d.c.ContainerExecAttach(context.Background(), e.ID, types.ExecStartCheck{
+ Tty: true,
+ })
+ if err != nil {
+ return 0, err
+ }
+ io.Copy(out, resp.Reader)
+ var ins types.ContainerExecInspect
+ for _ = range time.Tick(1 * time.Second) {
+ ins, err = d.c.ContainerExecInspect(context.Background(), e.ID)
+ if ins.Running {
+ continue
+ }
+ if err != nil {
+ return 0, err
+ }
+ break
+ }
+ return ins.ExitCode, nil
+
+}
+
+func (d *docker) Exec(instanceName string, command []string) (int, error) {
+ e, err := d.c.ContainerExecCreate(context.Background(), instanceName, types.ExecConfig{Cmd: command})
+ if err != nil {
+ return 0, err
+ }
+ err = d.c.ContainerExecStart(context.Background(), e.ID, types.ExecStartCheck{})
+ if err != nil {
+ return 0, err
+ }
+ var ins types.ContainerExecInspect
+ for _ = range time.Tick(1 * time.Second) {
+ ins, err = d.c.ContainerExecInspect(context.Background(), e.ID)
+ if ins.Running {
+ continue
+ }
+ if err != nil {
+ return 0, err
+ }
+ break
+ }
+ return ins.ExitCode, nil
+}
+
+func (d *docker) NetworkDisconnect(containerId, networkId string) error {
+ err := d.c.NetworkDisconnect(context.Background(), networkId, containerId, true)
+
+ if err != nil {
+ log.Printf("Disconnection of container from network err [%s]\n", err)
+
+ return err
+ }
+
+ return nil
+}
+
+func (d *docker) NetworkDelete(id string) error {
+ err := d.c.NetworkRemove(context.Background(), id)
+
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (d *docker) SwarmInit(advertiseAddr string) (*SwarmTokens, error) {
+ req := swarm.InitRequest{AdvertiseAddr: advertiseAddr, 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/docker/factory.go b/docker/factory.go
new file mode 100644
index 0000000000000000000000000000000000000000..da50cd467bb413edba0b0a57e4035b65a432aa4d
--- /dev/null
+++ b/docker/factory.go
@@ -0,0 +1,70 @@
+package docker
+
+import (
+ "crypto/tls"
+ "fmt"
+ "net"
+ "net/http"
+ "net/url"
+ "time"
+
+ "github.com/docker/docker/api"
+ "github.com/docker/docker/client"
+ "github.com/docker/go-connections/tlsconfig"
+ "github.com/play-with-docker/play-with-docker/pwd/types"
+ "github.com/play-with-docker/play-with-docker/router"
+)
+
+type FactoryApi interface {
+ GetForSession(session *types.Session) (DockerApi, error)
+ GetForInstance(instance *types.Instance) (DockerApi, error)
+}
+
+func NewClient(instance *types.Instance, proxyHost string) (*client.Client, error) {
+ var host string
+ var durl string
+
+ var tlsConfig *tls.Config
+ if (len(instance.Cert) > 0 && len(instance.Key) > 0) || instance.Tls {
+ host = router.EncodeHost(instance.SessionId, instance.RoutableIP, router.HostOpts{EncodedPort: 2376})
+ tlsConfig = tlsconfig.ClientDefault()
+ tlsConfig.InsecureSkipVerify = true
+ tlsConfig.ServerName = host
+ if len(instance.Cert) > 0 && len(instance.Key) > 0 {
+ tlsCert, err := tls.X509KeyPair(instance.Cert, instance.Key)
+ if err != nil {
+ return nil, fmt.Errorf("Could not load X509 key pair: %v. Make sure the key is not encrypted", err)
+ }
+ tlsConfig.Certificates = []tls.Certificate{tlsCert}
+ }
+ } else {
+ host = router.EncodeHost(instance.SessionId, instance.RoutableIP, router.HostOpts{EncodedPort: 2375})
+ }
+
+ transport := &http.Transport{
+ DialContext: (&net.Dialer{
+ Timeout: 1 * time.Second,
+ KeepAlive: 30 * time.Second,
+ }).DialContext,
+ MaxIdleConnsPerHost: 5,
+ }
+
+ if tlsConfig != nil {
+ transport.TLSClientConfig = tlsConfig
+ durl = fmt.Sprintf("https://%s", proxyHost)
+ } else {
+ transport.Proxy = http.ProxyURL(&url.URL{Host: proxyHost})
+ durl = fmt.Sprintf("http://%s", host)
+ }
+
+ cli := &http.Client{
+ Transport: transport,
+ }
+
+ dc, err := client.NewClient(durl, api.DefaultVersion, cli, nil)
+ if err != nil {
+ return nil, fmt.Errorf("Could not connect to DinD docker daemon: %v", err)
+ }
+
+ return dc, nil
+}
diff --git a/docker/factory_mock.go b/docker/factory_mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..69c3c0789467a5787b63b4fb723d67966326514d
--- /dev/null
+++ b/docker/factory_mock.go
@@ -0,0 +1,20 @@
+package docker
+
+import (
+ "github.com/play-with-docker/play-with-docker/pwd/types"
+ "github.com/stretchr/testify/mock"
+)
+
+type FactoryMock struct {
+ mock.Mock
+}
+
+func (m *FactoryMock) GetForSession(session *types.Session) (DockerApi, error) {
+ args := m.Called(session)
+ return args.Get(0).(DockerApi), args.Error(1)
+}
+
+func (m *FactoryMock) GetForInstance(instance *types.Instance) (DockerApi, error) {
+ args := m.Called(instance)
+ return args.Get(0).(DockerApi), args.Error(1)
+}
diff --git a/docker/local_cached_factory.go b/docker/local_cached_factory.go
new file mode 100644
index 0000000000000000000000000000000000000000..39a25812c7555cd3d0428410e34facffabdd2f2b
--- /dev/null
+++ b/docker/local_cached_factory.go
@@ -0,0 +1,113 @@
+package docker
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "sync"
+ "time"
+
+ "github.com/docker/docker/client"
+ "github.com/play-with-docker/play-with-docker/pwd/types"
+ "github.com/play-with-docker/play-with-docker/storage"
+)
+
+type localCachedFactory struct {
+ rw sync.Mutex
+ irw sync.Mutex
+ sessionClient DockerApi
+ instanceClients map[string]*instanceEntry
+ storage storage.StorageApi
+}
+
+type instanceEntry struct {
+ rw sync.Mutex
+ client DockerApi
+}
+
+func (f *localCachedFactory) GetForSession(session *types.Session) (DockerApi, error) {
+ f.rw.Lock()
+ defer f.rw.Unlock()
+
+ if f.sessionClient != nil {
+ if err := f.check(f.sessionClient.GetClient()); err == nil {
+ return f.sessionClient, nil
+ } else {
+ f.sessionClient.GetClient().Close()
+ }
+ }
+
+ c, err := client.NewClientWithOpts()
+ if err != nil {
+ return nil, err
+ }
+ err = f.check(c)
+ if err != nil {
+ return nil, err
+ }
+ d := NewDocker(c)
+ f.sessionClient = d
+ return f.sessionClient, nil
+}
+
+func (f *localCachedFactory) GetForInstance(instance *types.Instance) (DockerApi, error) {
+ key := instance.Name
+
+ f.irw.Lock()
+ c, found := f.instanceClients[key]
+ if !found {
+ c := &instanceEntry{}
+ f.instanceClients[key] = c
+ }
+ c = f.instanceClients[key]
+ f.irw.Unlock()
+
+ c.rw.Lock()
+ defer c.rw.Unlock()
+
+ if c.client != nil {
+ if err := f.check(c.client.GetClient()); err == nil {
+ return c.client, nil
+ } else {
+ c.client.GetClient().Close()
+ }
+ }
+
+ dc, err := NewClient(instance, "l2:443")
+ if err != nil {
+ return nil, err
+ }
+ err = f.check(dc)
+ if err != nil {
+ return nil, err
+ }
+ dockerClient := NewDocker(dc)
+ c.client = dockerClient
+
+ return dockerClient, nil
+}
+
+func (f *localCachedFactory) check(c *client.Client) error {
+ ok := false
+ for i := 0; i < 5; i++ {
+ _, err := c.Ping(context.Background())
+ if err != nil {
+ log.Printf("Connection to [%s] has failed, maybe instance is not ready yet, sleeping and retrying in 1 second. Try #%d. Got: %v\n", c.DaemonHost(), i+1, err)
+ time.Sleep(time.Second)
+ continue
+ }
+ ok = true
+ break
+ }
+ if !ok {
+ return fmt.Errorf("Connection to docker daemon was not established.")
+ }
+ return nil
+}
+
+func NewLocalCachedFactory(s storage.StorageApi) *localCachedFactory {
+ return &localCachedFactory{
+ instanceClients: make(map[string]*instanceEntry),
+ storage: s,
+ }
+}
diff --git a/docker/mock.go b/docker/mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..4f4c2c93bacf2a1876c8330eb1f0b42b4580b173
--- /dev/null
+++ b/docker/mock.go
@@ -0,0 +1,160 @@
+package docker
+
+import (
+ "io"
+ "net"
+ "time"
+
+ "github.com/docker/docker/api/types"
+ "github.com/docker/docker/client"
+ "github.com/stretchr/testify/mock"
+)
+
+type Mock struct {
+ mock.Mock
+}
+
+func (m *Mock) GetClient() *client.Client {
+ args := m.Called()
+ return args.Get(0).(*client.Client)
+}
+
+func (m *Mock) NetworkCreate(id string, opts types.NetworkCreate) error {
+ args := m.Called(id, opts)
+ return args.Error(0)
+}
+
+func (m *Mock) NetworkConnect(container, network, ip string) (string, error) {
+ args := m.Called(container, network, ip)
+ return args.String(0), args.Error(1)
+}
+
+func (m *Mock) NetworkInspect(id string) (types.NetworkResource, error) {
+ args := m.Called(id)
+ return args.Get(0).(types.NetworkResource), args.Error(1)
+}
+
+func (m *Mock) DaemonInfo() (types.Info, error) {
+ args := m.Called()
+ return args.Get(0).(types.Info), args.Error(1)
+}
+
+func (m *Mock) DaemonHost() string {
+ args := m.Called()
+ return args.String(0)
+}
+
+func (m *Mock) GetSwarmPorts() ([]string, []uint16, error) {
+ args := m.Called()
+ return args.Get(0).([]string), args.Get(1).([]uint16), args.Error(2)
+}
+
+func (m *Mock) GetPorts() ([]uint16, error) {
+ args := m.Called()
+ return args.Get(0).([]uint16), args.Error(1)
+}
+func (m *Mock) ContainerStats(name string) (io.ReadCloser, error) {
+ args := m.Called(name)
+ return args.Get(0).(io.ReadCloser), args.Error(1)
+}
+func (m *Mock) ContainerResize(name string, rows, cols uint) error {
+ args := m.Called(name, rows, cols)
+ return args.Error(0)
+}
+func (m *Mock) ContainerRename(old, new string) error {
+ args := m.Called(old, new)
+ return args.Error(0)
+}
+func (m *Mock) CreateAttachConnection(name string) (net.Conn, error) {
+ args := m.Called(name)
+ return args.Get(0).(net.Conn), args.Error(1)
+}
+func (m *Mock) CopyToContainer(containerName, destination, fileName string, content io.Reader) error {
+ args := m.Called(containerName, destination, fileName, content)
+ return args.Error(0)
+}
+
+func (m *Mock) CopyFromContainer(containerName, filePath string) (io.Reader, error) {
+ args := m.Called(containerName, filePath)
+ return args.Get(0).(io.Reader), args.Error(1)
+}
+func (m *Mock) ContainerDelete(id string) error {
+ args := m.Called(id)
+ return args.Error(0)
+}
+func (m *Mock) ContainerCreate(opts CreateContainerOpts) error {
+ args := m.Called(opts)
+ return args.Error(0)
+}
+func (m *Mock) ContainerIPs(id string) (map[string]string, error) {
+ args := m.Called(id)
+ return args.Get(0).(map[string]string), args.Error(1)
+}
+
+func (m *Mock) ExecAttach(instanceName string, command []string, out io.Writer) (int, error) {
+ args := m.Called(instanceName, command, out)
+ return args.Int(0), args.Error(1)
+}
+func (m *Mock) NetworkDisconnect(containerId, networkId string) error {
+ args := m.Called(containerId, networkId)
+ return args.Error(0)
+}
+func (m *Mock) NetworkDelete(id string) error {
+ args := m.Called(id)
+ return args.Error(0)
+}
+func (m *Mock) Exec(instanceName string, command []string) (int, error) {
+ args := m.Called(instanceName, command)
+ return args.Int(0), args.Error(1)
+}
+func (m *Mock) SwarmInit(advertiseAddr string) (*SwarmTokens, error) {
+ args := m.Called(advertiseAddr)
+ return args.Get(0).(*SwarmTokens), args.Error(1)
+}
+func (m *Mock) SwarmJoin(addr, token string) error {
+ args := m.Called(addr, token)
+ return args.Error(0)
+}
+func (m *Mock) ConfigCreate(name string, labels map[string]string, data []byte) error {
+ args := m.Called(name, labels, data)
+ return args.Error(0)
+}
+func (m *Mock) ConfigDelete(name string) error {
+ args := m.Called(name)
+ return args.Error(0)
+}
+
+type MockConn struct {
+}
+
+func (m *MockConn) Read(b []byte) (n int, err error) {
+ return len(b), nil
+}
+
+func (m *MockConn) Write(b []byte) (n int, err error) {
+ return len(b), nil
+}
+
+func (m *MockConn) Close() error {
+ return nil
+}
+
+func (m *MockConn) LocalAddr() net.Addr {
+ return &net.IPAddr{}
+}
+
+func (m *MockConn) RemoteAddr() net.Addr {
+ return &net.IPAddr{}
+}
+
+func (m *MockConn) SetDeadline(t time.Time) error {
+ return nil
+}
+
+func (m *MockConn) SetReadDeadline(t time.Time) error {
+ return nil
+}
+
+func (m *MockConn) SetWriteDeadline(t time.Time) error {
+ return nil
+}
diff --git a/dockerfiles/dind/.editorconfig b/dockerfiles/dind/.editorconfig
new file mode 100644
index 0000000000000000000000000000000000000000..e39eb0e4d2d013503f0f02de893673861bd0b1b6
--- /dev/null
+++ b/dockerfiles/dind/.editorconfig
@@ -0,0 +1,14 @@
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+indent_style = space
+indent_size = 4
+
+# Tab indentation (no size specified)
+[{Makefile,*.go}]
+indent_style = tab
diff --git a/dockerfiles/dind/.gitconfig b/dockerfiles/dind/.gitconfig
new file mode 100644
index 0000000000000000000000000000000000000000..87b8f9be00f890c66e82b81fe6617ed7732ec11a
--- /dev/null
+++ b/dockerfiles/dind/.gitconfig
@@ -0,0 +1,2 @@
+[url "https://"]
+ insteadOf = git://
diff --git a/dockerfiles/dind/.inputrc b/dockerfiles/dind/.inputrc
new file mode 100644
index 0000000000000000000000000000000000000000..6a5b035b1a092730381e34e8b8219c3407465b4f
--- /dev/null
+++ b/dockerfiles/dind/.inputrc
@@ -0,0 +1,73 @@
+# /etc/inputrc - global inputrc for libreadline
+# See readline(3readline) and `info rluserman' for more information.
+
+# Be 8 bit clean.
+set input-meta on
+set output-meta on
+
+# To allow the use of 8bit-characters like the german umlauts, uncomment
+# the line below. However this makes the meta key not work as a meta key,
+# which is annoying to those which don't need to type in 8-bit characters.
+
+# set convert-meta off
+
+# try to enable the application keypad when it is called. Some systems
+# need this to enable the arrow keys.
+# set enable-keypad on
+
+# see /usr/share/doc/bash/inputrc.arrows for other codes of arrow keys
+
+# do not bell on tab-completion
+# set bell-style none
+# set bell-style visible
+
+# some defaults / modifications for the emacs mode
+$if mode=emacs
+
+# allow the use of the Home/End keys
+"\e[1~": beginning-of-line
+"\e[4~": end-of-line
+
+# allow the use of the Delete/Insert keys
+"\e[3~": delete-char
+"\e[2~": quoted-insert
+
+# mappings for "page up" and "page down" to step to the beginning/end
+# of the history
+# "\e[5~": beginning-of-history
+# "\e[6~": end-of-history
+
+# alternate mappings for "page up" and "page down" to search the history
+# "\e[5~": history-search-backward
+# "\e[6~": history-search-forward
+
+# mappings for Ctrl-left-arrow and Ctrl-right-arrow for word moving
+"\e[1;5C": forward-word
+"\e[1;5D": backward-word
+"\e[5C": forward-word
+"\e[5D": backward-word
+"\e\e[C": forward-word
+"\e\e[D": backward-word
+
+$if term=rxvt
+"\e[7~": beginning-of-line
+"\e[8~": end-of-line
+"\eOc": forward-word
+"\eOd": backward-word
+$endif
+
+# for non RH/Debian xterm, can't hurt for RH/Debian xterm
+# "\eOH": beginning-of-line
+# "\eOF": end-of-line
+
+# for freebsd console
+# "\e[H": beginning-of-line
+# "\e[F": end-of-line
+
+$endif
+
+# faster completion
+set show-all-if-ambiguous on
+
+"\e[A": history-search-backward
+"\e[B": history-search-forward
diff --git a/dockerfiles/dind/.profile b/dockerfiles/dind/.profile
new file mode 100644
index 0000000000000000000000000000000000000000..1e1c8d944058a6e1e57a0379c3dc8f46c9cc0ea9
--- /dev/null
+++ b/dockerfiles/dind/.profile
@@ -0,0 +1,6 @@
+export PS1='\e[1m\e[31m[\h] \e[32m($(docker-prompt)) \e[34m\u@$(hostname -i)\e[35m \w\e[0m\n$ '
+alias vi='vim'
+export PATH=$PATH:/root/go/bin
+export DOCKER_HOST=""
+cat /etc/motd
+echo $BASHPID > /var/run/cwd
diff --git a/dockerfiles/dind/.vimrc b/dockerfiles/dind/.vimrc
new file mode 100644
index 0000000000000000000000000000000000000000..591902057a78b5b8abf12074d468b96213e86d20
--- /dev/null
+++ b/dockerfiles/dind/.vimrc
@@ -0,0 +1,6 @@
+syntax on
+set autoindent
+set expandtab
+set number
+set shiftwidth=2
+set softtabstop=2
diff --git a/dockerfiles/dind/Dockerfile b/dockerfiles/dind/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..0c6cde85e4a7001ed59af2901c6fc5a847ca49c0
--- /dev/null
+++ b/dockerfiles/dind/Dockerfile
@@ -0,0 +1,69 @@
+ARG VERSION=docker:dind
+FROM ${VERSION}
+
+RUN apk add --no-cache py-pip python3-dev libffi-dev openssl-dev git tmux apache2-utils vim build-base gettext-dev curl bash-completion bash util-linux jq openssh openssl tree \
+ && ln -s /usr/local/bin/docker /usr/bin/docker
+
+ENV GOPATH /root/go
+ENV IPTABLES_LEGACY /usr/local/sbin/.iptables-legacy/
+ENV PATH $IPTABLES_LEGACY:$GOPATH:$PATH
+
+
+ENV DOCKER_TLS_CERTDIR=""
+ENV DOCKER_CLI_EXPERIMENTAL=enabled
+
+# Install compose
+ENV COMPOSE_VERSION=2.18.1
+RUN mkdir -p /usr/lib/docker/cli-plugins \
+ && curl -LsS https://github.com/docker/compose/releases/download/v$COMPOSE_VERSION/docker-compose-linux-x86_64 -o /usr/lib/docker/cli-plugins/docker-compose \
+ && chmod +x /usr/lib/docker/cli-plugins/docker-compose
+
+
+# Install scout
+ENV SCOUT_VERSION=1.0.9
+RUN wget -O /tmp/scout.tar.gz https://github.com/docker/scout-cli/releases/download/v1.0.9/docker-scout_1.0.9_linux_amd64.tar.gz \
+ && tar -xvf /tmp/scout.tar.gz docker-scout -C /usr/local/bin \
+ && chmod +x /usr/local/bin/docker-scout \
+ && ln -s $(which docker-scout) /usr/lib/docker/cli-plugins \
+ && rm /tmp/scout.tar.gz
+
+
+
+# Add bash completion and set bash as default shell
+RUN curl -sS https://raw.githubusercontent.com/docker/cli/refs/heads/master/contrib/completion/bash/docker -o /etc/bash_completion.d/docker \
+ && sed -i "s/ash/bash/" /etc/passwd
+
+# Replace modprobe with a no-op to get rid of spurious warnings
+# (note: we can't just symlink to /bin/true because it might be busybox)
+RUN rm /sbin/modprobe && echo '#!/bin/true' >/sbin/modprobe && chmod +x /sbin/modprobe
+
+# Install a nice vimrc file and prompt (by soulshake)
+COPY ["docker-prompt", "sudo", "/usr/local/bin/"]
+COPY [".vimrc", ".profile", ".inputrc", ".gitconfig", "./root/"]
+COPY ["motd", "/etc/motd"]
+COPY ["daemon.json", "/etc/docker/"]
+
+
+# Move to our home
+WORKDIR /root
+
+# Setup certs and ssh keys
+RUN mkdir -p /var/run/pwd/certs && mkdir -p /var/run/pwd/uploads \
+ && ssh-keygen -N "" -t ed25519 -f /etc/ssh/ssh_host_ed25519_key >/dev/null \
+ && mkdir ~/.ssh && ssh-keygen -N "" -t ed25519 -f ~/.ssh/id_rsa \
+ && cat ~/.ssh/id_rsa.pub > ~/.ssh/authorized_keys
+
+# Remove IPv6 alias for localhost and start docker in the background ...
+CMD cat /etc/hosts >/etc/hosts.bak && \
+ sed 's/^::1.*//' /etc/hosts.bak > /etc/hosts && \
+ sed -i "s/\PWD_IP_ADDRESS/$PWD_IP_ADDRESS/" /etc/docker/daemon.json && \
+ sed -i "s/\DOCKER_TLSENABLE/$DOCKER_TLSENABLE/" /etc/docker/daemon.json && \
+ sed -i "s/\DOCKER_TLSCACERT/$DOCKER_TLSCACERT/" /etc/docker/daemon.json && \
+ sed -i "s/\DOCKER_TLSCERT/$DOCKER_TLSCERT/" /etc/docker/daemon.json && \
+ sed -i "s/\DOCKER_TLSKEY/$DOCKER_TLSKEY/" /etc/docker/daemon.json && \
+ mount -t securityfs none /sys/kernel/security && \
+ echo "root:root" | chpasswd &> /dev/null && \
+ /usr/sbin/sshd -o PermitRootLogin=yes -o PrintMotd=no 2>/dev/null && \
+ dockerd &>/docker.log & \
+ while true ; do script -q -c "/bin/bash -l" /dev/null ; done
+# ... and then put a shell in the foreground, restarting it if it exits
diff --git a/dockerfiles/dind/Dockerfile.dind-ee b/dockerfiles/dind/Dockerfile.dind-ee
new file mode 100644
index 0000000000000000000000000000000000000000..b7e7da05f2a13bb2abcb8ff3ef7345d296eac728
--- /dev/null
+++ b/dockerfiles/dind/Dockerfile.dind-ee
@@ -0,0 +1,54 @@
+ARG VERSION=franela/docker:ubuntu-19.03ee
+#ARG VERSION=franela/docker:18.09.2-ee-dind
+
+FROM ${VERSION}
+
+RUN apt-get update \
+ && apt-get install -y git tmux python-pip apache2-utils vim curl jq bash-completion screen tree zip \
+ && rm -rf /var/lib/apt/lists/*
+
+# Add kubectl client
+RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.11.7/bin/linux/amd64/kubectl \
+ && chmod +x ./kubectl \
+ && mv ./kubectl /usr/local/bin/kubectl
+
+ENV COMPOSE_VERSION=1.22.0
+
+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
+
+
+# Install a nice vimrc file and prompt (by soulshake)
+COPY ["docker-prompt", "sudo", "ucp-beta.sh", "/usr/local/bin/"]
+COPY [".vimrc",".profile", ".inputrc", ".gitconfig", "workshop_beta.lic", "ucp-config.toml", "./root/"]
+COPY ["motd", "/etc/motd"]
+COPY ["ee/daemon.json", "/etc/docker/"]
+COPY ["ee/cert.pem", "ee/key.pem", "/opt/pwd/certs/"]
+COPY ["ee/ucp-key.pem", "./root/key.pem"]
+COPY ["ee/ucp-cert.pem", "./root/cert.pem"]
+
+# Move to our home
+WORKDIR /root
+
+# Setup certs and uploads folders
+RUN mkdir -p /opt/pwd/certs /opt/pwd/uploads
+
+VOLUME ["/var/lib/kubelet"]
+
+# Remove IPv6 alias for localhost and start docker in the background ...
+CMD cat /etc/hosts >/etc/hosts.bak && \
+ sed 's/^::1.*//' /etc/hosts.bak > /etc/hosts && \
+ sed -i "s/\PWD_IP_ADDRESS/$PWD_IP_ADDRESS/" /etc/docker/daemon.json && \
+ sed -i "s/\DOCKER_TLSENABLE/$DOCKER_TLSENABLE/" /etc/docker/daemon.json && \
+ sed -i "s/\DOCKER_TLSCACERT/$DOCKER_TLSCACERT/" /etc/docker/daemon.json && \
+ sed -i "s/\DOCKER_TLSCERT/$DOCKER_TLSCERT/" /etc/docker/daemon.json && \
+ sed -i "s/\DOCKER_TLSKEY/$DOCKER_TLSKEY/" /etc/docker/daemon.json && \
+ mount -t securityfs none /sys/kernel/security && \
+ mount --make-rshared / && \
+ #mount --make-rshared -t tmpfs tmpfs /run && \
+ #mount --make-rshared /var/lib/kubelet && \
+ #mount --make-rshared /var/lib/docker && \
+ dockerd > /docker.log 2>&1 & \
+ while true ; do script -q -c "/bin/bash -l" /dev/null ; done
+# ... and then put a shell in the foreground, restarting it if it exits
diff --git a/dockerfiles/dind/copy_certs.ps1 b/dockerfiles/dind/copy_certs.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..d1fcf892f8727a8ac4827ae44330a34167435863
--- /dev/null
+++ b/dockerfiles/dind/copy_certs.ps1
@@ -0,0 +1,114 @@
+param (
+ [Parameter(Mandatory = $true)]
+ [string] $Node,
+ [Parameter(Mandatory = $true)]
+ [string] $SessionId,
+ [Parameter(Mandatory = $true)]
+ [string] $FQDN
+)
+
+
+function GetDirectUrlFromIp ($ip) {
+ $ip_dash=$ip -replace "\.","-"
+ $url="https://ip${ip_dash}-${SessionId}.direct.${FQDN}"
+ return $url
+}
+
+function WaitForUrl ($url) {
+ write-host $url
+ do {
+ try{
+ invoke-webrequest -UseBasicParsing -uri $url | Out-Null
+ } catch {}
+ $status = $?
+ sleep 1
+ } until($status)
+}
+
+function GetNodeRoutableIp ($nodeName) {
+ $JQFilter='.instances[] | select (.hostname == \"{0}\") | .routable_ip' -f $nodeName
+ $rip = (invoke-webrequest -UseBasicParsing -uri "https://$FQDN/sessions/$SessionId").Content | jq -r $JQFilter
+
+ IF([string]::IsNullOrEmpty($rip)) {
+ Write-Host "Could not fetch IP for node $nodeName"
+ exit 1
+ }
+ return $rip
+}
+
+function Set-UseUnsafeHeaderParsing
+{
+ param(
+ [Parameter(Mandatory,ParameterSetName='Enable')]
+ [switch]$Enable,
+
+ [Parameter(Mandatory,ParameterSetName='Disable')]
+ [switch]$Disable
+ )
+
+ $ShouldEnable = $PSCmdlet.ParameterSetName -eq 'Enable'
+
+ $netAssembly = [Reflection.Assembly]::GetAssembly([System.Net.Configuration.SettingsSection])
+
+ if($netAssembly)
+ {
+ $bindingFlags = [Reflection.BindingFlags] 'Static,GetProperty,NonPublic'
+ $settingsType = $netAssembly.GetType('System.Net.Configuration.SettingsSectionInternal')
+
+ $instance = $settingsType.InvokeMember('Section', $bindingFlags, $null, $null, @())
+
+ if($instance)
+ {
+ $bindingFlags = 'NonPublic','Instance'
+ $useUnsafeHeaderParsingField = $settingsType.GetField('useUnsafeHeaderParsing', $bindingFlags)
+
+ if($useUnsafeHeaderParsingField)
+ {
+ $useUnsafeHeaderParsingField.SetValue($instance, $ShouldEnable)
+ }
+ }
+ }
+}
+
+
+$ProgressPreference = 'SilentlyContinue'
+$ErrorActionPreference = 'Stop'
+
+Set-UseUnsafeHeaderParsing -Enable
+
+Start-Transcript -path ("C:\{0}.log" -f $MyInvocation.MyCommand.Name) -append
+
+add-type @"
+ using System.Net;
+ using System.Security.Cryptography.X509Certificates;
+
+ public class IDontCarePolicy : ICertificatePolicy {
+ public IDontCarePolicy() {}
+ public bool CheckValidationResult(
+ ServicePoint sPoint, X509Certificate cert,
+ WebRequest wRequest, int certProb) {
+ return true;
+ }
+ }
+"@
+
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+
+[System.Net.ServicePointManager]::CertificatePolicy = new-object IDontCarePolicy
+
+
+$dtr_ip = GetNodeRoutableIp $Node
+$dtr_url = GetDirectUrlFromIp $dtr_ip
+$dtr_hostname = $dtr_url -replace "https://",""
+
+WaitForUrl "${dtr_url}/ca"
+
+invoke-webrequest -UseBasicParsing -uri "$dtr_url/ca" -o c:\ca.crt
+
+$cert = new-object System.Security.Cryptography.X509Certificates.X509Certificate2 c:\ca.crt
+$store = new-object System.Security.Cryptography.X509Certificates.X509Store('Root','localmachine')
+$store.Open('ReadWrite')
+$store.Add($cert)
+$store.Close()
+
+Stop-Transcript
diff --git a/dockerfiles/dind/daemon.json b/dockerfiles/dind/daemon.json
new file mode 100644
index 0000000000000000000000000000000000000000..8276eca30404b32b8296830f2f968d533b58ba9c
--- /dev/null
+++ b/dockerfiles/dind/daemon.json
@@ -0,0 +1,17 @@
+{
+ "experimental": true,
+ "debug": true,
+ "log-level": "info",
+ "registry-mirrors": ["https://mirror.gcr.io"],
+ "insecure-registries": [
+ "127.0.0.1"
+ ],
+ "hosts": [
+ "unix:///var/run/docker.sock",
+ "tcp://0.0.0.0:2375"
+ ],
+ "tls": DOCKER_TLSENABLE,
+ "tlscacert": "DOCKER_TLSCACERT",
+ "tlscert": "DOCKER_TLSCERT",
+ "tlskey": "DOCKER_TLSKEY"
+}
diff --git a/dockerfiles/dind/docker-prompt b/dockerfiles/dind/docker-prompt
new file mode 100644
index 0000000000000000000000000000000000000000..3df79d2953132a3867d43daba361fc6d2cd35591
--- /dev/null
+++ b/dockerfiles/dind/docker-prompt
@@ -0,0 +1,22 @@
+
+#!/bin/sh
+case "$DOCKER_HOST" in
+*:3376)
+ echo swarm
+ ;;
+*:2376)
+ echo $DOCKER_MACHINE_NAME
+ ;;
+*:2375)
+ echo $DOCKER_MACHINE_NAME
+ ;;
+*:55555)
+ echo $DOCKER_MACHINE_NAME
+ ;;
+"")
+ echo local
+ ;;
+*)
+ echo unknown
+ ;;
+esac
diff --git a/dockerfiles/dind/ee/cert.pem b/dockerfiles/dind/ee/cert.pem
new file mode 100644
index 0000000000000000000000000000000000000000..e600f76f3a85f1c9d07e7732471bf46e0809bffc
--- /dev/null
+++ b/dockerfiles/dind/ee/cert.pem
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIE9jCCAt6gAwIBAgIQSCiXatddwed3bL9M9bierjANBgkqhkiG9w0BAQsFADAO
+MQwwCgYDVQQKEwNVQ1AwHhcNMTcwOTE1MjAzMzAwWhcNMjAwODMwMjAzMzAwWjAO
+MQwwCgYDVQQKEwNVQ1AwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCq
+prmPRweArtZQ6HHDeYCSC3WxQOy6hakc3VZa6JEldbEoVjOc7MqZNPvTIp8b/W8H
+O10ibEGZ03vyeq10UsiFQiYmdhn1SqEilZnFSo892PSpGaN7VO325uUnIccJqc3O
+0YOdvNCdp9roZZ/K7z9nuC37cLy6+Lq2oLr1WYAxncJHedUi3LQCC+2qEBIVL+md
+9yE8amFrYbDhbNqmIcAJ2KmkqBPa0Pa+Qe1FxqQI5zJOT5rOJgF3JbWeqFpm0Zjx
+CPTt0cPY4lyQ2U9lyMXJmS4+R0wekkZXywaU1mJi3JJIlMSBMWmWoTrx5mLVWOLv
+u44hYerfOmN+ImXRWq4NAPLi4722/OLzCmFn81fdUHOFyxg2Tr23b6I6sMyUfLJ0
+lqS+thJ7N/tcQe3nTeQm9dcruDbJpjJQrQkjq9CFFsxNEXBT6EFMRp8oDutOAyHf
+guVeqdH5kz6vprNiLfSTqqZSEeQokRkHTyxpZ4grBBCiocsAxm8yLNqhcg3w44CN
+9G/3pylgu7xYSEXHYnnlxsk0MHxDFZ4NTo0UBuyIuozoePIS63GvsyBsBzKzO/RZ
+NsnPm3klZ4QnT3dIe0eRtCyu/prRmEMD/zC20fRcAuiG7jyV9NB/9mbLeDjAAngW
+1UhrWpAMiObQZN4h5+ofc0EXFHVvOWaqBmYXlNlEeQIDAQABo1AwTjAOBgNVHQ8B
+Af8EBAMCA6gwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMAwGA1UdEwEB
+/wQCMAAwDwYDVR0RBAgwBocEfwAAATANBgkqhkiG9w0BAQsFAAOCAgEAFT1PFimD
+KGg8fVjuUO9IXf12LA7c+M6Eltyz22Ffxgopl1eHi4xHEfU94ueUAmODrag7Rc4E
+VmvrMFIsuFrX/xYjiu2gpHPOP2nQjNRAwKDU0gr2bZ+y98EBtlYO/aFMmYCxJr7B
+6esyA7I/cwLTxaNoTh67VTdPhfDmuEshoQn7Mtop38suevU5YBMTmUl7cp8bVdib
+j7UkTq0oRKmAchMAz3W0TgGw9ZKJzU6zEck/3Csz5RWlTI9HV7R7J8aGEIeHGf/i
+G+tfg0T8h+rQPkyCic5DIYuQzZ/P9pfJkedZuQU/mu0U/0IsNdkv9NX/4RQazu/Q
+OzQ71FOO2HR/S3hcLzS1Iy2zrHbARwji/Sr95gVE1Z4QCK2xSvyy9aqzHwRfc4SX
+AzaJhkACCnY7VDK6WJW7jnfkYco+l0tczDkyPjE7h3wP35tCuAZAvGkcrIbBL4oR
+8bnwYAOqiG0cPBmFDBYW7v19qIspw5XDjfMu4YEHon7pYdiKK0Brf0iL+Ep4b1oB
+8uvAysbc2Z/gIj1AsfnwSnrzcvzO6H1oCye277cSn2Z/ebiBaQi+kR3mubX96aPy
+bFc9Xb11/y0Y7kYmJ3ifHDJkWerpz5bWEm2KDq1qsFRH9zUMEVfJAXThITawqfuG
+3UBYWv8RePLnRbbnPuSaO9slNCoKl3NLqyk=
+-----END CERTIFICATE-----
diff --git a/dockerfiles/dind/ee/daemon.json b/dockerfiles/dind/ee/daemon.json
new file mode 100644
index 0000000000000000000000000000000000000000..bd4e5092627265798d5252600367fa9c47cc300f
--- /dev/null
+++ b/dockerfiles/dind/ee/daemon.json
@@ -0,0 +1,10 @@
+{
+ "experimental": true,
+ "debug": true,
+ "log-level": "info",
+ "insecure-registries": ["127.0.0.1"],
+ "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"],
+ "tls": true,
+ "tlscert": "/opt/pwd/certs/cert.pem",
+ "tlskey": "/opt/pwd/certs/key.pem"
+}
diff --git a/dockerfiles/dind/ee/key.pem b/dockerfiles/dind/ee/key.pem
new file mode 100644
index 0000000000000000000000000000000000000000..dbae394893d9acc7c0c2b59734a4aa980c08636d
--- /dev/null
+++ b/dockerfiles/dind/ee/key.pem
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIJJwIBAAKCAgEAqqa5j0cHgK7WUOhxw3mAkgt1sUDsuoWpHN1WWuiRJXWxKFYz
+nOzKmTT70yKfG/1vBztdImxBmdN78nqtdFLIhUImJnYZ9UqhIpWZxUqPPdj0qRmj
+e1Tt9ublJyHHCanNztGDnbzQnafa6GWfyu8/Z7gt+3C8uvi6tqC69VmAMZ3CR3nV
+Ity0AgvtqhASFS/pnfchPGpha2Gw4WzapiHACdippKgT2tD2vkHtRcakCOcyTk+a
+ziYBdyW1nqhaZtGY8Qj07dHD2OJckNlPZcjFyZkuPkdMHpJGV8sGlNZiYtySSJTE
+gTFplqE68eZi1Vji77uOIWHq3zpjfiJl0VquDQDy4uO9tvzi8wphZ/NX3VBzhcsY
+Nk69t2+iOrDMlHyydJakvrYSezf7XEHt503kJvXXK7g2yaYyUK0JI6vQhRbMTRFw
+U+hBTEafKA7rTgMh34LlXqnR+ZM+r6azYi30k6qmUhHkKJEZB08saWeIKwQQoqHL
+AMZvMizaoXIN8OOAjfRv96cpYLu8WEhFx2J55cbJNDB8QxWeDU6NFAbsiLqM6Hjy
+Eutxr7MgbAcyszv0WTbJz5t5JWeEJ093SHtHkbQsrv6a0ZhDA/8wttH0XALohu48
+lfTQf/Zmy3g4wAJ4FtVIa1qQDIjm0GTeIefqH3NBFxR1bzlmqgZmF5TZRHkCAwEA
+AQKCAgBDSNmBFJBwvH7kB8JTQGThMIOHEAJGyMyVBPA3h9sy2eSv8s0G4pY/MhTY
+ep4hext7znw6RlTXQfts79HUO4+0exBvucEiZfqCmFm44Fz6FcDhq6o5xpLM9t0D
+QN4pgToUgadTWk8m2jgFyYvnh82IJ6Z5rUm8rrVvrJAKjO9uoLUpWXAf/sU6yVk7
+5Ho8wFdsYTRJjeg7XplPSIwtVMFTIIpC0cKCVEH1YikbiebDW+UJ23k+Lt4FDGk/
+1UFPqPSUlON9oWeG7DlzIzua9j6F7k+9Xn80zpfNpc9CgATq1e0XkRCpn8HyEkAb
+gKsXU6SmwVyY7PKecXcpFIbwtMBK2zTG4VrmgsjwptK1S8lbqYftQeTxvNYdhjxA
+gdkBG5qIBkLcr8m796V2fDtJ6wvsVi+yDh+H7T8/vZuB9iaHJ3L1v36WiTODLTFW
+/OlgfimiBXuK8Z1EiB6+w522TdmhKOiWfjHdl7JSzsOla5i5cbcdeaD4AUzlmvGZ
+RCBE9Cd7RWGmDxnWz4NWFepwSfnOOQI9W95QkcRgwH61Y2axcdio0xJpQnUXiKHH
+rHhPTW0eDD7yoIqqKKK3evCOxpbJy6M/+fVqNZYWEfJ0cb7+Ska6aW3rUv8aeYFj
+xzitqKuL/0nFKpeppAkvXvoZf/mM0QtG+lgUHgOngwweYrrkwQKCAQEA3JtXFZDQ
+mIfkv0mAiwV5QbzQ63OxkO0MtPqSq50I8F6S+fIz+ILhxbMjcGq5dCbnJCFGJqn3
+7PXrT6nFXZ8j2/dcXmtxala2VAAq+GyA0TY/DQ6seTaKhsLq50vnzMXHT0pU8s/2
+4n7euf66lzQ1ByKrqXZCAyNajUXPoL37HFgFtCrEJlvi//K8x+tHr3QgF4Si8l31
+A1HLq2+KbppWXzc//knanstsCIxPvEelV0GZn3r5opiOczS30rYo87wKI9aCRgLZ
+GEKrMwlNVwwhScJd4msEYMsXUUxzDcNr5oi+iQmEDJpBKd98+3/Sp9XWVXUbik9a
+QfOvUcQMfDc1pwKCAQEAxgeiaYBb369Z6CW7rC3b3YnwOBJVK23PYcpN2DtnhRRI
+ARZgZBhwYKxDQ3djXZCiPEVtwO4WO8fCcY0GUFP2aVWuaokGjk1gNFwN6F046OdY
+WGETEe7AUCLuuwAv7Aqqug3Y6bxCtPGN3MNHT8qjTH99EMHx8L2+0UiIXnQreGmH
+VL/HEnpfDDZK4nfrwxdJOSueGdyOlflUIpDgmScIbKvIsyKhB2UstFBsCuDzhfE/
+a0VWDnZHgZPA/JhyhRy5eL9QGOqsdnzSxgvEbOyCR5p2jtO9otFw9fxpxF7uA0Yq
+EBye0gidmnF/FKDNK0iggtk34LTrDv2fz4tclXM43wKCAQBY79NC4XgHFjoVGBfX
+dCR3aRy8346Fg9uslpxMzWlOJoKQe6GSHYnRvy+ZYZ1Ug16KBVQXwEwwXtA39JSZ
+8s9tHaNCeYRmv4CQCuVH885XCcyPggvsbh2YyLoU91gDCPUaNThcD5VTqJw4VcZ5
+sNV0A/k6v29LfpRCAhP7lLvIqH/cK6WaZU71qrGK04K57FIHyTQ8C778UJyQh85C
+WrxZdJe696FIhXAPXinDGQtCSzMYxWYgs+ox7d3x9/g4kuVvn0oz2XAWRMJqN+TT
+JBPDfbWF02kXcKj84Jo9wTwd26Ec9BYlUobUz8G+TsDpYt8e4rBwqR8VGZ3jk+sI
+pOVfAoIBAA78xO33KPzk6IkJUgrV7a32opeby5Zd2TQte3bCCDOqNUjfyKvKrbaj
+UvPoNTz/lUe6eXQAkO41UCIH6lJqCFwwf+LQPA7JDF7qGKNdatE1sRn/PtI8n5Fx
+E2BTw0y6AfHS2nfWJ7ZKEdKDdQI08+b2PyDljMoLkkWEl82OPTv/wJ5JZWegm1Dx
+SvmY2d8KBCCvjGeoqaHwHM4A6P6uVZTj62yjUkyc+6Up8QNhwwyAFayosrqleQP1
+isWTRBeO9PqOgCFioWrWR511hog33iRNLGvi2pdYApSbZeXWyWy2Arj1cY+z1zm5
+HUUSZnTAKmW8yt3W03Nu/olWossszUECggEAD+dqDccmWF30yg82mxIMPb8pMV27
++ciQssiibGmhFvPcIfzish9FunXqLG7q+4M4M+O4WQ9unuaTH+z9TU7w3Foo4Xdf
+GePuwmZdpuYxClHAsNALuKWEJcjfFOdETLkAbk81+ghtyFblkPPI82wofs4K8OII
+1KPPDKoxeXmKXVF1UmOJX1KFyMnEjv0+Z1GrHnNV4703cNTMpDybaGpHsE77Vqd0
+ToZY9VG9eDLzaB6n5emSyFGBG73WQFU4EbLKjEBxtthgu8J9b17x96eF1NGZsEl1
+wEJvZpg7v6wyHK5XcYpwLY19+0khtvXwA7KKEr+sHqzF6arIqhl5hDLDAQ==
+-----END RSA PRIVATE KEY-----
diff --git a/dockerfiles/dind/ee/ucp-cert.pem b/dockerfiles/dind/ee/ucp-cert.pem
new file mode 100644
index 0000000000000000000000000000000000000000..7e124b1180aff1188f846863cb15b3847effaabd
--- /dev/null
+++ b/dockerfiles/dind/ee/ucp-cert.pem
@@ -0,0 +1,63 @@
+-----BEGIN CERTIFICATE-----
+MIIGPDCCBSSgAwIBAgISA4MIK4JV9npV+QdQS7wVa48rMA0GCSqGSIb3DQEBCwUA
+MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
+ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xODAzMzEyMTQ3MjZaFw0x
+ODA2MjkyMTQ3MjZaMDQxMjAwBgNVBAMMKSouZGlyZWN0LmJldGEtaHlicmlkLnBs
+YXktd2l0aC1kb2NrZXIuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEA6PQCi9Rqr7Ka1KXSGCfBQVzgPyx/hh+uST1dz7PDw2epghYyaqNByaQEVKNR
+3ubPvOoASzhdJ1dZdyUzKUoU/jm8hgVK7HHdQDpFEX60az+r4Xo32R6WirG5+GXd
+hU3M0yRzbu0zZx7eVZognP/HcXJDhuf16hiHKmCr6MYXV4JY9xLMxExZOTB4fpGA
+Loiyvn2OEZAhREhiSX+6n4x7KJga8gYn/0f89o7up1DYQSwev+gQgRjTGlo1xrgu
+Oztekc3ydvbhGv7aL7Uj/zqPcVvXnDfnioQV7kEDcz8gupFyV7gZKolR1G8IQJdm
+TaYHguzFXF5Q3lKVWx19/CSZ8wIDAQABo4IDMDCCAywwDgYDVR0PAQH/BAQDAgWg
+MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0G
+A1UdDgQWBBTVloZoUI5vKAN+D1PTgtYBgU184zAfBgNVHSMEGDAWgBSoSmpjBH3d
+uubRObemRWXv86jsoTBvBggrBgEFBQcBAQRjMGEwLgYIKwYBBQUHMAGGImh0dHA6
+Ly9vY3NwLmludC14My5sZXRzZW5jcnlwdC5vcmcwLwYIKwYBBQUHMAKGI2h0dHA6
+Ly9jZXJ0LmludC14My5sZXRzZW5jcnlwdC5vcmcvMDQGA1UdEQQtMCuCKSouZGly
+ZWN0LmJldGEtaHlicmlkLnBsYXktd2l0aC1kb2NrZXIuY29tMIH+BgNVHSAEgfYw
+gfMwCAYGZ4EMAQIBMIHmBgsrBgEEAYLfEwEBATCB1jAmBggrBgEFBQcCARYaaHR0
+cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcwgasGCCsGAQUFBwICMIGeDIGbVGhpcyBD
+ZXJ0aWZpY2F0ZSBtYXkgb25seSBiZSByZWxpZWQgdXBvbiBieSBSZWx5aW5nIFBh
+cnRpZXMgYW5kIG9ubHkgaW4gYWNjb3JkYW5jZSB3aXRoIHRoZSBDZXJ0aWZpY2F0
+ZSBQb2xpY3kgZm91bmQgYXQgaHR0cHM6Ly9sZXRzZW5jcnlwdC5vcmcvcmVwb3Np
+dG9yeS8wggEDBgorBgEEAdZ5AgQCBIH0BIHxAO8AdQDbdK/uyynssf7KPnFtLOW5
+qrs294Rxg8ddnU83th+/ZAAAAWJ+PniYAAAEAwBGMEQCIDngZdWcYWY0fPfUGTqX
+/Vt2qx+PRN5DN+m13TnA37e2AiBHIi5kMSxlvKNc3xzuJrvt/RKaj9xsBLmc8+uW
+ckaEdAB2ACk8UZZUyDlluqpQ/FgH1Ldvv1h6KXLcpMMM9OVFR/R4AAABYn4+eLUA
+AAQDAEcwRQIhAMkf8SYdt1egjzBE6nzOrY+f4WMS/N6XWN+gFl0mQIkhAiBn9+GG
+0XbLw33+WNJLUkau2ZdTo5kTw2qdUXdYpWJwrDANBgkqhkiG9w0BAQsFAAOCAQEA
+TAl62gFi+2l/yLItjNIrXeWh2ICH/epjeWlmF+rAb7Sb4iz9U8fsNBdDBQh25xJo
+6nLOlS2NG0hdUScylCYyGJZe6PeQvGO+qSLDamXf1DvXWvzbmQOCUkejgD7Uwbol
+5huuCAKoW4SsiaMku0J3545MEQx4Q5cPetsPawaByY5sgr2GZJzgM7lvtzr4hKWg
+x5QAns/bmcqe9LCJ2NLcgArliYu6dOHtS62kB7/Dz2DQRtCvpV553RaBe4k9Ruwl
+0ndHvjEC5OWa5sW1hwow5W3PC7Db7s0zqpt63EITkhrUOqtqtkwOMYBAkFIIe1eR
+T5fSFAdirKUOt5GnRJ40qw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/
+MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
+DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow
+SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT
+GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF
+q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8
+SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0
+Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA
+a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj
+/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T
+AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG
+CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv
+bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k
+c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw
+VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC
+ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz
+MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu
+Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF
+AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo
+uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/
+wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu
+X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG
+PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6
+KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
+-----END CERTIFICATE-----
diff --git a/dockerfiles/dind/ee/ucp-key.pem b/dockerfiles/dind/ee/ucp-key.pem
new file mode 100644
index 0000000000000000000000000000000000000000..e61bf3b72c59ab4e391d1556a5bffa1c621519d2
--- /dev/null
+++ b/dockerfiles/dind/ee/ucp-key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDo9AKL1GqvsprU
+pdIYJ8FBXOA/LH+GH65JPV3Ps8PDZ6mCFjJqo0HJpARUo1He5s+86gBLOF0nV1l3
+JTMpShT+ObyGBUrscd1AOkURfrRrP6vhejfZHpaKsbn4Zd2FTczTJHNu7TNnHt5V
+miCc/8dxckOG5/XqGIcqYKvoxhdXglj3EszETFk5MHh+kYAuiLK+fY4RkCFESGJJ
+f7qfjHsomBryBif/R/z2ju6nUNhBLB6/6BCBGNMaWjXGuC47O16RzfJ29uEa/tov
+tSP/Oo9xW9ecN+eKhBXuQQNzPyC6kXJXuBkqiVHUbwhAl2ZNpgeC7MVcXlDeUpVb
+HX38JJnzAgMBAAECggEAVqm4bMa4bea3HRcXYu8fQS7JKhdm1cHhd9PBm6yXzpE5
+CXEyjmNv7RD8n3Qm2BLsA67WLyWn2iPv35hSQTETQETAcudzKSVvFx7WZRzLB/8m
+9XofXsG3ZZ+avONAlwALjB1KaGEMN3fPZO8y5NVvIDBPGNggr1cyqbxPGAjh1Cav
+Laqki0rdPfr3FhxTyPBdmBFDcaMLc77Yl/7rmQJRYWb1qe+g4SEG4xXmEYpcpSUz
+zDJZAkY5XAO5cHU5EoKgKJedVBNxqAaRtaisO9yv+CKMqD83hAWhXqeK1bSphghs
+2qIkzNe134ZNUBbmK2FDsAbiPMHNcMKuI4ljfb78iQKBgQD5oZ/uzaYTt6ZQQzKq
+rQFA2DxSlBt4Ewae5n6JYzw0hIjRf7LvitZF9zKXcMkHP2QcL+5RiibyJ6ohGypa
+jpDP+m5e0B5tS6gEgFzBnrXWbjnrDxUR5Qj0lKg3uuOXw8OdwNxn+MulKkIfGyTW
+pCu7G1nh/kltwvN87s4cJycwnwKBgQDu5XUyIcok8nxcBwtxu3zFdtdNn+P4Yq1a
+W2sUEUEJUDwcUZqksPIxQhG/SMEEtBqii+EJj3nAlaWItBgTE37mzKGyKv16ZiM1
+hr+Rlv5AURxER+Eo4JLFqULZKwMaDlXDrFdV2ulF+6SXWOqKrp4/6sPYxtxHmKfs
+oBnXq/4yLQKBgCQFl5+NG2cC/EPevoP0fRbPXT0JVEFqdW0ek6ndoQVvDpM0myyH
+202zUyCZTNj348lRfVFU3zPYV2t5kQ4KPolUePLDk3BwF2m24CusbE7qDv+FaKPx
+ae5pOTD5jfgLbsHn36Y9N5240FvOve0fOZRBaSH8YLovBJXFnAZh+/y/AoGALZzQ
+CJddAjruNZ/+tmNmykkLiL2riERG9waXZkh5E28nWvzVuvYx9+e2fcBFYkGFCF4O
+xIWJaJTp+zTvl8zUIPsXMG524UTZGiI1N3YN63fRHtRekDB4tZbAtbg5qmLsSyT/
+s9vNSFhor6EBfyMiAfAwHpaxflYOUearqHslWK0CgYEAzi/B0azCOaDqzpp6RhAL
+rhTRFfu2HR8wN8EJLOSbBbUnlSSJHdnHJBwyyXe3shD/rETLV8dHx+6/k47e1l2d
+MUlsad/dOKQyL2pY7UodBzPJkIkmwknDnKzioGety8Tb98oUSTQ8oHfHMuRBOie9
+mq1MSTuZyZtsdSXnFhH3qNc=
+-----END PRIVATE KEY-----
diff --git a/dockerfiles/dind/motd b/dockerfiles/dind/motd
new file mode 100644
index 0000000000000000000000000000000000000000..2c1701b41730b498ca8a9ed2063ee904baeef1b2
--- /dev/null
+++ b/dockerfiles/dind/motd
@@ -0,0 +1,8 @@
+###############################################################
+# WARNING!!!! #
+# This is a sandbox environment. Using personal credentials #
+# is HIGHLY! discouraged. Any consequences of doing so are #
+# completely the user's responsibilites. #
+# #
+# The PWD team. #
+###############################################################
diff --git a/dockerfiles/dind/ssh_config b/dockerfiles/dind/ssh_config
new file mode 100644
index 0000000000000000000000000000000000000000..f30d239b63c035071dfae5327657bc23b66edbc4
--- /dev/null
+++ b/dockerfiles/dind/ssh_config
@@ -0,0 +1,2 @@
+Host *
+ StrictHostKeyChecking no
diff --git a/dockerfiles/dind/sudo b/dockerfiles/dind/sudo
new file mode 100644
index 0000000000000000000000000000000000000000..637614a061fbd623a499d1234f54741a4a7b323a
--- /dev/null
+++ b/dockerfiles/dind/sudo
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+# This is shim to help with the case were pasted commands from a readme assume you are not root. Since this isto be run by root, it should effectively be a dummy command that allows the parameters to pass through.
+
+exec "$@"
diff --git a/dockerfiles/dind/ucp-beta.sh b/dockerfiles/dind/ucp-beta.sh
new file mode 100644
index 0000000000000000000000000000000000000000..33b9c1b4f9aa0bfdc035e7e12375704dd6cb718c
--- /dev/null
+++ b/dockerfiles/dind/ucp-beta.sh
@@ -0,0 +1,98 @@
+#!/bin/bash
+
+set -e
+
+function wait_for_url {
+ # Wait for docker daemon to be ready
+ while ! curl -k -sS $1 > /dev/null; do
+ sleep 1;
+ done
+}
+
+function deploy_ucp {
+ wait_for_url "https://localhost:2376"
+
+ docker config create com.docker.ucp.config $HOME/ucp-config.toml
+
+ docker run --rm -i --name ucp \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ docker/ucp:3.2.3 install --debug --force-insecure-tcp --skip-cloud-provider-check \
+ --san *.direct.${PWD_HOST_FQDN} \
+ --license $(cat $HOME/workshop_beta.lic) \
+ --swarm-port 2375 \
+ --existing-config \
+ --admin-username admin \
+ --admin-password admin1234
+
+ rm $HOME/workshop_beta.lic $HOME/ucp-config.toml
+ echo "Finished deploying UCP"
+}
+
+function get_instance_ip {
+ ip -o -4 a s eth1 | awk '{print $4}' | cut -d '/' -f1
+}
+
+function get_node_routable_ip {
+ curl -sS https://${PWD_HOST_FQDN}/sessions/${SESSION_ID} | jq -r '.instances[] | select(.hostname == "'$1'") | .routable_ip'
+}
+
+function get_direct_url_from_ip {
+ local ip_dash="${1//./-}"
+ local url="https://ip${ip_dash}-${SESSION_ID}.direct.${PWD_HOST_FQDN}"
+ echo $url
+}
+
+function deploy_dtr {
+ if [ $# -lt 1 ]; then
+ echo "DTR node hostname"
+ return
+ fi
+
+
+ local dtr_ip=$(get_node_routable_ip $1)
+ local ucp_ip=$(get_instance_ip)
+
+ local dtr_url=$(get_direct_url_from_ip $dtr_ip)
+ local ucp_url=$(get_direct_url_from_ip $ucp_ip)
+
+ docker run -i --rm docker/dtr:2.7.3 install \
+ --dtr-external-url $dtr_url \
+ --ucp-node $1 \
+ --ucp-username admin \
+ --ucp-password admin1234 \
+ --ucp-insecure-tls \
+ --ucp-url $ucp_url
+}
+
+function setup_dtr_certs {
+ if [ $# -lt 1 ]; then
+ echo "DTR node hostname is missing"
+ return
+ fi
+
+
+ local dtr_ip=$(get_node_routable_ip $1)
+ local dtr_url=$(get_direct_url_from_ip $dtr_ip)
+ local dtr_hostname="${dtr_url/https:\/\/}"
+
+ wait_for_url "$dtr_url/ca"
+
+ curl -kfsSL $dtr_url/ca -o /usr/local/share/ca-certificates/$dtr_hostname.crt
+ update-ca-certificates
+}
+
+
+case "$1" in
+ deploy)
+ deploy_ucp
+ deploy_dtr $2
+ setup_dtr_certs $2
+ ;;
+ setup-certs)
+ setup_dtr_certs $2
+ ;;
+ *)
+ echo "Illegal option $1"
+ ;;
+esac
+
diff --git a/dockerfiles/dind/ucp-config.toml b/dockerfiles/dind/ucp-config.toml
new file mode 100644
index 0000000000000000000000000000000000000000..ba78e3509ebf844f821a521b07146cf6b8f1399a
--- /dev/null
+++ b/dockerfiles/dind/ucp-config.toml
@@ -0,0 +1,2 @@
+[cluster_config]
+ custom_kubelet_flags = ["--http-check-frequency=20s", "--containerized=false"]
diff --git a/dockerfiles/dind/ucp.sh b/dockerfiles/dind/ucp.sh
new file mode 100644
index 0000000000000000000000000000000000000000..9a6ce417401f7bf514c269ad150d02de8d0c4a5b
--- /dev/null
+++ b/dockerfiles/dind/ucp.sh
@@ -0,0 +1,94 @@
+#!/bin/bash
+
+set -e
+
+function wait_for_url {
+ # Wait for docker daemon to be ready
+ while ! curl -k -sS $1 > /dev/null; do
+ sleep 1;
+ done
+}
+
+function deploy_ucp {
+ wait_for_url "https://localhost:2376"
+ docker run --rm -i --name ucp \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ docker/ucp:3.2.3 install --debug --force-insecure-tcp --skip-cloud-provider-check \
+ --san *.direct.${PWD_HOST_FQDN} \
+ --license $(cat $HOME/workshop_beta.lic) \
+ --swarm-port 2375 \
+ --admin-username admin \
+ --admin-password admin1234
+
+ rm $HOME/workshop_beta.lic
+ echo "Finished deploying UCP"
+}
+
+function get_instance_ip {
+ ip -o -4 a s eth1 | awk '{print $4}' | cut -d '/' -f1
+}
+
+function get_node_routable_ip {
+ curl -sS https://${PWD_HOST_FQDN}/sessions/${SESSION_ID} | jq -r '.instances[] | select(.hostname == "'$1'") | .routable_ip'
+}
+
+function get_direct_url_from_ip {
+ local ip_dash="${1//./-}"
+ local url="https://ip${ip_dash}-${SESSION_ID}.direct.${PWD_HOST_FQDN}"
+ echo $url
+}
+
+function deploy_dtr {
+ if [ $# -lt 1 ]; then
+ echo "DTR node hostname"
+ return
+ fi
+
+
+ local dtr_ip=$(get_node_routable_ip $1)
+ local ucp_ip=$(get_instance_ip)
+
+ local dtr_url=$(get_direct_url_from_ip $dtr_ip)
+ local ucp_url=$(get_direct_url_from_ip $ucp_ip)
+
+ docker run -i --rm docker/dtr:2.7.3 install \
+ --dtr-external-url $dtr_url \
+ --ucp-node $1 \
+ --ucp-username admin \
+ --ucp-password admin1234 \
+ --ucp-insecure-tls \
+ --ucp-url $ucp_url
+}
+
+function setup_dtr_certs {
+ if [ $# -lt 1 ]; then
+ echo "DTR node hostname is missing"
+ return
+ fi
+
+
+ local dtr_ip=$(get_node_routable_ip $1)
+ local dtr_url=$(get_direct_url_from_ip $dtr_ip)
+ local dtr_hostname="${dtr_url/https:\/\/}"
+
+ wait_for_url "$dtr_url/ca"
+
+ curl -kfsSL $dtr_url/ca -o /usr/local/share/ca-certificates/$dtr_hostname.crt
+ update-ca-certificates
+}
+
+
+case "$1" in
+ deploy)
+ deploy_ucp
+ deploy_dtr $2
+ setup_dtr_certs $2
+ ;;
+ setup-certs)
+ setup_dtr_certs $2
+ ;;
+ *)
+ echo "Illegal option $1"
+ ;;
+esac
+
diff --git a/dockerfiles/dind/workshop.lic b/dockerfiles/dind/workshop.lic
new file mode 100644
index 0000000000000000000000000000000000000000..05a8892a9afeda079011394f2010b4a4774ff6e6
--- /dev/null
+++ b/dockerfiles/dind/workshop.lic
@@ -0,0 +1 @@
+{"key_id":"B3T_Uirjs-tpcGd4Tql8HL--kDo1iTOUaVUFNMhEXM1Z","private_key":"RbtCEoNZ4OBu-yIHNM1mGCJ6R_4SxF-ThghAd-I3b6_N","authorization":"ewogICAicGF5bG9hZCI6ICJleUpsZUhCcGNtRjBhVzl1SWpvaU1qQXhPUzB3TkMweU5GUXhPRG93TkRvek5Gb2lMQ0owYjJ0bGJpSTZJbU16U1VnMllWSjFWak00WjBWSVIwWXRVV1l0ZGxGM2MwMHdlR05vYnpoWE4xSklPRzFLYVRaT1VUUTlJaXdpYldGNFJXNW5hVzVsY3lJNk1UQXNJbk5qWVc1dWFXNW5SVzVoWW14bFpDSTZkSEoxWlN3aWJHbGpaVzV6WlZSNWNHVWlPaUpQWm1ac2FXNWxJaXdpZEdsbGNpSTZJbEJ5YjJSMVkzUnBiMjRpZlEiLAogICAic2lnbmF0dXJlcyI6IFsKICAgICAgewogICAgICAgICAiaGVhZGVyIjogewogICAgICAgICAgICAiandrIjogewogICAgICAgICAgICAgICAiZSI6ICJBUUFCIiwKICAgICAgICAgICAgICAgImtleUlEIjogIko3TEQ6NjdWUjpMNUhaOlU3QkE6Mk80Rzo0QUwzOk9GMk46SkhHQjpFRlRIOjVDVlE6TUZFTzpBRUlUIiwKICAgICAgICAgICAgICAgImtpZCI6ICJKN0xEOjY3VlI6TDVIWjpVN0JBOjJPNEc6NEFMMzpPRjJOOkpIR0I6RUZUSDo1Q1ZROk1GRU86QUVJVCIsCiAgICAgICAgICAgICAgICJrdHkiOiAiUlNBIiwKICAgICAgICAgICAgICAgIm4iOiAieWRJeS1sVTdvN1BjZVktNC1zLUNRNU9FZ0N5RjhDeEljUUlXdUs4NHBJaVpjaVk2NzMweUNZbndMU0tUbHctVTZVQ19RUmVXUmlvTU5ORTVEczVUWUVYYkdHNm9sbTJxZFdiQndjQ2ctMlVVSF9PY0I5V3VQNmdSUEhwTUZNc3hEeld3dmF5OEpVdUhnWVVMVXBtMUl2LW1xN2xwNW5RX1J4clQwS1pSQVFUWUxFTUVmR3dtM2hNT19nZUxQUy1oZ0tQdElIbGtnNl9XY294VEdvS1A3OWRfd2FIWXhHTmw3V2hTbmVpQlN4YnBiUUFLazIxbGc3OThYYjd2WnlFQVRETXJSUjlNZUU2QWRqNUhKcFkzQ295UkFQQ21hS0dSQ0s0dW9aU29JdTBoRlZsS1VQeWJidzAwMEdPLXdhMktOOFV3Z0lJbTBpNUkxdVc5R2txNHpqQnk1emhncXVVWGJHOWJXUEFPWXJxNVFhODFEeEdjQmxKeUhZQXAtRERQRTlUR2c0elltWGpKbnhacUhFZHVHcWRldlo4WE1JMHVrZmtHSUkxNHdVT2lNSUlJclhsRWNCZl80Nkk4Z1FXRHp4eWNaZV9KR1gtTEF1YXlYcnlyVUZlaFZOVWRaVWw5d1hOYUpCLWthQ3F6NVF3YVI5M3NHdy1RU2Z0RDBOdkxlN0N5T0gtRTZ2ZzZTdF9OZVR2Z3Y4WW5oQ2lYSWxaOEhPZkl3TmU3dEVGX1VjejVPYlB5a20zdHlsck5VanQwVnlBbXR0YWNWSTJpR2loY1VQcm1rNGxWSVo3VkRfTFNXLWk3eW9TdXJ0cHNQWGNlMnBLRElvMzBsSkdoT18zS1VtbDJTVVpDcXpKMXlFbUtweXNINUhEVzljc0lGQ0EzZGVBamZaVXZON1UiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJhbGciOiAiUlMyNTYiCiAgICAgICAgIH0sCiAgICAgICAgICJzaWduYXR1cmUiOiAid2xrQUhLd0l1TUs5Y0N3YUdINVB1MW50dGNkLVk0SkNsRnpLeTZtcmJlTzR3eXFOenpwUi16TG4tMlhsYnJGZTdlczYtSklSREhmNzBGR3JRZl9MZEc3QVQ4bC1HRXVoUk1SaF8xVTlXd1BkOGdsWnNFem44VFUyeGtzU3lkWEw2WER3TUlqMHJUMFdpQm43T29YcEc0ZGJrUGgwLWxfY1VKQnphQzlwbEZ3ZXdmdF9Ocl80a0FpcUlNa3FJZHdQaU5XOVc4NERPUFdpZ2FrcTZTTnRtMVpqT2E1UG1ldHUydk1iUGpnTzFZM19tUVFsUldpakRwRUR1Rzl1dl9yNDFsN1I2LTdKQTB6SGpvdVVqdkxDREdIMUQ3eUxnR1RFMTlXN2FMRHI4ZE5FeXBjdi1vQzVmb3pqM19ISjUyWXVDS0RnazJzb3Y5YVFYOHhNTW5DWGU4Y3JIYjlEVG05eVcyd09FN0kxYVZLYXRKbjZrSGprM1FSWGVNbnRNQnJ6TGlzanBjZnlBYzdGNlc1YnBTSUtXaUQtd2o5QTRlY0FPbFNxc0NBS3lkaWxnR2lqQTNPY1dOZHhvV0NhV1MzaXFvakFBTE1JNHlsOFlpdG50ckVMVFNuUDFFS08wTGFaaTJxVURfU0lBSmFOUlRPTVIzblRqQUNwd1ZwYXAyU3lkOEZwc1pFVllTZFJVLWJVZDJybmN1ZHZfcC1XdFZpYWVsQ3BvTWstdURzWGhud2JyWFB6Y3dkVHVobmg0V2kxMmRTcjRUQ3ZMRktSMklCaklwam1VZWt4MFBTazlKUkNXc2R2bjY0dElCZnV6dVZSRkVkSVBidnBZd2pWOUZZc19VQWJvVE85a2E1OWZmNm1zOThHYXVTbE9sYnkwSWE0TlBxTTRKY2ZvSFUiLAogICAgICAgICAicHJvdGVjdGVkIjogImV5Sm1iM0p0WVhSTVpXNW5kR2dpT2pFM05Dd2labTl5YldGMFZHRnBiQ0k2SW1aUklpd2lkR2x0WlNJNklqSXdNVGd0TURVdE1UWlVNREU2TURNNk1qTmFJbjAiCiAgICAgIH0KICAgXQp9"}
\ No newline at end of file
diff --git a/dockerfiles/k8s/.bashrc b/dockerfiles/k8s/.bashrc
new file mode 100644
index 0000000000000000000000000000000000000000..619998c253a315093c60e06dc8aef86217a55fb9
--- /dev/null
+++ b/dockerfiles/k8s/.bashrc
@@ -0,0 +1,3 @@
+export PS1="[\h \W]$ "
+cat /etc/motd
+echo $BASHPID > /var/run/cwd
diff --git a/dockerfiles/k8s/Dockerfile b/dockerfiles/k8s/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..065a7541b9adcc39e9a517183dbda551df1d4783
--- /dev/null
+++ b/dockerfiles/k8s/Dockerfile
@@ -0,0 +1,44 @@
+FROM centos:7
+
+COPY ./systemctl /usr/bin/systemctl
+COPY ./kubernetes.repo /etc/yum.repos.d/
+
+
+
+RUN yum install -y kubectl-1.27.2 kubeadm-1.27.2 kubelet-1.27.2 \
+ #&& mv -f /etc/systemd/system/kubelet.service.d/10-kubeadm.conf /etc/systemd/system/kubelet.service \
+ && yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo \
+ && yum install -y docker-ce git bash-completion \
+ && sed -i -e '4d;5d;8d' /lib/systemd/system/docker.service \
+ && yum clean all
+
+RUN curl -Lf -o /usr/bin/jq https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64 \
+ && curl -Lf -o /usr/bin/docker-compose https://github.com/docker/compose/releases/download/1.21.0/docker-compose-$(uname -s)-$(uname -m) \
+ && chmod +x /usr/bin/jq /usr/bin/docker-compose
+
+
+VOLUME ["/var/lib/kubelet"]
+
+COPY ./kube* /etc/systemd/system/
+COPY ./wrapkubeadm.sh /usr/local/bin/kubeadm
+COPY ./tokens.csv /etc/pki/tokens.csv
+COPY ./daemon.json /etc/docker/
+COPY ./resolv.conf.override /etc/
+COPY ./docker.service /usr/lib/systemd/system/
+COPY ./.bashrc /root/
+
+COPY motd /etc/motd
+
+
+RUN echo 'source <(kubectl completion bash)' >>~/.bashrc \
+ && kubectl completion bash >> /etc/bash_completion.d/kubectl
+
+RUN mkdir -p /root/.kube && ln -s /etc/kubernetes/admin.conf /root/.kube/config \
+ && rm -f /etc/machine-id
+
+WORKDIR /root
+
+CMD mount --make-shared / \
+ && systemctl start docker \
+ && systemctl start kubelet \
+ && while true; do script -q -c "/bin/bash -l" /dev/null; done
diff --git a/dockerfiles/k8s/daemon.json b/dockerfiles/k8s/daemon.json
new file mode 100644
index 0000000000000000000000000000000000000000..b65a922fc1f9535277fc10ea07a74331addd988a
--- /dev/null
+++ b/dockerfiles/k8s/daemon.json
@@ -0,0 +1,14 @@
+{
+ "experimental": true,
+ "debug": true,
+ "cri-containerd": true,
+ "log-level": "info",
+ "tls": false,
+ "insecure-registries": [
+ "127.0.0.1"
+ ],
+ "hosts": [
+ "unix:///var/run/docker.sock",
+ "tcp://0.0.0.0:2375"
+ ]
+}
diff --git a/dockerfiles/k8s/docker.service b/dockerfiles/k8s/docker.service
new file mode 100644
index 0000000000000000000000000000000000000000..2a68fe43a90ce82eedf995cb618f991bdd6b4aff
--- /dev/null
+++ b/dockerfiles/k8s/docker.service
@@ -0,0 +1,30 @@
+[Unit]
+Description=Docker Application Container Engine
+Documentation=https://docs.docker.com
+
+[Service]
+# the default is not to use systemd for cgroups because the delegate issues still
+# exists and systemd currently does not support the cgroup feature set required
+# for containers run by docker
+ExecStart=/usr/bin/dockerd
+ExecReload=/bin/kill -s HUP $MAINPID
+# Having non-zero Limit*s causes performance problems due to accounting overhead
+# in the kernel. We recommend using cgroups to do container-local accounting.
+LimitNOFILE=infinity
+LimitNPROC=infinity
+LimitCORE=infinity
+# Uncomment TasksMax if your systemd version supports it.
+# Only systemd 226 and above support this version.
+#TasksMax=infinity
+TimeoutStartSec=0
+# set delegate yes so that systemd does not reset the cgroups of docker containers
+Delegate=yes
+# kill only the docker process, not all processes in the cgroup
+KillMode=process
+# restart the docker process if it exits prematurely
+Restart=on-failure
+StartLimitBurst=3
+StartLimitInterval=60s
+
+[Install]
+WantedBy=multi-user.target
diff --git a/dockerfiles/k8s/kubelet.env b/dockerfiles/k8s/kubelet.env
new file mode 100644
index 0000000000000000000000000000000000000000..093a47ee79c7aecbafd8bba291920974c82d1820
--- /dev/null
+++ b/dockerfiles/k8s/kubelet.env
@@ -0,0 +1,6 @@
+KUBELET_KUBECONFIG_ARGS="--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf"
+KUBELET_SYSTEM_PODS_ARGS="--pod-manifest-path=/etc/kubernetes/manifests"
+KUBELET_DNS_ARGS="--cluster-dns=10.96.0.10 --cluster-domain=cluster.local"
+KUBELET_AUTHZ_ARGS="--authorization-mode=Webhook --client-ca-file=/etc/kubernetes/pki/ca.crt"
+KUBELET_CGROUP_ARGS="--cgroup-driver=cgroupfs"
+KUBELET_EXTRA_ARGS="--fail-swap-on=false --resolv-conf=/etc/resolv.conf.override --container-runtime-endpoint /run/docker/containerd/containerd.sock "
diff --git a/dockerfiles/k8s/kubelet.service b/dockerfiles/k8s/kubelet.service
new file mode 100644
index 0000000000000000000000000000000000000000..c5e29428b60bdd6df3aaa0d30d8d590992d524e2
--- /dev/null
+++ b/dockerfiles/k8s/kubelet.service
@@ -0,0 +1,4 @@
+[Service]
+Restart=always
+EnvironmentFile=/etc/systemd/system/kubelet.env
+ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_SYSTEM_PODS_ARGS $KUBELET_NETWORK_ARGS $KUBELET_DNS_ARGS $KUBELET_AUTHZ_ARGS $KUBELET_CGROUP_ARGS $KUBELET_EXTRA_ARGS
diff --git a/dockerfiles/k8s/kubernetes.repo b/dockerfiles/k8s/kubernetes.repo
new file mode 100644
index 0000000000000000000000000000000000000000..89f4873d0f54183ba33d3ecf51568a80d21167c4
--- /dev/null
+++ b/dockerfiles/k8s/kubernetes.repo
@@ -0,0 +1,7 @@
+[kubernetes]
+name=Kubernetes
+baseurl=https://pkgs.k8s.io/core:/stable:/v1.28/rpm/
+enabled=1
+gpgcheck=1
+gpgkey=https://pkgs.k8s.io/core:/stable:/v1.28/rpm/repodata/repomd.xml.key
+exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
diff --git a/dockerfiles/k8s/motd b/dockerfiles/k8s/motd
new file mode 100644
index 0000000000000000000000000000000000000000..348b9c7e1312a69705f2bae04d4c2ea44bcda0de
--- /dev/null
+++ b/dockerfiles/k8s/motd
@@ -0,0 +1,28 @@
+
+ WARNING!!!!
+
+ This is a sandbox environment. Using personal credentials
+ is HIGHLY! discouraged. Any consequences of doing so, are
+ completely the user's responsibilites.
+
+ You can bootstrap a cluster as follows:
+
+ 1. Initializes cluster master node:
+
+ kubeadm init --apiserver-advertise-address $(hostname -i) --pod-network-cidr 10.5.0.0/16
+
+
+ 2. Initialize cluster networking:
+
+ kubectl apply -f https://raw.githubusercontent.com/cloudnativelabs/kube-router/master/daemonset/kubeadm-kuberouter.yaml
+
+
+ 3. (Optional) Create an nginx deployment:
+
+ kubectl apply -f https://raw.githubusercontent.com/kubernetes/website/master/content/en/examples/application/nginx-app.yaml
+
+
+ The PWK team.
+
+
+
diff --git a/dockerfiles/k8s/resolv.conf.override b/dockerfiles/k8s/resolv.conf.override
new file mode 100644
index 0000000000000000000000000000000000000000..cae093a833dbfe99c7ade2c81fc9500f482e649f
--- /dev/null
+++ b/dockerfiles/k8s/resolv.conf.override
@@ -0,0 +1 @@
+nameserver 8.8.8.8
diff --git a/dockerfiles/k8s/systemctl b/dockerfiles/k8s/systemctl
new file mode 100644
index 0000000000000000000000000000000000000000..c1c8b188fb8eaa8639e58ae8b46fa2f114162da2
--- /dev/null
+++ b/dockerfiles/k8s/systemctl
@@ -0,0 +1,281 @@
+#!/bin/bash
+
+function get_unit_file(){
+
+ local UNIT=$1
+
+ for DIR in ${UNIT_PATHS[@]} ; do
+ if [ -f "${DIR}${UNIT}" ] ; then
+ echo "${DIR}${UNIT}"
+ break
+ fi
+ done
+
+}
+
+function read_option(){
+ local OPTION="$1"
+ local UNIT_FILE="$2"
+ local UNIT_INSTANCE="$3"
+
+ local UNIT=`basename $UNIT_FILE`
+ local UNIT_FULL=`echo $UNIT | sed "s/@/@$UNIT_INSTANCE/"`
+
+ VALUE="$(grep '^'$OPTION'[= ]' "$UNIT_FILE" | cut -d '=' -f2- | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
+
+ VALUE="`
+ echo $VALUE |
+ sed -e "s/%[i]/$UNIT_INSTANCE/g" \
+ -e "s/%[I]/\"$UNIT_INSTANCE\"/g" \
+ -e "s/%[n]/$UNIT_FULL/g" \
+ -e "s/%[N]/\"$UNIT_FULL\"/g"
+ `"
+ # TODO: Add more options from:
+ # https://www.freedesktop.org/software/systemd/man/systemd.unit.html#Specifiers
+
+ echo $VALUE
+}
+
+function get_unit_wants() {
+
+ local UNIT_FILE=$1
+ local UNIT=`basename $UNIT_FILE`
+
+ sort -u <<< `(
+ # Print wants from UNIT_PATHS
+ for DIR in ${UNIT_PATHS[@]} ; do
+ if [ -d "${DIR}${UNIT}.wants" ] ; then
+ ls -1 "${DIR}${UNIT}.wants/" | tr '\n' ' '
+ fi
+ done
+
+ # Print wants from unit-file
+ read_option Wants $UNIT_FILE
+ )`
+}
+
+function action_start(){
+
+ # Find depended services
+ local UNIT_FILE=$1
+ local UNIT_WANTS=(`get_unit_wants $1`)
+ local UNIT_INSTANCE=$2
+
+ # Start depended services
+ for UNIT in ${UNIT_WANTS[@]}; do
+ exec_action start $UNIT
+ done
+
+ # Load options
+ local User=`read_option User $UNIT_FILE $UNIT_INSTANCE`
+ local Type=`read_option Type $UNIT_FILE $UNIT_INSTANCE`
+ local EnvironmentFile=`read_option EnvironmentFile $UNIT_FILE $UNIT_INSTANCE`
+ local ExecStartPre=(`read_option ExecStartPre $UNIT_FILE $UNIT_INSTANCE`)
+ local ExecStart=`read_option ExecStart $UNIT_FILE $UNIT_INSTANCE`
+ local ExecStartPost=(`read_option ExecStartPost $UNIT_FILE $UNIT_INSTANCE`)
+ local Restart=(`read_option Restart $UNIT_FILE $UNIT_INSTANCE`)
+ local RestartSec=(`read_option RestartSec $UNIT_FILE $UNIT_INSTANCE`)
+ RestartSec=${RestartSec:=5}
+
+ [ -f "$EnvironmentFile" ] && source "$EnvironmentFile"
+
+ # Start service
+ if [ -z $Type ] || [[ "${Type,,}" == *"simple"* ]] ; then
+ if [ "$Restart" == "always" ]; then
+ COMMAND='nohup bash -c "while true ; do '"$ExecStart"'; sleep $RestartSec; done" &>/dev/null &'
+ else
+ COMMAND='nohup '"$ExecStart"' >>/dev/null 2>&1 &'
+ fi
+ elif [[ "${Type,,}" == *"forking"* ]] || [[ "${Type,,}" == *"oneshot"* ]] ; then
+ COMMAND="$ExecStart"
+ else
+ >&2 echo "Unknown service type $Type"
+ fi
+
+ #[ -z $User ] || COMMAND="su $User -c \"$COMMAND\""
+
+ while IFS=$'\n' read -a i; do
+ eval $i
+ done <<< "${ExecStartPre[@]}"
+
+ eval "$COMMAND"
+
+ while IFS=$'\n' read -a i; do
+ eval $i
+ done <<< "${ExecStartPost[@]}"
+}
+
+function action_stop(){
+
+ # Find depended services
+ local UNIT_FILE=$1
+ local UNIT_WANTS=(`get_unit_wants $1`)
+ local UNIT_INSTANCE=$2
+
+ # Load options
+ local User=`read_option User $UNIT_FILE $UNIT_INSTANCE`
+ local Type=`read_option Type $UNIT_FILE $UNIT_INSTANCE`
+ local EnvironmentFile=`read_option EnvironmentFile $UNIT_FILE $UNIT_INSTANCE`
+ local ExecStopPre=(`read_option ExecStartPre $UNIT_FILE $UNIT_INSTANCE`)
+ local ExecStop=`read_option ExecStop $UNIT_FILE $UNIT_INSTANCE`
+ local ExecStopPost=(`read_option ExecStartPost $UNIT_FILE $UNIT_INSTANCE`)
+ local ExecStart=`read_option ExecStart $UNIT_FILE $UNIT_INSTANCE`
+
+ [ -f "$EnvironmentFile" ] && source "$EnvironmentFile"
+
+ # Stop service
+ if [ -z $ExecStop ] ; then
+ COMMAND="kill -TERM \$(pgrep -f \"$ExecStart\")"
+ else
+ COMMAND="$ExecStop"
+ fi
+
+ #[ -z $User ] || COMMAND="su $User -c \"$COMMAND\""
+
+ while IFS=$'\n' read -a i; do
+ eval $i
+ done <<< "${ExecStopPre[@]}"
+
+ eval "$COMMAND"
+
+ while IFS=$'\n' read -a i; do
+ eval $i
+ done <<< "${ExecStopPost[@]}"
+}
+
+function action_restart(){
+ local UNIT_FILE=$1
+ local UNIT_INSTANCE=$2
+
+ action_stop $UNIT_FILE $UNIT_INSTANCE
+ action_start $UNIT_FILE $UNIT_INSTANCE
+}
+
+
+function action_enable(){
+
+ local UNIT_FILE=$1
+ local UNIT=`basename $UNIT_FILE`
+ local UNIT_INSTANCE=$2
+ local UNIT_FULL=`echo $UNIT | sed "s/@/@$UNIT_INSTANCE/"`
+
+ local WantedBy=`read_option WantedBy $UNIT_FILE`
+
+ if [ -z $WantedBy ] ; then
+ >&2 echo "Unit $UNIT have no WantedBy option."
+ exit 1
+ fi
+
+ local WANTEDBY_DIR="/etc/systemd/system/$WantedBy.wants"
+
+ if [ ! -f "$WANTEDBY_DIR/$UNIT_FULL" ] ; then
+ mkdir -p $WANTEDBY_DIR
+ echo Created symlink from $WANTEDBY_DIR/$UNIT_FULL to $UNIT_FILE.
+ ln -s $WANTEDBY_DIR/$UNIT_FULL $UNIT_FILE
+ fi
+
+}
+
+function action_disable(){
+
+ local UNIT_FILE=$1
+ local UNIT=`basename $UNIT_FILE`
+ local UNIT_INSTANCE=$2
+ local UNIT_FULL=`echo $UNIT | sed "s/@/@$UNIT_INSTANCE/"`
+
+ local WantedBy=`read_option WantedBy $UNIT_FILE`
+
+ if [ -z $WantedBy ] ; then
+ >&2 echo "Unit $UNIT have no WantedBy option."
+ exit 1
+ fi
+
+ local WANTEDBY_DIR="/etc/systemd/system/$WantedBy.wants"
+
+ if [ -f "$WANTEDBY_DIR/$UNIT_FULL" ] ; then
+ echo Removed $WANTEDBY_DIR/$UNIT_FULL.
+ rm -f $WANTEDBY_DIR/$UNIT_FULL.
+ rmdir --ignore-fail-on-non-empty $WANTEDBY_DIR
+ fi
+
+}
+
+function action_status(){
+
+ # Find depended services
+ local UNIT_FILE=$1
+ local UNIT_WANTS=(`get_unit_wants $1`)
+ local UNIT_INSTANCE=$2
+
+ local ExecStart=`read_option ExecStart $UNIT_FILE $UNIT_INSTANCE`
+
+
+ COMMAND="pgrep -f \"$ExecStart\" &>/dev/null"
+
+
+ if eval "$COMMAND"; then
+ exit 0
+ fi
+
+ >&2 echo "Loaded: not-found"
+ exit 1
+}
+
+function action_is_enabled(){
+ exit 0
+}
+
+function action_is_active(){
+ local UNIT=`basename $1`
+ if systemctl status $UNIT ; then
+ >&2 echo "active"
+ exit 0
+ fi
+
+ exit 1
+
+}
+
+function exec_action(){
+
+ local ACTION=$1
+ local UNIT=$2
+
+ [[ $UNIT =~ '.' ]] || UNIT="$UNIT.service"
+
+ if [[ $UNIT =~ '@' ]] ; then
+ local UNIT_INSTANCE=`echo $UNIT | cut -d'@' -f2- | cut -d. -f1`
+ local UNIT=`echo $UNIT | sed "s/$UNIT_INSTANCE//"`
+ fi
+
+ UNIT_FILE=`get_unit_file $UNIT`
+
+ if [ -z $UNIT_FILE ] ; then
+ >&2 echo "Failed to $ACTION $UNIT: Unit $UNIT not found."
+ exit 1
+ else
+ case "$ACTION" in
+ start ) action_start $UNIT_FILE $UNIT_INSTANCE ;;
+ stop ) action_stop $UNIT_FILE $UNIT_INSTANCE ;;
+ restart ) action_restart $UNIT_FILE $UNIT_INSTANCE ;;
+ enable ) action_enable $UNIT_FILE $UNIT_INSTANCE ;;
+ disable ) action_disable $UNIT_FILE $UNIT_INSTANCE ;;
+ status ) action_status $UNIT_FILE $UNIT_INSTANCE ;;
+ is-enabled ) action_is_enabled $UNIT_FILE $UNIT_INSTANCE ;;
+ is-active ) action_is_active $UNIT_FILE $UNIT_INSTANCE ;;
+ * ) >&2 echo "Unknown operation $ACTION." ; exit 1 ;;
+ esac
+ fi
+}
+
+ACTION="$1"
+UNITS="${@:2}"
+UNIT_PATHS=(
+ /etc/systemd/system/
+ /usr/lib/systemd/system/
+)
+
+
+for UNIT in ${UNITS[@]}; do
+ exec_action $ACTION $UNIT
+done
diff --git a/dockerfiles/k8s/tokens.csv b/dockerfiles/k8s/tokens.csv
new file mode 100644
index 0000000000000000000000000000000000000000..e7a4f92751462413d4a231aab4589ff555d2fd06
--- /dev/null
+++ b/dockerfiles/k8s/tokens.csv
@@ -0,0 +1 @@
+31ada4fd-adec-460c-809a-9e56ceb75269,pwd,pwd,"system:admin,system:masters"
diff --git a/dockerfiles/k8s/wrapkubeadm.sh b/dockerfiles/k8s/wrapkubeadm.sh
new file mode 100644
index 0000000000000000000000000000000000000000..2a78e4491fb633165812bc1c2339ce238979a66f
--- /dev/null
+++ b/dockerfiles/k8s/wrapkubeadm.sh
@@ -0,0 +1,153 @@
+#!/bin/bash
+# Copyright 2017 Mirantis
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -o pipefail
+set -o errtrace
+
+apiserver_static_pod="/etc/kubernetes/manifests/kube-apiserver"
+
+# jq filters follow.
+
+# TODO: think about more secure possibilities
+apiserver_anonymous_auth='.spec.containers[0].command|=map(select(startswith("--token-auth-file")|not))+["--token-auth-file=/etc/pki/tokens.csv"]'
+
+# Sets etcd2 as backend
+apiserver_etcd2_backend='.spec.containers[0].command|=map(select(startswith("--storage-backend")|not))+["--storage-backend=etcd2"]'
+
+# Make apiserver accept insecure connections on port 8080
+# TODO: don't use insecure port
+#apiserver_insecure_bind_port='.spec.containers[0].command|=map(select(startswith("--insecure-port=")|not))+["--insecure-port=2375"]'
+
+
+# Update kube-proxy CIDR, enable --masquerade-all and disable conntrack (see dind::frob-proxy below)
+function dind::proxy-cidr-and-no-conntrack {
+ cluster_cidr="$(ip addr show docker0 | grep -w inet | awk '{ print $2; }')"
+ echo ".items[0].spec.template.spec.containers[0].command |= .+ [\"--cluster-cidr=${cluster_cidr}\", \"--masquerade-all\", \"--conntrack-max-per-core=0\"]"
+}
+
+
+# Adds route to defualt eth0 interface so 10.96.x.x can go through
+function dind::add-route {
+ ip route add 10.96.0.0/16 dev eth0
+}
+
+
+
+function dind::join-filters {
+ local IFS="|"
+ echo "$*"
+}
+
+function dind::frob-apiserver {
+ local -a filters=("${apiserver_anonymous_auth}")
+
+ dind::frob-file "${apiserver_static_pod}" "${filters[@]}"
+}
+
+function dind::frob-file {
+ local path_base="$1"
+ shift
+ local filter="$(dind::join-filters "$@")"
+ local status=0
+ if [[ -f ${path_base}.yaml ]]; then
+ dind::yq "${filter}" "${path_base}.yaml" || status=$?
+ else
+ echo "${path_base}.json or ${path_base}.yaml not found" >&2
+ return 1
+ fi
+ if [[ ${status} -ne 0 ]]; then
+ echo "Failed to frob ${path_base}.yaml" >&2
+ return 1
+ fi
+}
+
+function dind::yq {
+ local filter="$1"
+ local path="$2"
+ # We need to use a temp file here because if you feed an object to
+ # 'kubectl convert' via stdin, you'll get a List object because
+ # multiple input objects are implied
+ tmp="$(mktemp tmp-XXXXXXXXXX.json)"
+ kubectl convert -f "${path}" --local -o json 2>/dev/null |
+ jq "${filter}" > "${tmp}"
+ kubectl convert -f "${tmp}" --local -o yaml 2>/dev/null >"${path}"
+ rm -f "${tmp}"
+}
+
+function dind::frob-proxy {
+ # Trying to change conntrack settings fails even in priveleged containers,
+ # so we need to avoid it. Here's sample error message from kube-proxy:
+ # I1010 21:53:00.525940 1 conntrack.go:57] Setting conntrack hashsize to 49152
+ # Error: write /sys/module/nf_conntrack/parameters/hashsize: operation not supported
+ # write /sys/module/nf_conntrack/parameters/hashsize: operation not supported
+ #
+ # Recipe by @errordeveloper:
+ # https://github.com/kubernetes/kubernetes/pull/34522#issuecomment-253248985
+ local force_apply=--force
+ if ! kubectl version --short >&/dev/null; then
+ # kubectl 1.4 doesn't have version --short and also it doesn't support apply --force
+ force_apply=
+ fi
+ KUBECONFIG=/etc/kubernetes/admin.conf kubectl -n kube-system get ds -l k8s-app=kube-proxy -o json |
+ jq "$(dind::join-filters "$(dind::proxy-cidr-and-no-conntrack)")" | KUBECONFIG=/etc/kubernetes/admin.conf kubectl apply ${force_apply} -f -
+
+ KUBECONFIG=/etc/kubernetes/admin.conf kubectl -n kube-system delete pods --now -l "k8s-app=kube-proxy"
+}
+
+
+function dind::wait-for-apiserver {
+ echo -n "Waiting for api server to startup"
+ local url="https://localhost:6443/api"
+ local n=60
+ while true; do
+ if curl -k -s "${url}" >&/dev/null; then
+ break
+ fi
+ if ((--n == 0)); then
+ echo "Error: timed out waiting for apiserver to become available" >&2
+ fi
+ echo -n "."
+ sleep 0.5
+ done
+ echo ""
+}
+
+function dind::frob-cluster {
+ #dind::frob-apiserver
+ dind::wait-for-apiserver
+ dind::frob-proxy
+}
+
+# Weave depends on /etc/machine-id being unique
+if [[ ! -f /etc/machine-id ]]; then
+ rm -f /etc/machine-id
+ systemd-machine-id-setup
+fi
+
+if [[ "$@" == "init"* || "$@" == "join"* ]]; then
+# Call kubeadm with params and skip flag
+ /usr/bin/kubeadm "$@" --ignore-preflight-errors all --cri-socket /run/docker/containerd/containerd.sock
+else
+# Call kubeadm with params
+ /usr/bin/kubeadm "$@"
+fi
+
+# Frob cluster
+if [[ "$@" == "init"* && $? -eq 0 && ! "$@" == *"--help"* ]]; then
+ dind::frob-cluster
+else
+ dind::add-route
+fi
+
diff --git a/dockerfiles/pwm/.gitconfig b/dockerfiles/pwm/.gitconfig
new file mode 100644
index 0000000000000000000000000000000000000000..87b8f9be00f890c66e82b81fe6617ed7732ec11a
--- /dev/null
+++ b/dockerfiles/pwm/.gitconfig
@@ -0,0 +1,2 @@
+[url "https://"]
+ insteadOf = git://
diff --git a/dockerfiles/pwm/.inputrc b/dockerfiles/pwm/.inputrc
new file mode 100644
index 0000000000000000000000000000000000000000..6a5b035b1a092730381e34e8b8219c3407465b4f
--- /dev/null
+++ b/dockerfiles/pwm/.inputrc
@@ -0,0 +1,73 @@
+# /etc/inputrc - global inputrc for libreadline
+# See readline(3readline) and `info rluserman' for more information.
+
+# Be 8 bit clean.
+set input-meta on
+set output-meta on
+
+# To allow the use of 8bit-characters like the german umlauts, uncomment
+# the line below. However this makes the meta key not work as a meta key,
+# which is annoying to those which don't need to type in 8-bit characters.
+
+# set convert-meta off
+
+# try to enable the application keypad when it is called. Some systems
+# need this to enable the arrow keys.
+# set enable-keypad on
+
+# see /usr/share/doc/bash/inputrc.arrows for other codes of arrow keys
+
+# do not bell on tab-completion
+# set bell-style none
+# set bell-style visible
+
+# some defaults / modifications for the emacs mode
+$if mode=emacs
+
+# allow the use of the Home/End keys
+"\e[1~": beginning-of-line
+"\e[4~": end-of-line
+
+# allow the use of the Delete/Insert keys
+"\e[3~": delete-char
+"\e[2~": quoted-insert
+
+# mappings for "page up" and "page down" to step to the beginning/end
+# of the history
+# "\e[5~": beginning-of-history
+# "\e[6~": end-of-history
+
+# alternate mappings for "page up" and "page down" to search the history
+# "\e[5~": history-search-backward
+# "\e[6~": history-search-forward
+
+# mappings for Ctrl-left-arrow and Ctrl-right-arrow for word moving
+"\e[1;5C": forward-word
+"\e[1;5D": backward-word
+"\e[5C": forward-word
+"\e[5D": backward-word
+"\e\e[C": forward-word
+"\e\e[D": backward-word
+
+$if term=rxvt
+"\e[7~": beginning-of-line
+"\e[8~": end-of-line
+"\eOc": forward-word
+"\eOd": backward-word
+$endif
+
+# for non RH/Debian xterm, can't hurt for RH/Debian xterm
+# "\eOH": beginning-of-line
+# "\eOF": end-of-line
+
+# for freebsd console
+# "\e[H": beginning-of-line
+# "\e[F": end-of-line
+
+$endif
+
+# faster completion
+set show-all-if-ambiguous on
+
+"\e[A": history-search-backward
+"\e[B": history-search-forward
diff --git a/dockerfiles/pwm/.profile b/dockerfiles/pwm/.profile
new file mode 100644
index 0000000000000000000000000000000000000000..3269f8b7da88876ca71c00d3f1c1ffc06830894c
--- /dev/null
+++ b/dockerfiles/pwm/.profile
@@ -0,0 +1,5 @@
+export PS1='\e[1m\e[31m[\h] \e[32m\e[34m\u@$(hostname -i)\e[35m \w\e[0m\n$ '
+alias vi='vim'
+export PATH=$PATH:/root/go/bin
+cat /etc/motd
+echo $BASHPID > /var/run/cwd
diff --git a/dockerfiles/pwm/.vimrc b/dockerfiles/pwm/.vimrc
new file mode 100644
index 0000000000000000000000000000000000000000..591902057a78b5b8abf12074d468b96213e86d20
--- /dev/null
+++ b/dockerfiles/pwm/.vimrc
@@ -0,0 +1,6 @@
+syntax on
+set autoindent
+set expandtab
+set number
+set shiftwidth=2
+set softtabstop=2
diff --git a/dockerfiles/pwm/Dockerfile b/dockerfiles/pwm/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..6e3450483b2600ba310ea7b06efeb8ae968dbc24
--- /dev/null
+++ b/dockerfiles/pwm/Dockerfile
@@ -0,0 +1,44 @@
+ARG VERSION=docker:stable-dind
+FROM ${VERSION}
+
+RUN apk add --no-cache git tmux vim curl bash build-base qemu-img qemu-system-x86_64
+
+ENV GOPATH /root/go
+ENV PATH $PATH:$GOPATH
+
+# Use specific moby commit due to vendoring mismatch
+ENV MOBY_COMMIT="d9d2a91780b34b92e669bbfa099f613bd9fad6bb"
+
+RUN mkdir /root/go && apk add --no-cache go \
+ && go get -u -d github.com/moby/tool/cmd/moby && (cd $GOPATH/src/github.com/moby/tool/cmd/moby && git checkout $MOBY_COMMIT && go install) \
+ && go get -u github.com/linuxkit/linuxkit/src/cmd/linuxkit \
+ && rm -rf /root/go/pkg && rm -rf /root/go/src && rm -rf /usr/lib/go
+
+
+# Add bash completion and set bash as default shell
+RUN mkdir /etc/bash_completion.d \
+ && curl https://raw.githubusercontent.com/docker/cli/master/contrib/completion/bash/docker -o /etc/bash_completion.d/docker \
+ && sed -i "s/ash/bash/" /etc/passwd
+
+
+# Replace modprobe with a no-op to get rid of spurious warnings
+# (note: we can't just symlink to /bin/true because it might be busybox)
+RUN rm /sbin/modprobe && echo '#!/bin/true' >/sbin/modprobe && chmod +x /sbin/modprobe
+
+# Install a nice vimrc file and prompt (by soulshake)
+COPY ["sudo", "/usr/local/bin/"]
+COPY [".vimrc", ".profile", ".inputrc", ".gitconfig", "./root/"]
+COPY ["motd", "/etc/motd"]
+COPY ["daemon.json", "/etc/docker/"]
+
+# Move to our home
+WORKDIR /root
+
+
+# Remove IPv6 alias for localhost and start docker in the background ...
+CMD cat /etc/hosts >/etc/hosts.bak && \
+ sed 's/^::1.*//' /etc/hosts.bak > /etc/hosts && \
+ mount -t securityfs none /sys/kernel/security && \
+ dockerd &>/docker.log & \
+ while true ; do /bin/bash -l; done
+# ... and then put a shell in the foreground, restarting it if it exits
diff --git a/dockerfiles/pwm/daemon.json b/dockerfiles/pwm/daemon.json
new file mode 100644
index 0000000000000000000000000000000000000000..2792f9397c5ec85dc63f08721dd25097149e8517
--- /dev/null
+++ b/dockerfiles/pwm/daemon.json
@@ -0,0 +1,7 @@
+{
+ "experimental": true,
+ "debug": true,
+ "log-level": "info",
+ "insecure-registries": ["127.0.0.1"],
+ "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"]
+}
diff --git a/dockerfiles/pwm/motd b/dockerfiles/pwm/motd
new file mode 100644
index 0000000000000000000000000000000000000000..2c1701b41730b498ca8a9ed2063ee904baeef1b2
--- /dev/null
+++ b/dockerfiles/pwm/motd
@@ -0,0 +1,8 @@
+###############################################################
+# WARNING!!!! #
+# This is a sandbox environment. Using personal credentials #
+# is HIGHLY! discouraged. Any consequences of doing so are #
+# completely the user's responsibilites. #
+# #
+# The PWD team. #
+###############################################################
diff --git a/dockerfiles/pwm/sudo b/dockerfiles/pwm/sudo
new file mode 100644
index 0000000000000000000000000000000000000000..637614a061fbd623a499d1234f54741a4a7b323a
--- /dev/null
+++ b/dockerfiles/pwm/sudo
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+# This is shim to help with the case were pasted commands from a readme assume you are not root. Since this isto be run by root, it should effectively be a dummy command that allows the parameters to pass through.
+
+exec "$@"
diff --git a/event/event.go b/event/event.go
new file mode 100644
index 0000000000000000000000000000000000000000..6c56b42cd40487e9c60ac9da217e4590b60f4227
--- /dev/null
+++ b/event/event.go
@@ -0,0 +1,28 @@
+package event
+
+type EventType string
+
+func (e EventType) String() string {
+ return string(e)
+}
+
+var (
+ INSTANCE_VIEWPORT_RESIZE = EventType("instance viewport resize")
+ INSTANCE_DELETE = EventType("instance delete")
+ INSTANCE_NEW = EventType("instance new")
+ INSTANCE_STATS = EventType("instance stats")
+ SESSION_NEW = EventType("session new")
+ SESSION_END = EventType("session end")
+ SESSION_READY = EventType("session ready")
+ SESSION_BUILDER_OUT = EventType("session builder out")
+ PLAYGROUND_NEW = EventType("playground_new")
+)
+
+type Handler func(id string, args ...interface{})
+type AnyHandler func(eventType EventType, id string, args ...interface{})
+
+type EventApi interface {
+ Emit(name EventType, id string, args ...interface{})
+ On(name EventType, handler Handler)
+ OnAny(handler AnyHandler)
+}
diff --git a/event/local_broker.go b/event/local_broker.go
new file mode 100644
index 0000000000000000000000000000000000000000..2f33292278bc4f02783458909f6bd423cd984b17
--- /dev/null
+++ b/event/local_broker.go
@@ -0,0 +1,47 @@
+package event
+
+import "sync"
+
+type localBroker struct {
+ sync.Mutex
+
+ handlers map[EventType][]Handler
+ anyHandlers []AnyHandler
+}
+
+func NewLocalBroker() *localBroker {
+ return &localBroker{handlers: map[EventType][]Handler{}, anyHandlers: []AnyHandler{}}
+}
+
+func (b *localBroker) On(name EventType, handler Handler) {
+ b.Lock()
+ defer b.Unlock()
+
+ if b.handlers[name] == nil {
+ b.handlers[name] = []Handler{}
+ }
+ b.handlers[name] = append(b.handlers[name], handler)
+}
+
+func (b *localBroker) OnAny(handler AnyHandler) {
+ b.Lock()
+ defer b.Unlock()
+
+ b.anyHandlers = append(b.anyHandlers, handler)
+}
+
+func (b *localBroker) Emit(name EventType, sessionId string, args ...interface{}) {
+ go func() {
+ b.Lock()
+ defer b.Unlock()
+
+ for _, handler := range b.anyHandlers {
+ handler(name, sessionId, args...)
+ }
+ if b.handlers[name] != nil {
+ for _, handler := range b.handlers[name] {
+ handler(sessionId, args...)
+ }
+ }
+ }()
+}
diff --git a/event/local_broker_test.go b/event/local_broker_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..90939c8f4b568b160c8972e7118bb7d2f4e383c1
--- /dev/null
+++ b/event/local_broker_test.go
@@ -0,0 +1,60 @@
+package event
+
+import (
+ "sync"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestLocalBroker_On(t *testing.T) {
+ broker := NewLocalBroker()
+
+ called := 0
+ receivedSessionId := ""
+ receivedArgs := []interface{}{}
+
+ wg := sync.WaitGroup{}
+ wg.Add(1)
+
+ broker.On(INSTANCE_NEW, func(sessionId string, args ...interface{}) {
+ called++
+ receivedSessionId = sessionId
+ receivedArgs = args
+ wg.Done()
+ })
+ broker.Emit(SESSION_READY, "1")
+ broker.Emit(INSTANCE_NEW, "2", "foo", "bar")
+
+ wg.Wait()
+
+ assert.Equal(t, 1, called)
+ assert.Equal(t, "2", receivedSessionId)
+ assert.Equal(t, []interface{}{"foo", "bar"}, receivedArgs)
+}
+
+func TestLocalBroker_OnAny(t *testing.T) {
+ broker := NewLocalBroker()
+
+ var receivedEvent EventType
+ receivedSessionId := ""
+ receivedArgs := []interface{}{}
+
+ wg := sync.WaitGroup{}
+ wg.Add(1)
+
+ broker.OnAny(func(eventType EventType, sessionId string, args ...interface{}) {
+ receivedSessionId = sessionId
+ receivedArgs = args
+ receivedEvent = eventType
+ wg.Done()
+ })
+ broker.Emit(SESSION_READY, "1")
+
+ wg.Wait()
+
+ var expectedArgs []interface{}
+ assert.Equal(t, SESSION_READY, receivedEvent)
+ assert.Equal(t, "1", receivedSessionId)
+ assert.Equal(t, expectedArgs, receivedArgs)
+}
diff --git a/event/mock.go b/event/mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..136fb5d0992876536b65fb21f2bc71610c029e11
--- /dev/null
+++ b/event/mock.go
@@ -0,0 +1,19 @@
+package event
+
+import "github.com/stretchr/testify/mock"
+
+type Mock struct {
+ M mock.Mock
+}
+
+func (m *Mock) Emit(name EventType, sessionId string, args ...interface{}) {
+ m.M.Called(name, sessionId, args)
+}
+
+func (m *Mock) On(name EventType, handler Handler) {
+ m.M.Called(name, handler)
+}
+
+func (m *Mock) OnAny(handler AnyHandler) {
+ m.M.Called(handler)
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000000000000000000000000000000000000..d57cbaf9489d6d3b5087257c26a1bad6ccd892ee
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,68 @@
+module github.com/play-with-docker/play-with-docker
+
+go 1.16
+
+require (
+ cloud.google.com/go v0.58.0 // indirect
+ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
+ github.com/Microsoft/go-winio v0.4.5 // indirect
+ github.com/PuerkitoBio/purell v1.1.0 // indirect
+ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
+ github.com/aws/aws-sdk-go v1.12.15
+ github.com/containerd/containerd v1.0.0-beta.2
+ github.com/docker/distribution v2.6.0-rc.1.0.20170726174610-edc3ab29cdff+incompatible // indirect
+ github.com/docker/docker v1.4.2-0.20200309214505-aa6a9891b09c
+ github.com/docker/go-connections v0.3.0
+ github.com/docker/go-units v0.3.2
+ github.com/emicklei/go-restful v2.4.0+incompatible // indirect
+ github.com/emicklei/go-restful-swagger12 v0.0.0-20170208215640-dcef7f557305 // indirect
+ github.com/ghodss/yaml v1.0.0 // indirect
+ github.com/go-ini/ini v1.30.0 // indirect
+ github.com/go-openapi/jsonpointer v0.0.0-20170102174223-779f45308c19 // indirect
+ github.com/go-openapi/jsonreference v0.0.0-20161105162150-36d33bfe519e // indirect
+ github.com/go-openapi/spec v0.0.0-20171105074921-a4fa9574c7aa // indirect
+ github.com/go-openapi/swag v0.0.0-20171111214437-cf0bdb963811 // indirect
+ github.com/google/go-github v13.0.1-0.20171014143926-a021c14a5f19+incompatible
+ github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 // indirect
+ github.com/googleapis/gnostic v0.1.0 // indirect
+ github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f // indirect
+ github.com/gorilla/handlers v1.3.0
+ github.com/gorilla/mux v1.5.0
+ github.com/gorilla/securecookie v0.0.0-20160422134519-667fe4e3466a
+ github.com/gorilla/websocket v1.2.0
+ github.com/gregjones/httpcache v0.0.0-20171119193500-2bcd89a1743f // indirect
+ github.com/hashicorp/golang-lru v0.5.1
+ github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b
+ github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 // indirect
+ github.com/juju/ratelimit v1.0.1 // indirect
+ github.com/mailru/easyjson v0.0.0-20171120080333-32fa128f234d // indirect
+ github.com/miekg/dns v0.0.0-20171019064225-822ae18e7187
+ github.com/morikuni/aec v1.0.0 // indirect
+ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
+ github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
+ github.com/opencontainers/image-spec v1.0.0 // indirect
+ github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
+ github.com/prometheus/client_golang v1.7.1
+ github.com/rs/xid v0.0.0-20170604230408-02dd45c33376
+ github.com/satori/go.uuid v1.1.1-0.20170321230731-5bf94b69c6b6
+ github.com/shirou/gopsutil v2.16.13-0.20170924065440-6e221c482653+incompatible
+ github.com/smartystreets/goconvey v1.6.4 // indirect
+ github.com/spf13/pflag v1.0.0 // indirect
+ github.com/stretchr/testify v1.4.0
+ github.com/urfave/negroni v0.2.0
+ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
+ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2
+ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
+ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
+ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
+ golang.org/x/text v0.3.6
+ google.golang.org/api v0.26.0
+ google.golang.org/genproto v0.0.0-20200611194920-44ba362f84c1 // indirect
+ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
+ gopkg.in/inf.v0 v0.9.0 // indirect
+ gotest.tools v2.2.0+incompatible // indirect
+ k8s.io/api v0.0.0-20171027084545-218912509d74 // indirect
+ k8s.io/apimachinery v0.0.0-20171027084411-18a564baac72
+ k8s.io/client-go v5.0.1+incompatible
+ k8s.io/kube-openapi v0.0.0-20171101183504-39a7bf85c140 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000000000000000000000000000000000000..e6f4e392bdd6c8398a7dbdca680551aa645d8ab0
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,545 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.58.0 h1:vtAfVc723K3xKq1BQydk/FyCldnaNFhGhpJxaJzgRMQ=
+cloud.google.com/go v0.58.0/go.mod h1:W+9FnSUw6nhVwXlFcp1eL+krq5+HQUJeUogSeJZZiWg=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/Microsoft/go-winio v0.4.5 h1:U2XsGR5dBg1yzwSEJoP2dE2/aAXpmad+CNG2hE9Pd5k=
+github.com/Microsoft/go-winio v0.4.5/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
+github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
+github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/aws/aws-sdk-go v1.12.15 h1:ywQy1Yg2WBLKAW2uj95nHisn1Islid2zkIUqEwm0m7A=
+github.com/aws/aws-sdk-go v1.12.15/go.mod h1:ZRmQr0FajVIyZ4ZzBYKG5P3ZqPz9IHG41ZoMu1ADI3k=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/containerd/containerd v1.0.0-beta.2 h1:TOLkeQ/oZ5D2IL7JEp3cJxLXl+DqDYvxn4AjVZy7RMU=
+github.com/containerd/containerd v1.0.0-beta.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/docker/distribution v2.6.0-rc.1.0.20170726174610-edc3ab29cdff+incompatible h1:357nGVUC8gSpeSc2Axup8HfrfTLLUfWfCsCUhiQSKIg=
+github.com/docker/distribution v2.6.0-rc.1.0.20170726174610-edc3ab29cdff+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v1.4.2-0.20200309214505-aa6a9891b09c h1:zviRyz1SWO8+WVJbi9/jlJCkrsZ54r/lTRbgtcaQhLs=
+github.com/docker/docker v1.4.2-0.20200309214505-aa6a9891b09c/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.3.0 h1:3lOnM9cSzgGwx8VfK/NGOW5fLQ0GjIlCkaktF+n1M6o=
+github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/docker/go-units v0.3.2 h1:Kjm80apys7gTtfVmCvVY8gwu10uofaFSrmAKOVrtueE=
+github.com/docker/go-units v0.3.2/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/emicklei/go-restful v2.4.0+incompatible h1:p9u+CKd2OEI+kUmFLDwuf0LtmBtDhcok4UjQDs0rDDk=
+github.com/emicklei/go-restful v2.4.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/emicklei/go-restful-swagger12 v0.0.0-20170208215640-dcef7f557305 h1:2vAWk0wMCWb/pYiyat2rRZp5I5ZM+efPlagySNZ3JeM=
+github.com/emicklei/go-restful-swagger12 v0.0.0-20170208215640-dcef7f557305/go.mod h1:qr0VowGBT4CS4Q8vFF8BSeKz34PuqKGxs/L0IAQA9DQ=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-ini/ini v1.30.0 h1:bcFeUQUA+99t1cZPXmtc7HpGv2KTlZGIFeBDWQh2DRw=
+github.com/go-ini/ini v1.30.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-openapi/jsonpointer v0.0.0-20170102174223-779f45308c19 h1:UmnefiS/Yrdfl15NXUA9T51lyQf72tCvWHfOiRLd1+g=
+github.com/go-openapi/jsonpointer v0.0.0-20170102174223-779f45308c19/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
+github.com/go-openapi/jsonreference v0.0.0-20161105162150-36d33bfe519e h1:gbNUNGpVJLxaXBxI7iCHZdg3PwgLOJ9lPQGVINRrC9E=
+github.com/go-openapi/jsonreference v0.0.0-20161105162150-36d33bfe519e/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
+github.com/go-openapi/spec v0.0.0-20171105074921-a4fa9574c7aa h1:qePsbAVdhUehfvnxXb76IUtOaCE7RhDSH4ToubTWww4=
+github.com/go-openapi/spec v0.0.0-20171105074921-a4fa9574c7aa/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
+github.com/go-openapi/swag v0.0.0-20171111214437-cf0bdb963811 h1:r38dWW65/T34YOCwzGTAoDxP16l+OpDJl/n07P+pvUo=
+github.com/go-openapi/swag v0.0.0-20171111214437-cf0bdb963811/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-github v13.0.1-0.20171014143926-a021c14a5f19+incompatible h1:BqvW/QNZtzSStnaHJevBVYCPhqBSrhLgj8SeEUr9iR4=
+github.com/google/go-github v13.0.1-0.20171014143926-a021c14a5f19+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
+github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0=
+github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
+github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200507031123-427632fa3b1c/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/gnostic v0.1.0 h1:rVsPeBmXbYv4If/cumu1AzZPwV58q433hvONV1UEZoI=
+github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f h1:9oNbS1z4rVpbnkHBdPZU4jo9bSmrLpII768arSyMFgk=
+github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
+github.com/gorilla/handlers v1.3.0 h1:tsg9qP3mjt1h4Roxp+M1paRjrVBfPSOpBuVclh6YluI=
+github.com/gorilla/handlers v1.3.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
+github.com/gorilla/mux v1.5.0 h1:mq8bRov+5x+pZNR/uAHyUEgovR9gLgYFwDQIeuYi9TM=
+github.com/gorilla/mux v1.5.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/securecookie v0.0.0-20160422134519-667fe4e3466a h1:YH0IojQwndMQdeRWdw1aPT8bkbiWaYR3WD+Zf5e09DU=
+github.com/gorilla/securecookie v0.0.0-20160422134519-667fe4e3466a/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
+github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
+github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gregjones/httpcache v0.0.0-20171119193500-2bcd89a1743f h1:kOkUP6rcVVqC+KlKKENKtgfFfJyDySYhqL9srXooghY=
+github.com/gregjones/httpcache v0.0.0-20171119193500-2bcd89a1743f/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b h1:IpLPmn6Re21F0MaV6Zsc5RdSE6KuoFpWmHiUSEs3PrE=
+github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b/go.mod h1:aA6DnFhALT3zH0y+A39we+zbrdMC2N0X/q21e6FI0LU=
+github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE=
+github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY=
+github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/mailru/easyjson v0.0.0-20171120080333-32fa128f234d h1:bM4HYnlVXPgUKmzl7o3drEaVfOk+sTBiADAQOWjU+8I=
+github.com/mailru/easyjson v0.0.0-20171120080333-32fa128f234d/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/miekg/dns v0.0.0-20171019064225-822ae18e7187 h1:gF0xdz8uynTvRxheFyq4UxAXc0bDxiB3QpSIGmfA5xk=
+github.com/miekg/dns v0.0.0-20171019064225-822ae18e7187/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
+github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/image-spec v1.0.0 h1:jcw3cCH887bLKETGYpv8afogdYchbShR0eH6oD9d5PQ=
+github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
+github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA=
+github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc=
+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8=
+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rs/xid v0.0.0-20170604230408-02dd45c33376 h1:pisBoZ1sLLFc+g7EZflpvatXVqmQKv8EjPP8/radknQ=
+github.com/rs/xid v0.0.0-20170604230408-02dd45c33376/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
+github.com/satori/go.uuid v1.1.1-0.20170321230731-5bf94b69c6b6 h1:oZag5hylqWwZrDdj/laMwWQnXaeWBQf66qm4PGQI6Wc=
+github.com/satori/go.uuid v1.1.1-0.20170321230731-5bf94b69c6b6/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/shirou/gopsutil v2.16.13-0.20170924065440-6e221c482653+incompatible h1:gh3Hq7V5Zb8X2xk4T9FbHVi1m2Yo+oEIyjjJUiezVoY=
+github.com/shirou/gopsutil v2.16.13-0.20170924065440-6e221c482653+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/spf13/pflag v1.0.0 h1:oaPbdDe/x0UncahuwiPxW1GYJyilRAdsPnq3e1yaPcI=
+github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/urfave/negroni v0.2.0 h1:cadBY8/+9L/dTagBqV7N0l/SJiB4Wg+os5QdmaFY5Wg=
+github.com/urfave/negroni v0.2.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
+golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200606014950-c42cb6316fb6/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.26.0 h1:VJZ8h6E8ip82FRpQl848c5vAadxlTXrUh8RzQzSRm08=
+google.golang.org/api v0.26.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200608115520-7c474a2e3482/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200611194920-44ba362f84c1 h1:rRk0Nk0YJuu8qID4s2qgHG/WOMKXFafQ0/4vD/6/EuM=
+google.golang.org/genproto v0.0.0-20200611194920-44ba362f84c1/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o=
+gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
+gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+k8s.io/api v0.0.0-20171027084545-218912509d74 h1:CYism0UbF96TF8s8sYrKagsJn3oqR456q/ER/cELIuA=
+k8s.io/api v0.0.0-20171027084545-218912509d74/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
+k8s.io/apimachinery v0.0.0-20171027084411-18a564baac72 h1:pPbfmsjOvePqojmf5AhR0o7bxZTCxE0nkDakanV50CM=
+k8s.io/apimachinery v0.0.0-20171027084411-18a564baac72/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
+k8s.io/client-go v5.0.1+incompatible h1:IPZ0cnux5ui8+X8r1HdeFPXucpQ4HyJQigjo1clq1QM=
+k8s.io/client-go v5.0.1+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
+k8s.io/kube-openapi v0.0.0-20171101183504-39a7bf85c140 h1:j1Zez+Xb4OWvCdROqeq8sP2ACi/qWV1tj/imP0/8a0k=
+k8s.io/kube-openapi v0.0.0-20171101183504-39a7bf85c140/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/handlers/bootstrap.go b/handlers/bootstrap.go
new file mode 100644
index 0000000000000000000000000000000000000000..48e7162b422e2313a0d8285c824d92822abfcb5b
--- /dev/null
+++ b/handlers/bootstrap.go
@@ -0,0 +1,287 @@
+package handlers
+
+import (
+ "bytes"
+ "context"
+ "crypto/tls"
+ "embed"
+ "fmt"
+ "html/template"
+ "io/fs"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "path"
+ "strings"
+ "time"
+
+ "golang.org/x/crypto/acme/autocert"
+ "golang.org/x/oauth2"
+
+ gh "github.com/gorilla/handlers"
+ "github.com/gorilla/mux"
+ lru "github.com/hashicorp/golang-lru"
+ "github.com/play-with-docker/play-with-docker/config"
+ "github.com/play-with-docker/play-with-docker/event"
+ "github.com/play-with-docker/play-with-docker/pwd"
+ "github.com/play-with-docker/play-with-docker/pwd/types"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
+ "github.com/urfave/negroni"
+ oauth2Github "golang.org/x/oauth2/github"
+ oauth2Google "golang.org/x/oauth2/google"
+ "google.golang.org/api/people/v1"
+)
+
+var (
+ core pwd.PWDApi
+ e event.EventApi
+ landings = map[string][]byte{}
+)
+
+//go:embed www/*
+var embeddedFiles embed.FS
+
+var staticFiles fs.FS
+
+var latencyHistogramVec = prometheus.NewHistogramVec(prometheus.HistogramOpts{
+ Name: "pwd_handlers_duration_ms",
+ Help: "How long it took to process a specific handler, in a specific host",
+ Buckets: []float64{300, 1200, 5000},
+}, []string{"action"})
+
+type HandlerExtender func(h *mux.Router)
+
+func init() {
+ prometheus.MustRegister(latencyHistogramVec)
+ staticFiles, _ = fs.Sub(embeddedFiles, "www")
+}
+
+func Bootstrap(c pwd.PWDApi, ev event.EventApi) {
+ core = c
+ e = ev
+}
+
+func Register(extend HandlerExtender) {
+ initPlaygrounds()
+
+ r := mux.NewRouter()
+ corsRouter := mux.NewRouter()
+
+ corsHandler := gh.CORS(gh.AllowCredentials(), gh.AllowedHeaders([]string{"x-requested-with", "content-type"}), gh.AllowedMethods([]string{"GET", "POST", "HEAD", "DELETE"}), gh.AllowedOriginValidator(func(origin string) bool {
+ if strings.HasSuffix(origin, ".play-with-docker.com") ||
+ strings.HasSuffix(origin, ".play-with-kubernetes.com") ||
+ strings.HasSuffix(origin, ".docker.com") ||
+ strings.HasSuffix(origin, ".play-with-go.dev") {
+ return true
+ }
+ return false
+ }), gh.AllowedOrigins([]string{}))
+
+ // Specific routes
+ r.HandleFunc("/ping", Ping).Methods("GET")
+ corsRouter.HandleFunc("/instances/images", GetInstanceImages).Methods("GET")
+ corsRouter.HandleFunc("/sessions/{sessionId}", GetSession).Methods("GET")
+ corsRouter.HandleFunc("/sessions/{sessionId}/close", CloseSession).Methods("POST")
+ corsRouter.HandleFunc("/sessions/{sessionId}", CloseSession).Methods("DELETE")
+ corsRouter.HandleFunc("/sessions/{sessionId}/setup", SessionSetup).Methods("POST")
+ corsRouter.HandleFunc("/sessions/{sessionId}/instances", NewInstance).Methods("POST")
+ corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/uploads", FileUpload).Methods("POST")
+ corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}", DeleteInstance).Methods("DELETE")
+ corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/exec", Exec).Methods("POST")
+ corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/fstree", fsTree).Methods("GET")
+ corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/file", file).Methods("GET")
+
+ r.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/editor", func(rw http.ResponseWriter, r *http.Request) {
+ serveAsset(rw, r, "editor.html")
+ })
+
+ r.HandleFunc("/ooc", func(rw http.ResponseWriter, r *http.Request) {
+ serveAsset(rw, r, "ooc.html")
+ }).Methods("GET")
+ r.HandleFunc("/503", func(rw http.ResponseWriter, r *http.Request) {
+ serveAsset(rw, r, "503.html")
+ }).Methods("GET")
+ r.HandleFunc("/p/{sessionId}", Home).Methods("GET")
+ r.PathPrefix("/assets").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+ serveAsset(rw, r, r.URL.Path[1:])
+ })
+ r.HandleFunc("/robots.txt", func(rw http.ResponseWriter, r *http.Request) {
+ serveAsset(rw, r, "robots.txt")
+ })
+
+ corsRouter.HandleFunc("/sessions/{sessionId}/ws/", WSH)
+ r.Handle("/metrics", promhttp.Handler())
+
+ // Generic routes
+ r.HandleFunc("/", Landing).Methods("GET")
+
+ corsRouter.HandleFunc("/users/me", LoggedInUser).Methods("GET")
+ r.HandleFunc("/users/{userId:.{3,}}", GetUser).Methods("GET")
+ r.HandleFunc("/oauth/providers", ListProviders).Methods("GET")
+ r.HandleFunc("/oauth/providers/{provider}/login", Login).Methods("GET")
+ r.HandleFunc("/oauth/providers/{provider}/callback", LoginCallback).Methods("GET")
+ r.HandleFunc("/playgrounds", NewPlayground).Methods("PUT")
+ r.HandleFunc("/playgrounds", ListPlaygrounds).Methods("GET")
+ r.HandleFunc("/my/playground", GetCurrentPlayground).Methods("GET")
+
+ corsRouter.HandleFunc("/", NewSession).Methods("POST")
+
+ if extend != nil {
+ extend(corsRouter)
+ }
+
+ n := negroni.Classic()
+
+ r.PathPrefix("/").Handler(negroni.New(negroni.Wrap(corsHandler(corsRouter))))
+ n.UseHandler(r)
+
+ httpServer := http.Server{
+ Addr: "0.0.0.0:" + config.PortNumber,
+ Handler: n,
+ IdleTimeout: 30 * time.Second,
+ ReadHeaderTimeout: 5 * time.Second,
+ }
+
+ if config.UseLetsEncrypt {
+ domainCache, err := lru.New(5000)
+ if err != nil {
+ log.Fatalf("Could not start domain cache. Got: %v", err)
+ }
+ certManager := autocert.Manager{
+ Prompt: autocert.AcceptTOS,
+ HostPolicy: func(ctx context.Context, host string) error {
+ if _, found := domainCache.Get(host); !found {
+ if playground := core.PlaygroundFindByDomain(host); playground == nil {
+ return fmt.Errorf("Playground for domain %s was not found", host)
+ }
+ domainCache.Add(host, true)
+ }
+ return nil
+ },
+ Cache: autocert.DirCache(config.LetsEncryptCertsDir),
+ }
+
+ httpServer.TLSConfig = &tls.Config{
+ GetCertificate: certManager.GetCertificate,
+ }
+
+ go func() {
+ rr := mux.NewRouter()
+ rr.HandleFunc("/ping", Ping).Methods("GET")
+ rr.Handle("/metrics", promhttp.Handler())
+ rr.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
+ target := fmt.Sprintf("https://%s%s", r.Host, r.URL.Path)
+ if len(r.URL.RawQuery) > 0 {
+ target += "?" + r.URL.RawQuery
+ }
+ http.Redirect(rw, r, target, http.StatusMovedPermanently)
+ })
+ nr := negroni.Classic()
+ nr.UseHandler(rr)
+ log.Println("Starting redirect server")
+ redirectServer := http.Server{
+ Addr: "0.0.0.0:3001",
+ Handler: certManager.HTTPHandler(nr),
+ IdleTimeout: 30 * time.Second,
+ ReadHeaderTimeout: 5 * time.Second,
+ }
+ log.Fatal(redirectServer.ListenAndServe())
+ }()
+
+ log.Println("Listening on port " + config.PortNumber)
+ log.Fatal(httpServer.ListenAndServeTLS("", ""))
+ } else {
+ log.Println("Listening on port " + config.PortNumber)
+ log.Fatal(httpServer.ListenAndServe())
+ }
+}
+
+func serveAsset(w http.ResponseWriter, r *http.Request, name string) {
+ a, err := fs.ReadFile(staticFiles, name)
+ if err != nil {
+ w.WriteHeader(http.StatusNotFound)
+ return
+ }
+ http.ServeContent(w, r, name, time.Time{}, bytes.NewReader(a))
+}
+
+func initPlaygrounds() {
+ pgs, err := core.PlaygroundList()
+ if err != nil {
+ log.Fatal("Error getting playgrounds for initialization")
+ }
+
+ for _, p := range pgs {
+ initAssets(p)
+ initOauthProviders(p)
+ }
+}
+
+func initAssets(p *types.Playground) {
+ if p.AssetsDir == "" {
+ p.AssetsDir = "default"
+ }
+
+ lpath := path.Join(p.AssetsDir, "landing.html")
+ landing, err := fs.ReadFile(staticFiles, lpath)
+ if err != nil {
+ log.Printf("Could not load %v: %v", lpath, err)
+ return
+ }
+
+ var b bytes.Buffer
+ t := template.New("landing.html").Delims("[[", "]]")
+ t, err = t.Parse(string(landing))
+ if err != nil {
+ log.Fatalf("Error parsing template %v", err)
+ }
+ if err := t.Execute(&b, struct{ SegmentId string }{config.SegmentId}); err != nil {
+ log.Fatalf("Error executing template %v", err)
+ }
+ landingBytes, err := ioutil.ReadAll(&b)
+ if err != nil {
+ log.Fatalf("Error reading template bytes %v", err)
+ }
+ landings[p.Id] = landingBytes
+}
+
+func initOauthProviders(p *types.Playground) {
+ config.Providers[p.Id] = map[string]*oauth2.Config{}
+
+ if p.GithubClientID != "" && p.GithubClientSecret != "" {
+ conf := &oauth2.Config{
+ ClientID: p.GithubClientID,
+ ClientSecret: p.GithubClientSecret,
+ Scopes: []string{"user:email"},
+ Endpoint: oauth2Github.Endpoint,
+ }
+
+ config.Providers[p.Id]["github"] = conf
+ }
+ if p.GoogleClientID != "" && p.GoogleClientSecret != "" {
+ conf := &oauth2.Config{
+ ClientID: p.GoogleClientID,
+ ClientSecret: p.GoogleClientSecret,
+ Scopes: []string{people.UserinfoEmailScope, people.UserinfoProfileScope},
+ Endpoint: oauth2Google.Endpoint,
+ }
+
+ config.Providers[p.Id]["google"] = conf
+ }
+ if p.DockerClientID != "" && p.DockerClientSecret != "" {
+
+ endpoint := getDockerEndpoint(p)
+ conf := &oauth2.Config{
+ ClientID: p.DockerClientID,
+ ClientSecret: p.DockerClientSecret,
+ Scopes: []string{"openid", "full_access:account"},
+ Endpoint: oauth2.Endpoint{
+ AuthURL: fmt.Sprintf("https://%s/authorize/", endpoint),
+ TokenURL: fmt.Sprintf("https://%s/oauth/token", endpoint),
+ },
+ }
+
+ config.Providers[p.Id]["docker"] = conf
+ }
+}
diff --git a/handlers/close_session.go b/handlers/close_session.go
new file mode 100644
index 0000000000000000000000000000000000000000..22753b181cbd9378e831230f511aba381a824777
--- /dev/null
+++ b/handlers/close_session.go
@@ -0,0 +1,30 @@
+package handlers
+
+import (
+ "log"
+ "net/http"
+
+ "github.com/gorilla/mux"
+ "github.com/play-with-docker/play-with-docker/storage"
+)
+
+func CloseSession(rw http.ResponseWriter, req *http.Request) {
+ vars := mux.Vars(req)
+ sessionId := vars["sessionId"]
+
+ session, err := core.SessionGet(sessionId)
+ if err == storage.NotFoundError {
+ rw.WriteHeader(http.StatusNotFound)
+ return
+ } else if err != nil {
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ if err := core.SessionClose(session); err != nil {
+ log.Println(err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+}
diff --git a/handlers/cookie_id.go b/handlers/cookie_id.go
new file mode 100644
index 0000000000000000000000000000000000000000..a7ab66181f26b52959e5f15ae614c3d2bd9f5d3e
--- /dev/null
+++ b/handlers/cookie_id.go
@@ -0,0 +1,44 @@
+package handlers
+
+import (
+ "net/http"
+
+ "github.com/play-with-docker/play-with-docker/config"
+)
+
+type CookieID struct {
+ Id string `json:"id"`
+ UserName string `json:"user_name"`
+ UserAvatar string `json:"user_avatar"`
+ ProviderId string `json:"provider_id"`
+}
+
+func (c *CookieID) SetCookie(rw http.ResponseWriter, host string) error {
+ if encoded, err := config.SecureCookie.Encode("id", c); err == nil {
+ cookie := &http.Cookie{
+ Name: "id",
+ Value: encoded,
+ Domain: host,
+ Path: "/",
+ SameSite: http.SameSiteDefaultMode,
+ Secure: false,
+ HttpOnly: true,
+ }
+ http.SetCookie(rw, cookie)
+ } else {
+ return err
+ }
+ return nil
+}
+func ReadCookie(r *http.Request) (*CookieID, error) {
+ if cookie, err := r.Cookie("id"); err == nil {
+ value := &CookieID{}
+ if err = config.SecureCookie.Decode("id", cookie.Value, &value); err == nil {
+ return value, nil
+ } else {
+ return nil, err
+ }
+ } else {
+ return nil, err
+ }
+}
diff --git a/handlers/delete_instance.go b/handlers/delete_instance.go
new file mode 100644
index 0000000000000000000000000000000000000000..8a7d3975349cd5d9ae9893bfb6dae28d25c06ec8
--- /dev/null
+++ b/handlers/delete_instance.go
@@ -0,0 +1,30 @@
+package handlers
+
+import (
+ "net/http"
+
+ "github.com/gorilla/mux"
+ "github.com/play-with-docker/play-with-docker/storage"
+)
+
+func DeleteInstance(rw http.ResponseWriter, req *http.Request) {
+ vars := mux.Vars(req)
+ sessionId := vars["sessionId"]
+ instanceName := vars["instanceName"]
+
+ s, err := core.SessionGet(sessionId)
+ if s != nil {
+ i := core.InstanceGet(s, instanceName)
+ err := core.InstanceDelete(s, i)
+ if err != nil {
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ } else if err == storage.NotFoundError {
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ } else if err != nil {
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+}
diff --git a/handlers/exec.go b/handlers/exec.go
new file mode 100644
index 0000000000000000000000000000000000000000..55eb939f57748a0688f2c3709fbc75fdd2098553
--- /dev/null
+++ b/handlers/exec.go
@@ -0,0 +1,51 @@
+package handlers
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+
+ "github.com/gorilla/mux"
+)
+
+type execRequest struct {
+ Cmd []string `json:"command"`
+}
+
+type execResponse struct {
+ ExitCode int `json:"status_code"`
+}
+
+func Exec(rw http.ResponseWriter, req *http.Request) {
+ vars := mux.Vars(req)
+ sessionId := vars["sessionId"]
+ instanceName := vars["instanceName"]
+
+ var er execRequest
+ err := json.NewDecoder(req.Body).Decode(&er)
+ if err != nil {
+ rw.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ s, _ := core.SessionGet(sessionId)
+ if s == nil {
+ rw.WriteHeader(http.StatusNotFound)
+ return
+ }
+ i := core.InstanceGet(s, instanceName)
+ if i == nil {
+ rw.WriteHeader(http.StatusNotFound)
+ return
+ }
+
+ code, err := core.InstanceExec(i, er.Cmd)
+
+ if err != nil {
+ log.Println(err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ json.NewEncoder(rw).Encode(execResponse{code})
+}
diff --git a/handlers/file_instance.go b/handlers/file_instance.go
new file mode 100644
index 0000000000000000000000000000000000000000..cd03baf0fd31b88b06854c38ed62d7155ca10942
--- /dev/null
+++ b/handlers/file_instance.go
@@ -0,0 +1,54 @@
+package handlers
+
+import (
+ "encoding/base64"
+ "io"
+ "log"
+ "net/http"
+
+ "github.com/gorilla/mux"
+)
+
+func file(rw http.ResponseWriter, req *http.Request) {
+ vars := mux.Vars(req)
+ sessionId := vars["sessionId"]
+ instanceName := vars["instanceName"]
+
+ query := req.URL.Query()
+
+ path := query.Get("path")
+
+ if path == "" {
+ rw.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ s, _ := core.SessionGet(sessionId)
+ if s == nil {
+ rw.WriteHeader(http.StatusNotFound)
+ return
+ }
+
+ i := core.InstanceGet(s, instanceName)
+ if i == nil {
+ rw.WriteHeader(http.StatusNotFound)
+ return
+ }
+
+ instanceFile, err := core.InstanceFile(i, path)
+ if err != nil {
+ log.Println(err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ encoder := base64.NewEncoder(base64.StdEncoding, rw)
+
+ if _, err = io.Copy(encoder, instanceFile); err != nil {
+ log.Println(err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ encoder.Close()
+}
diff --git a/handlers/file_upload.go b/handlers/file_upload.go
new file mode 100644
index 0000000000000000000000000000000000000000..52b54012feb6dba3781b588ae48f4ae7e007d192
--- /dev/null
+++ b/handlers/file_upload.go
@@ -0,0 +1,80 @@
+package handlers
+
+import (
+ "io"
+ "log"
+ "net/http"
+ "path/filepath"
+
+ "github.com/gorilla/mux"
+ "github.com/play-with-docker/play-with-docker/storage"
+)
+
+func FileUpload(rw http.ResponseWriter, req *http.Request) {
+ vars := mux.Vars(req)
+ sessionId := vars["sessionId"]
+ instanceName := vars["instanceName"]
+
+ s, err := core.SessionGet(sessionId)
+ if err == storage.NotFoundError {
+ rw.WriteHeader(http.StatusNotFound)
+ return
+ } else if err != nil {
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ i := core.InstanceGet(s, instanceName)
+
+ // Path to upload the file to
+ path := req.URL.Query().Get("path")
+
+ // allow up to 32 MB which is the default
+
+ // has a url query parameter, ignore body
+ if url := req.URL.Query().Get("url"); url != "" {
+
+ _, fileName := filepath.Split(url)
+
+ err := core.InstanceUploadFromUrl(i, fileName, path, req.URL.Query().Get("url"))
+ if err != nil {
+ log.Println(err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ rw.WriteHeader(http.StatusOK)
+ return
+ } else {
+ red, err := req.MultipartReader()
+ if err != nil {
+ log.Println(err)
+ rw.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ for {
+ p, err := red.NextPart()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ log.Println(err)
+ continue
+ }
+
+ if p.FileName() == "" {
+ continue
+ }
+ err = core.InstanceUploadFromReader(i, p.FileName(), path, p)
+ if err != nil {
+ log.Println(err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ log.Printf("Uploaded [%s] to [%s]\n", p.FileName(), i.Name)
+ }
+ rw.WriteHeader(http.StatusOK)
+ return
+ }
+
+}
diff --git a/handlers/fstree_instance.go b/handlers/fstree_instance.go
new file mode 100644
index 0000000000000000000000000000000000000000..9e1b17591d4e7d14ae539474be1941c55607986d
--- /dev/null
+++ b/handlers/fstree_instance.go
@@ -0,0 +1,42 @@
+package handlers
+
+import (
+ "io"
+ "log"
+ "net/http"
+
+ "github.com/gorilla/mux"
+)
+
+func fsTree(rw http.ResponseWriter, req *http.Request) {
+ vars := mux.Vars(req)
+ sessionId := vars["sessionId"]
+ instanceName := vars["instanceName"]
+
+ s, _ := core.SessionGet(sessionId)
+ if s == nil {
+ rw.WriteHeader(http.StatusNotFound)
+ return
+ }
+
+ i := core.InstanceGet(s, instanceName)
+ if i == nil {
+ rw.WriteHeader(http.StatusNotFound)
+ return
+ }
+
+ tree, err := core.InstanceFSTree(i)
+
+ if err != nil {
+ log.Println(err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ rw.Header().Set("content-type", "application/json")
+ if _, err = io.Copy(rw, tree); err != nil {
+ log.Println(err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+}
diff --git a/handlers/get_instance_images.go b/handlers/get_instance_images.go
new file mode 100644
index 0000000000000000000000000000000000000000..a2e42cd3bd79930ab352269131bc98a3d612de6b
--- /dev/null
+++ b/handlers/get_instance_images.go
@@ -0,0 +1,17 @@
+package handlers
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+)
+
+func GetInstanceImages(rw http.ResponseWriter, req *http.Request) {
+ playground := core.PlaygroundFindByDomain(req.Host)
+ if playground == nil {
+ log.Printf("Playground for domain %s was not found!", req.Host)
+ rw.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ json.NewEncoder(rw).Encode(playground.AvailableDinDInstanceImages)
+}
diff --git a/handlers/get_session.go b/handlers/get_session.go
new file mode 100644
index 0000000000000000000000000000000000000000..66ce321d735a34d6d0981ceaae054f9b729a30b0
--- /dev/null
+++ b/handlers/get_session.go
@@ -0,0 +1,43 @@
+package handlers
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+
+ "github.com/gorilla/mux"
+ "github.com/play-with-docker/play-with-docker/pwd/types"
+ "github.com/play-with-docker/play-with-docker/storage"
+)
+
+type SessionInfo struct {
+ *types.Session
+ Instances map[string]*types.Instance `json:"instances"`
+}
+
+func GetSession(rw http.ResponseWriter, req *http.Request) {
+ vars := mux.Vars(req)
+ sessionId := vars["sessionId"]
+
+ session, err := core.SessionGet(sessionId)
+ if err == storage.NotFoundError {
+ rw.WriteHeader(http.StatusNotFound)
+ return
+ } else if err != nil {
+ rw.WriteHeader(http.StatusNotFound)
+ return
+ }
+
+ instances, err := core.InstanceFindBySession(session)
+ if err != nil {
+ log.Println(err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ is := map[string]*types.Instance{}
+ for _, i := range instances {
+ is[i.Name] = i
+ }
+
+ json.NewEncoder(rw).Encode(SessionInfo{session, is})
+}
diff --git a/handlers/home.go b/handlers/home.go
new file mode 100644
index 0000000000000000000000000000000000000000..64a6d7c14b55795ec3020ab731138982b548579f
--- /dev/null
+++ b/handlers/home.go
@@ -0,0 +1,60 @@
+package handlers
+
+import (
+ "io/fs"
+ "log"
+ "net/http"
+ "path/filepath"
+
+ "github.com/gorilla/mux"
+ "github.com/play-with-docker/play-with-docker/storage"
+)
+
+func Home(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ sessionId := vars["sessionId"]
+
+ s, err := core.SessionGet(sessionId)
+ if err == storage.NotFoundError {
+ // Session doesn't exist (can happen if closing the sessions an reloading the page, or similar).
+ w.WriteHeader(http.StatusNotFound)
+ return
+ } else if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ if s.Stack != "" {
+ go core.SessionDeployStack(s)
+ }
+
+ playground := core.PlaygroundGet(s.PlaygroundId)
+ if playground == nil {
+ log.Printf("Playground with id %s for session %s was not found!", s.PlaygroundId, s.Id)
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ index, err := fs.ReadFile(staticFiles, filepath.Join(playground.AssetsDir, "/index.html"))
+ if err != nil {
+ index, err = fs.ReadFile(staticFiles, "default/index.html")
+ }
+
+ if err != nil {
+ w.WriteHeader(http.StatusFound)
+ return
+
+ }
+ w.Write(index)
+}
+
+func Landing(rw http.ResponseWriter, req *http.Request) {
+ playground := core.PlaygroundFindByDomain(req.Host)
+ if playground == nil {
+ log.Printf("Playground for domain %s was not found!", req.Host)
+ rw.WriteHeader(http.StatusNotFound)
+ return
+ }
+
+ rw.Write(landings[playground.Id])
+
+}
diff --git a/handlers/login.go b/handlers/login.go
new file mode 100644
index 0000000000000000000000000000000000000000..d2a3862519cd3d3b42361947b32ca0f06426db82
--- /dev/null
+++ b/handlers/login.go
@@ -0,0 +1,263 @@
+package handlers
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "golang.org/x/oauth2"
+
+ "github.com/google/go-github/github"
+ "github.com/gorilla/mux"
+ "github.com/play-with-docker/play-with-docker/config"
+ "github.com/play-with-docker/play-with-docker/pwd/types"
+ uuid "github.com/satori/go.uuid"
+ "google.golang.org/api/option"
+ "google.golang.org/api/people/v1"
+)
+
+func LoggedInUser(rw http.ResponseWriter, req *http.Request) {
+ cookie, err := ReadCookie(req)
+ if err != nil {
+ log.Println("Cannot read cookie")
+ rw.WriteHeader(http.StatusUnauthorized)
+ return
+ }
+
+ user, err := core.UserGet(cookie.Id)
+ if err != nil {
+ log.Printf("Couldn't get user with id %s. Got: %v\n", cookie.Id, err)
+ rw.WriteHeader(http.StatusUnauthorized)
+ return
+ }
+ json.NewEncoder(rw).Encode(user)
+}
+
+func ListProviders(rw http.ResponseWriter, req *http.Request) {
+ playground := core.PlaygroundFindByDomain(req.Host)
+ if playground == nil {
+ log.Printf("Playground for domain %s was not found!", req.Host)
+ rw.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ providers := []string{}
+ for name := range config.Providers[playground.Id] {
+ providers = append(providers, name)
+ }
+ json.NewEncoder(rw).Encode(providers)
+}
+
+func Login(rw http.ResponseWriter, req *http.Request) {
+ vars := mux.Vars(req)
+ providerName := vars["provider"]
+ playground := core.PlaygroundFindByDomain(req.Host)
+ if playground == nil {
+ log.Printf("Playground for domain %s was not found!", req.Host)
+ rw.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ provider, found := config.Providers[playground.Id][providerName]
+ if !found {
+ log.Printf("Could not find provider %s\n", providerName)
+ rw.WriteHeader(http.StatusNotFound)
+ return
+ }
+
+ loginRequest, err := core.UserNewLoginRequest(providerName)
+ if err != nil {
+ log.Printf("Could not start a new user login request for provider %s. Got: %v\n", providerName, err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ if playground.AuthRedirectBase != "" {
+ provider.RedirectURL = fmt.Sprintf("%s/oauth/providers/%s/callback", playground.AuthRedirectBase, providerName)
+ } else {
+ scheme := "http"
+ if req.TLS != nil {
+ scheme = "https"
+ }
+ host := "localhost"
+ if req.Host != "" {
+ host = req.Host
+ }
+ provider.RedirectURL = fmt.Sprintf("%s://%s/oauth/providers/%s/callback", scheme, host, providerName)
+ }
+
+ url := provider.AuthCodeURL(loginRequest.Id, oauth2.SetAuthURLParam("nonce", uuid.NewV4().String()))
+
+ http.Redirect(rw, req, url, http.StatusFound)
+}
+
+func LoginCallback(rw http.ResponseWriter, req *http.Request) {
+ vars := mux.Vars(req)
+ providerName := vars["provider"]
+ playground := core.PlaygroundFindByDomain(req.Host)
+ if playground == nil {
+ log.Printf("Playground for domain %s was not found!", req.Host)
+ rw.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ provider, found := config.Providers[playground.Id][providerName]
+ if !found {
+ log.Printf("Could not find provider %s\n", providerName)
+ rw.WriteHeader(http.StatusNotFound)
+ return
+ }
+
+ query := req.URL.Query()
+
+ code := query.Get("code")
+ loginRequestId := query.Get("state")
+
+ loginRequest, err := core.UserGetLoginRequest(loginRequestId)
+ if err != nil {
+ log.Printf("Could not get login request %s for provider %s. Got: %v\n", loginRequestId, providerName, err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ ctx := req.Context()
+ tok, err := provider.Exchange(ctx, code)
+ if err != nil {
+ log.Printf("Could not exchage code for access token for provider %s. Got: %v\n", providerName, err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ user := &types.User{Provider: providerName}
+ if providerName == "github" {
+ ts := oauth2.StaticTokenSource(
+ &oauth2.Token{AccessToken: tok.AccessToken},
+ )
+ tc := oauth2.NewClient(ctx, ts)
+ client := github.NewClient(tc)
+ u, _, err := client.Users.Get(ctx, "")
+ if err != nil {
+ log.Printf("Could not get user from github. Got: %v\n", err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ user.ProviderUserId = strconv.Itoa(u.GetID())
+ user.Name = u.GetName()
+ user.Avatar = u.GetAvatarURL()
+ user.Email = u.GetEmail()
+ } else if providerName == "google" {
+ ts := oauth2.StaticTokenSource(
+ &oauth2.Token{AccessToken: tok.AccessToken},
+ )
+ tc := oauth2.NewClient(ctx, ts)
+
+ p, err := people.NewService(ctx, option.WithHTTPClient(tc))
+ if err != nil {
+ log.Printf("Could not initialize people service . Got: %v\n", err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ person, err := p.People.Get("people/me").PersonFields("emailAddresses,names").Do()
+ if err != nil {
+ log.Printf("Could not initialize people service . Got: %v\n", err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ user.Email = person.EmailAddresses[0].Value
+ user.Name = person.Names[0].GivenName
+ user.ProviderUserId = person.ResourceName
+
+ } else if providerName == "docker" {
+ ts := oauth2.StaticTokenSource(
+ &oauth2.Token{AccessToken: tok.AccessToken},
+ )
+ tc := oauth2.NewClient(ctx, ts)
+
+ endpoint := getDockerEndpoint(playground)
+ resp, err := tc.Get(fmt.Sprintf("https://%s/userinfo", endpoint))
+ if err != nil {
+ log.Printf("Could not get user from docker. Got: %v\n", err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ userInfo := map[string]interface{}{}
+ if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
+ log.Printf("Could not decode user info. Got: %v\n", err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ user.ProviderUserId = strings.Split(userInfo["sub"].(string), "|")[1]
+ user.Name = userInfo["https://hub.docker.com"].(map[string]interface{})["username"].(string)
+ user.Email = userInfo["https://hub.docker.com"].(map[string]interface{})["email"].(string)
+ // Since DockerID doesn't return a user avatar, we try with twitter through avatars.io
+ // Worst case we get a generic avatar
+ user.Avatar = fmt.Sprintf("https://avatars.io/twitter/%s", user.Name)
+ }
+
+ user, err = core.UserLogin(loginRequest, user)
+ if err != nil {
+ log.Printf("Could not login user. Got: %v\n", err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ cookieData := CookieID{Id: user.Id, UserName: user.Name, UserAvatar: user.Avatar, ProviderId: user.ProviderUserId}
+
+ host := "localhost"
+ if req.Host != "" {
+ // we get the parent domain so cookie is set
+ // in all subdomain and siblings
+ host = getParentDomain(req.Host)
+ }
+
+ if err := cookieData.SetCookie(rw, host); err != nil {
+ log.Printf("Could not encode cookie. Got: %v\n", err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ r, _ := playground.Extras.GetString("LoginRedirect")
+
+ fmt.Fprintf(rw, `
+
+
+
+
+
+
+`, r)
+}
+
+// getParentDomain returns the parent domain (if available)
+// of the currend domain
+func getParentDomain(host string) string {
+ levels := strings.Split(host, ".")
+ if len(levels) > 2 {
+ return strings.Join(levels[1:], ".")
+ }
+ return host
+}
+
+func getDockerEndpoint(p *types.Playground) string {
+ if len(p.DockerHost) > 0 {
+ return p.DockerHost
+ }
+ return "login.docker.com"
+}
diff --git a/handlers/new_instance.go b/handlers/new_instance.go
new file mode 100644
index 0000000000000000000000000000000000000000..e2335f8a04c89b0f6261c9d794ed795f2089625c
--- /dev/null
+++ b/handlers/new_instance.go
@@ -0,0 +1,78 @@
+package handlers
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/http"
+
+ "github.com/gorilla/mux"
+ "github.com/play-with-docker/play-with-docker/provisioner"
+ "github.com/play-with-docker/play-with-docker/pwd/types"
+)
+
+func NewInstance(rw http.ResponseWriter, req *http.Request) {
+ vars := mux.Vars(req)
+ sessionId := vars["sessionId"]
+
+ body := types.InstanceConfig{PlaygroundFQDN: req.Host, DindVolumeSize: "5G"}
+
+ json.NewDecoder(req.Body).Decode(&body)
+
+ s, err := core.SessionGet(sessionId)
+ if err != nil {
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ playground := core.PlaygroundGet(s.PlaygroundId)
+ if playground == nil {
+ log.Printf("Playground with id %s for session %s was not found!", s.PlaygroundId, s.Id)
+ rw.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ if body.Type == "windows" && !playground.AllowWindowsInstances {
+ rw.WriteHeader(http.StatusUnauthorized)
+ return
+ }
+
+ instances, err := core.InstanceFindBySession(s)
+
+ if err != nil {
+ log.Println(err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ if playground.MaxInstances > 0 && len(instances) >= playground.MaxInstances {
+ log.Println(err)
+ rw.WriteHeader(http.StatusConflict)
+ return
+ }
+
+ if len(playground.DindVolumeSize) > 0 {
+ body.DindVolumeSize = playground.DindVolumeSize
+ }
+
+ // TODO I don't like how this is implemented here. NewInstance
+ // should be a function that's in the Playground struct.
+ if playground.Privileged {
+ body.Privileged = true
+ }
+
+ i, err := core.InstanceNew(s, body)
+ if err != nil {
+ if provisioner.OutOfCapacity(err) {
+ rw.WriteHeader(http.StatusServiceUnavailable)
+ fmt.Fprintln(rw, `{"error": "out_of_capacity"}`)
+ return
+ }
+ log.Println(err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ //TODO: Set a status error
+ } else {
+ json.NewEncoder(rw).Encode(i)
+ }
+}
diff --git a/handlers/new_session.go b/handlers/new_session.go
new file mode 100644
index 0000000000000000000000000000000000000000..59738bb20321f6c63d31433cf5648878995c8015
--- /dev/null
+++ b/handlers/new_session.go
@@ -0,0 +1,125 @@
+package handlers
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/http"
+ "path"
+ "strings"
+ "time"
+
+ "github.com/play-with-docker/play-with-docker/config"
+ "github.com/play-with-docker/play-with-docker/provisioner"
+ "github.com/play-with-docker/play-with-docker/pwd/types"
+)
+
+type NewSessionResponse struct {
+ SessionId string `json:"session_id"`
+ Hostname string `json:"hostname"`
+}
+
+func NewSession(rw http.ResponseWriter, req *http.Request) {
+ playground := core.PlaygroundFindByDomain(req.Host)
+ if playground == nil {
+ log.Printf("Playground for domain %s was not found!", req.Host)
+ rw.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ req.ParseForm()
+
+ userId := ""
+ if len(config.Providers[playground.Id]) > 0 {
+ cookie, err := ReadCookie(req)
+ if err != nil {
+ // User it not a human
+ rw.WriteHeader(http.StatusForbidden)
+ return
+ }
+ userId = cookie.Id
+ }
+
+ reqDur := req.Form.Get("session-duration")
+ stack := req.Form.Get("stack")
+ stackName := req.Form.Get("stack_name")
+ imageName := req.Form.Get("image_name")
+
+ if stack != "" {
+ stack = formatStack(stack)
+ if ok, err := stackExists(stack); err != nil {
+ log.Printf("Error retrieving stack: %s", err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ } else if !ok {
+ log.Printf("Stack [%s] could not be found", stack)
+ rw.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ }
+
+ var duration time.Duration
+ if reqDur != "" {
+ d, err := time.ParseDuration(reqDur)
+ if err != nil {
+ rw.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ if d > playground.DefaultSessionDuration {
+ log.Printf("Specified session duration was %s but maximum allowed by this playground is %s\n", d.String(), playground.DefaultSessionDuration.String())
+ rw.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ duration = d
+ } else {
+ duration = playground.DefaultSessionDuration
+ }
+
+ sConfig := types.SessionConfig{Playground: playground, UserId: userId, Duration: duration, Stack: stack, StackName: stackName, ImageName: imageName}
+ s, err := core.SessionNew(context.Background(), sConfig)
+ if err != nil {
+ if provisioner.OutOfCapacity(err) {
+ http.Redirect(rw, req, "/ooc", http.StatusFound)
+ return
+ }
+ log.Printf("%#v \n", err)
+ http.Redirect(rw, req, "/500", http.StatusInternalServerError)
+ return
+ //TODO: Return some error code
+ } else {
+ hostname := 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}
+ rw.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(rw).Encode(resp)
+ return
+ }
+
+ http.Redirect(rw, req, fmt.Sprintf("/p/%s", s.Id), http.StatusFound)
+ }
+}
+
+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(stack)
+ if err != nil {
+ return false, err
+ }
+ defer resp.Body.Close()
+
+ return resp.StatusCode == 200, nil
+}
diff --git a/handlers/ping.go b/handlers/ping.go
new file mode 100644
index 0000000000000000000000000000000000000000..5f25e4cdf27a21cfd250b0fffdea42dcd48e0dc6
--- /dev/null
+++ b/handlers/ping.go
@@ -0,0 +1,43 @@
+package handlers
+
+import (
+ "context"
+ "log"
+ "net/http"
+ "time"
+
+ "github.com/docker/docker/client"
+ "github.com/play-with-docker/play-with-docker/config"
+ "github.com/shirou/gopsutil/load"
+)
+
+func Ping(rw http.ResponseWriter, req *http.Request) {
+ defer latencyHistogramVec.WithLabelValues("ping").Observe(float64(time.Since(time.Now()).Nanoseconds()) / 1000000)
+ // Get system load average of the last 5 minutes and compare it against a threashold.
+
+ c, err := client.NewClientWithOpts()
+
+ if err != nil {
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ if _, err := c.Info(ctx); err != nil && err == context.DeadlineExceeded {
+ log.Printf("Docker info took to long to respond\n")
+ rw.WriteHeader(http.StatusGatewayTimeout)
+ return
+ }
+
+ a, err := load.Avg()
+ if err != nil {
+ log.Println("Cannot get system load average!", err)
+ } else {
+ if a.Load5 > config.MaxLoadAvg {
+ log.Printf("System load average is too high [%f]\n", a.Load5)
+ rw.WriteHeader(http.StatusInsufficientStorage)
+ }
+ }
+}
diff --git a/handlers/playground.go b/handlers/playground.go
new file mode 100644
index 0000000000000000000000000000000000000000..2f58923b63e1f381600349cd1bc2d4e9b2de1af8
--- /dev/null
+++ b/handlers/playground.go
@@ -0,0 +1,94 @@
+package handlers
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/http"
+ "time"
+
+ "github.com/play-with-docker/play-with-docker/config"
+ "github.com/play-with-docker/play-with-docker/pwd/types"
+)
+
+func NewPlayground(rw http.ResponseWriter, req *http.Request) {
+ if !ValidateToken(req) {
+ rw.WriteHeader(http.StatusForbidden)
+ return
+ }
+
+ var playground types.Playground
+
+ err := json.NewDecoder(req.Body).Decode(&playground)
+ if err != nil {
+ rw.WriteHeader(http.StatusBadRequest)
+ fmt.Fprintf(rw, "Error creating playground. Got: %v", err)
+ return
+ }
+
+ newPlayground, err := core.PlaygroundNew(playground)
+ if err != nil {
+ rw.WriteHeader(http.StatusBadRequest)
+ fmt.Fprintf(rw, "Error creating playground. Got: %v", err)
+ return
+ }
+
+ json.NewEncoder(rw).Encode(newPlayground)
+}
+
+func ListPlaygrounds(rw http.ResponseWriter, req *http.Request) {
+ if !ValidateToken(req) {
+ rw.WriteHeader(http.StatusForbidden)
+ return
+ }
+
+ playgrounds, err := core.PlaygroundList()
+ if err != nil {
+ log.Printf("Error listing playgrounds. Got: %v\n", err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ json.NewEncoder(rw).Encode(playgrounds)
+}
+
+type PlaygroundConfigurationResponse struct {
+ Id string `json:"id"`
+ Domain string `json:"domain"`
+ DefaultDinDInstanceImage string `json:"default_dind_instance_image"`
+ AvailableDinDInstanceImages []string `json:"available_dind_instance_images"`
+ AllowWindowsInstances bool `json:"allow_windows_instances"`
+ DefaultSessionDuration time.Duration `json:"default_session_duration"`
+ DindVolumeSize string `json:"dind_volume_size"`
+}
+
+func GetCurrentPlayground(rw http.ResponseWriter, req *http.Request) {
+ playground := core.PlaygroundFindByDomain(req.Host)
+ if playground == nil {
+ log.Printf("Playground for domain %s was not found!", req.Host)
+ rw.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ json.NewEncoder(rw).Encode(PlaygroundConfigurationResponse{
+ Id: playground.Id,
+ Domain: playground.Domain,
+ DefaultDinDInstanceImage: playground.DefaultDinDInstanceImage,
+ AvailableDinDInstanceImages: playground.AvailableDinDInstanceImages,
+ AllowWindowsInstances: playground.AllowWindowsInstances,
+ DefaultSessionDuration: playground.DefaultSessionDuration,
+ DindVolumeSize: playground.DindVolumeSize,
+ })
+}
+
+func ValidateToken(req *http.Request) bool {
+ _, password, ok := req.BasicAuth()
+ if !ok {
+ return false
+ }
+
+ if password != config.AdminToken {
+ return false
+ }
+
+ return true
+}
diff --git a/handlers/session_setup.go b/handlers/session_setup.go
new file mode 100644
index 0000000000000000000000000000000000000000..04d05214d4c6668a2a556e4cd8135fc11dcbb9d1
--- /dev/null
+++ b/handlers/session_setup.go
@@ -0,0 +1,50 @@
+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{PlaygroundFQDN: req.Host, DindVolumeSize: "5G"}
+
+ json.NewDecoder(req.Body).Decode(&body)
+
+ s, err := core.SessionGet(sessionId)
+ if err != nil {
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ playground := core.PlaygroundGet(s.PlaygroundId)
+ if playground == nil {
+ log.Printf("Playground with id %s for session %s was not found!", s.PlaygroundId, s.Id)
+ rw.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ if len(playground.DindVolumeSize) > 0 {
+ body.DindVolumeSize = playground.DindVolumeSize
+ }
+
+ body.Privileged = playground.Privileged
+ err = core.SessionSetup(s, body)
+ if err != nil {
+ if pwd.SessionNotEmpty(err) {
+ log.Println("Cannot setup a session that contains instances")
+ rw.WriteHeader(http.StatusConflict)
+ rw.Write([]byte("Cannot setup a session that contains instances"))
+ return
+ }
+ log.Println(err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+}
diff --git a/handlers/terms.go b/handlers/terms.go
new file mode 100644
index 0000000000000000000000000000000000000000..ca72d4709cc7fa80b9ae4b12251992349ea33ba3
--- /dev/null
+++ b/handlers/terms.go
@@ -0,0 +1,240 @@
+package handlers
+
+import (
+ "log"
+ "net"
+ "sync"
+ "time"
+
+ "github.com/play-with-docker/play-with-docker/event"
+ "github.com/play-with-docker/play-with-docker/pwd/types"
+
+ "golang.org/x/text/encoding"
+)
+
+type terminal struct {
+ conn net.Conn
+ write chan []byte
+ instance *types.Instance
+}
+
+func (t *terminal) Go(ch chan info, ech chan *types.Instance) {
+ go func() {
+ for d := range t.write {
+ _, err := t.conn.Write(d)
+ if err != nil {
+ ech <- t.instance
+ return
+ }
+ }
+ }()
+ go func() {
+ encoder := encoding.Replacement.NewEncoder()
+ buf := make([]byte, 1024)
+ for {
+ n, err := t.conn.Read(buf)
+ if err != nil {
+ ech <- t.instance
+ return
+ }
+ b, err := encoder.Bytes(buf[:n])
+ if err != nil {
+ ech <- t.instance
+ return
+ }
+ ch <- info{name: t.instance.Name, data: b}
+ }
+ }()
+}
+
+type info struct {
+ name string
+ data []byte
+}
+
+type state struct {
+ name string
+ status string
+}
+
+type manager struct {
+ session *types.Session
+ sendCh chan info
+ receiveCh chan info
+ stateCh chan state
+ terminals map[string]*terminal
+ errorCh chan *types.Instance
+ instances map[string]*types.Instance
+ sync.Mutex
+}
+
+func (m *manager) Send(name string, data []byte) {
+ m.sendCh <- info{name: name, data: data}
+}
+func (m *manager) Receive(cb func(name string, data []byte)) {
+ for i := range m.receiveCh {
+ cb(i.name, i.data)
+ }
+}
+func (m *manager) Status(cb func(name, status string)) {
+ for s := range m.stateCh {
+ cb(s.name, s.status)
+ }
+}
+
+func (m *manager) connect(instance *types.Instance) error {
+ if !m.trackingInstance(instance) {
+ return nil
+ }
+
+ return m.connectTerminal(instance)
+}
+
+func (m *manager) connectTerminal(instance *types.Instance) error {
+ m.Lock()
+ defer m.Unlock()
+
+ conn, err := core.InstanceGetTerminal(instance)
+ if err != nil {
+ return err
+ }
+ chw := make(chan []byte, 10)
+ t := terminal{conn: conn, write: chw, instance: instance}
+ m.terminals[instance.Name] = &t
+ t.Go(m.receiveCh, m.errorCh)
+ m.stateCh <- state{name: instance.Name, status: "connect"}
+
+ return nil
+}
+
+func (m *manager) disconnectTerminal(instance *types.Instance) {
+ m.Lock()
+ defer m.Unlock()
+
+ t := m.terminals[instance.Name]
+ if t != nil {
+ if t.write != nil {
+ close(t.write)
+ }
+ if t.conn != nil {
+ t.conn.Close()
+ }
+ delete(m.terminals, instance.Name)
+ }
+}
+
+func (m *manager) getTerminal(instanceName string) *terminal {
+ return m.terminals[instanceName]
+}
+
+func (m *manager) trackInstance(instance *types.Instance) {
+ m.Lock()
+ defer m.Unlock()
+
+ m.instances[instance.Name] = instance
+
+}
+func (m *manager) untrackInstance(instance *types.Instance) {
+ m.Lock()
+ defer m.Unlock()
+
+ delete(m.instances, instance.Name)
+}
+func (m *manager) trackingInstance(instance *types.Instance) bool {
+ m.Lock()
+ defer m.Unlock()
+ _, found := m.instances[instance.Name]
+
+ return found
+}
+
+func (m *manager) disconnect(instance *types.Instance) {
+ if !m.trackingInstance(instance) {
+ return
+ }
+
+ m.disconnectTerminal(instance)
+ m.untrackInstance(instance)
+}
+
+func (m *manager) process() {
+ for {
+ select {
+ case i := <-m.sendCh:
+ t := m.getTerminal(i.name)
+ if t != nil {
+ t.write <- i.data
+ }
+ case instance := <-m.errorCh:
+ // check if it still exists before reconnecting
+ i := core.InstanceGet(&types.Session{Id: instance.SessionId}, instance.Name)
+ if i == nil {
+ log.Println("Instance doesn't exist anymore. Won't reconnect")
+ continue
+ }
+ m.stateCh <- state{name: instance.Name, status: "reconnect"}
+ time.AfterFunc(time.Second, func() {
+ m.connect(instance)
+ })
+ }
+ }
+}
+func (m *manager) Close() {
+ for _, i := range m.instances {
+ m.disconnect(i)
+ }
+}
+
+func (m *manager) Start() error {
+ instances, err := core.InstanceFindBySession(m.session)
+ if err != nil {
+ return err
+ }
+ for _, i := range instances {
+ m.instances[i.Name] = i
+ m.connect(i)
+ }
+ go m.process()
+ return nil
+}
+
+func NewManager(s *types.Session) (*manager, error) {
+ m := &manager{
+ session: s,
+ sendCh: make(chan info, 10),
+ receiveCh: make(chan info, 10),
+ stateCh: make(chan state, 10),
+ terminals: make(map[string]*terminal),
+ errorCh: make(chan *types.Instance, 10),
+ instances: make(map[string]*types.Instance),
+ }
+
+ e.On(event.INSTANCE_NEW, func(sessionId string, args ...interface{}) {
+ if sessionId != s.Id {
+ return
+ }
+
+ // There is a new instance in a session we are tracking. We should track it's terminal
+ instanceName := args[0].(string)
+ instance := core.InstanceGet(s, instanceName)
+ if instance == nil {
+ log.Printf("Instance [%s] was not found in session [%s]\n", instanceName, sessionId)
+ return
+ }
+ m.trackInstance(instance)
+ m.connect(instance)
+ })
+
+ e.On(event.INSTANCE_DELETE, func(sessionId string, args ...interface{}) {
+ if sessionId != s.Id {
+ return
+ }
+
+ // There is a new instance in a session we are tracking. We should track it's terminal
+ instanceName := args[0].(string)
+ instance := &types.Instance{Name: instanceName}
+ m.disconnect(instance)
+ })
+
+ return m, nil
+}
diff --git a/handlers/user.go b/handlers/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..577adc997fe9cd2253421bb0448025df29e9eb82
--- /dev/null
+++ b/handlers/user.go
@@ -0,0 +1,36 @@
+package handlers
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+
+ "github.com/gorilla/mux"
+ "github.com/play-with-docker/play-with-docker/storage"
+)
+
+type PublicUserInfo struct {
+ Id string `json:"id"`
+ Avatar string `json:"avatar"`
+ Name string `json:"name"`
+}
+
+func GetUser(rw http.ResponseWriter, req *http.Request) {
+ vars := mux.Vars(req)
+ userId := vars["userId"]
+
+ u, err := core.UserGet(userId)
+ if err != nil {
+ if storage.NotFound(err) {
+ log.Printf("User with id %s was not found\n", userId)
+ rw.WriteHeader(http.StatusNotFound)
+ return
+ }
+ log.Println(err)
+ rw.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ pui := PublicUserInfo{Id: u.Id, Avatar: u.Avatar, Name: u.Name}
+ json.NewEncoder(rw).Encode(pui)
+}
diff --git a/handlers/ws.go b/handlers/ws.go
new file mode 100644
index 0000000000000000000000000000000000000000..fbf850af573493d4477062aa50e499308ce77ed6
--- /dev/null
+++ b/handlers/ws.go
@@ -0,0 +1,210 @@
+package handlers
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/http"
+ "sync"
+
+ "github.com/gorilla/mux"
+ "github.com/gorilla/websocket"
+ "github.com/play-with-docker/play-with-docker/event"
+ "github.com/play-with-docker/play-with-docker/storage"
+ "github.com/satori/go.uuid"
+)
+
+var upgrader = websocket.Upgrader{
+ CheckOrigin: func(r *http.Request) bool { return true },
+}
+
+type message struct {
+ Name string `json:"name"`
+ Args []interface{} `json:"args"`
+}
+
+type socket struct {
+ c *websocket.Conn
+ mx sync.Mutex
+ listeners map[string][]func(args ...interface{})
+ r *http.Request
+ id string
+ closed bool
+}
+
+func newSocket(r *http.Request, c *websocket.Conn) *socket {
+ return &socket{
+ c: c,
+ listeners: map[string][]func(args ...interface{}){},
+ r: r,
+ id: uuid.NewV4().String(),
+ }
+}
+
+func (s *socket) Id() string {
+ return s.id
+}
+
+func (s *socket) Request() *http.Request {
+ return s.r
+}
+
+func (s *socket) Close() {
+ s.closed = true
+ s.onMessage(message{Name: "close"})
+}
+
+func (s *socket) process() {
+ defer s.Close()
+ for {
+ mt, m, err := s.c.ReadMessage()
+ if err != nil {
+ log.Printf("Error reading message from websocket. Got: %v\n", err)
+ break
+ }
+ if mt != websocket.TextMessage {
+ log.Printf("Received websocket message, but it is not a text message.\n")
+ continue
+ }
+ go func() {
+ var msg message
+ if err := json.Unmarshal(m, &msg); err != nil {
+ log.Printf("Cannot unmarshal message received from websocket. Got: %v\n", err)
+ return
+ }
+ s.onMessage(msg)
+ }()
+ }
+}
+
+func (s *socket) onMessage(msg message) {
+ s.mx.Lock()
+ defer s.mx.Unlock()
+
+ cbs, found := s.listeners[msg.Name]
+ if !found {
+ return
+ }
+ for _, cb := range cbs {
+ go cb(msg.Args...)
+ }
+}
+
+func (s *socket) Emit(ev string, args ...interface{}) {
+ s.mx.Lock()
+ defer s.mx.Unlock()
+
+ if s.closed {
+ return
+ }
+
+ m := message{Name: ev, Args: args}
+ b, err := json.Marshal(m)
+ if err != nil {
+ log.Printf("Cannot marshal event to json. Got: %v\n", err)
+ return
+ }
+ if err := s.c.WriteMessage(websocket.TextMessage, b); err != nil {
+ log.Printf("Cannot write event to websocket connection. Got: %v\n", err)
+ s.Close()
+ return
+ }
+}
+
+func (s *socket) On(ev string, cb func(args ...interface{})) {
+ s.mx.Lock()
+ defer s.mx.Unlock()
+ listeners, found := s.listeners[ev]
+ if !found {
+ listeners = []func(args ...interface{}){}
+ }
+ listeners = append(listeners, cb)
+ s.listeners[ev] = listeners
+}
+
+func WSH(w http.ResponseWriter, r *http.Request) {
+ c, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ log.Print("upgrade:", err)
+ return
+ }
+ defer c.Close()
+
+ s := newSocket(r, c)
+ ws(s)
+ s.process()
+}
+
+func ws(so *socket) {
+ defer func() {
+ if r := recover(); r != nil {
+ fmt.Println("Recovered from ", r)
+ }
+ }()
+ vars := mux.Vars(so.Request())
+
+ sessionId := vars["sessionId"]
+
+ session, err := core.SessionGet(sessionId)
+ if err == storage.NotFoundError {
+ log.Printf("Session with id [%s] does not exist!\n", sessionId)
+ return
+ }
+
+ client := core.ClientNew(so.Id(), session)
+ if client == nil {
+ log.Printf("ERROR: Client was not created for session id %s and socket id %s\n", session.Id, so.Id())
+ }
+
+ m, err := NewManager(session)
+ if err != nil {
+ log.Printf("Error creating terminal manager. Got: %v", err)
+ return
+ }
+
+ go m.Receive(func(name string, data []byte) {
+ so.Emit("instance terminal out", name, string(data))
+ })
+ go m.Status(func(name, status string) {
+ so.Emit("instance terminal status", name, status)
+ })
+
+ err = m.Start()
+ if err != nil {
+ log.Println(err)
+ return
+ }
+
+ so.On("session close", func(args ...interface{}) {
+ m.Close()
+ core.SessionClose(session)
+ })
+
+ so.On("instance terminal in", func(args ...interface{}) {
+ if len(args) == 2 && args[0] != nil && args[1] != nil {
+ name := args[0].(string)
+ data := args[1].(string)
+ m.Send(name, []byte(data))
+ }
+ })
+
+ so.On("instance viewport resize", func(args ...interface{}) {
+ if len(args) == 2 && args[0] != nil && args[1] != nil {
+ // User resized his viewport
+ cols := args[0].(float64)
+ rows := args[1].(float64)
+ core.ClientResizeViewPort(client, uint(cols), uint(rows))
+ }
+ })
+
+ so.On("close", func(args ...interface{}) {
+ m.Close()
+ core.ClientClose(client)
+ })
+
+ e.OnAny(func(eventType event.EventType, sessionId string, args ...interface{}) {
+ if session.Id == sessionId {
+ so.Emit(eventType.String(), args...)
+ }
+ })
+}
diff --git a/handlers/www/503.html b/handlers/www/503.html
new file mode 100644
index 0000000000000000000000000000000000000000..843f965400368fad5bd97f2bf78da83fe5d084ea
--- /dev/null
+++ b/handlers/www/503.html
@@ -0,0 +1,23 @@
+
+
+
+ Docker Playground
+
+
+
+
+
+
+
+
+
+ An error has occurred. If you have some time, please report it. Thanks!
+
+
+
diff --git a/handlers/www/assets/app.js b/handlers/www/assets/app.js
new file mode 100644
index 0000000000000000000000000000000000000000..f1e9201db58495497a3179e87545c97ba6e610de
--- /dev/null
+++ b/handlers/www/assets/app.js
@@ -0,0 +1,928 @@
+(function() {
+ 'use strict';
+
+ var app = angular.module('DockerPlay', ['ngMaterial', 'ngFileUpload', 'ngclipboard']);
+
+ // Automatically redirects user to a new session when bypassing captcha.
+ // Controller keeps code/logic separate from the HTML
+ app.controller("BypassController", ['$scope', '$log', '$http', '$location', '$timeout', function($scope, $log, $http, $location, $timeout) {
+ setTimeout(function() {
+ document.getElementById("welcomeFormBypass").submit();
+ }, 500);
+ }]);
+
+ function SessionBuilderModalController($mdDialog, $scope) {
+ $scope.createBuilderTerminal();
+
+ $scope.closeSessionBuilder = function() {
+ $mdDialog.cancel();
+ }
+ }
+
+ app.controller('PlayController', ['$scope', '$rootScope', '$log', '$http', '$location', '$timeout', '$mdDialog', '$window', 'TerminalService', 'KeyboardShortcutService', 'InstanceService', 'SessionService', 'Upload', function($scope, $rootScope, $log, $http, $location, $timeout, $mdDialog, $window, TerminalService, KeyboardShortcutService, InstanceService, SessionService, Upload) {
+ $scope.sessionId = SessionService.getCurrentSessionId();
+ $rootScope.instances = [];
+ $scope.idx = {};
+ $scope.host = window.location.host;
+ $scope.idxByHostname = {};
+ $rootScope.selectedInstance = null;
+ $scope.isAlive = true;
+ $scope.ttl = '--:--:--';
+ $scope.connected = false;
+ $scope.type = {windows: false};
+ $scope.isInstanceBeingCreated = false;
+ $scope.newInstanceBtnText = '+ Add new instance';
+ $scope.deleteInstanceBtnText = 'Delete';
+ $scope.isInstanceBeingDeleted = false;
+ $scope.uploadProgress = 0;
+
+ $scope.uploadFiles = function (files, invalidFiles) {
+ let total = files.length;
+ let uploadFile = function() {
+ let file = files.shift();
+ if (!file){
+ $scope.uploadMessage = "";
+ $scope.uploadProgress = 0;
+ return
+ }
+ $scope.uploadMessage = "Uploading file(s) " + (total - files.length) + "/"+ total + " : " + file.name;
+ let upload = Upload.upload({url: '/sessions/' + $scope.sessionId + '/instances/' + $rootScope.selectedInstance.name + '/uploads', data: {file: file}, method: 'POST'})
+ .then(function(){}, function(){}, function(evt) {
+ $scope.uploadProgress = parseInt(100.0 * evt.loaded / evt.total);
+ });
+
+ // process next file
+ upload.finally(uploadFile);
+ }
+
+ uploadFile();
+ }
+
+ var selectedKeyboardShortcuts = KeyboardShortcutService.getCurrentShortcuts();
+
+ $scope.resizeHandler = null;
+
+ angular.element($window).bind('resize', function() {
+ if ($rootScope.selectedInstance) {
+ if (!$scope.resizeHandler) {
+ $scope.resizeHandler = setTimeout(function() {
+ $scope.resizeHandler = null
+ $scope.resize($scope.selectedInstance.term.proposeGeometry());
+ }, 1000);
+ }
+ }
+ });
+
+ $scope.$on("settings:shortcutsSelected", function(e, preset) {
+ selectedKeyboardShortcuts = preset;
+ });
+
+
+ $scope.showAlert = function(title, content, parent, cb) {
+ $mdDialog.show(
+ $mdDialog.alert()
+ .parent(angular.element(document.querySelector(parent || '#popupContainer')))
+ .clickOutsideToClose(true)
+ .title(title)
+ .textContent(content)
+ .ok('Got it!')
+ ).finally(function() {
+ if (cb) {
+ cb();
+ }
+ });
+ }
+
+ $scope.resize = function(geometry) {
+ $scope.socket.emit('instance viewport resize', geometry.cols, geometry.rows);
+ }
+
+ KeyboardShortcutService.setResizeFunc($scope.resize);
+
+ $scope.closeSession = function() {
+ // Remove alert before closing browser tab
+ window.onbeforeunload = null;
+ $scope.socket.emit('session close');
+ }
+
+ $scope.upsertInstance = function(info) {
+ var i = info;
+ if (!$scope.idx[i.name]) {
+ $rootScope.instances.push(i);
+ i.buffer = '';
+ $scope.idx[i.name] = i;
+ $scope.idxByHostname[i.hostname] = i;
+ } else {
+ $scope.idx[i.name] = Object.assign($scope.idx[i.name], info);
+ }
+
+ return $scope.idx[i.name];
+ }
+
+ $scope.newInstance = function() {
+ updateNewInstanceBtnState(true);
+ var instanceType = $scope.type.windows ? 'windows': 'linux';
+ $http({
+ method: 'POST',
+ url: '/sessions/' + $scope.sessionId + '/instances',
+ data : { ImageName : InstanceService.getDesiredImage(), type: instanceType }
+ }).then(function(response) {
+ $scope.upsertInstance(response.data);
+ }, function(response) {
+ if (response.status == 409) {
+ $scope.showAlert('Max instances reached', 'Maximum number of instances reached')
+ } else if (response.status == 503 && response.data.error == 'out_of_capacity') {
+ $scope.showAlert('Out Of Capacity', 'We are really sorry. But we are currently out of capacity and cannot create new instances. Please try again later.')
+ }
+ }).finally(function() {
+ updateNewInstanceBtnState(false);
+ });
+ }
+
+ $scope.setSessionState = function(state) {
+ $scope.ready = state;
+
+ if (!state) {
+ $mdDialog.show({
+ onComplete: function(){SessionBuilderModalController($mdDialog, $scope)},
+ contentElement: '#builderDialog',
+ parent: angular.element(document.body),
+ clickOutsideToClose: false,
+ scope: $scope,
+ preserveScope: true
+ });
+ }
+ }
+
+ $scope.loadPlaygroundConf = function() {
+ $http({
+ method: 'GET',
+ url: '/my/playground',
+ }).then(function(response) {
+ $scope.playground = response.data;
+ });
+
+ }
+ $scope.getSession = function(sessionId) {
+ $http({
+ method: 'GET',
+ url: '/sessions/' + $scope.sessionId,
+ }).then(function(response) {
+ $scope.setSessionState(response.data.ready);
+
+ if (response.data.created_at) {
+ $scope.expiresAt = moment(response.data.expires_at);
+ setInterval(function() {
+ $scope.ttl = moment.utc($scope.expiresAt.diff(moment())).format('HH:mm:ss');
+ $scope.$apply();
+ }, 1000);
+ }
+
+ var i = response.data;
+ for (var k in i.instances) {
+ var instance = i.instances[k];
+ $rootScope.instances.push(instance);
+ $scope.idx[instance.name] = instance;
+ $scope.idxByHostname[instance.hostname] = instance;
+ }
+
+ var base = '';
+ if (window.location.protocol == 'http:') {
+ base = 'ws://';
+ } else {
+ base = 'wss://';
+ }
+ base += window.location.host;
+ if (window.location.port) {
+ base += ':' + window.location.port;
+ }
+
+ var socket = new ReconnectingWebSocket(base + '/sessions/' + sessionId + '/ws/', null, {reconnectInterval: 1000});
+ socket.listeners = {};
+
+ socket.on = function(name, cb) {
+ if (!socket.listeners[name]) {
+ socket.listeners[name] = [];
+ }
+ socket.listeners[name].push(cb);
+ }
+
+ socket.emit = function() {
+ var name = arguments[0]
+ var args = [];
+ for (var i = 1; i < arguments.length; i++) {
+ args.push(arguments[i]);
+ }
+ socket.send(JSON.stringify({name: name, args: args}));
+ }
+
+ socket.addEventListener('open', function (event) {
+ $scope.connected = true;
+ for (var i in $rootScope.instances) {
+ var instance = $rootScope.instances[i];
+ if (instance.term) {
+ instance.term.setOption('disableStdin', false);
+ }
+ }
+ });
+ socket.addEventListener('close', function (event) {
+ $scope.connected = false;
+ for (var i in $rootScope.instances) {
+ var instance = $rootScope.instances[i];
+ if (instance.term) {
+ instance.term.setOption('disableStdin', true);
+ }
+ }
+ });
+ socket.addEventListener('message', function (event) {
+ var m = JSON.parse(event.data);
+ var ls = socket.listeners[m.name];
+ if (ls) {
+ for (var i=0; i 0) {
+ // if no instance has been passed, select the first.
+ $scope.showInstance($rootScope.instances[0]);
+ }
+ }, function(response) {
+ if (response.status == 404) {
+ document.write('session not found');
+ return
+ }
+ });
+ }
+
+ $scope.openPort = function(instance) {
+ var port = prompt('What port would you like to open?');
+ if (!port) return;
+
+ var url = $scope.getProxyUrl(instance, port);
+ window.open(url, '_blank');
+ }
+
+ $scope.getProxyUrl = function(instance, port) {
+ var url = 'http://' + instance.proxy_host + '-' + port + '.direct.' + $scope.host;
+
+ return url;
+ }
+
+ $scope.showInstance = function(instance) {
+ $rootScope.selectedInstance = instance;
+ $location.hash(instance.name);
+ if (!instance.term) {
+ $timeout(function() {
+ createTerminal(instance);
+ TerminalService.setFontSize(TerminalService.getFontSize());
+ instance.term.focus();
+ $timeout(function() {
+ }, 0, false);
+ }, 0, false);
+ return
+ }
+ instance.term.focus();
+ }
+
+ $scope.removeInstance = function(name) {
+ if ($scope.idx[name]) {
+ var handler = $scope.idx[name].terminalBufferInterval;
+ clearInterval(handler);
+ }
+ if ($scope.idx[name]) {
+ delete $scope.idx[name];
+ $rootScope.instances = $rootScope.instances.filter(function(i) {
+ return i.name != name;
+ });
+ if ($rootScope.instances.length) {
+ $scope.showInstance($rootScope.instances[0]);
+ }
+ }
+ }
+
+ $scope.deleteInstance = function(instance) {
+ updateDeleteInstanceBtnState(true);
+ $http({
+ method: 'DELETE',
+ url: '/sessions/' + $scope.sessionId + '/instances/' + instance.name,
+ }).then(function(response) {
+ $scope.removeInstance(instance.name);
+ }, function(response) {
+ console.log('error', response);
+ }).finally(function() {
+ updateDeleteInstanceBtnState(false);
+ });
+ };
+
+ $scope.openEditor = function(instance) {
+ var w = window.screen.availWidth * 45 / 100;
+ var h = window.screen.availHeight * 45 / 100;
+ $window.open('/sessions/' + instance.session_id + '/instances/'+instance.name+'/editor', 'editor',
+ 'width='+w+',height='+h+',resizable,scrollbars=yes,status=1');
+ };
+
+ $scope.loadPlaygroundConf();
+ $scope.getSession($scope.sessionId);
+
+ $scope.createBuilderTerminal = function() {
+ var builderTerminalContainer = document.getElementById('builder-terminal');
+ let term = new Terminal({
+ cursorBlink: false
+ });
+
+ term.open(builderTerminalContainer);
+ $scope.builderTerminal = term;
+ }
+ function createTerminal(instance, cb) {
+ if (instance.term) {
+ return instance.term;
+ }
+
+ var terminalContainer = document.getElementById('terminal-' + instance.name);
+
+ var term = new Terminal({
+ cursorBlink: false,
+ screenReaderMode: true
+ });
+
+ term.open(terminalContainer);
+
+
+ const handleCopy = (e) => {
+ // Ctrl + Alt + C
+ if (e.ctrlKey && e.altKey && (e.keyCode == 67)) {
+ document.execCommand('copy');
+ return false;
+ }
+ };
+
+ term.attachCustomKeyEventHandler(function(e) {
+ // handleCopy(e);
+ if (selectedKeyboardShortcuts == null) return;
+
+ var presets = selectedKeyboardShortcuts.presets
+ .filter(function(preset) { return preset.keyCode == e.keyCode })
+ .filter(function(preset) { return (preset.metaKey == undefined && !e.metaKey) || preset.metaKey == e.metaKey })
+ .filter(function(preset) { return (preset.ctrlKey == undefined && !e.ctrlKey) || preset.ctrlKey == e.ctrlKey })
+ .filter(function(preset) { return (preset.altKey == undefined && !e.altKey) || preset.altKey == e.altKey })
+ .forEach(function(preset) { preset.action({ terminal : term, e })});
+ });
+
+ // Set geometry during the next tick, to avoid race conditions.
+
+ setTimeout(function() {
+ $scope.resize(term.proposeGeometry());
+ }, 0);
+
+ instance.terminalBuffer = '';
+ instance.terminalBufferInterval = setInterval(function() {
+ if (instance.terminalBuffer.length > 0) {
+ $scope.socket.emit('instance terminal in', instance.name, instance.terminalBuffer);
+ instance.terminalBuffer = '';
+ }
+ }, 70);
+ term.on('data', function(d) {
+ instance.terminalBuffer += d;
+ });
+
+ instance.term = term;
+
+ if (cb) {
+ cb();
+ }
+ }
+
+ function updateNewInstanceBtnState(isInstanceBeingCreated) {
+ if (isInstanceBeingCreated === true) {
+ $scope.newInstanceBtnText = '+ Creating...';
+ $scope.isInstanceBeingCreated = true;
+ } else {
+ $scope.newInstanceBtnText = '+ Add new instance';
+ $scope.isInstanceBeingCreated = false;
+ }
+ }
+
+ function updateDeleteInstanceBtnState(isInstanceBeingDeleted) {
+ if (isInstanceBeingDeleted === true) {
+ $scope.deleteInstanceBtnText = 'Deleting...';
+ $scope.isInstanceBeingDeleted = true;
+ } else {
+ $scope.deleteInstanceBtnText = 'Delete';
+ $scope.isInstanceBeingDeleted = false;
+ }
+ }
+ }])
+ .config(['$mdIconProvider', '$locationProvider', '$mdThemingProvider', function($mdIconProvider, $locationProvider, $mdThemingProvider) {
+ $locationProvider.html5Mode({enabled: true, requireBase: false});
+ $mdIconProvider.defaultIconSet('../assets/social-icons.svg', 24);
+ $mdThemingProvider.theme('kube')
+ .primaryPalette('grey')
+ .accentPalette('grey');
+ }])
+ .component('settingsIcon', {
+ template : "settings",
+ controller : function($mdDialog) {
+ var $ctrl = this;
+ $ctrl.onClick = function() {
+ $mdDialog.show({
+ controller : function() {},
+ template : "",
+ parent: angular.element(document.body),
+ clickOutsideToClose : true
+ })
+ }
+ }
+ })
+ .component('templatesIcon', {
+ template : "build",
+ controller : function($mdDialog) {
+ var $ctrl = this;
+ $ctrl.onClick = function() {
+ $mdDialog.show({
+ controller : function() {},
+ template : "",
+ parent: angular.element(document.body),
+ clickOutsideToClose : true
+ })
+ }
+ }
+ })
+ .component("templatesDialog", {
+ templateUrl : "templates-modal.html",
+ controller : function($mdDialog, $scope, SessionService) {
+ var $ctrl = this;
+ $scope.building = false;
+ $scope.templates = SessionService.getAvailableTemplates();
+ $ctrl.close = function() {
+ $mdDialog.cancel();
+ }
+ $ctrl.setupSession = function(setup) {
+ $scope.building = true;
+ SessionService.setup(setup, function(err) {
+ $scope.building = false;
+ if (err) {
+ $scope.errorMessage = err;
+ return;
+ }
+ $ctrl.close();
+ });
+ }
+ }
+ })
+ .component("settingsDialog", {
+ templateUrl : "settings-modal.html",
+ controller : function($mdDialog, KeyboardShortcutService, $rootScope, InstanceService, TerminalService) {
+ var $ctrl = this;
+ $ctrl.$onInit = function() {
+ $ctrl.keyboardShortcutPresets = KeyboardShortcutService.getAvailablePresets();
+ $ctrl.selectedShortcutPreset = KeyboardShortcutService.getCurrentShortcuts();
+ $ctrl.instanceImages = InstanceService.getAvailableImages();
+ $ctrl.selectedInstanceImage = InstanceService.getDesiredImage();
+ $ctrl.terminalFontSizes = TerminalService.getFontSizes();
+ };
+
+ $ctrl.currentShortcutConfig = function(value) {
+ if (value !== undefined) {
+ value = JSON.parse(value);
+ KeyboardShortcutService.setCurrentShortcuts(value);
+ $ctrl.selectedShortcutPreset = angular.copy(KeyboardShortcutService.getCurrentShortcuts());
+ $rootScope.$broadcast('settings:shortcutsSelected', $ctrl.selectedShortcutPreset);
+ }
+ return JSON.stringify(KeyboardShortcutService.getCurrentShortcuts());
+ };
+
+ $ctrl.currentDesiredInstanceImage = function(value) {
+ if (value !== undefined) {
+ InstanceService.setDesiredImage(value);
+ }
+ return InstanceService.getDesiredImage(value);
+ };
+ $ctrl.currentTerminalFontSize = function(value) {
+ if (value !== undefined) {
+ // set font size
+ TerminalService.setFontSize(value);
+ return;
+ }
+
+ return TerminalService.getFontSize();
+ }
+
+ $ctrl.close = function() {
+ $mdDialog.cancel();
+ }
+ }
+ })
+ .service("SessionService", function($http) {
+ var templates = [
+ {
+ title: '3 Managers and 2 Workers',
+ icon: '/assets/swarm.png',
+ setup: {
+ instances: [
+ {hostname: 'manager1', is_swarm_manager: true},
+ {hostname: 'manager2', is_swarm_manager: true},
+ {hostname: 'manager3', is_swarm_manager: true},
+ {hostname: 'worker1', is_swarm_worker: true},
+ {hostname: 'worker2', is_swarm_worker: true}
+ ]
+ }
+ },
+ {
+ title: '5 Managers and no workers',
+ icon: '/assets/swarm.png',
+ setup: {
+ instances: [
+ {hostname: 'manager1', is_swarm_manager: true},
+ {hostname: 'manager2', is_swarm_manager: true},
+ {hostname: 'manager3', is_swarm_manager: true},
+ {hostname: 'manager4', is_swarm_manager: true},
+ {hostname: 'manager5', is_swarm_manager: true}
+ ]
+ }
+ },
+ {
+ title: '1 Manager and 1 Worker',
+ icon: '/assets/swarm.png',
+ setup: {
+ instances: [
+ {hostname: 'manager1', is_swarm_manager: true},
+ {hostname: 'worker1', is_swarm_worker: true}
+ ]
+ }
+ }
+ ];
+
+ return {
+ getAvailableTemplates: getAvailableTemplates,
+ getCurrentSessionId: getCurrentSessionId,
+ setup: setup,
+ };
+
+ function getCurrentSessionId() {
+ return window.location.pathname.replace('/p/', '');
+ }
+ function getAvailableTemplates() {
+ return templates;
+ }
+ function setup(plan, cb) {
+ return $http
+ .post("/sessions/" + getCurrentSessionId() + "/setup", plan)
+ .then(function(response) {
+ if (cb) cb();
+ }, function(response) {
+ if (cb) cb(response.data);
+ });
+ }
+ })
+ .service("InstanceService", function($http) {
+ var instanceImages = [];
+ _prepopulateAvailableImages();
+
+ return {
+ getAvailableImages : getAvailableImages,
+ setDesiredImage : setDesiredImage,
+ getDesiredImage : getDesiredImage,
+ };
+
+ function getAvailableImages() {
+ return instanceImages;
+ }
+
+ function getDesiredImage() {
+ var image = localStorage.getItem("settings.desiredImage");
+ if (image == null)
+ return instanceImages[0];
+ return image;
+ }
+
+ function setDesiredImage(image) {
+ if (image === null)
+ localStorage.removeItem("settings.desiredImage");
+ else
+ localStorage.setItem("settings.desiredImage", image);
+ }
+
+ function _prepopulateAvailableImages() {
+ return $http
+ .get("/instances/images")
+ .then(function(response) {
+ instanceImages = response.data;
+ });
+ }
+
+ })
+ .run(function(InstanceService) { /* forcing pre-populating for now */ })
+ .service("KeyboardShortcutService", ['TerminalService', function(TerminalService) {
+ var resizeFunc;
+
+ return {
+ getAvailablePresets : getAvailablePresets,
+ getCurrentShortcuts : getCurrentShortcuts,
+ setCurrentShortcuts : setCurrentShortcuts,
+ setResizeFunc : setResizeFunc
+ };
+
+ function setResizeFunc(f) {
+ resizeFunc = f;
+ }
+
+ function getAvailablePresets() {
+ return [
+ {
+ name : "None",
+ presets : [
+ {
+ description : "Toggle terminal fullscreen", command : "Alt+enter", altKey : true, keyCode : 13, action : function(context) { TerminalService.toggleFullScreen(context.terminal, resizeFunc); }
+ },
+ {
+ description: "Increase Font Size",
+ command: "Ctrl++",
+ ctrlKey : true,
+ keyCode: 187,
+ action: function(context) {
+ TerminalService.increaseFontSize();
+ context.e.preventDefault()
+ }
+ },
+ {
+ description: "Decrease Font Size",
+ command: "Ctrl+-",
+ ctrlKey: true,
+ keyCode: 189,
+ action: function(context) {
+ context.e.preventDefault()
+ TerminalService.decreaseFontSize();
+ }
+ }
+ ]
+ },
+ {
+ name : "Mac OSX",
+ presets : [
+ { description : "Clear terminal", command : "Cmd+K", metaKey : true, keyCode : 75, action : function(context) { context.terminal.clear(); }},
+ { description : "Toggle terminal fullscreen", command : "Alt+enter", altKey : true, keyCode : 13, action : function(context) { TerminalService.toggleFullScreen(context.terminal, resizeFunc); }},
+ {
+ description: "Increase Font Size",
+ command: "Cmd++",
+ metaKey : true,
+ keyCode: 187,
+ action: function(context) {
+ TerminalService.increaseFontSize();
+ context.e.preventDefault()
+ }
+ },
+ {
+ description: "Decrease Font Size",
+ command: "Cmd+-",
+ metaKey: true,
+ keyCode: 189,
+ action: function(context) {
+ context.e.preventDefault()
+ TerminalService.decreaseFontSize();
+ }
+ }
+ ]
+ }
+ ]
+ }
+
+ function getCurrentShortcuts() {
+ var shortcuts = localStorage.getItem("shortcut-preset-name");
+ if (shortcuts == null) {
+ shortcuts = getDefaultShortcutPrefixName();
+ if (shortcuts == null)
+ return null;
+ }
+
+ var preset = getAvailablePresets()
+ .filter(function(preset) { return preset.name == shortcuts; });
+ if (preset.length == 0)
+ console.error("Unable to find preset with name '" + shortcuts + "'");
+ return preset[0];
+ return (shortcuts == null) ? null : JSON.parse(shortcuts);
+ }
+
+ function setCurrentShortcuts(config) {
+ localStorage.setItem("shortcut-preset-name", config.name);
+ }
+
+ function getDefaultShortcutPrefixName() {
+ if (window.navigator.platform.toUpperCase().indexOf('MAC') >= 0)
+ return "Mac OSX";
+ return "None";
+ }
+ }])
+ .service('TerminalService', ['$window', '$rootScope', function($window, $rootScope) {
+ var fullscreen;
+ var fontSize = getFontSize();
+ return {
+ getFontSizes : getFontSizes,
+ setFontSize : setFontSize,
+ getFontSize : getFontSize,
+ increaseFontSize : increaseFontSize,
+ decreaseFontSize : decreaseFontSize,
+ toggleFullScreen : toggleFullScreen
+ };
+ function getFontSizes() {
+ var terminalFontSizes = [];
+ for (var i=3; i<40; i++) {
+ terminalFontSizes.push(i+'px');
+ }
+ return terminalFontSizes;
+ };
+ function getFontSize() {
+ if($rootScope.selectedInstance){
+ return $rootScope.selectedInstance.term.getOption("fontSize") + "px"
+ }else{
+ return $(".terminal").css("font-size")
+ }
+ }
+ function setFontSize(value) {
+ const { term }= $rootScope.selectedInstance;
+ fontSize = value;
+ var size = parseInt(value);
+ term.setOption("fontSize", size)
+ term.resize(1,1)
+ term.fit()
+ }
+ function increaseFontSize() {
+ var sizes = getFontSizes();
+ var size = getFontSize();
+ var i = sizes.indexOf(size);
+ if (i == -1) {
+ return;
+ }
+ if (i+1 > sizes.length) {
+ return;
+ }
+ setFontSize(sizes[i+1]);
+ }
+ function decreaseFontSize() {
+ var sizes = getFontSizes();
+ var size = getFontSize();
+ var i = sizes.indexOf(size);
+ if (i == -1) {
+ return;
+ }
+ if (i-1 < 0) {
+ return;
+ }
+ setFontSize(sizes[i-1]);
+ }
+ function toggleFullScreen(terminal, resize) {
+ if(fullscreen) {
+ terminal.toggleFullScreen();
+ terminal.containerElement.append(terminal.element)
+ setTimeout(()=>{
+ terminal.resize(1,1);
+ terminal.fit();
+ terminal.focus();
+ },100)
+ fullscreen = null;
+ } else {
+ // save the current parent
+ terminal.containerElement = $(terminal.element).parent()
+ $("body").append(terminal.element)
+ fullscreen = terminal.proposeGeometry();
+ terminal.toggleFullScreen();
+ terminal.fit();
+ terminal.focus();
+ }
+ }
+ }]);
+})();
diff --git a/handlers/www/assets/attach.js b/handlers/www/assets/attach.js
new file mode 100644
index 0000000000000000000000000000000000000000..2ef1f7378c97bc69aa05d36537934c82e46343d5
--- /dev/null
+++ b/handlers/www/assets/attach.js
@@ -0,0 +1,134 @@
+/*
+ * Implements the attach method, that
+ * attaches the terminal to a WebSocket stream.
+ *
+ * The bidirectional argument indicates, whether the terminal should
+ * send data to the socket as well and is true, by default.
+ */
+
+(function (attach) {
+ if (typeof exports === 'object' && typeof module === 'object') {
+ /*
+ * CommonJS environment
+ */
+ module.exports = attach(require('../../src/xterm'));
+ } else if (typeof define == 'function') {
+ /*
+ * Require.js is available
+ */
+ define(['../../src/xterm'], attach);
+ } else {
+ /*
+ * Plain browser environment
+ */
+ attach(window.Terminal);
+ }
+})(function (Xterm) {
+ 'use strict';
+
+ /**
+ * This module provides methods for attaching a terminal to a WebSocket
+ * stream.
+ *
+ * @module xterm/addons/attach/attach
+ */
+ var exports = {};
+
+ /**
+ * Attaches the given terminal to the given socket.
+ *
+ * @param {Xterm} term - The terminal to be attached to the given socket.
+ * @param {WebSocket} socket - The socket to attach the current terminal.
+ * @param {boolean} bidirectional - Whether the terminal should send data
+ * to the socket as well.
+ * @param {boolean} buffered - Whether the rendering of incoming data
+ * should happen instantly or at a maximum
+ * frequency of 1 rendering per 10ms.
+ */
+ exports.attach = function (term, socket, bidirectional, buffered) {
+ bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
+ term.socket = socket;
+
+ term._flushBuffer = function () {
+ term.write(term._attachSocketBuffer);
+ term._attachSocketBuffer = null;
+ clearTimeout(term._attachSocketBufferTimer);
+ term._attachSocketBufferTimer = null;
+ };
+
+ term._pushToBuffer = function (data) {
+ if (term._attachSocketBuffer) {
+ term._attachSocketBuffer += data;
+ } else {
+ term._attachSocketBuffer = data;
+ setTimeout(term._flushBuffer, 10);
+ }
+ };
+
+ term._getMessage = function (ev) {
+ if (buffered) {
+ term._pushToBuffer(ev.data);
+ } else {
+ term.write(ev.data);
+ }
+ };
+
+ term._sendData = function (data) {
+ socket.send(data);
+ };
+
+ socket.addEventListener('message', term._getMessage);
+
+ if (bidirectional) {
+ term.on('data', term._sendData);
+ }
+
+ socket.addEventListener('close', term.detach.bind(term, socket));
+ socket.addEventListener('error', term.detach.bind(term, socket));
+ };
+
+ /**
+ * Detaches the given terminal from the given socket
+ *
+ * @param {Xterm} term - The terminal to be detached from the given socket.
+ * @param {WebSocket} socket - The socket from which to detach the current
+ * terminal.
+ */
+ exports.detach = function (term, socket) {
+ term.off('data', term._sendData);
+
+ socket = (typeof socket == 'undefined') ? term.socket : socket;
+
+ if (socket) {
+ socket.removeEventListener('message', term._getMessage);
+ }
+
+ delete term.socket;
+ };
+
+ /**
+ * Attaches the current terminal to the given socket
+ *
+ * @param {WebSocket} socket - The socket to attach the current terminal.
+ * @param {boolean} bidirectional - Whether the terminal should send data
+ * to the socket as well.
+ * @param {boolean} buffered - Whether the rendering of incoming data
+ * should happen instantly or at a maximum
+ * frequency of 1 rendering per 10ms.
+ */
+ Xterm.prototype.attach = function (socket, bidirectional, buffered) {
+ return exports.attach(this, socket, bidirectional, buffered);
+ };
+
+ /**
+ * Detaches the current terminal from the given socket.
+ *
+ * @param {WebSocket} socket - The socket from which to detach the current
+ * terminal.
+ */
+ Xterm.prototype.detach = function (socket) {
+ return exports.detach(this, socket);
+ };
+
+ return exports;
+});
diff --git a/handlers/www/assets/button.png b/handlers/www/assets/button.png
new file mode 100644
index 0000000000000000000000000000000000000000..dcea54d0fe4a6e4bd6d16138099979087a7c9ee9
Binary files /dev/null and b/handlers/www/assets/button.png differ
diff --git a/handlers/www/assets/editor.css b/handlers/www/assets/editor.css
new file mode 100644
index 0000000000000000000000000000000000000000..2f8a4c7a682aa9476e571dfc80c8889d9df226ad
--- /dev/null
+++ b/handlers/www/assets/editor.css
@@ -0,0 +1,21 @@
+.alert-top {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width:100px;
+ display:none;
+ text-align: center;
+ padding: 3px;
+ height: 30px;
+ margin-bottom: 0px;
+}
+
+.alert-newfile {
+ text-align: center;
+ padding: 3px;
+ font-size: 15px;
+}
+
+.col-md-3 {
+ overflow-x: auto;
+}
diff --git a/handlers/www/assets/full_horizontal.svg b/handlers/www/assets/full_horizontal.svg
new file mode 100644
index 0000000000000000000000000000000000000000..d2201403176b4e90ae5dc90a94cb6cf9e9ece56d
--- /dev/null
+++ b/handlers/www/assets/full_horizontal.svg
@@ -0,0 +1,72 @@
+
\ No newline at end of file
diff --git a/handlers/www/assets/landing.css b/handlers/www/assets/landing.css
new file mode 100644
index 0000000000000000000000000000000000000000..09f6da21dfc600a6f47678b2bca8d65db6ec1449
--- /dev/null
+++ b/handlers/www/assets/landing.css
@@ -0,0 +1,82 @@
+/* Space out content a bit */
+body {
+ padding-top: 1.5rem;
+ padding-bottom: 1.5rem;
+}
+
+/* Everything but the jumbotron gets side spacing for mobile first views */
+.header,
+.marketing,
+.footer {
+ padding-right: 1rem;
+ padding-left: 1rem;
+}
+
+/* Custom page header */
+.header {
+ padding-bottom: 1rem;
+ border-bottom: .05rem solid #e5e5e5;
+}
+/* Make the masthead heading the same height as the navigation */
+.header h3 {
+ margin-top: 0;
+ margin-bottom: 0;
+ line-height: 3rem;
+}
+
+/* Custom page footer */
+.footer {
+ padding-top: 1.5rem;
+ color: #777;
+ border-top: .05rem solid #e5e5e5;
+}
+
+/* Customize container */
+@media (min-width: 48em) {
+ .container {
+ max-width: 46rem;
+ }
+}
+.container-narrow > hr {
+ margin: 2rem 0;
+}
+
+/* Main marketing message and sign up button */
+.jumbotron {
+ text-align: center;
+ border-bottom: .05rem solid #e5e5e5;
+}
+.jumbotron .btn {
+ padding: .75rem 1.5rem;
+ font-size: 1.5rem;
+}
+.btn.dropdown-toggle, .dropdown-menu a {
+ cursor: pointer;
+}
+
+/* Supporting marketing content */
+.marketing {
+ margin: 3rem 0;
+}
+.marketing p + h4 {
+ margin-top: 1.5rem;
+}
+
+/* Responsive: Portrait tablets and up */
+@media screen and (min-width: 48em) {
+ /* Remove the padding we set earlier */
+ .header,
+ .marketing,
+ .footer {
+ padding-right: 0;
+ padding-left: 0;
+ }
+ /* Space out the masthead */
+ .header {
+ margin-bottom: 2rem;
+ }
+ /* Remove the bottom border on the jumbotron for visual effect */
+ .jumbotron {
+ border-bottom: 0;
+ }
+}
diff --git a/handlers/www/assets/package-lock.json b/handlers/www/assets/package-lock.json
new file mode 100644
index 0000000000000000000000000000000000000000..77fbf9d50e14508f172789e45286b1d07575f1e6
--- /dev/null
+++ b/handlers/www/assets/package-lock.json
@@ -0,0 +1,11 @@
+{
+ "requires": true,
+ "lockfileVersion": 1,
+ "dependencies": {
+ "xterm": {
+ "version": "3.14.5",
+ "resolved": "https://registry.npmjs.org/xterm/-/xterm-3.14.5.tgz",
+ "integrity": "sha512-DVmQ8jlEtL+WbBKUZuMxHMBgK/yeIZwkXB81bH+MGaKKnJGYwA+770hzhXPfwEIokK9On9YIFPRleVp/5G7z9g=="
+ }
+ }
+}
diff --git a/handlers/www/assets/setup-xterm.js b/handlers/www/assets/setup-xterm.js
new file mode 100644
index 0000000000000000000000000000000000000000..b884826d3ca10a28d6038519bd4186ac5eaff7ee
--- /dev/null
+++ b/handlers/www/assets/setup-xterm.js
@@ -0,0 +1,3 @@
+Terminal.applyAddon(fit);
+Terminal.applyAddon(fullscreen);
+
diff --git a/handlers/www/assets/social-icons.svg b/handlers/www/assets/social-icons.svg
new file mode 100644
index 0000000000000000000000000000000000000000..3b39255310d5b1b4d1a2cfb0b2987837908ed2ff
--- /dev/null
+++ b/handlers/www/assets/social-icons.svg
@@ -0,0 +1,26 @@
+
\ No newline at end of file
diff --git a/handlers/www/assets/style.css b/handlers/www/assets/style.css
new file mode 100644
index 0000000000000000000000000000000000000000..b76538146ad73bc41848b8b3e615a7d33ef3a06e
--- /dev/null
+++ b/handlers/www/assets/style.css
@@ -0,0 +1,99 @@
+@import url('https://fonts.googleapis.com/css?family=Rationale');
+
+.selected button {
+ background-color: rgba(158,158,158,0.2);
+}
+
+.terminal-container {
+ background-color: #000;
+ padding: 0;
+ display: flex;
+ align-items: stretch;
+ justify-content: stretch;
+ flex: 1;
+}
+
+.terminal-instance{
+ width: 100%;
+}
+
+.clock {
+ font-family: 'Rationale', sans-serif;
+ font-size: 3.0em;
+ color: #1da4eb;
+ text-align: center;
+}
+
+.welcome {
+ background-color: #e7e7e7;
+}
+
+.welcome > div {
+ text-align: center;
+}
+
+.welcome > div > img {
+ max-width: 100%;
+}
+
+.g-recaptcha div {
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: auto;
+ margin-top: 50px;
+}
+
+.uploadStatus .bottom-block {
+ display: block;
+ position: relative;
+ background-color: rgba(255, 235, 169, 0.25);
+ height: 30px;
+ width: 100%;
+}
+
+.uploadStatus .bottom-block > span {
+ display: inline-block;
+ padding: 8px;
+ font-size: 0.9em;
+}
+
+.uploadStatus {
+ display: block;
+ position: relative;
+ width: 100%;
+ border: 2px solid #aad1f9;
+ transition: opacity 0.1s linear;
+ border-top: 0px;
+}
+
+.disconnected {
+ background-color: #FDF4B6;
+}
+md-input-container {
+ margin-bottom: 0;
+}
+md-input-container .md-errors-spacer {
+ height: 0;
+ min-height: 0;
+}
+
+.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);
+}
+.md-mini {
+ min-width: 24px;
+}
+
+.dragover {
+ opacity: 0.5;
+}
diff --git a/handlers/www/assets/swarm.png b/handlers/www/assets/swarm.png
new file mode 100644
index 0000000000000000000000000000000000000000..3e7414f7cde8be52c2a720b14d6abc927420747a
--- /dev/null
+++ b/handlers/www/assets/swarm.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:66de55a585a7f42fc78b3f84e7144e94ad07a0b7fcd9aaf66b3a142f89a1c02f
+size 123625
diff --git a/handlers/www/assets/xterm/addons/attach/attach.js b/handlers/www/assets/xterm/addons/attach/attach.js
new file mode 100644
index 0000000000000000000000000000000000000000..23a9030b3db980d9d2606bdf277a96ee505ce81f
--- /dev/null
+++ b/handlers/www/assets/xterm/addons/attach/attach.js
@@ -0,0 +1,106 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.attach = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;iterm;\n bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional;\n addonTerminal.__socket = socket;\n\n addonTerminal.__flushBuffer = () => {\n addonTerminal.write(addonTerminal.__attachSocketBuffer);\n addonTerminal.__attachSocketBuffer = null;\n };\n\n addonTerminal.__pushToBuffer = (data: string) => {\n if (addonTerminal.__attachSocketBuffer) {\n addonTerminal.__attachSocketBuffer += data;\n } else {\n addonTerminal.__attachSocketBuffer = data;\n setTimeout(addonTerminal.__flushBuffer, 10);\n }\n };\n\n // TODO: This should be typed but there seem to be issues importing the type\n let myTextDecoder: any;\n\n addonTerminal.__getMessage = function(ev: MessageEvent): void {\n let str: string;\n\n if (typeof ev.data === 'object') {\n if (!myTextDecoder) {\n myTextDecoder = new TextDecoder();\n }\n if (ev.data instanceof ArrayBuffer) {\n str = myTextDecoder.decode(ev.data);\n displayData(str);\n } else {\n const fileReader = new FileReader();\n\n fileReader.addEventListener('load', () => {\n str = myTextDecoder.decode(fileReader.result);\n displayData(str);\n });\n fileReader.readAsArrayBuffer(ev.data);\n }\n } else if (typeof ev.data === 'string') {\n displayData(ev.data);\n } else {\n throw Error(`Cannot handle \"${typeof ev.data}\" websocket message.`);\n }\n };\n\n /**\n * Push data to buffer or write it in the terminal.\n * This is used as a callback for FileReader.onload.\n *\n * @param str String decoded by FileReader.\n * @param data The data of the EventMessage.\n */\n function displayData(str?: string, data?: string): void {\n if (buffered) {\n addonTerminal.__pushToBuffer(str || data);\n } else {\n addonTerminal.write(str || data);\n }\n }\n\n addonTerminal.__sendData = (data: string) => {\n if (socket.readyState !== 1) {\n return;\n }\n socket.send(data);\n };\n\n addonTerminal._core.register(addSocketListener(socket, 'message', addonTerminal.__getMessage));\n\n if (bidirectional) {\n addonTerminal.__dataListener = addonTerminal.onData(addonTerminal.__sendData);\n addonTerminal._core.register(addonTerminal.__dataListener);\n }\n\n addonTerminal._core.register(addSocketListener(socket, 'close', () => detach(addonTerminal, socket)));\n addonTerminal._core.register(addSocketListener(socket, 'error', () => detach(addonTerminal, socket)));\n}\n\nfunction addSocketListener(socket: WebSocket, type: string, handler: (this: WebSocket, ev: Event) => any): IDisposable {\n socket.addEventListener(type, handler);\n return {\n dispose: () => {\n if (!handler) {\n // Already disposed\n return;\n }\n socket.removeEventListener(type, handler);\n handler = null;\n }\n };\n}\n\n/**\n * Detaches the given terminal from the given socket\n *\n * @param term The terminal to be detached from the given socket.\n * @param socket The socket from which to detach the current terminal.\n */\nexport function detach(term: Terminal, socket: WebSocket): void {\n const addonTerminal = term;\n addonTerminal.__dataListener.dispose();\n addonTerminal.__dataListener = undefined;\n\n socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;\n\n if (socket) {\n socket.removeEventListener('message', addonTerminal.__getMessage);\n }\n\n delete addonTerminal.__socket;\n}\n\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n /**\n * Attaches the current terminal to the given socket\n *\n * @param socket The socket to attach the current terminal.\n * @param bidirectional Whether the terminal should send data to the socket as well.\n * @param buffered Whether the rendering of incoming data should happen instantly or at a maximum\n * frequency of 1 rendering per 10ms.\n */\n (terminalConstructor.prototype).attach = function (socket: WebSocket, bidirectional: boolean, buffered: boolean): void {\n attach(this, socket, bidirectional, buffered);\n };\n\n /**\n * Detaches the current terminal from the given socket.\n *\n * @param socket The socket from which to detach the current terminal.\n */\n (terminalConstructor.prototype).detach = function (socket: WebSocket): void {\n detach(this, socket);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADmBA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAGA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AAAA;AACA;AACA;AACA;AASA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AA/EA;AAiFA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAQA;AACA;AACA;AACA;AAEA;AAEA;AACA;AACA;AAEA;AACA;AAZA;AAeA;AASA;AACA;AACA;AAOA;AACA;AACA;AACA;AArBA;"}
\ No newline at end of file
diff --git a/handlers/www/assets/xterm/addons/fit/fit.js b/handlers/www/assets/xterm/addons/fit/fit.js
new file mode 100644
index 0000000000000000000000000000000000000000..880ae025bc46870da20ea769d8e18a8b43fd99e1
--- /dev/null
+++ b/handlers/www/assets/xterm/addons/fit/fit.js
@@ -0,0 +1,51 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.fit = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;iterm)._core.viewport.scrollBarWidth;\n const geometry = {\n cols: Math.floor(availableWidth / (term)._core._renderCoordinator.dimensions.actualCellWidth),\n rows: Math.floor(availableHeight / (term)._core._renderCoordinator.dimensions.actualCellHeight)\n };\n return geometry;\n}\n\nexport function fit(term: Terminal): void {\n const geometry = proposeGeometry(term);\n if (geometry) {\n // Force a full render\n if (term.rows !== geometry.rows || term.cols !== geometry.cols) {\n (term)._core._renderCoordinator.clear();\n term.resize(geometry.cols, geometry.rows);\n }\n }\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n (terminalConstructor.prototype).proposeGeometry = function (): IGeometry {\n return proposeGeometry(this);\n };\n\n (terminalConstructor.prototype).fit = function (): void {\n fit(this);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADsBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAvBA;AAyBA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AATA;AAWA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AARA;"}
\ No newline at end of file
diff --git a/handlers/www/assets/xterm/addons/fullscreen/fullscreen.css b/handlers/www/assets/xterm/addons/fullscreen/fullscreen.css
new file mode 100644
index 0000000000000000000000000000000000000000..60e8c5114c170aad3aca4e1aa11e4544e64053d4
--- /dev/null
+++ b/handlers/www/assets/xterm/addons/fullscreen/fullscreen.css
@@ -0,0 +1,10 @@
+.xterm.fullscreen {
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ width: auto;
+ height: auto;
+ z-index: 255;
+}
diff --git a/handlers/www/assets/xterm/addons/fullscreen/fullscreen.js b/handlers/www/assets/xterm/addons/fullscreen/fullscreen.js
new file mode 100644
index 0000000000000000000000000000000000000000..b568be8840bbababa5a6470641930cee846f37b7
--- /dev/null
+++ b/handlers/www/assets/xterm/addons/fullscreen/fullscreen.js
@@ -0,0 +1,29 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.fullscreen = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i void;\n\n if (typeof fullscreen === 'undefined') {\n fn = (term.element.classList.contains('fullscreen')) ?\n term.element.classList.remove : term.element.classList.add;\n } else if (!fullscreen) {\n fn = term.element.classList.remove;\n } else {\n fn = term.element.classList.add;\n }\n\n fn = fn.bind(term.element.classList);\n fn('fullscreen');\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n (terminalConstructor.prototype).toggleFullScreen = function (fullscreen: boolean): void {\n toggleFullScreen(this, fullscreen);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADYA;AACA;AAEA;AACA;AACA;AACA;AAAA;AACA;AACA;AAAA;AACA;AACA;AAEA;AACA;AACA;AAdA;AAgBA;AACA;AACA;AACA;AACA;AAJA;"}
\ No newline at end of file
diff --git a/handlers/www/assets/xterm/addons/search/search.js b/handlers/www/assets/xterm/addons/search/search.js
new file mode 100644
index 0000000000000000000000000000000000000000..d632b071b7c1fa6d060b6261e663e67d9ead3208
--- /dev/null
+++ b/handlers/www/assets/xterm/addons/search/search.js
@@ -0,0 +1,262 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.search = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i?';
+var LINES_CACHE_TIME_TO_LIVE = 15 * 1000;
+var SearchHelper = (function () {
+ function SearchHelper(_terminal) {
+ this._terminal = _terminal;
+ this._linesCache = null;
+ this._linesCacheTimeoutId = 0;
+ this._destroyLinesCache = this._destroyLinesCache.bind(this);
+ }
+ SearchHelper.prototype.findNext = function (term, searchOptions) {
+ var incremental = searchOptions.incremental;
+ var result;
+ if (!term || term.length === 0) {
+ this._terminal.clearSelection();
+ return false;
+ }
+ var startCol = 0;
+ var startRow = this._terminal.buffer.viewportY;
+ if (this._terminal.hasSelection()) {
+ var currentSelection = this._terminal.getSelectionPosition();
+ startRow = incremental ? currentSelection.startRow : currentSelection.endRow;
+ startCol = incremental ? currentSelection.startColumn : currentSelection.endColumn;
+ }
+ this._initLinesCache();
+ var findingRow = startRow;
+ var cumulativeCols = startCol;
+ while (this._terminal.buffer.getLine(findingRow).isWrapped) {
+ findingRow--;
+ cumulativeCols += this._terminal.cols;
+ }
+ result = this._findInLine(term, findingRow, cumulativeCols, searchOptions);
+ if (!result) {
+ for (var y = startRow + 1; y < this._terminal.buffer.baseY + this._terminal.rows; y++) {
+ result = this._findInLine(term, y, 0, searchOptions);
+ if (result) {
+ break;
+ }
+ }
+ }
+ if (!result) {
+ for (var y = 0; y < findingRow; y++) {
+ result = this._findInLine(term, y, 0, searchOptions);
+ if (result) {
+ break;
+ }
+ }
+ }
+ return this._selectResult(result);
+ };
+ SearchHelper.prototype.findPrevious = function (term, searchOptions) {
+ var result;
+ if (!term || term.length === 0) {
+ this._terminal.clearSelection();
+ return false;
+ }
+ var isReverseSearch = true;
+ var startRow = this._terminal.buffer.viewportY + this._terminal.rows - 1;
+ var startCol = this._terminal.cols;
+ if (this._terminal.hasSelection()) {
+ var currentSelection = this._terminal.getSelectionPosition();
+ startRow = currentSelection.startRow;
+ startCol = currentSelection.startColumn;
+ }
+ this._initLinesCache();
+ result = this._findInLine(term, startRow, startCol, searchOptions, isReverseSearch);
+ if (!result) {
+ var cumulativeCols = this._terminal.cols;
+ if (this._terminal.buffer.getLine(startRow).isWrapped) {
+ cumulativeCols += startCol;
+ }
+ for (var y = startRow - 1; y >= 0; y--) {
+ result = this._findInLine(term, y, cumulativeCols, searchOptions, isReverseSearch);
+ if (result) {
+ break;
+ }
+ if (this._terminal.buffer.getLine(y).isWrapped) {
+ cumulativeCols += this._terminal.cols;
+ }
+ else {
+ cumulativeCols = this._terminal.cols;
+ }
+ }
+ }
+ if (!result) {
+ var searchFrom = this._terminal.buffer.baseY + this._terminal.rows - 1;
+ var cumulativeCols = this._terminal.cols;
+ for (var y = searchFrom; y >= startRow; y--) {
+ result = this._findInLine(term, y, cumulativeCols, searchOptions, isReverseSearch);
+ if (result) {
+ break;
+ }
+ if (this._terminal.buffer.getLine(y).isWrapped) {
+ cumulativeCols += this._terminal.cols;
+ }
+ else {
+ cumulativeCols = this._terminal.cols;
+ }
+ }
+ }
+ return this._selectResult(result);
+ };
+ SearchHelper.prototype._initLinesCache = function () {
+ var _this = this;
+ if (!this._linesCache) {
+ this._linesCache = new Array(this._terminal.buffer.length);
+ this._cursorMoveListener = this._terminal.onCursorMove(function () { return _this._destroyLinesCache(); });
+ this._resizeListener = this._terminal.onResize(function () { return _this._destroyLinesCache(); });
+ }
+ window.clearTimeout(this._linesCacheTimeoutId);
+ this._linesCacheTimeoutId = window.setTimeout(function () { return _this._destroyLinesCache(); }, LINES_CACHE_TIME_TO_LIVE);
+ };
+ SearchHelper.prototype._destroyLinesCache = function () {
+ this._linesCache = null;
+ if (this._cursorMoveListener) {
+ this._cursorMoveListener.dispose();
+ this._cursorMoveListener = undefined;
+ }
+ if (this._resizeListener) {
+ this._resizeListener.dispose();
+ this._resizeListener = undefined;
+ }
+ if (this._linesCacheTimeoutId) {
+ window.clearTimeout(this._linesCacheTimeoutId);
+ this._linesCacheTimeoutId = 0;
+ }
+ };
+ SearchHelper.prototype._isWholeWord = function (searchIndex, line, term) {
+ return (((searchIndex === 0) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex - 1]) !== -1)) &&
+ (((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex + term.length]) !== -1)));
+ };
+ SearchHelper.prototype._findInLine = function (term, row, col, searchOptions, isReverseSearch) {
+ if (searchOptions === void 0) { searchOptions = {}; }
+ if (isReverseSearch === void 0) { isReverseSearch = false; }
+ if (this._terminal.buffer.getLine(row).isWrapped) {
+ return;
+ }
+ var stringLine = this._linesCache ? this._linesCache[row] : void 0;
+ if (stringLine === void 0) {
+ stringLine = this.translateBufferLineToStringWithWrap(row, true);
+ if (this._linesCache) {
+ this._linesCache[row] = stringLine;
+ }
+ }
+ var searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
+ var searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
+ var resultIndex = -1;
+ if (searchOptions.regex) {
+ var searchRegex = RegExp(searchTerm, 'g');
+ var foundTerm = void 0;
+ if (isReverseSearch) {
+ while (foundTerm = searchRegex.exec(searchStringLine.slice(0, col))) {
+ resultIndex = searchRegex.lastIndex - foundTerm[0].length;
+ term = foundTerm[0];
+ searchRegex.lastIndex -= (term.length - 1);
+ }
+ }
+ else {
+ foundTerm = searchRegex.exec(searchStringLine.slice(col));
+ if (foundTerm && foundTerm[0].length > 0) {
+ resultIndex = col + (searchRegex.lastIndex - foundTerm[0].length);
+ term = foundTerm[0];
+ }
+ }
+ }
+ else {
+ if (isReverseSearch) {
+ if (col - searchTerm.length >= 0) {
+ resultIndex = searchStringLine.lastIndexOf(searchTerm, col - searchTerm.length);
+ }
+ }
+ else {
+ resultIndex = searchStringLine.indexOf(searchTerm, col);
+ }
+ }
+ if (resultIndex >= 0) {
+ if (resultIndex >= this._terminal.cols) {
+ row += Math.floor(resultIndex / this._terminal.cols);
+ resultIndex = resultIndex % this._terminal.cols;
+ }
+ if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {
+ return;
+ }
+ var line = this._terminal.buffer.getLine(row);
+ for (var i = 0; i < resultIndex; i++) {
+ var cell = line.getCell(i);
+ var char = cell.char;
+ if (char.length > 1) {
+ resultIndex -= char.length - 1;
+ }
+ var charWidth = cell.width;
+ if (charWidth === 0) {
+ resultIndex++;
+ }
+ }
+ return {
+ term: term,
+ col: resultIndex,
+ row: row
+ };
+ }
+ };
+ SearchHelper.prototype.translateBufferLineToStringWithWrap = function (lineIndex, trimRight) {
+ var lineString = '';
+ var lineWrapsToNext;
+ do {
+ var nextLine = this._terminal.buffer.getLine(lineIndex + 1);
+ lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
+ lineString += this._terminal.buffer.getLine(lineIndex).translateToString(!lineWrapsToNext && trimRight).substring(0, this._terminal.cols);
+ lineIndex++;
+ } while (lineWrapsToNext);
+ return lineString;
+ };
+ SearchHelper.prototype._selectResult = function (result) {
+ if (!result) {
+ this._terminal.clearSelection();
+ return false;
+ }
+ this._terminal.select(result.col, result.row, result.term.length);
+ this._terminal.scrollLines(result.row - this._terminal.buffer.viewportY);
+ return true;
+ };
+ return SearchHelper;
+}());
+exports.SearchHelper = SearchHelper;
+
+},{}],2:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var SearchHelper_1 = require("./SearchHelper");
+function findNext(terminal, term, searchOptions) {
+ if (searchOptions === void 0) { searchOptions = {}; }
+ var addonTerminal = terminal;
+ if (!addonTerminal.__searchHelper) {
+ addonTerminal.__searchHelper = new SearchHelper_1.SearchHelper(addonTerminal);
+ }
+ return addonTerminal.__searchHelper.findNext(term, searchOptions);
+}
+exports.findNext = findNext;
+function findPrevious(terminal, term, searchOptions) {
+ var addonTerminal = terminal;
+ if (!addonTerminal.__searchHelper) {
+ addonTerminal.__searchHelper = new SearchHelper_1.SearchHelper(addonTerminal);
+ }
+ return addonTerminal.__searchHelper.findPrevious(term, searchOptions);
+}
+exports.findPrevious = findPrevious;
+function apply(terminalConstructor) {
+ terminalConstructor.prototype.findNext = function (term, searchOptions) {
+ return findNext(this, term, searchOptions);
+ };
+ terminalConstructor.prototype.findPrevious = function (term, searchOptions) {
+ return findPrevious(this, term, searchOptions);
+ };
+}
+exports.apply = apply;
+
+},{"./SearchHelper":1}]},{},[2])(2)
+});
+//# sourceMappingURL=search.js.map
diff --git a/handlers/www/assets/xterm/addons/search/search.js.map b/handlers/www/assets/xterm/addons/search/search.js.map
new file mode 100644
index 0000000000000000000000000000000000000000..33a7b37ebef76189504efc5cdca9beb2330223ab
--- /dev/null
+++ b/handlers/www/assets/xterm/addons/search/search.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"search.js","sources":["../../../src/addons/search/search.ts","../../../src/addons/search/SearchHelper.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { SearchHelper } from './SearchHelper';\nimport { Terminal } from 'xterm';\nimport { ISearchAddonTerminal, ISearchOptions } from './Interfaces';\n\n/**\n * Find the next instance of the term, then scroll to and select it. If it\n * doesn't exist, do nothing.\n * @param term The search term.\n * @param searchOptions Search options\n * @return Whether a result was found.\n */\nexport function findNext(terminal: Terminal, term: string, searchOptions: ISearchOptions = {}): boolean {\n const addonTerminal = terminal;\n if (!addonTerminal.__searchHelper) {\n addonTerminal.__searchHelper = new SearchHelper(addonTerminal);\n }\n return addonTerminal.__searchHelper.findNext(term, searchOptions);\n}\n\n/**\n * Find the previous instance of the term, then scroll to and select it. If it\n * doesn't exist, do nothing.\n * @param term The search term.\n * @param searchOptions Search options\n * @return Whether a result was found.\n */\nexport function findPrevious(terminal: Terminal, term: string, searchOptions: ISearchOptions): boolean {\n const addonTerminal = terminal;\n if (!addonTerminal.__searchHelper) {\n addonTerminal.__searchHelper = new SearchHelper(addonTerminal);\n }\n return addonTerminal.__searchHelper.findPrevious(term, searchOptions);\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n (terminalConstructor.prototype).findNext = function(term: string, searchOptions: ISearchOptions): boolean {\n return findNext(this, term, searchOptions);\n };\n\n (terminalConstructor.prototype).findPrevious = function(term: string, searchOptions: ISearchOptions): boolean {\n return findPrevious(this, term, searchOptions);\n };\n}\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces';\nimport { IDisposable } from 'xterm';\n\nconst NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:\"\\',./<>?';\nconst LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs\n\n/**\n * A class that knows how to search the terminal and how to display the results.\n */\nexport class SearchHelper implements ISearchHelper {\n /**\n * translateBufferLineToStringWithWrap is a fairly expensive call.\n * We memoize the calls into an array that has a time based ttl.\n * _linesCache is also invalidated when the terminal cursor moves.\n */\n private _linesCache: string[] = null;\n private _linesCacheTimeoutId = 0;\n private _cursorMoveListener: IDisposable | undefined;\n private _resizeListener: IDisposable | undefined;\n\n constructor(private _terminal: ISearchAddonTerminal) {\n this._destroyLinesCache = this._destroyLinesCache.bind(this);\n }\n\n /**\n * Find the next instance of the term, then scroll to and select it. If it\n * doesn't exist, do nothing.\n * @param term The search term.\n * @param searchOptions Search options.\n * @return Whether a result was found.\n */\n public findNext(term: string, searchOptions?: ISearchOptions): boolean {\n const {incremental} = searchOptions;\n let result: ISearchResult;\n\n if (!term || term.length === 0) {\n this._terminal.clearSelection();\n return false;\n }\n\n let startCol: number = 0;\n let startRow = this._terminal.buffer.viewportY;\n\n if (this._terminal.hasSelection()) {\n // Start from the selection end if there is a selection\n // For incremental search, use existing row\n const currentSelection = this._terminal.getSelectionPosition();\n startRow = incremental ? currentSelection.startRow : currentSelection.endRow;\n startCol = incremental ? currentSelection.startColumn : currentSelection.endColumn;\n }\n\n this._initLinesCache();\n\n // A row that has isWrapped = false\n let findingRow = startRow;\n // index of beginning column that _findInLine need to scan.\n let cumulativeCols = startCol;\n // If startRow is wrapped row, scan for unwrapped row above.\n // So we can start matching on wrapped line from long unwrapped line.\n while (this._terminal.buffer.getLine(findingRow).isWrapped) {\n findingRow--;\n cumulativeCols += this._terminal.cols;\n }\n\n // Search startRow\n result = this._findInLine(term, findingRow, cumulativeCols, searchOptions);\n\n // Search from startRow + 1 to end\n if (!result) {\n\n for (let y = startRow + 1; y < this._terminal.buffer.baseY + this._terminal.rows; y++) {\n\n // If the current line is wrapped line, increase index of column to ignore the previous scan\n // Otherwise, reset beginning column index to zero with set new unwrapped line index\n result = this._findInLine(term, y, 0, searchOptions);\n if (result) {\n break;\n }\n }\n }\n\n // Search from the top to the startRow (search the whole startRow again in\n // case startCol > 0)\n if (!result) {\n for (let y = 0; y < findingRow; y++) {\n result = this._findInLine(term, y, 0, searchOptions);\n if (result) {\n break;\n }\n }\n }\n\n // Set selection and scroll if a result was found\n return this._selectResult(result);\n }\n\n /**\n * Find the previous instance of the term, then scroll to and select it. If it\n * doesn't exist, do nothing.\n * @param term The search term.\n * @param searchOptions Search options.\n * @return Whether a result was found.\n */\n public findPrevious(term: string, searchOptions?: ISearchOptions): boolean {\n let result: ISearchResult;\n\n if (!term || term.length === 0) {\n this._terminal.clearSelection();\n return false;\n }\n\n const isReverseSearch = true;\n let startRow = this._terminal.buffer.viewportY + this._terminal.rows - 1;\n let startCol = this._terminal.cols;\n\n if (this._terminal.hasSelection()) {\n // Start from the selection start if there is a selection\n const currentSelection = this._terminal.getSelectionPosition();\n startRow = currentSelection.startRow;\n startCol = currentSelection.startColumn;\n }\n\n this._initLinesCache();\n\n // Search startRow\n result = this._findInLine(term, startRow, startCol, searchOptions, isReverseSearch);\n\n // Search from startRow - 1 to top\n if (!result) {\n // If the line is wrapped line, increase number of columns that is needed to be scanned\n // Se we can scan on wrapped line from unwrapped line\n let cumulativeCols = this._terminal.cols;\n if (this._terminal.buffer.getLine(startRow).isWrapped) {\n cumulativeCols += startCol;\n }\n for (let y = startRow - 1; y >= 0; y--) {\n result = this._findInLine(term, y, cumulativeCols, searchOptions, isReverseSearch);\n if (result) {\n break;\n }\n // If the current line is wrapped line, increase scanning range,\n // preparing for scanning on unwrapped line\n if (this._terminal.buffer.getLine(y).isWrapped) {\n cumulativeCols += this._terminal.cols;\n } else {\n cumulativeCols = this._terminal.cols;\n }\n }\n }\n\n // Search from the bottom to startRow (search the whole startRow again in\n // case startCol > 0)\n if (!result) {\n const searchFrom = this._terminal.buffer.baseY + this._terminal.rows - 1;\n let cumulativeCols = this._terminal.cols;\n for (let y = searchFrom; y >= startRow; y--) {\n result = this._findInLine(term, y, cumulativeCols, searchOptions, isReverseSearch);\n if (result) {\n break;\n }\n if (this._terminal.buffer.getLine(y).isWrapped) {\n cumulativeCols += this._terminal.cols;\n } else {\n cumulativeCols = this._terminal.cols;\n }\n }\n }\n\n // Set selection and scroll if a result was found\n return this._selectResult(result);\n }\n\n /**\n * Sets up a line cache with a ttl\n */\n private _initLinesCache(): void {\n if (!this._linesCache) {\n this._linesCache = new Array(this._terminal.buffer.length);\n this._cursorMoveListener = this._terminal.onCursorMove(() => this._destroyLinesCache());\n this._resizeListener = this._terminal.onResize(() => this._destroyLinesCache());\n }\n\n window.clearTimeout(this._linesCacheTimeoutId);\n this._linesCacheTimeoutId = window.setTimeout(() => this._destroyLinesCache(), LINES_CACHE_TIME_TO_LIVE);\n }\n\n private _destroyLinesCache(): void {\n this._linesCache = null;\n if (this._cursorMoveListener) {\n this._cursorMoveListener.dispose();\n this._cursorMoveListener = undefined;\n }\n if (this._resizeListener) {\n this._resizeListener.dispose();\n this._resizeListener = undefined;\n }\n if (this._linesCacheTimeoutId) {\n window.clearTimeout(this._linesCacheTimeoutId);\n this._linesCacheTimeoutId = 0;\n }\n }\n\n /**\n * A found substring is a whole word if it doesn't have an alphanumeric character directly adjacent to it.\n * @param searchIndex starting indext of the potential whole word substring\n * @param line entire string in which the potential whole word was found\n * @param term the substring that starts at searchIndex\n */\n private _isWholeWord(searchIndex: number, line: string, term: string): boolean {\n return (((searchIndex === 0) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex - 1]) !== -1)) &&\n (((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex + term.length]) !== -1)));\n }\n\n /**\n * Searches a line for a search term. Takes the provided terminal line and searches the text line, which may contain\n * subsequent terminal lines if the text is wrapped. If the provided line number is part of a wrapped text line that\n * started on an earlier line then it is skipped since it will be properly searched when the terminal line that the\n * text starts on is searched.\n * @param term The search term.\n * @param row The line to start the search from.\n * @param col The column to start the search from.\n * @param searchOptions Search options.\n * @return The search result if it was found.\n */\n protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult {\n\n // Ignore wrapped lines, only consider on unwrapped line (first row of command string).\n if (this._terminal.buffer.getLine(row).isWrapped) {\n return;\n }\n let stringLine = this._linesCache ? this._linesCache[row] : void 0;\n if (stringLine === void 0) {\n stringLine = this.translateBufferLineToStringWithWrap(row, true);\n if (this._linesCache) {\n this._linesCache[row] = stringLine;\n }\n }\n\n const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();\n const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();\n\n let resultIndex = -1;\n if (searchOptions.regex) {\n const searchRegex = RegExp(searchTerm, 'g');\n let foundTerm: RegExpExecArray;\n if (isReverseSearch) {\n // This loop will get the resultIndex of the _last_ regex match in the range 0..col\n while (foundTerm = searchRegex.exec(searchStringLine.slice(0, col))) {\n resultIndex = searchRegex.lastIndex - foundTerm[0].length;\n term = foundTerm[0];\n searchRegex.lastIndex -= (term.length - 1);\n }\n } else {\n foundTerm = searchRegex.exec(searchStringLine.slice(col));\n if (foundTerm && foundTerm[0].length > 0) {\n resultIndex = col + (searchRegex.lastIndex - foundTerm[0].length);\n term = foundTerm[0];\n }\n }\n } else {\n if (isReverseSearch) {\n if (col - searchTerm.length >= 0) {\n resultIndex = searchStringLine.lastIndexOf(searchTerm, col - searchTerm.length);\n }\n } else {\n resultIndex = searchStringLine.indexOf(searchTerm, col);\n }\n }\n\n if (resultIndex >= 0) {\n // Adjust the row number and search index if needed since a \"line\" of text can span multiple rows\n if (resultIndex >= this._terminal.cols) {\n row += Math.floor(resultIndex / this._terminal.cols);\n resultIndex = resultIndex % this._terminal.cols;\n }\n if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {\n return;\n }\n\n const line = this._terminal.buffer.getLine(row);\n\n for (let i = 0; i < resultIndex; i++) {\n const cell = line.getCell(i);\n // Adjust the searchIndex to normalize emoji into single chars\n const char = cell.char;\n if (char.length > 1) {\n resultIndex -= char.length - 1;\n }\n // Adjust the searchIndex for empty characters following wide unicode\n // chars (eg. CJK)\n const charWidth = cell.width;\n if (charWidth === 0) {\n resultIndex++;\n }\n }\n return {\n term,\n col: resultIndex,\n row\n };\n }\n }\n /**\n * Translates a buffer line to a string, including subsequent lines if they are wraps.\n * Wide characters will count as two columns in the resulting string. This\n * function is useful for getting the actual text underneath the raw selection\n * position.\n * @param line The line being translated.\n * @param trimRight Whether to trim whitespace to the right.\n */\n public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): string {\n let lineString = '';\n let lineWrapsToNext: boolean;\n\n do {\n const nextLine = this._terminal.buffer.getLine(lineIndex + 1);\n lineWrapsToNext = nextLine ? nextLine.isWrapped : false;\n lineString += this._terminal.buffer.getLine(lineIndex).translateToString(!lineWrapsToNext && trimRight).substring(0, this._terminal.cols);\n lineIndex++;\n } while (lineWrapsToNext);\n\n return lineString;\n }\n\n /**\n * Selects and scrolls to a result.\n * @param result The result to select.\n * @return Whethera result was selected.\n */\n private _selectResult(result: ISearchResult): boolean {\n if (!result) {\n this._terminal.clearSelection();\n return false;\n }\n this._terminal.select(result.col, result.row, result.term.length);\n this._terminal.scrollLines(result.row - this._terminal.buffer.viewportY);\n return true;\n }\n}\n",null],"names":[],"mappings":"AEAA;;;ADQA;AACA;AAKA;AAWA;AAAA;AALA;AACA;AAKA;AACA;AASA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AAEA;AAGA;AACA;AACA;AACA;AAEA;AAGA;AAEA;AAGA;AACA;AACA;AACA;AAGA;AAGA;AAEA;AAIA;AACA;AACA;AACA;AACA;AACA;AAIA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAGA;AACA;AASA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AAEA;AACA;AACA;AACA;AAEA;AAGA;AAGA;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAGA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAIA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAGA;AACA;AAKA;AAAA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAQA;AACA;AACA;AACA;AAaA;AAAA;AAAA;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AAEA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AAEA;AACA;AAEA;AACA;AACA;AACA;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AASA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AAOA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AAzUa;;;;;ADTb;AAWA;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AANA;AAeA;AACA;AACA;AACA;AACA;AACA;AACA;AANA;AAQA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AARA;"}
\ No newline at end of file
diff --git a/handlers/www/assets/xterm/addons/terminado/terminado.js b/handlers/www/assets/xterm/addons/terminado/terminado.js
new file mode 100644
index 0000000000000000000000000000000000000000..362246f40c75bc1dd847b849e6dc314813d7a960
--- /dev/null
+++ b/handlers/www/assets/xterm/addons/terminado/terminado.js
@@ -0,0 +1,70 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.terminado = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;iterm;\n bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional;\n addonTerminal.__socket = socket;\n\n addonTerminal.__flushBuffer = () => {\n addonTerminal.write(addonTerminal.__attachSocketBuffer);\n addonTerminal.__attachSocketBuffer = null;\n };\n\n addonTerminal.__pushToBuffer = (data: string) => {\n if (addonTerminal.__attachSocketBuffer) {\n addonTerminal.__attachSocketBuffer += data;\n } else {\n addonTerminal.__attachSocketBuffer = data;\n setTimeout(addonTerminal.__flushBuffer, 10);\n }\n };\n\n addonTerminal.__getMessage = (ev: MessageEvent) => {\n const data = JSON.parse(ev.data);\n if (data[0] === 'stdout') {\n if (buffered) {\n addonTerminal.__pushToBuffer(data[1]);\n } else {\n addonTerminal.write(data[1]);\n }\n }\n };\n\n addonTerminal.__sendData = (data: string) => {\n socket.send(JSON.stringify(['stdin', data]));\n };\n\n addonTerminal.__setSize = (size: {rows: number, cols: number}) => {\n socket.send(JSON.stringify(['set_size', size.rows, size.cols]));\n };\n\n socket.addEventListener('message', addonTerminal.__getMessage);\n\n if (bidirectional) {\n addonTerminal._core.register(addonTerminal.onData(addonTerminal.__sendData));\n }\n addonTerminal._core.register(addonTerminal.onResize(addonTerminal.__setSize));\n\n socket.addEventListener('close', () => terminadoDetach(addonTerminal, socket));\n socket.addEventListener('error', () => terminadoDetach(addonTerminal, socket));\n}\n\n/**\n * Detaches the given terminal from the given socket\n *\n * @param term The terminal to be detached from the given socket.\n * @param socket The socket from which to detach the current terminal.\n */\nexport function terminadoDetach(term: Terminal, socket: WebSocket): void {\n const addonTerminal = term;\n addonTerminal.__dataListener.dispose();\n addonTerminal.__dataListener = undefined;\n\n socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;\n\n if (socket) {\n socket.removeEventListener('message', addonTerminal.__getMessage);\n }\n\n delete addonTerminal.__socket;\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n /**\n * Attaches the current terminal to the given socket\n *\n * @param socket - The socket to attach the current terminal.\n * @param bidirectional - Whether the terminal should send data to the socket as well.\n * @param buffered - Whether the rendering of incoming data should happen instantly or at a\n * maximum frequency of 1 rendering per 10ms.\n */\n (terminalConstructor.prototype).terminadoAttach = function (socket: WebSocket, bidirectional: boolean, buffered: boolean): void {\n return terminadoAttach(this, socket, bidirectional, buffered);\n };\n\n /**\n * Detaches the current terminal from the given socket.\n *\n * @param socket The socket from which to detach the current terminal.\n */\n (terminalConstructor.prototype).terminadoDetach = function (socket: WebSocket): void {\n return terminadoDetach(this, socket);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADoBA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;AAEA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AA/CA;AAuDA;AACA;AACA;AACA;AAEA;AAEA;AACA;AACA;AAEA;AACA;AAZA;AAcA;AASA;AACA;AACA;AAOA;AACA;AACA;AACA;AArBA;"}
\ No newline at end of file
diff --git a/handlers/www/assets/xterm/addons/webLinks/webLinks.js b/handlers/www/assets/xterm/addons/webLinks/webLinks.js
new file mode 100644
index 0000000000000000000000000000000000000000..2176e8755c82e15b32218809f4036706dfc34ec3
--- /dev/null
+++ b/handlers/www/assets/xterm/addons/webLinks/webLinks.js
@@ -0,0 +1,42 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.webLinks = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i void = handleLink, options: ILinkMatcherOptions = {}): void {\n options.matchIndex = 1;\n term.registerLinkMatcher(strictUrlRegex, handler, options);\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n (terminalConstructor.prototype).webLinksInit = function (handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): void {\n webLinksInit(this, handler, options);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADOA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAQA;AAAA;AAAA;AACA;AACA;AACA;AAHA;AAKA;AACA;AACA;AACA;AACA;AAJA;"}
\ No newline at end of file
diff --git a/handlers/www/assets/xterm/addons/zmodem/zmodem.js b/handlers/www/assets/xterm/addons/zmodem/zmodem.js
new file mode 100644
index 0000000000000000000000000000000000000000..70a7ff77312ddcf6150acac184c6dfa1d0f76878
--- /dev/null
+++ b/handlers/www/assets/xterm/addons/zmodem/zmodem.js
@@ -0,0 +1,45 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.zmodem = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i,