Hacking towards the db.json, trimming as much as possible
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
06b344b801
commit
8e2bce536f
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.venv
|
||||||
|
data/
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -7,6 +7,6 @@
|
|||||||
.venv/
|
.venv/
|
||||||
__pycache__
|
__pycache__
|
||||||
build/
|
build/
|
||||||
|
data/
|
||||||
dist/
|
dist/
|
||||||
pip-wheel-metadata/
|
pip-wheel-metadata/
|
||||||
documentation/build/
|
|
||||||
|
@ -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"]
|
|
16
Makefile
16
Makefile
@ -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
|
|
29
README.md
29
README.md
@ -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
|
|
||||||
```
|
```
|
||||||
|
10
compose.yml
10
compose.yml
@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
|
||||||
magic-app:
|
|
||||||
build: ./
|
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
volumes:
|
|
||||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
|
131
magic_app/app.py
131
magic_app/app.py
@ -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")
|
||||||
|
20
magic_app/templates/install.html
Normal file
20
magic_app/templates/install.html
Normal 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>
|
@ -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
|
||||||
|
6
setup.py
6
setup.py
@ -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",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user