diff --git a/Makefile b/Makefile index 9015a3d..3e3e671 100644 --- a/Makefile +++ b/Makefile @@ -6,8 +6,8 @@ PYTHON_PORT := 2087 BICEP_PORT := 2088 HEALTH_PORT := 2089 -# External services (used at container startup for catalog fetching) -DEVOPS_MCP_URL ?= https://devops-mcp.i80.dk +# DevOpsMCP repo — source of fresh catalog JSON files +DEVOPS_MCP_REPO ?= $(HOME)/Projects/DevOpsMCP .PHONY: build run stop restart logs shell test smoke clean help @@ -43,12 +43,24 @@ run-quick: ## Start container without rebuilding image -p $(PYTHON_PORT):$(PYTHON_PORT) \ -p $(BICEP_PORT):$(BICEP_PORT) \ -p $(HEALTH_PORT):$(HEALTH_PORT) \ - -e DEVOPS_MCP_URL=$(DEVOPS_MCP_URL) \ -e PYTHONUNBUFFERED=1 \ $(IMAGE):$(TAG) @sleep 5 @$(MAKE) health +run-with-data: ## Start container with local data dir mounted (test volume path) + $(MAKE) stop + docker run -d \ + --name $(CONTAINER) \ + -p $(PYTHON_PORT):$(PYTHON_PORT) \ + -p $(BICEP_PORT):$(BICEP_PORT) \ + -p $(HEALTH_PORT):$(HEALTH_PORT) \ + -e PYTHONUNBUFFERED=1 \ + -v "$(DEVOPS_MCP_REPO):/data:ro" \ + $(IMAGE):$(TAG) + @sleep 5 + @$(MAKE) health + stop: ## Stop and remove container (if running) -docker stop $(CONTAINER) 2>/dev/null -docker rm $(CONTAINER) 2>/dev/null @@ -66,10 +78,17 @@ shell: ## Open bash inside running container ## ── Health & Testing ───────────────────────────────────────────────────────── -health: ## Check health endpoint +health: ## Check health endpoint (local container) @curl -sf http://localhost:$(HEALTH_PORT)/health | python3 -m json.tool \ || echo " ✗ Health endpoint not reachable — is the container running? (make run)" +health-prod: ## Check health endpoint (production lsp.i80.dk) + @curl -sf https://lsp.i80.dk/health | python3 -m json.tool \ + || echo " ✗ lsp.i80.dk not reachable" + +push-catalogs: ## Push fresh Bicep catalogs to autobox + hot-reload iLSP + bash scripts/push_catalogs.sh + smoke: ## Run end-to-end smoke test against local container bash scripts/smoke_test.sh localhost diff --git a/ilsp.nomad b/ilsp.nomad index 552021f..70d87f3 100644 --- a/ilsp.nomad +++ b/ilsp.nomad @@ -62,6 +62,12 @@ job "ilsp" { unlimited = false } + volume "ilsp-data" { + type = "host" + source = "ilsp-data" + read_only = false + } + # Health check only — Traefik not used for LSP TCP traffic service { provider = "consul" @@ -127,6 +133,12 @@ EOH memory = 1536 # MB — dotnet needs headroom memory_max = 2560 # MB burst } + + volume_mount { + volume = "ilsp-data" + destination = "/data" + read_only = false + } } } } diff --git a/ilsp/bicep_lsp/modules.py b/ilsp/bicep_lsp/modules.py index 3b07cfb..605ba2c 100644 --- a/ilsp/bicep_lsp/modules.py +++ b/ilsp/bicep_lsp/modules.py @@ -17,8 +17,9 @@ logger = logging.getLogger(__name__) # Catalog is baked into the image root at /bicep_modules_catalog.json _CATALOG_PATHS = [ - pathlib.Path("/bicep_modules_catalog.json"), - pathlib.Path(__file__).parent.parent.parent / "bicep_modules_catalog.json", + pathlib.Path("/data/bicep_modules_catalog.json"), # volume-mount (freshest) + pathlib.Path("/bicep_modules_catalog.json"), # baked into image (fallback) + pathlib.Path(__file__).parent.parent.parent / "bicep_modules_catalog.json", # dev ] diff --git a/ilsp/server.py b/ilsp/server.py index bd76e07..d4091f5 100644 --- a/ilsp/server.py +++ b/ilsp/server.py @@ -49,7 +49,20 @@ async def _health_app() -> web.Application: "bicep_modules": len(BicepModuleCatalog._modules), }) + async def reload(_: web.Request) -> web.Response: + """Hot-reload catalogs from /data/ volume without restarting.""" + before = len(BicepModuleCatalog._modules) + BicepModuleCatalog.load() + after = len(BicepModuleCatalog._modules) + logger.info("Catalog reloaded: %d → %d modules", before, after) + return web.json_response({ + "status": "reloaded", + "bicep_modules_before": before, + "bicep_modules_after": after, + }) + app.router.add_get("/health", health) + app.router.add_post("/reload", reload) return app diff --git a/scripts/push_catalogs.sh b/scripts/push_catalogs.sh new file mode 100755 index 0000000..53e1102 --- /dev/null +++ b/scripts/push_catalogs.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# push_catalogs.sh — Push fresh Bicep + IAC source catalogs to autobox iLSP volume +# +# Usage: +# ./scripts/push_catalogs.sh # push both catalogs +# ./scripts/push_catalogs.sh --no-reload # push but don't call /reload +# +# Catalogs are read from DevOpsMCP repo (generated by sync_bicep_modules.py / sync_iac_module_sources.py) +# and written to /opt/nomad/volumes/ilsp-data/ on autobox — the host volume iLSP mounts at /data. + +set -euo pipefail + +DEVOPS_MCP_REPO="${DEVOPS_MCP_REPO:-$HOME/Projects/DevOpsMCP}" +AUTOBOX="autobox.i80.dk" +REMOTE_DIR="/opt/nomad/volumes/ilsp-data" +HEALTH_URL="https://lsp.i80.dk/health" +RELOAD_URL="https://lsp.i80.dk/reload" +NO_RELOAD=false + +for arg in "$@"; do + [[ "$arg" == "--no-reload" ]] && NO_RELOAD=true +done + +# Catalog files to push +BICEP_CATALOG="$DEVOPS_MCP_REPO/bicep_modules_catalog.json" +IAC_CATALOG="$DEVOPS_MCP_REPO/iac_source_catalog.json" + +echo "── iLSP catalog push ──────────────────────────────" + +for f in "$BICEP_CATALOG" "$IAC_CATALOG"; do + if [[ ! -f "$f" ]]; then + echo " ✗ Not found: $f" + echo " Run: python3 $DEVOPS_MCP_REPO/scripts/sync_bicep_modules.py" + exit 1 + fi + echo " ✓ $(basename "$f") ($(du -sh "$f" | cut -f1))" +done + +echo "" +echo " → Copying to $AUTOBOX:$REMOTE_DIR/ …" +ssh "$AUTOBOX" "mkdir -p $REMOTE_DIR" +scp "$BICEP_CATALOG" "$IAC_CATALOG" "$AUTOBOX:$REMOTE_DIR/" +echo " ✓ Upload done" + +if [[ "$NO_RELOAD" == "true" ]]; then + echo " (skipping /reload — restart iLSP manually to apply)" + exit 0 +fi + +echo "" +echo " → Calling $RELOAD_URL …" +if response=$(curl -sf -X POST "$RELOAD_URL" 2>/dev/null); then + echo " ✓ $response" +else + echo " ✗ /reload failed (is lsp.i80.dk reachable? try: make health-prod)" + echo " Catalogs are on disk — they'll be used on next restart." + exit 1 +fi + +echo "" +echo " Done. Bicep completions updated."