fastapi-gsap/gsap_broker/templates/manifest.py
Tyler J King 77964e4042 feat(templates): add template system — manifest, policy, loader, registries
bastion.toml manifest parser with variable validation and dependency
declarations. Declarative compliance policy schema with per-platform
check implementations. Template loader with variable substitution
(Bastion-owned files only — never touches Ansible/Terraform).
PolicyRegistry and AccordRegistry with builtin fallbacks.

BOUNDARY: loader never touches automation framework files.

Signed-off-by: Tyler King <tking@guildhouse.dev>
2026-04-14 11:09:41 -04:00

98 lines
3 KiB
Python

# Copyright 2026 Guildhouse Dev
# SPDX-License-Identifier: Apache-2.0
"""Bastion template manifest schema.
A template is a Git repository with a bastion.toml at the root.
It contains policies, accords, harnesses, scripts, HBOM profiles,
and optionally framework-specific automation (Ansible, Terraform, etc).
The manifest declares:
- What the template provides (contents)
- What it depends on (dependencies)
- What the deployer must customize (variables)
- What Bastion version and connectors it requires (compatibility)
"""
from __future__ import annotations
import tomllib
from pathlib import Path
from typing import Any, Optional
from pydantic import BaseModel, Field
class TemplateMetadata(BaseModel):
"""Template identification and categorization."""
name: str
version: str
description: str = ""
authors: list[str] = []
license: str = "Apache-2.0"
repository: Optional[str] = None
vertical: Optional[str] = None
sub_vertical: Optional[str] = None
compliance_frameworks: list[str] = []
target_fleet_size: Optional[str] = None
requires_vdi: bool = False
requires_tpm: bool = False
class TemplateCompatibility(BaseModel):
"""What Bastion version and connectors this template needs."""
bastion_min: str = "0.5.0"
connectors_required: list[str] = []
connectors_optional: list[str] = []
class TemplateDependency(BaseModel):
"""A dependency on another template repository."""
git: str
tag: Optional[str] = None
branch: Optional[str] = None
class TemplateVariable(BaseModel):
"""A variable the deployer must or may customize."""
type: str = "string"
required: bool = False
default: Optional[str] = None
description: str = ""
sensitive: bool = False
class TemplateContents(BaseModel):
"""Directory mapping for template content."""
policies: str = "policies/"
accords: str = "accords/"
harnesses: str = "harnesses/"
scripts: str = "scripts/"
hbom_profiles: str = "hbom/"
dashboards: str = "dashboards/"
class TemplateManifest(BaseModel):
"""Root schema for bastion.toml."""
template: TemplateMetadata
compatibility: TemplateCompatibility = TemplateCompatibility()
dependencies: dict[str, TemplateDependency] = {}
variables: dict[str, TemplateVariable] = {}
contents: TemplateContents = TemplateContents()
@classmethod
def from_toml(cls, path: str | Path) -> TemplateManifest:
"""Parse a bastion.toml file."""
with open(path, "rb") as f:
data = tomllib.load(f)
return cls.model_validate(data)
def validate_variables(self, provided: dict[str, Any]) -> list[str]:
"""Check that all required variables are provided.
Returns list of error messages (empty = valid)."""
errors = []
for name, var in self.variables.items():
if var.required and name not in provided and var.default is None:
errors.append(f"Required variable '{name}' not provided")
return errors