Repository navigation
Conversation
The console shows everyone's usage, but with no issuer configured it let every caller in as the same viewer, and that was the default whenever the NebariApp was off. An issuer without a client ID also started, accepting a token issued to any client in the realm. The server now needs an issuer and a client ID, or an explicit -dev ($CONSOLE_DEV_MODE=true). The chart's console.devMode sets it, and the chart refuses to render the console with none of the NebariApp's sign-in, console.oidc or devMode; console.enabled=false leaves the console out. The dev cluster's values set devMode, and usage-e2e's render of the parent's NetworkPolicy turns the console off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With openmeter.namespace set to the pack's namespace, a first helm install failed: the ClickHouse password's pre-install hook ran before the chart created that namespace. keepDataOnUninstall also kept nothing there, since uninstalling the pack deletes its namespace and the retained Postgres Cluster and volumes with it. And the key-manager can read every Secret in its namespace (#215), OpenMeter's credentials included. The chart now refuses that namespace with an error naming it, so OpenMeter always gets its own. The pack-clients NetworkPolicy no longer needs its same-namespace exception. The docs say to create the namespace first and label it nebari.dev/managed=true for the console's NebariApp, and that a reinstall carries on under the same openmeter.namespace. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A token signed with a key the validator hadn't cached, while the JWKS couldn't be fetched (Keycloak down mid key rotation), was refused like a forged one: the console and the key manager answered 401 "sign in again", which sends people round a sign-in loop that can't succeed. The validator now remembers whether its last JWKS fetch failed, and returns ErrKeysUnavailable for an unknown kid while it has, including while the cooldown suppresses another fetch. Both services answer it with 503, like ErrNotReady. Closes #217 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
📄 Docs preview for |
|
Thanks @dcmcand (copying my reply on #211 here so the change is on record with the code). Understood on both points: nothing merges without your review, and we don't need a fork. On the namespace: I'll drop the "separate namespace only" change from this PR and make OpenMeter work in the pack's namespace, with an empty |
52cabe5 refused openmeter.namespace equal to the pack's namespace, to get around two problems with that layout. Packs need to support it, so this fixes the problems instead. - A first helm install failed: ClickHouse's password Secret was made by a pre-install hook, and hooks run before the chart creates the pack's namespace. The Job and its ServiceAccount, Role and RoleBinding are now ordinary resources. ClickHouse and OpenMeter wait for the Secret, which Kubernetes retries. The Job's name carries a hash of its spec, since a Job's pod template can't be changed. ensure-secret only creates a missing Secret, so an upgrade from the hook keeps the existing password. - keepDataOnUninstall kept nothing: uninstalling deleted the namespace with the retained Postgres Cluster and volumes. With OpenMeter in the pack's namespace and keepDataOnUninstall on, the Namespace now carries helm.sh/resource-policy: keep and argocd.argoproj.io/sync-options: Delete=false. openmeter.namespace is still required, since a subchart can't be handed the pack's computed namespace, and the error names the pack's namespace. The cross-namespace NetworkPolicy renders only when the namespaces differ, as before. The docs describe both layouts again, keeping the label and Argo CD steps for a separate namespace. usage-e2e.sh runs with OM_NAMESPACE set to the pack's namespace. Tested on kind: usage-e2e.sh with OpenMeter in llm-operator-system; a fresh helm install into a namespace that doesn't exist yet; uninstall keeping the namespace, Cluster, volumes and Secret; reinstall reading back a customer written before; keepDataOnUninstall=false deleting the namespace; and an upgrade from 6604308 keeping the hook-made Secret. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dcmcand
left a comment
There was a problem hiding this comment.
Verdict: Request changes. The namespace and uninstall changes below need to land, plus two stale doc lines and a few inline items.
@TimHunt12 thanks for turning this around so quickly. Here's the way forward I'd like on the namespace, plus a change to how uninstall works.
Namespace: one helper both charts call
You're right that a parent can't hand a computed value to a subchart. But named templates are shared across a parent and its subcharts (the parent already calls the subchart's om.name in openmeterURL). The helper can't be used from the subchart today because it reads fullnameOverride and nameOverride, which the subchart can't see. If the helper reads only .Release and .Values.global, both charts can call it and get the same answer.
That also covers something the pack should have had from the start: an overridable namespace. So:
global.namespaceis the pack's namespace. Empty means<release>-nebari-llm-serving-system, which is what every install without a name override gets today.nebari-llm-serving.operatorNamespacereads onlyglobal.namespaceand.Release.Name.fullnameOverrideandnameOverridego back to naming resources only.- The subchart's
om.namespaceusesnamespace(that is,openmeter.namespace) if set, else the pack's helper when it's part of the pack (openmeter.inPack: true, set in the parent'svalues.yaml), else the release namespace. The subchart still installs on its own, the waydev/usage-e2e.shdoes. openmeter.namespacebecomes an optional override. Empty puts OpenMeter in the pack's namespace, and thefailgoes away.
Here's a prototype against 22c8c35:
Prototype diff (3 files)
diff --git charts/nebari-llm-serving/charts/nebari-openmeter/templates/_helpers.tpl charts/nebari-llm-serving/charts/nebari-openmeter/templates/_helpers.tpl
index 986bc8d..70cd674 100644
--- charts/nebari-llm-serving/charts/nebari-openmeter/templates/_helpers.tpl
+++ charts/nebari-llm-serving/charts/nebari-openmeter/templates/_helpers.tpl
@@ -16,11 +16,18 @@ is the name plus a hash and must fit 63).
{{- end -}}
{{/*
-The namespace every resource of the subchart goes in: namespace, or the
-release namespace when it is empty.
+The namespace every resource of the subchart goes in: namespace; else, as
+part of the pack (inPack), the pack's namespace from the pack's own helper;
+else the release namespace.
*/}}
{{- define "om.namespace" -}}
-{{- .Values.namespace | default .Release.Namespace -}}
+{{- if .Values.namespace -}}
+{{ .Values.namespace }}
+{{- else if .Values.inPack -}}
+{{ include "nebari-llm-serving.operatorNamespace" . }}
+{{- else -}}
+{{ .Release.Namespace }}
+{{- end -}}
{{- end -}}
{{- define "om.labels" -}}
diff --git charts/nebari-llm-serving/templates/_helpers.tpl charts/nebari-llm-serving/templates/_helpers.tpl
index 5a41297..f1135ef 100644
--- charts/nebari-llm-serving/templates/_helpers.tpl
+++ charts/nebari-llm-serving/templates/_helpers.tpl
@@ -49,10 +49,21 @@ app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
-Operator namespace (the namespace where the operator and key manager run)
+The pack's namespace: global.namespace, else <release>-nebari-llm-serving-system.
+It reads only .Release and .Values.global, which every chart in the release
+sees, so the bundled OpenMeter subchart calls this same helper. Not derived
+from fullnameOverride or nameOverride: those name resources, not the namespace.
*/}}
{{- define "nebari-llm-serving.operatorNamespace" -}}
-{{- printf "%s-system" (include "nebari-llm-serving.fullname" .) | trunc 63 | trimSuffix "-" }}
+{{- with (.Values.global).namespace -}}
+{{ . }}
+{{- else -}}
+{{- $full := .Release.Name -}}
+{{- if not (contains "nebari-llm-serving" .Release.Name) -}}
+{{- $full = printf "%s-nebari-llm-serving" .Release.Name -}}
+{{- end -}}
+{{- printf "%s-system" ($full | trunc 63 | trimSuffix "-") | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
{{- end }}
{{/*
@@ -70,15 +81,10 @@ openmeter.enabled.
{{- end }}
{{/*
-The bundled OpenMeter's namespace: openmeter.namespace, which the subchart
-installs into. Required: a subchart cannot be handed the pack's computed
-namespace, and the release namespace is wherever helm happened to run.
+The bundled OpenMeter's namespace: openmeter.namespace, else the pack's.
*/}}
{{- define "nebari-llm-serving.openmeterNamespace" -}}
-{{- if not .Values.openmeter.namespace -}}
-{{- fail (printf "openmeter.enabled needs openmeter.namespace: the pack's namespace, %s, or another namespace that exists" (include "nebari-llm-serving.operatorNamespace" .)) -}}
-{{- end -}}
-{{- .Values.openmeter.namespace -}}
+{{- .Values.openmeter.namespace | default (include "nebari-llm-serving.operatorNamespace" .) -}}
{{- end }}
{{/*
diff --git charts/nebari-llm-serving/values.yaml charts/nebari-llm-serving/values.yaml
index 9b12391..cf0207e 100644
--- charts/nebari-llm-serving/values.yaml
+++ charts/nebari-llm-serving/values.yaml
@@ -1,3 +1,8 @@
+global:
+ # namespace - the pack's namespace, which the bundled OpenMeter shares unless
+ # openmeter.namespace is set. Empty: <release>-nebari-llm-serving-system.
+ namespace: ""
+
# createNamespace controls whether the chart provisions the operator's own
# namespace and labels it `nebari.dev/managed=true` (required by the
# validating webhook). Set to false when something else owns the namespace
@@ -186,6 +191,8 @@ keyManager:
# subchart's; see charts/nebari-openmeter/values.yaml.
openmeter:
enabled: false
+ # Set by the pack, not by you: tells the subchart to default to the pack namespace.
+ inPack: true
# namespace - where the bundled OpenMeter runs; required with
# openmeter.enabled. Either the pack's own (<fullname>-system), or another
# namespace that exists. In the pack's namespace, the key-manager can readI've only rendered it so far:
- With default values it's byte-identical to
main, for release namesr,nebari-llm-servingandmy-nebari-llm-serving-prod, under Helm 3.19.0 and 4.3.0. - With OpenMeter on and nothing else set, all 44 namespaced resources land in
r-nebari-llm-serving-system. global.namespace=acmemoves the pack and OpenMeter together.openmeter.namespace=usage-ommoves only OpenMeter, and the collector's URL points across.fullnameOverride=llm-operatorrenames resources but leaves the namespace alone.- The subchart on its own still installs into its release namespace.
We'll test it on a cluster before merge.
Along with the helper, we need:
- An Upgrading note. Anyone on 0.1.2 to 0.1.4 who set
fullnameOverrideornameOverridehas a namespace named after it (foo-system, or<release>-bar-system). They need to setglobal.namespaceto that namespace before upgrading. Otherwise the upgrade renders a Namespace with a new name, and Helm deletes the old one (pkg/kube/client.go:436-455at v3.19.0 deletes anything in the old manifest that's missing from the new one), taking their LLMModels, API keys and provider Secrets with it. Argo CD does the same with automated prune on. dev/usage-e2e.shrenders the collector and the cross-namespace policy withfullnameOverride=llm-operator(lines 92 and 112) and expects them inllm-operator-system. Bothhelm templatecalls need--set global.namespace=$NS.- Docs.
global.namespaceinconfiguration.mdxand in the single-namespace section ofarchitecture.mdx, andopenmeter.namespacedescribed as optional. These spots call the pack's namespace<fullname>-system, which is no longer true when an override is set:usage.mdx:111and:131,configuration.mdx:131,values.yaml:190, and the subchart'svalues.yaml:164.
Uninstall: if the pack creates the namespace, uninstall removes it
Please drop the helm.sh/resource-policy: keep and Delete=false annotations from the pack's Namespace (namespace.yaml:21-27). Keeping the namespace keeps much more than OpenMeter's data:
- The API-key Secrets have no owner references, and only the LLMModel finalizer deletes them (
llmmodel_controller.go:281-286). After a reinstall, the operator keeps their data and rebuilds the SecurityPolicy from them, so keys from before the uninstall work again. - The LLMModel CRs survive, since the CRDs live in
crds/. The model Deployments, Services and PVCs are owned by those CRs (llmmodel_controller.go:119,157,310-328), so the model pods keep running on their GPUs with no operator or key manager left.
On main, uninstall deletes the namespace and all of that with it, and we should keep that behavior. The rule is: if the pack created the namespace, uninstall removes it.
That means keepDataOnUninstall only keeps data when OpenMeter's namespace outlives the release: a separate namespace, or createNamespace: false with the namespace created by something else. In the default layout it can't keep anything. We need to say so clearly:
- Docs. The "Uninstall and reinstall" section of
usage.mdx, thekeepDataOnUninstallrow inconfiguration.mdx, and the subchart README's uninstall section. - A
templates/NOTES.txtwarning. Print it when OpenMeter is in a namespace the chart creates andkeepDataOnUninstallis on. Templates can't emit a non-fatal warning (onlyfailandrequired, and both abort), and afailwould break the default install. Argo CD renders withhelm templateand never showsNOTES.txt, so the docs carry the message for Argo CD users. - An Argo CD example. If the Application creates the namespace (
CreateNamespace=true, withmanagedNamespaceMetadatasettingnebari.dev/managed: "true") and the values setcreateNamespace: false,openmeter.enabled: trueandopenmeter.keepDataOnUninstall: true, deleting the Application should leave the namespace. Argo CD doesn't track a namespace it creates that way (controller/sync_namespace.go:38-39at v3.5.6), so it never deletes it. The Postgres Cluster's keep annotation and the StatefulSets'whenDeleted: Retainthen keep the data. Let's add that next toexamples/argocd-application.yamland confirm it on the cluster. One case needs testing in particular: an existing Application switched fromcreateNamespace: truetofalse. The chart's Namespace already carries Argo CD's tracking annotation there, so I believe deleting the Application would still delete the namespace unless that annotation is removed first. - Update the existing examples.
examples/argocd-application.yamlhas drifted:- It pins chart
0.1.2and doesn't render against the current chart:frontend.keycloak.urlis now required. - It sets
CreateNamespace=trueandmanagedNamespaceMetadata, but leaves the chart'screateNamespaceon. The chart's Namespace takes precedence (gitops-engine/pkg/sync/sync_context.go:1161-1176), so themanagedNamespaceMetadatalabels are ignored, and deleting the Application deletes the namespace. - The two examples should tell one story. In the existing one the chart owns the namespace, so I believe we can drop
CreateNamespace=trueandmanagedNamespaceMetadatathere. In the new OpenMeter one, Argo CD owns it. - With
global.namespace, the Application'sdestination.namespaceandglobal.namespacehave to match. Both examples should show that.
- It pins chart
Key: 🛑 Blocker (must fix before merge) · 💡 Suggestion (improvement, not blocking) · ❓ Question (needs author input) · ❤️ Kudos
| {{- $name := include "om.name" (dict "root" . "component" "ensure-secret") }} | ||
| {{- $hook := dict "helm.sh/hook" "pre-install,pre-upgrade" "helm.sh/hook-delete-policy" "before-hook-creation,hook-succeeded" }} | ||
| {{- $command := list "/openmeter-setup" "ensure-secret" "-name" (include "om.clickhouseSecret" .) "-key" "clickhouse-password" }} | ||
| {{- $job := printf "%s-%s" ($name | trunc 57 | trimSuffix "-") (printf "%s %s" (include "om.setupImage" .) ($command | join " ") | sha256sum | trunc 5) }} |
There was a problem hiding this comment.
💡 Suggestion: The Job name hashes only the image and command, so a change to the rest of the pod template keeps the old name.
Hash the rendered pod template, the way the price-sync Job does at openmeter.yaml:185.
A Job's pod template is immutable. If a later chart changes resources or securityContext with the same setup image, helm upgrade fails with "field is immutable". The comment at line 12 says the name carries "a hash of the spec", which needs the same fix.
| if err != nil { | ||
| // JWKS not fetched yet: transient, tell the client to retry. | ||
| if errors.Is(err, oidc.ErrNotReady) { | ||
| if errors.Is(err, oidc.ErrNotReady) || errors.Is(err, oidc.ErrKeysUnavailable) { |
There was a problem hiding this comment.
💡 Suggestion: No key-manager test covers the new ErrKeysUnavailable branch.
Add a middleware test case that expects 503 and Retry-After for ErrKeysUnavailable.
Removing || errors.Is(err, oidc.ErrKeysUnavailable) leaves go test ./internal/api/ green. Only the console's identity_test.go pins this mapping today.
| that already exists. Until the key manager's Role is narrowed to the API-key | ||
| Secrets, it can read every Secret in its namespace, OpenMeter's credentials | ||
| included; a separate namespace keeps them apart. | ||
| included; a separate namespace keeps them apart. Create a separate namespace |
There was a problem hiding this comment.
💡 Suggestion: "Create a separate namespace first" reads as a step for every install, but the pack's namespace is allowed again.
Start the sentence with "To use a separate namespace, create it first".
| // and refuses an issuer without a client ID (any client in the realm would do). | ||
| func newIdentity(issuer, discovery, clientID, groups string, dev bool) (openmeter.Identity, func(), error) { | ||
| switch { | ||
| case dev && issuer != "": |
There was a problem hiding this comment.
❤️ Kudos: newIdentity refuses -dev together with an issuer, so a dev flag left on in a real deployment can't turn off sign-in.
The dev and issuer both set case in TestNewIdentityFailsClosed fails without this branch.
| @@ -1,3 +1,4 @@ | |||
| {{- if .Values.console.enabled }} | |||
There was a problem hiding this comment.
❤️ Kudos: console.enabled removes the console's Deployment, Service, NebariApp and NetworkPolicy together, so OpenMeter can run without a UI that needs sign-in.
There was a problem hiding this comment.
🛑 Blocker: Line 146 still says the ClickHouse password Secret is "made by a hook".
Change "made by a hook" to "made by a Job".
The Secret still isn't part of the release, so the rest of that sentence holds.
There was a problem hiding this comment.
🛑 Blocker: Line 83 still calls the ClickHouse password step a "hook".
Change "the ClickHouse password hook" to "the ClickHouse password Job".
Three fixes for #211, from the last review round. Merging this into
usage-meteringadds them to #211.1. The usage console fails closed (
2c665d8)The console shows everyone's usage, but with no issuer set it let every caller in as the same viewer, and that was the default whenever the NebariApp was off. An issuer without a client ID also started, accepting a token issued to any client in the realm.
-dev(CONSOLE_DEV_MODE=true). Otherwise it refuses to start.console.devMode(defaultfalse) sets dev mode, and the chart refuses to render the console with none of the NebariApp's sign-in,console.oidcordevMode. The newconsole.enabled(defaulttrue) can leave the console out.devMode: true. The docs and the subchart README describe the new values.2. OpenMeter works in the pack's namespace (
22c8c35, replacing52cabe5)Packs need to let people choose namespaces, the pack's own included (see @dcmcand's comment on #211).
52cabe5refused the pack's namespace;22c8c35fixes the two problems that layout had instead:helm installfailed, because the ClickHouse password's pre-install hook ran before the chart created the pack's namespace. The password Job and its ServiceAccount, Role and RoleBinding are now ordinary resources, not hooks. ClickHouse and OpenMeter wait for the Secret (Kubernetes retriesCreateContainerConfigError). The Job's name carries a hash of its spec, since a Job can't be changed after it's created. The Job only creates a missing Secret, so an upgrade keeps the existing password.keepDataOnUninstallkept nothing, because uninstalling deleted the namespace with the retained Postgres Cluster and volumes in it. With OpenMeter in the pack's namespace andkeepDataOnUninstallon, the Namespace now carrieshelm.sh/resource-policy: keepandargocd.argoproj.io/sync-options: Delete=false.openmeter.namespaceis still required, because a subchart can't be handed the pack's computed namespace; the error names the pack's namespace. A separate namespace still works, and keeps OpenMeter's credentials away from the key manager until #215; the docs keep the label and Argo CD steps for it. The cross-namespace NetworkPolicy renders only when the namespaces differ.3. 503, not 401, when Keycloak's signing keys can't be fetched (
90f9cea, closes #217)A token signed with a key the validator hadn't cached, while the JWKS couldn't be fetched (Keycloak down mid key rotation), was refused like a forged token: 401 "sign in again", which sends people round a sign-in loop that can't succeed. The shared validator now returns
ErrKeysUnavailablein that case, including while the cooldown suppresses another fetch, and the console and the key manager both answer it with 503, likeErrNotReady.Testing
go vetandgo testpass inshared,key-manager,usage-console/serverandopenmeter-setup. New tests cover the console's startup rules and the keys-unavailable case; the latter fails without the fix.openmeter.enabledoff, the render is byte-identical tomain, andhelm lintpasses.make usage-e2epasses on a local kind cluster at90f9ceawith OpenMeter in its own namespace, and at22c8c35with OpenMeter in the pack's namespace (OM_NAMESPACE=llm-operator-system). Each person's and team's requests reach OpenMeter, and the console (in dev mode) and OpenMeter's cost queries show the same totals. The console image refuses to start with no sign-in configured, and with an issuer but no client ID.22c8c35:helm installwith OpenMeter in the pack's namespace, which didn't exist yet, succeeds;helm uninstallkeeps the namespace, the Postgres Cluster, the volumes and the password Secret;keepDataOnUninstall=false, uninstalling deletes the namespace;6604308(the hook version) keeps the existing password Secret.🤖 Generated with Claude Code