env secrets and variables
This commit is contained in:
@@ -0,0 +1,196 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from app.db.apps import get_app_variables, get_apps
|
||||||
|
|
||||||
|
APP_DATA_ROOT = Path("/opt/appfactory/data/apps")
|
||||||
|
ENV_HEADER = "# GENERATED BY APPFACTORY - DO NOT EDIT"
|
||||||
|
ENV_KEY_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
||||||
|
|
||||||
|
|
||||||
|
class AppEnvironmentError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def apply_app_environment(app_id: str) -> dict:
|
||||||
|
app_dir = _app_runtime_dir(app_id)
|
||||||
|
env_path = app_dir / ".env"
|
||||||
|
variables = get_app_variables(app_id)
|
||||||
|
new_content = _render_env_content(variables)
|
||||||
|
old_content = _read_text(env_path)
|
||||||
|
|
||||||
|
if old_content == new_content:
|
||||||
|
return {"changed": False, "restarted": False, "container": None}
|
||||||
|
|
||||||
|
_atomic_write(env_path, new_content)
|
||||||
|
container = _find_running_container(app_id)
|
||||||
|
if not container:
|
||||||
|
return {"changed": True, "restarted": False, "container": None}
|
||||||
|
|
||||||
|
_restart_container(container)
|
||||||
|
return {"changed": True, "restarted": True, "container": container}
|
||||||
|
|
||||||
|
|
||||||
|
def apply_all_app_environments() -> dict:
|
||||||
|
results = {}
|
||||||
|
errors = {}
|
||||||
|
for app in get_apps():
|
||||||
|
app_id = app.get("id") or ""
|
||||||
|
try:
|
||||||
|
results[app_id] = apply_app_environment(app_id)
|
||||||
|
except AppEnvironmentError as exc:
|
||||||
|
errors[app_id] = str(exc)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
failed = ", ".join(sorted(errors))
|
||||||
|
raise AppEnvironmentError(f"Environment apply failed for apps: {failed}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def _app_runtime_dir(app_id: str) -> Path:
|
||||||
|
if not app_id or "/" in app_id or "\\" in app_id or app_id in {".", ".."}:
|
||||||
|
raise AppEnvironmentError("Invalid app id")
|
||||||
|
|
||||||
|
root = APP_DATA_ROOT.resolve()
|
||||||
|
app_dir = (root / app_id).resolve()
|
||||||
|
if root not in app_dir.parents and app_dir != root:
|
||||||
|
raise AppEnvironmentError("Invalid app runtime path")
|
||||||
|
|
||||||
|
return app_dir
|
||||||
|
|
||||||
|
|
||||||
|
def _render_env_content(variables: list[dict]) -> str:
|
||||||
|
lines = [ENV_HEADER]
|
||||||
|
seen_keys: set[str] = set()
|
||||||
|
|
||||||
|
for variable in variables:
|
||||||
|
key = (variable.get("key") or "").strip()
|
||||||
|
validate_environment_key(key)
|
||||||
|
if key in seen_keys:
|
||||||
|
raise AppEnvironmentError(f"Duplicate environment variable key: {key}")
|
||||||
|
|
||||||
|
value = variable.get("value")
|
||||||
|
if value is None:
|
||||||
|
raise AppEnvironmentError(f"Missing value for environment variable key: {key}")
|
||||||
|
|
||||||
|
seen_keys.add(key)
|
||||||
|
lines.append(f"{key}={_format_env_value(str(value))}")
|
||||||
|
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def validate_environment_key(key: str) -> None:
|
||||||
|
if not ENV_KEY_RE.match(key):
|
||||||
|
raise AppEnvironmentError(f"Invalid environment variable key: {key or '<empty>'}")
|
||||||
|
|
||||||
|
|
||||||
|
def _format_env_value(value: str) -> str:
|
||||||
|
if value == "":
|
||||||
|
return ""
|
||||||
|
|
||||||
|
needs_quotes = (
|
||||||
|
value != value.strip()
|
||||||
|
or any(char in value for char in (' ', '\t', '\r', '\n', '"', "'", "\\", "#", "$"))
|
||||||
|
)
|
||||||
|
if not needs_quotes:
|
||||||
|
return value
|
||||||
|
|
||||||
|
escaped = (
|
||||||
|
value
|
||||||
|
.replace("\\", "\\\\")
|
||||||
|
.replace("\r", "\\r")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\t", "\\t")
|
||||||
|
.replace('"', '\\"')
|
||||||
|
.replace("$", "\\$")
|
||||||
|
)
|
||||||
|
return f'"{escaped}"'
|
||||||
|
|
||||||
|
|
||||||
|
def _read_text(path: Path) -> str | None:
|
||||||
|
try:
|
||||||
|
return path.read_text(encoding="utf-8")
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None
|
||||||
|
except OSError as exc:
|
||||||
|
raise AppEnvironmentError(f"Could not read runtime environment file: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _atomic_write(path: Path, content: str) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
tmp_path = path.with_name(f".{path.name}.tmp.{os.getpid()}")
|
||||||
|
fd = None
|
||||||
|
try:
|
||||||
|
fd = os.open(tmp_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||||
|
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as tmp_file:
|
||||||
|
fd = None
|
||||||
|
tmp_file.write(content)
|
||||||
|
tmp_file.flush()
|
||||||
|
os.fsync(tmp_file.fileno())
|
||||||
|
os.chmod(tmp_path, 0o600)
|
||||||
|
os.replace(tmp_path, path)
|
||||||
|
_fsync_directory(path.parent)
|
||||||
|
except OSError as exc:
|
||||||
|
raise AppEnvironmentError(f"Could not write runtime environment file: {exc}") from exc
|
||||||
|
finally:
|
||||||
|
if fd is not None:
|
||||||
|
os.close(fd)
|
||||||
|
try:
|
||||||
|
tmp_path.unlink()
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _fsync_directory(path: Path) -> None:
|
||||||
|
if os.name == "nt":
|
||||||
|
return
|
||||||
|
|
||||||
|
fd = os.open(path, os.O_RDONLY)
|
||||||
|
try:
|
||||||
|
os.fsync(fd)
|
||||||
|
finally:
|
||||||
|
os.close(fd)
|
||||||
|
|
||||||
|
|
||||||
|
def _find_running_container(app_id: str) -> str | None:
|
||||||
|
for container in _container_candidates(app_id):
|
||||||
|
result = _run_docker(["inspect", "-f", "{{.State.Running}}", container])
|
||||||
|
if result.returncode == 0 and result.stdout.strip().lower() == "true":
|
||||||
|
return container
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _container_candidates(app_id: str) -> list[str]:
|
||||||
|
normalized = app_id.replace("_", "-")
|
||||||
|
candidates = [app_id, normalized, f"appfactory-{app_id}", f"appfactory-{normalized}"]
|
||||||
|
unique = []
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate not in unique:
|
||||||
|
unique.append(candidate)
|
||||||
|
return unique
|
||||||
|
|
||||||
|
|
||||||
|
def _restart_container(container: str) -> None:
|
||||||
|
result = _run_docker(["restart", container])
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise AppEnvironmentError(f"Could not restart container {container}: {_safe_command_error(result.stderr)}")
|
||||||
|
|
||||||
|
|
||||||
|
def _run_docker(args: list[str]) -> subprocess.CompletedProcess:
|
||||||
|
try:
|
||||||
|
return subprocess.run(["docker", *args], capture_output=True, text=True, timeout=30)
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise AppEnvironmentError("Docker command is not available") from exc
|
||||||
|
except subprocess.TimeoutExpired as exc:
|
||||||
|
raise AppEnvironmentError("Docker command timed out") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_command_error(value: str) -> str:
|
||||||
|
value = (value or "").strip()
|
||||||
|
if not value:
|
||||||
|
return "no error output"
|
||||||
|
return value[:500]
|
||||||
+200
-7
@@ -33,6 +33,7 @@ from ..db.audit import log_audit_event
|
|||||||
from ..db.health import get_latest_service_health, get_service_health, get_service_health_history
|
from ..db.health import get_latest_service_health, get_service_health, get_service_health_history
|
||||||
from ..db.incidents import get_service_incidents
|
from ..db.incidents import get_service_incidents
|
||||||
from ..db.jobs import create_job, get_jobs, has_active_deploy_job
|
from ..db.jobs import create_job, get_jobs, has_active_deploy_job
|
||||||
|
from ..environment import AppEnvironmentError, apply_all_app_environments, apply_app_environment, validate_environment_key
|
||||||
from ..routes.deployments import render_status_pill
|
from ..routes.deployments import render_status_pill
|
||||||
from ..routes.incidents import render_incident_history_rows
|
from ..routes.incidents import render_incident_history_rows
|
||||||
from ..shell import run_command
|
from ..shell import run_command
|
||||||
@@ -75,6 +76,54 @@ def bool_checked(value) -> str:
|
|||||||
return " checked" if value else ""
|
return " checked" if value else ""
|
||||||
|
|
||||||
|
|
||||||
|
def app_detail_url(app_id: str, anchor: str = "", message: str = "", error: str = "") -> str:
|
||||||
|
params = {}
|
||||||
|
if message:
|
||||||
|
params["message"] = message
|
||||||
|
if error:
|
||||||
|
params["error"] = error
|
||||||
|
|
||||||
|
url = f"/portal/apps/{quote(app_id, safe='')}"
|
||||||
|
if params:
|
||||||
|
url += f"?{urlencode(params)}"
|
||||||
|
if anchor:
|
||||||
|
url += f"#{anchor}"
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def redirect_app_detail(app_id: str, anchor: str = "", message: str = "", error: str = "") -> RedirectResponse:
|
||||||
|
return RedirectResponse(url=app_detail_url(app_id, anchor=anchor, message=message, error=error), status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_environment_message(app_id: str) -> str:
|
||||||
|
result = apply_app_environment(app_id)
|
||||||
|
if not result.get("changed"):
|
||||||
|
return "Environment unchanged."
|
||||||
|
if result.get("restarted"):
|
||||||
|
return "Environment applied and container restarted."
|
||||||
|
return "Environment file regenerated. Container was not running."
|
||||||
|
|
||||||
|
|
||||||
|
def apply_all_environments_message() -> str:
|
||||||
|
results = apply_all_app_environments()
|
||||||
|
changed = sum(1 for result in results.values() if result.get("changed"))
|
||||||
|
restarted = sum(1 for result in results.values() if result.get("restarted"))
|
||||||
|
return f"Regenerated environments for {len(results)} apps. Changed: {changed}. Restarted: {restarted}."
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_variable_key_allowed(app_id: str, key: str, variable_id: int | None = None) -> None:
|
||||||
|
try:
|
||||||
|
validate_environment_key(key)
|
||||||
|
except AppEnvironmentError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
for variable in get_app_variables(app_id):
|
||||||
|
if variable_id is not None and int(variable.get("id")) == variable_id:
|
||||||
|
continue
|
||||||
|
if (variable.get("key") or "").strip() == key:
|
||||||
|
raise HTTPException(status_code=400, detail="Variable key already exists")
|
||||||
|
|
||||||
|
|
||||||
def render_template_options(templates: list[dict], selected_template: str, include_blank: bool = True) -> str:
|
def render_template_options(templates: list[dict], selected_template: str, include_blank: bool = True) -> str:
|
||||||
options = ['<option value="">Bez šablony</option>'] if include_blank else []
|
options = ['<option value="">Bez šablony</option>'] if include_blank else []
|
||||||
selected_exists = not selected_template
|
selected_exists = not selected_template
|
||||||
@@ -110,6 +159,44 @@ def render_template_label(templates: list[dict], template_id: str) -> str:
|
|||||||
return "Neznámá šablona"
|
return "Neznámá šablona"
|
||||||
|
|
||||||
|
|
||||||
|
def render_environment_usage_example(language: str, runtime: str, template: str) -> str:
|
||||||
|
normalized = " ".join((language or "", runtime or "", template or "")).lower()
|
||||||
|
title = "Použití v kódu"
|
||||||
|
if "fastapi" in normalized or "python" in normalized:
|
||||||
|
code = """import os
|
||||||
|
|
||||||
|
value = os.environ["MY_VARIABLE"]
|
||||||
|
optional_value = os.getenv("OPTIONAL_VARIABLE", "default")"""
|
||||||
|
description = "Python / FastAPI čte Variables i Secrets ze systémového prostředí."
|
||||||
|
elif "dotnet" in normalized or ".net" in normalized or "csharp" in normalized or "c#" in normalized:
|
||||||
|
code = """var value = Environment.GetEnvironmentVariable("MY_VARIABLE");
|
||||||
|
var optionalValue = builder.Configuration["OPTIONAL_VARIABLE"] ?? "default";"""
|
||||||
|
description = ".NET čte Variables i Secrets z environment variables, případně přes Configuration."
|
||||||
|
elif "node" in normalized or "javascript" in normalized or "typescript" in normalized:
|
||||||
|
code = """const value = process.env.MY_VARIABLE;
|
||||||
|
const optionalValue = process.env.OPTIONAL_VARIABLE ?? "default";"""
|
||||||
|
description = "Node.js čte Variables i Secrets přes process.env."
|
||||||
|
elif "php" in normalized:
|
||||||
|
code = """$value = getenv('MY_VARIABLE');
|
||||||
|
$optionalValue = getenv('OPTIONAL_VARIABLE') ?: 'default';"""
|
||||||
|
description = "PHP čte Variables i Secrets ze systémového prostředí."
|
||||||
|
elif "java" in normalized or "spring" in normalized:
|
||||||
|
code = """String value = System.getenv("MY_VARIABLE");
|
||||||
|
String optionalValue = System.getenv().getOrDefault("OPTIONAL_VARIABLE", "default");"""
|
||||||
|
description = "Java čte Variables i Secrets přes System.getenv."
|
||||||
|
else:
|
||||||
|
code = """MY_VARIABLE is available as an environment variable inside the container."""
|
||||||
|
description = "Variables i Secrets jsou dostupné jako environment variables v kontejneru."
|
||||||
|
|
||||||
|
return f"""
|
||||||
|
<div class="env-usage-example">
|
||||||
|
<h3>{html.escape(title)}</h3>
|
||||||
|
<p class="muted">{html.escape(description)}</p>
|
||||||
|
<pre><code>{html.escape(code)}</code></pre>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def diff_metadata(before: dict, after: dict) -> dict:
|
def diff_metadata(before: dict, after: dict) -> dict:
|
||||||
changes = {}
|
changes = {}
|
||||||
for field in METADATA_FIELDS:
|
for field in METADATA_FIELDS:
|
||||||
@@ -174,6 +261,8 @@ def apps_page(
|
|||||||
request: Request,
|
request: Request,
|
||||||
q: str = Query(""),
|
q: str = Query(""),
|
||||||
status: str = Query(""),
|
status: str = Query(""),
|
||||||
|
message: str = Query(""),
|
||||||
|
error: str = Query(""),
|
||||||
page_number: int = Query(1, alias="page", ge=1),
|
page_number: int = Query(1, alias="page", ge=1),
|
||||||
user=Depends(require_user),
|
user=Depends(require_user),
|
||||||
):
|
):
|
||||||
@@ -345,6 +434,11 @@ def apps_page(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
notice = ""
|
||||||
|
if message:
|
||||||
|
notice = f'<p class="alert">{html.escape(message)}</p>'
|
||||||
|
if error:
|
||||||
|
notice = f'<p class="alert alert-danger">{html.escape(error)}</p>'
|
||||||
|
|
||||||
return page(
|
return page(
|
||||||
"Služby",
|
"Služby",
|
||||||
@@ -368,7 +462,11 @@ def apps_page(
|
|||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Nasazené služby</h2>
|
<h2>Nasazené služby</h2>
|
||||||
|
{notice}
|
||||||
<p><a class="btn" href="/portal/new-app">+ Nová služba</a></p>
|
<p><a class="btn" href="/portal/new-app">+ Nová služba</a></p>
|
||||||
|
<form method="post" action="/portal/apps/environment/apply-all" class="inline-form">
|
||||||
|
<button type="submit" class="btn-secondary">Regenerovat všechny .env z databáze</button>
|
||||||
|
</form>
|
||||||
<form method="get" action="/portal/apps" class="filter-form">
|
<form method="get" action="/portal/apps" class="filter-form">
|
||||||
<input name="q" value="{html.escape(query)}" placeholder="Název nebo ID služby">
|
<input name="q" value="{html.escape(query)}" placeholder="Název nebo ID služby">
|
||||||
<select name="status">{"".join(status_options)}</select>
|
<select name="status">{"".join(status_options)}</select>
|
||||||
@@ -399,8 +497,24 @@ def apps_page(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/apps/environment/apply-all")
|
||||||
|
def apply_all_app_environments_action(user=Depends(require_user)):
|
||||||
|
try:
|
||||||
|
message = apply_all_environments_message()
|
||||||
|
except AppEnvironmentError as exc:
|
||||||
|
return RedirectResponse(url="/portal/apps?error=" + quote(f"Environment apply failed: {exc}"), status_code=303)
|
||||||
|
|
||||||
|
log_audit_event(
|
||||||
|
user,
|
||||||
|
action="app.environment.applied_all",
|
||||||
|
target_type="app",
|
||||||
|
metadata={"result": message},
|
||||||
|
)
|
||||||
|
return RedirectResponse(url="/portal/apps?message=" + quote(message), status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/apps/{app_id}", response_class=HTMLResponse)
|
@router.get("/apps/{app_id}", response_class=HTMLResponse)
|
||||||
def app_detail(app_id: str, request: Request, user=Depends(require_user)):
|
def app_detail(app_id: str, request: Request, message: str = "", error: str = "", user=Depends(require_user)):
|
||||||
app = get_app(app_id)
|
app = get_app(app_id)
|
||||||
if not app:
|
if not app:
|
||||||
raise HTTPException(status_code=404, detail="App not found")
|
raise HTTPException(status_code=404, detail="App not found")
|
||||||
@@ -415,7 +529,10 @@ def app_detail(app_id: str, request: Request, user=Depends(require_user)):
|
|||||||
escaped_app_id = html.escape(app.get("id", ""))
|
escaped_app_id = html.escape(app.get("id", ""))
|
||||||
app_url_id = quote(app.get("id", ""), safe="")
|
app_url_id = quote(app.get("id", ""), safe="")
|
||||||
name = html.escape(app.get("name", "") or "")
|
name = html.escape(app.get("name", "") or "")
|
||||||
language = html.escape(app.get("language", "") or "")
|
language_raw = app.get("language", "") or ""
|
||||||
|
runtime_raw = app.get("runtime", "") or ""
|
||||||
|
template_raw = app.get("template", "") or ""
|
||||||
|
language = html.escape(language_raw)
|
||||||
version = html.escape(app.get("version", "") or "")
|
version = html.escape(app.get("version", "") or "")
|
||||||
status = html.escape(app.get("status", "") or "")
|
status = html.escape(app.get("status", "") or "")
|
||||||
memory = html.escape(app.get("memory", "") or "")
|
memory = html.escape(app.get("memory", "") or "")
|
||||||
@@ -423,7 +540,7 @@ def app_detail(app_id: str, request: Request, user=Depends(require_user)):
|
|||||||
updated_at = html.escape(app.get("updated_at", "") or "")
|
updated_at = html.escape(app.get("updated_at", "") or "")
|
||||||
description = html.escape(app.get("description", "") or "")
|
description = html.escape(app.get("description", "") or "")
|
||||||
owner = html.escape(app.get("owner", "") or "")
|
owner = html.escape(app.get("owner", "") or "")
|
||||||
runtime = html.escape(app.get("runtime", "") or "")
|
runtime = html.escape(runtime_raw)
|
||||||
repository_url = html.escape(app.get("repository_url", "") or "")
|
repository_url = html.escape(app.get("repository_url", "") or "")
|
||||||
repository_name = html.escape(app.get("repository_name", "") or "")
|
repository_name = html.escape(app.get("repository_name", "") or "")
|
||||||
default_branch = html.escape(app.get("default_branch", "") or "")
|
default_branch = html.escape(app.get("default_branch", "") or "")
|
||||||
@@ -435,6 +552,7 @@ def app_detail(app_id: str, request: Request, user=Depends(require_user)):
|
|||||||
templates = get_app_templates()
|
templates = get_app_templates()
|
||||||
template_options = render_template_options(templates, app.get("template", "") or "")
|
template_options = render_template_options(templates, app.get("template", "") or "")
|
||||||
template_value = render_template_label(templates, app.get("template", "") or "")
|
template_value = render_template_label(templates, app.get("template", "") or "")
|
||||||
|
environment_usage_example = render_environment_usage_example(language_raw, runtime_raw, template_raw)
|
||||||
variables = get_app_variables(app.get("id", ""))
|
variables = get_app_variables(app.get("id", ""))
|
||||||
incidents = get_service_incidents(app.get("id", ""), limit=20)
|
incidents = get_service_incidents(app.get("id", ""), limit=20)
|
||||||
current_health = get_service_health(app.get("id", ""))
|
current_health = get_service_health(app.get("id", ""))
|
||||||
@@ -532,12 +650,19 @@ def app_detail(app_id: str, request: Request, user=Depends(require_user)):
|
|||||||
if not variable_rows:
|
if not variable_rows:
|
||||||
variable_rows = '<tr><td colspan="4">Zatím nejsou evidované žádné proměnné.</td></tr>'
|
variable_rows = '<tr><td colspan="4">Zatím nejsou evidované žádné proměnné.</td></tr>'
|
||||||
|
|
||||||
|
notice = ""
|
||||||
|
if message:
|
||||||
|
notice = f'<p class="alert">{html.escape(message)}</p>'
|
||||||
|
if error:
|
||||||
|
notice = f'<p class="alert alert-danger">{html.escape(error)}</p>'
|
||||||
|
|
||||||
return page(
|
return page(
|
||||||
"Detail slu\u017eby",
|
"Detail slu\u017eby",
|
||||||
f"""
|
f"""
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>{escaped_app_id}</h2>
|
<h2>{escaped_app_id}</h2>
|
||||||
<p class="muted">{name}</p>
|
<p class="muted">{name}</p>
|
||||||
|
{notice}
|
||||||
<p>
|
<p>
|
||||||
<a class="btn" href="/portal/apps">← Zpět na služby</a>
|
<a class="btn" href="/portal/apps">← Zpět na služby</a>
|
||||||
<a class="btn btn-secondary" href="/portal/deployments?app_id={app_url_id}">Nasazení služby</a>
|
<a class="btn btn-secondary" href="/portal/deployments?app_id={app_url_id}">Nasazení služby</a>
|
||||||
@@ -601,6 +726,10 @@ def app_detail(app_id: str, request: Request, user=Depends(require_user)):
|
|||||||
|
|
||||||
<div class="card" id="promenne">
|
<div class="card" id="promenne">
|
||||||
<h2>Proměnné</h2>
|
<h2>Proměnné</h2>
|
||||||
|
<form method="post" action="/portal/apps/{app_url_id}/environment/apply" class="inline-form">
|
||||||
|
<button type="submit" class="btn-secondary">Regenerovat .env z databáze</button>
|
||||||
|
</form>
|
||||||
|
{environment_usage_example}
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Key</th>
|
<th>Key</th>
|
||||||
@@ -786,6 +915,7 @@ def add_app_variable(
|
|||||||
variable_key = clean_optional(key)
|
variable_key = clean_optional(key)
|
||||||
if not variable_key:
|
if not variable_key:
|
||||||
raise HTTPException(status_code=400, detail="Key is required")
|
raise HTTPException(status_code=400, detail="Key is required")
|
||||||
|
ensure_variable_key_allowed(app_id, variable_key)
|
||||||
|
|
||||||
create_app_variable(app_id, variable_key, value, bool(is_secret))
|
create_app_variable(app_id, variable_key, value, bool(is_secret))
|
||||||
log_audit_event(
|
log_audit_event(
|
||||||
@@ -796,7 +926,12 @@ def add_app_variable(
|
|||||||
metadata={"key": variable_key, "is_secret": bool(is_secret)},
|
metadata={"key": variable_key, "is_secret": bool(is_secret)},
|
||||||
)
|
)
|
||||||
|
|
||||||
return RedirectResponse(url=f"/portal/apps/{quote(app_id, safe='')}#promenne", status_code=303)
|
try:
|
||||||
|
message = apply_environment_message(app_id)
|
||||||
|
except AppEnvironmentError as exc:
|
||||||
|
return redirect_app_detail(app_id, anchor="promenne", error=f"Variable saved, but environment apply failed: {exc}")
|
||||||
|
|
||||||
|
return redirect_app_detail(app_id, anchor="promenne", message=message)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/apps/{app_id}/variables/{variable_id}/update")
|
@router.post("/apps/{app_id}/variables/{variable_id}/update")
|
||||||
@@ -819,8 +954,9 @@ def save_app_variable(
|
|||||||
variable_key = clean_optional(key)
|
variable_key = clean_optional(key)
|
||||||
if not variable_key:
|
if not variable_key:
|
||||||
raise HTTPException(status_code=400, detail="Key is required")
|
raise HTTPException(status_code=400, detail="Key is required")
|
||||||
|
ensure_variable_key_allowed(app_id, variable_key, variable_id=variable_id)
|
||||||
|
|
||||||
stored_value = None if existing.get("is_secret") and value == "" else value
|
stored_value = None if existing.get("is_secret") and bool(is_secret) and value == "" else value
|
||||||
update_app_variable(variable_id, app_id, variable_key, stored_value, bool(is_secret))
|
update_app_variable(variable_id, app_id, variable_key, stored_value, bool(is_secret))
|
||||||
log_audit_event(
|
log_audit_event(
|
||||||
user,
|
user,
|
||||||
@@ -830,7 +966,12 @@ def save_app_variable(
|
|||||||
metadata={"key": variable_key, "is_secret": bool(is_secret)},
|
metadata={"key": variable_key, "is_secret": bool(is_secret)},
|
||||||
)
|
)
|
||||||
|
|
||||||
return RedirectResponse(url=f"/portal/apps/{quote(app_id, safe='')}#promenne", status_code=303)
|
try:
|
||||||
|
message = apply_environment_message(app_id)
|
||||||
|
except AppEnvironmentError as exc:
|
||||||
|
return redirect_app_detail(app_id, anchor="promenne", error=f"Variable saved, but environment apply failed: {exc}")
|
||||||
|
|
||||||
|
return redirect_app_detail(app_id, anchor="promenne", message=message)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/apps/{app_id}/variables/{variable_id}/delete")
|
@router.post("/apps/{app_id}/variables/{variable_id}/delete")
|
||||||
@@ -852,7 +993,33 @@ def remove_app_variable(app_id: str, variable_id: int, user=Depends(require_user
|
|||||||
metadata={"key": existing.get("key"), "is_secret": bool(existing.get("is_secret"))},
|
metadata={"key": existing.get("key"), "is_secret": bool(existing.get("is_secret"))},
|
||||||
)
|
)
|
||||||
|
|
||||||
return RedirectResponse(url=f"/portal/apps/{quote(app_id, safe='')}#promenne", status_code=303)
|
try:
|
||||||
|
message = apply_environment_message(app_id)
|
||||||
|
except AppEnvironmentError as exc:
|
||||||
|
return redirect_app_detail(app_id, anchor="promenne", error=f"Variable deleted, but environment apply failed: {exc}")
|
||||||
|
|
||||||
|
return redirect_app_detail(app_id, anchor="promenne", message=message)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/apps/{app_id}/environment/apply")
|
||||||
|
def apply_app_environment_action(app_id: str, user=Depends(require_user)):
|
||||||
|
app = get_app(app_id)
|
||||||
|
if not app:
|
||||||
|
raise HTTPException(status_code=404, detail="App not found")
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = apply_environment_message(app_id)
|
||||||
|
except AppEnvironmentError as exc:
|
||||||
|
return redirect_app_detail(app_id, anchor="promenne", error=f"Environment apply failed: {exc}")
|
||||||
|
|
||||||
|
log_audit_event(
|
||||||
|
user,
|
||||||
|
action="app.environment.applied",
|
||||||
|
target_type="app",
|
||||||
|
target_id=app_id,
|
||||||
|
metadata={"result": message},
|
||||||
|
)
|
||||||
|
return redirect_app_detail(app_id, anchor="promenne", message=message)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/apps/{app_id}/redeploy")
|
@router.post("/apps/{app_id}/redeploy")
|
||||||
@@ -861,6 +1028,11 @@ def redeploy_app(app_id: str, user=Depends(require_user)):
|
|||||||
if not app:
|
if not app:
|
||||||
raise HTTPException(status_code=404, detail="App not found")
|
raise HTTPException(status_code=404, detail="App not found")
|
||||||
|
|
||||||
|
try:
|
||||||
|
apply_environment_message(app_id)
|
||||||
|
except AppEnvironmentError as exc:
|
||||||
|
return redirect_app_detail(app_id, error=f"Environment apply failed: {exc}")
|
||||||
|
|
||||||
active_job = has_active_deploy_job("app", app_id)
|
active_job = has_active_deploy_job("app", app_id)
|
||||||
if active_job:
|
if active_job:
|
||||||
return RedirectResponse(url=f"/portal/jobs/{active_job['id']}", status_code=303)
|
return RedirectResponse(url=f"/portal/jobs/{active_job['id']}", status_code=303)
|
||||||
@@ -1024,6 +1196,27 @@ def create_app(
|
|||||||
user=user,
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
apply_environment_message(app_id)
|
||||||
|
except AppEnvironmentError as exc:
|
||||||
|
log_audit_event(
|
||||||
|
user,
|
||||||
|
action="app.environment.apply_failed",
|
||||||
|
target_type="app",
|
||||||
|
target_id=app_id,
|
||||||
|
metadata={"error": str(exc)},
|
||||||
|
)
|
||||||
|
return render_result(
|
||||||
|
title="GenerovánĂ prostĹ™edĂ: FAILED",
|
||||||
|
back_url="/portal/apps",
|
||||||
|
sections=[
|
||||||
|
("VĂ˝stup vytvoĹ™enĂ", create_result.stdout),
|
||||||
|
("Chyba vytvoĹ™enĂ", create_result.stderr),
|
||||||
|
("Chyba prostĹ™edĂ", str(exc)),
|
||||||
|
],
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
|
||||||
job_id = create_job(
|
job_id = create_job(
|
||||||
job_type="deploy_app",
|
job_type="deploy_app",
|
||||||
target_type="app",
|
target_type="app",
|
||||||
|
|||||||
@@ -573,6 +573,21 @@ textarea:disabled {
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.env-usage-example {
|
||||||
|
margin: 14px 0 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.env-usage-example h3 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
color: var(--secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.env-usage-example pre {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
input[readonly] {
|
input[readonly] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
|||||||
Reference in New Issue
Block a user