Print a working OpenBao URL in NOTES.txt without an Ingress
What does this MR do?
With the Ingress disabled, the commands NOTES.txt prints did not always lead to the active OpenBao node: for a LoadBalancer Service the URL had no port and a mangled address, for a ClusterIP Service the port-forward went to whichever pod kubectl listed first, and with TLS on, every URL started with http://. I installed the chart on k3s v1.33.0 (the version the test-e2e job runs), ran the commands Helm printed as a user would paste them, and asked the URL they printed for /v1/sys/health. The first commit adds tests for NOTES.txt as it is; each of the next four fixes one of the problems that showed, and the sixth, raised in review, fixes the URL for an IPv6 address. Every fix comes with tests that fail on the commit before it:
- Tests for
NOTES.txt, which had none: the Ingress URL with and without Ingress TLS, and the NodePort commands, as they are today. - LoadBalancer, port: the URL ended in
.Values.serviceActive.port, a key the values schema does not allow underserviceActive, so it was always empty and the command printedhttp://$SERVICE_IP:. It now printsconfig.apiPort, the port the Service listens on. - LoadBalancer, address: the template printed every value of
status.loadBalancer.ingress[0]run together. The API server addsipMode: VIPto an entry with an IP, so the address came out as172.27.0.2VIP; with an IP and a hostname both set, both came out joined. It now prints the IP, or the hostname when there is no IP. - ClusterIP: the command port-forwarded to the first pod listed with the release's name and instance labels, which the standby pods and the
helm testpod carry as well. With the two replicas the HorizontalPodAutoscaler keeps by default, it picked the standby, whose/v1/sys/healthanswers 429 with"standby": true. A standby forwards most requests to the active node by default, but it answers its own health check. It now forwards to the-activeService, which the NodePort and LoadBalancer commands already use. - TLS: without an Ingress the URL was always
http://. Withconfig.tlsDisable: falseOpenBao serves TLS only, and answeredClient sent an HTTP request to an HTTPS server.The scheme now followsconfig.tlsDisable, asopenbao.internalUrldoes; the Ingress URL still followsingress.tls.enabled. - IPv6: when the LoadBalancer or node address is IPv6, the URL came out as
http://2001:db8::10:8200, which curl refuses as malformed. The address is only known when the printed commands run, so they now put it in brackets there when it contains a colon, and quote the URL they echo, sincebashwithnullglobset would drop the bracketed URL as a pattern that matches no file.
The default install (Ingress enabled) prints the same as before, and a NodePort Service without TLS prints the same URL as before for an IPv4 address or a hostname.
!35 (closed) worked on the same template in 2025 and was closed by its author in December 2025. Its review pointed out that setting nodePort from config.apiPort falls outside the default 30000-32767 NodePort range, and the nil .Values.service.type it also fixed was fixed on main in January 2026 by cc21c606. I do not touch the NodePort allocation here.
Two things I left alone:
- With
gatewayRoute.enabledand no Ingress,NOTES.txtstill prints the commands for the Service type rather than the route's URL, because the scheme and the port depend on the Gateway's listener, which the chart does not create. That is left for a follow-up. - I did not bump
versioninChart.yaml: !79 (merged) and !82 (merged) both move it to0.20.1already. Tell me if you want a bump here.
Author's Checklist
For anything in this list which will not be completed, please provide a reason in the MR discussion.
Required
- Ensure a release milestone is set. (I cannot set a milestone on this project.)
- MR title and description are up to date, accurate, and descriptive.
- MR targeting the appropriate branch.
- MR has a green pipeline on GitLab.com. (The three
test-e2ejobs wait for a manual start.helm-lint,helm-unittestandcheck-values-schemapass locally on every commit, see the test plan.) - When ready for review, MR is labeled workflowready for review per the MR workflow.
Expected
- Test plan indicating conditions for success has been posted and passes.
- Documentation is created or updated. (
NOTES.txtis the only text this changes.) - Tests are added.
Test plan and results
Chart checks, in registry.gitlab.com/gitlab-com/gl-infra/common-ci-tasks-images/helm:3.19.0 (the image the CI jobs use: Helm 3.19.0, helm-unittest 1.0.3, helm-values-schema-json 2.6.0), on each of the six commits:
helm unittest ./: 78, 80, 80, 82, 86 and 88 tests, all passing.helm lint ./ --strict: 1 chart linted, 0 failed.helm schema && git diff --exit-code values.schema.json: no difference.
Each commit's tests/notes_test.yaml run against the previous commit's templates: the tests of the first commit pass on main, and the tests of each fix fail on the templates before it (2, 1, 2, 3 and 7 failures).
On k3s v1.33.0+k3s1 in Docker, installing the chart with the storage values of scripts/dev/setup-bao.sh against a PostgreSQL in the cluster, --set ingress.enabled=false, then running the printed commands and requesting <URL>/v1/sys/health:
| Service | TLS | main |
this branch |
|---|---|---|---|
| LoadBalancer | off | http://172.27.0.2VIP:, curl cannot resolve the host |
http://172.27.0.2:8200, 200 |
| LoadBalancer | on | http://172.27.0.2VIP:, curl cannot resolve the host |
https://172.27.0.2:8200, 200 |
| NodePort | on | http://172.27.0.2:31077, 400 Client sent an HTTP request to an HTTPS server. |
https://172.27.0.2:31344, 200 |
At commit 4, with TLS on, the LoadBalancer URL was http://172.27.0.2:8200, answered with that same 400.
ClusterIP, once the HorizontalPodAutoscaler had started the second pod: on main with TLS off, the commands picked the standby pod (kubectl get pods lists it first by name) and /v1/sys/health answered 429 with "standby": true. On this branch with TLS on, they forward to svc/ob-openbao-active, whose only endpoint was the active pod: https://127.0.0.1:8080/v1/sys/health answered 200 and /v1/sys/leader "is_self": true.
The address template, on a LoadBalancer Service whose class no controller handles, with its status written by hand (kubectl patch --subresource=status):
=== status written: ingress[0] = {"ip":"192.0.2.10"}
stored: {"ingress":[{"ip":"192.0.2.10","ipMode":"VIP"}]}
old template: 192.0.2.10VIP
new template: 192.0.2.10
=== status written: ingress[0] = {"hostname":"lb.example.org"}
stored: {"ingress":[{"hostname":"lb.example.org"}]}
old template: lb.example.org
new template: lb.example.org
=== status written: ingress[0] = {"ip":"192.0.2.10","hostname":"lb.example.org"}
stored: {"ingress":[{"hostname":"lb.example.org","ip":"192.0.2.10","ipMode":"VIP"}]}
old template: lb.example.org192.0.2.10VIP
new template: 192.0.2.10The IPv6 address only exists when the commands run, so the chart's tests pin the commands, and I ran them. Rendered by helm template at commits 5 and 6, run in busybox sh, dash, bash, bash -O nullglob and zsh with kubectl answering 127.0.0.1, ::1 or localhost, and the printed URL asked for /v1/sys/health on an OpenBao 2.6.2 dev server listening on [::]:8200: every IPv4 and hostname URL answered 200 at both commits, and for ::1 commit 5 printed http://::1:8200 (curl exit 3, malformed URL) where commit 6 printed http://[::1]:8200 (200), in every shell and for both Service types. With the kubectl of k3s v1.33.0, a Service like the one above with an IPv6 address written into its status, and an IPv6 InternalIP put first in the node's addresses (the kubelet rewrites them on its next status update, so the commands ran right after the patch):
LoadBalancer, stored: [{"ip":"2001:db8::10","ipMode":"VIP"}]
commit 5: http://2001:db8::10:8200
commit 6: http://[2001:db8::10]:8200
NodePort, node addresses: [{"address":"fd00:10:42::2","type":"InternalIP"},{"address":"172.18.0.10","type":"InternalIP"},...]
commit 5: http://fd00:10:42::2:32623
commit 6: http://[fd00:10:42::2]:32623Related issues
Closes #22 (closed)