diff --git a/biscd/biscd/__init__.py b/biscd/biscd/__init__.py index 32438ae..95ec3be 100644 --- a/biscd/biscd/__init__.py +++ b/biscd/biscd/__init__.py @@ -10,7 +10,6 @@ from flask import Flask from flask_bootstrap import Bootstrap from flask_login import LoginManager - def read_config(application): # pylint: disable=no-member conf = confuse.Configuration('biscd', __name__) diff --git a/biscd/biscd/config_default.yaml b/biscd/biscd/config_default.yaml index d22e169..739846d 100644 --- a/biscd/biscd/config_default.yaml +++ b/biscd/biscd/config_default.yaml @@ -9,5 +9,7 @@ logging: log_to_stdout: true logfile: info.log +production_path: /opt/biscd + SECRET_KEY: this-should-be-very-secret BOOTSTRAP_SERVE_LOCAL: True \ No newline at end of file diff --git a/biscd/biscd/froms.py b/biscd/biscd/froms.py index cb9f948..7ffae26 100644 --- a/biscd/biscd/froms.py +++ b/biscd/biscd/froms.py @@ -36,3 +36,6 @@ class NewProjectForm(FlaskForm): project = Project.get(name=projectname.data) if any(project): raise ValidationError('Please use a different projectname.') + +class EmptyForm(FlaskForm): + submit = SubmitField('Submit') diff --git a/biscd/biscd/models/project.py b/biscd/biscd/models/project.py index b68f23c..41acf7d 100644 --- a/biscd/biscd/models/project.py +++ b/biscd/biscd/models/project.py @@ -1,4 +1,11 @@ from pathlib import Path +from shutil import rmtree +import string +import random +from werkzeug.utils import secure_filename +from git import Repo, InvalidGitRepositoryError, GitCommandError +from flask import current_app as app + from biscd import config from .yaml_serializable import YamlSerializable @@ -14,6 +21,70 @@ class Project(YamlSerializable): def _yaml_object_name(cls): return 'projects' - def __init__(self, name=None, branche='master'): + def __init__(self, name=None, branch='master'): self.name = name - self.branche = branche + self.branch = branch + self.relative_production_path = None + self.git_repo = None + + @property + def absulute_path(self): + return Path(config['production_path'].get()) / self.relative_production_path + + def update(self): + # pylint: disable=no-member + self._make_sure_production_path_exists() + path = self.absulute_path + app.logger.info(f"Updating {path}") + try: + repo = Repo(path) + app.logger.info('Already under git control') + except InvalidGitRepositoryError: + app.logger.info(f"Cloning repo from {self.git_repo}") + repo = Repo.clone_from(self.git_repo, path) + app.logger.info('Done!') + remotes = repo.remotes + if not any(remotes): + app.logger.error(f"Repo {path} doesn't have any remotes.") + return ["Repo doesn't have any remotes. please fix manually", 'error'] + response = remotes[0].pull() + for item in response: + app.logger.debug(item) + + try: + response = repo.git.checkout(self.branch) + except AttributeError: + return [f"Branch is not set for {self.name}", 'error'] + except GitCommandError: + return [ + f"Repo '{self.git_repo}' does not have an existing branch called {self.branch}", + 'error'] + return None + + def delete_files(self): + self._make_sure_production_path_exists() + path = self.absulute_path + app.logger.info(f"Deleting {path}") + rmtree(path) + self.relative_production_path = None + self.save() + + def _make_sure_production_path_exists(self): + prod_path_dir = Path(config['production_path'].get()) + if hasattr(self, 'production_path') and self.relative_production_path: + full_path = prod_path_dir / self.relative_production_path + if not full_path.is_dir(): + full_path.mkdir(parents=True) + else: + def generate_full_path(): + rand_num = ''.join(random.choices(string.digits, k=6)) + return prod_path_dir / (secure_filename(self.name) + rand_num) + + full_path = generate_full_path() + # Make sure path is unique + while full_path.is_dir(): + full_path = generate_full_path() + + full_path.mkdir(parents=True) + self.relative_production_path = str(full_path.relative_to(prod_path_dir)) + self.save() diff --git a/biscd/biscd/models/yaml_serializable.py b/biscd/biscd/models/yaml_serializable.py index bf75b99..18e1071 100644 --- a/biscd/biscd/models/yaml_serializable.py +++ b/biscd/biscd/models/yaml_serializable.py @@ -29,11 +29,14 @@ class YamlSerializable(object): def _yaml_object_name(cls): pass - @property - def config_dict(self): + def config_dict(self, properties_only=False): # pylint: disable=no-member - ymlserializable_dict = self.__dict__.copy() + if self.name is None: + raise TypeError("Name cannot be None") + ymlserializable_dict = {k: v for k, v in self.__dict__.items() if v is not None} ymlserializable_dict.pop('name') + if properties_only: + return ymlserializable_dict return {self.name: ymlserializable_dict} def save(self, overwrite=True): @@ -41,13 +44,19 @@ class YamlSerializable(object): if self.name is None: raise TypeError("Name cannot be None") ymlsls = self._get_all_from_file() - if self.name in [[*ymlsl][0] for ymlsl in ymlsls]: - if overwrite: - ymlsls[self.name] = self.config_dict - else: - raise ValueError(f"A {type(self).__name__} with name {self.name} already exists!") - else: - ymlsls.append(self.config_dict) + found_match = False + for ymlsl in ymlsls: + if self.name == [*ymlsl][0]: + found_match = True + if overwrite: + ymlsl[self.name] = self.config_dict(properties_only=True) + else: + raise ValueError( + f"A {type(self).__name__} with name {self.name} already exists!") + break + + if not found_match: + ymlsls.append(self.config_dict()) print(ymlsls) self._save_all_to_file(ymlsls) diff --git a/biscd/biscd/routes.py b/biscd/biscd/routes.py index f287563..893e7c6 100644 --- a/biscd/biscd/routes.py +++ b/biscd/biscd/routes.py @@ -2,8 +2,9 @@ from flask import render_template, flash, abort, redirect, request, url_for from flask_login import current_user, login_user, logout_user, login_required from werkzeug.urls import url_parse from biscd import app +from .utils import flash_result from .models import Project, User -from .froms import NewProjectForm, LoginForm, RegistrationForm +from .froms import NewProjectForm, LoginForm, RegistrationForm, EmptyForm @app.route('/', methods=['GET', 'POST']) @app.route('/index', methods=['GET', 'POST']) @@ -22,7 +23,7 @@ def login(): if form.validate_on_submit(): user = User.first(name=form.username.data) if user is None or not user.check_password(form.password.data): - flash('Invalid username or password') + flash('Invalid username or password', 'warning') return redirect(url_for('login')) login_user(user, remember=form.remember_me.data) next_page = request.args.get('next') @@ -31,8 +32,8 @@ def login(): return redirect(next_page) return render_template('login.html', title='Sign In', form=form) -@login_required @app.route('/logout') +@login_required def logout(): logout_user() return redirect(url_for('index')) @@ -46,13 +47,29 @@ def register(): user = User(name=form.username.data, email=form.email.data) user.set_password(form.password.data) user.save() - flash('Congratulations, you are now a registered user!') + flash('Congratulations, you are now a registered user!', 'info') return redirect(url_for('login')) return render_template('register.html', title='Register', form=form) -@app.route('/project/', methods=['GET', 'POST']) +@app.route('/project/', methods=['GET']) @login_required -def project_dashboard(project_url): - project = Project.first_or_404(url=project_url) +def project_dashboard(project_name): + project = Project.first_or_404(name=project_name) return render_template('project.html', project=project) + +@app.route('/project//update', methods=['GET']) +@login_required +def project_update(project_name): + project = Project.first_or_404(name=project_name) + result = project.update() + flash_result(result) + return redirect(url_for('project_dashboard', project_name=project.name)) + +@app.route('/project//delete_files', methods=['GET']) +@login_required +def project_delete_files(project_name): + project = Project.first_or_404(name=project_name) + result = project.delete_files() + flash_result(result) + return redirect(url_for('project_dashboard', project_name=project.name)) \ No newline at end of file diff --git a/biscd/biscd/templates/base.html b/biscd/biscd/templates/base.html index 0e12f0a..d46884f 100644 --- a/biscd/biscd/templates/base.html +++ b/biscd/biscd/templates/base.html @@ -64,10 +64,25 @@
- {% with messages = get_flashed_messages() %} + {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} - {% for message in messages %} - + {% for category, message in messages %} +
+
+ {% if category == 'message' or category == 'info' %} +
+ {% elif category == 'success' %} +
+ {% elif category == 'warning' %} +
+ {% elif category == 'error' %} +
+ {% endif %} + + {{ message }} +
+
+
{% endfor %} {% endif %} {% endwith %} diff --git a/biscd/biscd/templates/index.html b/biscd/biscd/templates/index.html index 16da500..7addd14 100644 --- a/biscd/biscd/templates/index.html +++ b/biscd/biscd/templates/index.html @@ -10,7 +10,6 @@
{% if project_names %} -

Your Projects

diff --git a/biscd/biscd/templates/project.html b/biscd/biscd/templates/project.html index 23159e5..432d7a8 100644 --- a/biscd/biscd/templates/project.html +++ b/biscd/biscd/templates/project.html @@ -3,24 +3,38 @@ {% block app_content %}

{{ project.name }}

-
- - - - - - - - - - + + + + + + + +

+
+ +
+
BranchGit RepoRequirements fileTestsUrl
+ + + + + + + + + + - + + + +
Branch {{ project.branch }}
Git Repo {{ project.git_repo }}
Requirements file {{ project.requirements_file }}
Tests {{ project.tests }}{{ project.url }}
Production Path{{ project.relative_production_path }}
diff --git a/biscd/biscd/utils.py b/biscd/biscd/utils.py index ea33ab5..35f1e63 100644 --- a/biscd/biscd/utils.py +++ b/biscd/biscd/utils.py @@ -1,6 +1,6 @@ from datetime import datetime -from flask import request +from flask import request, flash def get_timestamp(): return datetime.now().astimezone().isoformat(timespec='milliseconds') @@ -10,6 +10,13 @@ def get_remote_addr(): #print(f"HTTP_X_FORWARDED_FOR: {request.environ.get('HTTP_X_FORWARDED_FOR')}") return request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr) +def flash_result(*result): + if result[0] is not None: + if isinstance(result, list): + flash(*result) + else: + flash(result) + class ObjectView(object): def __init__(self, d): self.__dict__ = d diff --git a/installation-files/config_example.yaml b/installation-files/config_example.yaml index ee40efd..7885e1d 100644 --- a/installation-files/config_example.yaml +++ b/installation-files/config_example.yaml @@ -9,5 +9,7 @@ logging: log_to_stdout: true logfile: info.log +production_path: /opt/biscd + SECRET_KEY: this-should-be-very-secret BOOTSTRAP_SERVE_LOCAL: True \ No newline at end of file diff --git a/installation-files/projects_example.yaml b/installation-files/projects_example.yaml index c450106..03a88c4 100644 --- a/installation-files/projects_example.yaml +++ b/installation-files/projects_example.yaml @@ -5,11 +5,10 @@ projects: requirements_file: requirements.txt secret: thisissecret tests: tests.py - url: thehunt - Foo: branch: master git_repo: https://git.sciuro.org/Burathar/The-Hunt + relative_production_path: Foo045878 requirements_file: requirements.txt secret: thisissecret tests: tests.py - url: bar \ No newline at end of file