diff --git a/.envrc.sample b/.envrc.sample index 991560b..5f58940 100644 --- a/.envrc.sample +++ b/.envrc.sample @@ -1,8 +1,9 @@ export CELERY_BROKER_URL=redis://localhost:6379 export CELERY_RESULT_BACKEND=redis://localhost:6379 -export FLASK_ENV=development export FLASK_APP=wsgi:app +export FLASK_ENV=development export REDIS_HOST=localhost export REDIS_PORT=6379 export REDIS_SESSION_DB=0 +export SECRET_KEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" export SERVER_PORT=0.0.0.0:8000 diff --git a/magic_app/app.py b/magic_app/app.py index dda2f14..bd6672d 100644 --- a/magic_app/app.py +++ b/magic_app/app.py @@ -57,9 +57,9 @@ def configure_celery(app): def configure_views(app): """Configure API resource views.""" - from magic_app.views import home + from magic_app.views import apps - app.register_blueprint(home) + app.register_blueprint(apps) def configure_logging(app): diff --git a/magic_app/config.py b/magic_app/config.py index bf04d1f..cf980af 100644 --- a/magic_app/config.py +++ b/magic_app/config.py @@ -1,6 +1,7 @@ """The Application settings.""" from os import environ, pardir from os.path import abspath, dirname, join +from pathlib import Path class Base: @@ -8,10 +9,10 @@ class Base: DEBUG = False JSON_AS_ASCII = False + SECRET_KEY = environ["SECRET_KEY"] APP_DIR = abspath(dirname(__file__)) PROJECT_ROOT = abspath(join(APP_DIR, pardir)) - SWAGGER_DIR = abspath(join(PROJECT_ROOT, "swagger_docs")) REDIS_HOST = environ["REDIS_HOST"] REDIS_PORT = environ["REDIS_PORT"] @@ -25,9 +26,9 @@ class Development(Base): """The Development configuration.""" ENV = "development" - CELERY_ALWAYS_EAGER = True DEBUG = True + DATA_DIR = Path(Base.PROJECT_ROOT) / "data" class Testing(Base): @@ -41,6 +42,7 @@ class Production(Base): """The production configuration.""" ENV = "production" + DATA_DIR = "/data" CONFIG = { diff --git a/magic_app/docker.py b/magic_app/docker.py new file mode 100644 index 0000000..235eb61 --- /dev/null +++ b/magic_app/docker.py @@ -0,0 +1 @@ +"""Docker interaction module""" diff --git a/magic_app/forms.py b/magic_app/forms.py new file mode 100644 index 0000000..00c6efb --- /dev/null +++ b/magic_app/forms.py @@ -0,0 +1,50 @@ +"""Forms for app installation.""" +from os import environ + +from flask import request +from flask_wtf import FlaskForm +from wtforms import PasswordField, StringField +from wtforms.validators import DataRequired, Length + + +class GiteaInstallForm(FlaskForm): + """Gitea installation form.""" + + # "simple" + app_name = StringField("Application name", default="Git with a cup of tea") + domain = StringField("Domain name", validators=[DataRequired()],) + stack_name = StringField("Stack name", default="magic-app-gitea") + + # "advanced" + db_host = StringField("Database host", default="mariadb:3306") + db_name = StringField("Database name", default="gitea") + db_type = StringField("Database type", default="mysql") + db_user = StringField("Database user", default="mysql") + ssh_host_port = StringField("SSH host port", default="2225") + + # secrets + db_passwd = PasswordField( + "Database password", validators=[DataRequired(), Length(min=32)], + ) + db_root_passwd = PasswordField( + "Database root password", validators=[DataRequired(), Length(min=32)], + ) + internal_token = PasswordField( + "Internal secret token", validators=[DataRequired(), Length(min=105)], + ) + jwt_secret = PasswordField( + "JWT secret", validators=[DataRequired(), Length(min=43)], + ) + secret_key = PasswordField( + "Secret key", validators=[DataRequired(), Length(min=64)], + ) + + +def form_to_env(app_name, request_form): + """Load form data into a environment.""" + env = environ.copy() + + for key in request_form.keys(): + env[key.upper()] = request.form[key] + + return env diff --git a/magic_app/tasks.py b/magic_app/tasks.py index 297063c..edd5af9 100644 --- a/magic_app/tasks.py +++ b/magic_app/tasks.py @@ -1,7 +1,14 @@ """Celery tasks module.""" from magic_app.app import celery +from magic_app.forms import form_to_env +from magic_app.templates import clone_app_template, create_data_dir @celery.task -def hello_world(): - print("Hello, World") +def install_app(app_name: str, form_data) -> None: + """Install an application.""" + create_data_dir() + clone_app_template(app_name) + + # Note(decentral1se): this is where I left off... + env = form_to_env() # noqa diff --git a/magic_app/templates.py b/magic_app/templates.py new file mode 100644 index 0000000..b66027f --- /dev/null +++ b/magic_app/templates.py @@ -0,0 +1,29 @@ +"""Compose template handling.""" +from os import mkdir +from os.path import exists +from shlex import split +from shutil import rmtree +from subprocess import run + +from flask import current_app + +APP_TEMPLATES = { + "gitea": "https://git.autonomic.zone/compose-stacks/gitea", +} + + +def create_data_dir() -> None: + """Create data directory for compose templates.""" + try: + mkdir(current_app.config["DATA_DIR"]) + except FileExistsError: + pass + + +def clone_app_template(app_name: str) -> None: + """Clone an application template repository.""" + clone_path = current_app.config["DATA_DIR"] / app_name + clone_url = APP_TEMPLATES[app_name] + if exists(clone_path): + rmtree(clone_path) + run(split(f"git clone {clone_url} {clone_path}")) diff --git a/magic_app/templates/app_install.html b/magic_app/templates/app_install.html new file mode 100644 index 0000000..764d59a --- /dev/null +++ b/magic_app/templates/app_install.html @@ -0,0 +1,19 @@ +{% from "macros.html" import with_errors %} + + + + + + Install {{ app_name | capitalize }} + + +

Install {{ app_name | capitalize }}

+
+ {% for field in form %} + {{ field.label() }} {{ with_errors(field, style='font-weight: bold') }} + {% endfor %} + +
+ + + diff --git a/magic_app/templates/app_list.html b/magic_app/templates/app_list.html new file mode 100644 index 0000000..167c106 --- /dev/null +++ b/magic_app/templates/app_list.html @@ -0,0 +1,17 @@ + + + + + Application Listing + + + + + + diff --git a/magic_app/templates/app_status.html b/magic_app/templates/app_status.html new file mode 100644 index 0000000..61b6233 --- /dev/null +++ b/magic_app/templates/app_status.html @@ -0,0 +1,27 @@ + + + + + {{ app_name | capitalize }} Status + + + + + + + + + + + + +
AppStatus
{{ app_name }}{{ status }}
+ + + diff --git a/magic_app/templates/base.html b/magic_app/templates/base.html deleted file mode 100644 index 58a4b2d..0000000 --- a/magic_app/templates/base.html +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/magic_app/templates/macros.html b/magic_app/templates/macros.html new file mode 100644 index 0000000..518cb1e --- /dev/null +++ b/magic_app/templates/macros.html @@ -0,0 +1,16 @@ +{# + From: https://wtforms.readthedocs.io/en/2.3.x/specific_problems/#rendering-errors + Usage: with_errors(form.field, style='font-weight: bold') +#} + +{% macro with_errors(field) %} +
+ {% if field.errors %} + {% set css_class = 'has_error ' + kwargs.pop('class', '') %} + {{ field(class=css_class, **kwargs) }} + + {% else %} + {{ field(**kwargs) }} + {% endif %} +
+{% endmacro %} diff --git a/magic_app/views.py b/magic_app/views.py index 355faca..4a1eae2 100644 --- a/magic_app/views.py +++ b/magic_app/views.py @@ -1,10 +1,44 @@ """View routing.""" -from flask import Blueprint +from flask import Blueprint, redirect, render_template, url_for -home = Blueprint("home", __name__) +from magic_app.forms import GiteaInstallForm + +apps = Blueprint("apps", __name__) -@home.route("/") -def hello_world(): +@apps.route("/") +def listing(): + return render_template("app_list.html", apps=["gitea"]) - return "Hello, World" + +@apps.route("/install/", methods=("GET", "POST")) +def install(app_name): + """Install an application.""" + from magic_app.tasks import install_app + + if app_name == "gitea": + form = GiteaInstallForm() + + if form.validate_on_submit(): + install_app.apply_async(args=[app_name]) + return redirect(url_for("apps.status", app_name=app_name)) + + return render_template("app_install.html", app_name=app_name, form=form) + + +@apps.route("/install-test/") +def install_test(app_name): + """Development aid to quickly test installation logic.""" + from magic_app.tasks import install_app + + install_app.apply_async(args=[app_name]) + + return f"Try again?" + + +@apps.route("/status/") +def status(app_name): + """Show status of applications.""" + return render_template( + "app_status.html", status="UNKNOWN", app_name=app_name + ) diff --git a/spikes/templates/second/install.html b/spikes/templates/second/install.html index 052e65f..e77b19b 100644 --- a/spikes/templates/second/install.html +++ b/spikes/templates/second/install.html @@ -10,10 +10,8 @@

Install {{ app_name }}

- {% for field in form %} - {{ field.label() }} - {{ with_errors(field, style='font-weight: bold') }} - {% endfor %} + {% for field in form %} {{ field.label() }} {{ with_errors(field, + style='font-weight: bold') }} {% endfor %}