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:
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user