Rebrand: Sentry -> Cairn OBS

Full rebrand across cosmetic branding, code identifiers, and
infrastructure/data-plane naming, using the supplied Cairn OBS logo
package. Cosmetic: favicon/logo swap (also closes a stale license-audit
finding -- the old favicon was SvelteKit's unreplaced scaffold logo),
new centered welcome landing page, larger/legible sidebar logo, page
titles, CLAUDE.md/README/docs prose.

Code identifiers: Go module path github.com/sentry/sentry ->
github.com/cairnobs/cairnobs across all 13 modules and ~91 files (protoc
regenerated); Rust crates sentry-agent/sentry-parser/sentry-search ->
cairnobs-*; CLI sentryctl -> cairnobsctl; Terraform provider fully
renamed (sentry_dashboard etc. -> cairnobs_dashboard, provider type,
env vars); every session/auth cookie name; agent config paths and
Windows service identity.

Deliberately preserved: the gRPC wire protocol's protobuf packages
(sentry.logs.v1, sentry.agent.v1) and their Go import directory
(proto/sentry/...) -- renaming the wire-level package would break every
currently-deployed agent binary (confirmed two real hosts, including
mail.inbuxa.com, are actively streaming through this exact contract)
until rebuilt and redeployed in lockstep with an ingest cutover. Only
the Go module path wrapping the generated code changes.

Infrastructure: every docker-compose container name (root and three
component-level compose files); the Helm chart (directory, Chart.yaml,
named-template helpers, all templates, values.yaml image repos);
Kubernetes Operator (CRD group sentry.io -> cairnobs.io, both CRD YAML
files, Go identifiers, RBAC markers); the coupled enterprise/tenantcrd
package. Caught and fixed real path-coupling bugs along the way: the
Helm chart's search/ingest volume mounts and the dev-only-credential
detection constant vs. docker-compose.yml's literal values had to move
together or a security warning would have silently stopped firing.

Data plane: Postgres database sentry_metadata -> cairnobs_metadata and
role sentry -> cairnobs; ClickHouse database sentry -> cairnobs; Kafka
topic sentry.logs.raw -> cairnobs.logs.raw and its consumer groups.
Source-level defaults, docker-compose.yml, and every migrate.sh/
provision script default updated together; already-applied migration
files left untouched per this repo's immutable-migration convention.

Verified at every layer: all 13 Go modules build/vet/test clean, both
Rust workspaces (agent, search) build/clippy/test clean, npm run check/
build clean, docker compose config validates on all four compose files.
Live-verified against a real docker stack multiple times through this
work, including a final fresh-volume run confirming the actual renamed
Postgres database/role, ClickHouse database, and Kafka topic all work
end to end with a real login and query, zero console errors.
This commit is contained in:
2026-08-21 20:53:32 -07:00
parent 9e21ea17bb
commit 13cf9a30cb
291 changed files with 1565 additions and 1441 deletions
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v2
name: cairnobs
description: >-
Cairn OBS: Kubernetes-native distributed log aggregation. Phase 4 adds
multi-tenant-aware deployment (per-tenant ClickHouse credential Secrets
via the tenant-operator, optional enterprise-auth) on top of the same
single-cluster topology Phases 0-3 ran under docker-compose -- see
deploy/README.md for what "multi-tenant-aware" does and doesn't mean
here.
type: application
version: 0.4.0
appVersion: "phase-4"
+142
View File
@@ -0,0 +1,142 @@
# deploy/helm/cairnobs
A Helm chart covering every `docker-compose.yml` service (Redpanda,
ClickHouse, Postgres, ingest, search, alerting, web) plus, when
`enterprise.enabled: true`: enterprise-auth, the `deploy/operator`
tenant-operator, and `Tenant` CRs from `values.tenants`. See
`/deploy/README.md` for what "multi-tenant-aware" does and doesn't mean
at this layer, and its verification-status section before trusting this
against a real cluster.
This chart never builds images -- push every image its `values.yaml`
references to a registry the cluster can pull from first, same division
of labor as `docker compose build` vs. `docker compose up`.
## `api` vs `enterprise-api`: one Deployment, chosen by `enterprise.enabled`
`templates/api.yaml` and `templates/enterprise-api.yaml` are mutually
exclusive, gated on opposite sides of the same `enterprise.enabled` flag
-- exactly one of them ever renders, both under the same
`{{ .Release.Name }}-api` Service name and port 8080. This is the fix
for what `/docs/security/threat-model.md` named as Phase 4's single
largest remaining gap once both storage engines' isolation mechanisms
were built: previously nothing forced or even flagged whether a
deployment ran the tenant-isolated binary. Now it's not a second knob to
remember -- the same flag that turns on RBAC/audit/SSO also swaps which
query binary actually serves `/query` and `/dashboards` traffic. Every
consumer (`alerting`'s `API_QUERY_URL`, `web`'s build args) needs zero
conditional logic of its own, since both variants answer on the same
name/port.
`enterprise-api` starts with an empty tenant set until
`-provision-tenant` has been run for at least one tenant (see
`/enterprise/README.md`) -- until then it's up and healthy, but every
`/query` request correctly fails closed with no tenant to route to.
## Startup ordering
`docker-compose.yml` uses `depends_on: condition: service_healthy` /
`service_completed_successfully` to sequence startup (e.g. `api` waits
for `clickhouse-migrate` to actually finish, not just for `clickhouse` to
be reachable). This chart approximates that more loosely:
- Migration Jobs (`clickhouse-migrate`, `metadata-migrate`,
`redpanda-provision`) are plain `Job` resources (not Helm hooks --
making the StatefulSets they depend on into hooks too, to get
ordering, would break `helm upgrade`/`helm uninstall`'s normal
ownership tracking of stateful resources, a worse tradeoff), with
`backoffLimit: 6` so they retry a few times if their dependency isn't
up yet.
- App Deployments get an `initContainer` that busy-waits for their
dependency's **TCP port**, not for a specific Job's completion (see
`templates/_helpers.tpl`'s `cairnobs.waitForTCP`) -- this covers "is
ClickHouse/Postgres/Redpanda up" but not "has the migration Job
actually finished."
- The gap that leaves (a pod starts before its migration has completed)
is covered by every Go service here already calling `os.Exit(1)` on a
failed startup DB ping (see e.g. `api/cmd/api/main.go`) --
Kubernetes' pod restart policy retries with backoff until the schema
is ready. This is a real, working, but *looser* guarantee than
docker-compose's explicit ordering -- documented here rather than
implied to be equivalent.
## Trying the two-tenant example
```sh
# Quote each --set value -- zsh globs an unquoted tenants[0] as a
# pattern and fails with "no matches found." Also note: no --include-crds
# here -- that's a helm template-only flag (install always installs
# crds/ by default); confirmed the hard way running this against a real
# kind cluster, see /docs/phase-4-runbook.md §7.
helm install cairnobs . \
--set enterprise.enabled=true \
--set tenantOperator.enabled=true \
--set 'tenants[0].name=acme' --set 'tenants[0].displayName=Acme Corp' \
--set 'tenants[1].name=globex' --set 'tenants[1].displayName=Globex Corporation'
kubectl get tenants
# expect: both Provisioning -- the Tenant CRs above are just a
# declarative request; nothing has actually provisioned ClickHouse for
# either yet (see below).
kubectl exec -it deploy/cairnobs-api -- /enterprise-api -provision-tenant=acme -display-name="Acme Corp"
kubectl exec -it deploy/cairnobs-api -- /enterprise-api -provision-tenant=globex -display-name="Globex Corporation"
kubectl get tenants
# expect: both Active now.
kubectl get secret cairnobs-tenant-acme-clickhouse cairnobs-tenant-globex-clickhouse
```
Before any of this: `ingest` needs a real mTLS cert Secret
(`--set ingest.tlsSecretName=...`, see `values.yaml`'s comment on it and
`/docs/phase-4-runbook.md` §7 for the exact `kubectl create secret`
invocation using `hack/dev-certs`) or it crash-loops on startup --
unconditional by design, no disable switch.
**Genuinely run against a real `kind` cluster, not just described**: see
`/docs/phase-4-runbook.md` §7 for the exact steps (image loading into
`kind`, the two chart bugs it found and fixed) and confirmation that
both tenants reached `status.phase: Active` with real credentials.
This proves Phase 4's "two tenants... with their own users, roles,
dashboards" exit criteria (`/CLAUDE.md`) end to end at the deployment-
topology layer: `-provision-tenant` (`enterprise/internal/
tenantprovision`) is what actually creates each tenant's ClickHouse
database/user/grant and marks it active in `rbacstore`; running inside
the `enterprise-api` Deployment's Pod means it automatically syncs that
real result into the `Tenant` CRD too (`enterprise/internal/tenantcrd`,
via the ServiceAccount/Role `tenantOperator.enabled` also grants that
Deployment) -- the credential Secret you see above has *real*
credentials, not a placeholder, and `Tenant.status.phase: Active` means
the same thing `rbacstore.tenants.status='active'` does, not two
different claims about two different systems. The `Tenant` CRD and
`-provision-tenant` used to be genuinely disconnected (a Secret existed
the moment the CR was created, with a password that authenticated
against nothing) -- see `/deploy/README.md`'s "lightweight unification"
section for the full history. OIDC/SAML login still needs a manual
`tenant_memberships` grant (`enterprise-auth -grant-membership-*` --
see `/docs/phase-4-runbook.md` §3a/§3b) before a human can actually
query as either tenant.
## `web`'s image needs rebuilding per environment
`web` is a static SvelteKit build (`adapter-static`) -- its three API
base URLs (`VITE_API_BASE_URL`/`VITE_ALERTING_API_BASE_URL`/
`VITE_ENTERPRISE_AUTH_BASE_URL`) are baked in at **image build time**
(`web/Dockerfile`'s build args), not read from the container's
environment at runtime. `values.yaml`'s `web.builtWithApiBaseURL` etc.
document what the image you point `web.image` at needs to have been
built with (an Ingress hostname, a LoadBalancer IP, etc.) -- this chart
has no Ingress resources and can't itself act on those values; rebuild
`web`'s image with the right build args for wherever this release is
actually reachable from a browser before pointing real users at it.
## Validating without a cluster
```sh
helm lint .
helm template cairnobs . --include-crds > /tmp/rendered.yaml
```
See `/deploy/README.md`'s verification section for what was checked
this way versus against a real cluster (now done -- see above).
@@ -0,0 +1,93 @@
# Hand-written, not `controller-gen crd` output -- see
# api/v1alpha1/groupversion_info.go's doc comment. Kept in sync with
# api/v1alpha1/tenant_types.go by hand; api/v1alpha1/api_test.go's
# round-trip tests catch a Go/YAML drift in the *shape* of the types,
# but not a drift in this file's field descriptions/validation rules --
# review both together when either changes.
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: tenants.cairnobs.io
spec:
group: cairnobs.io
names:
kind: Tenant
listKind: TenantList
plural: tenants
singular: tenant
scope: Namespaced
versions:
- name: v1alpha1
served: true
storage: true
subresources:
status: {}
additionalPrinterColumns:
- name: Phase
type: string
jsonPath: .status.phase
- name: Age
type: date
jsonPath: .metadata.creationTimestamp
schema:
openAPIV3Schema:
type: object
description: >-
Tenant is the K8s-native representation of one Sentry tenant's
deployment-topology state -- see
deploy/operator/internal/controller/tenant_controller.go's doc
comment for what the controller does and does not manage.
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
spec:
type: object
required: [displayName]
properties:
displayName:
type: string
description: Human-readable only -- the object's own metadata.name is the stable identifier.
suspended:
type: boolean
description: Admin-facing lever for the Suspended phase.
default: false
status:
type: object
properties:
phase:
type: string
enum: [Provisioning, Active, Suspended, Deprovisioning]
clickHouseDatabaseName:
type: string
clickHouseSecretRef:
type: string
tantivyIndexPath:
type: string
observedGeneration:
type: integer
format: int64
conditions:
type: array
items:
type: object
required: [type, status]
properties:
type:
type: string
status:
type: string
enum: ["True", "False", "Unknown"]
reason:
type: string
message:
type: string
observedGeneration:
type: integer
format: int64
lastTransitionTime:
type: string
format: date-time
@@ -0,0 +1,55 @@
{{/*
Standard labels applied to every resource this chart renders.
*/}}
{{- define "cairnobs.labels" -}}
app.kubernetes.io/part-of: cairnobs
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
{{- end -}}
{{/*
Per-component selector labels -- usage:
{{ include "cairnobs.selectorLabels" (list $ "api") }}
A plain string arg (the old shape this started with) can't reach
$.Release from inside the defined template -- `include`'s argument
becomes the template's entire root context, so a bare "api" string
leaves no way to get back to the chart root. A two-element list carries
both.
*/}}
{{- define "cairnobs.selectorLabels" -}}
{{- $root := index . 0 -}}
{{- $name := index . 1 -}}
app.kubernetes.io/name: cairnobs-{{ $name }}
app.kubernetes.io/instance: {{ $root.Release.Name }}
{{- end -}}
{{/*
An initContainer that busy-waits for a TCP host:port to accept
connections -- usage: {{ include "cairnobs.waitForTCP" (list "name-suffix" "host" "port") }}
This approximates docker-compose.yml's `depends_on: condition:
service_healthy` (waits for the dependency's process to be reachable),
but NOT `condition: service_completed_successfully` (waits for a
one-shot Job, like clickhouse-migrate, to have actually finished). That
second guarantee doesn't have a lightweight equivalent here without
giving every app pod's ServiceAccount RBAC to read Job status, which is
a lot of privilege for a startup-ordering nicety -- see
deploy/helm/cairnobs/README.md's "Startup ordering" section. The gap it
leaves (a pod starts before its migration Job has finished) is covered
by the app's own crash-and-restart-on-connect/schema failure: every Go
service here already os.Exit(1)s on a failed Postgres/ClickHouse ping at
startup (see e.g. api/cmd/api/main.go), so Kubernetes' restart policy
naturally retries until the schema is ready. Documented as a real,
accepted tradeoff, not implied to be a hard ordering guarantee.
*/}}
{{- define "cairnobs.waitForTCP" -}}
{{- $name := index . 0 -}}
{{- $host := index . 1 -}}
{{- $port := index . 2 -}}
- name: wait-for-{{ $name }}
image: busybox:1.36
command:
- sh
- -c
- until nc -z -w2 {{ $host }} {{ $port }}; do echo "waiting for {{ $host }}:{{ $port }}"; sleep 2; done
{{- end -}}
@@ -0,0 +1,81 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-alerting
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "alerting") | nindent 4 }}
spec:
# See values.yaml's comment: replicas is not a real knob here yet.
replicas: {{ .Values.alerting.replicas }}
selector:
matchLabels:
{{- include "cairnobs.selectorLabels" (list $ "alerting") | nindent 6 }}
template:
metadata:
labels:
{{- include "cairnobs.selectorLabels" (list $ "alerting") | nindent 8 }}
spec:
initContainers:
{{- include "cairnobs.waitForTCP" (list "postgres" (printf "%s-postgres" .Release.Name) "5432") | nindent 8 }}
{{- include "cairnobs.waitForTCP" (list "api" (printf "%s-api" .Release.Name) "8080") | nindent 8 }}
containers:
- name: alerting
image: "{{ .Values.alerting.image.repository }}:{{ .Values.alerting.image.tag }}"
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
env:
- 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
- name: API_QUERY_URL
value: "http://{{ .Release.Name }}-api:8080"
{{- if .Values.enterprise.enabled }}
# RoleService credential for POST /query, once api's
# ENTERPRISE_AUTH_URL enforcement is on -- see
# /docs/phase-4-isolation-design.md's alerting<->api gap and
# alerting/internal/queryclient's doc comment. NOT generated
# by this chart: mint one with
# `enterprise-auth -mint-service-token=alerting` (see
# enterprise/README.md) and supply it via
# --set-string alerting.apiServiceToken=... or a values
# override backed by a Secret you manage -- a chart
# generating its own long-lived service credential and
# storing it in the same release's values would defeat the
# point of it being a distinct, revocable credential.
{{- if .Values.alerting.apiServiceToken }}
- name: API_SERVICE_TOKEN
value: {{ .Values.alerting.apiServiceToken | quote }}
{{- end }}
{{- end }}
ports:
- name: http
containerPort: 8081
readinessProbe:
exec:
command: ["/alerting", "-healthcheck"]
initialDelaySeconds: 5
periodSeconds: 5
resources:
{{- toYaml .Values.alerting.resources | nindent 12 }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-alerting
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "alerting") | nindent 4 }}
spec:
selector:
{{- include "cairnobs.selectorLabels" (list $ "alerting") | nindent 4 }}
ports:
- name: http
port: 8081
+86
View File
@@ -0,0 +1,86 @@
{{/*
Mutually exclusive with enterprise-api.yaml's Deployment+Service, gated
the opposite way -- see that file's doc comment for why: "does a
deployment run the tenant-isolated binary or not" should be a single
values.yaml decision (enterprise.enabled), not two independently
driftable ones. Both render a 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.
*/}}
{{- if not .Values.enterprise.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-api
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "api") | nindent 4 }}
spec:
replicas: {{ .Values.api.replicas }}
selector:
matchLabels:
{{- include "cairnobs.selectorLabels" (list $ "api") | nindent 6 }}
template:
metadata:
labels:
{{- include "cairnobs.selectorLabels" (list $ "api") | nindent 8 }}
spec:
initContainers:
{{- include "cairnobs.waitForTCP" (list "clickhouse" (printf "%s-clickhouse" .Release.Name) "9000") | nindent 8 }}
{{- include "cairnobs.waitForTCP" (list "postgres" (printf "%s-postgres" .Release.Name) "5432") | nindent 8 }}
{{- include "cairnobs.waitForTCP" (list "search" (printf "%s-search" .Release.Name) "50052") | nindent 8 }}
containers:
- name: api
image: "{{ .Values.api.image.repository }}:{{ .Values.api.image.tag }}"
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
env:
- name: CLICKHOUSE_ADDR
value: "{{ .Release.Name }}-clickhouse:9000"
- name: CLICKHOUSE_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
# No ENTERPRISE_AUTH_URL here -- this file only renders when
# enterprise.enabled is false (see the top of this file), so
# authz.RequireRole*/RequireRoleOrService stay a permanent
# no-op for this Deployment. enterprise-api.yaml is where
# that enforcement actually turns on.
ports:
- name: http
containerPort: 8080
readinessProbe:
exec:
command: ["/api", "-healthcheck"]
initialDelaySeconds: 5
periodSeconds: 5
resources:
{{- toYaml .Values.api.resources | nindent 12 }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-api
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "api") | nindent 4 }}
spec:
selector:
{{- include "cairnobs.selectorLabels" (list $ "api") | nindent 4 }}
ports:
- name: http
port: 8080
{{- end }}
@@ -0,0 +1,113 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ .Release.Name }}-clickhouse
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "clickhouse") | nindent 4 }}
spec:
serviceName: {{ .Release.Name }}-clickhouse
replicas: 1
selector:
matchLabels:
{{- include "cairnobs.selectorLabels" (list $ "clickhouse") | nindent 6 }}
template:
metadata:
labels:
{{- include "cairnobs.selectorLabels" (list $ "clickhouse") | nindent 8 }}
spec:
containers:
- name: clickhouse
image: "{{ .Values.clickhouse.image.repository }}:{{ .Values.clickhouse.image.tag }}"
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
env:
# Required to avoid the official image's network lockdown of
# the implicit `default` user -- see values.yaml's comment on
# this password and docker-compose.yml's original.
- name: CLICKHOUSE_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-clickhouse
key: password
# Same reasoning as docker-compose.yml's identical setting --
# enterprise/internal/tenantprovision needs CREATE USER/GRANT
# on this admin connection, which the official image's
# default user doesn't have without this. Confirmed the hard
# way: -provision-tenant failed with "Not enough
# privileges... grant CREATE USER ON *.*" against a real kind
# cluster before this was added -- this chart had never
# actually been exercised against a live cluster before that.
- name: CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT
value: "1"
ports:
- name: http
containerPort: 8123
- name: native
containerPort: 9000
volumeMounts:
- name: data
mountPath: /var/lib/clickhouse
readinessProbe:
httpGet:
path: /ping
port: http
initialDelaySeconds: 5
periodSeconds: 5
resources:
{{- toYaml .Values.clickhouse.resources | nindent 12 }}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: {{ .Values.clickhouse.persistence.size }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-clickhouse
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "clickhouse") | nindent 4 }}
spec:
clusterIP: None
selector:
{{- include "cairnobs.selectorLabels" (list $ "clickhouse") | nindent 4 }}
ports:
- name: http
port: 8123
- name: native
port: 9000
---
# One-shot: applies /storage/migrations/*.sql -- same image
# storage/Dockerfile builds for docker-compose.yml's clickhouse-migrate
# service. Plain Job, not a Helm hook -- see redpanda.yaml's comment and
# deploy/helm/cairnobs/README.md's "Startup ordering" section.
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-clickhouse-migrate
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
spec:
backoffLimit: 6
template:
metadata:
labels:
{{- include "cairnobs.selectorLabels" (list $ "clickhouse-migrate") | nindent 8 }}
spec:
restartPolicy: OnFailure
containers:
- name: clickhouse-migrate
image: "{{ .Values.clickhouse.migrateImage.repository }}:{{ .Values.clickhouse.migrateImage.tag }}"
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
env:
- name: CLICKHOUSE_HTTP
value: "http://{{ .Release.Name }}-clickhouse:8123"
- name: CLICKHOUSE_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-clickhouse
key: password
@@ -0,0 +1,172 @@
{{/*
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 }}
{{- if .Values.tenantOperator.enabled }}
# Grants enterprise-api's -provision-tenant (enterprise/internal/
# tenantcrd) permission to sync real provisioning results into the
# Tenant CRD -- a Role, not a ClusterRole (unlike tenant-operator's:
# this binary only ever provisions tenants that live in its own release
# namespace, no reason to widen it), scoped to exactly the two resource
# types tenantcrd.Syncer touches. Only rendered when tenantOperator is
# also enabled -- no Tenant CRD installed, nothing to sync into.
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ .Release.Name }}-enterprise-api
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ .Release.Name }}-enterprise-api
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
rules:
- apiGroups: ["cairnobs.io"]
resources: ["tenants"]
verbs: ["get", "list", "create"]
- apiGroups: ["cairnobs.io"]
resources: ["tenants/status"]
verbs: ["get", "update", "patch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "create", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ .Release.Name }}-enterprise-api
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ .Release.Name }}-enterprise-api
subjects:
- kind: ServiceAccount
name: {{ .Release.Name }}-enterprise-api
namespace: {{ .Release.Namespace }}
---
{{- end }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-api
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "api") | nindent 4 }}
app.kubernetes.io/component: enterprise-api
spec:
replicas: {{ .Values.api.replicas }}
selector:
matchLabels:
{{- include "cairnobs.selectorLabels" (list $ "api") | nindent 6 }}
template:
metadata:
labels:
{{- include "cairnobs.selectorLabels" (list $ "api") | nindent 8 }}
spec:
{{- if .Values.tenantOperator.enabled }}
serviceAccountName: {{ .Release.Name }}-enterprise-api
{{- end }}
initContainers:
{{- include "cairnobs.waitForTCP" (list "clickhouse" (printf "%s-clickhouse" .Release.Name) "9000") | nindent 8 }}
{{- include "cairnobs.waitForTCP" (list "postgres" (printf "%s-postgres" .Release.Name) "5432") | nindent 8 }}
{{- include "cairnobs.waitForTCP" (list "search" (printf "%s-search" .Release.Name) "50052") | nindent 8 }}
{{- include "cairnobs.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"
{{- if .Values.tenantOperator.enabled }}
# Enables enterprise/internal/tenantcrd -- -provision-tenant
# (run via `kubectl exec` into this Deployment's Pod, using
# its ServiceAccount/Role above) syncs real provisioning
# results into the Tenant CRD this namespace's tenants live
# in. Unset (the default, when tenantOperator isn't enabled)
# is a documented no-op -- see apiconfig.Config.TenantCRDNamespace.
- name: TENANT_CRD_NAMESPACE
value: {{ .Release.Namespace | quote }}
{{- end }}
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 "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "api") | nindent 4 }}
spec:
selector:
{{- include "cairnobs.selectorLabels" (list $ "api") | nindent 4 }}
ports:
- name: http
port: 8080
{{- end }}
@@ -0,0 +1,87 @@
{{- if .Values.enterprise.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-enterprise-auth
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "enterprise-auth") | nindent 4 }}
spec:
replicas: {{ .Values.enterprise.replicas }}
selector:
matchLabels:
{{- include "cairnobs.selectorLabels" (list $ "enterprise-auth") | nindent 6 }}
template:
metadata:
labels:
{{- include "cairnobs.selectorLabels" (list $ "enterprise-auth") | nindent 8 }}
spec:
initContainers:
{{- include "cairnobs.waitForTCP" (list "postgres" (printf "%s-postgres" .Release.Name) "5432") | nindent 8 }}
containers:
- name: enterprise-auth
image: "{{ .Values.enterprise.image.repository }}:{{ .Values.enterprise.image.tag }}"
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
env:
- name: ENTERPRISE_SESSION_SIGNING_KEY
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-enterprise-auth
key: sessionSigningKey
- 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
{{- if .Values.enterprise.oidc.issuerURL }}
- name: OIDC_ISSUER_URL
value: {{ .Values.enterprise.oidc.issuerURL | quote }}
- name: OIDC_CLIENT_ID
value: {{ .Values.enterprise.oidc.clientID | quote }}
- name: OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-enterprise-auth
key: oidcClientSecret
- name: OIDC_REDIRECT_URL
value: {{ .Values.enterprise.oidc.redirectURL | quote }}
{{- end }}
{{- if .Values.enterprise.saml.idpMetadataURL }}
- name: SAML_ENTITY_ID
value: {{ .Values.enterprise.saml.entityID | quote }}
- name: SAML_ACS_URL
value: {{ .Values.enterprise.saml.acsURL | quote }}
- name: SAML_IDP_METADATA_URL
value: {{ .Values.enterprise.saml.idpMetadataURL | quote }}
{{- end }}
ports:
- name: http
containerPort: 8082
readinessProbe:
exec:
command: ["/enterprise-auth", "-healthcheck"]
initialDelaySeconds: 5
periodSeconds: 5
resources:
{{- toYaml .Values.enterprise.resources | nindent 12 }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-enterprise-auth
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "enterprise-auth") | nindent 4 }}
spec:
selector:
{{- include "cairnobs.selectorLabels" (list $ "enterprise-auth") | nindent 4 }}
ports:
- name: http
port: 8082
{{- end }}
@@ -0,0 +1,64 @@
{{/*
Only rendered once per-tenant write-routing is actually turned on (see
ingest.yaml's -mode=server comment) -- this Deployment is what takes
over consuming sentry.logs.raw once ingest.yaml's own consumer half
stops, routing each record to its own tenant's dedicated ClickHouse
database (enterprise/internal/chwriter) instead of the one shared table.
No Service: this is a pure background worker, nothing calls it, only
kubelet's own probes talk to its /healthz.
*/}}
{{- if and .Values.enterprise.enabled .Values.ingest.requireTenantCredential }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-enterprise-ingest
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "enterprise-ingest") | nindent 4 }}
spec:
replicas: {{ .Values.ingest.replicas }}
selector:
matchLabels:
{{- include "cairnobs.selectorLabels" (list $ "enterprise-ingest") | nindent 6 }}
template:
metadata:
labels:
{{- include "cairnobs.selectorLabels" (list $ "enterprise-ingest") | nindent 8 }}
spec:
initContainers:
{{- include "cairnobs.waitForTCP" (list "redpanda" (printf "%s-redpanda" .Release.Name) "9092") | nindent 8 }}
{{- include "cairnobs.waitForTCP" (list "clickhouse" (printf "%s-clickhouse" .Release.Name) "9000") | nindent 8 }}
{{- include "cairnobs.waitForTCP" (list "postgres" (printf "%s-postgres" .Release.Name) "5432") | nindent 8 }}
containers:
- name: enterprise-ingest
image: "{{ .Values.enterprise.ingestImage.repository }}:{{ .Values.enterprise.ingestImage.tag }}"
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
env:
- name: REDPANDA_BROKERS
value: "{{ .Release.Name }}-redpanda:9092"
- name: CLICKHOUSE_ADDR
value: "{{ .Release.Name }}-clickhouse:9000"
- 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
readinessProbe:
exec:
command: ["/enterprise-ingest", "-healthcheck"]
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
exec:
command: ["/enterprise-ingest", "-healthcheck"]
initialDelaySeconds: 10
periodSeconds: 10
resources:
{{- toYaml .Values.ingest.resources | nindent 12 }}
{{- end }}
@@ -0,0 +1,95 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-ingest
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "ingest") | nindent 4 }}
spec:
replicas: {{ .Values.ingest.replicas }}
selector:
matchLabels:
{{- include "cairnobs.selectorLabels" (list $ "ingest") | nindent 6 }}
template:
metadata:
labels:
{{- include "cairnobs.selectorLabels" (list $ "ingest") | nindent 8 }}
spec:
initContainers:
{{- include "cairnobs.waitForTCP" (list "redpanda" (printf "%s-redpanda" .Release.Name) "9092") | nindent 8 }}
{{- include "cairnobs.waitForTCP" (list "clickhouse" (printf "%s-clickhouse" .Release.Name) "9000") | nindent 8 }}
containers:
- name: ingest
image: "{{ .Values.ingest.image.repository }}:{{ .Values.ingest.image.tag }}"
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
{{- if and .Values.enterprise.enabled .Values.ingest.requireTenantCredential }}
# -mode=server only: this Deployment stops running the
# ClickHouse-writing consumer half (the default -mode=all)
# once per-tenant write-routing is on -- enterprise-ingest.yaml
# (below) takes over consuming sentry.logs.raw instead, so it
# can write each tenant's records to their own database rather
# than the one shared table ingest's own consumer always
# writes to. The agent-facing server half (PushBatch, tenant
# tagging via TenantResolver) keeps running here unconditionally
# either way -- only which process consumes the topic changes.
args: ["-mode=server"]
{{- end }}
env:
- name: REDPANDA_BROKERS
value: "{{ .Release.Name }}-redpanda:9092"
- name: CLICKHOUSE_ADDR
value: "{{ .Release.Name }}-clickhouse:9000"
- name: CLICKHOUSE_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-clickhouse
key: password
{{- if and .Values.enterprise.enabled .Values.ingest.requireTenantCredential }}
# Enables ingest/internal/grpcserver.TenantResolver.
# Deliberately its OWN opt-in, not folded into
# enterprise.enabled directly (same reasoning
# api.yaml/enterprise-api.yaml's ENTERPRISE_AUTH_URL isn't
# set just because enterprise.enabled is true -- see that
# env var's own comment there): turning this on requires
# every agent to already present a valid `Authorization:
# Bearer <ingest token>` (minted via `enterprise-auth
# -create-ingest-credential-tenant=<id>`) or be refused
# outright, which would silently break ingest for any
# not-yet-reconfigured agent if it defaulted on alongside
# enterprise.enabled. Off (the default) leaves every record
# without a tenant_id header, same as every Phase 0-3
# deployment.
- name: ENTERPRISE_AUTH_URL
value: "http://{{ .Release.Name }}-enterprise-auth:8082"
{{- end }}
ports:
- name: grpc
containerPort: 4317
{{- if .Values.ingest.tlsSecretName }}
volumeMounts:
- name: tls
mountPath: /etc/cairnobs-ingest
readOnly: true
{{- end }}
resources:
{{- toYaml .Values.ingest.resources | nindent 12 }}
{{- if .Values.ingest.tlsSecretName }}
volumes:
- name: tls
secret:
secretName: {{ .Values.ingest.tlsSecretName }}
{{- end }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-ingest
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "ingest") | nindent 4 }}
spec:
selector:
{{- include "cairnobs.selectorLabels" (list $ "ingest") | nindent 4 }}
ports:
- name: grpc
port: 4317
@@ -0,0 +1,110 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ .Release.Name }}-postgres
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "postgres") | nindent 4 }}
spec:
serviceName: {{ .Release.Name }}-postgres
replicas: 1
selector:
matchLabels:
{{- include "cairnobs.selectorLabels" (list $ "postgres") | nindent 6 }}
template:
metadata:
labels:
{{- include "cairnobs.selectorLabels" (list $ "postgres") | nindent 8 }}
spec:
containers:
- name: postgres
image: "{{ .Values.postgres.image.repository }}:{{ .Values.postgres.image.tag }}"
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
env:
- name: POSTGRES_DB
value: sentry_metadata
- name: POSTGRES_USER
value: sentry
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-postgres
key: password
ports:
- name: postgres
containerPort: 5432
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
readinessProbe:
exec:
command: ["pg_isready", "-U", "sentry", "-d", "sentry_metadata"]
initialDelaySeconds: 5
periodSeconds: 5
resources:
{{- toYaml .Values.postgres.resources | nindent 12 }}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: {{ .Values.postgres.persistence.size }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-postgres
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "postgres") | nindent 4 }}
spec:
clusterIP: None
selector:
{{- include "cairnobs.selectorLabels" (list $ "postgres") | nindent 4 }}
ports:
- name: postgres
port: 5432
---
# One-shot: applies /metadata/migrations/*.sql (including Phase 4's
# tenants/users/tenant_memberships/audit_log schema) -- same image
# metadata/Dockerfile builds for docker-compose.yml's metadata-migrate
# service. Plain Job, not a Helm hook -- see redpanda.yaml's comment.
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-metadata-migrate
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
spec:
backoffLimit: 6
template:
metadata:
labels:
{{- include "cairnobs.selectorLabels" (list $ "metadata-migrate") | nindent 8 }}
spec:
restartPolicy: OnFailure
containers:
- name: metadata-migrate
image: "{{ .Values.postgres.migrateImage.repository }}:{{ .Values.postgres.migrateImage.tag }}"
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
env:
- name: POSTGRES_HOST
value: "{{ .Release.Name }}-postgres"
- name: POSTGRES_PORT
value: "5432"
- name: POSTGRES_USER
value: sentry
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-postgres
key: password
- name: POSTGRES_DATABASE
value: sentry_metadata
- name: AUDIT_WRITER_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-postgres
key: auditWriterPassword
@@ -0,0 +1,108 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ .Release.Name }}-redpanda
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "redpanda") | nindent 4 }}
spec:
serviceName: {{ .Release.Name }}-redpanda
replicas: 1
selector:
matchLabels:
{{- include "cairnobs.selectorLabels" (list $ "redpanda") | nindent 6 }}
template:
metadata:
labels:
{{- include "cairnobs.selectorLabels" (list $ "redpanda") | nindent 8 }}
spec:
containers:
- name: redpanda
image: "{{ .Values.redpanda.image.repository }}:{{ .Values.redpanda.image.tag }}"
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
args:
- redpanda
- start
- --smp=1
- --memory=1G
- --reserve-memory=0M
- --overprovisioned
- --node-id=0
- --check=false
- --kafka-addr=PLAINTEXT://0.0.0.0:9092
- --advertise-kafka-addr=PLAINTEXT://{{ .Release.Name }}-redpanda:9092
ports:
- name: kafka
containerPort: 9092
- name: admin
containerPort: 9644
volumeMounts:
- name: data
mountPath: /var/lib/redpanda/data
readinessProbe:
exec:
command: ["rpk", "cluster", "health", "--exit-when-healthy"]
initialDelaySeconds: 5
periodSeconds: 5
resources:
{{- toYaml .Values.redpanda.resources | nindent 12 }}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: {{ .Values.redpanda.persistence.size }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-redpanda
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "redpanda") | nindent 4 }}
spec:
clusterIP: None
selector:
{{- include "cairnobs.selectorLabels" (list $ "redpanda") | nindent 4 }}
ports:
- name: kafka
port: 9092
- name: admin
port: 9644
---
# One-shot: creates the sentry.logs.raw topic. Same image
# transport/Dockerfile builds for docker-compose.yml's redpanda-provision
# service. Deliberately a plain Job, not a Helm hook -- see
# deploy/helm/cairnobs/README.md's "Startup ordering" section for why
# (StatefulSets-as-hooks breaks helm upgrade/uninstall's ownership
# tracking of stateful resources). backoffLimit gives it room to retry
# until redpanda's StatefulSet is actually ready; ingest/search's own
# crash-and-restart-on-connect-failure covers the rest of the ordering,
# same as every dependency in this chart.
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-redpanda-provision
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
spec:
backoffLimit: 6
template:
metadata:
labels:
{{- include "cairnobs.selectorLabels" (list $ "redpanda-provision") | nindent 8 }}
spec:
restartPolicy: OnFailure
containers:
- name: redpanda-provision
image: "{{ .Values.redpanda.provisionImage.repository }}:{{ .Values.redpanda.provisionImage.tag }}"
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
env:
- name: REDPANDA_BROKERS
value: "{{ .Release.Name }}-redpanda:9092"
- name: REDPANDA_ADMIN_HOSTS
value: "{{ .Release.Name }}-redpanda:9644"
- name: REDPANDA_TOPIC_PARTITIONS
value: "6"
@@ -0,0 +1,71 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-search
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "search") | nindent 4 }}
spec:
# See values.yaml's comment: replicas is not a real knob here yet.
replicas: {{ .Values.search.replicas }}
strategy:
type: Recreate # single PVC below (ReadWriteOnce) -- avoid two pods racing to mount it during a rollout
selector:
matchLabels:
{{- include "cairnobs.selectorLabels" (list $ "search") | nindent 6 }}
template:
metadata:
labels:
{{- include "cairnobs.selectorLabels" (list $ "search") | nindent 8 }}
spec:
initContainers:
{{- include "cairnobs.waitForTCP" (list "redpanda" (printf "%s-redpanda" .Release.Name) "9092") | nindent 8 }}
containers:
- name: search
image: "{{ .Values.search.image.repository }}:{{ .Values.search.image.tag }}"
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
env:
- name: REDPANDA_BROKERS
value: "{{ .Release.Name }}-redpanda:9092"
- name: REDPANDA_TOPIC_PARTITIONS
value: "6"
- name: RUST_LOG
value: "info"
ports:
- name: grpc
containerPort: 50052
volumeMounts:
- name: index-data
mountPath: /var/lib/cairnobs-search
resources:
{{- toYaml .Values.search.resources | nindent 12 }}
volumes:
- name: index-data
persistentVolumeClaim:
claimName: {{ .Release.Name }}-search-index
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ .Release.Name }}-search-index
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: {{ .Values.search.persistence.size }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-search
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "search") | nindent 4 }}
spec:
selector:
{{- include "cairnobs.selectorLabels" (list $ "search") | nindent 4 }}
ports:
- name: grpc
port: 50052
@@ -0,0 +1,81 @@
{{/*
Shared control-plane secrets -- the cluster-wide passwords
docker-compose.yml hardcodes as "cairnobs-dev-only"/etc (see its
clickhouse/metadata-postgres/metadata-migrate comments) become real
generated-or-supplied Secrets here. Each follows the same pattern: a
values override wins if set, otherwise a value is generated once and
kept stable across `helm upgrade` via `lookup` (so upgrades don't
silently rotate a live credential out from under a running Deployment --
same "never rotate a live credential without coordinating the
consumer-side change" reasoning as
deploy/operator/internal/controller/tenant_controller.go's
reconcileSecret). `lookup` returns nothing under `helm template`
(no live cluster) -- expected; see deploy/README.md's verification
section for what that means for this file specifically.
*/}}
{{- define "cairnobs.stableSecretValue" -}}
{{- $ns := index . 0 -}}
{{- $name := index . 1 -}}
{{- $key := index . 2 -}}
{{- $override := index . 3 -}}
{{- $existing := lookup "v1" "Secret" $ns $name -}}
{{- if $override -}}
{{ $override }}
{{- else if $existing -}}
{{ index $existing.data $key | b64dec }}
{{- else -}}
{{ randAlphaNum 40 }}
{{- end -}}
{{- end -}}
---
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name }}-clickhouse
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
type: Opaque
stringData:
# The official clickhouse-server image locks down *network* access
# entirely for the implicit `default` user unless this is genuinely
# non-empty -- see docker-compose.yml's clickhouse service comment.
# Not a substitute for task 2's per-tenant credentials (still unbuilt
# -- see deploy/operator's Tenant controller); this is the shared
# admin/migration credential only.
password: {{ include "cairnobs.stableSecretValue" (list .Release.Namespace (printf "%s-clickhouse" .Release.Name) "password" .Values.clickhouse.password) }}
---
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name }}-postgres
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
type: Opaque
stringData:
password: {{ include "cairnobs.stableSecretValue" (list .Release.Namespace (printf "%s-postgres" .Release.Name) "password" .Values.postgres.password) }}
# Restricted audit_writer Postgres role (Phase 4 task 4) -- INSERT+SELECT
# only, via its own pool, never the shared role above. See
# /docs/phase-4-isolation-design.md's audit-logging section and
# metadata/README.md.
auditWriterPassword: {{ include "cairnobs.stableSecretValue" (list .Release.Namespace (printf "%s-postgres" .Release.Name) "auditWriterPassword" .Values.postgres.auditWriterPassword) }}
{{- if .Values.enterprise.enabled }}
---
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name }}-enterprise-auth
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
type: Opaque
stringData:
# Must be >= 32 bytes -- see enterprise/internal/config.Load and
# enterprise/internal/session.MinSigningKeyBytes. Rotating this
# invalidates every outstanding session/service token -- same
# "don't rotate a live credential silently" reasoning as above,
# which is why it's kept stable via the lookup above rather than
# regenerated on every `helm upgrade`.
sessionSigningKey: {{ include "cairnobs.stableSecretValue" (list .Release.Namespace (printf "%s-enterprise-auth" .Release.Name) "sessionSigningKey" .Values.enterprise.sessionSigningKey) }}
{{- if .Values.enterprise.oidc.clientSecret }}
oidcClientSecret: {{ .Values.enterprise.oidc.clientSecret | quote }}
{{- end }}
{{- end }}
@@ -0,0 +1,93 @@
{{- if and .Values.enterprise.enabled .Values.tenantOperator.enabled }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ .Release.Name }}-tenant-operator
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
---
# ClusterRole, not Role: Tenant is cluster-scoped-CRD-but-namespaced-object
# (see crds/cairnobs.io_tenants.yaml's scope: Namespaced), and this chart
# doesn't assume it's the only namespace the operator might one day watch
# -- narrowed to exactly the two resource types
# deploy/operator/internal/controller/tenant_controller.go's
# +kubebuilder:rbac markers name (tenants, tenants/status), not a
# wildcard grant. No `secrets` permission -- this controller stopped
# managing the ClickHouse credential Secret once enterprise-api
# -provision-tenant took over creating it with real credentials (see
# that controller's doc comment); see enterprise-api.yaml's own
# ServiceAccount/Role for the `secrets` grant that binary needs instead.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ .Release.Name }}-tenant-operator
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
rules:
- apiGroups: ["cairnobs.io"]
resources: ["tenants"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["cairnobs.io"]
resources: ["tenants/status"]
verbs: ["get", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ .Release.Name }}-tenant-operator
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ .Release.Name }}-tenant-operator
subjects:
- kind: ServiceAccount
name: {{ .Release.Name }}-tenant-operator
namespace: {{ .Release.Namespace }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-tenant-operator
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "tenant-operator") | nindent 4 }}
spec:
# One replica -- see deploy/operator/cmd/tenant-operator/main.go's
# comment: no leader election yet, a second replica could
# double-generate a Secret.
replicas: 1
selector:
matchLabels:
{{- include "cairnobs.selectorLabels" (list $ "tenant-operator") | nindent 6 }}
template:
metadata:
labels:
{{- include "cairnobs.selectorLabels" (list $ "tenant-operator") | nindent 8 }}
spec:
serviceAccountName: {{ .Release.Name }}-tenant-operator
containers:
- name: tenant-operator
image: "{{ .Values.tenantOperator.image.repository }}:{{ .Values.tenantOperator.image.tag }}"
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
ports:
- name: metrics
containerPort: 8080
- name: probes
containerPort: 8081
readinessProbe:
httpGet:
path: /readyz
port: probes
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: probes
initialDelaySeconds: 10
periodSeconds: 10
resources:
{{- toYaml .Values.tenantOperator.resources | nindent 12 }}
{{- end }}
@@ -0,0 +1,14 @@
{{- if .Values.enterprise.enabled }}
{{- range .Values.tenants }}
---
apiVersion: cairnobs.io/v1alpha1
kind: Tenant
metadata:
name: {{ .name }}
labels:
{{- include "cairnobs.labels" $ | nindent 4 }}
spec:
displayName: {{ .displayName | default .name | quote }}
suspended: {{ .suspended | default false }}
{{- end }}
{{- end }}
+43
View File
@@ -0,0 +1,43 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-web
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "web") | nindent 4 }}
spec:
replicas: {{ .Values.web.replicas }}
selector:
matchLabels:
{{- include "cairnobs.selectorLabels" (list $ "web") | nindent 6 }}
template:
metadata:
labels:
{{- include "cairnobs.selectorLabels" (list $ "web") | nindent 8 }}
spec:
containers:
- name: web
# No env vars here -- see values.yaml's web.builtWith* comment:
# this is a static build, its API base URLs are baked into the
# image, not configurable at the Deployment level.
image: "{{ .Values.web.image.repository }}:{{ .Values.web.image.tag }}"
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
ports:
- name: http
containerPort: 3000
resources:
{{- toYaml .Values.web.resources | nindent 12 }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-web
labels:
{{- include "cairnobs.labels" . | nindent 4 }}
{{- include "cairnobs.selectorLabels" (list $ "web") | nindent 4 }}
spec:
selector:
{{- include "cairnobs.selectorLabels" (list $ "web") | nindent 4 }}
ports:
- name: http
port: 3000
+218
View File
@@ -0,0 +1,218 @@
# Default values for the cairnobs chart. See deploy/helm/cairnobs/README.md
# for the multi-tenant-specific values (enterprise.*, tenants) and what
# "multi-tenant-aware" does and doesn't mean at this layer.
#
# Image repositories default to locally-built tags matching each
# service's docker-compose.yml container_name, minus the "cairnobs-"
# container_name prefix duplication -- push these to a registry this
# cluster can actually pull from before installing; this chart never
# builds images itself (same division of labor as docker-compose.yml:
# `docker compose build` vs. `docker compose up`).
global:
imagePullPolicy: IfNotPresent
redpanda:
image:
repository: docker.redpanda.com/redpandadata/redpanda
tag: v24.2.7
persistence:
size: 10Gi
resources: {}
# Built from ./transport (docker-compose.yml's redpanda-provision
# service) -- the one-shot topic-creation Job below.
provisionImage:
repository: cairnobs-redpanda-provision
tag: latest
clickhouse:
image:
repository: clickhouse/clickhouse-server
tag: "24.8"
persistence:
size: 20Gi
resources: {}
# Leave empty to auto-generate and persist across upgrades -- see
# templates/secrets.yaml's stableSecretValue helper.
password: ""
# Built from ./storage (docker-compose.yml's clickhouse-migrate
# service) -- the one-shot schema-migration Job.
migrateImage:
repository: cairnobs-clickhouse-migrate
tag: latest
postgres:
image:
repository: postgres
tag: 16-alpine
persistence:
size: 10Gi
resources: {}
password: ""
auditWriterPassword: ""
# Built from ./metadata (docker-compose.yml's metadata-migrate
# service) -- the one-shot schema-migration Job.
migrateImage:
repository: cairnobs-metadata-migrate
tag: latest
ingest:
image:
repository: cairnobs-ingest
tag: latest
replicas: 1
resources: {}
# mTLS server cert/key/CA -- see hack/dev-certs/generate.sh for the
# dev equivalent of what this Secret must contain
# (server.pem/server-key.pem/ca.pem) in a real deployment. Unlike
# docker-compose.yml's bind-mounted ./hack/dev-certs/out, a cluster
# deployment supplies this as a real Secret -- named here, not
# generated by this chart (cert issuance is out of scope, same
# "boring, well-understood" preference as everywhere else in this
# repo -- use cert-manager or an equivalent, don't hand-roll it here).
tlsSecretName: ""
# Only meaningful when enterprise.enabled is also true -- see
# templates/ingest.yaml's ENTERPRISE_AUTH_URL comment for why this is
# its own deliberate opt-in, not folded into enterprise.enabled
# directly: turning it on requires every agent to already present a
# valid ingest credential (`enterprise-auth
# -create-ingest-credential-tenant=<id>`) or be refused outright. Also
# controls per-tenant write-routing: true switches ingest.yaml's
# Deployment to -mode=server only and renders enterprise-ingest.yaml
# to take over consuming sentry.logs.raw, writing each tenant's
# records into their own ClickHouse database instead of the one
# shared table -- both flags gate together since write-routing is only
# meaningful once records actually carry a tenant_id to route on.
requireTenantCredential: false
search:
image:
repository: cairnobs-search
tag: latest
# Pinned to 1: search consumes the same Redpanda topic ingest's
# consumer does with its own offset tracking (see search/README.md).
# A second replica would form a second, independent consumer instance
# against the same partitions with no coordination -- correctness,
# not just resource waste, is the reason this isn't a `replicas` knob
# yet. Matches CLAUDE.md's Phase 4 non-goal: "no general multi-cluster
# orchestration."
replicas: 1
resources: {}
persistence:
size: 20Gi
api:
image:
repository: cairnobs-api
tag: latest
replicas: 2
resources: {}
alerting:
image:
repository: cairnobs-alerting
tag: latest
# Pinned to 1 for the same reason as search: rulestore.ClaimDueRules
# has no leader-election/partitioning story for multiple evaluator
# replicas yet -- two would both try to claim and evaluate the same
# due rules. Named explicitly rather than silently defaulted, since
# it's the kind of knob someone reasonably expects to just work.
replicas: 1
resources: {}
# See templates/alerting.yaml's comment -- only meaningful when
# enterprise.enabled is true. Empty by default.
apiServiceToken: ""
web:
image:
repository: cairnobs-web
tag: latest
replicas: 2
resources: {}
# NOT wired to any Deployment env var -- web is a static SvelteKit
# build (adapter-static, see web/package.json), and VITE_API_BASE_URL/
# VITE_ALERTING_API_BASE_URL/VITE_ENTERPRISE_AUTH_BASE_URL are baked in
# at *image build time* (docker-compose.yml's web.build.args), not
# read at container runtime. Deploying this chart into a real cluster
# means rebuilding the web image with these three build args pointed
# at wherever api/alerting/enterprise-auth are actually reachable from
# a browser (an Ingress host, a LoadBalancer IP, etc.) -- this section
# exists to document that requirement, not because the chart can act
# on it.
builtWithApiBaseURL: "http://localhost:8080"
builtWithAlertingApiBaseURL: "http://localhost:8081"
builtWithEnterpriseAuthBaseURL: "http://localhost:8082"
# enterprise-auth (commercial license) + the tenant-operator that
# reconciles the Tenant CRD -- both off by default, matching
# docker-compose.yml's own "included, not wired into enforcement by
# default" stance (see its enterprise-auth service comment) and
# enterprise/README.md's "Status" section on what's built vs. deferred.
enterprise:
enabled: false
image:
repository: cairnobs-enterprise-auth
tag: latest
# enterprise-api (templates/enterprise-api.yaml) -- swaps in for
# api.yaml's plain api Deployment when enterprise.enabled is true, on
# the same Service/port every consumer already expects. Built from the
# repo root (needs api/ and proto/, not just enterprise/), unlike
# enterprise-auth's image above -- see
# enterprise/cmd/enterprise-api/Dockerfile.
apiImage:
repository: cairnobs-enterprise-api
tag: latest
# enterprise-ingest (templates/enterprise-ingest.yaml) -- only
# rendered when ingest.requireTenantCredential is also true (see that
# value's comment); takes over consuming sentry.logs.raw from
# ingest.yaml's own consumer once per-tenant write-routing is on. Same
# "repo root build context" reasoning as apiImage above -- see
# enterprise/cmd/enterprise-ingest/Dockerfile.
ingestImage:
repository: cairnobs-enterprise-ingest
tag: latest
replicas: 1
resources: {}
# Leave empty to auto-generate (>= 32 bytes) and persist across
# upgrades -- see templates/secrets.yaml.
sessionSigningKey: ""
oidc:
issuerURL: ""
clientID: ""
clientSecret: ""
redirectURL: ""
saml:
entityID: ""
acsURL: ""
idpMetadataURL: ""
# Installs deploy/operator (the Tenant CRD controller) alongside this
# chart. Only meaningful when enterprise.enabled is also true -- gated
# on that, not a separate flag, since a Tenant CR with nothing to
# reflect (see below) has nothing to do. Also turns on enterprise-api's
# own Tenant-CRD-syncing permissions (a ServiceAccount/Role, and the
# TENANT_CRD_NAMESPACE env var) -- see templates/enterprise-api.yaml.
tenantOperator:
enabled: false
image:
repository: cairnobs-tenant-operator
tag: latest
resources: {}
# One entry per tenant to provision -- rendered as Tenant CRs
# (templates/tenants.yaml), a declarative request an admin/GitOps
# process makes. The operator (tenant-operator, above) only ever
# *reflects* real state onto these objects (Phase/Conditions, derived
# from what enterprise-api's `-provision-tenant` has reported) -- it's
# `-provision-tenant` (run via `kubectl exec` into the enterprise-api
# Pod), not the operator, that actually calls ClickHouse and writes the
# credential Secret. See deploy/operator/internal/controller/
# tenant_controller.go's and enterprise/internal/tenantcrd's doc
# comments for the full split. Empty by default; a real two-tenant
# deployment (Phase 4's exit criteria) sets e.g.:
# tenants:
# - name: acme
# displayName: "Acme Corp"
# - name: globex
# displayName: "Globex Corporation"
tenants: []