feat: add policy deployment status tracking with cluster-level rule state monitoring and live firewall rule version comparison

Add policy_read_payload helper to build policy response with deployment status, implement nexafabric_rule_version to parse policy ID and version from rule comments, add policy_deployment_status to check active/stale/partial/unresolved states by comparing expected rules from preview against live firewall rules per cluster with version matching, extend PolicyRead schema with
This commit is contained in:
2026-07-09 19:52:10 +02:00
parent 6d5dc310df
commit 1802c2cbee
4 changed files with 216 additions and 3 deletions
+158 -2
View File
@@ -526,6 +526,159 @@ def flow_policy_decision(active_matches: list[dict[str, object]], policy_matches
return "observed"
def policy_read_payload(policy: Policy, deployment_status: dict[str, object] | None = None) -> dict[str, object]:
return {
"id": policy.id,
"project_id": policy.project_id,
"name": policy.name,
"version": policy.version,
"enabled": policy.enabled,
"enforcement_mode": policy.enforcement_mode,
"definition": policy.definition,
"last_compiled": policy.last_compiled,
"deployment_status": deployment_status,
}
def nexafabric_rule_version(rule: dict[str, object], policy_id: str) -> int | None:
comment = str(rule.get("comment") or "")
marker = f"NexaFabric policy={policy_id} version="
if marker not in comment:
return None
try:
return int(comment.split(marker, 1)[1].split(" ", 1)[0])
except (IndexError, ValueError):
return None
async def policy_deployment_status(db: Session, policy: Policy) -> dict[str, object]:
if not policy.enabled:
return {"state": "disabled", "label": "Disabled", "expected_rules": 0, "active_rules": 0, "stale_rules": 0, "clusters": []}
if policy.enforcement_mode == "audit":
return {"state": "audit", "label": "Audit mode", "expected_rules": 0, "active_rules": 0, "stale_rules": 0, "clusters": []}
clusters = db.scalars(select(Cluster).order_by(Cluster.name)).all()
if not clusters:
return {"state": "unknown", "label": "No cluster", "expected_rules": 0, "active_rules": 0, "stale_rules": 0, "clusters": []}
expected_rules = 0
active_rules = 0
stale_rules = 0
unresolved = 0
cluster_results: list[dict[str, object]] = []
for cluster in clusters:
try:
preview = resolve_firewall_preview(db, cluster, await FirewallOrchestrator().preview(cluster, policy))
except Exception as exc:
cluster_results.append({"cluster_id": cluster.id, "cluster_name": cluster.name, "state": "error", "error": str(exc)})
continue
writable_rules = [
rule for rule in preview.generated_rules if rule.get("provider_target") and rule.get("provider_rule") and not rule.get("audit_only")
]
expected_rules += len(writable_rules)
unresolved += len(preview.conflicts)
provider = get_provider(cluster.provider)
list_rules = getattr(provider, "list_firewall_rules", None)
if not list_rules:
cluster_results.append(
{
"cluster_id": cluster.id,
"cluster_name": cluster.name,
"state": "unknown",
"expected_rules": len(writable_rules),
"active_rules": 0,
"stale_rules": 0,
"reason": "Provider cannot list active firewall rules.",
}
)
continue
cluster_active = 0
cluster_stale = 0
seen_targets: dict[str, dict[str, object]] = {}
for rule in writable_rules:
target = rule.get("provider_target")
if isinstance(target, dict):
seen_targets[str(target)] = target
for target in seen_targets.values():
try:
rules = await list_rules(
ProviderConnection(
api_url=cluster.api_url,
token=cluster.token_ref or "",
verify_tls=cluster.verify_tls,
read_only=True,
),
target,
)
except Exception as exc:
cluster_results.append({"cluster_id": cluster.id, "cluster_name": cluster.name, "state": "error", "error": str(exc)})
continue
for active_rule in rules:
version = nexafabric_rule_version(active_rule, policy.id)
if version is None:
continue
if version == policy.version:
cluster_active += 1
else:
cluster_stale += 1
active_rules += cluster_active
stale_rules += cluster_stale
if preview.conflicts:
cluster_state = "unresolved"
elif cluster_active >= len(writable_rules) and writable_rules:
cluster_state = "active"
elif cluster_active:
cluster_state = "partial"
elif cluster_stale:
cluster_state = "stale"
else:
cluster_state = "not_applied"
cluster_results.append(
{
"cluster_id": cluster.id,
"cluster_name": cluster.name,
"state": cluster_state,
"expected_rules": len(writable_rules),
"active_rules": cluster_active,
"stale_rules": cluster_stale,
"conflicts": preview.conflicts,
}
)
if unresolved:
state = "unresolved"
label = "Needs attention"
elif expected_rules and active_rules >= expected_rules:
state = "active"
label = "Active"
elif active_rules:
state = "partial"
label = "Partially active"
elif stale_rules:
state = "stale"
label = "Outdated"
elif expected_rules:
state = "not_applied"
label = "Not applied"
else:
state = "unknown"
label = "No resolved rules"
return {
"state": state,
"label": label,
"expected_rules": expected_rules,
"active_rules": active_rules,
"stale_rules": stale_rules,
"clusters": cluster_results,
}
def dashboard_top_talkers(db: Session) -> list[dict[str, int | str]]:
totals: dict[str, int] = {}
workloads = db.scalars(select(Workload)).all()
@@ -1593,8 +1746,11 @@ def delete_security_rule(rule_id: str, user: CurrentUser, db: Session = Depends(
@api_router.get("/policies", response_model=list[PolicyRead])
def policies(_: CurrentUser, db: Session = Depends(get_db)) -> list[Policy]:
return db.scalars(select(Policy).order_by(Policy.name)).all()
async def policies(_: CurrentUser, db: Session = Depends(get_db)) -> list[dict[str, object]]:
return [
policy_read_payload(policy, await policy_deployment_status(db, policy))
for policy in db.scalars(select(Policy).order_by(Policy.name)).all()
]
@api_router.post("/policies", response_model=PolicyRead)
+1
View File
@@ -309,6 +309,7 @@ class PolicyRead(OrmModel):
enforcement_mode: str
definition: dict[str, Any]
last_compiled: dict[str, Any] | None
deployment_status: dict[str, Any] | None = None
class WorkloadInsight(BaseModel):
+1
View File
@@ -124,6 +124,7 @@ export type Policy = {
enforcement_mode: string;
definition: Record<string, unknown>;
last_compiled: Record<string, unknown> | null;
deployment_status: Record<string, unknown> | null;
};
export type Workload = {
+56 -1
View File
@@ -1,6 +1,6 @@
import { FormEvent, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Eye, GitBranch, Pencil, Play, Plus, Trash2 } from "lucide-react";
import { AlertTriangle, CheckCircle2, Clock3, Eye, GitBranch, Pencil, Play, Plus, Shield, Trash2 } from "lucide-react";
import { api, Policy, Project, ServiceCatalogItem } from "../api/client";
import { DataTable } from "../components/DataTable";
@@ -22,6 +22,60 @@ function policyService(policy: Policy) {
return `${String(value.protocol ?? "")}/${String(value.ports ?? "")}`;
}
function policyStatus(policy: Policy) {
const status = policy.deployment_status ?? {};
return {
state: String(status.state ?? "unknown"),
label: String(status.label ?? "Unknown"),
expectedRules: Number(status.expected_rules ?? 0),
activeRules: Number(status.active_rules ?? 0),
staleRules: Number(status.stale_rules ?? 0),
};
}
function statusClass(state: string) {
if (state === "active") {
return "border-accent/40 bg-accent/10 text-accent";
}
if (state === "audit") {
return "border-amber-400/40 bg-amber-400/10 text-amber-300";
}
if (["stale", "partial", "unresolved"].includes(state)) {
return "border-danger/40 bg-danger/10 text-danger";
}
return "border-border bg-canvas text-slate-500";
}
function StatusIcon({ state }: { state: string }) {
if (state === "active") {
return <CheckCircle2 size={15} />;
}
if (state === "audit") {
return <Shield size={15} />;
}
if (["stale", "partial", "unresolved"].includes(state)) {
return <AlertTriangle size={15} />;
}
return <Clock3 size={15} />;
}
function PolicyDeploymentStatus({ policy }: { policy: Policy }) {
const status = policyStatus(policy);
const detail =
status.state === "audit"
? "No live firewall write"
: `${status.activeRules}/${status.expectedRules} active${status.staleRules ? ` · ${status.staleRules} stale` : ""}`;
return (
<div className="flex flex-col gap-1">
<span className={`inline-flex w-fit items-center gap-1.5 rounded-md border px-2 py-1 text-xs ${statusClass(status.state)}`}>
<StatusIcon state={status.state} />
{status.label}
</span>
<span className="text-xs text-slate-500">{detail}</span>
</div>
);
}
const defaultPolicyForm = {
project_id: "",
name: "Web to DB",
@@ -200,6 +254,7 @@ export function Policies() {
{ key: "destination", label: "Destination", render: (row) => policyValue(row as unknown as Policy, "destination") },
{ key: "service", label: "Service", render: (row) => policyService(row as unknown as Policy) },
{ key: "enforcement_mode", label: "Mode" },
{ key: "deployment_status", label: "Status", render: (row) => <PolicyDeploymentStatus policy={row as unknown as Policy} /> },
{ key: "version", label: "Version" },
{
key: "actions",