Phase 4: Helm chart enforces api vs enterprise-api, closing the deployment-topology gap

deploy/helm/sentry/templates/api.yaml and the new enterprise-api.yaml
are mutually exclusive, gated on opposite sides of the same
enterprise.enabled flag -- exactly one renders, both as a Deployment+
Service named {{ .Release.Name }}-api on port 8080, so every consumer
(alerting's API_QUERY_URL, web's build args) needs zero conditional
logic of its own. This is the concrete fix for what the threat model
named as the single largest remaining gap once both storage engines'
isolation mechanisms were built: previously nothing forced or flagged
whether a deployment ran the tenant-isolated binary. Now the same flag
that turns on RBAC/audit/SSO also chooses the query binary.

Verified by parsing (not eyeballing) helm template's rendered output
under both value sets: exactly one sentry-api Deployment/Service either
way, with the right image, and kubeconform -strict clean against the
real Kubernetes 1.31 schema. Not applied to a live cluster (still no
cluster in this environment) -- docker-compose.yml also still runs
plain api unconditionally, so this enforcement is Helm-only for now.

Updated the threat model, architecture doc, CLAUDE.md, and deploy/
READMEs to reflect this and to name what's left: ingest has no tenant
concept for either storage engine (undesigned), and the Tenant CRD
(deploy/operator) and enterprise-api -provision-tenant are still two
separate, unreconciled provisioning mechanisms.
This commit is contained in:
2026-08-14 06:20:20 -07:00
parent ba2276aa1a
commit 3037b31b0f
9 changed files with 287 additions and 69 deletions
@@ -0,0 +1,111 @@
{{/*
Mutually exclusive with api.yaml's Deployment+Service -- see that file's
doc comment. This is the concrete fix for the deployment-topology gap
/docs/security/threat-model.md names as the single largest remaining
Phase 4 issue once both storage engines' isolation mechanisms were
built: "nothing forces or flags whether a deployment runs the isolated
binary." With this file, it's not a separate knob to forget -- the same
enterprise.enabled that turns on RBAC/audit/SSO also swaps which query
binary actually serves traffic. Uses the "api" selector label (not
"enterprise-api") deliberately, so the shared Service name+port below
routes to whichever Deployment is actually rendered, with zero
conditional logic needed in any consumer (alerting, web).
*/}}
{{- if .Values.enterprise.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-api
labels:
{{- include "sentry.labels" . | nindent 4 }}
{{- include "sentry.selectorLabels" (list $ "api") | nindent 4 }}
app.kubernetes.io/component: enterprise-api
spec:
replicas: {{ .Values.api.replicas }}
selector:
matchLabels:
{{- include "sentry.selectorLabels" (list $ "api") | nindent 6 }}
template:
metadata:
labels:
{{- include "sentry.selectorLabels" (list $ "api") | nindent 8 }}
spec:
initContainers:
{{- include "sentry.waitForTCP" (list "clickhouse" (printf "%s-clickhouse" .Release.Name) "9000") | nindent 8 }}
{{- include "sentry.waitForTCP" (list "postgres" (printf "%s-postgres" .Release.Name) "5432") | nindent 8 }}
{{- include "sentry.waitForTCP" (list "search" (printf "%s-search" .Release.Name) "50052") | nindent 8 }}
{{- include "sentry.waitForTCP" (list "enterprise-auth" (printf "%s-enterprise-auth" .Release.Name) "8082") | nindent 8 }}
containers:
- name: enterprise-api
image: "{{ .Values.enterprise.apiImage.repository }}:{{ .Values.enterprise.apiImage.tag }}"
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
env:
# :8080, not enterprise-api's own :8083 default -- this
# container occupies the same Service/port every consumer
# (alerting's API_QUERY_URL, web's build args) already
# expects "-api:8080" to mean. See this file's doc comment.
- name: HTTP_LISTEN_ADDR
value: ":8080"
- name: CLICKHOUSE_ADDR
value: "{{ .Release.Name }}-clickhouse:9000"
# tenantprovision's admin connection -- the same credential
# clickhouse-migrate uses, needs access_management, never a
# tenant-scoped grant. See enterprise/internal/tenantprovision's
# doc comment.
- name: CLICKHOUSE_ADMIN_USERNAME
value: "default"
- name: CLICKHOUSE_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-clickhouse
key: password
- name: SEARCH_GRPC_ADDR
value: "{{ .Release.Name }}-search:50052"
- name: POSTGRES_ADDR
value: "{{ .Release.Name }}-postgres:5432"
- name: POSTGRES_DATABASE
value: sentry_metadata
- name: POSTGRES_USERNAME
value: sentry
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-postgres
key: password
# Restricted audit_writer Postgres role (Phase 4 task 4) --
# its own pool, never the shared "sentry" credential above.
# See enterprise/internal/audit's doc comment.
- name: AUDIT_WRITER_USERNAME
value: "audit_writer"
- name: AUDIT_WRITER_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-postgres
key: auditWriterPassword
- name: ENTERPRISE_AUTH_URL
value: "http://{{ .Release.Name }}-enterprise-auth:8082"
ports:
- name: http
containerPort: 8080
readinessProbe:
exec:
command: ["/enterprise-api", "-healthcheck"]
initialDelaySeconds: 5
periodSeconds: 5
resources:
{{- toYaml .Values.api.resources | nindent 12 }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-api
labels:
{{- include "sentry.labels" . | nindent 4 }}
{{- include "sentry.selectorLabels" (list $ "api") | nindent 4 }}
spec:
selector:
{{- include "sentry.selectorLabels" (list $ "api") | nindent 4 }}
ports:
- name: http
port: 8080
{{- end }}