Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ High-level, user-facing catalog changes by month: new templates and notable vers
## 2026-08

- **Every template now deploys into a GVC you already have.** Eleven templates used to create their own; none do. This closes a data-loss path: a chart that creates a GVC and later stops declaring it makes `helm upgrade` prune it — and deleting a GVC takes **every workload, volumeset and identity inside**, in about six seconds, while printing `upgraded successfully`. Each converted template now refuses that upgrade at render time. **If you run any 1.x/2.x release listed below, do not `helm upgrade` onto the new major** — install it as a new release against an existing GVC, move your data across, then remove the old release. Each README carries the migration steps
- **cpln-advisor 2.0.0** — no longer creates a GVC. This one mattered most: 1.0.0 named the GVC it created after the one you installed into, so it **adopted your existing GVC** — and `helm uninstall` would then have deleted it along with every unrelated workload in it. Also fixes placement: on a GVC with more than one location, 1.0.0 would have run a second scheduler firing every scan twice and a second, independent database. Note the bundled Postgres still cannot be pinned to one location, so a multi-location GVC gives it one empty database per extra location — harmless today because the service DNS is location-local, but changing `location` later repoints the app at the empty one
- **mongodb-cluster 2.0.0** — no longer creates a GVC, and **the shipped default could never fully work**. It defaulted to nine members, but MongoDB allows at most seven voters: the eighth and ninth joined nothing, ran a healthy-looking `mongod`, reported ready, and were never in the replica set. The default is now one location × three members and a roster above seven is refused. `backup.mode: physical` is **removed** — its restore could never run, because Percona Backup for MongoDB must execute `mongod` and the agent image does not contain it, so it wrote real-looking snapshots that could never be restored and failed silently doing it. The logical restore is verified end to end, and the documented steps used to point at an address that cannot resolve from your machine
- **grafana-multi-location 2.0.0** — no longer creates a GVC, and picks up the converted `postgres-multi-location` and `redis-multi-location`. Losing the location named in `alerting.location` still stops alert evaluation while dashboards look perfectly healthy — set `alerting.highAvailability.enabled: true` if that matters, and note a fresh install now fails loudly rather than half-working
- **airflow 2.0.0** — no longer creates a GVC. **KEDA autoscaling is now opt-in and off by default**, because it is a GVC-level setting a chart cannot turn on for you: enabling it against a GVC without KEDA is accepted silently and leaves the workers dead at zero replicas with no error anywhere. Turn KEDA on for your GVC first, then set `keda.enabled: true`. Also fixes a placement bug where a multi-location GVC would have run one scheduler and one broker **per location**
Expand Down
80 changes: 35 additions & 45 deletions briefings/cpln-advisor.md

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions cpln-advisor/versions/1.0.0/.helmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
7 changes: 4 additions & 3 deletions cpln-advisor/versions/1.0.0/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,20 @@ appVersion: "1.0.0"

annotations:
created: "2026-08-17"
lastModified: "2026-08-17"
lastModified: "2026-09-01"
category: "observability"
createsGvc: true
createsGvc: false

dependencies:
- name: cpln-common
version: 1.0.0
repository: "oci://ghcr.io/controlplane-com/templates"

# The advisor's database. Bundled so a single install brings up everything:
# the chart arrives with a readiness probe, a private firewall, its own
# identity and policy, and scheduled backups behind a switch.
#
# 3.4.1, NOT 3.3.0: from 3.4.0 the template reads its username/password/database
# Version 3.4.1 of the postgres template reads its username/password/database
# from a prerequisite secret instead of taking them inline, which is what lets
# this chart bundle a database and still keep every credential out of values.
- name: postgres
Expand Down
114 changes: 39 additions & 75 deletions cpln-advisor/versions/1.0.0/README.md

Large diffs are not rendered by default.

46 changes: 5 additions & 41 deletions cpln-advisor/versions/1.0.0/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,8 @@ Advisor Policy Name
{{- end }}

{{/*
Bundled Postgres workload name. The `postgres` subchart names it
`{{ .Release.Name }}-postgres`, and as a subchart that Release.Name is OURS — so
this must track the subchart's own helper. A rename there breaks this silently.
*/}}
{{- define "cpln-advisor.postgres.name" -}}
{{- printf "%s-postgres" .Release.Name }}
{{- end }}

{{/*
Internal address of Redis. Plain redis:// is correct — the sidecar adds mTLS.
Internal address of Redis, in the GVC this release is installed into. Plain
redis:// is correct — the sidecar adds mTLS.
*/}}
{{- define "cpln-advisor.redis.url" -}}
{{- printf "redis://%s.%s.cpln.local:6379" (include "cpln-advisor.redis.name" .) .Values.global.cpln.gvc }}
Expand All @@ -72,15 +64,6 @@ Internal address of the API, on the CONTAINER port (8000), not 443.
{{- printf "http://%s.%s.cpln.local:8000" (include "cpln-advisor.api.name" .) .Values.global.cpln.gvc }}
{{- end }}

{{/*
Every credential the advisor reads, by key, out of the ONE prerequisite dictionary
secret. Nothing sensitive passes through values, so nothing sensitive lands in the
Helm release. Key names match the app's own environment variable names.
*/}}
{{- define "cpln-advisor.secretRef" -}}
{{- printf "cpln://secret/%s.%s" .name .key }}
{{- end }}

{{/* Resource ratio guard */}}

{{/*
Expand Down Expand Up @@ -170,18 +153,10 @@ Call with (dict "who" "api" "r" .Values.api.resources).

{{- define "cpln-advisor.validate" -}}
{{- if not .Values.global.cpln.gvc -}}
{{- fail "cpln-advisor: global.cpln.gvc is required — the name of the GVC this chart creates, e.g. 'advisor'. It lives under `global` so cpln-common tags every resource with it, and so a subchart would inherit it." -}}
{{- end -}}
{{- if not .Values.gvc.locations -}}
{{- fail "cpln-advisor: gvc.locations must contain exactly one location, e.g. `locations:` / ` - name: aws-us-east-1`. Run `cpln location get` to list the ones available to your org." -}}
{{- end -}}
{{- if ne (len .Values.gvc.locations) 1 -}}
{{- fail (printf "cpln-advisor: gvc.locations must contain EXACTLY ONE location, got %d. A workload runs in every location of its GVC and minScale/maxScale are per-location, so a second location silently doubles the API, worker and scheduler — a second scheduler would fire every cron twice. The bundled Postgres is a single stateful workload on a read-write-once volume, so a second location would also give it a second, independent database rather than a replica." (len .Values.gvc.locations)) -}}
{{- end -}}
{{- range .Values.gvc.locations -}}
{{- if not .name -}}
{{- fail "cpln-advisor: every entry in gvc.locations needs a `name`, e.g. `- name: aws-us-east-1`" -}}
{{- fail "cpln-advisor: global.cpln.gvc is empty. This chart installs into an EXISTING GVC and does not create one — the install tooling supplies this from the GVC you select (`cpln helm install ... --gvc YOUR_GVC`), so an empty value usually means no GVC was selected. It is read here to build internal service addresses and to tag every resource, and the bundled postgres subchart inherits it." -}}
{{- end -}}
{{- if .Values.gvc -}}
{{- fail "cpln-advisor: the `gvc` values key is no longer used and has been ignored. This chart installs into an EXISTING GVC selected at install time (`--gvc YOUR_GVC`) rather than creating one, so `gvc.locations` no longer controls anything — it is a prerequisite instead: the GVC you install into must already exist and must have exactly ONE location. Remove the `gvc` block from your values file. See README > GVC." -}}
{{- end -}}
{{- if not .Values.auth.secretName -}}
{{- fail "cpln-advisor: auth.secretName is required — the name of a `dictionary` secret that MUST EXIST BEFORE INSTALL, holding the keys ADVISOR_API_TOKEN, ADVISOR_SECRET_KEY, ADVISOR_SESSION_SECRET, ADVISOR_USERNAME, ADVISOR_PASSWORD and DATABASE_URL. This chart creates no secret and accepts no credential as a value. See README Prerequisites." -}}
Expand All @@ -200,21 +175,10 @@ Call with (dict "who" "api" "r" .Values.api.resources).

{{/* Labeling */}}

{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "cpln-advisor.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Common labels
*/}}
{{- define "cpln-advisor.tags" -}}
{{- include "cpln-common.tags" . }}
{{- end }}

{{- define "cpln-advisor.selectorLabels" -}}
app.cpln.io/name: {{ .Release.Name }}
app.cpln.io/instance: {{ .Release.Name }}
{{- end }}
22 changes: 0 additions & 22 deletions cpln-advisor/versions/1.0.0/templates/gvc.yaml

This file was deleted.

2 changes: 1 addition & 1 deletion cpln-advisor/versions/1.0.0/templates/identity.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
---
{{- include "cpln-advisor.validate" . -}}
kind: identity
gvc: {{ .Values.global.cpln.gvc }}
name: {{ include "cpln-advisor.identity.name" . }}
Expand Down
5 changes: 1 addition & 4 deletions cpln-advisor/versions/1.0.0/templates/policy.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
---
{{- include "cpln-advisor.validate" . -}}
kind: policy
name: {{ include "cpln-advisor.policy.name" . }}
description: CPLN Advisor policy
tags: {{- include "cpln-advisor.tags" . | nindent 4 }}
bindings:
# `reveal`, NOT `view` — view exposes metadata only, and the cpln://secret/…
# reference then resolves to an empty string. The containers start happily and
# behave as though nothing were configured, so this failure is silent.
- permissions:
- reveal
principalLinks:
Expand Down
31 changes: 10 additions & 21 deletions cpln-advisor/versions/1.0.0/templates/workload-api.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
---
{{- include "cpln-advisor.validate" . -}}
kind: workload
name: {{ include "cpln-advisor.api.name" . }}
description: CPLN Advisor API
Expand All @@ -25,29 +25,12 @@ spec:
- -c
- alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port 8000
env:
# Nothing here may be named CPLN_* — that prefix is RESERVED and a
# workload setting one is rejected. The platform injects CPLN_ORG,
# CPLN_GVC, CPLN_TOKEN and friends itself, which is where the app reads
# its org from. The Control Plane API token cannot be injected for the
# same reason: set it once on the Configuration page, where it is stored
# encrypted in the database.
- name: LOG_LEVEL
value: {{ .Values.logLevel | quote }}
- name: REDIS_URL
value: {{ include "cpln-advisor.redis.url" . | quote }}
# Postgres, from the bundled `postgres` subchart. The URL carries the
# password, so it is a key in YOUR credentials secret rather than a value
# here. Its username/password/database must match the database's own
# prerequisite secret — nothing cross-checks the two.
- name: DATABASE_URL
value: cpln://secret/{{ .Values.auth.secretName }}.DATABASE_URL
# The app derives the dashboard's public URL — the Slack "View in Advisor"
# links and the CORS origin — from Control Plane's built-in env vars, as
# `https://{ADVISOR_WEB_WORKLOAD}-{gvc alias}.cpln.app`. It defaults to
# the literal "web", so it MUST be told this chart's actual dashboard
# workload name or every derived link points at a workload that does not
# exist. It is NOT built from CPLN_GLOBAL_ENDPOINT, which is the host of
# whichever workload reads it — internal here, and not where a browser goes.
- name: ADVISOR_WEB_WORKLOAD
value: {{ include "cpln-advisor.web.name" . | quote }}
{{- if .Values.appUrl }}
Expand Down Expand Up @@ -81,30 +64,36 @@ spec:
httpGet:
path: /health # the only unauthenticated route, and it leaks nothing
port: 8000
scheme: HTTP
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
successThreshold: 1
timeoutSeconds: 2
livenessProbe:
httpGet:
path: /health
port: 8000
scheme: HTTP
initialDelaySeconds: 20
periodSeconds: 30
failureThreshold: 3
successThreshold: 1
timeoutSeconds: 2
defaultOptions:
capacityAI: true
timeoutSeconds: 120 # a scan's Control Plane and LLM calls can take a while
autoscaling:
maxConcurrency: 0
metric: disabled
minScale: 1
# ONE replica, because two would race on the same startup migration. That
# is now the only reason — Postgres handles concurrent writers fine — so
# raising this is a question of moving `alembic upgrade head` out of the
# container start command, not of the database.
maxScale: 1
# Non-root. `filesystemGroupId` is gone with the shared volume it existed for.
# (Never 1337: that is the mesh proxy's UID, and a container running as it
# bypasses the Envoy redirect.)
scaleToZeroDelay: 300
target: 95
securityOptions:
runAsUser: 10001
firewallConfig:
Expand Down
8 changes: 7 additions & 1 deletion cpln-advisor/versions/1.0.0/templates/workload-redis.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
---
{{- include "cpln-advisor.validate" . -}}
kind: workload
name: {{ include "cpln-advisor.redis.name" . }}
description: Task broker and cache for the advisor
Expand Down Expand Up @@ -40,12 +40,18 @@ spec:
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
successThreshold: 1
timeoutSeconds: 2
defaultOptions:
capacityAI: false # a broker should not be resized under load
timeoutSeconds: 5
autoscaling:
maxConcurrency: 0
metric: disabled
minScale: 1
maxScale: 1 # ONE replica: two brokers means tasks split between them
scaleToZeroDelay: 300
target: 95
firewallConfig:
external:
inboundAllowCIDR: [] # no inbound from the internet
Expand Down
9 changes: 5 additions & 4 deletions cpln-advisor/versions/1.0.0/templates/workload-scheduler.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
---
{{- include "cpln-advisor.validate" . -}}
kind: workload
name: {{ include "cpln-advisor.scheduler.name" . }}
description: CPLN Advisor cron scheduler (fires scheduled scans and retention)
Expand Down Expand Up @@ -48,13 +48,14 @@ spec:
value: cpln://secret/{{ .Values.auth.secretName }}.ADVISOR_SECRET_KEY
defaultOptions:
capacityAI: true # the scheduler only enqueues; let it be sized down
timeoutSeconds: 5
autoscaling:
maxConcurrency: 0
scaleToZeroDelay: 300
target: 95
metric: disabled
minScale: 1
maxScale: 1 # two schedulers would fire every cron twice
# Non-root. `filesystemGroupId` is gone with the shared volume it existed for.
# (Never 1337: that is the mesh proxy's UID, and a container running as it
# bypasses the Envoy redirect.)
securityOptions:
runAsUser: 10001
firewallConfig:
Expand Down
6 changes: 5 additions & 1 deletion cpln-advisor/versions/1.0.0/templates/workload-web.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
---
{{- include "cpln-advisor.validate" . -}}
kind: workload
name: {{ include "cpln-advisor.web.name" . }}
description: CPLN Advisor dashboard
Expand Down Expand Up @@ -45,9 +45,12 @@ spec:
httpGet:
path: /login # the one page that renders without a session
port: 3000
scheme: HTTP
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
successThreshold: 1
timeoutSeconds: 2
defaultOptions:
capacityAI: true
timeoutSeconds: 120 # the proxy waits on the API, and a scan is not quick
Expand All @@ -57,6 +60,7 @@ spec:
minScale: {{ .Values.web.replicas.min }}
maxScale: {{ .Values.web.replicas.max }}
scaleToZeroDelay: 300
maxConcurrency: 0
firewallConfig:
external:
# PUBLIC, deliberately and unconditionally: the dashboard is the only way
Expand Down
9 changes: 5 additions & 4 deletions cpln-advisor/versions/1.0.0/templates/workload-worker.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
---
{{- include "cpln-advisor.validate" . -}}
kind: workload
name: {{ include "cpln-advisor.worker.name" . }}
description: CPLN Advisor background worker
Expand Down Expand Up @@ -58,6 +58,7 @@ spec:
value: cpln://secret/{{ .Values.auth.secretName }}.ADVISOR_SECRET_KEY
defaultOptions:
capacityAI: false # a long scan should not be resized underneath itself
timeoutSeconds: 5
autoscaling:
metric: disabled
minScale: 1
Expand All @@ -66,9 +67,9 @@ spec:
# LLM calls are rate-limited upstream, and a second worker mostly buys
# more 429s. Raise it deliberately, not by default.
maxScale: 1
# Non-root. `filesystemGroupId` is gone with the shared volume it existed for.
# (Never 1337: that is the mesh proxy's UID, and a container running as it
# bypasses the Envoy redirect.)
maxConcurrency: 0
scaleToZeroDelay: 300
target: 95
securityOptions:
runAsUser: 10001
firewallConfig:
Expand Down
Loading
Loading