From f363ec8db31cafcadc68e1a77f3bbe138703f799 Mon Sep 17 00:00:00 2001 From: Jeremy Eder Date: Fri, 20 Mar 2026 15:31:25 -0400 Subject: [PATCH 1/2] feat: deploy GPS to OpenShift via internal registry Add `image` command to deploy.sh that builds and pushes to the OpenShift internal registry (with IMAGE_REGISTRY env var override for non-admin users who can't query the registry route). Add `IMAGE` support in `cmd_apply` to set the correct internal pull URL. Namespace creation uses `oc new-project` on OpenShift for proper RBAC. - Containerfile: set UV_CACHE_DIR and chmod g+rwX for OpenShift's restricted SCC (arbitrary UID) - Build with --platform=linux/amd64 and --provenance=false for cross-arch compat and registry compatibility - Remove imagePullSecrets via JSON patch in OpenShift overlay (internal registry uses default SA token) - Remove namespace.yaml from kustomize base (deploy.sh handles creation) - Rename all resources from mcp-server to gps-mcp-server - Rename namespace from gps to gps-mcp-server Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/deploy.yml | 12 ++-- Containerfile | 5 ++ deploy/deploy.sh | 70 +++++++++++++++++-- deploy/k8s/base/deployment.yaml | 12 ++-- deploy/k8s/base/kustomization.yaml | 1 - deploy/k8s/base/namespace.yaml | 2 +- deploy/k8s/base/pvcs.yaml | 2 +- deploy/k8s/base/service.yaml | 6 +- .../k8s/overlays/openshift/kustomization.yaml | 5 ++ .../openshift/remove-pull-secret.yaml | 2 + deploy/k8s/overlays/openshift/route.yaml | 6 +- docs/DEPLOYMENT.md | 2 +- 12 files changed, 96 insertions(+), 29 deletions(-) create mode 100644 deploy/k8s/overlays/openshift/remove-pull-secret.yaml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 09cc1fc..01ff0da 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -168,8 +168,8 @@ jobs: --docker-server=quay.io \ --docker-username="${{ secrets.QUAY_USERNAME }}" \ --docker-password="${{ secrets.QUAY_PASSWORD }}" \ - --namespace=gps --dry-run=client -o yaml | oc apply -f - - oc secrets link default quay-pull-secret --for=pull -n gps 2>/dev/null || true + --namespace=gps-mcp-server --dry-run=client -o yaml | oc apply -f - + oc secrets link default quay-pull-secret --for=pull -n gps-mcp-server 2>/dev/null || true - name: Deploy to OpenShift env: @@ -178,10 +178,10 @@ jobs: cd deploy/k8s/overlays/openshift kustomize edit set image "python:3.11-slim=quay.io/ambient_code/gps:${TAG}" kustomize build . > /dev/null - oc apply -k . -n gps - oc rollout status deployment/mcp-server -n gps --timeout=120s + oc apply -k . -n gps-mcp-server + oc rollout status deployment/gps-mcp-server -n gps-mcp-server --timeout=120s - name: Show deployment status run: | - oc get pods -n gps - oc get routes -n gps + oc get pods -n gps-mcp-server + oc get routes -n gps-mcp-server diff --git a/Containerfile b/Containerfile index 9ee275f..c3a65cb 100644 --- a/Containerfile +++ b/Containerfile @@ -3,6 +3,11 @@ WORKDIR /app COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv COPY mcp_server.py VERSION ./ COPY data/gps.db data/DATA_CATALOG.yaml data/ + +# OpenShift runs containers as arbitrary UID — ensure writable dirs +ENV UV_CACHE_DIR=/tmp/uv-cache +RUN chmod -R g+rwX /app + EXPOSE 8000 HEALTHCHECK CMD curl -f http://localhost:8000/health || exit 1 CMD ["uv", "run", "--script", "mcp_server.py", "--http", "--port", "8000"] diff --git a/deploy/deploy.sh b/deploy/deploy.sh index e892340..8145a98 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -3,7 +3,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -NAMESPACE="${NAMESPACE:-gps}" +NAMESPACE="${NAMESPACE:-gps-mcp-server}" OVERLAY="${OVERLAY:-base}" # Detect kubectl or oc @@ -18,14 +18,17 @@ usage() { echo "" echo "Commands:" echo " build Build gps.db and run tests" + echo " image Build and push container image to registry" echo " apply Apply Kubernetes manifests" echo " status Show deployment status" echo " logs Tail MCP server logs" - echo " all build + apply" + echo " all image + build + apply" echo "" echo "Environment:" - echo " NAMESPACE Kubernetes namespace (default: gps)" - echo " OVERLAY Kustomize overlay (default: base, option: openshift)" + echo " NAMESPACE Kubernetes namespace (default: gps-mcp-server)" + echo " OVERLAY Kustomize overlay (default: base, option: openshift)" + echo " IMAGE_REGISTRY Override container registry host" + echo " IMAGE Override full image reference for apply" } # Parse --overlay flag from any position @@ -54,6 +57,31 @@ else KUSTOMIZE_DIR="$SCRIPT_DIR/k8s/base" fi +cmd_image() { + echo "=== Building and pushing container image ===" + + if [[ -n "${IMAGE_REGISTRY:-}" ]]; then + REGISTRY="$IMAGE_REGISTRY" + else + REGISTRY=$($KUBECTL get route default-route -n openshift-image-registry -o jsonpath='{.spec.host}') + fi + + echo "Registry: $REGISTRY" + echo "Namespace: $NAMESPACE" + + echo "=== Logging into registry ===" + docker login "$REGISTRY" -u "$($KUBECTL whoami)" -p "$($KUBECTL whoami -t)" + + local IMAGE_REF="$REGISTRY/$NAMESPACE/gps-mcp-server:latest" + echo "=== Building $IMAGE_REF ===" + docker build --provenance=false --platform=linux/amd64 -f "$REPO_ROOT/Containerfile" -t "$IMAGE_REF" "$REPO_ROOT" + + echo "=== Pushing $IMAGE_REF ===" + docker push "$IMAGE_REF" + + echo "=== Image pushed successfully ===" +} + cmd_build() { echo "=== Building GPS database ===" uv run "$REPO_ROOT/scripts/build_db.py" --force @@ -61,11 +89,38 @@ cmd_build() { } cmd_apply() { + echo "=== Ensuring namespace $NAMESPACE exists ===" + if ! $KUBECTL get namespace "$NAMESPACE" &>/dev/null; then + if [[ "$KUBECTL" == "oc" ]]; then + oc new-project "$NAMESPACE" --display-name="GPS" + else + kubectl create namespace "$NAMESPACE" + fi + fi + echo "=== Applying manifests to $NAMESPACE (overlay: $OVERLAY) ===" + + # Set image override if specified or defaulting for OpenShift + local EFFECTIVE_IMAGE="${IMAGE:-}" + if [[ -z "$EFFECTIVE_IMAGE" && "$OVERLAY" == "openshift" ]]; then + EFFECTIVE_IMAGE="image-registry.openshift-image-registry.svc:5000/$NAMESPACE/gps-mcp-server:latest" + fi + + if [[ -n "$EFFECTIVE_IMAGE" ]]; then + echo "=== Setting image to $EFFECTIVE_IMAGE ===" + cp "$KUSTOMIZE_DIR/kustomization.yaml" "$KUSTOMIZE_DIR/kustomization.yaml.bak" + (cd "$KUSTOMIZE_DIR" && kustomize edit set image "python:3.11-slim=$EFFECTIVE_IMAGE") + fi + $KUBECTL apply -k "$KUSTOMIZE_DIR" + + # Restore original kustomization.yaml + if [[ -n "$EFFECTIVE_IMAGE" ]]; then + mv "$KUSTOMIZE_DIR/kustomization.yaml.bak" "$KUSTOMIZE_DIR/kustomization.yaml" + fi echo "" echo "=== Waiting for rollout ===" - $KUBECTL -n "$NAMESPACE" rollout status deployment/mcp-server --timeout=120s + $KUBECTL -n "$NAMESPACE" rollout status deployment/gps-mcp-server --timeout=120s echo "" cmd_status } @@ -84,14 +139,15 @@ cmd_status() { } cmd_logs() { - $KUBECTL -n "$NAMESPACE" logs -f deployment/mcp-server + $KUBECTL -n "$NAMESPACE" logs -f deployment/gps-mcp-server } case "${1:-}" in build) cmd_build ;; + image) cmd_image ;; apply) cmd_apply ;; status) cmd_status ;; logs) cmd_logs ;; - all) cmd_build && cmd_apply ;; + all) cmd_image && cmd_build && cmd_apply ;; *) usage; exit 1 ;; esac diff --git a/deploy/k8s/base/deployment.yaml b/deploy/k8s/base/deployment.yaml index 9104a16..d2ef3e1 100644 --- a/deploy/k8s/base/deployment.yaml +++ b/deploy/k8s/base/deployment.yaml @@ -1,25 +1,25 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: mcp-server - namespace: gps + name: gps-mcp-server + namespace: gps-mcp-server labels: - app: mcp-server + app: gps-mcp-server app.kubernetes.io/part-of: gps spec: replicas: 1 selector: matchLabels: - app: mcp-server + app: gps-mcp-server template: metadata: labels: - app: mcp-server + app: gps-mcp-server spec: imagePullSecrets: - name: quay-pull-secret containers: - - name: mcp-server + - name: gps-mcp-server image: python:3.11-slim command: ["uv", "run", "mcp_server.py", "--http", "--port", "8000"] ports: diff --git a/deploy/k8s/base/kustomization.yaml b/deploy/k8s/base/kustomization.yaml index af5e5e9..88944eb 100644 --- a/deploy/k8s/base/kustomization.yaml +++ b/deploy/k8s/base/kustomization.yaml @@ -1,7 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - - namespace.yaml - pvcs.yaml - deployment.yaml - service.yaml diff --git a/deploy/k8s/base/namespace.yaml b/deploy/k8s/base/namespace.yaml index 4001ea7..0aebdbd 100644 --- a/deploy/k8s/base/namespace.yaml +++ b/deploy/k8s/base/namespace.yaml @@ -1,6 +1,6 @@ apiVersion: v1 kind: Namespace metadata: - name: gps + name: gps-mcp-server labels: app.kubernetes.io/part-of: gps diff --git a/deploy/k8s/base/pvcs.yaml b/deploy/k8s/base/pvcs.yaml index a377e19..70b8a5a 100644 --- a/deploy/k8s/base/pvcs.yaml +++ b/deploy/k8s/base/pvcs.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: name: gps-data - namespace: gps + namespace: gps-mcp-server spec: accessModes: [ReadWriteOnce] resources: diff --git a/deploy/k8s/base/service.yaml b/deploy/k8s/base/service.yaml index d6698ef..33e9789 100644 --- a/deploy/k8s/base/service.yaml +++ b/deploy/k8s/base/service.yaml @@ -1,11 +1,11 @@ apiVersion: v1 kind: Service metadata: - name: mcp-server - namespace: gps + name: gps-mcp-server + namespace: gps-mcp-server spec: selector: - app: mcp-server + app: gps-mcp-server ports: - port: 8000 targetPort: 8000 diff --git a/deploy/k8s/overlays/openshift/kustomization.yaml b/deploy/k8s/overlays/openshift/kustomization.yaml index 1ed3522..80402aa 100644 --- a/deploy/k8s/overlays/openshift/kustomization.yaml +++ b/deploy/k8s/overlays/openshift/kustomization.yaml @@ -3,3 +3,8 @@ kind: Kustomization resources: - ../../base - route.yaml +patches: + - target: + kind: Deployment + name: gps-mcp-server + path: remove-pull-secret.yaml diff --git a/deploy/k8s/overlays/openshift/remove-pull-secret.yaml b/deploy/k8s/overlays/openshift/remove-pull-secret.yaml new file mode 100644 index 0000000..060f611 --- /dev/null +++ b/deploy/k8s/overlays/openshift/remove-pull-secret.yaml @@ -0,0 +1,2 @@ +- op: remove + path: /spec/template/spec/imagePullSecrets diff --git a/deploy/k8s/overlays/openshift/route.yaml b/deploy/k8s/overlays/openshift/route.yaml index 7583618..4e698ac 100644 --- a/deploy/k8s/overlays/openshift/route.yaml +++ b/deploy/k8s/overlays/openshift/route.yaml @@ -1,13 +1,13 @@ apiVersion: route.openshift.io/v1 kind: Route metadata: - name: mcp-server - namespace: gps + name: gps-mcp-server + namespace: gps-mcp-server spec: tls: termination: edge to: kind: Service - name: mcp-server + name: gps-mcp-server port: targetPort: http diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 0f0d4be..9cc6a9d 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -103,7 +103,7 @@ metadata: name: gps-mcp spec: selector: - app: mcp-server + app: gps-mcp-server ports: - port: 8000 targetPort: 8000 From 35681e4dfcbc50c749337bbbdc263cf17bb4d469 Mon Sep 17 00:00:00 2001 From: Jeremy Eder Date: Fri, 20 Mar 2026 15:47:30 -0400 Subject: [PATCH 2/2] docs: fix stale references, add image/registry docs, update heading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update heading in README.md and CLAUDE.md to include "MCP Server" - Fix stale gps-mcp → gps-mcp-server references in DEPLOYMENT.md - Add docker build flags (--provenance=false --platform=linux/amd64) - Document image command and OpenShift internal registry workflow - Add IMAGE_REGISTRY and IMAGE env vars to Environment section Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/CLAUDE.md | 2 +- README.md | 2 +- docs/DEPLOYMENT.md | 25 ++++++++++++++++++------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index f830535..62c0b57 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -1,4 +1,4 @@ -# GPS - Global Positioning System +# GPS - Global Positioning System MCP Server Read-only caching tier for org and engineering data. Materializes people, teams, issues, features, releases, and governance into a SQLite database. Optimized for sub-millisecond agent queries over MCP — no auth, no rate limits. diff --git a/README.md b/README.md index f08d2b5..c9fe5b9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# GPS - Global Positioning System +# GPS - Global Positioning System MCP Server A read-only caching tier that gives agents and humans sub-millisecond access to org and engineering data. GPS materializes people, teams, issues, features, release schedules, component mappings, and governance documents into a SQLite database and serves them over [MCP](https://modelcontextprotocol.io) — optimized for high-frequency agent queries with zero upstream latency. diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 9cc6a9d..99ed1a8 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -100,7 +100,7 @@ Deploy GPS as a Kubernetes Service. Sessions connect via HTTP — useful when ma apiVersion: v1 kind: Service metadata: - name: gps-mcp + name: gps-mcp-server spec: selector: app: gps-mcp-server @@ -116,7 +116,7 @@ Point sessions at it: "mcpServers": { "gps": { "type": "streamable-http", - "url": "http://gps-mcp.gps.svc:8000/mcp" + "url": "http://gps-mcp-server.gps-mcp-server.svc:8000/mcp" } } } @@ -141,10 +141,10 @@ Build and run: uv run scripts/build_db.py --force # Build container image -docker build -f Containerfile -t gps . +docker build --provenance=false --platform=linux/amd64 -f Containerfile -t gps-mcp-server . # Run -docker run -p 8000:8000 gps +docker run -p 8000:8000 gps-mcp-server ``` The container exposes HTTP transport on port 8000 with a health check at `/health`. @@ -171,6 +171,12 @@ deploy/deploy.sh status # View logs deploy/deploy.sh logs + +# Build and push to OpenShift internal registry +IMAGE_REGISTRY= deploy/deploy.sh image --overlay openshift + +# Full pipeline: build db + push image + deploy +IMAGE_REGISTRY= deploy/deploy.sh all --overlay openshift ``` ### Overlays @@ -186,10 +192,15 @@ deploy/deploy.sh apply --overlay openshift ### Environment -Set `NAMESPACE` to deploy to a specific namespace: +| Variable | Purpose | +|----------|---------| +| `NAMESPACE` | Deploy to a specific namespace | +| `IMAGE_REGISTRY` | Override container registry host (e.g., OpenShift internal registry route) | +| `IMAGE` | Override full image reference for apply | ```bash NAMESPACE=my-gps deploy/deploy.sh apply +IMAGE_REGISTRY=default-route-openshift-image-registry.apps.example.com deploy/deploy.sh image --overlay openshift ``` ## MCP Client Configuration @@ -237,10 +248,10 @@ The `deploy.yml` workflow (manual dispatch only) handles the full release pipeli # Ensure secrets are configured, then: gh workflow run deploy.yml -f bump_type=minor gh run watch -oc get pods -n gps +oc get pods -n gps-mcp-server ``` -The workflow automatically creates a `quay-pull-secret` in the `gps` namespace from the Quay credentials. +The workflow automatically creates a `quay-pull-secret` in the `gps-mcp-server` namespace from the Quay credentials. ## Security Notes