Hacking towards the db.json, trimming as much as possible
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Luke Murphy 2020-07-01 10:40:54 +02:00
parent 06b344b801
commit 8e2bce536f
No known key found for this signature in database
GPG Key ID: 5E2EF5A63E3718CC
10 changed files with 154 additions and 72 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
.venv
data/

2
.gitignore vendored
View File

@ -7,6 +7,6 @@
.venv/ .venv/
__pycache__ __pycache__
build/ build/
data/
dist/ dist/
pip-wheel-metadata/ pip-wheel-metadata/
documentation/build/

View File

@ -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"]

View File

@ -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

View File

@ -6,34 +6,19 @@ A swarm of dreams.
## Proof Of Concept ## Proof Of Concept
- [ ] List apps from an `app.json` (points to https://git.autonomic.zone/compose-stacks) - [x] 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) - [x] Clone selected app template and parse configuration for inputs (env vars and secrets)
- [ ] Generate a form so those values can be filled out - [ ] Generate a form so those values can be filled out and allow it to be saved
- [ ] Save the form inputs to a `db.json` - [ ] Save the form inputs to a `db.json`
- [ ] Deploy an applicaiton to a local swarm (assumes access to local docker socket) - [ ] 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 - [ ] 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) - [ ] Make sure re-deploy works (taking care of updating secret and app versions)
## Run It ### Development
### Go Go Go No Docker
```bash ```bash
$ python3 -m venv .venv && source .venv/bin/activate $ python3 -m venv .venv
$ source .venv/bin/activate
$ pip install -r requirements.txt $ pip install -r requirements.txt
$ make python-run $ python magic_app/app.py
```
### 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
``` ```

View File

@ -1,10 +0,0 @@
---
version: "3.8"
services:
magic-app:
build: ./
ports:
- "5000:5000"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"

View File

@ -1,40 +1,145 @@
from json import loads from json import loads
from os.path import exists
from pathlib import Path 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 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__) app = Flask(__name__)
# Note(decentral1se): load from env vars at some point
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
def get_apps(): yaml = YAML()
"""All apps that can be installed."""
apps_path = (Path(app.root_path) / "static" / "apps.json").absolute() # 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: try:
apps_path = STATIC_DIR / "apps.json"
with open(apps_path, "r") as handle: with open(apps_path, "r") as handle:
return [key for key in loads(handle.read())] return loads(handle.read())
except Exception as exception: 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("/") @app.route("/")
def home(): def home():
apps = get_apps() """Home page."""
apps = [app_name for app_name in get_apps_json()]
return render_template("index.html", apps=apps) return render_template("index.html", apps=apps)
@app.route("/apps") @app.route("/apps")
def apps_list(): def apps_list():
"""The JSON list of all apps."""
return send_from_directory("static", "apps.json") return send_from_directory("static", "apps.json")
@app.route("/<app>") @app.route("/<app_name>")
def app_config(app_name): def install_app(app_name):
# TODO(decentral1se): clone the app repository to somewhere under /tmp read """Install an app"""
# all the env vars, secrets, etc. and fire up a wtform and deploy button clone_app_template(app_name)
# using a new app.html template in the templates directory
pass 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/<app_name>")
def deploy_app(app_name):
return "TODO"
if __name__ == "__main__": if __name__ == "__main__":
app.run(host="0.0.0.0") app.run(debug=True, host="0.0.0.0")

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Install {{ app_name }}</title>
</head>
<body>
<p>Install {{ app_name }}</p>
<form method="POST" action="/deploy">
{% for field in form %}
<div>
{{ field.label() }}
{{ field() }}
</div>
{% endfor %}
<input type="submit" value="Deploy" />
</form>
</body>
</html>

View File

@ -1,2 +1,3 @@
flask-wtf>=0.14.3,<0.15 flask-wtf>=0.14.3,<0.15
flask>=1.1.2,<2 flask>=1.1.2,<2
ruamel.yaml>=0.16.10,<0.17

View File

@ -8,5 +8,9 @@ setup(
author_email="helo@autonomic.zone", author_email="helo@autonomic.zone",
description="Magic App", description="Magic App",
packages=find_packages(), 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",
],
) )