Skip to content

Commit e7410fe

Browse files
ret2libcclaude
andcommitted
Validate rendered cloud-init templates against official schema
Add jsonschema dev dependency and two new tests that render the Jinja2 cloud-init template (with and without Tailscale) then validate the output against canonical/cloud-init's JSON schema. Tests skip gracefully when the network is unavailable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a7cf569 commit e7410fe

3 files changed

Lines changed: 215 additions & 0 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ packages = ["dropkit"]
6161

6262
[dependency-groups]
6363
dev = [
64+
"jsonschema>=4.26.0",
6465
"pytest>=8.4.2",
6566
"pytest-cov",
6667
"ruff>=0.8.0",

tests/test_cloudinit.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
11
"""Tests for cloud-init template parsing and rendering."""
22

3+
import json
4+
import urllib.request
5+
6+
import pytest
7+
import yaml
38
from jinja2 import Template, TemplateSyntaxError
9+
from jsonschema import Draft4Validator
410

511
from dropkit.config import Config
612

13+
CLOUD_CONFIG_SCHEMA_URL = (
14+
"https://raw.githubusercontent.com/canonical/cloud-init/main/"
15+
"cloudinit/config/schemas/schema-cloud-config-v1.json"
16+
)
17+
18+
19+
@pytest.fixture(scope="module")
20+
def cloud_config_schema():
21+
"""Fetch the official cloud-init JSON schema (skip if offline)."""
22+
try:
23+
with urllib.request.urlopen(CLOUD_CONFIG_SCHEMA_URL, timeout=10) as resp: # noqa: S310
24+
return json.loads(resp.read())
25+
except (urllib.error.URLError, TimeoutError):
26+
pytest.skip("Could not fetch cloud-init schema (offline?)")
27+
728

829
def _load_default_template() -> str:
930
"""Load the default cloud-init template content."""
@@ -74,3 +95,36 @@ def test_docker_install_uses_distro_detection():
7495

7596
# Architecture detected dynamically, not hardcoded amd64
7697
assert "dpkg --print-architecture" in rendered
98+
99+
100+
def _render_template(tailscale_enabled: bool = True) -> str:
101+
"""Render the default template with sample variables."""
102+
content = _load_default_template()
103+
template = Template(content)
104+
return template.render(
105+
username="testuser",
106+
full_name="Test User",
107+
email="test@example.com",
108+
ssh_keys=["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5 test@host"],
109+
tailscale_enabled=tailscale_enabled,
110+
)
111+
112+
113+
def test_rendered_template_valid_cloud_config_schema(cloud_config_schema):
114+
"""Verify the rendered template passes cloud-init schema validation."""
115+
rendered = _render_template(tailscale_enabled=True)
116+
doc = yaml.safe_load(rendered)
117+
validator = Draft4Validator(cloud_config_schema)
118+
errors = list(validator.iter_errors(doc))
119+
messages = [f" - {e.message}" for e in errors]
120+
assert not errors, "Cloud-init schema errors:\n" + "\n".join(messages)
121+
122+
123+
def test_rendered_template_no_tailscale_valid_schema(cloud_config_schema):
124+
"""Verify the rendered template without Tailscale also passes schema validation."""
125+
rendered = _render_template(tailscale_enabled=False)
126+
doc = yaml.safe_load(rendered)
127+
validator = Draft4Validator(cloud_config_schema)
128+
errors = list(validator.iter_errors(doc))
129+
messages = [f" - {e.message}" for e in errors]
130+
assert not errors, "Cloud-init schema errors:\n" + "\n".join(messages)

0 commit comments

Comments
 (0)