This repository has been archived on 2020-09-13. You can view files and clone it, but cannot push or open issues or pull requests.
magic-app/spikes/second.py

206 lines
6.6 KiB
Python
Raw Normal View History

2020-07-01 18:46:29 +00:00
"""Less flashy version. More hard-coding."""
2020-07-01 18:02:19 +00:00
2020-07-02 11:41:31 +00:00
from hashlib import md5
2020-07-01 19:19:09 +00:00
from json import dumps, loads
from os import environ, mkdir
2020-07-01 19:19:09 +00:00
from os.path import exists
2020-07-01 18:02:19 +00:00
from pathlib import Path
2020-07-01 18:42:47 +00:00
from shlex import split
from subprocess import run
from typing import Dict
2020-07-01 18:02:19 +00:00
2020-07-01 19:19:09 +00:00
from flask import Flask, render_template, request
2020-07-01 18:42:47 +00:00
from flask_wtf import FlaskForm
2020-07-01 18:02:19 +00:00
from ruamel.yaml import YAML
2020-07-01 18:42:47 +00:00
from wtforms import PasswordField, StringField
from wtforms.validators import URL, DataRequired, Length
2020-07-01 18:02:19 +00:00
app = Flask(__name__)
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
yaml = YAML()
2020-07-02 11:41:31 +00:00
APPS_SPEC = {
"gitea": {
"url": "https://git.autonomic.zone/compose-stacks/gitea",
"secrets": [
"db_passwd",
"db_root_passwd",
"internal_token",
"jwt_secret",
"secret_key",
],
"configs": ["app_ini"],
}
}
2020-07-01 18:02:19 +00:00
DATA_DIR = Path("./data")
2020-07-01 18:42:47 +00:00
def clone_app_template(app_name: str) -> None:
2020-07-01 18:42:47 +00:00
"""Git clone an app template repository."""
clone_path = DATA_DIR / app_name
2020-07-02 11:41:31 +00:00
clone_url = APPS_SPEC[app_name]["url"]
2020-07-01 18:42:47 +00:00
run(split(f"git clone {clone_url} {clone_path}"))
2020-07-01 18:02:19 +00:00
def create_docker_secret(key: str, value: str) -> None:
"""Load a docker secret into swarm."""
command = f"echo {value} | docker secret create {key} -"
run(command, shell=True)
def dump_db(db: Dict) -> None:
2020-07-01 19:19:09 +00:00
"""Dump the database."""
with open(DATA_DIR / "db.json", "w") as handle:
handle.write(dumps(db))
2020-07-01 19:19:09 +00:00
def load_db() -> Dict:
2020-07-01 19:19:09 +00:00
"""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 {}
2020-07-02 11:41:31 +00:00
def get_hash(value: str) -> str:
"""Hash a value for swarm versioning with good 'ol md5."""
hasher = md5()
hasher.update(value.encode())
return hasher.hexdigest()
def arrange_configs_and_secrets(app_name, form_data, env):
"""Version secrets and configs for swarm."""
def _create_versions(values, secrets=False):
"""Step through listing and create an env var key/val.
Takes "app_ini" and produces {"APP_INI_VERSION": "app_ini_laksjdklajsdkla"}.
Hash is based on the value of the input passed in from the form.
"""
for value in values:
if value in form_data:
hashed = get_hash(form_data[value])
env_key = f"{value.upper()}_VERSION"
env_val = f"{value}_{hashed}"
env[env_key] = env_val
if secrets:
create_docker_secret(env_val, form_data[value])
_create_versions(APPS_SPEC[app_name]["configs"])
_create_versions(APPS_SPEC[app_name]["secrets"], secrets=True)
2020-07-02 11:41:31 +00:00
return env
def stack_deploy(app_name, stack_name, env):
"""Depoy an application to the swarm."""
compose_yml = DATA_DIR / app_name / "compose.yml"
command = f"docker stack deploy -c {compose_yml} {stack_name}"
2020-07-02 13:00:27 +00:00
# Note(decentral1se): we're not sure if this ever succeeds actually stuff
# like https://github.com/issuu/sure-deploy could be used and once we
# graduate this to a dockerise app, we could install that all in the
# container and run it from here. Of course, if we're using JS or not might
# affect this, say, we have a page that is real-time reporting or we're
# using a static page? Unsure what the UX could be like, thinking of
# cloudron loading bar but never like those...
run(split(command), env=env)
def get_loaded_env(app_name, request_form):
2020-07-02 11:09:51 +00:00
"""Load environment from install form for compose.yml injection."""
2020-07-02 12:50:23 +00:00
env = environ.copy()
for key in request_form.keys():
2020-07-02 13:00:27 +00:00
# Note(decentral1se): {"app_ini": "foo"} -> {"APP_INI": "foo"}
2020-07-02 12:50:23 +00:00
env[key.upper()] = request.form[key]
2020-07-02 13:00:27 +00:00
# Note(decentral1se): maybe we don't need this in the end since we will be
# quite sure that our configs are working and we are not getting rate
# limited
2020-07-02 12:50:23 +00:00
env["LETS_ENCRYPT_ENV"] = "staging"
return env
class GiteaInstallForm(FlaskForm):
"""Gitea installation form."""
app_name = StringField("Application name", default="Git with a cup of tea")
2020-07-02 11:09:51 +00:00
domain = StringField("Domain name", validators=[DataRequired()],)
stack_name = StringField("Stack name", default="magic-app-gitea")
db_host = StringField("Database host", default="mariadb:3306")
db_name = StringField("Database name", default="gitea")
db_passwd = PasswordField(
2020-07-02 11:09:51 +00:00
"Database password", validators=[DataRequired(), Length(min=32)],
)
db_root_passwd = PasswordField(
2020-07-02 11:09:51 +00:00
"Database root password", validators=[DataRequired(), Length(min=32)],
)
db_type = StringField("Database type", default="mysql")
db_user = StringField("Database user", default="mysql")
internal_token = PasswordField(
2020-07-02 11:09:51 +00:00
"Internal secret token", validators=[DataRequired(), Length(min=105)],
)
jwt_secret = PasswordField(
2020-07-02 11:09:51 +00:00
"JWT secret", validators=[DataRequired(), Length(min=43)],
)
secret_key = PasswordField(
2020-07-02 11:41:31 +00:00
"Secret key", validators=[DataRequired(), Length(min=64)],
)
2020-07-02 12:50:23 +00:00
ssh_host_port = StringField("SSH host port", default="2225")
2020-07-01 18:02:19 +00:00
@app.route("/")
2020-07-01 18:42:47 +00:00
def index():
"""Home page for app installation possibilities."""
2020-07-02 11:41:31 +00:00
return render_template("second/index.html", apps=[app for app in APPS_SPEC])
2020-07-01 18:42:47 +00:00
@app.route("/install/<app_name>")
def install(app_name):
"""Installation page for an app."""
2020-07-01 18:42:47 +00:00
if app_name == "gitea":
form = GiteaInstallForm()
return render_template("second/install.html", app_name=app_name, form=form)
2020-07-01 18:02:19 +00:00
2020-07-01 19:19:09 +00:00
@app.route("/deploy/<app_name>", methods=["POST"])
def deploy(app_name):
"""Deployment end-point for an app."""
2020-07-02 10:26:02 +00:00
if app_name == "gitea":
form = GiteaInstallForm(request.form)
if not form.validate():
return render_template("second/install.html", app_name=app_name, form=form)
2020-07-02 10:26:02 +00:00
try:
mkdir(DATA_DIR)
except FileExistsError:
2020-07-02 13:00:27 +00:00
# Note(decentral1se): we already cloned it, so keep going
pass
clone_app_template(app_name)
2020-07-02 13:00:27 +00:00
# Note(decentral1se): there are badly named and the functions have too many
# concerns but they basically load every value required into the
# environment so that it can be injected when docker stack deploy is run,
# oh and secrets are also created as part of this step ;)
2020-07-02 11:41:31 +00:00
env = get_loaded_env(app_name, form.data)
env = arrange_configs_and_secrets(app_name, form.data, env)
2020-07-01 19:19:09 +00:00
2020-07-02 13:00:27 +00:00
stack_name = form.data["stack_name"]
dump_db({stack_name: form.data})
stack_deploy(app_name, stack_name, env)
2020-07-02 12:29:15 +00:00
return render_template(
2020-07-02 13:00:27 +00:00
"second/deploy.html", app_name=app_name, stack_name=stack_name
2020-07-02 12:29:15 +00:00
)
2020-07-01 19:19:09 +00:00
2020-07-01 18:02:19 +00:00
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0")