feat(ops): show deployment capability receipt
This commit is contained in:
@@ -43,6 +43,7 @@ from govoplan_core.db.session import get_database
|
||||
from govoplan_core.settings import settings as core_settings
|
||||
|
||||
from govoplan_ops.backend.manifest import OPS_READ_SCOPES, OPS_RUN_SCOPES
|
||||
from govoplan_ops.backend.infrastructure import deployment_capability_status
|
||||
|
||||
router = APIRouter(prefix="/ops", tags=["ops"])
|
||||
_module_check_cache: dict[str, tuple[float, dict[str, Any]]] = {}
|
||||
@@ -207,6 +208,7 @@ def _ops_status_payload(
|
||||
)
|
||||
storage_check = _storage_check()
|
||||
backup_check = _backup_restore_check()
|
||||
infrastructure = deployment_capability_status()
|
||||
recovery_metrics = runtime_cluster.get("recovery", {}).get("metrics", {})
|
||||
checks = [
|
||||
_check(
|
||||
@@ -229,6 +231,7 @@ def _ops_status_payload(
|
||||
storage_check,
|
||||
backup_check,
|
||||
_deployment_security_check(current_profile),
|
||||
_infrastructure_capability_check(infrastructure),
|
||||
*module_checks,
|
||||
]
|
||||
readiness = _readiness(checks, maintenance_mode)
|
||||
@@ -262,6 +265,12 @@ def _ops_status_payload(
|
||||
"failed_operation_count": int(recovery_metrics.get("failed") or 0),
|
||||
"outcome_unknown_count": int(recovery_metrics.get("outcome_unknown") or 0),
|
||||
"active_operation_count": int(recovery_metrics.get("active") or 0),
|
||||
"infrastructure_capability_count": len(
|
||||
infrastructure.get("capabilities", [])
|
||||
),
|
||||
"pending_post_install_task_count": len(
|
||||
infrastructure.get("post_install_tasks", [])
|
||||
),
|
||||
},
|
||||
"readiness": readiness,
|
||||
"checks": checks,
|
||||
@@ -269,9 +278,40 @@ def _ops_status_payload(
|
||||
"deployment_profiles": _deployment_profiles(current_profile),
|
||||
"sizing": _sizing_assumptions(),
|
||||
"runtime_cluster": runtime_cluster,
|
||||
"infrastructure": infrastructure,
|
||||
}
|
||||
|
||||
|
||||
def _infrastructure_capability_check(
|
||||
infrastructure: Mapping[str, object],
|
||||
) -> dict[str, Any]:
|
||||
if infrastructure.get("available") is True:
|
||||
capabilities = infrastructure.get("capabilities")
|
||||
count = len(capabilities) if isinstance(capabilities, list) else 0
|
||||
return _check(
|
||||
"infrastructure_capability_receipt",
|
||||
"Infrastructure capabilities",
|
||||
"ok",
|
||||
f"A validated non-secret deployment receipt reports {count} capabilities.",
|
||||
)
|
||||
if infrastructure.get("configured") is True:
|
||||
return _check(
|
||||
"infrastructure_capability_receipt",
|
||||
"Infrastructure capabilities",
|
||||
"warning",
|
||||
str(
|
||||
infrastructure.get("error")
|
||||
or "The configured deployment capability receipt is unavailable."
|
||||
),
|
||||
)
|
||||
return _check(
|
||||
"infrastructure_capability_receipt",
|
||||
"Infrastructure capabilities",
|
||||
"ok",
|
||||
"No deployment capability receipt is mounted in this runtime profile.",
|
||||
)
|
||||
|
||||
|
||||
def _runtime_cluster_status(
|
||||
*,
|
||||
expected_composition_hash: str | None = None,
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
|
||||
MAX_CAPABILITY_DOCUMENT_BYTES = 256 * 1024
|
||||
CAPABILITY_STATES = {
|
||||
"configured",
|
||||
"available_unconfigured",
|
||||
"externally_supplied",
|
||||
"unavailable",
|
||||
}
|
||||
|
||||
|
||||
def deployment_capability_status(
|
||||
path: Path | None = None,
|
||||
) -> dict[str, object]:
|
||||
configured_path = path
|
||||
if configured_path is None:
|
||||
raw_path = os.getenv("GOVOPLAN_DEPLOYMENT_CAPABILITIES_PATH", "").strip()
|
||||
if not raw_path:
|
||||
return _unavailable(configured=False, error=None)
|
||||
configured_path = Path(raw_path)
|
||||
try:
|
||||
document = _read_document(configured_path)
|
||||
return {
|
||||
"configured": True,
|
||||
"available": True,
|
||||
"schema_version": 1,
|
||||
"installation_id": document["installation_id"],
|
||||
"profile": document["profile"],
|
||||
"capabilities": document["capabilities"],
|
||||
"post_install_tasks": document["post_install_tasks"],
|
||||
"error": None,
|
||||
}
|
||||
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
||||
return _unavailable(configured=True, error=str(exc))
|
||||
|
||||
|
||||
def _read_document(path: Path) -> dict[str, object]:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ValueError("Deployment capability receipt is not a regular file.")
|
||||
size = path.stat().st_size
|
||||
if size > MAX_CAPABILITY_DOCUMENT_BYTES:
|
||||
raise ValueError("Deployment capability receipt exceeds 256 KiB.")
|
||||
raw = path.read_bytes()
|
||||
if len(raw) != size:
|
||||
raise ValueError("Deployment capability receipt changed while being read.")
|
||||
payload = json.loads(raw.decode("utf-8"))
|
||||
if not isinstance(payload, Mapping) or payload.get("schema_version") != 1:
|
||||
raise ValueError("Deployment capability receipt has an unsupported schema.")
|
||||
installation_id = _required_text(payload, "installation_id", maximum=100)
|
||||
profile = _required_text(payload, "profile", maximum=100)
|
||||
raw_capabilities = payload.get("capabilities")
|
||||
if not isinstance(raw_capabilities, list) or len(raw_capabilities) > 100:
|
||||
raise ValueError("Deployment capability receipt has invalid capabilities.")
|
||||
capabilities = [_capability(item) for item in raw_capabilities]
|
||||
ids = [str(item["id"]) for item in capabilities]
|
||||
if len(ids) != len(set(ids)):
|
||||
raise ValueError("Deployment capability receipt repeats a capability id.")
|
||||
raw_tasks = payload.get("post_install_tasks", [])
|
||||
if not isinstance(raw_tasks, list) or len(raw_tasks) > 100:
|
||||
raise ValueError("Deployment capability receipt has invalid post-install tasks.")
|
||||
tasks = [_task(item) for item in raw_tasks]
|
||||
return {
|
||||
"installation_id": installation_id,
|
||||
"profile": profile,
|
||||
"capabilities": capabilities,
|
||||
"post_install_tasks": tasks,
|
||||
}
|
||||
|
||||
|
||||
def _capability(value: object) -> dict[str, object]:
|
||||
if not isinstance(value, Mapping):
|
||||
raise ValueError("Deployment capability entries must be objects.")
|
||||
state = _required_text(value, "state", maximum=40)
|
||||
if state not in CAPABILITY_STATES:
|
||||
raise ValueError(f"Deployment capability state is unsupported: {state!r}.")
|
||||
endpoint = value.get("endpoint", {})
|
||||
if not isinstance(endpoint, Mapping) or len(endpoint) > 10:
|
||||
raise ValueError("Deployment capability endpoint metadata is invalid.")
|
||||
normalized_endpoint: dict[str, object] = {}
|
||||
for key, raw in endpoint.items():
|
||||
if not isinstance(key, str) or len(key) > 50:
|
||||
raise ValueError("Deployment capability endpoint key is invalid.")
|
||||
if isinstance(raw, bool) or raw is None:
|
||||
normalized_endpoint[key] = raw
|
||||
elif isinstance(raw, int):
|
||||
normalized_endpoint[key] = raw
|
||||
elif isinstance(raw, str) and len(raw) <= 500:
|
||||
normalized_endpoint[key] = raw
|
||||
else:
|
||||
raise ValueError("Deployment capability endpoint value is invalid.")
|
||||
secret_refs = _string_list(value.get("secret_refs"), maximum_items=30)
|
||||
if any(not item.startswith("env:") for item in secret_refs):
|
||||
raise ValueError("Deployment capability secrets must use environment references.")
|
||||
return {
|
||||
"id": _required_text(value, "id", maximum=120),
|
||||
"label": _required_text(value, "label", maximum=200),
|
||||
"state": state,
|
||||
"source": _required_text(value, "source", maximum=120),
|
||||
"detail": _required_text(value, "detail", maximum=1000),
|
||||
"endpoint": normalized_endpoint,
|
||||
"secret_refs": secret_refs,
|
||||
"dependent_modules": _string_list(
|
||||
value.get("dependent_modules"),
|
||||
maximum_items=100,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _task(value: object) -> dict[str, object]:
|
||||
if not isinstance(value, Mapping):
|
||||
raise ValueError("Deployment post-install task entries must be objects.")
|
||||
return {
|
||||
"id": _required_text(value, "id", maximum=120),
|
||||
"resume_key": _required_text(value, "resume_key", maximum=240),
|
||||
"capability_id": _required_text(value, "capability_id", maximum=120),
|
||||
"state": _required_text(value, "state", maximum=40),
|
||||
"owner_module": _required_text(value, "owner_module", maximum=120),
|
||||
"summary": _required_text(value, "summary", maximum=1000),
|
||||
"required_inputs": _string_list(
|
||||
value.get("required_inputs"),
|
||||
maximum_items=30,
|
||||
),
|
||||
"secret_boundary": _required_text(
|
||||
value,
|
||||
"secret_boundary",
|
||||
maximum=120,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _required_text(
|
||||
value: Mapping[str, Any],
|
||||
key: str,
|
||||
*,
|
||||
maximum: int,
|
||||
) -> str:
|
||||
raw = value.get(key)
|
||||
text = str(raw).strip() if raw is not None else ""
|
||||
if not text or len(text) > maximum:
|
||||
raise ValueError(f"Deployment capability field {key!r} is invalid.")
|
||||
return text
|
||||
|
||||
|
||||
def _string_list(value: object, *, maximum_items: int) -> list[str]:
|
||||
if not isinstance(value, list) or len(value) > maximum_items:
|
||||
raise ValueError("Deployment capability list field is invalid.")
|
||||
result: list[str] = []
|
||||
for item in value:
|
||||
if not isinstance(item, str) or not item.strip() or len(item) > 500:
|
||||
raise ValueError("Deployment capability list item is invalid.")
|
||||
result.append(item.strip())
|
||||
return result
|
||||
|
||||
|
||||
def _unavailable(*, configured: bool, error: str | None) -> dict[str, object]:
|
||||
return {
|
||||
"configured": configured,
|
||||
"available": False,
|
||||
"schema_version": None,
|
||||
"installation_id": None,
|
||||
"profile": None,
|
||||
"capabilities": [],
|
||||
"post_install_tasks": [],
|
||||
"error": error,
|
||||
}
|
||||
@@ -129,7 +129,7 @@ manifest = ModuleManifest(
|
||||
id="ops.health-governance-and-sizing",
|
||||
title="Inspect platform health and deployment posture",
|
||||
summary="Ops combines module-owned health checks with deployment profile, governance inventory, worker assumptions, and sizing guidance.",
|
||||
body="Read-only status distinguishes configured capabilities from healthy integrations. Authorized operators can run bounded probes; a probe must not perform unbounded business work or silently repair data. Use readiness and worker results when diagnosing a node, and use the deployment profile and sizing assumptions when planning horizontal capacity.",
|
||||
body="Read-only status distinguishes configured capabilities from healthy integrations. When the deployment mounts a signed or locally generated non-secret infrastructure capability receipt, Ops shows whether PostgreSQL, Redis, SMTP, file storage, load balancing, and ingress are configured, externally supplied, available but unconfigured, or unavailable. Secret values never cross this boundary; only stable environment or credential-envelope references may be disclosed. Pending post-install tasks remain visible with a stable resume key. Authorized operators can run bounded probes; a probe must not perform unbounded business work or silently repair data. Use readiness and worker results when diagnosing a node, and use the deployment profile and sizing assumptions when planning horizontal capacity.",
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("operator", "system_admin"),
|
||||
related_modules=("audit", "docs", "notifications"),
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from govoplan_ops.backend.infrastructure import deployment_capability_status
|
||||
|
||||
|
||||
class InfrastructureCapabilityTests(unittest.TestCase):
|
||||
def test_reads_bounded_non_secret_capability_receipt(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-ops-capabilities-") as root:
|
||||
path = Path(root) / "capabilities.json"
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"installation_id": "govoplan-test",
|
||||
"profile": "evaluation",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "mail.smtp",
|
||||
"label": "SMTP delivery",
|
||||
"state": "available_unconfigured",
|
||||
"source": "operator-supplied",
|
||||
"detail": "Mail needs a profile.",
|
||||
"endpoint": {},
|
||||
"secret_refs": ["env:SMTP_CREDENTIAL_REF"],
|
||||
"dependent_modules": ["mail"],
|
||||
}
|
||||
],
|
||||
"post_install_tasks": [
|
||||
{
|
||||
"id": "mail.smtp-profile",
|
||||
"resume_key": "govoplan-test:mail.smtp-profile:v1",
|
||||
"capability_id": "mail.smtp",
|
||||
"state": "pending",
|
||||
"owner_module": "mail",
|
||||
"summary": "Configure Mail.",
|
||||
"required_inputs": ["credential envelope reference"],
|
||||
"secret_boundary": "credential-envelope-reference-only",
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = deployment_capability_status(path)
|
||||
|
||||
self.assertTrue(result["available"])
|
||||
self.assertEqual("mail.smtp", result["capabilities"][0]["id"])
|
||||
self.assertEqual("mail.smtp-profile", result["post_install_tasks"][0]["id"])
|
||||
|
||||
def test_rejects_inline_secret_instead_of_reference(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-ops-capabilities-") as root:
|
||||
path = Path(root) / "capabilities.json"
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"installation_id": "govoplan-test",
|
||||
"profile": "evaluation",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "mail.smtp",
|
||||
"label": "SMTP delivery",
|
||||
"state": "configured",
|
||||
"source": "operator-supplied",
|
||||
"detail": "Configured.",
|
||||
"endpoint": {},
|
||||
"secret_refs": ["plaintext-secret"],
|
||||
"dependent_modules": [],
|
||||
}
|
||||
],
|
||||
"post_install_tasks": [],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = deployment_capability_status(path)
|
||||
|
||||
self.assertFalse(result["available"])
|
||||
self.assertIn("environment references", str(result["error"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -17,6 +17,28 @@ export type OpsDeploymentProfile = {
|
||||
fit: string;
|
||||
};
|
||||
|
||||
export type OpsInfrastructureCapability = {
|
||||
id: string;
|
||||
label: string;
|
||||
state: "configured" | "available_unconfigured" | "externally_supplied" | "unavailable";
|
||||
source: string;
|
||||
detail: string;
|
||||
endpoint: Record<string, string | number | boolean | null>;
|
||||
secret_refs: string[];
|
||||
dependent_modules: string[];
|
||||
};
|
||||
|
||||
export type OpsPostInstallTask = {
|
||||
id: string;
|
||||
resume_key: string;
|
||||
capability_id: string;
|
||||
state: string;
|
||||
owner_module: string;
|
||||
summary: string;
|
||||
required_inputs: string[];
|
||||
secret_boundary: string;
|
||||
};
|
||||
|
||||
export type OpsSizingAssumption = {
|
||||
area: string;
|
||||
baseline: string;
|
||||
@@ -186,6 +208,8 @@ export type OpsStatus = {
|
||||
failed_operation_count?: number;
|
||||
outcome_unknown_count?: number;
|
||||
active_operation_count?: number;
|
||||
infrastructure_capability_count?: number;
|
||||
pending_post_install_task_count?: number;
|
||||
};
|
||||
readiness: {
|
||||
ready: boolean;
|
||||
@@ -220,6 +244,16 @@ export type OpsStatus = {
|
||||
deployment_profiles: OpsDeploymentProfile[];
|
||||
sizing: OpsSizingAssumption[];
|
||||
runtime_cluster: OpsRuntimeCluster;
|
||||
infrastructure: {
|
||||
configured: boolean;
|
||||
available: boolean;
|
||||
schema_version?: number | null;
|
||||
installation_id?: string | null;
|
||||
profile?: string | null;
|
||||
capabilities: OpsInfrastructureCapability[];
|
||||
post_install_tasks: OpsPostInstallTask[];
|
||||
error?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchOpsStatus(settings: ApiSettings): Promise<OpsStatus> {
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
type OpsCheck,
|
||||
type OpsDeploymentProfile,
|
||||
type OpsGovernanceModule,
|
||||
type OpsInfrastructureCapability,
|
||||
type OpsRecoveryOperation,
|
||||
type OpsRuntimeNode,
|
||||
type OpsSizingAssumption,
|
||||
@@ -168,6 +169,7 @@ export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth:
|
||||
<MetricCard label="Database capacity" value={databaseCapacityValue(status)} tone={databaseCapacityTone(status)} detail="Peak pooled connections / available connections" />
|
||||
<MetricCard label="Recovery" value={status?.summary.recovery_required_count ?? 0} tone={status?.summary.recovery_required_count ? "danger" : "good"} detail="Operations requiring recovery attention" />
|
||||
<MetricCard label="Provider bindings" value={status?.governance.summary.configured_external_provider_count ?? 0} tone={status?.governance.summary.provider_attention_count ? "warning" : "good"} detail={`${status?.governance.summary.provider_attention_count ?? 0} requiring attention`} />
|
||||
<MetricCard label="i18n:govoplan-ops.infrastructure_capabilities" value={status?.summary.infrastructure_capability_count ?? 0} tone={status?.infrastructure.available ? "good" : "neutral"} detail={i18nMessage("i18n:govoplan-ops.pending_post_install_tasks", { value0: status?.summary.pending_post_install_task_count ?? 0 })} />
|
||||
</div>
|
||||
|
||||
<div className="dashboard-grid">
|
||||
@@ -197,6 +199,16 @@ export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth:
|
||||
<ProfileList profiles={status?.deployment_profiles ?? []} />
|
||||
</Card>
|
||||
|
||||
<Card title="i18n:govoplan-ops.infrastructure_capabilities">
|
||||
<InfrastructureCapabilityTable items={status?.infrastructure.capabilities ?? []} />
|
||||
{(status?.infrastructure.post_install_tasks.length ?? 0) > 0 && <dl className="detail-list">
|
||||
{status?.infrastructure.post_install_tasks.map((task) => <div key={task.resume_key}>
|
||||
<dt><StatusBadge status="warning" label={task.state} /></dt>
|
||||
<dd><strong>{task.summary}</strong><span className="muted"> · {task.owner_module} · {task.required_inputs.join(", ")}</span></dd>
|
||||
</div>)}
|
||||
</dl>}
|
||||
</Card>
|
||||
|
||||
<Card title="i18n:govoplan-ops.sizing_assumptions.6ade9a90">
|
||||
<SizingTable items={status?.sizing ?? []} />
|
||||
</Card>
|
||||
@@ -507,6 +519,33 @@ function SizingTable({ items }: {items: OpsSizingAssumption[];}) {
|
||||
|
||||
}
|
||||
|
||||
function InfrastructureCapabilityTable({ items }: {items: OpsInfrastructureCapability[];}) {
|
||||
if (!items.length) return <p className="muted">i18n:govoplan-ops.no_infrastructure_capabilities</p>;
|
||||
const columns: DataGridColumn<OpsInfrastructureCapability>[] = [
|
||||
{ id: "capability", header: "i18n:govoplan-ops.capability", width: "minmax(220px, 1fr)", minWidth: 190, resizable: true, sortable: true, filterable: true, value: (item) => `${item.label} ${item.id}`, render: (item) => <div><strong>{item.label}</strong><span className="muted block">{item.id}</span></div> },
|
||||
{ id: "state", header: "i18n:govoplan-ops.status.bae7d5be", width: 190, sortable: true, filterable: true, value: (item) => item.state, render: (item) => <StatusBadge status={capabilityTone(item.state)} label={item.state.replaceAll("_", " ")} /> },
|
||||
{ id: "source", header: "i18n:govoplan-ops.source", width: "minmax(180px, .7fr)", minWidth: 160, resizable: true, filterable: true, value: (item) => item.source },
|
||||
{ id: "endpoint", header: "i18n:govoplan-ops.endpoint", width: "minmax(220px, 1fr)", minWidth: 180, resizable: true, filterable: true, value: (item) => endpointLabel(item.endpoint) },
|
||||
{ id: "consumers", header: "i18n:govoplan-ops.consumers", width: "minmax(220px, 1fr)", minWidth: 180, resizable: true, filterable: true, value: (item) => item.dependent_modules.join(" "), render: (item) => item.dependent_modules.join(", ") || "-" }
|
||||
];
|
||||
return <DataGrid id="ops-infrastructure-capabilities" rows={items} columns={columns} getRowKey={(item) => item.id} />;
|
||||
}
|
||||
|
||||
function endpointLabel(endpoint: OpsInfrastructureCapability["endpoint"]): string {
|
||||
if (typeof endpoint.host === "string") {
|
||||
const scheme = typeof endpoint.scheme === "string" ? `${endpoint.scheme}://` : "";
|
||||
const port = typeof endpoint.port === "number" ? `:${endpoint.port}` : "";
|
||||
return `${scheme}${endpoint.host}${port}`;
|
||||
}
|
||||
return typeof endpoint.reference === "string" ? endpoint.reference : "-";
|
||||
}
|
||||
|
||||
function capabilityTone(state: OpsInfrastructureCapability["state"]): string {
|
||||
if (state === "configured" || state === "externally_supplied") return "success";
|
||||
if (state === "available_unconfigured") return "warning";
|
||||
return "inactive";
|
||||
}
|
||||
|
||||
function stateTone(state: string): string {
|
||||
if (state === "ok") return "success";
|
||||
if (state === "warning") return "warning";
|
||||
|
||||
@@ -11,6 +11,13 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-ops.surface.governance": "Governance inventory",
|
||||
"i18n:govoplan-ops.surface.deployment": "Deployment profiles",
|
||||
"i18n:govoplan-ops.surface.sizing": "Sizing assumptions",
|
||||
"i18n:govoplan-ops.infrastructure_capabilities": "Infrastructure capabilities",
|
||||
"i18n:govoplan-ops.pending_post_install_tasks": "{value0} pending post-install task(s)",
|
||||
"i18n:govoplan-ops.no_infrastructure_capabilities": "No deployment capability receipt is available.",
|
||||
"i18n:govoplan-ops.capability": "Capability",
|
||||
"i18n:govoplan-ops.source": "Source",
|
||||
"i18n:govoplan-ops.endpoint": "Endpoint",
|
||||
"i18n:govoplan-ops.consumers": "Consumers",
|
||||
"i18n:govoplan-ops.surface.run_probes": "Run operational probes",
|
||||
"i18n:govoplan-ops.surface.drain_node": "Drain runtime node",
|
||||
"i18n:govoplan-ops.reason.loading": "Operations status is loading.",
|
||||
@@ -80,6 +87,13 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-ops.surface.governance": "Governance-Inventar",
|
||||
"i18n:govoplan-ops.surface.deployment": "Bereitstellungsprofile",
|
||||
"i18n:govoplan-ops.surface.sizing": "Dimensionierungsannahmen",
|
||||
"i18n:govoplan-ops.infrastructure_capabilities": "Infrastruktur-Fähigkeiten",
|
||||
"i18n:govoplan-ops.pending_post_install_tasks": "{value0} ausstehende Nachinstallationsaufgabe(n)",
|
||||
"i18n:govoplan-ops.no_infrastructure_capabilities": "Es ist kein Bereitstellungsnachweis für Infrastruktur-Fähigkeiten verfügbar.",
|
||||
"i18n:govoplan-ops.capability": "Fähigkeit",
|
||||
"i18n:govoplan-ops.source": "Quelle",
|
||||
"i18n:govoplan-ops.endpoint": "Endpunkt",
|
||||
"i18n:govoplan-ops.consumers": "Verwendende Module",
|
||||
"i18n:govoplan-ops.surface.run_probes": "Betriebsprüfungen ausführen",
|
||||
"i18n:govoplan-ops.surface.drain_node": "Laufzeitknoten leeren",
|
||||
"i18n:govoplan-ops.reason.loading": "Der Betriebsstatus wird geladen.",
|
||||
|
||||
Reference in New Issue
Block a user