feat: volume-based catalog refresh with hot-reload
- modules.py: check /data/ volume first, then baked-in /bicep_modules_catalog.json - server.py: add POST /reload endpoint — reloads catalogs without restart - ilsp.nomad: add 'ilsp-data' host volume mounted at /data - Makefile: add push-catalogs, health-prod, run-with-data targets; DEVOPS_MCP_REPO var - scripts/push_catalogs.sh: SCP both catalogs to autobox + call /reload Workflow: sync scripts on Mac → make push-catalogs → completions live in <5s
This commit is contained in:
27
Makefile
27
Makefile
@@ -6,8 +6,8 @@ PYTHON_PORT := 2087
|
|||||||
BICEP_PORT := 2088
|
BICEP_PORT := 2088
|
||||||
HEALTH_PORT := 2089
|
HEALTH_PORT := 2089
|
||||||
|
|
||||||
# External services (used at container startup for catalog fetching)
|
# DevOpsMCP repo — source of fresh catalog JSON files
|
||||||
DEVOPS_MCP_URL ?= https://devops-mcp.i80.dk
|
DEVOPS_MCP_REPO ?= $(HOME)/Projects/DevOpsMCP
|
||||||
|
|
||||||
.PHONY: build run stop restart logs shell test smoke clean help
|
.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 $(PYTHON_PORT):$(PYTHON_PORT) \
|
||||||
-p $(BICEP_PORT):$(BICEP_PORT) \
|
-p $(BICEP_PORT):$(BICEP_PORT) \
|
||||||
-p $(HEALTH_PORT):$(HEALTH_PORT) \
|
-p $(HEALTH_PORT):$(HEALTH_PORT) \
|
||||||
-e DEVOPS_MCP_URL=$(DEVOPS_MCP_URL) \
|
|
||||||
-e PYTHONUNBUFFERED=1 \
|
-e PYTHONUNBUFFERED=1 \
|
||||||
$(IMAGE):$(TAG)
|
$(IMAGE):$(TAG)
|
||||||
@sleep 5
|
@sleep 5
|
||||||
@$(MAKE) health
|
@$(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)
|
stop: ## Stop and remove container (if running)
|
||||||
-docker stop $(CONTAINER) 2>/dev/null
|
-docker stop $(CONTAINER) 2>/dev/null
|
||||||
-docker rm $(CONTAINER) 2>/dev/null
|
-docker rm $(CONTAINER) 2>/dev/null
|
||||||
@@ -66,10 +78,17 @@ shell: ## Open bash inside running container
|
|||||||
|
|
||||||
## ── Health & Testing ─────────────────────────────────────────────────────────
|
## ── Health & Testing ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
health: ## Check health endpoint
|
health: ## Check health endpoint (local container)
|
||||||
@curl -sf http://localhost:$(HEALTH_PORT)/health | python3 -m json.tool \
|
@curl -sf http://localhost:$(HEALTH_PORT)/health | python3 -m json.tool \
|
||||||
|| echo " ✗ Health endpoint not reachable — is the container running? (make run)"
|
|| 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
|
smoke: ## Run end-to-end smoke test against local container
|
||||||
bash scripts/smoke_test.sh localhost
|
bash scripts/smoke_test.sh localhost
|
||||||
|
|
||||||
|
|||||||
12
ilsp.nomad
12
ilsp.nomad
@@ -62,6 +62,12 @@ job "ilsp" {
|
|||||||
unlimited = false
|
unlimited = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
volume "ilsp-data" {
|
||||||
|
type = "host"
|
||||||
|
source = "ilsp-data"
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
|
|
||||||
# Health check only — Traefik not used for LSP TCP traffic
|
# Health check only — Traefik not used for LSP TCP traffic
|
||||||
service {
|
service {
|
||||||
provider = "consul"
|
provider = "consul"
|
||||||
@@ -127,6 +133,12 @@ EOH
|
|||||||
memory = 1536 # MB — dotnet needs headroom
|
memory = 1536 # MB — dotnet needs headroom
|
||||||
memory_max = 2560 # MB burst
|
memory_max = 2560 # MB burst
|
||||||
}
|
}
|
||||||
|
|
||||||
|
volume_mount {
|
||||||
|
volume = "ilsp-data"
|
||||||
|
destination = "/data"
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
# Catalog is baked into the image root at /bicep_modules_catalog.json
|
# Catalog is baked into the image root at /bicep_modules_catalog.json
|
||||||
_CATALOG_PATHS = [
|
_CATALOG_PATHS = [
|
||||||
pathlib.Path("/bicep_modules_catalog.json"),
|
pathlib.Path("/data/bicep_modules_catalog.json"), # volume-mount (freshest)
|
||||||
pathlib.Path(__file__).parent.parent.parent / "bicep_modules_catalog.json",
|
pathlib.Path("/bicep_modules_catalog.json"), # baked into image (fallback)
|
||||||
|
pathlib.Path(__file__).parent.parent.parent / "bicep_modules_catalog.json", # dev
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,20 @@ async def _health_app() -> web.Application:
|
|||||||
"bicep_modules": len(BicepModuleCatalog._modules),
|
"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_get("/health", health)
|
||||||
|
app.router.add_post("/reload", reload)
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
61
scripts/push_catalogs.sh
Executable file
61
scripts/push_catalogs.sh
Executable file
@@ -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."
|
||||||
Reference in New Issue
Block a user