Refactor Containers into a plugin (#348)

* Removing Containers code
* Closes #301
selenium-screenshot-testing
Kevin Chung 2017-08-20 19:40:46 -04:00 committed by GitHub
parent 92b7ca06ca
commit 2e41886591
16 changed files with 41 additions and 417 deletions

View File

@ -113,7 +113,7 @@ def create_app(config='CTFd.config.Config'):
from CTFd.challenges import challenges
from CTFd.scoreboard import scoreboard
from CTFd.auth import auth
from CTFd.admin import admin, admin_statistics, admin_challenges, admin_pages, admin_scoreboard, admin_containers, admin_keys, admin_teams
from CTFd.admin import admin, admin_statistics, admin_challenges, admin_pages, admin_scoreboard, admin_keys, admin_teams
from CTFd.utils import init_utils, init_errors, init_logs
init_utils(app)
@ -131,7 +131,6 @@ def create_app(config='CTFd.config.Config'):
app.register_blueprint(admin_teams)
app.register_blueprint(admin_scoreboard)
app.register_blueprint(admin_keys)
app.register_blueprint(admin_containers)
app.register_blueprint(admin_pages)
from CTFd.plugins import init_plugins

View File

@ -10,7 +10,7 @@ from sqlalchemy.sql import not_
from sqlalchemy.exc import IntegrityError
from CTFd.utils import admins_only, is_admin, cache, export_ctf, import_ctf
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd.scoreboard import get_standings
from CTFd.plugins.keys import get_key_class, KEY_CLASSES
@ -18,7 +18,6 @@ from CTFd.admin.statistics import admin_statistics
from CTFd.admin.challenges import admin_challenges
from CTFd.admin.scoreboard import admin_scoreboard
from CTFd.admin.pages import admin_pages
from CTFd.admin.containers import admin_containers
from CTFd.admin.keys import admin_keys
from CTFd.admin.teams import admin_teams

View File

@ -1,6 +1,6 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from CTFd.utils import admins_only, is_admin, cache
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, Hints, Unlocks, DatabaseError
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, Hints, Unlocks, DatabaseError
from CTFd.plugins.keys import get_key_class, KEY_CLASSES
from CTFd.plugins.challenges import get_chal_class, CHALLENGE_CLASSES

View File

@ -1,67 +0,0 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from CTFd.utils import admins_only, is_admin, cache
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd import utils
admin_containers = Blueprint('admin_containers', __name__)
@admin_containers.route('/admin/containers', methods=['GET'])
@admins_only
def list_container():
containers = Containers.query.all()
for c in containers:
c.status = utils.container_status(c.name)
c.ports = ', '.join(utils.container_ports(c.name, verbose=True))
return render_template('admin/containers.html', containers=containers)
@admin_containers.route('/admin/containers/<int:container_id>/stop', methods=['POST'])
@admins_only
def stop_container(container_id):
container = Containers.query.filter_by(id=container_id).first_or_404()
if utils.container_stop(container.name):
return '1'
else:
return '0'
@admin_containers.route('/admin/containers/<int:container_id>/start', methods=['POST'])
@admins_only
def run_container(container_id):
container = Containers.query.filter_by(id=container_id).first_or_404()
if utils.container_status(container.name) == 'missing':
if utils.run_image(container.name):
return '1'
else:
return '0'
else:
if utils.container_start(container.name):
return '1'
else:
return '0'
@admin_containers.route('/admin/containers/<int:container_id>/delete', methods=['POST'])
@admins_only
def delete_container(container_id):
container = Containers.query.filter_by(id=container_id).first_or_404()
if utils.delete_image(container.name):
db.session.delete(container)
db.session.commit()
db.session.close()
return '1'
@admin_containers.route('/admin/containers/new', methods=['POST'])
@admins_only
def new_container():
name = request.form.get('name')
if not set(name) <= set('abcdefghijklmnopqrstuvwxyz0123456789-_'):
return redirect(url_for('admin_containers.list_container'))
buildfile = request.form.get('buildfile')
files = request.files.getlist('files[]')
utils.create_image(name=name, buildfile=buildfile, files=files)
utils.run_image(name)
return redirect(url_for('admin_containers.list_container'))

View File

@ -1,6 +1,6 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from CTFd.utils import admins_only, is_admin, cache
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd.plugins.keys import get_key_class, KEY_CLASSES
from CTFd import utils

View File

@ -1,6 +1,6 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from CTFd.utils import admins_only, is_admin, cache
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd import utils

View File

@ -1,6 +1,6 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from CTFd.utils import admins_only, is_admin, cache
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd.scoreboard import get_standings
from CTFd import utils

View File

@ -1,6 +1,6 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from CTFd.utils import admins_only, is_admin, cache
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd import utils

View File

@ -1,6 +1,6 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from CTFd.utils import admins_only, is_admin, cache
from CTFd.models import db, Teams, Solves, Awards, Unlocks, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd.models import db, Teams, Solves, Awards, Unlocks, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from passlib.hash import bcrypt_sha256
from sqlalchemy.sql import not_

View File

@ -41,19 +41,6 @@ class Pages(db.Model):
return "<Pages route {0}>".format(self.route)
class Containers(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80))
buildfile = db.Column(db.Text)
def __init__(self, name, buildfile):
self.name = name
self.buildfile = buildfile
def __repr__(self):
return "<Container ID:(0) {1}>".format(self.id, self.name)
class Challenges(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80))

View File

@ -47,9 +47,6 @@
</li>
<li><a href="{{ request.script_root }}/admin/teams">Teams</a></li>
<li><a href="{{ request.script_root }}/admin/scoreboard">Scoreboard</a></li>
{% if can_create_container() %}
<li><a href="{{ request.script_root }}/admin/containers">Containers</a></li>
{% endif %}
<li><a href="{{ request.script_root }}/admin/chals">Challenges</a></li>
<li><a href="{{ request.script_root }}/admin/statistics">Statistics</a></li>
<li><a href="{{ request.script_root }}/admin/config">Config</a></li>

View File

@ -1,163 +0,0 @@
{% extends "admin/base.html" %}
{% block content %}
<div class="modal fade" id="create-container-modal" tabindex="-1" role="dialog" aria-labelledby="container-modal-label">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="container-modal-label">Create Container</h4>
</div>
<form method="POST" action="{{ request.script_root }}/admin/containers/new" enctype="multipart/form-data">
<div class="modal-body">
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" name="name" placeholder="Enter container name">
</div>
<div class="form-group">
<label for="buildfile-editor" class="control-label">Build File</label>
<textarea id="buildfile-editor" class="form-control" name="buildfile" rows="10" placeholder="Enter container build file"></textarea>
</div>
<div class="form-group">
<label for="container-files">Associated Files
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="These files are uploaded alongside your buildfile"></i>
</label>
<input type="file" name="files[]" id="container-files" multiple>
<sub class="help-block">Attach multiple files using Control+Click or Cmd+Click.</sub>
</div>
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Create</button>
</div>
</form>
</div>
</div>
</div>
<div id="confirm" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h2 class="text-center"><span id="confirm-container-title"></span> Container</h2>
</div>
<div class="modal-body" style="height:110px">
<div class="row-fluid">
<div class="col-md-12">
<form method="POST">
<input id="nonce" type="hidden" name="nonce" value="{{ nonce }}">
<div class="small-6 small-centered text-center columns">
<p>Are you sure you want to <span id="confirm-container-method"></span> <strong id="confirm-container-name"></strong>?</p>
<button type="button" data-dismiss="modal" class="btn btn-theme btn-outlined">No</button>
<button type="button" id="confirm-container" class="btn btn-theme btn-outlined">Yes</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<br>
<div style="text-align:center">
<h1>Containers</h1>
<button class="btn btn-theme btn-outlined create-challenge" data-toggle="modal" data-target="#create-container-modal">
New Container
</button>
</div>
<br>
{% if containers %}
<table id="teamsboard">
<thead>
<tr>
<td class="text-center"><strong>Status</strong>
</td>
<td class="text-center"><strong>Name</strong>
</td>
<td class="text-center"><strong>Ports</strong>
</td>
<td class="text-center"><strong>Settings</strong>
</td>
</tr>
</thead>
<tbody>
{% for c in containers %}
<tr>
<td class="text-center">{{ c.status }}</td>
<td class="text-center container_item" id="{{ c.id }}">{{ c.name }}</td>
<td class="text-center">{{ c.ports }}</td>
<td class="text-center">
<span>
{% if c.status != 'running' %}
<i class="fa fa-play"></i>
{% else %}
<i class="fa fa-stop"></i>
{% endif %}
<i class="fa fa-times"></i>
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script>
function load_confirm_modal(title, url, container_name){
var modal = $('#confirm')
modal.find('#confirm-container-name').text(container_name)
modal.find('#confirm-container-title').text(title)
modal.find('#confirm-container-method').text(title.toLowerCase())
$('#confirm form').attr('action', url);
$('#confirm').modal('show');
}
$('#confirm-container').click(function(e){
e.preventDefault();
var id = $('#confirm input[name="id"]').val()
var user_data = $('#confirm form').serializeArray()
$.post($('#confirm form').attr('action'), $('#confirm form').serialize(), function(data){
var data = $.parseJSON(JSON.stringify(data))
if (data == "1"){
location.reload()
}
})
});
$('.fa-times').click(function(){
var elem = $(this).parent().parent().parent().find('.container_item');
var container = elem.attr('id');
var container_name = elem.text().trim();
load_confirm_modal('Delete', '/admin/containers/'+container+'/delete', container_name)
});
$('.fa-play').click(function(){
var elem = $(this).parent().parent().parent().find('.container_item');
var container = elem.attr('id');
var container_name = elem.text().trim();
load_confirm_modal('Start', '/admin/containers/'+container+'/start', container_name)
});
$('.fa-stop').click(function(){
var elem = $(this).parent().parent().parent().find('.container_item');
var container = elem.attr('id');
var container_name = elem.text().trim();
load_confirm_modal('Stop', '/admin/containers/'+container+'/stop', container_name)
});
$(document).ready(function(){
$('[data-toggle="tooltip"]').tooltip();
});
</script>
{% endblock %}

View File

@ -47,9 +47,6 @@
</li>
<li><a href="{{ request.script_root }}/admin/teams">Teams</a></li>
<li><a href="{{ request.script_root }}/admin/scoreboard">Scoreboard</a></li>
{% if can_create_container() %}
<li><a href="{{ request.script_root }}/admin/containers">Containers</a></li>
{% endif %}
<li><a href="{{ request.script_root }}/admin/chals">Challenges</a></li>
<li><a href="{{ request.script_root }}/admin/statistics">Statistics</a></li>
<li><a href="{{ request.script_root }}/admin/config">Config</a></li>

View File

@ -13,7 +13,6 @@ import shutil
import six
import smtplib
import socket
import subprocess
import sys
import tempfile
import time
@ -29,7 +28,7 @@ from itsdangerous import TimedSerializer, BadTimeSignature, Signer, BadSignature
from six.moves.urllib.parse import urlparse, urljoin, quote, unquote
from werkzeug.utils import secure_filename
from CTFd.models import db, WrongKeys, Pages, Config, Tracking, Teams, Files, Containers, ip2long, long2ip
from CTFd.models import db, WrongKeys, Pages, Config, Tracking, Teams, Files, ip2long, long2ip
cache = Cache()
migrate = Migrate()
@ -105,7 +104,6 @@ def init_utils(app):
app.jinja_env.globals.update(can_send_mail=can_send_mail)
app.jinja_env.globals.update(ctf_name=ctf_name)
app.jinja_env.globals.update(ctf_theme=ctf_theme)
app.jinja_env.globals.update(can_create_container=can_create_container)
app.jinja_env.globals.update(get_configurable_plugins=get_configurable_plugins)
app.jinja_env.globals.update(get_config=get_config)
app.jinja_env.globals.update(hide_scores=hide_scores)
@ -557,136 +555,6 @@ def base64decode(s, urldecode=False):
return decoded
@cache.memoize()
def can_create_container():
try:
subprocess.check_output(['docker', 'version'])
return True
except (subprocess.CalledProcessError, OSError):
return False
def is_port_free(port):
s = socket.socket()
result = s.connect_ex(('127.0.0.1', port))
if result == 0:
s.close()
return False
return True
def create_image(name, buildfile, files):
if not can_create_container():
return False
folder = tempfile.mkdtemp(prefix='ctfd')
tmpfile = tempfile.NamedTemporaryFile(dir=folder, delete=False)
tmpfile.write(buildfile)
tmpfile.close()
for f in files:
if f.filename.strip():
filename = os.path.basename(f.filename)
f.save(os.path.join(folder, filename))
# repository name component must match "[a-z0-9](?:-*[a-z0-9])*(?:[._][a-z0-9](?:-*[a-z0-9])*)*"
# docker build -f tmpfile.name -t name
try:
cmd = ['docker', 'build', '-f', tmpfile.name, '-t', name, folder]
print(cmd)
subprocess.call(cmd)
container = Containers(name, buildfile)
db.session.add(container)
db.session.commit()
db.session.close()
rmdir(folder)
return True
except subprocess.CalledProcessError:
return False
def delete_image(name):
try:
subprocess.call(['docker', 'rm', name])
subprocess.call(['docker', 'rmi', name])
return True
except subprocess.CalledProcessError:
return False
def run_image(name):
try:
info = json.loads(subprocess.check_output(['docker', 'inspect', '--type=image', name]))
try:
ports_asked = info[0]['Config']['ExposedPorts'].keys()
ports_asked = [int(re.sub('[A-Za-z/]+', '', port)) for port in ports_asked]
except KeyError:
ports_asked = []
cmd = ['docker', 'run', '-d']
ports_used = []
for port in ports_asked:
if is_port_free(port):
cmd.append('-p')
cmd.append('{}:{}'.format(port, port))
else:
cmd.append('-p')
ports_used.append('{}'.format(port))
cmd += ['--name', name, name]
print(cmd)
subprocess.call(cmd)
return True
except subprocess.CalledProcessError:
return False
def container_start(name):
try:
cmd = ['docker', 'start', name]
subprocess.call(cmd)
return True
except subprocess.CalledProcessError:
return False
def container_stop(name):
try:
cmd = ['docker', 'stop', name]
subprocess.call(cmd)
return True
except subprocess.CalledProcessError:
return False
def container_status(name):
try:
data = json.loads(subprocess.check_output(['docker', 'inspect', '--type=container', name]))
status = data[0]["State"]["Status"]
return status
except subprocess.CalledProcessError:
return 'missing'
def container_ports(name, verbose=False):
try:
info = json.loads(subprocess.check_output(['docker', 'inspect', '--type=container', name]))
if verbose:
ports = info[0]["NetworkSettings"]["Ports"]
if not ports:
return []
final = []
for port in ports.keys():
final.append("".join([ports[port][0]["HostPort"], '->', port]))
return final
else:
ports = info[0]['Config']['ExposedPorts'].keys()
if not ports:
return []
ports = [int(re.sub('[A-Za-z/]+', '', port)) for port in ports]
return ports
except subprocess.CalledProcessError:
return []
def export_ctf(segments=None):
db = dataset.connect(get_config('SQLALCHEMY_DATABASE_URI'))
if segments is None:
@ -714,7 +582,6 @@ def export_ctf(segments=None):
'alembic_version',
'config',
'pages',
'containers',
]
}
@ -775,7 +642,6 @@ def import_ctf(backup, segments=None, erase=False):
'alembic_version',
'config',
'pages',
'containers',
]
}
@ -812,19 +678,6 @@ def import_ctf(backup, segments=None, erase=False):
db.session.add(page)
db.session.commit()
elif item == 'containers':
saved = json.loads(data)
for entry in saved['results']:
name = entry['name']
buildfile = entry['buildfile']
container = Containers.query.filter_by(name=name).first()
if container:
container.buildfile = buildfile
else:
container = Containers(name, buildfile)
db.session.add(container)
db.session.commit()
for segment in segments:
group = groups[segment]
for item in group:

View File

@ -0,0 +1,32 @@
"""Removes containers table in favor of using a plugin to manage Containers
Revision ID: cbf5620f8e15
Revises: 1ec4a28fe0ff
Create Date: 2017-08-12 04:11:45.970248
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cbf5620f8e15'
down_revision = '1ec4a28fe0ff'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('containers')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('containers',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=80), nullable=True),
sa.Column('buildfile', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###

View File

@ -44,16 +44,6 @@ def test_admin_scoreboard():
destroy_ctfd(app)
def test_admin_containers():
"""Does admin containers return a 200 by default"""
app = create_ctfd()
with app.app_context():
client = login_as_user(app, name="admin", password="password")
r = client.get('/admin/containers')
assert r.status_code == 200
destroy_ctfd(app)
def test_admin_chals():
"""Does admin chals return a 200 by default"""
app = create_ctfd()