From 2e41886591926a1fa214c58add0a4f414fcda739 Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Sun, 20 Aug 2017 19:40:46 -0400 Subject: [PATCH] Refactor Containers into a plugin (#348) * Removing Containers code * Closes #301 --- CTFd/__init__.py | 3 +- CTFd/admin/__init__.py | 3 +- CTFd/admin/challenges.py | 2 +- CTFd/admin/containers.py | 67 ------- CTFd/admin/keys.py | 2 +- CTFd/admin/pages.py | 2 +- CTFd/admin/scoreboard.py | 2 +- CTFd/admin/statistics.py | 2 +- CTFd/admin/teams.py | 2 +- CTFd/models.py | 13 -- CTFd/themes/admin/templates/base.html | 3 - CTFd/themes/admin/templates/containers.html | 163 ------------------ CTFd/themes/admin/templates/page.html | 3 - CTFd/utils.py | 149 +--------------- ..._of_using_a_plugin_to_manage_containers.py | 32 ++++ tests/test_admin_facing.py | 10 -- 16 files changed, 41 insertions(+), 417 deletions(-) delete mode 100644 CTFd/admin/containers.py delete mode 100644 CTFd/themes/admin/templates/containers.html create mode 100644 migrations/versions/cbf5620f8e15_removes_containers_table_in_favor_of_using_a_plugin_to_manage_containers.py diff --git a/CTFd/__init__.py b/CTFd/__init__.py index c52e23b..8226cde 100644 --- a/CTFd/__init__.py +++ b/CTFd/__init__.py @@ -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 diff --git a/CTFd/admin/__init__.py b/CTFd/admin/__init__.py index 36cd996..92abd0c 100644 --- a/CTFd/admin/__init__.py +++ b/CTFd/admin/__init__.py @@ -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 diff --git a/CTFd/admin/challenges.py b/CTFd/admin/challenges.py index 64e730a..a5e4699 100644 --- a/CTFd/admin/challenges.py +++ b/CTFd/admin/challenges.py @@ -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 diff --git a/CTFd/admin/containers.py b/CTFd/admin/containers.py deleted file mode 100644 index d139f9d..0000000 --- a/CTFd/admin/containers.py +++ /dev/null @@ -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//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//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//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')) diff --git a/CTFd/admin/keys.py b/CTFd/admin/keys.py index 111ff76..8a051bb 100644 --- a/CTFd/admin/keys.py +++ b/CTFd/admin/keys.py @@ -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 diff --git a/CTFd/admin/pages.py b/CTFd/admin/pages.py index 4e06697..a6fc12b 100644 --- a/CTFd/admin/pages.py +++ b/CTFd/admin/pages.py @@ -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 diff --git a/CTFd/admin/scoreboard.py b/CTFd/admin/scoreboard.py index 1bc0cf2..8f86fc4 100644 --- a/CTFd/admin/scoreboard.py +++ b/CTFd/admin/scoreboard.py @@ -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 diff --git a/CTFd/admin/statistics.py b/CTFd/admin/statistics.py index 78174a3..73febb8 100644 --- a/CTFd/admin/statistics.py +++ b/CTFd/admin/statistics.py @@ -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 diff --git a/CTFd/admin/teams.py b/CTFd/admin/teams.py index 7ade797..263ab5e 100644 --- a/CTFd/admin/teams.py +++ b/CTFd/admin/teams.py @@ -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_ diff --git a/CTFd/models.py b/CTFd/models.py index f56e91c..c212030 100644 --- a/CTFd/models.py +++ b/CTFd/models.py @@ -41,19 +41,6 @@ class Pages(db.Model): return "".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 "".format(self.id, self.name) - - class Challenges(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(80)) diff --git a/CTFd/themes/admin/templates/base.html b/CTFd/themes/admin/templates/base.html index 21faea6..595ea32 100644 --- a/CTFd/themes/admin/templates/base.html +++ b/CTFd/themes/admin/templates/base.html @@ -47,9 +47,6 @@
  • Teams
  • Scoreboard
  • - {% if can_create_container() %} -
  • Containers
  • - {% endif %}
  • Challenges
  • Statistics
  • Config
  • diff --git a/CTFd/themes/admin/templates/containers.html b/CTFd/themes/admin/templates/containers.html deleted file mode 100644 index 82496aa..0000000 --- a/CTFd/themes/admin/templates/containers.html +++ /dev/null @@ -1,163 +0,0 @@ -{% extends "admin/base.html" %} - -{% block content %} - - - - - - -
    -
    -
    -

    Containers

    - -
    -
    -{% if containers %} - - - - - - - - - - - {% for c in containers %} - - - - - - - {% endfor %} - -
    Status - Name - Ports - Settings -
    {{ c.status }}{{ c.name }}{{ c.ports }} - - {% if c.status != 'running' %} - - {% else %} - - {% endif %} - - -
    -{% endif %} -
    -{% endblock %} - -{% block scripts %} - -{% endblock %} \ No newline at end of file diff --git a/CTFd/themes/admin/templates/page.html b/CTFd/themes/admin/templates/page.html index 5fb171b..e2c043a 100644 --- a/CTFd/themes/admin/templates/page.html +++ b/CTFd/themes/admin/templates/page.html @@ -47,9 +47,6 @@
  • Teams
  • Scoreboard
  • - {% if can_create_container() %} -
  • Containers
  • - {% endif %}
  • Challenges
  • Statistics
  • Config
  • diff --git a/CTFd/utils.py b/CTFd/utils.py index 34744c2..07f6855 100644 --- a/CTFd/utils.py +++ b/CTFd/utils.py @@ -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: diff --git a/migrations/versions/cbf5620f8e15_removes_containers_table_in_favor_of_using_a_plugin_to_manage_containers.py b/migrations/versions/cbf5620f8e15_removes_containers_table_in_favor_of_using_a_plugin_to_manage_containers.py new file mode 100644 index 0000000..6e9dc43 --- /dev/null +++ b/migrations/versions/cbf5620f8e15_removes_containers_table_in_favor_of_using_a_plugin_to_manage_containers.py @@ -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 ### diff --git a/tests/test_admin_facing.py b/tests/test_admin_facing.py index 598ede4..2878eab 100644 --- a/tests/test_admin_facing.py +++ b/tests/test_admin_facing.py @@ -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()