2020-07-01 17:56:35 +00:00
|
|
|
"""The all singing all dancing auto-generation from compose.yml attempt.
|
|
|
|
|
|
|
|
Largely unfinished, just a stab in the dark.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
2020-06-30 19:03:58 +00:00
|
|
|
from json import loads
|
2020-07-01 08:40:54 +00:00
|
|
|
from os.path import exists
|
2020-06-30 19:03:58 +00:00
|
|
|
from pathlib import Path
|
2020-07-01 08:40:54 +00:00
|
|
|
from re import findall
|
|
|
|
from shlex import split
|
|
|
|
from subprocess import run
|
2020-06-30 19:03:58 +00:00
|
|
|
|
2020-06-30 18:35:01 +00:00
|
|
|
from flask import Flask, render_template, send_from_directory
|
2020-07-01 08:40:54 +00:00
|
|
|
from flask_wtf import FlaskForm
|
|
|
|
from ruamel.yaml import YAML
|
|
|
|
from wtforms import StringField
|
|
|
|
from wtforms.validators import DataRequired
|
2020-06-30 18:28:47 +00:00
|
|
|
|
|
|
|
app = Flask(__name__)
|
|
|
|
|
2020-07-01 08:40:54 +00:00
|
|
|
# Note(decentral1se): load from env vars at some point
|
|
|
|
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
|
2020-06-30 18:28:47 +00:00
|
|
|
|
2020-07-01 08:40:54 +00:00
|
|
|
yaml = YAML()
|
|
|
|
|
|
|
|
# Note(decentral1se): paths needs to change dependening
|
|
|
|
# on deployment mode ./data in dev, /data in production
|
|
|
|
DATA_DIR = Path("./data")
|
|
|
|
|
2020-07-01 18:08:00 +00:00
|
|
|
STATIC_DIR = Path(".").absolute() / "static"
|
2020-07-01 08:40:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
class InstallAppInputForm(FlaskForm):
|
|
|
|
"""Dynamically generated input form for app deploy."""
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
def get_apps_json():
|
|
|
|
"""Retrieve the apps listing."""
|
|
|
|
global STATIC_DIR
|
2020-06-30 19:03:58 +00:00
|
|
|
|
|
|
|
try:
|
2020-07-01 08:40:54 +00:00
|
|
|
apps_path = STATIC_DIR / "apps.json"
|
2020-06-30 19:03:58 +00:00
|
|
|
with open(apps_path, "r") as handle:
|
2020-07-01 08:40:54 +00:00
|
|
|
return loads(handle.read())
|
|
|
|
except Exception as exception:
|
|
|
|
app.logger.error(f"Failed to load apps.json, saw {exception}")
|
|
|
|
|
|
|
|
|
|
|
|
def clone_app_template(app_name):
|
|
|
|
"""Git clone an app template repository."""
|
|
|
|
global DATA_DIR
|
|
|
|
|
|
|
|
try:
|
|
|
|
apps_json = get_apps_json()
|
|
|
|
app_repo_url = apps_json[app_name]
|
|
|
|
except KeyError:
|
|
|
|
app.logger.error(f"No repository for {app_name} found")
|
|
|
|
# Note(decentral1se): should catch this or make more specific error
|
|
|
|
raise RuntimeError()
|
|
|
|
|
|
|
|
try:
|
|
|
|
clone_path = DATA_DIR / app_name
|
|
|
|
command = f"git clone {app_repo_url} {clone_path}"
|
|
|
|
run(split(command))
|
2020-06-30 19:03:58 +00:00
|
|
|
except Exception as exception:
|
2020-07-01 08:40:54 +00:00
|
|
|
app.logger.error(f"Failed to run {command}, saw {exception}")
|
|
|
|
# Note(decentral1se): should catch this or make more specific error
|
|
|
|
raise RuntimeError()
|
|
|
|
|
|
|
|
|
|
|
|
def get_env_vars(path):
|
|
|
|
"""Get required env vars from a compose.yml."""
|
|
|
|
try:
|
|
|
|
compose_path = Path(path) / "compose.yml"
|
|
|
|
with open(compose_path, "r") as handle:
|
|
|
|
compose_yml = handle.read()
|
|
|
|
except Exception as exception:
|
|
|
|
app.logger.error(f"Failed to load {compose_path}, saw {exception}")
|
|
|
|
# Note(decentral1se): should catch this or make more specific error
|
|
|
|
raise RuntimeError()
|
|
|
|
|
|
|
|
# Note(decentral1se): matches all env vars (e.g ${FOO} and returns FOO)
|
|
|
|
return findall(r"\${(.*?)}", compose_yml)
|
|
|
|
|
|
|
|
|
|
|
|
def get_secrets(path):
|
|
|
|
"""Get required secrets from a compose.yml."""
|
|
|
|
try:
|
|
|
|
compose_path = Path(path) / "compose.yml"
|
|
|
|
with open(compose_path, "r") as handle:
|
|
|
|
compose_yml = yaml.load(handle.read())
|
|
|
|
except Exception as exception:
|
|
|
|
app.logger.error(f"Failed to load {compose_path}, saw {exception}")
|
|
|
|
# Note(decentral1se): should catch this or make more specific error
|
|
|
|
raise RuntimeError()
|
|
|
|
|
|
|
|
# Note(decentral1se): retrieves top level names like "db_passwd" not
|
|
|
|
# "gitea_db_passwd_v1" and I am unsure at this point which is the best to
|
|
|
|
# bring in but at least the plumbing is there
|
|
|
|
return [secret for secret in compose_yml["secrets"].keys()]
|
|
|
|
|
|
|
|
|
|
|
|
def get_compose_inputs(path):
|
|
|
|
"""Retrieve a list of all required inputs for stack deploy."""
|
|
|
|
return {
|
|
|
|
"env_vars": get_env_vars(path),
|
|
|
|
"secrets": get_secrets(path),
|
|
|
|
}
|
2020-06-30 19:03:58 +00:00
|
|
|
|
|
|
|
|
2020-06-30 18:28:47 +00:00
|
|
|
@app.route("/")
|
|
|
|
def home():
|
2020-07-01 08:40:54 +00:00
|
|
|
"""Home page."""
|
|
|
|
apps = [app_name for app_name in get_apps_json()]
|
2020-07-01 18:13:15 +00:00
|
|
|
return render_template("first/index.html", apps=apps)
|
2020-06-30 18:28:47 +00:00
|
|
|
|
|
|
|
|
2020-06-30 18:35:01 +00:00
|
|
|
@app.route("/apps")
|
2020-06-30 19:03:58 +00:00
|
|
|
def apps_list():
|
2020-07-01 08:40:54 +00:00
|
|
|
"""The JSON list of all apps."""
|
2020-06-30 18:35:01 +00:00
|
|
|
return send_from_directory("static", "apps.json")
|
|
|
|
|
|
|
|
|
2020-07-01 08:40:54 +00:00
|
|
|
@app.route("/<app_name>")
|
|
|
|
def install_app(app_name):
|
|
|
|
"""Install an app"""
|
|
|
|
clone_app_template(app_name)
|
|
|
|
|
|
|
|
inputs = get_compose_inputs(DATA_DIR / app_name)
|
|
|
|
|
|
|
|
for env_var in inputs["env_vars"]:
|
|
|
|
setattr(InstallAppInputForm, env_var, StringField())
|
|
|
|
|
|
|
|
for secret in inputs["secrets"]:
|
|
|
|
setattr(InstallAppInputForm, secret, StringField())
|
|
|
|
|
|
|
|
form = InstallAppInputForm()
|
|
|
|
|
2020-07-01 18:13:15 +00:00
|
|
|
return render_template("first/install.html", app_name=app_name, form=form)
|
2020-07-01 08:40:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
@app.route("/deploy/<app_name>")
|
|
|
|
def deploy_app(app_name):
|
|
|
|
return "TODO"
|
2020-06-30 19:03:58 +00:00
|
|
|
|
|
|
|
|
2020-06-30 18:28:47 +00:00
|
|
|
if __name__ == "__main__":
|
2020-07-01 08:40:54 +00:00
|
|
|
app.run(debug=True, host="0.0.0.0")
|