feat(P2): contract + stack-IR kyverno-json policies + resolver wiring (REQ-295..299)
contract/ policies (4): require-id-pattern, require-env-in-enum, require-infrastructure-min-1, forbid-unknown-fields — declarative mirrors of contract.schema.json constraints. stack-ir/ policies (3): require-tagging-standard (nova:owner/contract/ environment/cost-center tags — ports nova_tagging.py), forbid-public-ingress (v1.0 demo rule, now declarative), require-encryption-by-default (v1.8 D-encryption-default — S3 + EBS encryption config). core/contract_resolver.py: pre-resolve contract-policy evaluation (REQ-296) + post-resolve stack-IR-policy evaluation (REQ-298). Additive — the resolver's return shape + exceptions unchanged; PCRs attach to stack_instance.policyResults. Policy evaluation never breaks the resolver (confidence signal decides gate). tests: test_stack_ir_policies.py + passing/failing fixtures. Skip-without-kj. 16 existing resolver tests unchanged. ---ci--- project: acdl phase: 2 milestone: v1.25 status: execute phase_role: execution requirements: covered: [REQ-295, REQ-296, REQ-297, REQ-298, REQ-299] partial: [] ---/ci---
This commit is contained in:
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": "bad",
|
||||
"title": "failing stack",
|
||||
"kind": "l1",
|
||||
"depth": 1,
|
||||
"environment": "dev"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"id": "bucket",
|
||||
"type": "aws:s3:bucket",
|
||||
"module": "s3@1.0.0",
|
||||
"inputs": {
|
||||
"bucket_name": "acdl-dev-bad-bucket",
|
||||
"region": "us-east-1",
|
||||
"tags": {
|
||||
"nova:owner": "team-a"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "service",
|
||||
"type": "aws:ecs:service",
|
||||
"module": "microservice@1.0.0",
|
||||
"inputs": {
|
||||
"image": "nginx:latest",
|
||||
"port": 80,
|
||||
"public_ingress": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": "msvc",
|
||||
"title": "microservice",
|
||||
"kind": "l1",
|
||||
"depth": 1,
|
||||
"environment": "dev"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"id": "bucket",
|
||||
"type": "aws:s3:bucket",
|
||||
"module": "s3@1.0.0",
|
||||
"inputs": {
|
||||
"bucket_name": "acdl-dev-msvc-bucket",
|
||||
"region": "us-east-1",
|
||||
"bucket_encryption": {"rule": {"apply_server_side_encryption_by_default": {"sse_algorithm": "AES256"}}},
|
||||
"tags": {
|
||||
"nova:owner": "team-a",
|
||||
"nova:contract": "msvc",
|
||||
"nova:environment": "dev",
|
||||
"nova:cost-center": "cc-1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "service",
|
||||
"type": "aws:ecs:service",
|
||||
"module": "microservice@1.0.0",
|
||||
"inputs": {
|
||||
"image": "nginx:latest",
|
||||
"port": 80,
|
||||
"tags": {
|
||||
"nova:owner": "team-a",
|
||||
"nova:contract": "msvc",
|
||||
"nova:environment": "dev",
|
||||
"nova:cost-center": "cc-1"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Tests for stack-IR kyverno-json policies (REQ-299, v1.25).
|
||||
|
||||
Tests the 3 policies in adapters/kyverno-json/policies/stack-ir/:
|
||||
require-tagging-standard, forbid-public-ingress, require-encryption-by-
|
||||
default. Uses the passing + failing fixtures. Skips when kj is absent.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
import importlib.util
|
||||
_ENGINE_PATH = Path(__file__).resolve().parent.parent / "adapters" / "kyverno-json" / "kyverno_json_engine.py"
|
||||
_spec = importlib.util.spec_from_file_location("kyverno_json_engine", _ENGINE_PATH)
|
||||
_mod = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_mod)
|
||||
KyvernoJsonEngine = _mod.KyvernoJsonEngine
|
||||
|
||||
POLICY_DIR = Path(__file__).resolve().parent.parent / "adapters" / "kyverno-json" / "policies" / "stack-ir"
|
||||
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "stack_ir"
|
||||
|
||||
|
||||
def _kj_installed() -> bool:
|
||||
return _mod._which_kj() is not None
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _require_kj():
|
||||
if not _kj_installed():
|
||||
pytest.skip("kj not installed (scripts/install-kyverno-json.sh)")
|
||||
|
||||
|
||||
def _load(name):
|
||||
with open(FIXTURES / name, "r", encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
class TestPassingFixture:
|
||||
def test_passing_fixture_all_pass(self):
|
||||
eng = KyvernoJsonEngine()
|
||||
out = eng.evaluate(_load("passing.json"), POLICY_DIR, "cid-pass")
|
||||
assert isinstance(out, list)
|
||||
assert len(out) >= 1
|
||||
# No fail results on the passing fixture.
|
||||
fails = [p for p in out if p["result"] == "fail"]
|
||||
assert fails == [], f"expected no fails on passing fixture, got: {fails}"
|
||||
|
||||
|
||||
class TestFailingFixture:
|
||||
def test_failing_fixture_has_fails(self):
|
||||
eng = KyvernoJsonEngine()
|
||||
out = eng.evaluate(_load("failing.json"), POLICY_DIR, "cid-fail")
|
||||
fails = [p for p in out if p["result"] == "fail"]
|
||||
assert len(fails) >= 1, "expected at least one fail on the failing fixture"
|
||||
|
||||
|
||||
class TestPolicyFilesExist:
|
||||
def test_three_policies_present(self):
|
||||
files = sorted(os.listdir(POLICY_DIR))
|
||||
assert "require-tagging-standard.json" in files
|
||||
assert "forbid-public-ingress.json" in files
|
||||
assert "require-encryption-by-default.json" in files
|
||||
|
||||
|
||||
class TestPolicyValidity:
|
||||
def test_policies_are_valid_json(self):
|
||||
for f in os.listdir(POLICY_DIR):
|
||||
if f.endswith(".json"):
|
||||
with open(POLICY_DIR / f, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
assert data["apiVersion"] == "json.kyverno.io/v1alpha1"
|
||||
assert data["kind"] == "ValidatingPolicy"
|
||||
assert "nova.cloudinit.dev/severity" in data["metadata"]["annotations"]
|
||||
|
||||
def test_policy_names_match_filenames(self):
|
||||
for f in os.listdir(POLICY_DIR):
|
||||
if f.endswith(".json"):
|
||||
with open(POLICY_DIR / f, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
expected = f.rsplit(".", 1)[0]
|
||||
assert data["metadata"]["name"] == expected
|
||||
Reference in New Issue
Block a user