@@ -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+
9511051func (r * OpenStackControlPlaneReconciler ) findObjectsForSrc (ctx context.Context , src client.Object ) []reconcile.Request {
9521052 requests := []reconcile.Request {}
9531053
0 commit comments