Add Policy DSL Validator, Schema Exporter, and Simulation Smoke tools
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implemented PolicyDslValidator with command-line options for strict mode and JSON output. - Created PolicySchemaExporter to generate JSON schemas for policy-related models. - Developed PolicySimulationSmoke tool to validate policy simulations against expected outcomes. - Added project files and necessary dependencies for each tool. - Ensured proper error handling and usage instructions across tools.
This commit is contained in:
130
deploy/tools/check-channel-alignment.py
Normal file
130
deploy/tools/check-channel-alignment.py
Normal file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Ensure deployment bundles reference the images defined in a release manifest.
|
||||
|
||||
Usage:
|
||||
./deploy/tools/check-channel-alignment.py \
|
||||
--release deploy/releases/2025.10-edge.yaml \
|
||||
--target deploy/helm/stellaops/values-dev.yaml \
|
||||
--target deploy/compose/docker-compose.dev.yaml
|
||||
|
||||
For every target file, the script scans `image:` declarations and verifies that
|
||||
any image belonging to a repository listed in the release manifest matches the
|
||||
exact digest or tag recorded there. Images outside of the manifest (for example,
|
||||
supporting services such as `nats`) are ignored.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
from typing import Dict, Iterable, List, Optional, Set
|
||||
|
||||
IMAGE_LINE = re.compile(r"^\s*image:\s*['\"]?(?P<image>\S+)['\"]?\s*$")
|
||||
|
||||
|
||||
def extract_images(path: pathlib.Path) -> List[str]:
|
||||
images: List[str] = []
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
match = IMAGE_LINE.match(line)
|
||||
if match:
|
||||
images.append(match.group("image"))
|
||||
return images
|
||||
|
||||
|
||||
def image_repo(image: str) -> str:
|
||||
if "@" in image:
|
||||
return image.split("@", 1)[0]
|
||||
# Split on the last colon to preserve registries with ports (e.g. localhost:5000)
|
||||
if ":" in image:
|
||||
prefix, tag = image.rsplit(":", 1)
|
||||
if "/" in tag:
|
||||
# handle digestive colon inside path (unlikely)
|
||||
return image
|
||||
return prefix
|
||||
return image
|
||||
|
||||
|
||||
def load_release_map(release_path: pathlib.Path) -> Dict[str, str]:
|
||||
release_map: Dict[str, str] = {}
|
||||
for image in extract_images(release_path):
|
||||
repo = image_repo(image)
|
||||
release_map[repo] = image
|
||||
return release_map
|
||||
|
||||
|
||||
def check_target(
|
||||
target_path: pathlib.Path,
|
||||
release_map: Dict[str, str],
|
||||
ignore_repos: Set[str],
|
||||
) -> List[str]:
|
||||
errors: List[str] = []
|
||||
for image in extract_images(target_path):
|
||||
repo = image_repo(image)
|
||||
if repo in ignore_repos:
|
||||
continue
|
||||
if repo not in release_map:
|
||||
continue
|
||||
expected = release_map[repo]
|
||||
if image != expected:
|
||||
errors.append(
|
||||
f"{target_path}: {image} does not match release value {expected}"
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
def parse_args(argv: Optional[Iterable[str]] = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--release",
|
||||
required=True,
|
||||
type=pathlib.Path,
|
||||
help="Path to the release manifest (YAML)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target",
|
||||
action="append",
|
||||
required=True,
|
||||
type=pathlib.Path,
|
||||
help="Deployment profile to validate against the release manifest",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ignore-repo",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Repository prefix to ignore (may be repeated)",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: Optional[Iterable[str]] = None) -> int:
|
||||
args = parse_args(argv)
|
||||
|
||||
release_map = load_release_map(args.release)
|
||||
ignore_repos = {repo.rstrip("/") for repo in args.ignore_repo}
|
||||
|
||||
if not release_map:
|
||||
print(f"error: no images found in release manifest {args.release}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
total_errors: List[str] = []
|
||||
for target in args.target:
|
||||
if not target.exists():
|
||||
total_errors.append(f"{target}: file not found")
|
||||
continue
|
||||
total_errors.extend(check_target(target, release_map, ignore_repos))
|
||||
|
||||
if total_errors:
|
||||
print("✖ channel alignment check failed:", file=sys.stderr)
|
||||
for err in total_errors:
|
||||
print(f" - {err}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print("✓ deployment profiles reference release images for the inspected repositories.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,53 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
COMPOSE_DIR="$ROOT_DIR/compose"
|
||||
HELM_DIR="$ROOT_DIR/helm/stellaops"
|
||||
|
||||
compose_profiles=(
|
||||
"docker-compose.dev.yaml:env/dev.env.example"
|
||||
"docker-compose.stage.yaml:env/stage.env.example"
|
||||
"docker-compose.airgap.yaml:env/airgap.env.example"
|
||||
"docker-compose.mirror.yaml:env/mirror.env.example"
|
||||
)
|
||||
|
||||
docker_ready=false
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
docker_ready=true
|
||||
else
|
||||
echo "⚠️ docker CLI present but Compose plugin unavailable; skipping compose validation" >&2
|
||||
fi
|
||||
else
|
||||
echo "⚠️ docker CLI not found; skipping compose validation" >&2
|
||||
fi
|
||||
|
||||
if [[ "$docker_ready" == "true" ]]; then
|
||||
for entry in "${compose_profiles[@]}"; do
|
||||
IFS=":" read -r compose_file env_file <<<"$entry"
|
||||
printf '→ validating %s with %s\n' "$compose_file" "$env_file"
|
||||
docker compose \
|
||||
--env-file "$COMPOSE_DIR/$env_file" \
|
||||
-f "$COMPOSE_DIR/$compose_file" config >/dev/null
|
||||
done
|
||||
fi
|
||||
|
||||
helm_values=(
|
||||
"$HELM_DIR/values-dev.yaml"
|
||||
"$HELM_DIR/values-stage.yaml"
|
||||
"$HELM_DIR/values-airgap.yaml"
|
||||
"$HELM_DIR/values-mirror.yaml"
|
||||
)
|
||||
|
||||
if command -v helm >/dev/null 2>&1; then
|
||||
for values in "${helm_values[@]}"; do
|
||||
printf '→ linting Helm chart with %s\n' "$(basename "$values")"
|
||||
helm lint "$HELM_DIR" -f "$values"
|
||||
helm template test-release "$HELM_DIR" -f "$values" >/dev/null
|
||||
done
|
||||
else
|
||||
echo "⚠️ helm CLI not found; skipping Helm lint/template" >&2
|
||||
fi
|
||||
|
||||
printf 'Profiles validated (where tooling was available).\n'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
COMPOSE_DIR="$ROOT_DIR/compose"
|
||||
HELM_DIR="$ROOT_DIR/helm/stellaops"
|
||||
|
||||
compose_profiles=(
|
||||
"docker-compose.dev.yaml:env/dev.env.example"
|
||||
"docker-compose.stage.yaml:env/stage.env.example"
|
||||
"docker-compose.prod.yaml:env/prod.env.example"
|
||||
"docker-compose.airgap.yaml:env/airgap.env.example"
|
||||
"docker-compose.mirror.yaml:env/mirror.env.example"
|
||||
"docker-compose.telemetry.yaml:"
|
||||
"docker-compose.telemetry-storage.yaml:"
|
||||
)
|
||||
|
||||
docker_ready=false
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
docker_ready=true
|
||||
else
|
||||
echo "⚠️ docker CLI present but Compose plugin unavailable; skipping compose validation" >&2
|
||||
fi
|
||||
else
|
||||
echo "⚠️ docker CLI not found; skipping compose validation" >&2
|
||||
fi
|
||||
|
||||
if [[ "$docker_ready" == "true" ]]; then
|
||||
for entry in "${compose_profiles[@]}"; do
|
||||
IFS=":" read -r compose_file env_file <<<"$entry"
|
||||
printf '→ validating %s with %s\n' "$compose_file" "$env_file"
|
||||
if [[ -n "$env_file" ]]; then
|
||||
docker compose \
|
||||
--env-file "$COMPOSE_DIR/$env_file" \
|
||||
-f "$COMPOSE_DIR/$compose_file" config >/dev/null
|
||||
else
|
||||
docker compose -f "$COMPOSE_DIR/$compose_file" config >/dev/null
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
helm_values=(
|
||||
"$HELM_DIR/values-dev.yaml"
|
||||
"$HELM_DIR/values-stage.yaml"
|
||||
"$HELM_DIR/values-prod.yaml"
|
||||
"$HELM_DIR/values-airgap.yaml"
|
||||
"$HELM_DIR/values-mirror.yaml"
|
||||
)
|
||||
|
||||
if command -v helm >/dev/null 2>&1; then
|
||||
for values in "${helm_values[@]}"; do
|
||||
printf '→ linting Helm chart with %s\n' "$(basename "$values")"
|
||||
helm lint "$HELM_DIR" -f "$values"
|
||||
helm template test-release "$HELM_DIR" -f "$values" >/dev/null
|
||||
done
|
||||
else
|
||||
echo "⚠️ helm CLI not found; skipping Helm lint/template" >&2
|
||||
fi
|
||||
|
||||
printf 'Profiles validated (where tooling was available).\n'
|
||||
|
||||
Reference in New Issue
Block a user