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.incidents import get_service_incidents
|
||||
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.incidents import render_incident_history_rows
|
||||
from ..shell import run_command
|
||||
@@ -75,6 +76,54 @@ def bool_checked(value) -> str:
|
||||
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:
|
||||
options = ['<option value="">Bez šablony</option>'] if include_blank else []
|
||||
selected_exists = not selected_template
|
||||
@@ -110,6 +159,44 @@ def render_template_label(templates: list[dict], template_id: str) -> str:
|
||||
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:
|
||||
changes = {}
|
||||
for field in METADATA_FIELDS:
|
||||
@@ -174,6 +261,8 @@ def apps_page(
|
||||
request: Request,
|
||||
q: str = Query(""),
|
||||
status: str = Query(""),
|
||||
message: str = Query(""),
|
||||
error: str = Query(""),
|
||||
page_number: int = Query(1, alias="page", ge=1),
|
||||
user=Depends(require_user),
|
||||
):
|
||||
@@ -345,6 +434,11 @@ def apps_page(
|
||||
</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(
|
||||
"Služby",
|
||||
@@ -368,7 +462,11 @@ def apps_page(
|
||||
|
||||
<div class="card">
|
||||
<h2>Nasazené služby</h2>
|
||||
{notice}
|
||||
<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">
|
||||
<input name="q" value="{html.escape(query)}" placeholder="Název nebo ID služby">
|
||||
<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)
|
||||
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)
|
||||
if not app:
|
||||
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", ""))
|
||||
app_url_id = quote(app.get("id", ""), safe="")
|
||||
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 "")
|
||||
status = html.escape(app.get("status", "") 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 "")
|
||||
description = html.escape(app.get("description", "") 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_name = html.escape(app.get("repository_name", "") 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()
|
||||
template_options = render_template_options(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", ""))
|
||||
incidents = get_service_incidents(app.get("id", ""), limit=20)
|
||||
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:
|
||||
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(
|
||||
"Detail slu\u017eby",
|
||||
f"""
|
||||
<div class="card">
|
||||
<h2>{escaped_app_id}</h2>
|
||||
<p class="muted">{name}</p>
|
||||
{notice}
|
||||
<p>
|
||||
<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>
|
||||
@@ -601,6 +726,10 @@ def app_detail(app_id: str, request: Request, user=Depends(require_user)):
|
||||
|
||||
<div class="card" id="promenne">
|
||||
<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>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
@@ -786,6 +915,7 @@ def add_app_variable(
|
||||
variable_key = clean_optional(key)
|
||||
if not variable_key:
|
||||
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))
|
||||
log_audit_event(
|
||||
@@ -796,7 +926,12 @@ def add_app_variable(
|
||||
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")
|
||||
@@ -819,8 +954,9 @@ def save_app_variable(
|
||||
variable_key = clean_optional(key)
|
||||
if not variable_key:
|
||||
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))
|
||||
log_audit_event(
|
||||
user,
|
||||
@@ -830,7 +966,12 @@ def save_app_variable(
|
||||
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")
|
||||
@@ -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"))},
|
||||
)
|
||||
|
||||
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")
|
||||
@@ -861,6 +1028,11 @@ def redeploy_app(app_id: str, user=Depends(require_user)):
|
||||
if not app:
|
||||
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)
|
||||
if active_job:
|
||||
return RedirectResponse(url=f"/portal/jobs/{active_job['id']}", status_code=303)
|
||||
@@ -1024,6 +1196,27 @@ def create_app(
|
||||
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_type="deploy_app",
|
||||
target_type="app",
|
||||
|
||||
@@ -573,6 +573,21 @@ textarea:disabled {
|
||||
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] {
|
||||
width: 100%;
|
||||
font-family: monospace;
|
||||
|
||||
Reference in New Issue
Block a user