Repository navigation
Helm chart: read the VAPID private key (web push) from an existing Kubernetes Secret - #5090
Merged
Merged
Conversation
vapid.existingSecret.name names a Secret the operator manages; every pod given VAPID_PRIVATE_KEY (app, worker, telemetry writer, nginx, home, test server and the migrate Job) then reads it from there, under privateKey unless vapid.existingSecret.privateKeyKey names another key, and the chart renders it nowhere as a plain value. Setting vapid.privateKey as well is refused, saying which one to clear, as the Kubernetes agent chart refuses apiKey next to existingSecretName: a key left in the values would stay in every revision Helm stores. The public key and the subject stay plain values. An install that sets none of this renders byte for byte as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A cluster-free suite renders the chart with every workload on, in both database layouts: with vapid.existingSecret, exactly the workloads that get VAPID_PRIVATE_KEY from vapid.privateKey get it from the Secret, none as a plain value; a new workload that is handed the key must be added to the suite's list on purpose. Naming no Secret renders exactly as before, the plain key is copied nowhere else, both set and wrong shapes are refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reference named no vapid value at all. It now lists the three, the existing Secret and its key, with how to create the Secret, and the production checklist suggests the Secret for installs whose values are in Git. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Kubernetes tab of Push Notifications shows how to put the private key in a Secret of your own and name it in vapid.existingSecret, that the chart refuses it beside vapid.privateKey, and that the public key stays a value. In all 17 languages, line for line. PermissionsAndSelfHostedDocsFacts holds the paragraph to the chart: the values, the schema, the helper's Secret, key and refusal, and the example's names; its chart check reads a variable that has a Secret branch too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Kubernetes Secret created from a file keeps the file's last newline, and
web-push refuses a private key with anything around it ('Vapid private key
must be a URL safe Base 64'), so with the key from vapid.existingSecret every
web push would fail. The server now reads the key trimmed; a key with nothing
around it is read as before, and one of only whitespace is no key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…xisting-secret Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
UI review of the vapid.existingSecret docs.
- Push Notifications, Kubernetes tab, all 17 languages: the tab leads with
the one values.yaml most installs need; keeping the private key in a
Secret is a fold under it ("Keep the private key in a Secret", worded as
the Kubernetes agent page's own "Keep the ingestion key in a Secret"
heading in each language). The kubectl line no longer scrolls sideways
in the 590px article column: a flag a line, with --namespace oneuptime,
as the Foundry and agent pages write it.
- A fold on a Persian page kept its marker pointing left once opened (the
right-to-left rule turned only the closed marker, and the open rule
outranked it with the left-to-right turn). The marker now points down
when open either way round, keyed on the fold's own direction (:dir),
so the English copy of a page on a Persian address keeps the
left-to-right marker. RightToLeftDocsLayout reads where each marker
points.
- Chart reference: an aligned values table, -n oneuptime on the kubectl
line (as the CA example on the same page), one restart command in a
paragraph of its own instead of "and so on"; the checklist names
vapid.existingSecret.name; values.yaml says to create the Secret in the
release's namespace.
- HelmChart/Tests/run.sh: the summary column fits the new suite's name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
The maintainer's task: "extend the helm chart to take in an existing secret for the vapid.privateKey."
The VAPID private key signs every web push. Until now the chart only took it as
vapid.privateKey, rendered as a plain env value, so it sat in the release's values (Helm keeps them in every revision's Secret) and, for Argo CD / Flux users, in their Git repository. The chart already reads other credentials from a Secret the operator manages (slackApp.existingSecret,runner.existingSecret,externalPostgres.existingSecret, ...), and the Kubernetes agent chart does it for its ingestion key, but not this one.What changed
Chart (
HelmChart/Public/oneuptime)vapid.existingSecret.name(empty by default) andvapid.existingSecret.privateKeyKey(defaultprivateKey). With a name set,VAPID_PRIVATE_KEYcomes fromvalueFrom.secretKeyRefin every workload that gets it - app, worker, telemetry writer, nginx, home, test server and the migrate Job (also as the pre-upgrade hook) - and no workload renders it as a plain value as well. All of them go through the one helper (oneuptime.env.runtime), so a new workload on that helper gets it too._helpers.tpl:oneuptime.vapid.existingSecretName(trims the name; refuses both set) andoneuptime.vapid.privateKeyKey(trims the key, defaults toprivateKey). Both read through parentheses, so values from before the option (helm upgrade --reuse-values) still render.values.schema.json:vapid.existingSecretis an object ofnameandprivateKeyKey(string or null) and nothing else, sopasswordKey(the name other blocks use), a string in place of the block, a numeric name or a list are refused athelm install/helm upgrade.values.yamldocuments it, with thekubectl create secretline.Server (
packages/Common/Server/EnvironmentConfig.ts)VAPID_PRIVATE_KEYis read trimmed. A Secret created from a file (kubectl create secret generic --from-file) keeps the file's last newline, and web-push refuses a key with anything around it ("Vapid private key must be a URL safe Base 64"), so with the key from a Secret every web push would have failed. A key with nothing around it is read exactly as before; one of only whitespace counts as no key, as an empty one always did.Docs
self-hosted/push-notifications), Kubernetes tab, in all 17 languages, line for line: how to put the key in a Secret and name it, that the chart refuses both, how to name another key, and that the public key stays a value.docs/configuration.md): a new "Web push notifications" section - it named novapidvalue at all before - with the five values, the Secret, and how to roll the pods after changing it.docs/production-checklist.md: one item suggesting the Secret when values live in Git.HelmChart/README.md: the new suite in the suites table.Decisions (made by the chart's own precedents)
vapid.privateKeyandvapid.existingSecret.nameset: refused, not resolved. The message says which to clear:vapid.privateKey and vapid.existingSecret.name are both set. Set one of them: to read the VAPID private key from the Secret <name>, clear privateKey (--set vapid.privateKey="").This follows the maintainer's own newest precedent (the Kubernetes agent chart refusesoneuptime.apiKeynext tooneuptime.existingSecretName, 8778402) and its reason: a Secret of your own is there to keep the key out of the release's values, and a key left inprivateKeywould stay in every revision Helm stores. "Secret wins silently" (the older app-integration blocks) would leave the key there without telling anyone, and two sources of truth for one key.privateKey. It is the namenpx web-push generate-vapid-keys --jsongives the field and the chart's own value name, sokubectl create secret generic oneuptime-vapid --from-literal=privateKey=...plusexistingSecret.nameis the whole setup. Empty, null or only spaces fall back to it.<release>-secretsSecret, and the plain-value path is unchanged (it never used that Secret either).optional: trueon the reference: like every other operator-managed Secret in this chart, a missing Secret or key stops the pods with a clearCreateContainerConfigError, rather than web push silently staying off. A rolling update keeps the old pods serving meanwhile.config.env) is unchanged - not part of the ask.What changes for existing customers
Nothing, unless they set the new value. An install that sets none of it renders byte for byte as before: I rendered master's chart and this branch with every workload on - defaults, a plain key, the operator database layout,
migrate.hook,deployment.disableDeployments- and diffed (normalising only the chart's random secrets): identical. AVAPID_PRIVATE_KEYwith whitespace around it (from any source) now works instead of failing every web push.Tests
HelmChart/Public/oneuptime/tests/vapid-existing-secret_test.yaml(56 helm-unittest cases): for each of the 7 workloads - plain value; from the Secret with exactly oneVAPID_PRIVATE_KEYentry and novalueon it; neither set (renders the empty value as before); key name left empty (readsprivateKey); both set refused with the exact message (each refusal test leaves its workload the only one rendering the helper, since a render stops at the first failing template). Plus: the migrate hook; a named key; name and key trimmed; key only spaces / null; quoting of names YAML would read as other types; empty and only-spaces names = not configured; a key name alone changes nothing;existingSecret: null(values from before the option); a private key of only spaces is not counted as set; the refusal names the trimmed Secret; the public key and subject stay plain; the e2e CronJob gets the public key only; probes, the shared Runner and the global Runner never get the private key; the schema refuses 4 wrong shapes.HelmChart/Tests/suites/vapid-existing-secret.sh(new cluster-free suite, 40 checks, registered inrun.shso the helm-test job runs it): renders the whole chart with every workload on, in both database layouts: naming no Secret (empty, spaces, key only, block from before) renders exactly as the plain key does; the plain key lands only inVAPID_PRIVATE_KEYentries (no copy in any Secret, ConfigMap or annotation); with the Secret, exactly the same workloads get it, each from the Secret and none as a plain value; a workload not in its list that gains the key fails (so a new one is added on purpose); another key name reaches every workload;helm templaterefuses both set with the exact message and without printing the key; the schema refuses each wrong shape and names the property (Helm-version independent).packages/Common/Tests/Server/EnvironmentConfigVapidPrivateKey.test.ts(10 cases): the key with a trailing\n/\r\n, spaces and tabs is read trimmed; untouched otherwise (every base64url character kept); only whitespace or unset = no key; web-push refuses the raw newline-ended key and takes the trimmed one (with a real generated key pair); andPushNotificationService.initializeWebPushsets web push up with a newline-ended key, and leaves it off for a newline-only one. The service is imported inbeforeAllhooks with a time budget of their own, since its first import compiles for ~30 s on a cold cache.packages/App/Tests/FeatureSet/Docs/PermissionsAndSelfHostedDocsFacts.test.ts: a new test holds the page's paragraphs to the chart (values and defaults, the schema's two keys andadditionalProperties: false, the helper's Secret / key / refusal, the public key never from a Secret, and the page's two examples naming values the chart has and the default key). ItschartRendersFromnow reads a variable that also has a Secret branch.What I ran
HelmChart/Tests/run.sh lint unit extra-values vapid-existing-secret marketplace(private helm 3.19.0, sha256-checked): all pass (oneuptime chart unit tests 602, agent chart 384).Tests/FeatureSet/Docs/Docs*) and every docs suite naming the page or theself-hostedcategory - 86 suites, 16,251 tests, all pass, before and after merging master;npm run docs:localize-anchors -- --apply --page self-hosted/push-notifications(nothing to localize),npm run docs:check-anchors,npm run i18n:validate. App's root-level tests (17 suites, 263 tests).--no-cache); theEnvironmentConfig*andWebPush*suites withBILLING_ENABLEDoff and on (11 suites, 239 tests each way);npx jest Guard(151 suites, 4,764 tests).ee/moved away, as Common Test and App Test run them.Tests/Ops(64 suites pass, 1 skipped), before and after merging master.npm run compilein Common and App,npm run compile-testsin Common;tsc --listFilesin both shows no file fromScripts/or another package; eslint and prettier on the changed code files; the ReDoS checker (no new patterns in product code). The docs pages andHelmChart/README.mdwere not prettier-formatted on master either; CI lints code only, and reformatting the pages would break the docs' line-for-line translation checks.secrets-lifecycle,keda-bootstrap; they need KinD and nothing they cover changed), a live install, and the full App and Common jest runs (CI runs them). No failures seen that also fail on master.UI review
What I looked at. The Push Notifications page's Kubernetes tab, served by the docs' real routes and templates (a jest harness around
DocsFeatureSet, with the docs' own stylesheet, fonts and scripts) and drawn in Chromium at 1280 and 390 px wide, light and dark, with the fold closed and open, in all 17 languages (German for length, Persian right to left, Japanese, Hindi). The chart reference section and the production checklist, drawn as GitHub draws Markdown, at 1280 and 390 px. Thehelm templaterefusal and the schema errors as helm 3.19.0 prints them, the values.yaml comment, and the helm test runner's summary.What I changed, and why.
--namespace oneuptime, as the Microsoft Foundry and Kubernetes agent pages write it. The text says "in OneUptime's namespace", and the command now shows where to put yours.:dir(rtl)), so when an untranslated page is shown in English at a Persian address, its folds keep the left-to-right marker. This fixes every Persian fold, not just this one.-n oneuptime, like the CA example on the same page. The note about restarting pods is now a paragraph of its own with one command (kubectl rollout restart deployment -n oneuptime) instead of "deployment/-app, and so on". The production checklist namesvapid.existingSecret.name, the value you actually set. The values.yaml comment says to create the Secret in the release's namespace.HelmChart/Tests/run.sh: the summary's name column is now 22 characters wide, sovapid-existing-secretno longer pushes its count out of line.Left as built. The refusal text: it matches the agent chart's word for word (8778402) and says what to clear. The schema errors, which Helm writes. The settings table on the page, which the fold sits right under. On a phone, a long code span such as
vapid.existingSecret.privateKeyKeystill breaks inside the fold, as long code spans do across the docs.Tests.
PermissionsAndSelfHostedDocsFacts: the Secret paragraphs open the fold. Inside the Kubernetes tab, the fold comes after the plain example and closes with the tab set. The command must stay on three lines and keep the namespace.RightToLeftDocsLayout: works out from the stylesheet's borders and rotations which way a fold's marker points: right or left when closed, down when open, in both directions. It also checks that no fold rule is keyed on[dir=], with a control test for the reader. Both new checks fail on master's stylesheet and on a fix keyed on the attribute.PermissionsAndSelfHostedDocsTranslationsalready holds the fold line for line in every language (containers, fences, inline code).What I ran. The 88 docs suites that read the page, the stylesheet or the self-hosted category, plus the general
Docs*suites: 22,623 tests.docs:localize-anchors,docs:check-anchorsandi18n:validate. The helm suites lint, unit, extra-values, vapid-existing-secret and marketplace, with a private, sha256-checked helm 3.19.0.Tests/Ops(64 suites), App's root-level tests, and Common'snpx jest Guard(151 suites). App'snpm run compileand thetsc --listFilescheck. The changed suites again with ee/ hidden. Prettier and eslint on every changed code file, and the ReDoS checker. Master had not moved since the builder's merge.🤖 Generated with Claude Code