Files
Enclume/enclume/webapp.py
T
hcornet de7a691861
Construction et publication / verifier (push) Successful in 41s
Construction et publication / verifier (pull_request) Successful in 11s
Construction et publication / publier (pull_request) Skipped
Construction et publication / publier (push) Failing after 6m3s
Enclume : editeur CLAPI, catalogue de 26 packs, empaquetage Docker
2026-09-10 11:32:03 +02:00

164 lines
5.9 KiB
Python

"""Routes de l'application.
Le serveur est volontairement sans etat a ce stade : il valide, il genere, il
ne conserve rien. Le projet vit dans le navigateur. Le partage, les projets
enregistres et la connexion viendront s'ajouter par-dessus sans changer ce
noyau.
"""
from __future__ import annotations
import io
import json
import os
import zipfile
from flask import Flask, Response, jsonify, render_template, request
from .catalog import CATEGORIES, Catalog
from .generator import build_plan, generate, render_bundle
from .model import CENTREON_VERSIONS, COMMAND_TYPES, Project, detect_macros, strip_secrets
from .packs import pack_to_project
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CATALOG_DIR = os.environ.get("ENCLUME_CATALOG", os.path.join(BASE_DIR, "catalog"))
MAX_BODY = 2 * 1024 * 1024 # un projet honnete pese quelques dizaines de ko
def create_app() -> Flask:
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = MAX_BODY
app.config["CATALOG"] = Catalog(CATALOG_DIR)
def catalog() -> Catalog:
return app.config["CATALOG"]
def project_from_request() -> Project:
payload = request.get_json(force=True, silent=True) or {}
return Project.from_dict(payload.get("project", payload))
@app.after_request
def security_headers(response: Response) -> Response:
response.headers.setdefault(
"Content-Security-Policy",
"default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'; "
"frame-ancestors 'none'; base-uri 'none'; form-action 'self'",
)
response.headers.setdefault("X-Content-Type-Options", "nosniff")
response.headers.setdefault("Referrer-Policy", "no-referrer")
return response
# ----------------------------------------------------------------- pages
@app.context_processor
def contexte_commun() -> dict:
return {"version": os.environ.get("ENCLUME_VERSION", "dev")}
@app.get("/")
def editor() -> str:
cat = catalog()
return render_template(
"editeur.html",
versions=CENTREON_VERSIONS,
command_types=COMMAND_TYPES,
categories=CATEGORIES,
grouped=cat.by_category(),
pack_count=len(cat.packs),
service_count=cat.service_count,
)
@app.get("/sante")
def health() -> Response:
cat = catalog()
return jsonify(
{
"statut": "ok",
"version": os.environ.get("ENCLUME_VERSION", "dev"),
"revision": os.environ.get("ENCLUME_COMMIT", "inconnu"),
"packs": len(cat.packs),
"erreurs_catalogue": cat.errors,
}
)
# ------------------------------------------------------------------- api
@app.get("/api/packs")
def api_packs() -> Response:
return jsonify(
[
{
"id": p.id,
"name": p.name,
"category": p.category,
"category_label": p.category_label,
"status": p.status,
"plugin": p.plugin,
"description": p.description,
"host_template": p.host_template.name,
"services": [
{"name": s.name, "description": s.description, "default": s.enabled_by_default}
for s in p.services
],
}
for p in sorted(catalog().packs.values(), key=lambda p: (p.category, p.name))
]
)
@app.get("/api/packs/<pack_id>/projet")
def api_pack_project(pack_id: str) -> Response:
pack = catalog().get(pack_id)
if pack is None:
return jsonify({"erreur": "pack inconnu"}), 404
only_defaults = request.args.get("tout") != "1"
return jsonify(pack_to_project(pack, only_defaults=only_defaults).to_dict())
@app.post("/api/valider")
def api_validate() -> Response:
plan = build_plan(project_from_request())
return jsonify({"summary": plan.summary, "issues": plan.issues, "has_errors": plan.has_errors})
@app.post("/api/generer")
def api_generate() -> Response:
payload = request.get_json(force=True, silent=True) or {}
return jsonify(generate(payload.get("project", payload)))
@app.post("/api/macros")
def api_macros() -> Response:
payload = request.get_json(force=True, silent=True) or {}
return jsonify(detect_macros(str(payload.get("line", ""))))
@app.post("/telecharger")
def download_zip() -> Response:
raw = request.form.get("project") or "{}"
project = Project.from_dict(json.loads(raw))
plan = build_plan(project)
files = render_bundle(plan)
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive:
for name, content in files.items():
archive.writestr(name, content)
# Le projet joint a l'archive ne contient aucun mot de passe.
archive.writestr(
"projet.json",
json.dumps(
strip_secrets(Project.from_dict(json.loads(raw))).to_dict(),
indent=2,
ensure_ascii=False,
),
)
buffer.seek(0)
slug = "".join(c if c.isalnum() or c in "-_" else "-" for c in project.name).strip("-") or "projet"
return Response(
buffer.read(),
mimetype="application/zip",
headers={"Content-Disposition": f'attachment; filename="{slug}-clapi.zip"'},
)
@app.post("/recharger")
def reload_catalog() -> Response:
app.config["CATALOG"] = Catalog(CATALOG_DIR)
cat = catalog()
return jsonify({"packs": len(cat.packs), "erreurs": cat.errors})
return app