diff --git a/.github/workflows/helm-release.yml b/.github/workflows/helm-release.yml new file mode 100644 index 0000000..0636dd7 --- /dev/null +++ b/.github/workflows/helm-release.yml @@ -0,0 +1,29 @@ +name: Release Charts + +on: + push: + tags: + - v* + +jobs: + release: + # depending on default permission settings for your org (contents being read-only or read-write for workloads), you will have to add permissions + # see: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Run chart-releaser + uses: helm/chart-releaser-action@v1.7.0 + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/helm/nextcloud-mcp-server/README.md b/helm/nextcloud-mcp-server/README.md index 64df8b5..addc52d 100644 --- a/helm/nextcloud-mcp-server/README.md +++ b/helm/nextcloud-mcp-server/README.md @@ -157,6 +157,21 @@ ingress: | `autoscaling.maxReplicas` | Maximum replicas | `10` | | `autoscaling.targetCPUUtilizationPercentage` | Target CPU % | `80` | +#### Health Probes + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `livenessProbe.httpGet.path` | Liveness probe endpoint | `/health/live` | +| `livenessProbe.initialDelaySeconds` | Initial delay for liveness | `30` | +| `livenessProbe.periodSeconds` | Check interval for liveness | `10` | +| `readinessProbe.httpGet.path` | Readiness probe endpoint | `/health/ready` | +| `readinessProbe.initialDelaySeconds` | Initial delay for readiness | `10` | +| `readinessProbe.periodSeconds` | Check interval for readiness | `5` | + +The application exposes HTTP health check endpoints: +- `/health/live` - Liveness probe (checks if application is running) +- `/health/ready` - Readiness probe (checks if application is ready to serve traffic) + #### Document Processing (Optional) | Parameter | Description | Default | @@ -382,14 +397,41 @@ kubectl get pods -l app.kubernetes.io/name=nextcloud-mcp-server kubectl logs -l app.kubernetes.io/name=nextcloud-mcp-server --tail=100 -f ``` -### Test connectivity to Nextcloud +### Check health endpoints + +The application exposes health check endpoints for monitoring: ```bash # Port forward to the service kubectl port-forward svc/nextcloud-mcp 8000:8000 -# In another terminal, test the connection -curl http://localhost:8000/ +# Check liveness (if app is running) +curl http://localhost:8000/health/live + +# Check readiness (if app is ready to serve traffic) +curl http://localhost:8000/health/ready +``` + +**Example responses:** + +Liveness (always returns 200 if running): +```json +{ + "status": "alive", + "mode": "basic" +} +``` + +Readiness (returns 200 if ready, 503 if not ready): +```json +{ + "status": "ready", + "checks": { + "nextcloud_configured": "ok", + "auth_mode": "basic", + "auth_configured": "ok" + } +} ``` ### Common Issues diff --git a/helm/nextcloud-mcp-server/values.yaml b/helm/nextcloud-mcp-server/values.yaml index 99f3f8c..d842735 100644 --- a/helm/nextcloud-mcp-server/values.yaml +++ b/helm/nextcloud-mcp-server/values.yaml @@ -202,18 +202,24 @@ resources: memory: 128Mi # Liveness probe configuration +# Checks if the application process is running livenessProbe: - tcpSocket: + httpGet: + path: /health/live port: http + scheme: HTTP initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 # Readiness probe configuration +# Checks if the application is ready to serve traffic readinessProbe: - tcpSocket: + httpGet: + path: /health/ready port: http + scheme: HTTP initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 3 diff --git a/nextcloud_mcp_server/app.py b/nextcloud_mcp_server/app.py index 4a19bb8..c0f61c4 100644 --- a/nextcloud_mcp_server/app.py +++ b/nextcloud_mcp_server/app.py @@ -648,8 +648,71 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None): await stack.enter_async_context(mcp.session_manager.run()) yield + # Health check endpoints for Kubernetes probes + def health_live(request): + """Liveness probe endpoint. + + Returns 200 OK if the application process is running. + This is a simple check that doesn't verify external dependencies. + """ + return JSONResponse( + { + "status": "alive", + "mode": "oauth" if oauth_enabled else "basic", + } + ) + + async def health_ready(request): + """Readiness probe endpoint. + + Returns 200 OK if the application is ready to serve traffic. + Checks that required configuration is present. + """ + checks = {} + is_ready = True + + # Check Nextcloud host configuration + nextcloud_host = os.getenv("NEXTCLOUD_HOST") + if nextcloud_host: + checks["nextcloud_configured"] = "ok" + else: + checks["nextcloud_configured"] = "error: NEXTCLOUD_HOST not set" + is_ready = False + + # Check authentication configuration + if oauth_enabled: + # OAuth mode - just verify we got this far (token_verifier initialized in lifespan) + checks["auth_mode"] = "oauth" + checks["auth_configured"] = "ok" + else: + # BasicAuth mode - verify credentials are set + username = os.getenv("NEXTCLOUD_USERNAME") + password = os.getenv("NEXTCLOUD_PASSWORD") + if username and password: + checks["auth_mode"] = "basic" + checks["auth_configured"] = "ok" + else: + checks["auth_mode"] = "basic" + checks["auth_configured"] = "error: credentials not set" + is_ready = False + + status_code = 200 if is_ready else 503 + return JSONResponse( + { + "status": "ready" if is_ready else "not_ready", + "checks": checks, + }, + status_code=status_code, + ) + # Add Protected Resource Metadata (PRM) endpoint for OAuth mode routes = [] + + # Add health check routes (available in both OAuth and BasicAuth modes) + routes.append(Route("/health/live", health_live, methods=["GET"])) + routes.append(Route("/health/ready", health_ready, methods=["GET"])) + logger.info("Health check endpoints enabled: /health/live, /health/ready") + if oauth_enabled: def oauth_protected_resource_metadata(request):