Initial commit

This commit is contained in:
Ben Firshman 2015-11-15 20:59:29 +01:00
commit 87d9e8e07d
18 changed files with 7797 additions and 0 deletions

15
README.md Normal file
View File

@ -0,0 +1,15 @@
Example Voting App
==================
This is an example Docker app with multiple services. It is run with Docker Compose and uses Docker Networking to connect containers together.
Running
-------
Since this app makes use of Compose's experimental networking support, it must be started with:
$ cd vote-apps/
$ docker-compose --x-networking up -d
The app will be running on port 5000 on your Docker host, and the results will be on port 5001.

View File

@ -0,0 +1,25 @@
voting-app:
build: ./voting-app/.
volumes:
- ./voting-app:/app
ports:
- "5000:80"
redis:
image: redis
ports: ["6379"]
worker:
build: ./worker
db:
image: postgres:9.4
volumes:
- "myvolume:/var/lib/postgresql/data"
result-app:
build: ./result-app/.
volumes:
- ./result-app:/app
ports:
- "5001:80"

View File

@ -0,0 +1,15 @@
FROM node:0.10
RUN mkdir /app
WORKDIR /app
ADD package.json /app/package.json
RUN npm install && npm ls
RUN mv /app/node_modules /node_modules
ADD . /app
ENV PORT 80
EXPOSE 80
CMD ["node", "server.js"]

View File

@ -0,0 +1,20 @@
{
"name": "result-app",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT",
"dependencies": {
"body-parser": "^1.14.1",
"cookie-parser": "^1.4.0",
"express": "^4.13.3",
"method-override": "^2.3.5",
"async": "^1.5.0",
"pg": "^4.4.3",
"socket.io": "^1.3.7"
}
}

View File

@ -0,0 +1,78 @@
var express = require('express'),
async = require('async'),
pg = require("pg"),
cookieParser = require('cookie-parser'),
bodyParser = require('body-parser'),
methodOverride = require('method-override'),
app = express(),
server = require('http').Server(app),
io = require('socket.io')(server);
io.set('transports', ['polling']);
var port = process.env.PORT || 4000;
io.sockets.on('connection', function (socket) {
socket.emit('message', { text : 'Welcome!' });
socket.on('subscribe', function (data) {
socket.join(data.channel);
});
});
async.retry(
{times: 1000, interval: 1000},
function(callback) {
pg.connect('postgres://postgres@voteapps_db_1/postgres', function(err, client, done) {
if (err) {
console.error("Failed to connect to db");
}
callback(err, client);
});
},
function(err, client) {
if (err) {
return console.err("Giving up");
}
console.log("Connected to db");
getVotes(client);
}
);
function getVotes(client) {
client.query('SELECT vote, COUNT(id) AS count FROM votes GROUP BY vote', [], function(err, result) {
if (err) {
console.error("Error performing query: " + err);
} else {
var data = result.rows.reduce(function(obj, row) {
obj[row.vote] = row.count;
return obj;
}, {});
io.sockets.emit("scores", JSON.stringify(data));
}
setTimeout(function() {getVotes(client) }, 1000);
});
}
app.use(cookieParser());
app.use(bodyParser());
app.use(methodOverride('X-HTTP-Method-Override'));
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
res.header("Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, OPTIONS");
next();
});
app.use(express.static(__dirname + '/views'));
app.get('/', function (req, res) {
res.sendFile(path.resolve(__dirname + '/views/index.html'));
});
server.listen(port, function () {
var port = server.address().port;
console.log('App running on port ' + port);
});

View File

@ -0,0 +1,45 @@
var app = angular.module('catsvsdogs', []);
var socket = io.connect({transports:['polling']});
var bg1 = document.getElementById('background-stats-1');
var bg2 = document.getElementById('background-stats-2');
app.controller('statsCtrl', function($scope){
var animateStats = function(a,b){
if(a+b>0){
var percentA = a/(a+b)*100;
var percentB = 100-percentA;
bg1.style.width= percentA+"%";
bg2.style.width = percentB+"%";
}
};
$scope.aPercent = 50;
$scope.bPercent = 50;
var updateScores = function(){
socket.on('scores', function (json) {
data = JSON.parse(json);
var a = parseInt(data.a || 0);
var b = parseInt(data.b || 0);
animateStats(a, b);
$scope.$apply(function() {
if(a + b > 0){
$scope.aPercent = a/(a+b) * 100;
$scope.bPercent = b/(a+b) * 100;
$scope.total = a + b
}
});
});
};
var init = function(){
document.body.style.opacity=1;
updateScores();
};
socket.on('message',function(data){
init();
});
});

View File

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html ng-app="catsvsdogs">
<head>
<meta charset="utf-8">
<title>Cats vs Dogs -- Result</title>
<base href="/index.html">
<meta name = "viewport" content = "width=device-width, initial-scale = 1.0">
<meta name="keywords" content="docker-compose, docker, stack">
<meta name="author" content="Tutum dev team">
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body ng-controller="statsCtrl" >
<div id="background-stats">
<div id="background-stats-1">
</div><!--
--><div id="background-stats-2">
</div>
</div>
<div id="content-container">
<div id="content-container-center">
<div id="choice">
<div class="choice cats">
<div class="label">Cats</div>
<div class="stat">{{aPercent | number:1}}%</div>
</div>
<div class="divider"></div>
<div class="choice dogs">
<div class="label">Dogs</div>
<div class="stat">{{bPercent | number:1}}%</div>
</div>
</div>
</div>
</div>
<div id="result" ng-if="total > 100">
{{total}} votes
</div>
<script src="socket.io.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
<script src="app.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,111 @@
@import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,600);
*{
box-sizing:border-box;
}
html,body{
margin:0;
padding:0;
height:100%;
font-family: 'Open Sans';
}
body{
opacity:0;
transition: all 1s linear;
}
.divider{
height: 150px;
width:2px;
background-color: #C0C9CE;
position: relative;
top: 50%;
float: left;
transform: translateY(-50%);
}
#background-stats-1{
background-color: #2196f3;
}
#background-stats-2{
background-color: #00cbca;
}
#content-container{
z-index:2;
position:relative;
margin:0 auto;
display:table;
padding:10px;
max-width:940px;
height:100%;
}
#content-container-center{
display:table-cell;
text-align:center;
vertical-align:middle;
}
#result{
position: absolute;
bottom: 40px;
right: 20px;
color: #fff;
opacity: 0.5;
font-size: 45px;
font-weight: 600;
}
#choice{
transition: all 300ms linear;
line-height:1.3em;
background:#fff;
box-shadow: 10px 0 0 #fff, -10px 0 0 #fff;
vertical-align:middle;
font-size:40px;
font-weight: 600;
width: 450px;
height: 200px;
}
#choice a{
text-decoration:none;
}
#choice a:hover, #choice a:focus{
outline:0;
text-decoration:underline;
}
#choice .choice{
width: 49%;
position: relative;
top: 50%;
transform: translateY(-50%);
text-align: left;
padding-left: 50px;
}
#choice .choice .label{
text-transform: uppercase;
}
#choice .choice.dogs{
color: #00cbca;
float: right;
}
#choice .choice.cats{
color: #2196f3;
float: left;
}
#background-stats{
z-index:1;
height:100%;
width:100%;
position:absolute;
}
#background-stats div{
transition: width 400ms ease-in-out;
display:inline-block;
margin-bottom:-4px;
width:50%;
height:100%;
}

View File

@ -0,0 +1,18 @@
# Using official python runtime base image
FROM python:2.7
# Set the application directory
WORKDIR /app
# Install our requirements.txt
ADD requirements.txt /app/requirements.txt
RUN pip install -r requirements.txt
# Copy our code from the current folder to /app inside the container
ADD . /app
# Make port 5000 available for links and/or publish
EXPOSE 80
# Define our command to be run when launching the container
CMD ["python", "app.py"]

View File

@ -0,0 +1,44 @@
from flask import Flask
from flask import render_template
from flask import request
from flask import make_response
from utils import connect_to_redis
import os
import socket
import random
import json
option_a = os.getenv('OPTION_A', "Cats")
option_b = os.getenv('OPTION_B', "Dogs")
hostname = socket.gethostname()
redis = connect_to_redis("voteapps_redis_1")
app = Flask(__name__)
@app.route("/", methods=['POST','GET'])
def hello():
voter_id = request.cookies.get('voter_id')
if not voter_id:
voter_id = hex(random.getrandbits(64))[2:-1]
vote = None
if request.method == 'POST':
vote = request.form['vote']
data = json.dumps({'voter_id': voter_id, 'vote': vote})
redis.rpush('votes', data)
resp = make_response(render_template(
'index.html',
option_a=option_a,
option_b=option_b,
hostname=hostname,
vote=vote,
))
resp.set_cookie('voter_id', voter_id)
return resp
if __name__ == "__main__":
app.run(host='0.0.0.0', port=80, debug=True)

View File

@ -0,0 +1,2 @@
Flask
Redis

View File

@ -0,0 +1,129 @@
@import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,600);
*{
box-sizing:border-box;
}
html,body{
margin: 0;
padding: 0;
background-color: #F7F8F9;
height: 100vh;
font-family: 'Open Sans';
}
button{
border-radius: 0;
width: 100%;
height: 50%;
}
button[type="submit"] {
-webkit-appearance:none; -webkit-border-radius:0;
}
button i{
float: right;
padding-right: 30px;
margin-top: 3px;
}
button.a{
background-color: #1aaaf8;
}
button.b{
background-color: #00cbca;
}
#tip{
text-align: left;
color: #c0c9ce;
font-size: 14px;
}
#hostname{
position: absolute;
bottom: 100px;
right: 0;
left: 0;
color: #8f9ea8;
font-size: 14px;
}
#content-container{
z-index: 2;
position: relative;
margin: 0 auto;
display: table;
padding: 10px;
max-width: 940px;
height: 100%;
}
#content-container-center{
display: table-cell;
text-align: center;
}
#content-container-center h3{
color: #254356;
}
#choice{
transition: all 300ms linear;
line-height: 1.3em;
display: inline;
vertical-align: middle;
font-size: 3em;
}
#choice a{
text-decoration:none;
}
#choice a:hover, #choice a:focus{
outline:0;
text-decoration:underline;
}
#choice button{
display: block;
height: 80px;
width: 330px;
border: none;
color: white;
text-transform: uppercase;
font-size:18px;
font-weight: 700;
margin-top: 10px;
margin-bottom: 10px;
text-align: left;
padding-left: 50px;
}
#choice button.a:hover{
background-color: #1488c6;
}
#choice button.b:hover{
background-color: #00a2a1;
}
#choice button.a:focus{
background-color: #1488c6;
}
#choice button.b:focus{
background-color: #00a2a1;
}
#background-stats{
z-index:1;
height:100%;
width:100%;
position:absolute;
}
#background-stats div{
transition: width 400ms ease-in-out;
display:inline-block;
margin-bottom:-4px;
width:50%;
height:100%;
}

View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{option_a}} vs {{option_b}}!</title>
<base href="/index.html">
<meta name = "viewport" content = "width=device-width, initial-scale = 1.0">
<meta name="keywords" content="docker-compose, docker, stack">
<meta name="author" content="Tutum dev team">
<link rel='stylesheet' href="{{ url_for('static',filename='stylesheets/style.css') }}" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
</head>
<body>
<div id="content-container">
<div id="content-container-center">
<h3>{{option_a}} vs {{option_b}}!</h3>
<form id="choice" name='form' method="POST" action="/">
<button id="a" type="submit" name="vote" class="a" value="a">{{option_a}}</button>
<button id="b" type="submit" name="vote" class="b" value="b">{{option_b}}</button>
</form>
<div id="tip">
(Tip: you can change your vote)
</div>
<div id="hostname">
Vote processed by {{hostname}}
</div>
</div>
</div>
<script src="http://code.jquery.com/jquery-latest.min.js" type="text/javascript"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.js"></script>
{% if vote %}
<script>
var vote = "{{vote}}";
if(vote == "a"){
$(".a").prop('disabled', true);
$(".a").html('{{option_a}} <i class="fa fa-check-circle"></i>');
$(".b").css('opacity','0.5');
}
if(vote == "b"){
$(".b").prop('disabled', true);
$(".b").html('{{option_b}} <i class="fa fa-check-circle"></i>');
$(".a").css('opacity','0.5');
}
</script>
{% endif %}
</body>
</html>

View File

@ -0,0 +1,17 @@
import time
from redis import Redis, ConnectionError
def connect_to_redis(host):
time.sleep(2)
print "Connecting to redis"
while True:
try:
redis = Redis(host=host, db=0)
redis.ping()
print "Connected to redis"
return redis
except ConnectionError:
print "Failed to connect to redis - retrying"
time.sleep(1)

View File

@ -0,0 +1,15 @@
FROM java:7
RUN apt-get update -qq && apt-get install -y maven && apt-get clean
WORKDIR /code
ADD pom.xml /code/pom.xml
RUN ["mvn", "dependency:resolve"]
RUN ["mvn", "verify"]
# Adding source, compile and package into a fat jar
ADD src /code/src
RUN ["mvn", "package"]
CMD ["/usr/lib/jvm/java-7-openjdk-amd64/bin/java", "-jar", "target/worker-jar-with-dependencies.jar"]

84
vote-apps/worker/pom.xml Normal file
View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>worker</groupId>
<artifactId>worker</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20140107</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.2</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.4-1200-jdbc41</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<finalName>worker</finalName>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>worker.Worker</mainClass>
<classpathPrefix>dependency-jars/</classpathPrefix>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>attached</goal>
</goals>
<phase>package</phase>
<configuration>
<finalName>worker</finalName>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>worker.Worker</mainClass>
</manifest>
</archive>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,101 @@
package worker;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.exceptions.JedisConnectionException;
import java.sql.*;
import org.json.JSONObject;
class Worker {
public static void main(String[] args) {
try {
Jedis redis = connectToRedis("voteapps_redis_1");
Connection dbConn = connectToDB("voteapps_db_1");
System.err.println("Watching vote queue");
while (true) {
String voteJSON = redis.blpop(0, "votes").get(1);
JSONObject voteData = new JSONObject(voteJSON);
String voterID = voteData.getString("voter_id");
String vote = voteData.getString("vote");
System.err.printf("Processing vote for '%s' by '%s'\n", vote, voterID);
updateVote(dbConn, voterID, vote);
}
} catch (SQLException e) {
e.printStackTrace();
System.exit(1);
}
}
static void updateVote(Connection dbConn, String voterID, String vote) throws SQLException {
PreparedStatement insert = dbConn.prepareStatement(
"INSERT INTO votes (id, vote) VALUES (?, ?)");
insert.setString(1, voterID);
insert.setString(2, vote);
try {
insert.executeUpdate();
} catch (SQLException e) {
PreparedStatement update = dbConn.prepareStatement(
"UPDATE votes SET vote = ? WHERE id = ?");
update.setString(1, vote);
update.setString(2, voterID);
update.executeUpdate();
}
}
static Jedis connectToRedis(String host) {
Jedis conn = new Jedis(host);
while (true) {
try {
conn.keys("*");
break;
} catch (JedisConnectionException e) {
System.err.println("Failed to connect to redis - retrying");
sleep(1000);
}
}
System.err.println("Connected to redis");
return conn;
}
static Connection connectToDB(String host) throws SQLException {
Connection conn = null;
try {
Class.forName("org.postgresql.Driver");
String url = "jdbc:postgresql://" + host + "/postgres";
while (conn == null) {
try {
conn = DriverManager.getConnection(url, "postgres", "");
} catch (SQLException e) {
System.err.println("Failed to connect to db - retrying");
sleep(1000);
}
}
PreparedStatement st = conn.prepareStatement(
"CREATE TABLE IF NOT EXISTS votes (id VARCHAR(255) NOT NULL UNIQUE, vote VARCHAR(255) NOT NULL)");
st.executeUpdate();
} catch (ClassNotFoundException e) {
e.printStackTrace();
System.exit(1);
}
return conn;
}
static void sleep(long duration) {
try {
Thread.sleep(duration);
} catch (InterruptedException e) {
System.exit(1);
}
}
}