Skip to content

[lb pub endpt] MetalLB public endpoint reuse: service cert SANs, BYO cert, front port - #2207

Open
stuggi wants to merge 2 commits into
openstack-k8s-operators:mainfrom
stuggi:lb-pub-endpt-reuse
Open

stuggi wants to merge 2 commits into
openstack-k8s-operators:mainfrom
stuggi:lb-pub-endpt-reuse

Conversation

@stuggi

@stuggi stuggi commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Support a MetalLB LoadBalancer public endpoint with a custom hostname, certificate and port (RHOSP 17.1 -> RHOSO adoption), in the openstack-operator. Two commits: "Public endpoint reuse: service cert, front port, live port name" and "Reconcile when a bring-your-own service cert secret changes".

Jira: OSPRH-39364 (cert SAN + bring-your-own cert), OSPRH-39366 (front port). Depends on the lib-common changes this bumps (service.GetVHostServerName, endpointURL validation).

What changes

Service cert (OSPRH-39364)

  • SANs come from lib-common's canonical set {custom host, short, FQDN} (service.GetVHostServerName, the same helper the vhost uses), for the public and the internal endpoint. A bad endpointURL falls back to the in-cluster names and is logged. additionalSubjectNames stays cert-only.
  • Bring-your-own pod cert via the existing spec.<svc>.template.tls.api.{public,internal}.secretName (no new API field). Every EnsureEndpointConfig call site snapshots the user's Template.TLS before the per-reconcile restore and passes the snapshot as the BYO signal; reading it after the restore would mistake the operator's own issued secret name for a user override and delete the issued Certificate. Octavia now participates too (it passed an empty value before).
  • apiOverride.tls.secretName is the Route cert only and is now validated only when a Route exists.
  • Fix the cert cleanup delete target (the Service cert, not the Route cert).

Which field governs which cert:

Endpoint Pod (Service) cert Route cert
public, Route-backed always operator-issued; tls.api.public.secretName is ignored (logged) apiOverride.tls.secretName, else issuer
public, LoadBalancer tls.api.public.secretName, else operator-issued none
internal tls.api.internal.secretName, else operator-issued none

Front port and live port name (OSPRH-39366)

  • For a TLS LoadBalancer public endpoint with an endpointURL, inject a ports[] override with the endpointURL port (the scheme default, 443/80, when the URL has none). There is no implicit director legacy port: without an endpointURL the Service keeps its native port. Adopters who keep a RHOSP 17.1 front port put it in the endpointURL, e.g. https://keystone.example.com:13000 (the endpointURL host must be an FQDN). The port name is read from the live Service; targetPort is untouched. A user-set port number is kept; only its name is re-synced.
  • Injection is skipped for: non-TLS, Route-backed, non-LoadBalancer Services, multi-port Services, and while the live port is not yet named after its endpoint type (public), since an override carrying the old <service>-public name would be rejected by the service operator's by-name port merge before the Service is patched.
  • A watch on managed Services (port names and type) wakes the control plane after the service operator renames the port, so the injection follows without waiting for another event.
  • CreateRoute takes the target port name from the live Service (single port; otherwise the Service name, the annotation still wins).
  • A user endpointURL is kept when an endpoint moves from a Route to a LoadBalancer (it used to be cleared for one reconcile, flapping the catalog endpoint).
  • Glance: the loop returns the requeue of the API that asked for it, and a later API's valid secret no longer overwrites CustomTLSReady=False set earlier in the same reconcile.

BYO secret watch (second commit)

Editing the content of a BYO cert secret now reconciles the control plane, like apiOverride.tls.secretName does. The field index covers tls.api.{public,internal} of all services (found by walking the spec, so new services are covered) plus the simple TLS sections of horizon, novncproxy, prometheus and alertmanager. Operator-owned secrets (ovn, vencrypt, metadata) are not indexed. The indexed spec comes from the informer cache, i.e. the persisted user input.

Upgrade notes

  • tls.api.internal.secretName used to be ignored. If a control plane already carries it, the pod cert switches to that secret and the operator-issued Certificate is deleted (missing secret: requeue every 10s with CustomTLSReady=False).
  • The front port follows the endpointURL port for TLS LoadBalancer public endpoints, once the service operator names the port public. Without an endpointURL nothing changes: the Service keeps its native port.
  • Cert SANs are unchanged for endpoints without a custom endpointURL (same list and order, no re-issue). Endpoints with a user endpointURL gain the host as a SAN.

Rollout ordering

This is the openstack-operator side. With a service operator that still names its port <svc>-public, no front port is injected and the Service is unchanged; cert SANs, BYO certs and the Route target port work. The custom front port, endpointURL admission validation and the custom-hostname vhost need the per-service operator changes (port rename, new lib-common), which follow separately.

Notes for reviewers

  • The parent OpenStackControlPlane spec is never persisted by the reconcile (the only metadata writes are the finalizer add/remove); the snapshot and the secret index rely on that. A comment at the finalizer add documents the constraint.
  • DNS CNAME handling is intentionally not part of this change.
  • go.mod: only lib-common/modules/common is bumped; the other lib-common modules are left for the next bump.

Testing

  • make test passes (unit + envtest).
  • New unit tests cover: the SAN set (public/internal, bad URL, additionalSubjectNames), BYO public/internal, Route-backed BYO ignored, the cert delete target, BYO missing/valid conditions, port injection (TLS LB, skips, endpointURL follow, stale name, unrenamed port, multi-port, non-LoadBalancer), the Route target port, Route -> LoadBalancer keeping endpointURL, the Service watch predicate, and the secret index walker.
  • New envtests cover: operator-issued keystone service certs surviving repeated reconciles, a removed missing BYO secret clearing CustomTLSReady, and the Service watch enqueuing the contr
    ol plane on port rename / type change but not for unmanaged Services.

@openshift-ci

openshift-ci Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: stuggi

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci Bot added the approved label Nov 2, 2026
@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Summary

Summary by CodeRabbit

  • New Features

    • Custom TLS certificates are recognized across more services, and endpoint certificates use configured service hostnames.
    • Service port or type changes are detected, and route targets can follow the configured service port name.
    • Endpoint port overrides are synchronized with eligible live service ports.
  • Bug Fixes

    • TLS settings and endpoint updates are handled more consistently during reconciliation, including preserving endpoint changes for processing.
    • Readiness reporting and operator-issued certificates are handled more reliably.
    • Disabling route creation no longer clears a service’s endpoint URL override.

Endpoint reconciliation now resolves service certificates from canonical endpoint hostnames and configured secrets, and applies Service port and Route target-port rules. Service reconcilers preserve user-provided TLS settings. The control-plane controller indexes TLS secret names and watches relevant managed Service changes.

Changes: Endpoint TLS and reconciliation

Layer / File(s) Summary
Endpoint certificate and port behavior
internal/openstack/common.go, internal/openstack/common_test.go
Endpoint certificates use canonical endpoint hostnames and resolve configured service secrets. The code handles missing secrets, readiness conditions, eligible LoadBalancer port overrides, Route target ports, and Route TLS behavior. Tests cover these cases.
Service reconciler TLS inputs
internal/openstack/{barbican,cinder,designate,glance,heat,horizon,ironic,keystone,manila,neutron,nova,octavia,placement,swift,telemetry,watcher}.go, go.mod, api/go.mod
Service reconcilers pass saved user TLS settings to endpoint configuration. Glance retains non-empty reconciliation results. Both module files update the lib-common/modules/common revision.
Control-plane TLS indexing and Service watches
internal/controller/core/openstackcontrolplane_controller.go, internal/controller/core/openstackcontrolplane_controller_test.go, test/functional/ctlplane/*
The controller indexes user-provided TLS service secrets and watches managed Service port-name or type changes. Tests cover secret lookup, reconcile enqueueing, TLS readiness, and service certificates.

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant ServiceReconciler
  participant EnsureEndpointConfig
  participant CertificateResolver
  participant KubernetesAPI
  ServiceReconciler->>EnsureEndpointConfig: pass endpoint configuration and TLS settings
  EnsureEndpointConfig->>CertificateResolver: resolve certificate using endpoint hostnames and configured secret
  CertificateResolver->>KubernetesAPI: validate Secret or manage issued Certificate
Loading
Merge Risk: Minimal · up to 010bb

The checked certificate-readiness and Secret-rotation paths do not show a merge-blocking issue. The change is mergeable after normal checks.

Pre-merge checks: Passed 5
Check name Status Explanation
Title check Passed The title names MetalLB public endpoint reuse, service certificate SANs, bring-your-own certificate, and front port. The changeset implements these: SAN set from GetVHostServerName, BYO pod certs via …
Docstring Coverage Passed Docstring coverage is 93.33% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentati…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description directly explains the MetalLB LoadBalancer endpoint, TLS certificate, BYO secret, port, Service watch, and reconciliation changes in the pull request.
Full details: Title check

Explanation

The title names MetalLB public endpoint reuse, service certificate SANs, bring-your-own certificate, and front port. The changeset implements these: SAN set from GetVHostServerName, BYO pod certs via tls.api.{public,internal}.secretName, and legacy front port injection for TLS LoadBalancer public endpoints. The title is long (85 characters) and uses a bracketed prefix, but it describes real, primary changes.

Full details: Docstring Coverage

Explanation

Docstring coverage is 93.33% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 30 functions across 22 files. (2 skipped: 2 unsupported.)

Finishing Touches
Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

ast-grep (0.51.1)

ast-grep could not parse rule config: /ast-grep-rules/ast-grep-essentials/rules/rust/security/tokio-postgres-empty-password-rust.yml

Tip

🎯 Customize your risk assessments

Guide how CodeRabbit assesses merge risk and security architecture risk. Highlight sensitive changes and explain your security boundaries using Merge Risk Instructions and Security Risk Instructions in your review settings. Security architecture risk assessments require the Advanced plan or above.

✅ Automatic approvals, with your rules

Let CodeRabbit approve pull requests once required threads are resolved, pre-merge checks pass, and your risk limits are met. Enable Automatic approvals in review settings, with overrides for teams or file paths.

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.
Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Central YAML (base), Organization UI (inherited)
  • Review profile: CHILL
  • Plan: Enterprise
  • Run ID: a15be832-4e10-4720-8d13-39a220df2a83
Commits

Reviewing files that changed from the base of the PR and between 6c94a2b and 010bb6e.

Files ignored due to path filters: 2
  • api/go.sum is excluded by !**/*.sum
  • go.sum is excluded by !**/*.sum
Files selected for processing: 20
  • api/go.mod
  • go.mod
  • internal/openstack/barbican.go
  • internal/openstack/cinder.go
  • internal/openstack/common.go
  • internal/openstack/common_test.go
  • internal/openstack/designate.go
  • internal/openstack/glance.go
  • internal/openstack/heat.go
  • internal/openstack/horizon.go
  • internal/openstack/ironic.go
  • internal/openstack/keystone.go
  • internal/openstack/manila.go
  • internal/openstack/neutron.go
  • internal/openstack/nova.go
  • internal/openstack/octavia.go
  • internal/openstack/placement.go
  • internal/openstack/swift.go
  • internal/openstack/telemetry.go
  • internal/openstack/watcher.go

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown

OpenStackControlPlane CRD Size Report

Metric Value
CRD JSON size 345173 bytes (337KB)
Base branch size 345173 bytes
Change +0.00%
Status yellow — growing
Threshold reference
Color Range Meaning
🟢 green < 300KB Comfortable
🟡 yellow 300–400KB Growing
🟠 orange 400–750KB Concerning
🔴 red > 750KB Approaching 1.5MB etcd limit (cut in half to allow space for update)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional comments: 1
internal/openstack/common.go-413-427: 1

413-427: Major · Functional Correctness · Heavy lift

Unverified finding
Verification ran but could not confirm this finding. It is shown for review, not as a verified issue.

The legacy front-port default changes the public port of existing TLS LoadBalancer deployments on upgrade.

This block has no opt-in. It runs for every TLS LoadBalancer public endpoint without a Route that has one live port named public. If endpointURL is unset, it injects endpoint.GetLegacyPublicTLSPort(legacyServiceName) into ports[].

Consider an existing RHOSO deployment that already exposes a public endpoint through MetalLB on its native port, for example keystone on 5000. After the service operator renames the port to public, the ServicePortOrTypeChangedPredicate watch fires. The next reconcile then moves the LoadBalancer front port to the director legacy value, for example 13000. The catalog follows the live Service. External clients, firewall rules, and DNS/VIP configurations that target the old port stop working. The user did not request this change.

The feature targets RHOSP-to-RHOSO adoption, so the legacy default should apply only when the user requests it. Options:

  • Inject only when endpointURL is set, and derive the port from that URL. This path is already explicit user input.
  • Gate the legacy-port lookup behind an explicit opt-in, such as an annotation or a spec flag.
♻️ Minimal fix: drop the implicit legacy default
 				if p, ok, err := service.GetEndpointURLPort(ed.Service.OverrideSpec.EndpointURL); err == nil && ok {
 					desiredPort, wantInject = p, true
-				} else if ed.Service.OverrideSpec.EndpointURL == nil || *ed.Service.OverrideSpec.EndpointURL == "" {
-					if legacyPort, ok := endpoint.GetLegacyPublicTLSPort(legacyServiceName); ok {
-						desiredPort, wantInject = legacyPort, true
-					}
-				}
+				} else if legacyPortOptIn(&svc) { // explicit adoption opt-in
+					if legacyPort, ok := endpoint.GetLegacyPublicTLSPort(legacyServiceName); ok {
+						desiredPort, wantInject = legacyPort, true
+					}
+				}

Review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Central YAML (base), Organization UI (inherited)
  • Review profile: CHILL
  • Plan: Enterprise
  • Run ID: 992fceb9-dcc2-4b15-8c14-438b7cbc7f1e
Commits

Reviewing files that changed from the base of the PR and between ea0a6b8 and 6c94a2b.

Files ignored due to path filters: 2
  • api/go.sum is excluded by !**/*.sum
  • go.sum is excluded by !**/*.sum
Files selected for processing: 24
  • api/go.mod
  • go.mod
  • internal/controller/core/openstackcontrolplane_controller.go
  • internal/controller/core/openstackcontrolplane_controller_test.go
  • internal/openstack/barbican.go
  • internal/openstack/cinder.go
  • internal/openstack/common.go
  • internal/openstack/common_test.go
  • internal/openstack/designate.go
  • internal/openstack/glance.go
  • internal/openstack/heat.go
  • internal/openstack/horizon.go
  • internal/openstack/ironic.go
  • internal/openstack/keystone.go
  • internal/openstack/manila.go
  • internal/openstack/neutron.go
  • internal/openstack/nova.go
  • internal/openstack/octavia.go
  • internal/openstack/placement.go
  • internal/openstack/swift.go
  • internal/openstack/telemetry.go
  • internal/openstack/watcher.go
  • test/functional/ctlplane/openstackoperator_controller_test.go
  • test/functional/ctlplane/secret_watch_test.go

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.

@centosinfra-prod-github-app

Copy link
Copy Markdown

Build succeeded (check pipeline).
https://gateway-cloud-softwarefactory.apps.ocp.cloud.ci.centos.org/zuul/t/rdoproject.org/buildset/2f0a6c5ef3164320ab513628f09c7435

✔️ openstack-k8s-operators-content-provider SUCCESS in 2h 50m 15s
✔️ openstack-operator-content-provider-cs10 SUCCESS in 1h 58m 17s
✔️ podified-multinode-edpm-deployment-crc SUCCESS in 1h 22m 30s
✔️ openstack-operator-crc-podified-edpm-baremetal SUCCESS in 1h 25m 25s
✔️ podified-multinode-edpm-deployment-crc-antelope SUCCESS in 1h 19m 04s
❌ adoption-standalone-to-crc-ceph-provider FAILURE in 1h 54m 58s (non-voting)
✔️ openstack-operator-tempest-multinode SUCCESS in 1h 49m 58s
❌ openstack-operator-edpm-baremetal-minor-update FAILURE in 2h 03m 14s (non-voting)

Warning:
File .ssh/known_hosts.old found in build home directory during playbook review.rdoproject.org/config/playbooks/base-minimal/post-logs.yaml for job podified-multinode-edpm-deployment-crc-antelope. Add to preserve-home-paths if safe, or otherwise remove.
File .ssh/known_hosts.old found in build home directory during playbook review.rdoproject.org/config/playbooks/base-minimal/post-logs.yaml for job podified-multinode-edpm-deployment-crc. Add to preserve-home-paths if safe, or otherwise remove.
File .ssh/known_hosts.old found in build home directory during playbook review.rdoproject.org/config/playbooks/base-minimal/post-logs.yaml for job openstack-operator-tempest-multinode. Add to preserve-home-paths if safe, or otherwise remove.
File .ssh/known_hosts.old found in build home directory during playbook review.rdoproject.org/config/playbooks/base-minimal/post-logs.yaml for job adoption-standalone-to-crc-ceph-provider. Add to preserve-home-paths if safe, or otherwise remove.

stuggi and others added 2 commits November 3, 2026 08:40
…port name

Support a MetalLB LoadBalancer public endpoint with a custom hostname,
certificate and port (RHOSP 17.1 -> RHOSO 18 adoption).

- Service cert: SANs come from the canonical service.GetVHostServerName set
  ({custom host, short, FQDN}); the custom endpointURL host is added for
  public and internal endpoints, a bad endpointURL falls back to the
  in-cluster names and is logged. A bring-your-own pod-level cert is taken
  from tls.api.<endpoint>.secretName (snapshotted pre-restore at every
  EnsureEndpointConfig call site), else one is issued. It applies to the
  LoadBalancer public endpoint and the internal endpoint; a Route-backed
  public endpoint keeps the operator-issued cert (a custom cert there goes
  on the Route via apiOverride.tls.secretName, which is validated only when
  a Route exists). Fix the cert cleanup delete target (Service cert, not
  Route cert).
- Front port: for a TLS LoadBalancer public endpoint with an endpointURL,
  inject a ports[] override with the endpointURL port (the scheme default
  when the URL has none). There is no implicit director legacy port: without
  an endpointURL the Service keeps its native port, and adopters who keep a
  RHOSP 17.1 front port put it in the endpointURL (e.g.
  https://keystone.example.com:13000). Injection is skipped for non-TLS or
  routed endpoints, non-LoadBalancer Services, multi-port Services, and
  while the live port is not yet named after its endpoint type ("public"),
  since an override carrying the old <service>-public name is rejected by
  the service operator's by-name port merge before the Service is patched.
  A watch on managed Services' port names and type wakes the control plane
  when the service operator renames the port. The override Spec is copied
  before it is mutated.
- Routes: CreateRoute reads the target port name from the live Service
  instead of assuming it equals the object name. A user endpointURL is kept
  when a Route is replaced by a LoadBalancer.
- Glance: return the requeue of the API that asked for it, and do not let a
  later endpoint's valid secret overwrite CustomTLSReady False set earlier
  in the same reconcile.

Jira: OSPRH-39364
Jira: OSPRH-39366

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Martin Schuppert <mschuppert@redhat.com>
…anges

When a user updates the content of a secret referenced by a service
template's bring-your-own pod level cert - tls.api.{public,internal}
.secretName, or the simple TLS sections of horizon, novncproxy,
prometheus and alertmanager - the control plane was not reconciled, so a
missing or invalid secret was only noticed through the requeue.

Index those secret names of all services so the existing Secret watch
enqueues the control plane, like apiOverride.tls.secretName. The tls.api
names are collected by walking the spec for tls.APIService, so new
services are covered without a list to maintain; the simple TLS sections
are read explicitly. Operator owned secrets (ovn, vencrypt, metadata) are
not indexed. The indexed spec comes from the informer cache, i.e. the
persisted user input, never the operator-issued names written back in
memory.

Add unit tests for the secret name collection and for findObjectsForSrc.

Jira: OSPRH-39364
Jira: OSPRH-39366

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Martin Schuppert <mschuppert@redhat.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant