diff --git a/Makefile b/Makefile index 88b15d570..e60dbb189 100644 --- a/Makefile +++ b/Makefile @@ -251,7 +251,7 @@ image-mcp-server: ## Build mcp-server image docker build -t $(REGISTRY)/mcp-server:$(TAG) -f mcp-server/Dockerfile . # -------- codegen -------- -.PHONY: generate manifests +.PHONY: generate manifests module-schema generate: ## Run controller-gen deepcopy generators cd operator && go run sigs.k8s.io/controller-tools/cmd/controller-gen object paths=./api/... @@ -264,6 +264,10 @@ manifests: ## Regenerate CRDs + RBAC manifests (and sync chart CRD copies) output:rbac:artifacts:config=config/rbac cp operator/config/crd/gameplane.local_*.yaml charts/gameplane/crds/ +module-schema: ## Regenerate the editor JSON Schema for module template.yaml from the CRD + python3 hack/gen-module-schema.py + @echo "commit modules/.schema/gametemplate.schema.json in the gameplane-module repo + bump the submodule pointer" + # -------- local dev cluster (kind) -------- .PHONY: dev-up dev-down dev-load dev-push dev-install dev-up: ## Create/prepare cluster + install Gameplane (CLUSTER=kind|remote) diff --git a/docs/module-authoring.md b/docs/module-authoring.md index c6645771e..b51d2bbe1 100644 --- a/docs/module-authoring.md +++ b/docs/module-authoring.md @@ -28,6 +28,19 @@ modules// submodule mounts the `gameplane-module` repo root at `modules/`, so the paths below still resolve. +### Editor schemas + +Both YAML files carry a first-line `# yaml-language-server: $schema=…` modeline +pointing at JSON Schemas under `modules/.schema/`, so editors (VS Code's +`redhat.vscode-yaml`) validate and autocomplete them instead of guessing an +unrelated schema. `module.schema.json` is hand-maintained; `gametemplate.schema.json` +is **generated from the GameTemplate CRD** — after changing CRD spec fields, +regenerate and commit it (in the submodule): + +```sh +make module-schema # hack/gen-module-schema.py → modules/.schema/gametemplate.schema.json +``` + `template.yaml` is the same `GameTemplate` you would write today, with one difference: omit `metadata.name`. The name is set on install from `module.yaml#name`, so a single bundle can be installed under different diff --git a/hack/gen-module-schema.py b/hack/gen-module-schema.py new file mode 100644 index 000000000..e0f943903 --- /dev/null +++ b/hack/gen-module-schema.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Generate a JSON Schema for module template.yaml files from the GameTemplate CRD. + +template.yaml in a Gameplane module IS a GameTemplate custom resource, so its +schema is the CRD's openAPIV3Schema. Editors (redhat.vscode-yaml) otherwise +guess a schema from their schema store and mis-flag valid fields; a +`# yaml-language-server: $schema=` modeline in each template.yaml points at the +file this writes. + +Output goes into the gameplane-module submodule so it lives with the files it +validates (and resolves for a standalone checkout of that repo too). Re-run +after CRD changes: make module-schema + +The module.yaml schema is hand-maintained (small, stable manifest) at the same +location and is NOT generated here. +""" +import json +import sys +from pathlib import Path + +import yaml + +REPO = Path(__file__).resolve().parent.parent +CRD = REPO / "charts/gameplane/crds/gameplane.local_gametemplates.yaml" +OUT = REPO / "modules/.schema/gametemplate.schema.json" + + +def main() -> int: + if not CRD.exists(): + print(f"CRD not found: {CRD}", file=sys.stderr) + return 1 + crd = yaml.safe_load(CRD.read_text()) + versions = crd["spec"]["versions"] + # Single served version today; take the storage version to be safe. + ver = next((v for v in versions if v.get("storage")), versions[0]) + schema = ver["schema"]["openAPIV3Schema"] + + # JSON Schema envelope. The CRD's openAPIV3Schema already describes the whole + # document (apiVersion/kind/metadata/spec/status); we only add the draft + # marker + a title and pin apiVersion/kind so a wrong header is flagged. + schema["$schema"] = "http://json-schema.org/draft-07/schema#" + schema["title"] = "Gameplane GameTemplate (module template.yaml)" + schema.setdefault("properties", {}) + schema["properties"]["apiVersion"] = { + "const": f"{crd['spec']['group']}/{ver['name']}", + "description": "gameplane.local/v1alpha1", + } + schema["properties"]["kind"] = { + "const": crd["spec"]["names"]["kind"], + "description": "GameTemplate", + } + + OUT.parent.mkdir(parents=True, exist_ok=True) + OUT.write_text(json.dumps(schema, indent=2, sort_keys=False) + "\n") + print(f"wrote {OUT.relative_to(REPO)} ({OUT.stat().st_size} bytes)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())