Skip to content

Commit 915f894

Browse files
t0mdavid-mclaudeCopilot
authored
Fix volume bind (#20)
* Add Matomo analytics integration with GDPR consent support (#341) * Add Matomo Tag Manager as third analytics tracking mode Adds Matomo Tag Manager support alongside existing Google Analytics and Piwik Pro integrations. Includes settings.json configuration (url + tag), build-time script injection via hook-analytics.py, Klaro GDPR consent banner integration, and runtime consent granting via MTM data layer API. https://claude.ai/code/session_0165AXHkmRZ6bx23n7Tbyz8h * Fix Matomo Tag Manager snippet to match official docs - Accept full container JS URL instead of separate url + tag fields, supporting both self-hosted and Matomo Cloud URL patterns - Match the official snippet: var _mtm alias, _mtm.push shorthand - Remove redundant type="text/javascript" attribute - Remove unused "tag" field from settings.json https://claude.ai/code/session_0165AXHkmRZ6bx23n7Tbyz8h * Split Matomo config into base url + tag fields Separate the Matomo setting into `url` (base URL, e.g. https://cdn.matomo.cloud/openms.matomo.cloud) and `tag` (container ID, e.g. yDGK8bfY), consistent with how other providers use a tag field. The script constructs the full path: {url}/container_{tag}.js https://claude.ai/code/session_0165AXHkmRZ6bx23n7Tbyz8h * install matomo tag --------- Co-authored-by: Claude <noreply@anthropic.com> * Remove duplicate `address` key in `.streamlit/config.toml` (#346) * Initial plan * fix: remove duplicate address entry in config.toml Co-authored-by: t0mdavid-m <57191390+t0mdavid-m@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: t0mdavid-m <57191390+t0mdavid-m@users.noreply.github.com> * Fix integration test failures caused by sys.modules pollution and shutil.SameFileError (#349) * Initial plan * Fix integration test failures: restore sys.modules mocks, handle SameFileError, update CI workflow Co-authored-by: t0mdavid-m <57191390+t0mdavid-m@users.noreply.github.com> * Remove unnecessary pyopenms mock from test_topp_workflow_parameter.py, simplify test_parameter_presets.py Co-authored-by: t0mdavid-m <57191390+t0mdavid-m@users.noreply.github.com> * Fix Windows build: correct site-packages path in cleanup step Co-authored-by: t0mdavid-m <57191390+t0mdavid-m@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: t0mdavid-m <57191390+t0mdavid-m@users.noreply.github.com> * Remove server address from bundled config.toml for Windows installer (#351) On Windows, 0.0.0.0 is not a valid connect address — the browser fails to open http://0.0.0.0:8501. By removing the address entry from the bundled .streamlit/config.toml, Streamlit defaults to localhost, which works correctly for local deployments. Docker deployments are unaffected as they pass --server.address 0.0.0.0 on the command line. https://claude.ai/code/session_016amsLCZeFogTksmtk1geb5 Co-authored-by: Claude <noreply@anthropic.com> * reenable cross origin protection * Add CLAUDE.md and Claude Code skills for MS webapp development (#357) * Add CLAUDE.md and Claude Code skills for webapp development Adds project documentation (CLAUDE.md) and 6 skills to help developers scaffold and extend OpenMS web applications built from this template: - /create-page: add a new Streamlit page with proper registration - /create-workflow: scaffold a full TOPP workflow (class + 4 pages) - /add-python-tool: add a custom Python analysis script with auto-UI - /add-presets: add parameter presets for workflows - /configure-deployment: set up Docker and CI/CD for a new app - /add-visualization: add pyopenms-viz or OpenMS-Insight visualizations https://claude.ai/code/session_01WYotmLfqRtB8WJXj1Eosiz * Strengthen MS domain context in CLAUDE.md and skills Make it clear to Claude that this is THE framework for building mass spectrometry web applications for proteomics and metabolomics research. Add domain-specific context about MS data types, TOPP tool pipelines, and scientific visualization needs. https://claude.ai/code/session_01WYotmLfqRtB8WJXj1Eosiz --------- Co-authored-by: Claude <noreply@anthropic.com> * Add Kubernetes manifests and CI/CD workflows for deployment (#347) * Add Kubernetes manifests and CI workflows for de.NBI migration Decompose the monolithic Docker container into Kubernetes workloads: - Streamlit Deployment with health probes and session affinity - Redis Deployment + Service for job queue - RQ Worker Deployment for background workflows - CronJob for workspace cleanup - Ingress with WebSocket support and cookie-based sticky sessions - Shared PVC (ReadWriteMany) for workspace data - ConfigMap for runtime configuration (replaces build-time settings) - Kustomize base + template-app overlay for multi-app deployment Code changes: - Remove unsafe enableCORS=false and enableXsrfProtection=false from config.toml - Make workspace path configurable via WORKSPACES_DIR env var in clean-up-workspaces.py CI/CD: - Add build-and-push-image.yml to push Docker images to ghcr.io - Add k8s-manifests-ci.yml for manifest validation and kind integration tests https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix kubeconform validation to skip kustomization.yaml kustomization.yaml is a Kustomize config file, not a standard K8s resource, so kubeconform has no schema for it. Exclude it via -ignore-filename-pattern. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Add matrix strategy to test both Dockerfiles in integration tests The integration-test job now uses a matrix with Dockerfile_simple and Dockerfile. Each matrix entry checks if its Dockerfile exists before running — all steps are guarded with an `if` condition so they skip gracefully when a Dockerfile is absent. This allows downstream forks that only have one Dockerfile to pass CI without errors. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Adapt K8s base manifests for de.NBI Cinder CSI storage - Switch workspace PVC from ReadWriteMany to ReadWriteOnce with cinder-csi storage class (required by de.NBI KKP cluster) - Increase PVC storage to 500Gi - Add namespace: openms to kustomization.yaml - Reduce pod resource requests (1Gi/500m) and limits (8Gi/4 CPU) so all workspace-mounting pods fit on a single node https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Add pod affinity rules to co-locate all workspace pods on same node The workspaces PVC uses ReadWriteOnce (Cinder CSI block storage) which requires all pods mounting it to run on the same node. Without explicit affinity rules, the scheduler was failing silently, leaving pods in Pending state with no events. Adds a `volume-group: workspaces` label and podAffinity with requiredDuringSchedulingIgnoredDuringExecution to streamlit deployment, rq-worker deployment, and cleanup cronjob. This ensures the scheduler explicitly co-locates all workspace-consuming pods on the same node. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix CI: wait for ingress-nginx admission webhook before deploying The controller pod being Ready doesn't guarantee the admission webhook service is accepting connections. Add a polling loop that waits for the webhook endpoint to have an IP assigned before applying the Ingress resource, preventing "connection refused" errors during kustomize apply. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix CI: add -n openms namespace to integration test steps The kustomize overlay deploys into the openms namespace, but the verification steps (Redis wait, Redis ping, deployment checks) were querying the default namespace, causing "no matching resources found". https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix CI: retry kustomize deploy for webhook readiness Replace the unreliable endpoint-IP polling with a retry loop on kubectl apply (up to 5 attempts with backoff). This handles the race where the ingress-nginx admission webhook has an endpoint IP but isn't yet accepting TCP connections. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ --------- Co-authored-by: Claude <noreply@anthropic.com> * Claude/kubernetes migration plan kq jw d (#358) * Add Kubernetes manifests and CI workflows for de.NBI migration Decompose the monolithic Docker container into Kubernetes workloads: - Streamlit Deployment with health probes and session affinity - Redis Deployment + Service for job queue - RQ Worker Deployment for background workflows - CronJob for workspace cleanup - Ingress with WebSocket support and cookie-based sticky sessions - Shared PVC (ReadWriteMany) for workspace data - ConfigMap for runtime configuration (replaces build-time settings) - Kustomize base + template-app overlay for multi-app deployment Code changes: - Remove unsafe enableCORS=false and enableXsrfProtection=false from config.toml - Make workspace path configurable via WORKSPACES_DIR env var in clean-up-workspaces.py CI/CD: - Add build-and-push-image.yml to push Docker images to ghcr.io - Add k8s-manifests-ci.yml for manifest validation and kind integration tests https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix kubeconform validation to skip kustomization.yaml kustomization.yaml is a Kustomize config file, not a standard K8s resource, so kubeconform has no schema for it. Exclude it via -ignore-filename-pattern. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Add matrix strategy to test both Dockerfiles in integration tests The integration-test job now uses a matrix with Dockerfile_simple and Dockerfile. Each matrix entry checks if its Dockerfile exists before running — all steps are guarded with an `if` condition so they skip gracefully when a Dockerfile is absent. This allows downstream forks that only have one Dockerfile to pass CI without errors. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Adapt K8s base manifests for de.NBI Cinder CSI storage - Switch workspace PVC from ReadWriteMany to ReadWriteOnce with cinder-csi storage class (required by de.NBI KKP cluster) - Increase PVC storage to 500Gi - Add namespace: openms to kustomization.yaml - Reduce pod resource requests (1Gi/500m) and limits (8Gi/4 CPU) so all workspace-mounting pods fit on a single node https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Add pod affinity rules to co-locate all workspace pods on same node The workspaces PVC uses ReadWriteOnce (Cinder CSI block storage) which requires all pods mounting it to run on the same node. Without explicit affinity rules, the scheduler was failing silently, leaving pods in Pending state with no events. Adds a `volume-group: workspaces` label and podAffinity with requiredDuringSchedulingIgnoredDuringExecution to streamlit deployment, rq-worker deployment, and cleanup cronjob. This ensures the scheduler explicitly co-locates all workspace-consuming pods on the same node. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix CI: wait for ingress-nginx admission webhook before deploying The controller pod being Ready doesn't guarantee the admission webhook service is accepting connections. Add a polling loop that waits for the webhook endpoint to have an IP assigned before applying the Ingress resource, preventing "connection refused" errors during kustomize apply. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix CI: add -n openms namespace to integration test steps The kustomize overlay deploys into the openms namespace, but the verification steps (Redis wait, Redis ping, deployment checks) were querying the default namespace, causing "no matching resources found". https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix CI: retry kustomize deploy for webhook readiness Replace the unreliable endpoint-IP polling with a retry loop on kubectl apply (up to 5 attempts with backoff). This handles the race where the ingress-nginx admission webhook has an endpoint IP but isn't yet accepting TCP connections. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix REDIS_URL to use prefixed service name in overlay Kustomize namePrefix renames the Redis service to template-app-redis, but the REDIS_URL env var in streamlit and rq-worker deployments still referenced the unprefixed name "redis", causing the rq-worker to CrashLoopBackOff with "Name or service not known". Add JSON patches in the overlay to set the correct prefixed hostname. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Add Traefik IngressRoute for direct LB IP access The cluster uses Traefik, not nginx, so the nginx Ingress annotations are ignored. Add a Traefik IngressRoute with PathPrefix(/) catch-all routing and sticky session cookie for Streamlit session affinity. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix CI: skip Traefik IngressRoute CRD in validation and integration tests kubeconform doesn't know the Traefik IngressRoute CRD schema, and the kind cluster in integration tests doesn't have Traefik installed. Skip the IngressRoute in kubeconform validation and filter it out with yq before applying to the kind cluster. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix IngressRoute service name for kustomize namePrefix Kustomize namePrefix doesn't rewrite service references inside CRDs, so the IngressRoute was pointing to 'streamlit' instead of 'template-app-streamlit', causing Traefik to return 404. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * fix: use ConfigMap as settings override instead of full replacement The ConfigMap was replacing the entire settings.json, losing keys like "version" and "repository-name" that the app expects (causing KeyError). Now the ConfigMap only contains deployment-specific overrides, which are merged into the Docker image's base settings.json at container startup using jq. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * fix: add set -euo pipefail to fail fast on settings merge error Addresses CodeRabbit review: if jq merge fails, the container should not start with unmerged settings. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ --------- Co-authored-by: Claude <noreply@anthropic.com> * Claude/kubernetes migration plan kq jw d (#359) * Add Kubernetes manifests and CI workflows for de.NBI migration Decompose the monolithic Docker container into Kubernetes workloads: - Streamlit Deployment with health probes and session affinity - Redis Deployment + Service for job queue - RQ Worker Deployment for background workflows - CronJob for workspace cleanup - Ingress with WebSocket support and cookie-based sticky sessions - Shared PVC (ReadWriteMany) for workspace data - ConfigMap for runtime configuration (replaces build-time settings) - Kustomize base + template-app overlay for multi-app deployment Code changes: - Remove unsafe enableCORS=false and enableXsrfProtection=false from config.toml - Make workspace path configurable via WORKSPACES_DIR env var in clean-up-workspaces.py CI/CD: - Add build-and-push-image.yml to push Docker images to ghcr.io - Add k8s-manifests-ci.yml for manifest validation and kind integration tests https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix kubeconform validation to skip kustomization.yaml kustomization.yaml is a Kustomize config file, not a standard K8s resource, so kubeconform has no schema for it. Exclude it via -ignore-filename-pattern. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Add matrix strategy to test both Dockerfiles in integration tests The integration-test job now uses a matrix with Dockerfile_simple and Dockerfile. Each matrix entry checks if its Dockerfile exists before running — all steps are guarded with an `if` condition so they skip gracefully when a Dockerfile is absent. This allows downstream forks that only have one Dockerfile to pass CI without errors. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Adapt K8s base manifests for de.NBI Cinder CSI storage - Switch workspace PVC from ReadWriteMany to ReadWriteOnce with cinder-csi storage class (required by de.NBI KKP cluster) - Increase PVC storage to 500Gi - Add namespace: openms to kustomization.yaml - Reduce pod resource requests (1Gi/500m) and limits (8Gi/4 CPU) so all workspace-mounting pods fit on a single node https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Add pod affinity rules to co-locate all workspace pods on same node The workspaces PVC uses ReadWriteOnce (Cinder CSI block storage) which requires all pods mounting it to run on the same node. Without explicit affinity rules, the scheduler was failing silently, leaving pods in Pending state with no events. Adds a `volume-group: workspaces` label and podAffinity with requiredDuringSchedulingIgnoredDuringExecution to streamlit deployment, rq-worker deployment, and cleanup cronjob. This ensures the scheduler explicitly co-locates all workspace-consuming pods on the same node. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix CI: wait for ingress-nginx admission webhook before deploying The controller pod being Ready doesn't guarantee the admission webhook service is accepting connections. Add a polling loop that waits for the webhook endpoint to have an IP assigned before applying the Ingress resource, preventing "connection refused" errors during kustomize apply. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix CI: add -n openms namespace to integration test steps The kustomize overlay deploys into the openms namespace, but the verification steps (Redis wait, Redis ping, deployment checks) were querying the default namespace, causing "no matching resources found". https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix CI: retry kustomize deploy for webhook readiness Replace the unreliable endpoint-IP polling with a retry loop on kubectl apply (up to 5 attempts with backoff). This handles the race where the ingress-nginx admission webhook has an endpoint IP but isn't yet accepting TCP connections. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix REDIS_URL to use prefixed service name in overlay Kustomize namePrefix renames the Redis service to template-app-redis, but the REDIS_URL env var in streamlit and rq-worker deployments still referenced the unprefixed name "redis", causing the rq-worker to CrashLoopBackOff with "Name or service not known". Add JSON patches in the overlay to set the correct prefixed hostname. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Add Traefik IngressRoute for direct LB IP access The cluster uses Traefik, not nginx, so the nginx Ingress annotations are ignored. Add a Traefik IngressRoute with PathPrefix(/) catch-all routing and sticky session cookie for Streamlit session affinity. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix CI: skip Traefik IngressRoute CRD in validation and integration tests kubeconform doesn't know the Traefik IngressRoute CRD schema, and the kind cluster in integration tests doesn't have Traefik installed. Skip the IngressRoute in kubeconform validation and filter it out with yq before applying to the kind cluster. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix IngressRoute service name for kustomize namePrefix Kustomize namePrefix doesn't rewrite service references inside CRDs, so the IngressRoute was pointing to 'streamlit' instead of 'template-app-streamlit', causing Traefik to return 404. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * fix: use ConfigMap as settings override instead of full replacement The ConfigMap was replacing the entire settings.json, losing keys like "version" and "repository-name" that the app expects (causing KeyError). Now the ConfigMap only contains deployment-specific overrides, which are merged into the Docker image's base settings.json at container startup using jq. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * fix: add set -euo pipefail to fail fast on settings merge error Addresses CodeRabbit review: if jq merge fails, the container should not start with unmerged settings. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * fix: change imagePullPolicy to Always for mutable main tag With IfNotPresent, rollout restarts reuse the cached image even when a new version has been pushed with the same tag. Always ensures Kubernetes pulls the latest image on every pod start. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * fix: build full Dockerfile instead of Dockerfile_simple Switch CI to build the full Docker image with OpenMS and TOPP tools, not the lightweight pyOpenMS-only image. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ --------- Co-authored-by: Claude <noreply@anthropic.com> * Claude/fix mzml files validation y zfla (#361) * Add Kubernetes manifests and CI workflows for de.NBI migration Decompose the monolithic Docker container into Kubernetes workloads: - Streamlit Deployment with health probes and session affinity - Redis Deployment + Service for job queue - RQ Worker Deployment for background workflows - CronJob for workspace cleanup - Ingress with WebSocket support and cookie-based sticky sessions - Shared PVC (ReadWriteMany) for workspace data - ConfigMap for runtime configuration (replaces build-time settings) - Kustomize base + template-app overlay for multi-app deployment Code changes: - Remove unsafe enableCORS=false and enableXsrfProtection=false from config.toml - Make workspace path configurable via WORKSPACES_DIR env var in clean-up-workspaces.py CI/CD: - Add build-and-push-image.yml to push Docker images to ghcr.io - Add k8s-manifests-ci.yml for manifest validation and kind integration tests https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix kubeconform validation to skip kustomization.yaml kustomization.yaml is a Kustomize config file, not a standard K8s resource, so kubeconform has no schema for it. Exclude it via -ignore-filename-pattern. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Add matrix strategy to test both Dockerfiles in integration tests The integration-test job now uses a matrix with Dockerfile_simple and Dockerfile. Each matrix entry checks if its Dockerfile exists before running — all steps are guarded with an `if` condition so they skip gracefully when a Dockerfile is absent. This allows downstream forks that only have one Dockerfile to pass CI without errors. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Adapt K8s base manifests for de.NBI Cinder CSI storage - Switch workspace PVC from ReadWriteMany to ReadWriteOnce with cinder-csi storage class (required by de.NBI KKP cluster) - Increase PVC storage to 500Gi - Add namespace: openms to kustomization.yaml - Reduce pod resource requests (1Gi/500m) and limits (8Gi/4 CPU) so all workspace-mounting pods fit on a single node https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Add pod affinity rules to co-locate all workspace pods on same node The workspaces PVC uses ReadWriteOnce (Cinder CSI block storage) which requires all pods mounting it to run on the same node. Without explicit affinity rules, the scheduler was failing silently, leaving pods in Pending state with no events. Adds a `volume-group: workspaces` label and podAffinity with requiredDuringSchedulingIgnoredDuringExecution to streamlit deployment, rq-worker deployment, and cleanup cronjob. This ensures the scheduler explicitly co-locates all workspace-consuming pods on the same node. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix CI: wait for ingress-nginx admission webhook before deploying The controller pod being Ready doesn't guarantee the admission webhook service is accepting connections. Add a polling loop that waits for the webhook endpoint to have an IP assigned before applying the Ingress resource, preventing "connection refused" errors during kustomize apply. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix CI: add -n openms namespace to integration test steps The kustomize overlay deploys into the openms namespace, but the verification steps (Redis wait, Redis ping, deployment checks) were querying the default namespace, causing "no matching resources found". https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix CI: retry kustomize deploy for webhook readiness Replace the unreliable endpoint-IP polling with a retry loop on kubectl apply (up to 5 attempts with backoff). This handles the race where the ingress-nginx admission webhook has an endpoint IP but isn't yet accepting TCP connections. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix REDIS_URL to use prefixed service name in overlay Kustomize namePrefix renames the Redis service to template-app-redis, but the REDIS_URL env var in streamlit and rq-worker deployments still referenced the unprefixed name "redis", causing the rq-worker to CrashLoopBackOff with "Name or service not known". Add JSON patches in the overlay to set the correct prefixed hostname. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Add Traefik IngressRoute for direct LB IP access The cluster uses Traefik, not nginx, so the nginx Ingress annotations are ignored. Add a Traefik IngressRoute with PathPrefix(/) catch-all routing and sticky session cookie for Streamlit session affinity. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix CI: skip Traefik IngressRoute CRD in validation and integration tests kubeconform doesn't know the Traefik IngressRoute CRD schema, and the kind cluster in integration tests doesn't have Traefik installed. Skip the IngressRoute in kubeconform validation and filter it out with yq before applying to the kind cluster. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Fix IngressRoute service name for kustomize namePrefix Kustomize namePrefix doesn't rewrite service references inside CRDs, so the IngressRoute was pointing to 'streamlit' instead of 'template-app-streamlit', causing Traefik to return 404. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * fix: use ConfigMap as settings override instead of full replacement The ConfigMap was replacing the entire settings.json, losing keys like "version" and "repository-name" that the app expects (causing KeyError). Now the ConfigMap only contains deployment-specific overrides, which are merged into the Docker image's base settings.json at container startup using jq. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * fix: add set -euo pipefail to fail fast on settings merge error Addresses CodeRabbit review: if jq merge fails, the container should not start with unmerged settings. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * fix: change imagePullPolicy to Always for mutable main tag With IfNotPresent, rollout restarts reuse the cached image even when a new version has been pushed with the same tag. Always ensures Kubernetes pulls the latest image on every pod start. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * fix: build full Dockerfile instead of Dockerfile_simple Switch CI to build the full Docker image with OpenMS and TOPP tools, not the lightweight pyOpenMS-only image. https://claude.ai/code/session_01RNJ3dVjV1VTHcC9ugE3FQJ * Scope IngressRoute to hostname and drop unused nginx Ingress Traefik is the only ingress controller on the cluster; the nginx Ingress in k8s/base/ingress.yaml was orphaned (no nginx class available) and the overlay was patching it instead of the active Traefik IngressRoute. - Add Host() match to the base IngressRoute (placeholder filled by overlays) - template-app overlay patches the IngressRoute with template.webapps.openms.de - Remove ingress.yaml from the base kustomization resources list (file kept in the repo for nginx-based consumers) https://claude.ai/code/session_01YNDYJTx1eSKaL9vQe1GQzV * fix: use PVC mount for workspaces in online mode In online mode, src/common/common.py hard-coded workspaces_dir to the literal ".." which, from WORKDIR /app, resolved to /. Workspace UUID directories were therefore created on each pod's ephemeral local filesystem instead of the shared PVC mounted at /workspaces-streamlit-template, so the Streamlit pod and the RQ worker each saw their own disconnected copy. The worker's params.json load in tasks.py then hit an empty dict, producing `KeyError: 'mzML-files'` as soon as Workflow.execution() ran. - common.py: in the online branch, use WORKSPACES_DIR env var (default /workspaces-streamlit-template) so Streamlit, the RQ worker, and the cleanup cronjob (which already reads WORKSPACES_DIR) all agree on one location. - k8s streamlit & rq-worker deployments: set WORKSPACES_DIR explicitly so the env is overridable and visible at deploy time. - WorkflowManager.start_workflow: call save_parameters() before dispatch so the latest session state is flushed to disk, closing a small race where a fragment rerun could leave params.json stale when the worker picked up the job. https://claude.ai/code/session_01TsxtENPpuCZ1Ap3mX2ZpHr --------- Co-authored-by: Claude <noreply@anthropic.com> * Fix contrib tag (#360) * fix(ci): pin OpenMS contrib download to matching release tag The Windows build step downloaded contrib_build-Windows.tar.gz from OpenMS/contrib without a --tag, always pulling the latest release. When the GH Actions cache (7-day eviction) expired, a newer contrib got pulled that was incompatible with the pinned OpenMS release/3.5.0 source tree, breaking MSVC compilation in DIAPrescoring.cpp. Pin the download to release/${OPENMS_VERSION} and tie the cache key to the OpenMS version so contrib stays in lockstep with the source. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ci): pass release tag as positional arg to gh release download `gh release download` takes the tag as a positional argument, not a `--tag` flag. Silently failed to match on Windows with the system error "The system cannot find the file specified". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: allow contrib version override via OPENMS_CONTRIB_VERSION Adds OPENMS_CONTRIB_VERSION env var that falls back to OPENMS_VERSION when empty. Lets us point OPENMS_VERSION at a non-release branch (e.g. develop) while keeping the Windows contrib download pinned to a known release tag, so CI doesn't fail on a missing contrib release. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: ignore docs/superpowers/ (local design notes) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add Kubernetes deployment docs and refactor Claude skills (#362) * Remove stale patches from template-app overlay The Deployment/streamlit patch with Ingress-shaped path /spec/rules/0/host never applied and produced a silent no-op. The duplicate IngressRoute service-name patch was redundant with the first IngressRoute patch block. This brings the on-disk overlay in line with the production cluster's running version. * Rename configure-deployment skill to configure-docker-compose-deployment First step of splitting the skill into three focused skills (configure-app-settings, configure-docker-compose-deployment, configure-k8s-deployment). Rename is in its own commit so git log --follow traces the docker-compose content cleanly. * Scope docker-compose skill to docker-compose-only Removes app-level content (settings.json, Dockerfile choice, production app examples) that will live in configure-app-settings. Adds a prerequisite note pointing to configure-app-settings. * Add configure-app-settings skill Covers app-level configuration (settings.json, Dockerfile choice, README, dependencies) shared by every deployment mode. Prerequisite for configure-docker-compose-deployment and configure-k8s-deployment. * Fix settings.json key-field list inconsistency The Key fields prose listed max_threads (not in the JSON sample) and omitted enable_workspaces (which is in the sample). Align the prose with the sample and describe max_threads separately since it is a nested object rather than a flat field. * Add configure-k8s-deployment skill New skill walking through Kustomize overlay creation and kubectl apply for deploying a forked app to Kubernetes. Patch list reflects the three-patch canonical shape (IngressRoute match + service, streamlit Redis URL, rq-worker Redis URL). * Fix inline-code rendering in k8s skill The Host(`...`) escape syntax produced literal backslashes that broke the inline-code span when rendered by markdown parsers. Rewrite as Host(...) without nested backticks so the span renders cleanly. * Add K8s deployment doc — overview and architecture sections * Add K8s deployment doc — manifest reference section * Add K8s deployment doc — fork-and-deploy guide * Add K8s deployment doc — CI/CD pipeline section * Clarify PR-blocking behavior depends on branch protection The workflow does not block merges directly — it produces a check status that a branch-protection rule can gate on. Make the preconditions explicit. * Register Kubernetes Deployment page in Streamlit documentation * Cross-link docs/deployment.md to Kubernetes deployment page Adds a preamble listing both deployment paths and introduces a ## Docker Compose heading above the existing content. The existing docker-compose content is preserved verbatim. * Add smoke test for Kubernetes Deployment documentation page Extends the parametrized test_documentation cases to cover the new Documentation page added by this branch, closing the gap where it was the only selectbox entry without test coverage. * ci: add ghcr-cleanup workflow (scheduled disabled, dry-run default) * ci: scaffold build-and-test workflow with lint-manifests job * ci: add build job skeleton with matrix, buildx, ghcr login * ci: add metadata extraction, build-push, and registry cache * ci: add kind integration steps to build job * ci: lowercase image name for OCI cache refs github.repository preserves the original casing (OpenMS/streamlit-template). Docker OCI references require lowercase, so cache-from/cache-to fail with 'invalid reference format'. docker/metadata-action handles this internally for tags, but the cache refs bypass it. Compute IMAGE_NAME_LC once and use it in both cache refs. * ci: don't pass unprefixed local tag to buildx push With push: true, docker/build-push-action pushes every tag in its tags input. A bare name like 'openms-streamlit:simple-test' (no registry prefix) gets resolved to Docker Hub and fails with 401 unauthorized, because the workflow's GHCR token has no rights on docker.io. The local tag was only needed for the kind retag step. Since load: true already loads the image into the runner's docker daemon, we can create the stable local alias with a plain 'docker tag' step after build, picking any tag from docker/metadata-action's output. * ci: delete old docker workflows now superseded by build-and-test * k8s: pin overlay image tag to main-full (new CI scheme) * docs(skill): update k8s deploy skill for unified CI workflow * docs(k8s): update deployment doc for unified CI workflow * ci: pin container-retention-policy to v3.0.1 The @v3 floating tag does not exist on snok/container-retention-policy (v2 is the latest floating major tag; v3 only has v3.0.0 and v3.0.1 as exact version tags). The workflow fails to resolve the action with 'unable to find version v3'. Pin to v3.0.1 (latest v3 release). * fix(docker): stop cache-busting on GITHUB_TOKEN The ENV GH_TOKEN=${GITHUB_TOKEN} at the top baked the per-run token into an early layer, so every workflow run rebuilt from scratch. Moved the ARG next to the one RUN that uses it (gh release download) so earlier layers stay cacheable. * docs: fix typo (Gihub -> GitHub) in Dockerfile comments * ci: enable scheduled GHCR cleanup (weekly Sun 03:00 UTC) * k8s: serve template app on both .de and .org TLDs Updates the Traefik IngressRoute match in the template-app overlay to accept both Host() values, and mirrors the same dual-host pattern in the nginx Ingress fallback (two rules entries, same backend). Outer parentheses on the || group are required for correct precedence against PathPrefix. * ci: integration-test both .de and .org hosts on nginx and traefik Adds a dual-host curl assertion to the existing nginx kind integration and a new traefik-integration job that brings up Traefik via Helm, deploys the full overlay (no IngressRoute filter), and curls both hostnames through the IngressRoute. The traefik-integration job runs once on Dockerfile_simple — ingress routing is image-agnostic, and adding the full image variant would double the runtime without catching new regressions. * ci: enable kind to bind workspace PVC and clean up port-forwards The cinder-csi storage class isn't available in kind clusters. Patch it to 'standard' (kind's default local-path-provisioner) at apply time, alongside the existing imagePullPolicy substitution. Without this, the workspace PVC stays unbound, streamlit and rq-worker pods stay Pending, and the new dual-host curl assertions fail with 503. The existing 'Verify all deployments are available' step had been masking this with '|| true' since the integration test was added. Also wire up a trap-based EXIT cleanup for the kubectl port-forward processes; the previous trailing 'kill' line was unreachable under set -e if any curl assertion failed. * skill(configure-k8s-deployment): document dual-host overlay edit Updates the overlay-edit step to require editing both Host() values (.de and .org) plus the parallel nginx Ingress two-rules pattern. Updates the verification checklist accordingly. * skill(configure-k8s-deployment): fix markdown rendering and clarify nginx patch CommonMark code spans don't process backslash escapes for backticks, so `Host(\`…\`)` rendered as broken fragments. Wrap with double backticks instead — the inner backticks are then literal. Also clarify the nginx fallback note: 'patch both rules[].host entries' could be misread as directly editing the shared base file; 'add an overlay patch for both rules[].host entries' is unambiguous. * docs(kubernetes-deployment): document dual-host serving Updates the architecture diagram, manifest reference, customization table, and CI/CD section to describe the dual-host (.de + .org) default. Adds a short subsection on the per-host stroute cookie and why cross-TLD switches are harmless. * docs(kubernetes-deployment): fix stale job count and missing kind patches in Job 3 Two factual errors caught in review: - "both jobs run on pull requests" was true with 2 jobs, but there are now 3 (lint-manifests, build, traefik-integration). All three run on PRs. - Job 3's description omitted that the deploy step still patches imagePullPolicy and storageClassName for kind compatibility, even though it doesn't filter the IngressRoute. Job 2's description already mentions both patches; Job 3 should be parallel. * ci: use nginx Ingress hostnames for nginx-job curl assertions The nginx Ingress is unpatched by the overlay, so it retains its base hostnames (streamlit.openms.example.de / .org) from k8s/base/ingress.yaml. The previous curl step used the Traefik IngressRoute hostnames (template.webapps.openms.*), which the nginx ingress controller does not match — every request 404'd. Traefik's curl step is unchanged: the IngressRoute IS patched to the template.webapps.openms.* hostnames, so those are correct there. * k8s: mount admin password from streamlit-secrets Secret The Save-as-Demo feature reads the admin password from st.secrets (i.e. .streamlit/secrets.toml), but the Streamlit pod never had that file mounted, so the feature was always disabled in cluster deployments. Mount an optional Secret named streamlit-secrets as /app/.streamlit/secrets.toml, add a reference .example manifest (not included in kustomization -- the Secret is created out-of-band so no password lands in git), gitignore any filled-in copy, and document the imperative kubectl-create flow alongside the manifest alternative. https://claude.ai/code/session_01LAJZ5EWBJkznj7vQnKt8vV * fix errors * ci: bump pyopenms to 3.5.0 and pin python 3.10 to match Dockerfile The committed Dockerfile builds OpenMS from release/3.5.0 on python 3.10, but requirements.txt pinned pyopenms==3.3.0 and ci.yml ran on python 3.11, causing test_gui.py to fail with AttributeError on MSExperiment.to_df() (the API was renamed from get_df to to_df in 3.5). * fix(view): use pyopenms 3.5 get_df API instead of unreleased to_df `MSExperiment.to_df()` exists only on the OpenMS develop branch and is not in the published pyopenms 3.5.0 wheel that CI installs from PyPI, causing AttributeError in the raw data viewer. Switch to `get_df()` and `get_df(long=True)` — both return the same column names that the existing rename logic expects (rt/ms_level/mz_array/intensity_array for the wide form, rt/mz/intensity for the long form). * fix(k8s): mount streamlit-secrets as directory so optional: true works CI pods crashlooped because a `subPath: secrets.toml` file mount cannot resolve when the optional Secret is absent. Mount the Secret as a directory at /app/admin-secrets/ instead, and register that path via [secrets].files in .streamlit/config.toml so st.secrets picks it up without shadowing the baked-in config.toml / credentials.toml. * docs(k8s): add streamlit-secrets example to template-app overlay Mirrors the base example with overlay-specific guidance: `namePrefix` only rewrites Kustomize-managed resources, so imperative Secrets must still use the literal name `streamlit-secrets`. * k8s: two-tier scheduling via Kustomize components + LimitRange Factor node placement and memory sizing out of the base manifests into reusable Kustomize components (memory-tier-low / memory-tier-high), so each fork picks its tier with a single line in its overlay. - base: remove per-pod `resources` from streamlit and rq-worker Deployments; sizing now comes from the tier component - base: promote redis to Guaranteed QoS (requests == limits for both cpu and memory) so it bottoms the kernel OOM list - base: add LimitRange so containers without explicit resources inherit safe defaults (512Mi/250m request, 2Gi/2 limit, 64Gi/16 max) - components/memory-tier-low: nodeSelector=low, streamlit 512Mi/2Gi, rq-worker 1Gi/16Gi (Burstable) - components/memory-tier-high: nodeSelector=high, streamlit 512Mi/4Gi, rq-worker 2Gi/180Gi (Burstable — uniform across heavy workers so a single active app can burst into the shared pool) - overlays: rename template-app/ to prod/ (one overlay per repo; the repo itself identifies the app) and pull in memory-tier-low - docs & skill: document the new overlays/prod/ path and the one-line tier selector; update CI to kustomize the renamed overlay https://claude.ai/code/session_01LW4iBWt5YftuqFGc3jM5ZP * ci(k8s): label kind node to match the overlay's memory tier The memory-tier-low component adds nodeSelector openms.de/memory-tier=low to every Deployment. kind clusters have no such label, so after the rename to overlays/prod all pods stayed Pending and 'Wait for Redis to be ready' timed out. Label --all kind nodes in both the nginx and Traefik integration jobs before deploying so the nodeSelector matches. Also raise the LimitRange max.memory from 64Gi to 200Gi. The original cap was written before memory-tier-high settled on a 180Gi rq-worker limit; without the bump, a high-tier fork (e.g. OpenDIAKiosk) would be rejected by admission when deployed into the shared openms namespace after the template's LimitRange is applied. https://claude.ai/code/session_01LW4iBWt5YftuqFGc3jM5ZP * k8s: move streamlit-secrets.yaml.example into overlays/prod/ Completes the overlay rename started in 6c61365 now that the branch has merged main, which added the example file under the old path. Also rewrite two remaining docs references to overlays/<your-app-name>/ and the CI description to the new prod overlay. https://claude.ai/code/session_01LW4iBWt5YftuqFGc3jM5ZP * ci(k8s): two-node kind cluster with both tier labels Spin up a 2-node kind cluster (control-plane labeled memory-tier=low + ingress-ready, worker labeled memory-tier=high) so the Build-and-Test job passes regardless of which memory-tier component a fork's overlay pulls in. Previously we labeled --all nodes with a single tier after creation, which broke as soon as a fork flipped memory-tier-low to memory-tier-high. - .github/kind-config.yaml: 2-node topology with per-node labels. - .github/workflows/build-and-test.yml: point both helm/kind-action invocations (nginx build + traefik-integration) at the config and drop the now-redundant dynamic label step. https://claude.ai/code/session_01LW4iBWt5YftuqFGc3jM5ZP * ci(k8s): clear control-plane NoSchedule taint in two-node kind config Previous run (2f28ed9) showed build + traefik-integration jobs still timing out on 'Wait for Redis'. Root cause: multi-node kind clusters apply node-role.kubernetes.io/control-plane:NoSchedule to the control-plane, which untolerated app pods can't land on even though the nodeSelector matches. The single-node kind used previously had no such taint, which is why CI worked until we added a second node. Add a kubeadmConfigPatches stanza setting nodeRegistration.taints to the empty list so the control-plane is schedulable. Labels and cluster shape (1 control-plane + 1 worker) stay the same. https://claude.ai/code/session_01LW4iBWt5YftuqFGc3jM5ZP * k8s: store demo workspaces on the workspaces PVC Adds a seed-demos initContainer to the Streamlit Deployment that merges image-shipped demos into /workspaces-streamlit-template/.demos/ with cp -rn, so new demos in an image appear after redeploy while admin-saved demos and edits persist across redeploys. - Point demo_workspaces.source_dirs at the PV path via the ConfigMap override (both streamlit and rq-worker pick this up through the jq settings merge at startup). - Make get_demo_target_dir() settings-driven so "Save as Demo" writes to the PV, with backwards-compatible fallbacks for the legacy source_dir string and for environments without settings (tests). - Skip hidden top-level dirs in clean-up-workspaces.py so the nightly cron does not garbage-collect .demos/. - Document the .demos/ layout and the re-seed flow. https://claude.ai/code/session_01Y87aULHSdyBobPdaD4L6tW * k8s: ship streamlit-secrets by default, hide admin UI when empty The Secret used to be an out-of-band copy-the-example step, so forgetting the resources-list edit left the pod booting with an empty admin-secrets mount and a user-facing "Admin not configured" error for a feature that was never wired up in the first place. Now the Secret is committed to the base with an empty admin password and included in k8s/base/kustomization.yaml, so kubectl apply -k always creates it. The "Save as Demo" expander is gated on a non-empty password and is hidden entirely (no error box) when not configured. Operators enable the feature by patching the live Secret or by editing the file locally with git update-index --skip-worktree, both documented. Exception handling in is_admin_configured() is tightened to also catch StreamlitSecretNotFoundError so a missing secrets file never raises. https://claude.ai/code/session_01V1noocAR7uXWjWsC9oLGhz * ci: reuse built docker images across ingress tests Split the build+test flow into three stages so the traefik ingress test no longer rebuilds Dockerfile_simple from scratch: build (matrix: full, simple) -> uploads each image as a workflow artifact test-nginx (matrix: full, simple) -> downloads artifact, kind loads, tests nginx ingress test-traefik (simple only) -> downloads simple artifact, kind loads, tests traefik ingress Artifacts (not GHCR) are used because the build job only pushes on non-PR events and fork PRs cannot auth to GHCR at all, so registry sharing would not work for every PR path. * ci: run test-traefik against both image variants Mirror the build/test-nginx matrix so the traefik ingress test also covers the full and simple variants instead of just simple. * ci: harden ingress-test wait/curl flow for slow simple deployments test-traefik (simple) failed in the combined "Wait for Redis and deployments to be ready" step because the deployment took longer than 120s to become available, and unlike the test-nginx wait the failure was not soft. Align test-traefik with test-nginx: - Split Redis wait (hard, 60s) from deployment wait (soft, `|| true`). - Bump deployment timeout 120s -> 180s in both jobs. - Widen the curl warm-up loop from 5x2s to 30x2s in both jobs so a marginally late deployment is tolerated; a real failure still surfaces via the trailing unconditional curl. * Rework configure-k8s-deployment skill as an interview The previous skill was a manual find-and-replace checklist that assumed Claude could run kubectl against the cluster. Restructure it as an interview-driven file-editing guide with a clear handoff to a human operator (or CI) for cluster apply. - Drop kubectl, kubectl kustomize, and rollout-verification steps that Claude can't actually execute. - Drop nginx ingress fallback; production is Traefik-only. - Add a Step 1 recon over a fixed set of base/overlay/CI files so defaults are derived from the repo, and the skill bails on layouts it doesn't recognize. - Replace the manual checklist with six interview questions, each paired with what it controls in the running deployment, the proposed default, and the reasoning. Slug, GHCR ref, image tag, ingress subdomain, memory tier, workspace storage size. - Make storage a single 1-line edit to k8s/base/workspace-pvc.yaml when the user picks a non-default size; keep the PVC base name unchanged (namePrefix scopes it per-fork, no collisions). - Pin the default storage size to 500 Gi to match the stock base, so the default needs zero file edits. - Explain that images[0].name is a Kustomize match key and must not change. * k8s: drop cross-fork pod-affinity, rely on RWO PVC for co-location The shared volume-group: workspaces label and required pod-affinity attracted every fork's workspace pods onto a single node per memory tier and deadlocked the first replica of any fork landing on an otherwise-empty tier (no peer pod for the required affinity to match). Per-fork RWO PVCs (<slug>-workspaces-pvc) already constrain all of a fork's workspace-using pods to the node the volume is attached to via the scheduler's VolumeBinding plugin, so the explicit affinity adds nothing on top. Removing it scopes co-location naturally to one fork and lets a fresh tier bootstrap without manual affinity-strip. NodeSelector continues to pick the memory tier; the RWO mount picks the specific node within that tier. * ci: derive slug + Traefik hosts from overlay so forks stay green The kind integration jobs in build-and-test.yml hardcoded `template-app` as the slug label and `template.webapps.openms.{de,org}` as the Traefik hostnames. The configure-k8s-deployment skill rewrites those values when a fork customizes its overlay, after which `kubectl wait -l app=...` returns "no matching resources found" and Traefik curl tests hit the wrong Host header. This broke OpenMS/quantms-web PR #19 on its first overlay PR (run 24964475081). Have test-nginx and test-traefik discover SLUG (from `commonLabels.app`) and TRAEFIK_HOSTS (parsed from the rendered IngressRoute match) right after deploy, and substitute them into the wait/curl steps. The nginx hostnames stay hardcoded — they come from `k8s/base/ingress.yaml`, which the skill never edits and Kustomize doesn't rewrite. Update the configure-k8s-deployment skill to (a) check during recon that the workflow uses dynamic discovery, (b) flag forks still on the old hardcoded shape so the skill applies the patch before editing the overlay, and (c) note in the handoff that no fork-specific workflow edits are needed. * refix ci * refix admin panel --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
1 parent 93ab8ec commit 915f894

6 files changed

Lines changed: 38 additions & 53 deletions

File tree

.claude/skills/configure-k8s-deployment.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Before asking the user anything, read a small known set of files directly (do no
2929
3. `k8s/base/kustomization.yaml`, `k8s/base/streamlit-deployment.yaml`, `k8s/base/rq-worker-deployment.yaml`, `k8s/base/workspace-pvc.yaml` — confirm the layout still matches the template:
3030
- PVC `metadata.name` is `workspaces-pvc`.
3131
- Deployments reference `image: openms-streamlit` (the placeholder Kustomize swaps).
32-
- `streamlit-deployment.yaml` has `claimName: workspaces-pvc` and `volume-group: workspaces` (both as a pod label and as the pod-affinity `matchExpressions` value).
32+
- `streamlit-deployment.yaml` has `claimName: workspaces-pvc`. (Co-location of the workspace-using pods is enforced by the shared RWO PVC mount, not by a pod-affinity rule.)
3333
4. `.github/workflows/build-and-test.yml` — confirm which tags CI publishes (the OpenMS template publishes `<branch>-full`, `<branch>-simple`, `<tag>-full`, `<tag>-simple`, plus `latest` on `main`-full pushes).
3434

3535
If any of those files are missing, renamed, or significantly restructured, stop and ask the user how to proceed. Do not pattern-match the standard answers onto an unknown layout.
@@ -155,7 +155,7 @@ spec:
155155
storage: <size> # Q6: e.g. 100Gi, 1Ti, 3Ti
156156
```
157157

158-
Do **not** rename the PVC, the base `kustomization.yaml` resource list, the `claimName` in `streamlit-deployment.yaml`, or the `volume-group` pod-affinity label. Kustomize's `namePrefix` already gives the in-cluster PVC a unique per-fork name; renaming the base creates a 3-file cascade for no benefit.
158+
Do **not** rename the PVC, the base `kustomization.yaml` resource list, or the `claimName` in `streamlit-deployment.yaml`. Kustomize's `namePrefix` already gives the in-cluster PVC a unique per-fork name; renaming the base creates a 3-file cascade for no benefit.
159159

160160
Operator caveat (mention in handoff, not your job to verify): in-place expansion of an *already-deployed* PVC requires the StorageClass to have `allowVolumeExpansion: true`. If the operator's `cinder-csi` class does not allow expansion, growing a live PVC requires recreation, not a manifest edit. Resizing on first deploy is unaffected.
161161

@@ -164,7 +164,7 @@ Operator caveat (mention in handoff, not your job to verify): in-place expansion
164164
After committing the edits, tell the user the next steps belong to a human operator (or CI) and are out of scope for you:
165165

166166
1. Open a PR with the overlay edits and have it reviewed.
167-
2. Merge to `main`. CI (`build-and-test.yml`) rebuilds and pushes the image to GHCR with the tag from Q3.
167+
2. Merge to `main`. CI (`build-and-test.yml`) rebuilds and pushes the image to GHCR with the tag from Q3. The kind integration jobs (`test-nginx`, `test-traefik`) auto-discover slug and Traefik hostnames from the overlay output, so no workflow edits are needed for fork-specific values.
168168
3. Cluster operator runs `kubectl apply -k k8s/overlays/prod/` against the OpenMS cluster.
169169
4. Operator verifies with `kubectl -n openms rollout status deployment/<slug>-streamlit` and a browser check on `https://<sub>.webapps.openms.de`.
170170

@@ -185,4 +185,5 @@ After committing the edits, tell the user the next steps belong to a human opera
185185
- [ ] Redis URL written in both Deployment patches (`streamlit` and `rq-worker`)
186186
- [ ] Memory-tier component selected
187187
- [ ] Storage size in `k8s/base/workspace-pvc.yaml` updated only if the user picked a non-default size; PVC name and `claimName` untouched
188+
- [ ] `.github/workflows/build-and-test.yml` uses dynamic overlay discovery (no `template-app` / `template.webapps.openms.*` literals); patched in if the fork's workflow was on the old hardcoded shape
188189
- [ ] Changes committed on a feature branch (no PR opened unless the user asked for one)

.github/workflows/build-and-test.yml

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -158,19 +158,24 @@ jobs:
158158
sleep "${i}0"
159159
done
160160
161+
- name: Discover overlay identity
162+
run: |
163+
SLUG=$(yq '.commonLabels.app' k8s/overlays/prod/kustomization.yaml)
164+
echo "SLUG=$SLUG" >> "$GITHUB_ENV"
165+
161166
- name: Wait for Redis to be ready
162167
run: |
163-
kubectl wait -n openms --for=condition=ready pod -l app=quantms-ddalfq,component=redis --timeout=60s
168+
kubectl wait -n openms --for=condition=ready pod -l app=${SLUG},component=redis --timeout=60s
164169
165170
- name: Verify Redis Service is reachable
166171
run: |
167-
kubectl run redis-test -n openms --image=redis:7-alpine --rm -i --restart=Never -- redis-cli -h quantms-ddalfq-redis.openms.svc.cluster.local ping
172+
kubectl run redis-test -n openms --image=redis:7-alpine --rm -i --restart=Never -- redis-cli -h ${SLUG}-redis.openms.svc.cluster.local ping
168173
169174
- name: Verify all deployments are available
170175
run: |
171-
kubectl wait -n openms --for=condition=available deployment -l app=quantms-ddalfq --timeout=180s || true
172-
kubectl get pods -n openms -l app=quantms-ddalfq
173-
kubectl get services -n openms -l app=quantms-ddalfq
176+
kubectl wait -n openms --for=condition=available deployment -l app=${SLUG} --timeout=180s || true
177+
kubectl get pods -n openms -l app=${SLUG}
178+
kubectl get services -n openms -l app=${SLUG}
174179
175180
- name: Curl both hostnames via nginx ingress
176181
run: |
@@ -245,29 +250,39 @@ jobs:
245250
sleep "${i}0"
246251
done
247252
253+
- name: Discover overlay identity
254+
run: |
255+
SLUG=$(yq '.commonLabels.app' k8s/overlays/prod/kustomization.yaml)
256+
TRAEFIK_HOSTS=$(kubectl kustomize k8s/overlays/prod/ \
257+
| yq 'select(.kind == "IngressRoute") | .spec.routes[0].match' \
258+
| grep -oP "Host\(\`\K[^\`]+" | tr '\n' ' ')
259+
echo "SLUG=$SLUG" >> "$GITHUB_ENV"
260+
echo "TRAEFIK_HOSTS=$TRAEFIK_HOSTS" >> "$GITHUB_ENV"
261+
248262
- name: Wait for Redis to be ready
249263
run: |
250-
kubectl wait -n openms --for=condition=ready pod -l app=quantms-ddalfq,component=redis --timeout=60s
264+
kubectl wait -n openms --for=condition=ready pod -l app=${SLUG},component=redis --timeout=60s
251265
252266
- name: Verify all deployments are available
253267
run: |
254-
kubectl wait -n openms --for=condition=available deployment -l app=quantms-ddalfq --timeout=180s || true
255-
kubectl get pods -n openms -l app=quantms-ddalfq
256-
kubectl get services -n openms -l app=quantms-ddalfq
268+
kubectl wait -n openms --for=condition=available deployment -l app=${SLUG} --timeout=180s || true
269+
kubectl get pods -n openms -l app=${SLUG}
270+
kubectl get services -n openms -l app=${SLUG}
257271
258272
- name: Curl both hostnames via Traefik
259273
run: |
260274
kubectl -n traefik port-forward svc/traefik 8080:80 &
261275
PF_PID=$!
262276
trap 'kill "$PF_PID" 2>/dev/null || true' EXIT
277+
FIRST_HOST=$(echo ${TRAEFIK_HOSTS} | awk '{print $1}')
263278
for i in $(seq 1 30); do
264279
sleep 2
265-
if curl -fsSo /dev/null --max-time 2 http://127.0.0.1:8080/_stcore/health -H "Host: opendda.webapps.openms.de"; then
280+
if curl -fsSo /dev/null --max-time 2 http://127.0.0.1:8080/_stcore/health -H "Host: ${FIRST_HOST}"; then
266281
break
267282
fi
268283
echo "port-forward / app not ready yet, retry $i"
269284
done
270-
for host in opendda.webapps.openms.de opendda.webapps.openms.org; do
285+
for host in ${TRAEFIK_HOSTS}; do
271286
curl -fsS --resolve "$host:8080:127.0.0.1" "http://$host:8080/_stcore/health"
272287
echo ""
273288
echo "$host -> 200 OK"

docs/kubernetes-deployment.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ Every production OpenMS webapp (quantms-web, umetaflow, FLASHApp) deploys via th
3535
│ Streamlit Deployment │
3636
│ (N replicas, default 2) │
3737
│ │
38-
│ [pod affinity: co-locate with │
39-
rq-worker + cleanup-cronjob pods]
38+
│ [co-located with rq-worker +
39+
│ cleanup pods via shared RWO PVC]
4040
└────────┬────────────────────────┬───────┘
4141
│ REDIS_URL │
4242
│ │ /workspaces-...
@@ -76,9 +76,11 @@ Every production OpenMS webapp (quantms-web, umetaflow, FLASHApp) deploys via th
7676
| Traefik IngressRoute | External HTTP entrypoint with sticky sessions |||
7777
| nginx Ingress | Alternative HTTP entrypoint used by the CI kind cluster |||
7878

79-
### Pod affinity
79+
### Pod co-location via the RWO PVC
8080

81-
All workspace-using pods (Streamlit, RQ worker, Cleanup) carry a `volume-group: workspaces` label and a `requiredDuringSchedulingIgnoredDuringExecution` pod-affinity rule keyed on `kubernetes.io/hostname`. This forces every workspace-using pod onto the same node, so they can share the `ReadWriteOnce` PVC.
81+
All workspace-using pods (Streamlit, RQ worker, Cleanup) of a given fork mount the same `<slug>-workspaces-pvc` (`ReadWriteOnce`, `cinder-csi`). Once the first pod schedules, the volume is attached to that node and the kube-scheduler's `VolumeBinding` plugin pins every subsequent pod that mounts the same PVC to the same node. NodeSelector (`openms.de/memory-tier`) picks which set of nodes the fork is eligible for; the RWO mount picks the specific node within that set.
82+
83+
There is no pod-affinity rule. Forks are isolated from each other — co-location applies within a fork (because they share a PVC), not across forks (each fork has its own PVC).
8284

8385
Co-location is a placement constraint, not a replica cap. The Streamlit deployment can scale to N replicas — they all land on the same node alongside the worker.
8486

@@ -130,14 +132,14 @@ Main Streamlit Deployment. Key fields:
130132
- Mounts the workspace PVC at `/workspaces-streamlit-template`
131133
- Mounts `settings-overrides.json` from the ConfigMap as a `subPath`
132134
- Readiness and liveness probes hit `/_stcore/health`
133-
- Pod affinity: `volume-group: workspaces`
135+
- Co-located with the RQ worker (and any cleanup Job) on the node the RWO `workspaces-pvc` is attached to
134136
- `seed-demos` initContainer merges image-shipped demos into `.demos/` on the PVC (see [Demo workspaces](#demo-workspaces))
135137

136138
### `streamlit-service.yaml`
137139
ClusterIP Service exposing Streamlit on port 8501.
138140

139141
### `rq-worker-deployment.yaml`
140-
RQ worker Deployment (1 replica). Runs `rq worker openms-workflows --url $REDIS_URL`. Shares the workspace PVC via the same `volume-group: workspaces` affinity rule.
142+
RQ worker Deployment (1 replica). Runs `rq worker openms-workflows --url $REDIS_URL`. Shares the workspace PVC, so it co-locates onto the same node as the Streamlit pods via the RWO mount.
141143

142144
### `cleanup-cronjob.yaml`
143145
CronJob that runs `python clean-up-workspaces.py` nightly at 03:00 UTC. Uses `concurrencyPolicy: Forbid`, retains 3 successful and 3 failed jobs. Shares the workspace PVC.

k8s/base/cleanup-cronjob.yaml

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,8 @@ spec:
1515
metadata:
1616
labels:
1717
component: cleanup
18-
volume-group: workspaces
1918
spec:
2019
restartPolicy: OnFailure
21-
affinity:
22-
podAffinity:
23-
requiredDuringSchedulingIgnoredDuringExecution:
24-
- labelSelector:
25-
matchExpressions:
26-
- key: volume-group
27-
operator: In
28-
values:
29-
- workspaces
30-
topologyKey: kubernetes.io/hostname
3120
containers:
3221
- name: cleanup
3322
image: openms-streamlit

k8s/base/rq-worker-deployment.yaml

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,7 @@ spec:
1313
metadata:
1414
labels:
1515
component: rq-worker
16-
volume-group: workspaces
1716
spec:
18-
affinity:
19-
podAffinity:
20-
requiredDuringSchedulingIgnoredDuringExecution:
21-
- labelSelector:
22-
matchExpressions:
23-
- key: volume-group
24-
operator: In
25-
values:
26-
- workspaces
27-
topologyKey: kubernetes.io/hostname
2817
containers:
2918
- name: rq-worker
3019
image: openms-streamlit

k8s/base/streamlit-deployment.yaml

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,7 @@ spec:
1313
metadata:
1414
labels:
1515
component: streamlit
16-
volume-group: workspaces
1716
spec:
18-
affinity:
19-
podAffinity:
20-
requiredDuringSchedulingIgnoredDuringExecution:
21-
- labelSelector:
22-
matchExpressions:
23-
- key: volume-group
24-
operator: In
25-
values:
26-
- workspaces
27-
topologyKey: kubernetes.io/hostname
2817
initContainers:
2918
- name: seed-demos
3019
image: openms-streamlit

0 commit comments

Comments
 (0)