Skip to content

Commit 232dc1c

Browse files
committed
Introducing swarm services option
1 parent 81f897e commit 232dc1c

28 files changed

Lines changed: 8464 additions & 3 deletions

app/assets/javascripts/problems.coffee

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,65 @@ setup_hooks = ->
2727
return
2828
return
2929

30+
setup_console = ->
31+
if !document.getElementById('drawer')
32+
return
33+
34+
# Update deadlines
35+
deadline = new Date(Date.parse(new Date) + expiration * 1000)
36+
37+
getTimeRemaining = (endtime) ->
38+
t = Date.parse(endtime) - Date.parse(new Date)
39+
seconds = Math.floor(t / 1000 % 60)
40+
minutes = Math.floor(t / 1000 / 60 % 60)
41+
hours = Math.floor(t / (1000 * 60 * 60) % 24)
42+
{
43+
'total': t
44+
'hours': hours
45+
'minutes': minutes
46+
'seconds': seconds
47+
}
48+
49+
initializeClock = (id, endtime) ->
50+
clock = document.getElementById(id)
51+
hoursSpan = clock.querySelector('.hours')
52+
minutesSpan = clock.querySelector('.minutes')
53+
secondsSpan = clock.querySelector('.seconds')
54+
55+
updateClock = ->
56+
t = getTimeRemaining(endtime)
57+
hoursSpan.innerHTML = ('0' + t.hours).slice(-2)
58+
minutesSpan.innerHTML = ('0' + t.minutes).slice(-2)
59+
secondsSpan.innerHTML = ('0' + t.seconds).slice(-2)
60+
if t.total <= 0
61+
clearInterval timeinterval
62+
clock.innerHTML = "This session has expired. Re-authenticate for a new one."
63+
return
64+
65+
updateClock()
66+
timeinterval = setInterval(updateClock, 1000)
67+
return
68+
69+
initializeClock 'expiration', deadline
70+
71+
# Get the console URL
72+
check_url = ->
73+
$.ajax
74+
url: '/console_url'
75+
method: 'GET'
76+
accepts: text: 'application/json'
77+
statusCode:
78+
200: (data) ->
79+
$('#loader').remove()
80+
window.open(data.url, 'docker_term')
81+
return
82+
102: ->
83+
setTimeout check_url, 1000
84+
return
85+
return
86+
87+
check_url()
88+
return
89+
3090
$(document).on('turbolinks:load', setup_hooks)
91+
$(document).on('turbolinks:load', setup_console)

app/assets/stylesheets/application.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
* defined in the other CSS/SCSS files in this directory. It is generally better to create a new
1010
* file per style scope.
1111
*
12+
*= require_tree .
13+
*= require_self
1214
*/
1315
@import "bootstrap-sprockets";
1416
@import "bootstrap";
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,161 @@
11
// Place all the styles related to the problems controller here.
22
// They will automatically be included in application.css.
33
// You can use Sass (SCSS) here: http://sass-lang.com/
4+
5+
* { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; box-sizing: border-box; /* adds animation for all transitions */ -webkit-transition: .25s ease-in-out; -moz-transition: .25s ease-in-out; -o-transition: .25s ease-in-out; transition: .25s ease-in-out; margin: 0; padding: 0; -webkit-text-size-adjust: none; }
6+
7+
.expiration {
8+
font-size: 0.5em;
9+
}
10+
11+
.docker_term {
12+
width: 800px;
13+
height: 450px;
14+
position: absolute;
15+
top: 50%;
16+
left: 50%;
17+
margin-top: -225px;
18+
margin-left: -400px;
19+
background: #000;
20+
overflow: hidden;
21+
}
22+
23+
#drawer-toggle {
24+
position: absolute;
25+
bottom: 0;
26+
opacity: 0;
27+
}
28+
29+
#drawer-toggle-label {
30+
-webkit-touch-callout: none;
31+
-webkit-user-select: none;
32+
-khtml-user-select: none;
33+
-moz-user-select: none;
34+
-ms-user-select: none;
35+
user-select: none;
36+
bottom: 0px;
37+
left: 0px;
38+
height:50px;
39+
width: 50px;
40+
display: block;
41+
position: fixed;
42+
background: rgba(255,255,255,.0);
43+
z-index: 1000;
44+
}
45+
46+
/* adds our "hamburger" menu icon */
47+
48+
#drawer-toggle-label:before {
49+
content: '';
50+
display: block;
51+
position: absolute;
52+
height: 2px;
53+
width: 24px;
54+
background: #8d8d8d;
55+
left: 13px;
56+
top: 24px;
57+
box-shadow: 0 6px 0 #8d8d8d, 0 12px 0 #8d8d8d;
58+
}
59+
60+
#console-header {
61+
width: 100%;
62+
position: fixed;
63+
left: 0px;
64+
bottom: 0px;
65+
background: #efefef;
66+
padding: 10px 10px 10px 50px;
67+
font-size: 30px;
68+
line-height: 30px;
69+
z-index: 998;
70+
}
71+
72+
/* drawer menu pane - note the 0px width */
73+
74+
#drawer {
75+
position: fixed;
76+
bottom: -500px;
77+
left: 0;
78+
height: 500px;
79+
width: 100%;
80+
background: #2f2f2f;
81+
overflow-x: hidden;
82+
overflow-y: scroll;
83+
padding: 20px;
84+
z-index: 999;
85+
-webkit-overflow-scrolling: touch;
86+
}
87+
88+
/* actual page content pane */
89+
#page-content {
90+
margin-left: 0px;
91+
margin-bottom: 50px;
92+
padding-bottom: 50px;
93+
width: 100%;
94+
height: calc(100% - 50px);
95+
overflow-x:hidden;
96+
overflow-y:scroll;
97+
-webkit-overflow-scrolling: touch;
98+
padding: 20px;
99+
}
100+
101+
/* checked styles (menu open state) */
102+
#drawer-toggle:checked ~ #drawer-toggle-label {
103+
width: 100%;
104+
}
105+
106+
#drawer-toggle:checked ~ #drawer-toggle-label,
107+
#drawer-toggle:checked ~ #console-header {
108+
bottom: 500px;
109+
}
110+
111+
#drawer-toggle:checked ~ #drawer {
112+
bottom: 0px;
113+
}
114+
115+
#drawer-toggle:checked ~ #page-content {
116+
margin-bottom: 350px;
117+
}
118+
119+
/* Responsive MQ */
120+
@media all and (max-width:350px) {
121+
122+
#drawer-toggle:checked ~ #drawer-toggle-label {
123+
width: 100%;
124+
height: 50px;
125+
}
126+
127+
#drawer-toggle:checked ~ #drawer-toggle-label,
128+
#drawer-toggle:checked ~ #console-header {
129+
bottom: calc(100% - 50px);
130+
}
131+
132+
#drawer-toggle:checked ~ #drawer {
133+
height: calc(100% - 50px);
134+
padding: 20px;
135+
}
136+
137+
#drawer-toggle:checked ~ #page-content {
138+
margin-bottom: calc(100% - 50px);
139+
}
140+
141+
}
142+
143+
#loader {
144+
border: 16px solid #f3f3f3; /* Light grey */
145+
border-top: 16px solid #3498db; /* Blue */
146+
border-radius: 50%;
147+
position: absolute;
148+
top: 50%;
149+
left: 50%;
150+
margin-top: -40px;
151+
margin-left: -40px;
152+
width: 80px;
153+
height: 80px;
154+
animation: spin 2s linear infinite;
155+
}
156+
157+
@keyframes spin {
158+
0% { transform: rotate(0deg); }
159+
100% { transform: rotate(360deg); }
160+
}
161+

app/controllers/problems_controller.rb

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ def index
1616
end
1717
@points_available = Problem.where(visible: true).sum(:points)
1818

19+
# Build URL for webconsole
20+
@expiration = current_user.stack_expiry
21+
1922
if params[:problem_id]
2023
# If a specific problem was open, keep it open
2124
@problem_view = Problem.find(params[:problem_id])
@@ -116,9 +119,57 @@ def update
116119
end
117120
end
118121

122+
def console_url
123+
unless current_user.container_id.nil? or current_user.container_id.empty?
124+
@console_host = ENV.fetch("CONSOLE_HOST") { "http://127.0.0.1:8888" }
125+
@hash = Digest::SHA1.hexdigest(
126+
"#{current_user.id}#{current_user.container_id}"
127+
)
128+
@params = CGI.escape(Base64.strict_encode64(
129+
"#{@hash},#{current_user.problem_id},#{current_user.id}"
130+
))
131+
@status = :ok
132+
@render = { url: "#{@console_host}/?q=#{@params}" }
133+
else
134+
@status = :processing
135+
@render = { error: 'Container not ready yet...' }
136+
137+
end
138+
respond_to do |format|
139+
format.json { render json: @render.to_json, status: @status }
140+
end
141+
end
142+
143+
def start_stack
144+
if swarm_services_enabled?
145+
@problem = Problem.find(params[:id])
146+
147+
if @problem.stack.empty? or @problem.network.empty?
148+
flash[:danger] = "No stack or network defined for that challenge!"
149+
redirect_to @problem
150+
return
151+
end
152+
153+
challenge = Hash.new(0)
154+
challenge["user_id"] = current_user.id
155+
challenge["problem_id"] = @problem.id
156+
challenge["network"] = @problem.network
157+
challenge["containers"] = @problem.stack
158+
challenge["lifespan"] = "30"
159+
CreateStackJob.perform_later challenge
160+
DestroyStackJob.set(wait: 30.minutes)
161+
.perform_later(challenge)
162+
163+
redirect_to :back
164+
else
165+
flash[:danger] = "Unauthorized"
166+
redirect_to @problem
167+
end
168+
end
169+
119170
private
120171
def problem_params
121-
params.require(:problem).permit(:name, :category, :description, :points, :solution, :correct_message, :false_message, :picture, :visible, :solution_case_sensitive, :solution_regex, :dependent_problems)
172+
params.require(:problem).permit(:name, :category, :description, :points, :solution, :correct_message, :false_message, :picture, :visible, :solution_case_sensitive, :solution_regex, :dependent_problems, :network, :stack)
122173
end
123174

124175
def belong_to_team

app/jobs/create_stack_job.rb

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
class CreateStackJob < DockerApiJob
2+
queue_as :default
3+
4+
# Takes hash: challenge
5+
# user_id
6+
# problem_id
7+
# network
8+
# containers
9+
# lifespan
10+
def perform(challenge)
11+
check_existing(challenge)
12+
create_stack(challenge)
13+
end
14+
15+
private
16+
def check_existing(challenge)
17+
containers = docker_get_containers(challenge)
18+
networks = docker_get_networks(challenge)
19+
20+
if networks.length > 0 or containers.length > 0
21+
user_id = challenge['user_id']
22+
User.find(user_id).update_attribute(:container_id, '')
23+
User.find(user_id).update_attribute(:stack_expiry, DateTime.now)
24+
User.find(user_id).update_attribute(:problem_id, -1)
25+
delete_stack(containers, networks)
26+
end
27+
end
28+
29+
def create_stack(challenge)
30+
# Start with network (Name: problem_id-user_id)
31+
network = JSON(challenge['network'])
32+
network['Name'] = "hta-#{challenge['problem_id']}-#{challenge['user_id']}"
33+
network['Labels'] = {
34+
"user_id" => "#{challenge['user_id']}",
35+
"problem_id" => "#{challenge['problem_id']}",
36+
"lifetime" => "#{challenge['lifespan']}"
37+
}
38+
res = docker_post_request('/networks/create', network.to_json)
39+
network_id = JSON(res.body)["Id"]
40+
41+
if res.code != '201'
42+
raise Exception.new("Couldn't create challenge network.")
43+
end
44+
45+
# Then build containers
46+
containers = JSON(challenge['containers'])
47+
containers.each do |container|
48+
container['Labels'] = {
49+
"user_id" => "#{challenge['user_id']}",
50+
"problem_id" => "#{challenge['problem_id']}",
51+
"lifetime" => "#{challenge['lifespan']}"
52+
}
53+
54+
# Make sure the container is reachable by its designed name
55+
container['Networks'] = [{
56+
"Target": network_id,
57+
"Aliases": [
58+
container['Name'],
59+
container['Name'].gsub("-","."),
60+
]
61+
}]
62+
63+
# find entrypoint
64+
is_entry = false
65+
if container['Name'].downcase == "entrypoint"
66+
is_entry = true
67+
end
68+
69+
# Add user_id to container name for identification
70+
container_name = container['Name']
71+
container['Name'] = "hta-#{container_name}-#{challenge['user_id']}"
72+
73+
res = docker_post_request('/services/create', container.to_json)
74+
if res.code != '201'
75+
raise Exception.new("Couldn't create challenge containers.")
76+
end
77+
78+
# If container is entry point, assign to user
79+
if is_entry
80+
id = JSON(res.body)["ID"]
81+
User.find(challenge['user_id']).update_attribute(:container_id, id)
82+
User.find(challenge['user_id']).update_attribute(:stack_expiry, DateTime.now + Integer(challenge['lifespan']).minutes)
83+
User.find(challenge['user_id']).update_attribute(:problem_id, challenge['problem_id'])
84+
end
85+
end
86+
end
87+
88+
end

0 commit comments

Comments
 (0)