From 8e2bce536f9271e9cfc8b4b9a185ec47b84ed008 Mon Sep 17 00:00:00 2001 From: Luke Murphy Date: Wed, 1 Jul 2020 10:40:54 +0200 Subject: [PATCH] Hacking towards the db.json, trimming as much as possible --- .dockerignore | 2 + .gitignore | 2 +- Dockerfile | 9 --- Makefile | 16 ---- README.md | 29 ++----- compose.yml | 10 --- magic_app/app.py | 131 ++++++++++++++++++++++++++++--- magic_app/templates/install.html | 20 +++++ requirements.txt | 1 + setup.py | 6 +- 10 files changed, 154 insertions(+), 72 deletions(-) create mode 100644 .dockerignore delete mode 100644 Dockerfile delete mode 100644 Makefile delete mode 100644 compose.yml create mode 100644 magic_app/templates/install.html diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2984bb7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.venv +data/ diff --git a/.gitignore b/.gitignore index ca15349..5005c85 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,6 @@ .venv/ __pycache__ build/ +data/ dist/ pip-wheel-metadata/ -documentation/build/ diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 571de16..0000000 --- a/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.8-alpine - -WORKDIR /app - -ADD . /app - -RUN pip install -r requirements.txt - -CMD ["python", "magic_app/app.py"] diff --git a/Makefile b/Makefile deleted file mode 100644 index e489b5a..0000000 --- a/Makefile +++ /dev/null @@ -1,16 +0,0 @@ -python-run: - @python magic_app/app.py - -docker-build: - @docker build -t autonomic/magicapp . - -docker-run: - @docker run --rm -p 5000:5000 autonomic/magicapp - -compose-run: - @docker-compose -f compose.yml up - -docker-publish: - @docker push autonomic/magicapp:v0.1.0 - -.PHONY: python-run docker-build docker-run docker-publish compose-run diff --git a/README.md b/README.md index de28e82..eb58fd9 100644 --- a/README.md +++ b/README.md @@ -6,34 +6,19 @@ A swarm of dreams. ## Proof Of Concept -- [ ] List apps from an `app.json` (points to https://git.autonomic.zone/compose-stacks) -- [ ] Clone selected app template and parse configuration for inputs (env vars and secrets) -- [ ] Generate a form so those values can be filled out +- [x] List apps from an `app.json` (points to https://git.autonomic.zone/compose-stacks) +- [x] Clone selected app template and parse configuration for inputs (env vars and secrets) +- [ ] Generate a form so those values can be filled out and allow it to be saved - [ ] Save the form inputs to a `db.json` - [ ] Deploy an applicaiton to a local swarm (assumes access to local docker socket) - [ ] Create an "edit app" page where the `db.json` is re-called and can be updated - [ ] Make sure re-deploy works (taking care of updating secret and app versions) -## Run It - -### Go Go Go No Docker +### Development ```bash -$ python3 -m venv .venv && source .venv/bin/activate +$ python3 -m venv .venv +$ source .venv/bin/activate $ pip install -r requirements.txt -$ make python-run -``` - -### Go Go Go Yes Docker - -```bash -$ make docker-build -$ make docker-run -``` - -### Go Go Go Yes Docker Compose - -```bash -$ pip install "docker-compose>=1.26.0,<2" -$ make compose-run +$ python magic_app/app.py ``` diff --git a/compose.yml b/compose.yml deleted file mode 100644 index 8fd2cd4..0000000 --- a/compose.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -version: "3.8" - -services: - magic-app: - build: ./ - ports: - - "5000:5000" - volumes: - - "/var/run/docker.sock:/var/run/docker.sock" diff --git a/magic_app/app.py b/magic_app/app.py index cf5bf96..888e788 100644 --- a/magic_app/app.py +++ b/magic_app/app.py @@ -1,40 +1,145 @@ from json import loads +from os.path import exists from pathlib import Path +from re import findall +from shlex import split +from subprocess import run from flask import Flask, render_template, send_from_directory +from flask_wtf import FlaskForm +from ruamel.yaml import YAML +from wtforms import StringField +from wtforms.validators import DataRequired app = Flask(__name__) +# Note(decentral1se): load from env vars at some point +app.secret_key = b'_5#y2L"F4Q8z\n\xec]/' -def get_apps(): - """All apps that can be installed.""" - apps_path = (Path(app.root_path) / "static" / "apps.json").absolute() +yaml = YAML() + +# Note(decentral1se): paths needs to change dependening +# on deployment mode ./data in dev, /data in production +DATA_DIR = Path("./data") + +STATIC_DIR = Path(app.root_path).absolute() / "static" + + +class InstallAppInputForm(FlaskForm): + """Dynamically generated input form for app deploy.""" + + pass + + +def get_apps_json(): + """Retrieve the apps listing.""" + global STATIC_DIR try: + apps_path = STATIC_DIR / "apps.json" with open(apps_path, "r") as handle: - return [key for key in loads(handle.read())] + return loads(handle.read()) except Exception as exception: - print("Failed to load apps.json, saw {}".format(str(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)) + except Exception as exception: + 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), + } @app.route("/") def home(): - apps = get_apps() + """Home page.""" + apps = [app_name for app_name in get_apps_json()] return render_template("index.html", apps=apps) @app.route("/apps") def apps_list(): + """The JSON list of all apps.""" return send_from_directory("static", "apps.json") -@app.route("/") -def app_config(app_name): - # TODO(decentral1se): clone the app repository to somewhere under /tmp read - # all the env vars, secrets, etc. and fire up a wtform and deploy button - # using a new app.html template in the templates directory - pass +@app.route("/") +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() + + return render_template("install.html", app_name=app_name, form=form) + + +@app.route("/deploy/") +def deploy_app(app_name): + return "TODO" if __name__ == "__main__": - app.run(host="0.0.0.0") + app.run(debug=True, host="0.0.0.0") diff --git a/magic_app/templates/install.html b/magic_app/templates/install.html new file mode 100644 index 0000000..b09156f --- /dev/null +++ b/magic_app/templates/install.html @@ -0,0 +1,20 @@ + + + + + + Install {{ app_name }} + + +

Install {{ app_name }}

+
+ {% for field in form %} +
+ {{ field.label() }} + {{ field() }} +
+ {% endfor %} + +
+ + diff --git a/requirements.txt b/requirements.txt index c5294e4..624ea6d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ flask-wtf>=0.14.3,<0.15 flask>=1.1.2,<2 +ruamel.yaml>=0.16.10,<0.17 diff --git a/setup.py b/setup.py index 038cca2..119b15c 100644 --- a/setup.py +++ b/setup.py @@ -8,5 +8,9 @@ setup( author_email="helo@autonomic.zone", description="Magic App", packages=find_packages(), - install_requires=["flask>=1.1.2,<2"], + install_requires=[ + "flask>=1.1.2,<2", + "flask-wtf>=0.14.3,<0.15", + "ruamel.yaml>=0.16.10,<0.17", + ], )