Skip to content

Commit e4ddd09

Browse files
Merge pull request #2207 from stuggi/lb-pub-endpt-reuse
[lb pub endpt] MetalLB public endpoint reuse: service cert SANs, BYO cert, front port
2 parents fbc23a8 + 7220df9 commit e4ddd09

22 files changed

Lines changed: 2362 additions & 110 deletions

‎internal/controller/core/openstackcontrolplane_controller.go‎

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"context"
2222
"fmt"
2323
"maps"
24+
"reflect"
2425
"slices"
2526
"strings"
2627
"time"
@@ -41,6 +42,7 @@ import (
4142
"github.com/openstack-k8s-operators/lib-common/modules/common/backup"
4243
condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition"
4344
common_helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper"
45+
"github.com/openstack-k8s-operators/lib-common/modules/common/tls"
4446
"github.com/openstack-k8s-operators/lib-common/modules/common/webhook"
4547
corev1 "k8s.io/api/core/v1"
4648

@@ -125,7 +127,7 @@ func (r *OpenStackControlPlaneReconciler) GetLogger(ctx context.Context) logr.Lo
125127
// +kubebuilder:rbac:groups=redis.openstack.org,resources=redises,verbs=get;list;watch;create;update;patch;delete
126128
// +kubebuilder:rbac:groups=route.openshift.io,resources=routes,verbs=get;list;watch;create;update;patch;delete;
127129
// +kubebuilder:rbac:groups=route.openshift.io,resources=routes/custom-host,verbs=create;update;patch
128-
// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;
130+
// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch
129131
// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete;
130132
// +kubebuilder:rbac:groups=cert-manager.io,resources=issuers,verbs=get;list;watch;create;update;patch;delete;
131133
// +kubebuilder:rbac:groups=cert-manager.io,resources=certificates,verbs=get;list;watch;create;update;patch;delete;
@@ -228,6 +230,17 @@ func (r *OpenStackControlPlaneReconciler) Reconcile(ctx context.Context, req ctr
228230
instance.Status.ObservedGeneration = instance.Generation
229231

230232
// If we're not deleting this and the service object doesn't have our finalizer, add it.
233+
//
234+
// This must stay the only metadata write that is followed by more spec mutation, and
235+
// it returns before any is made. The reconcilers mutate instance.Spec in memory (issued
236+
// cert secret names, overrides, CA bundle names), and the deferred PatchInstance builds
237+
// its MergeFrom patch on the main resource whenever the metadata changed - that patch
238+
// would then persist those spec changes. The persisted spec is the user input the
239+
// reconcilers snapshot as the bring-your-own cert signal and the secret field indexes read;
240+
// persisted operator-issued names would be mistaken for BYO secrets (see resolveServiceCert).
241+
// The finalizer removal in reconcileDelete is safe for the same reason: no spec mutation
242+
// has happened on that path. Do not add annotation/label/finalizer writes after the spec
243+
// mutations without going through a separate patch.
231244
if instance.DeletionTimestamp.IsZero() && controllerutil.AddFinalizer(instance, helper.GetFinalizer()) || isNewInstance {
232245
return ctrl.Result{}, nil
233246
}
@@ -793,6 +806,7 @@ const (
793806
tlsPodLevelInternalCACustomIssuer = ".spec.tls.podLevel.internal.ca.customIssuer"
794807
tlsPodLevelOvnCACustomIssuer = ".spec.tls.podLevel.ovn.ca.customIssuer"
795808
apiOverrideTLSSecretField = ".spec.apiOverride.tls.secretName"
809+
tlsServiceSecretField = ".spec.template.tls.serviceSecretName"
796810
)
797811

798812
// fieldIndexers maps each indexed field to the func extracting its value(s)
@@ -836,6 +850,8 @@ var fieldIndexers = map[string]client.IndexerFunc{
836850
},
837851
// the custom public endpoint cert secrets of all services
838852
apiOverrideTLSSecretField: indexAPIOverrideTLSSecret,
853+
// the bring-your-own pod level service cert secrets of all services
854+
tlsServiceSecretField: indexTLSServiceSecret,
839855
}
840856

841857
// SetupWithManager sets up the controller with the Manager.
@@ -900,6 +916,13 @@ func (r *OpenStackControlPlaneReconciler) SetupWithManager(
900916
handler.EnqueueRequestsFromMapFunc(r.findControlPlaneForSrc),
901917
builder.WithPredicates(backup.AnnotationChangedPredicate(openstack.ServiceCertSelector)),
902918
).
919+
// the service operators rename the Service ports and set its type; the
920+
// endpointURL front port is injected only after that, see EnsureEndpointConfig
921+
Watches(
922+
&corev1.Service{},
923+
handler.EnqueueRequestsFromMapFunc(r.findControlPlaneForSrc),
924+
builder.WithPredicates(openstack.ServicePortOrTypeChangedPredicate()),
925+
).
903926
Complete(r)
904927
}
905928

@@ -948,6 +971,83 @@ func apiOverrideTLSSecretNames(spec *corev1beta1.OpenStackControlPlaneSpec) []st
948971
return names
949972
}
950973

974+
// indexTLSServiceSecret is the field index func for tlsServiceSecretField
975+
func indexTLSServiceSecret(rawObj client.Object) []string {
976+
return tlsServiceSecretNames(&rawObj.(*corev1beta1.OpenStackControlPlane).Spec)
977+
}
978+
979+
// tlsServiceSecretNames returns the bring-your-own pod level service cert secret
980+
// names, i.e. the user input the operator reads in EnsureEndpointConfig:
981+
// tls.api.{public,internal} of all services (found by walking the spec, so new
982+
// services are covered) and the simple TLS sections of horizon, novncproxy,
983+
// prometheus and alertmanager. Operator owned secrets (ovn, vencrypt, metadata, ...)
984+
// are deliberately not indexed. The indexed spec comes from the informer cache,
985+
// i.e. the persisted user input - never the operator-issued names written back in
986+
// memory.
987+
func tlsServiceSecretNames(spec *corev1beta1.OpenStackControlPlaneSpec) []string {
988+
names := tlsAPISecretNames(reflect.ValueOf(spec))
989+
990+
secrets := []*string{}
991+
if t := spec.Horizon.Template; t != nil {
992+
secrets = append(secrets, t.TLS.SecretName)
993+
}
994+
if t := spec.Nova.Template; t != nil {
995+
for _, cell := range t.CellTemplates {
996+
secrets = append(secrets, cell.NoVNCProxyServiceTemplate.TLS.Service.SecretName)
997+
}
998+
}
999+
if t := spec.Telemetry.Template; t != nil {
1000+
secrets = append(secrets, t.MetricStorage.PrometheusTLS.SecretName, t.MetricStorage.AlertmanagerTLS.SecretName)
1001+
}
1002+
for _, sn := range secrets {
1003+
if sn != nil && *sn != "" {
1004+
names = append(names, *sn)
1005+
}
1006+
}
1007+
return names
1008+
}
1009+
1010+
var tlsAPIServiceType = reflect.TypeFor[tls.APIService]()
1011+
1012+
// tlsAPISecretNames walks v for tls.APIService and returns the secret names of
1013+
// its public and internal endpoint.
1014+
func tlsAPISecretNames(v reflect.Value) []string {
1015+
var names []string
1016+
switch v.Kind() {
1017+
case reflect.Pointer:
1018+
if !v.IsNil() {
1019+
names = tlsAPISecretNames(v.Elem())
1020+
}
1021+
case reflect.Struct:
1022+
if v.Type() == tlsAPIServiceType {
1023+
api := v.Interface().(tls.APIService)
1024+
for _, sn := range []*string{api.Public.SecretName, api.Internal.SecretName} {
1025+
if sn != nil && *sn != "" {
1026+
names = append(names, *sn)
1027+
}
1028+
}
1029+
break
1030+
}
1031+
for i := range v.NumField() {
1032+
if v.Type().Field(i).IsExported() {
1033+
names = append(names, tlsAPISecretNames(v.Field(i))...)
1034+
}
1035+
}
1036+
case reflect.Slice, reflect.Array:
1037+
// skip slices of scalars (e.g. []byte)
1038+
if k := v.Type().Elem().Kind(); k == reflect.Struct || k == reflect.Pointer || k == reflect.Map {
1039+
for i := range v.Len() {
1040+
names = append(names, tlsAPISecretNames(v.Index(i))...)
1041+
}
1042+
}
1043+
case reflect.Map:
1044+
for _, k := range v.MapKeys() {
1045+
names = append(names, tlsAPISecretNames(v.MapIndex(k))...)
1046+
}
1047+
}
1048+
return names
1049+
}
1050+
9511051
func (r *OpenStackControlPlaneReconciler) findObjectsForSrc(ctx context.Context, src client.Object) []reconcile.Request {
9521052
requests := []reconcile.Request{}
9531053

‎internal/controller/core/openstackcontrolplane_controller_test.go‎

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,16 @@ import (
77
"slices"
88
"testing"
99

10+
horizonv1 "github.com/openstack-k8s-operators/horizon-operator/api/v1beta1"
11+
keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1"
12+
"github.com/openstack-k8s-operators/lib-common/modules/common/tls"
13+
neutronv1 "github.com/openstack-k8s-operators/neutron-operator/api/v1beta1"
14+
novav1 "github.com/openstack-k8s-operators/nova-operator/api/nova/v1beta1"
15+
telemetryv1 "github.com/openstack-k8s-operators/telemetry-operator/api/v1beta1"
1016
corev1 "k8s.io/api/core/v1"
1117
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1218
"k8s.io/apimachinery/pkg/runtime"
19+
"k8s.io/utils/ptr"
1320
"sigs.k8s.io/controller-runtime/pkg/client"
1421
"sigs.k8s.io/controller-runtime/pkg/client/fake"
1522

@@ -94,6 +101,125 @@ func TestAPIOverrideTLSSecretNames(t *testing.T) {
94101
}
95102
}
96103

104+
// setTLSAPISecrets walks v and sets a unique secretName on both endpoints of
105+
// every tls.APIService found, collecting the names.
106+
func setTLSAPISecrets(v reflect.Value, names *[]string) {
107+
switch v.Kind() {
108+
case reflect.Struct:
109+
if v.Type() == tlsAPIServiceType {
110+
api := v.Addr().Interface().(*tls.APIService)
111+
for _, svc := range []*tls.GenericService{&api.Public, &api.Internal} {
112+
name := fmt.Sprintf("tls-secret-%d", len(*names))
113+
svc.SecretName = &name
114+
*names = append(*names, name)
115+
}
116+
return
117+
}
118+
for i := range v.NumField() {
119+
if v.Type().Field(i).IsExported() {
120+
setTLSAPISecrets(v.Field(i), names)
121+
}
122+
}
123+
case reflect.Pointer:
124+
if v.IsNil() && v.Type().Elem().Kind() == reflect.Struct {
125+
v.Set(reflect.New(v.Type().Elem()))
126+
}
127+
if !v.IsNil() {
128+
setTLSAPISecrets(v.Elem(), names)
129+
}
130+
case reflect.Map:
131+
if v.Type().Elem().Kind() == reflect.Struct {
132+
v.Set(reflect.MakeMap(v.Type()))
133+
elem := reflect.New(v.Type().Elem()).Elem()
134+
setTLSAPISecrets(elem, names)
135+
v.SetMapIndex(reflect.ValueOf("key").Convert(v.Type().Key()), elem)
136+
}
137+
}
138+
}
139+
140+
// TestTLSServiceSecretNames verifies the tls.api secrets of every service in the
141+
// spec (incl. map values like glance) are collected.
142+
func TestTLSServiceSecretNames(t *testing.T) {
143+
spec := &corev1beta1.OpenStackControlPlaneSpec{}
144+
if got := tlsServiceSecretNames(spec); len(got) != 0 {
145+
t.Fatalf("expected no secrets for empty spec, got %v", got)
146+
}
147+
148+
var want []string
149+
setTLSAPISecrets(reflect.ValueOf(spec).Elem(), &want)
150+
if len(want) == 0 {
151+
t.Fatal("found no tls.api fields in the spec")
152+
}
153+
got := tlsServiceSecretNames(spec)
154+
slices.Sort(got)
155+
slices.Sort(want)
156+
if !slices.Equal(got, want) {
157+
t.Errorf("tls.api secret names not collected\nwant: %v\ngot: %v", want, got)
158+
}
159+
160+
// empty secretName is ignored
161+
spec = &corev1beta1.OpenStackControlPlaneSpec{}
162+
spec.Keystone.Template = &keystonev1.KeystoneAPISpecCore{}
163+
spec.Keystone.Template.TLS.API.Public.SecretName = ptr.To("")
164+
if got := tlsServiceSecretNames(spec); len(got) != 0 {
165+
t.Errorf("expected empty secretName to be ignored, got %v", got)
166+
}
167+
}
168+
169+
// TestTLSServiceSecretNamesSimpleTLS verifies the simple TLS sections the operator
170+
// reads as bring-your-own are indexed, and the operator owned ones are not.
171+
func TestTLSServiceSecretNamesSimpleTLS(t *testing.T) {
172+
spec := &corev1beta1.OpenStackControlPlaneSpec{}
173+
spec.Horizon.Template = &horizonv1.HorizonSpecCore{}
174+
spec.Horizon.Template.TLS.SecretName = ptr.To("horizon-cert")
175+
spec.Nova.Template = &novav1.NovaSpecCore{CellTemplates: map[string]novav1.NovaCellTemplate{"cell1": {}}}
176+
cell := spec.Nova.Template.CellTemplates["cell1"]
177+
cell.NoVNCProxyServiceTemplate.TLS.Service.SecretName = ptr.To("novnc-cert")
178+
cell.NoVNCProxyServiceTemplate.TLS.Vencrypt.SecretName = ptr.To("vencrypt-operator-owned")
179+
spec.Nova.Template.CellTemplates["cell1"] = cell
180+
spec.Telemetry.Template = &telemetryv1.TelemetrySpecCore{}
181+
spec.Telemetry.Template.MetricStorage.PrometheusTLS.SecretName = ptr.To("prometheus-cert")
182+
spec.Telemetry.Template.MetricStorage.AlertmanagerTLS.SecretName = ptr.To("alertmanager-cert")
183+
spec.Neutron.Template = &neutronv1.NeutronAPISpecCore{}
184+
spec.Neutron.Template.TLS.Ovn.SecretName = ptr.To("ovn-operator-owned")
185+
186+
got := tlsServiceSecretNames(spec)
187+
slices.Sort(got)
188+
want := []string{"alertmanager-cert", "horizon-cert", "novnc-cert", "prometheus-cert"}
189+
if !slices.Equal(got, want) {
190+
t.Errorf("want %v, got %v", want, got)
191+
}
192+
}
193+
194+
// TestFindObjectsForSrcTLSServiceSecret verifies a change of a BYO service cert
195+
// secret enqueues the control plane referencing it.
196+
func TestFindObjectsForSrcTLSServiceSecret(t *testing.T) {
197+
scheme := runtime.NewScheme()
198+
if err := corev1beta1.AddToScheme(scheme); err != nil {
199+
t.Fatal(err)
200+
}
201+
202+
cp := &corev1beta1.OpenStackControlPlane{ObjectMeta: metav1.ObjectMeta{Name: "cp", Namespace: "ns"}}
203+
cp.Spec.Keystone.Template = &keystonev1.KeystoneAPISpecCore{}
204+
cp.Spec.Keystone.Template.TLS.API.Internal.SecretName = ptr.To("byo-cert")
205+
206+
builder := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cp)
207+
for field, indexer := range fieldIndexers {
208+
builder = builder.WithIndex(&corev1beta1.OpenStackControlPlane{}, field, indexer)
209+
}
210+
r := &OpenStackControlPlaneReconciler{Client: builder.Build()}
211+
212+
secret := func(name string) client.Object {
213+
return &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "ns"}}
214+
}
215+
if got := r.findObjectsForSrc(context.Background(), secret("byo-cert")); len(got) != 1 || got[0].Name != "cp" {
216+
t.Errorf("expected control plane to be enqueued, got %v", got)
217+
}
218+
if got := r.findObjectsForSrc(context.Background(), secret("other")); len(got) != 0 {
219+
t.Errorf("expected no requests for unrelated secret, got %v", got)
220+
}
221+
}
222+
97223
// TestFindObjectsForSrcAPIOverrideTLSSecret verifies a change of a custom
98224
// route cert secret enqueues the control plane referencing it.
99225
func TestFindObjectsForSrcAPIOverrideTLSSecret(t *testing.T) {

‎internal/openstack/barbican.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ func ReconcileBarbican(ctx context.Context, instance *corev1beta1.OpenStackContr
108108
instance.Spec.Barbican.Template.Auth.ApplicationCredentialSecret = acSecretName
109109
}
110110

111+
// snapshot pre-restore BYO service cert secret (pure user input; see resolveServiceCert)
112+
barbicanUserTLS := instance.Spec.Barbican.Template.BarbicanAPI.TLS
111113
// preserve any previously set TLS certs, set CA cert
112114
if instance.Spec.TLS.PodLevel.Enabled {
113115
instance.Spec.Barbican.Template.BarbicanAPI.TLS = barbican.Spec.BarbicanAPI.TLS
@@ -136,7 +138,7 @@ func ReconcileBarbican(ctx context.Context, instance *corev1beta1.OpenStackContr
136138
instance.Spec.Barbican.APIOverride,
137139
corev1beta1.OpenStackControlPlaneExposeBarbicanReadyCondition,
138140
false, // TODO: (mschuppert) could be removed when all integrated service support TLS
139-
instance.Spec.Barbican.Template.BarbicanAPI.TLS,
141+
barbicanUserTLS,
140142
)
141143
if err != nil {
142144
return ctrlResult, err

‎internal/openstack/cinder.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ func ReconcileCinder(ctx context.Context, instance *corev1beta1.OpenStackControl
132132
instance.Spec.Cinder.Template.Auth.ApplicationCredentialSecret = acSecretName
133133
}
134134

135+
// snapshot pre-restore BYO service cert secret (pure user input; see resolveServiceCert)
136+
cinderUserTLS := instance.Spec.Cinder.Template.CinderAPI.TLS
135137
// preserve any previously set TLS certs,set CA cert
136138
if instance.Spec.TLS.PodLevel.Enabled {
137139
instance.Spec.Cinder.Template.CinderAPI.TLS = cinder.Spec.CinderAPI.TLS
@@ -160,7 +162,7 @@ func ReconcileCinder(ctx context.Context, instance *corev1beta1.OpenStackControl
160162
instance.Spec.Cinder.APIOverride,
161163
corev1beta1.OpenStackControlPlaneExposeCinderReadyCondition,
162164
false, // TODO (mschuppert) could be removed when all integrated service support TLS
163-
instance.Spec.Cinder.Template.CinderAPI.TLS,
165+
cinderUserTLS,
164166
)
165167
if err != nil {
166168
return ctrlResult, err

0 commit comments

Comments
 (0)