"""Less flashy version. More hard-coding.""" from json import dumps, loads from os import environ from os.path import exists from pathlib import Path from random import choice from shlex import split from string import ascii_lowercase from subprocess import run from typing import Dict from flask import Flask, render_template, request from flask_wtf import FlaskForm from ruamel.yaml import YAML from wtforms import PasswordField, StringField from wtforms.validators import URL, DataRequired, Length app = Flask(__name__) app.secret_key = b'_5#y2L"F4Q8z\n\xec]/' yaml = YAML() APPS = {"gitea": "https://git.autonomic.zone/compose-stacks/gitea"} DATA_DIR = Path("./data") def get_secret(n: int) -> str: """Generate a ASCII secret that is n chars long.""" return "".join(choice(ascii_lowercase) for _ in range(n)) def clone_app_template(app_name: str) -> None: """Git clone an app template repository.""" clone_path = DATA_DIR / app_name clone_url = APPS[app_name] run(split(f"git clone {clone_url} {clone_path}")) def dump_db(db: Dict) -> None: """Dump the database.""" with open(DATA_DIR / "db.json", "w") as handle: handle.write(dumps(db)) def load_db() -> Dict: """Load the database.""" db_path = DATA_DIR / "db.json" if exists(db_path): with open(db_path, "r") as handle: return loads(handle.read()) return {} def stack_deploy(app_name, env): """Depoy an application to the swarm.""" compose_yml = DATA_DIR / app_name / "compose.yml" command = f"docker stack deploy -c {compose_yml} {app_name}" run(split(command), env=env) def get_loaded_env(app_name, request_form): """Load an environment with install form values for compose.yml injection.""" environment = environ.copy() for key in request_form.keys(): environment[key.upper()] = request.form[key] return environment class GiteaInstallForm(FlaskForm): """Gitea installation form.""" app_name = StringField("Application name", default="Git with a cup of tea") domain = StringField( # TODO(decentral1se): missing domain name validator "Domain name", validators=[DataRequired("Please enter a domain name")], ) db_host = StringField("Database host", default="mariadb:3306") db_name = StringField("Database name", default="gitea") db_passwd = PasswordField( "Database password", validators=[ DataRequired(), Length( min=32, message=( "Your chosen password length is too short, " "must be at least 32 characters long" ), ), ], ) db_root_passwd = PasswordField( "Database root password", validators=[ DataRequired(), Length( min=32, message=( "Your chosen password length is too short, " "it must be at least 32 characters long" ), ), ], ) db_type = StringField("Database type", default="mysql") db_user = StringField("Database user", default="mysql") internal_token = PasswordField( "Internal secret token", validators=[ DataRequired(), Length( min=105, message=( "Your chosen token length is too short, " "it must be at least 105 characters long" ), ), ], ) jwt_secret = PasswordField( "JWT secret", validators=[ DataRequired(), Length( min=43, message=( "Your chosen password length is too short, " "it must be at least 32 characters long" ), ), ], ) secret_key = PasswordField( "Secret key", validators=[ DataRequired(), Length( min=64, message=( "Your chosen password length is too short, " "it must be at least 64 characters long" ), ), ], ) ssh_port = StringField("SSH port", default="2222") @app.route("/") def index(): """Home page for app installation possibilities.""" return render_template("second/index.html", apps=[app for app in APPS]) @app.route("/install/") def install(app_name): """Installation page for an app.""" if app_name == "gitea": form = GiteaInstallForm() return render_template("second/install.html", app_name=app_name, form=form) @app.route("/deploy/", methods=["POST"]) def deploy(app_name): """Deployment end-point for an app.""" if app_name == "gitea": form = GiteaInstallForm(request.form) if not form.validate(): return render_template("second/install.html", app_name=app_name, form=form) environment = get_loaded_env(app_name, request.form) # Note(decentral1se): how to handle the following? # configs -> ${STACK_NAME}_app_ini_${APP_INI_VERSION} # secrets -> ${STACK_NAME}_db_passwd_${DB_PASSWD_VERSION} # and dump it all to the db.json when we're done here too stack_deploy(app_name, environment) if __name__ == "__main__": app.run(debug=True, host="0.0.0.0")