diff --git a/Dockerfile.helm-runner b/Dockerfile.helm-runner new file mode 100644 index 00000000..03eb5c6f --- /dev/null +++ b/Dockerfile.helm-runner @@ -0,0 +1,17 @@ +FROM registry.access.redhat.com/ubi9/ubi-minimal@sha256:ae09ecc3d754bc1726cbda3e2599cc7839e09fe1cc547ce173cf669b645be3cc + +ARG HELM_VERSION=3.15.4 +ARG TARGETARCH=amd64 + +RUN microdnf install -y ca-certificates curl gzip tar && microdnf clean all +RUN curl -fsSLo /tmp/helm.tar.gz "https://get.helm.sh/helm-v${HELM_VERSION}-linux-${TARGETARCH}.tar.gz" \ + && curl -fsSLo /tmp/helm.tar.gz.sha256sum "https://get.helm.sh/helm-v${HELM_VERSION}-linux-${TARGETARCH}.tar.gz.sha256sum" \ + && cd /tmp \ + && sha256sum -c helm.tar.gz.sha256sum \ + && tar -xzf helm.tar.gz \ + && install -m 0755 "linux-${TARGETARCH}/helm" /usr/local/bin/helm \ + && rm -rf /tmp/helm.tar.gz /tmp/helm.tar.gz.sha256sum "/tmp/linux-${TARGETARCH}" + +USER 1001 +WORKDIR /tmp/work +ENTRYPOINT ["/usr/local/bin/helm"] diff --git a/deploy/deploy.sh b/deploy/deploy.sh index aaf40380..7e084601 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -16,7 +16,7 @@ set -euo pipefail DEPLOY_START=$(date +%s) VALUES_FILE="/tmp/pulse-deploy-values-$$.yaml" -trap 'rm -f /tmp/pulse-ui-build.log /tmp/pulse-ui-push.log "$VALUES_FILE"' EXIT +trap 'rm -f /tmp/pulse-ui-build.log /tmp/pulse-ui-push.log /tmp/pulse-helm-runner.digest "$VALUES_FILE"' EXIT # ─── Configuration ─────────────────────────────────────────────────────────── @@ -26,6 +26,8 @@ NAMESPACE="openshiftpulse" RELEASE="pulse" UI_IMAGE="${PULSE_UI_IMAGE:-quay.io/amobrem/openshiftpulse}" AGENT_IMAGE="${PULSE_AGENT_IMAGE:-quay.io/amobrem/pulse-agent}" +HELM_RUNNER_IMAGE="${PULSE_HELM_RUNNER_IMAGE:-quay.io/amobrem/pulse-helm-runner}" +HELM_RUNNER_IMAGE_REF="${PULSE_HELM_RUNNER_IMAGE_REF:-}" UI_TAG="" AGENT_TAG="" _WS_TOKEN_OVERRIDE="${PULSE_AGENT_WS_TOKEN:-}" @@ -252,6 +254,10 @@ fi if [[ -z "$AGENT_TAG" ]]; then AGENT_TAG=$(git_tag "$AGENT_REPO") fi +if [[ -n "$HELM_RUNNER_IMAGE_REF" && ! "$HELM_RUNNER_IMAGE_REF" =~ ^[^[:space:]@]+@sha256:[a-fA-F0-9]{64}$ ]]; then + error "PULSE_HELM_RUNNER_IMAGE_REF must be an immutable digest reference (repo@sha256:...)" + exit 1 +fi info "UI tag: $UI_TAG" info "Agent tag: $AGENT_TAG" @@ -305,6 +311,7 @@ if [[ "$DRY_RUN" == "true" ]]; then echo " Namespace: $NAMESPACE" echo " UI image: ${UI_IMAGE}:${UI_TAG}" echo " Agent image: ${AGENT_IMAGE}:${AGENT_TAG}" + echo " Helm runner: ${HELM_RUNNER_IMAGE_REF:-${HELM_RUNNER_IMAGE}:${UI_TAG} (digest resolved after push)}" if [[ -n "${ANTHROPIC_VERTEX_PROJECT_ID:-}" ]]; then echo " AI backend: Vertex AI (${ANTHROPIC_VERTEX_PROJECT_ID} / ${CLOUD_ML_REGION:-us-east5})" elif [[ -n "${ANTHROPIC_API_KEY:-}" ]]; then @@ -333,10 +340,15 @@ fi pnpm run build info "UI built (dist/)" - info "Building UI and Agent images in parallel..." + info "Building UI, Agent, and Helm runner images..." podman build --platform linux/amd64 -t "${UI_IMAGE}:${UI_TAG}" "$PROJECT_DIR" &>/tmp/pulse-ui-build.log & UI_BUILD_PID=$! + if [[ -z "$HELM_RUNNER_IMAGE_REF" ]]; then + podman build --platform linux/amd64 -t "${HELM_RUNNER_IMAGE}:${UI_TAG}" -f "$PROJECT_DIR/Dockerfile.helm-runner" "$PROJECT_DIR" + info "Helm runner image built" + fi + cd "$AGENT_REPO" # Default Dockerfile is the full single-stage build. # Use Dockerfile.fast only when the pre-built deps image is available in-cluster. @@ -355,6 +367,7 @@ fi info "Pushing images..." podman tag "${UI_IMAGE}:${UI_TAG}" "${UI_IMAGE}:latest" podman tag "${AGENT_IMAGE}:${AGENT_TAG}" "${AGENT_IMAGE}:latest" + [[ -z "$HELM_RUNNER_IMAGE_REF" ]] && podman tag "${HELM_RUNNER_IMAGE}:${UI_TAG}" "${HELM_RUNNER_IMAGE}:latest" podman push "${UI_IMAGE}:${UI_TAG}" &>/tmp/pulse-ui-push.log & UI_PUSH_PID=$! @@ -363,6 +376,15 @@ fi podman push "${AGENT_IMAGE}:latest" info "Pushed ${AGENT_IMAGE}:${AGENT_TAG} + latest" + if [[ -z "$HELM_RUNNER_IMAGE_REF" ]]; then + podman push --digestfile /tmp/pulse-helm-runner.digest "${HELM_RUNNER_IMAGE}:${UI_TAG}" + [[ -s /tmp/pulse-helm-runner.digest ]] || { error "Helm runner push did not produce a digest"; exit 1; } + podman push "${HELM_RUNNER_IMAGE}:latest" + HELM_RUNNER_DIGEST=$(cat /tmp/pulse-helm-runner.digest) + HELM_RUNNER_IMAGE_REF="${HELM_RUNNER_IMAGE}@${HELM_RUNNER_DIGEST}" + info "Pushed Helm runner ${HELM_RUNNER_IMAGE_REF}" + fi + if wait $UI_PUSH_PID; then podman push "${UI_IMAGE}:latest" info "Pushed ${UI_IMAGE}:${UI_TAG} + latest" @@ -433,15 +455,22 @@ fi # Generate values file (keeps secrets out of process listings) AI_BACKEND="none" AI_VALUES="" +AI_VALUES_COMPAT="" if [[ -n "${ANTHROPIC_VERTEX_PROJECT_ID:-}" ]]; then AI_VALUES=" vertexAI: projectId: ${ANTHROPIC_VERTEX_PROJECT_ID} region: ${CLOUD_ML_REGION:-us-east5} existingSecret: gcp-sa-key" + AI_VALUES_COMPAT=" vertexAI: + projectId: ${ANTHROPIC_VERTEX_PROJECT_ID} + region: ${CLOUD_ML_REGION:-us-east5} + existingSecret: gcp-sa-key" AI_BACKEND="vertex" elif [[ -n "${ANTHROPIC_API_KEY:-}" ]]; then AI_VALUES=" anthropicApiKey: existingSecret: anthropic-api-key" + AI_VALUES_COMPAT=" anthropicApiKey: + existingSecret: anthropic-api-key" AI_BACKEND="anthropic" fi @@ -474,6 +503,9 @@ openshiftpulse: enabled: $MONITORING_ENABLED alertmanager: enabled: $MONITORING_ENABLED + helmInstall: + runnerImage: "$HELM_RUNNER_IMAGE_REF" + serviceAccountName: openshiftpulse-helm-installer agent: enabled: true serviceName: $AGENT_DEPLOY @@ -487,9 +519,24 @@ agent: rbac: allowWriteOperations: ${AGENT_ALLOW_WRITES:-false} allowSecretAccess: ${AGENT_ALLOW_SECRETS:-false} + mcp: + enabled: false wsAuth: existingSecret: $WS_SECRET $AI_VALUES +openshift-sre-agent: + enabled: true + image: + repository: $AGENT_IMAGE + tag: "$AGENT_TAG" + rbac: + allowWriteOperations: ${AGENT_ALLOW_WRITES:-false} + allowSecretAccess: ${AGENT_ALLOW_SECRETS:-false} + mcp: + enabled: false + wsAuth: + existingSecret: $WS_SECRET +$AI_VALUES_COMPAT YAML chmod 600 "$VALUES_FILE" diff --git a/deploy/helm/openshiftpulse/templates/deployment.yaml b/deploy/helm/openshiftpulse/templates/deployment.yaml index 5b78be1a..950eec4b 100644 --- a/deploy/helm/openshiftpulse/templates/deployment.yaml +++ b/deploy/helm/openshiftpulse/templates/deployment.yaml @@ -114,8 +114,6 @@ spec: echo "Agent WS token: injected at template time" fi {{- end }} - SA_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null || echo "") - sed -i "s|__SA_TOKEN__|${SA_TOKEN}|g" /tmp/nginx.conf exec nginx -c /tmp/nginx.conf -g 'daemon off;' ports: - containerPort: 8080 @@ -123,6 +121,9 @@ spec: - name: nginx-config mountPath: /etc/nginx/nginx.conf subPath: nginx.conf + - name: nginx-config + mountPath: /opt/app-root/src/config.js + subPath: config.js {{- if .Values.agent.enabled }} - name: agent-token mountPath: /etc/nginx/agent-token diff --git a/deploy/helm/openshiftpulse/templates/nginx-config.yaml b/deploy/helm/openshiftpulse/templates/nginx-config.yaml index 8a0f0b4c..15c65bb7 100644 --- a/deploy/helm/openshiftpulse/templates/nginx-config.yaml +++ b/deploy/helm/openshiftpulse/templates/nginx-config.yaml @@ -6,6 +6,13 @@ metadata: labels: {{- include "openshiftpulse.labels" . | nindent 4 }} data: + config.js: | + window.__OPENSHIFTPULSE_CONFIG__ = { + helmInstall: { + runnerImage: {{ .Values.helmInstall.runnerImage | quote }}, + serviceAccountName: {{ .Values.helmInstall.serviceAccountName | quote }} + } + }; nginx.conf: | worker_processes auto; error_log /dev/stderr warn; @@ -44,11 +51,10 @@ data: proxy_pass https://kubernetes.default.svc/; proxy_ssl_verify on; proxy_ssl_trusted_certificate /var/run/secrets/kubernetes.io/serviceaccount/ca.crt; - set $k8s_token $http_x_forwarded_access_token; - if ($k8s_token = '') { - set $k8s_token '__SA_TOKEN__'; + if ($http_x_forwarded_access_token = '') { + return 401 '{"error":"X-Forwarded-Access-Token header is required"}'; } - proxy_set_header Authorization "Bearer $k8s_token"; + proxy_set_header Authorization "Bearer $http_x_forwarded_access_token"; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_http_version 1.1; @@ -60,11 +66,10 @@ data: proxy_pass https://{{ .Values.monitoring.prometheus.host }}/; proxy_ssl_verify on; proxy_ssl_trusted_certificate /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt; - set $prom_token $http_x_forwarded_access_token; - if ($prom_token = '') { - set $prom_token '__SA_TOKEN__'; + if ($http_x_forwarded_access_token = '') { + return 401 '{"error":"X-Forwarded-Access-Token header is required"}'; } - proxy_set_header Authorization "Bearer $prom_token"; + proxy_set_header Authorization "Bearer $http_x_forwarded_access_token"; proxy_read_timeout 60s; } {{- else }} @@ -84,11 +89,10 @@ data: proxy_pass https://{{ .Values.monitoring.alertmanager.host }}/; proxy_ssl_verify on; proxy_ssl_trusted_certificate /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt; - set $am_token $http_x_forwarded_access_token; - if ($am_token = '') { - set $am_token '__SA_TOKEN__'; + if ($http_x_forwarded_access_token = '') { + return 401 '{"error":"X-Forwarded-Access-Token header is required"}'; } - proxy_set_header Authorization "Bearer $am_token"; + proxy_set_header Authorization "Bearer $http_x_forwarded_access_token"; proxy_read_timeout 60s; } {{- else }} diff --git a/deploy/helm/openshiftpulse/values.yaml b/deploy/helm/openshiftpulse/values.yaml index 3a277fc4..e69b4e2f 100644 --- a/deploy/helm/openshiftpulse/values.yaml +++ b/deploy/helm/openshiftpulse/values.yaml @@ -36,6 +36,12 @@ monitoring: selfMonitor: enabled: true +# In-cluster Helm installs use a project-owned runner image. Leave empty to +# fail closed rather than execute a mutable third-party image. +helmInstall: + runnerImage: "" + serviceAccountName: openshiftpulse-helm-installer + # Resource limits resources: app: diff --git a/deploy/helm/pulse/Chart.lock b/deploy/helm/pulse/Chart.lock index d155a254..14535a86 100644 --- a/deploy/helm/pulse/Chart.lock +++ b/deploy/helm/pulse/Chart.lock @@ -4,6 +4,6 @@ dependencies: version: 4.7.0 - name: openshift-sre-agent repository: file://../../../../pulse-agent/chart - version: 2.6.0 -digest: sha256:a9ce7715a3e0c6ae6513674f5de98df4be4d79627d42a96091fe7d620fa69d17 -generated: "2026-04-28T18:09:47.246482-07:00" + version: 1.13.0 +digest: sha256:0408c4a4e57404efbda9a53cf921bb4cde8634b182e14bdb7c87cdc8d760aca5 +generated: "2026-06-09T16:58:30.041978735+02:00" diff --git a/deploy/helm/pulse/Chart.yaml b/deploy/helm/pulse/Chart.yaml index da880374..bead4301 100644 --- a/deploy/helm/pulse/Chart.yaml +++ b/deploy/helm/pulse/Chart.yaml @@ -16,7 +16,7 @@ dependencies: repository: "file://../openshiftpulse" - name: openshift-sre-agent - version: "2.7.1" + version: "1.13.0" repository: "file://../../../../pulse-agent/chart" alias: agent condition: agent.enabled diff --git a/deploy/helm/pulse/charts/openshift-sre-agent-1.13.0.tgz b/deploy/helm/pulse/charts/openshift-sre-agent-1.13.0.tgz new file mode 100644 index 00000000..22272bb4 Binary files /dev/null and b/deploy/helm/pulse/charts/openshift-sre-agent-1.13.0.tgz differ diff --git a/deploy/helm/pulse/charts/openshift-sre-agent-2.6.0.tgz b/deploy/helm/pulse/charts/openshift-sre-agent-2.6.0.tgz deleted file mode 100644 index d1f4bd51..00000000 Binary files a/deploy/helm/pulse/charts/openshift-sre-agent-2.6.0.tgz and /dev/null differ diff --git a/deploy/helm/pulse/charts/openshiftpulse-4.7.0.tgz b/deploy/helm/pulse/charts/openshiftpulse-4.7.0.tgz index 64bc938b..365cb601 100644 Binary files a/deploy/helm/pulse/charts/openshiftpulse-4.7.0.tgz and b/deploy/helm/pulse/charts/openshiftpulse-4.7.0.tgz differ diff --git a/deploy/helm/pulse/values.yaml b/deploy/helm/pulse/values.yaml index be679669..27631caf 100644 --- a/deploy/helm/pulse/values.yaml +++ b/deploy/helm/pulse/values.yaml @@ -24,6 +24,10 @@ openshiftpulse: enabled: true host: alertmanager-main.openshift-monitoring.svc:9094 + helmInstall: + runnerImage: "" + serviceAccountName: openshiftpulse-helm-installer + agent: enabled: true # Auto-derived from release name if empty: -openshift-sre-agent @@ -65,6 +69,9 @@ agent: allowWriteOperations: false allowSecretAccess: false + mcp: + enabled: false + database: type: postgresql postgresql: @@ -79,3 +86,10 @@ agent: wsAuth: existingSecret: "pulse-ws-token" secretKey: "token" + +# Compatibility for stale packaged dependencies that were not aliased as +# `agent`. Keep this until the archived dependency is refreshed everywhere. +openshift-sre-agent: + enabled: true + mcp: + enabled: false diff --git a/deploy/test-helm.sh b/deploy/test-helm.sh index 51cdd1ad..8cee36cc 100755 --- a/deploy/test-helm.sh +++ b/deploy/test-helm.sh @@ -40,7 +40,14 @@ AGENT_VALUES=( --set agent.enabled=true --set agent.image.repository=quay.io/test/agent --set agent.image.tag=test + --set agent.wsAuth.existingSecret=pulse-ws-token --set agent.anthropicApiKey.existingSecret=test-secret + # Compatibility for stale packaged dependencies before helm dependency update. + --set openshift-sre-agent.enabled=true + --set openshift-sre-agent.image.repository=quay.io/test/agent + --set openshift-sre-agent.image.tag=test + --set openshift-sre-agent.wsAuth.existingSecret=pulse-ws-token + --set openshift-sre-agent.anthropicApiKey.existingSecret=test-secret ) # 1. Helm lint @@ -53,8 +60,7 @@ fi # 2. Template renders without errors echo "--- Template rendering ---" -RENDERED=$(helm template pulse "$CHART_DIR" "${COMMON[@]}" "${AGENT_VALUES[@]}" 2>&1) -if [[ $? -eq 0 ]]; then +if RENDERED=$(helm template pulse "$CHART_DIR" "${COMMON[@]}" "${AGENT_VALUES[@]}" 2>&1); then pass "Template renders (agent enabled)" else fail "Template renders (agent enabled)" @@ -118,10 +124,10 @@ else fi # 10. Agent disabled renders without errors -RENDERED_NO_AGENT=$(helm template pulse "$CHART_DIR" "${COMMON[@]}" \ +if RENDERED_NO_AGENT=$(helm template pulse "$CHART_DIR" "${COMMON[@]}" \ --set openshiftpulse.agent.enabled=false \ - --set agent.enabled=false 2>&1) -if [[ $? -eq 0 ]]; then + --set agent.enabled=false \ + --set openshift-sre-agent.enabled=false 2>&1); then pass "Template renders (agent disabled)" else fail "Template renders (agent disabled)" @@ -149,6 +155,32 @@ else fail "RollingUpdate strategy missing" fi +# 14. User token proxy must fail closed instead of falling back to service-account token +if has "$RENDERED" "__SA_TOKEN__"; then + fail "nginx config still contains service-account token fallback" +else + pass "nginx config has no service-account token fallback" +fi + +if has "$RENDERED" "X-Forwarded-Access-Token header is required"; then + pass "nginx returns 401 when forwarded access token is missing" +else + fail "nginx missing-token 401 guard not rendered" +fi + +if has "$RENDERED" "sed -i \"s|__SA_TOKEN__"; then + fail "deployment still injects service-account token into nginx" +else + pass "deployment does not inject service-account token into nginx" +fi + +# 15. MCP server must not render by default +if has "$RENDERED" "app.kubernetes.io/component: mcp-server"; then + fail "MCP server renders by default" +else + pass "MCP server disabled by default" +fi + # Summary echo "" echo "===========================" diff --git a/public/config.js b/public/config.js new file mode 100644 index 00000000..a3a8511b --- /dev/null +++ b/public/config.js @@ -0,0 +1 @@ +window.__OPENSHIFTPULSE_CONFIG__ = window.__OPENSHIFTPULSE_CONFIG__ || {}; diff --git a/public/index.html b/public/index.html index 9b7d1a9c..d7c35261 100644 --- a/public/index.html +++ b/public/index.html @@ -3,9 +3,10 @@ - - OpenShift Pulse - + + OpenShift Pulse + +