1
0
mirror of https://github.com/bingohuang/docker-labs.git synced 2025-07-14 01:57:32 +08:00

Initial commit

This commit is contained in:
Jonathan Leibiusky (@xetorthio) 2016-10-08 03:12:48 +02:00
parent f2ae4344fd
commit dde49d8700
19 changed files with 5031 additions and 0 deletions

1
README.md Normal file
View File

@ -0,0 +1 @@
# play-with-docker

35
api.go Normal file
View File

@ -0,0 +1,35 @@
package main
import (
"log"
"net/http"
"golang.org/x/net/websocket"
"github.com/go-zoo/bone"
"github.com/urfave/negroni"
"github.com/xetorthio/play-with-docker/handlers"
)
func main() {
mux := bone.New()
mux.Get("/", http.HandlerFunc(handlers.NewSession))
mux.Get("/sessions/:sessionId", http.HandlerFunc(handlers.GetSession))
mux.Post("/sessions/:sessionId/instances", http.HandlerFunc(handlers.NewInstance))
mux.Delete("/sessions/:sessionId/instances/:instanceId", http.HandlerFunc(handlers.DeleteInstance))
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./www/index.html")
})
mux.Get("/p/:sessionId", h)
mux.Get("/assets/*", http.FileServer(http.Dir("./www")))
mux.Get("/exec/:id", websocket.Handler(handlers.Exec))
n := negroni.Classic()
n.UseHandler(mux)
log.Fatal(http.ListenAndServe(":3000", n))
}

View File

@ -0,0 +1,21 @@
package handlers
import (
"net/http"
"github.com/go-zoo/bone"
"github.com/xetorthio/play-with-docker/services"
)
func DeleteInstance(rw http.ResponseWriter, req *http.Request) {
sessionId := bone.GetValue(req, "sessionId")
instanceId := bone.GetValue(req, "instanceId")
s := services.GetSession(sessionId)
i := services.GetInstance(s, instanceId)
err := services.DeleteInstance(s, i)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
return
}
}

36
handlers/exec.go Normal file
View File

@ -0,0 +1,36 @@
package handlers
import (
"io"
"golang.org/x/net/context"
"golang.org/x/net/websocket"
"github.com/go-zoo/bone"
"github.com/xetorthio/play-with-docker/services"
)
// Echo the data received on the WebSocket.
func Exec(ws *websocket.Conn) {
id := bone.GetValue(ws.Request(), "id")
ctx := context.Background()
conn, err := services.GetExecConnection(id, ctx)
if err != nil {
return
}
defer conn.Close()
go func() {
io.Copy(ws, conn.Reader)
}()
go func() {
io.Copy(conn.Conn, ws)
}()
select {
case <-ctx.Done():
}
//io.Copy(ws, os.Stdout)
//go func() {
//io.Copy(*conn, ws)
//}()
}

22
handlers/get_session.go Normal file
View File

@ -0,0 +1,22 @@
package handlers
import (
"encoding/json"
"net/http"
"github.com/go-zoo/bone"
"github.com/xetorthio/play-with-docker/services"
)
func GetSession(rw http.ResponseWriter, req *http.Request) {
sessionId := bone.GetValue(req, "sessionId")
session := services.GetSession(sessionId)
if session == nil {
rw.WriteHeader(http.StatusNotFound)
return
}
json.NewEncoder(rw).Encode(session)
}

23
handlers/new_instance.go Normal file
View File

@ -0,0 +1,23 @@
package handlers
import (
"encoding/json"
"log"
"net/http"
"github.com/go-zoo/bone"
"github.com/xetorthio/play-with-docker/services"
)
func NewInstance(rw http.ResponseWriter, req *http.Request) {
sessionId := bone.GetValue(req, "sessionId")
s := services.GetSession(sessionId)
i, err := services.NewInstance(s)
if err != nil {
log.Println(err)
//TODO: Set a status error
} else {
json.NewEncoder(rw).Encode(i)
}
}

19
handlers/new_session.go Normal file
View File

@ -0,0 +1,19 @@
package handlers
import (
"fmt"
"log"
"net/http"
"github.com/xetorthio/play-with-docker/services"
)
func NewSession(rw http.ResponseWriter, req *http.Request) {
s, err := services.NewSession()
if err != nil {
log.Println(err)
//TODO: Return some error code
} else {
http.Redirect(rw, req, fmt.Sprintf("/p/%s", s.Id), http.StatusFound)
}
}

89
services/docker.go Normal file
View File

@ -0,0 +1,89 @@
package services
import (
"log"
"strings"
ptypes "github.com/xetorthio/play-with-docker/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"golang.org/x/net/context"
)
var c *client.Client
func init() {
var err error
c, err = client.NewEnvClient()
if err != nil {
// this wont happen if daemon is offline, only for some critical errors
log.Fatal("Cannot initialize docker client")
}
}
func GetContainerInfo(id string) (types.ContainerJSON, error) {
return c.ContainerInspect(context.Background(), id)
}
func CreateNetwork(name string) error {
opts := types.NetworkCreate{Attachable: true}
_, err := c.NetworkCreate(context.Background(), name, opts)
if err != nil {
return err
}
return nil
}
func GetExecConnection(id string, ctx context.Context) (*types.HijackedResponse, error) {
conf := types.ExecConfig{Tty: true, AttachStdin: true, AttachStderr: true, AttachStdout: true, Cmd: []string{"sh"}}
resp, err := c.ContainerExecCreate(ctx, id, conf)
if err != nil {
return nil, err
}
//err = c.ContainerExecStart(context.Background(), resp.ID, types.ExecStartCheck{Tty: true})
//if err != nil {
//return nil, err
//}
conn, err := c.ContainerExecAttach(ctx, resp.ID, conf)
if err != nil {
return nil, err
}
return &conn, nil
}
func CreateInstance(net string) (*ptypes.Instance, error) {
h := &container.HostConfig{NetworkMode: container.NetworkMode(net), Privileged: true}
conf := &container.Config{Image: "docker:dind"}
container, err := c.ContainerCreate(context.Background(), conf, h, nil, "")
if err != nil {
return nil, err
}
err = c.ContainerStart(context.Background(), container.ID, types.ContainerStartOptions{})
if err != nil {
return nil, err
}
cinfo, err := GetContainerInfo(container.ID)
if err != nil {
return nil, err
}
return &ptypes.Instance{Name: strings.Replace(cinfo.Name, "/", "", 1), IP: cinfo.NetworkSettings.Networks[net].IPAddress}, nil
}
func DeleteContainer(id string) error {
return c.ContainerRemove(context.Background(), id, types.ContainerRemoveOptions{Force: true})
}

39
services/instance.go Normal file
View File

@ -0,0 +1,39 @@
package services
import "github.com/xetorthio/play-with-docker/types"
var instances map[string]map[string]*types.Instance
func init() {
instances = make(map[string]map[string]*types.Instance)
}
func NewInstance(session *types.Session) (*types.Instance, error) {
//TODO: Validate that a session can only have 10 instances
//TODO: Create in redis
instance, err := CreateInstance(session.Id)
if err != nil {
return nil, err
}
if instances[session.Id] == nil {
instances[session.Id] = make(map[string]*types.Instance)
}
instances[session.Id][instance.Name] = instance
return instance, nil
}
func GetInstance(session *types.Session, instanceId string) *types.Instance {
//TODO: Use redis
i := instances[session.Id][instanceId]
return i
}
func DeleteInstance(session *types.Session, instance *types.Instance) error {
//TODO: Use redis
delete(instances[session.Id], instance.Name)
return DeleteContainer(instance.Name)
}

39
services/session.go Normal file
View File

@ -0,0 +1,39 @@
package services
import (
"github.com/twinj/uuid"
"github.com/xetorthio/play-with-docker/types"
)
var sessions map[string]*types.Session
func init() {
sessions = make(map[string]*types.Session)
}
func NewSession() (*types.Session, error) {
s := &types.Session{}
s.Id = uuid.NewV4().String()
s.Instances = map[string]*types.Instance{}
//TODO: Store in something like redis
sessions[s.Id] = s
if err := CreateNetwork(s.Id); err != nil {
return nil, err
}
//TODO: Schedule deletion after an hour
return s, nil
}
func GetSession(sessionId string) *types.Session {
//TODO: Use redis
s := sessions[sessionId]
if instances[sessionId] != nil {
s.Instances = instances[sessionId]
}
return s
}

11
types/session.go Normal file
View File

@ -0,0 +1,11 @@
package types
type Session struct {
Id string `json:"id"`
Instances map[string]*Instance `json:"instances"`
}
type Instance struct {
Name string `json:"name"`
IP string `json:"ip"`
}

67
www/assets/app.js Normal file
View File

@ -0,0 +1,67 @@
(function () {
'use strict';
var app = angular.module('DockerPlay', ['ngMaterial']);
app.controller('PlayController', ['$scope', '$log', '$http', '$location', '$timeout', function($scope, $log, $http, $location, $timeout) {
$scope.sessionId = window.location.pathname.replace('/p/', '');
$scope.instances = [];
$scope.selectedInstance = null;
$scope.newInstance = function() {
$http({
method: 'POST',
url: '/sessions/' + $scope.sessionId + '/instances',
}).then(function(response) {
var i = response.data;
$scope.instances.push(i);
$scope.showInstance(i);
}, function(response) {
console.log('error', response);
});
}
$scope.getSession = function(sessionId) {
$http({
method: 'GET',
url: '/sessions/' + $scope.sessionId,
}).then(function(response) {
var i = response.data;
for (var k in i.instances) {
var instance = i.instances[k];
$scope.instances.push(instance);
}
if ($scope.instances.length) {
$scope.showInstance($scope.instances[0]);
}
}, function(response) {
console.log('error', response);
});
}
$scope.showInstance = function(instance) {
$scope.selectedInstance = instance;
if (!instance.isAttached) {
$timeout(function() {createTerminal(instance.name)});
instance.isAttached = true;
}
}
$scope.deleteInstance = function(instance) {
$http({
method: 'DELETE',
url: '/sessions/' + $scope.sessionId + '/instances/' + instance.name,
}).then(function(response) {
$scope.instances = $scope.instances.filter(function(i) { return i.name != instance.name });
if ($scope.instances.length) {
$scope.showInstance($scope.instances[0]);
}
}, function(response) {
console.log('error', response);
});
}
$scope.getSession($scope.sessionId);
}]);
})();

134
www/assets/attach.js Normal file
View File

@ -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;
});

57
www/assets/main.js Normal file
View File

@ -0,0 +1,57 @@
//var term,
//protocol,
//socketURL,
//socket,
//pid,
//charWidth,
//charHeight;
//var terminalContainer = document.getElementById('terminal-container'),
//optionElements = {
//cursorBlink: document.querySelector('#option-cursor-blink')
//},
//colsElement = document.getElementById('cols'),
//rowsElement = document.getElementById('rows');
//function setTerminalSize () {
//var cols = parseInt(colsElement.value),
//rows = parseInt(rowsElement.value),
//width = (cols * charWidth).toString() + 'px',
//height = (rows * charHeight).toString() + 'px';
//terminalContainer.style.width = width;
//terminalContainer.style.height = height;
//term.resize(cols, rows);
//}
//colsElement.addEventListener('change', setTerminalSize);
//rowsElement.addEventListener('change', setTerminalSize);
//optionElements.cursorBlink.addEventListener('change', createTerminal);
//createTerminal();
function createTerminal(name) {
var terminalContainer = document.getElementById('terminal-' + name);
// Clean terminal
while (terminalContainer.children.length) {
terminalContainer.removeChild(terminalContainer.children[0]);
}
term = new Terminal({
cursorBlink: false
});
protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/exec/' + name;
term.open(terminalContainer);
socket = new WebSocket(socketURL);
socket.onopen = runRealTerminal;
}
function runRealTerminal() {
term.attach(socket);
term._initialized = true;
}

3
www/assets/style.css Normal file
View File

@ -0,0 +1,3 @@
.selected button {
background-color: rgba(158,158,158,0.2);
}

2196
www/assets/xterm.css Normal file

File diff suppressed because it is too large Load Diff

2137
www/assets/xterm.js Normal file

File diff suppressed because it is too large Load Diff

23
www/assets/xterm.js.map Normal file

File diff suppressed because one or more lines are too long

79
www/index.html Normal file
View File

@ -0,0 +1,79 @@
<!doctype html>
<html ng-app="DockerPlay" ng-controller="PlayController">
<head>
<title>Docker Playground</title>
<link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.css">
<link rel="stylesheet" href="/assets/xterm.css" />
<link rel="stylesheet" href="/assets/style.css" />
</head>
<body>
<div layout="column" style="height:100%;" ng-cloak>
<section layout="row" flex>
<md-sidenav
class="md-sidenav-left"
md-component-id="left"
md-is-locked-open="$mdMedia('gt-md')"
md-whiteframe="4">
<md-toolbar class="md-theme-indigo">
<h1 class="md-toolbar-tools">Instances</h1>
</md-toolbar>
<md-content layout-padding>
<md-button ng-click="newInstance()" class="md-primary">
+ Add new instance
</md-button>
<md-list>
<md-list-item class="md-3-line" ng-repeat="instance in instances" ng-click="showInstance(instance)" ng-class="instance.name == selectedInstance.name ? 'selected' : false">
<div class="md-list-item-text">{{instance.name}}</div>
<md-divider ng-if="!$last"></md-divider>
</md-list-item>
</md-list>
</md-content>
</md-sidenav>
<md-content flex layout-padding ng-if="!instances.length">
<div layout="column" layout-align="top center">
<p>
Add instances to your playground.
</p>
</div>
<div flex></div>
</md-content>
<md-content flex layout-padding ng-repeat="instance in instances" ng-show="instance.name == selectedInstance.name">
<md-card md-theme="default" md-theme-watch>
<md-card-title>
<md-card-title-text>
<span class="md-headline">{{instance.name}}</span>
<span class="md-subhead">{{instance.ip}}</span>
<md-card-title-text>
</md-card-title>
<md-card-actions layout="row">
<md-button class="md-warn" ng-click="deleteInstance(instance)">Delete</md-button>
</md-card-actions>
<md-card-content>
<div id="terminal-{{instance.name}}"></div>
</md-card-content>
</md-card>
</md-content>
</section>
</div>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-animate.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-aria.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-messages.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.js"></script>
<script src="/assets/app.js"></script>
<script src="/assets/main.js"></script>
<script src="/assets/xterm.js"></script>
<script src="/assets/attach.js"></script>
</body>
</html>