From 0e4e74718ba59ea28f4c61482da9d9a0c04041f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Douglas=20Mendiz=C3=A1bal?= Date: Thu, 4 Dec 2025 16:43:29 -0500 Subject: [PATCH 1/5] Add support for External Keystone Service This patch adds a new `ExternalKeystoneAPI` property to KeystoneAPI to enable the use of an existing Keystone Service that is external to the OpenShift environment used to run this operator. For example, a multi-region deployment where one region is running a centralized Keystone service can use this to deploy additional regions that can use the centralized Keystone service without the need to run their own instance of Keystone. Assisted-by: Cursor (Auto Model) --- .../keystone.openstack.org_keystoneapis.yaml | 5 + api/v1beta1/conditions.go | 21 ++ api/v1beta1/keystoneapi.go | 15 +- api/v1beta1/keystoneapi_types.go | 13 + .../keystone.openstack.org_keystoneapis.yaml | 5 + internal/controller/keystoneapi_controller.go | 260 ++++++++++++++---- .../controller/keystoneendpoint_controller.go | 6 + .../controller/keystoneservice_controller.go | 8 +- 8 files changed, 270 insertions(+), 63 deletions(-) diff --git a/api/bases/keystone.openstack.org_keystoneapis.yaml b/api/bases/keystone.openstack.org_keystoneapis.yaml index 3e57d2e9..77e2098c 100644 --- a/api/bases/keystone.openstack.org_keystoneapis.yaml +++ b/api/bases/keystone.openstack.org_keystoneapis.yaml @@ -98,6 +98,11 @@ spec: description: EnableSecureRBAC - Enable Consistent and Secure RBAC policies type: boolean + externalKeystoneAPI: + default: false + description: ExternalKeystoneAPI - Enable use of external Keystone + API endpoints instead of deploying a local Keystone API + type: boolean extraMounts: default: [] description: ExtraMounts containing conf files diff --git a/api/v1beta1/conditions.go b/api/v1beta1/conditions.go index e263abda..f31899a4 100644 --- a/api/v1beta1/conditions.go +++ b/api/v1beta1/conditions.go @@ -111,4 +111,25 @@ const ( // KeystoneServiceOSUserReadyErrorMessage KeystoneServiceOSUserReadyErrorMessage = "Keystone Service user error occured %s" + + // + // External Keystone API condition messages + // + // ExternalKeystoneAPIDBMessage + ExternalKeystoneAPIDBMessage = "External Keystone API configured - database is not managed by this operator" + + // ExternalKeystoneAPIDBAccountMessage + ExternalKeystoneAPIDBAccountMessage = "External Keystone API configured - database account is not managed by this operator" + + // ExternalKeystoneAPIRabbitMQTransportURLMessage + ExternalKeystoneAPIRabbitMQTransportURLMessage = "External Keystone API configured - RabbitMQ is not managed by this operator" + + // ExternalKeystoneAPIMemcachedReadyMessage + ExternalKeystoneAPIMemcachedReadyMessage = "External Keystone API configured - memcached is not managed by this operator" + + // ExternalKeystoneAPIServiceConfigReadyMessage + ExternalKeystoneAPIServiceMessage = "External Keystone API configured - service is not managed by this operator" + + // ExternalKeystoneAPINetworkAttachmentsReadyMessage + ExternalKeystoneAPINetworkAttachmentsReadyMessage = "External Keystone API configured - network attachments are not managed by this operator" ) diff --git a/api/v1beta1/keystoneapi.go b/api/v1beta1/keystoneapi.go index b75b703d..832200b7 100644 --- a/api/v1beta1/keystoneapi.go +++ b/api/v1beta1/keystoneapi.go @@ -26,7 +26,6 @@ import ( "github.com/openstack-k8s-operators/lib-common/modules/common/endpoint" "github.com/openstack-k8s-operators/lib-common/modules/common/helper" "github.com/openstack-k8s-operators/lib-common/modules/common/secret" - "github.com/openstack-k8s-operators/lib-common/modules/common/tls" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/predicate" @@ -122,6 +121,7 @@ func GetAdminServiceClient( ctx context.Context, h *helper.Helper, keystoneAPI *KeystoneAPI, + endpointInterface ...endpoint.Endpoint, ) (*openstack.OpenStack, ctrl.Result, error) { os, ctrlResult, err := GetScopedAdminServiceClient( ctx, @@ -130,6 +130,7 @@ func GetAdminServiceClient( &gophercloud.AuthScope{ System: true, }, + endpointInterface..., ) if err != nil { return nil, ctrlResult, err @@ -144,9 +145,15 @@ func GetScopedAdminServiceClient( h *helper.Helper, keystoneAPI *KeystoneAPI, scope *gophercloud.AuthScope, + endpointInterface ...endpoint.Endpoint, ) (*openstack.OpenStack, ctrl.Result, error) { - // get public endpoint as authurl from keystone instance - authURL, err := keystoneAPI.GetEndpoint(endpoint.EndpointInternal) + // get endpoint as authurl from keystone instance + // default to internal endpoint if not specified + epInterface := endpoint.EndpointInternal + if len(endpointInterface) > 0 { + epInterface = endpoint.Endpoint(endpointInterface[0]) + } + authURL, err := keystoneAPI.GetEndpoint(epInterface) if err != nil { return nil, ctrl.Result{}, err } @@ -163,7 +170,7 @@ func GetScopedAdminServiceClient( h, keystoneAPI.Spec.TLS.CaBundleSecretName, 10*time.Second, - tls.InternalCABundleKey) + interfaceBundleKeys[epInterface]) if err != nil { return nil, ctrl.Result{}, err } diff --git a/api/v1beta1/keystoneapi_types.go b/api/v1beta1/keystoneapi_types.go index c65ab6c7..7c2b0ec7 100644 --- a/api/v1beta1/keystoneapi_types.go +++ b/api/v1beta1/keystoneapi_types.go @@ -53,6 +53,14 @@ const ( APIDefaultTimeout = 60 ) +var ( + // interfaceBundleKeys maps endpoint winterfaces to their corresponding key in the CA bundle secret + interfaceBundleKeys = map[endpoint.Endpoint]string{ + endpoint.EndpointInternal: tls.InternalCABundleKey, + endpoint.EndpointPublic: tls.CABundleKey, + } +) + // KeystoneAPISpec defines the desired state of KeystoneAPI type KeystoneAPISpec struct { KeystoneAPISpecCore `json:",inline"` @@ -213,6 +221,11 @@ type KeystoneAPISpecCore struct { // This is only needed when multiple realms are federated. // Config files mount path is set to /var/lib/httpd/metadata/ FederatedRealmConfig string `json:"federatedRealmConfig"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default=false + // ExternalKeystoneAPI - Enable use of external Keystone API endpoints instead of deploying a local Keystone API + ExternalKeystoneAPI bool `json:"externalKeystoneAPI"` } // APIOverrideSpec to override the generated manifest of several child resources. diff --git a/config/crd/bases/keystone.openstack.org_keystoneapis.yaml b/config/crd/bases/keystone.openstack.org_keystoneapis.yaml index 3e57d2e9..77e2098c 100644 --- a/config/crd/bases/keystone.openstack.org_keystoneapis.yaml +++ b/config/crd/bases/keystone.openstack.org_keystoneapis.yaml @@ -98,6 +98,11 @@ spec: description: EnableSecureRBAC - Enable Consistent and Secure RBAC policies type: boolean + externalKeystoneAPI: + default: false + description: ExternalKeystoneAPI - Enable use of external Keystone + API endpoints instead of deploying a local Keystone API + type: boolean extraMounts: default: [] description: ExtraMounts containing conf files diff --git a/internal/controller/keystoneapi_controller.go b/internal/controller/keystoneapi_controller.go index 6f0771e7..50733c3b 100644 --- a/internal/controller/keystoneapi_controller.go +++ b/internal/controller/keystoneapi_controller.go @@ -244,6 +244,11 @@ func (r *KeystoneAPIReconciler) Reconcile(ctx context.Context, req ctrl.Request) return r.reconcileDelete(ctx, instance, helper) } + // Check if external Keystone API is configured + if instance.Spec.ExternalKeystoneAPI { + return r.reconcileExternalKeystoneAPI(ctx, instance, helper) + } + // Handle non-deleted clusters return r.reconcileNormal(ctx, instance, helper) } @@ -451,6 +456,14 @@ func (r *KeystoneAPIReconciler) reconcileDelete(ctx context.Context, instance *k Log := r.GetLogger(ctx) Log.Info("Reconciling Service delete") + // If using external Keystone API, we don't have any resources to clean up + if instance.Spec.ExternalKeystoneAPI { + // Just remove the finalizer + controllerutil.RemoveFinalizer(instance, helper.GetFinalizer()) + Log.Info("Reconciled External Keystone API delete successfully") + return ctrl.Result{}, nil + } + // We need to allow all KeystoneEndpoint and KeystoneService processing to finish // in the case of a delete before we remove the finalizers. For instance, in the // case of the Memcached dependency, if Memcached is deleted before all Keystone @@ -747,6 +760,184 @@ func (r *KeystoneAPIReconciler) reconcileInit( return ctrl.Result{}, nil } +func (r *KeystoneAPIReconciler) reconcileExternalKeystoneAPI( + ctx context.Context, + instance *keystonev1.KeystoneAPI, + helper *helper.Helper, +) (ctrl.Result, error) { + Log := r.GetLogger(ctx) + Log.Info("Reconciling External Keystone API") + + // When using external Keystone API, we skip all deployment logic + // and just use the endpoints from the override spec + + // serviceLabels? + + // Verify override spec is valid + if instance.Spec.Override.Service == nil { + instance.Status.Conditions.Set(condition.FalseCondition( + condition.InputReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + condition.InputReadyWaitingMessage)) + return ctrl.Result{}, nil + } + + configMapVars := make(map[string]env.Setter) + + // Verify secret is available (needed for admin client operations) + ctrlResult, err := r.verifySecret(ctx, instance, helper, configMapVars) + if err != nil { + return ctrlResult, err + } else if (ctrlResult != ctrl.Result{}) { + return ctrlResult, nil + } + + // service DB is not needed + instance.Status.Conditions.MarkTrue(mariadbv1.MariaDBAccountReadyCondition, keystonev1.ExternalKeystoneAPIDBAccountMessage) + instance.Status.Conditions.MarkTrue(condition.DBReadyCondition, keystonev1.ExternalKeystoneAPIDBMessage) + instance.Status.Conditions.MarkTrue(condition.DBSyncReadyCondition, keystonev1.ExternalKeystoneAPIDBMessage) + + // RabbitMQ transportURL is not needed + instance.Status.Conditions.MarkTrue(condition.RabbitMqTransportURLReadyCondition, keystonev1.ExternalKeystoneAPIRabbitMQTransportURLMessage) + + // memcached is not needed + instance.Status.Conditions.MarkTrue(condition.MemcachedReadyCondition, keystonev1.ExternalKeystoneAPIMemcachedReadyMessage) + + // Mark service conditions as ready since they're being managed externally + instance.Status.Conditions.MarkTrue(condition.ServiceAccountReadyCondition, "External Keystone API - no service account needed") + instance.Status.Conditions.MarkTrue(condition.RoleReadyCondition, "External Keystone API - no role needed") + instance.Status.Conditions.MarkTrue(condition.RoleBindingReadyCondition, "External Keystone API - no role binding needed") + instance.Status.Conditions.MarkTrue(condition.ServiceConfigReadyCondition, keystonev1.ExternalKeystoneAPIServiceMessage) + instance.Status.Conditions.MarkTrue(condition.BootstrapReadyCondition, keystonev1.ExternalKeystoneAPIServiceMessage) + instance.Status.Conditions.MarkTrue(condition.CreateServiceReadyCondition, keystonev1.ExternalKeystoneAPIServiceMessage) + instance.Status.Conditions.MarkTrue(condition.DeploymentReadyCondition, keystonev1.ExternalKeystoneAPIServiceMessage) + instance.Status.Conditions.MarkTrue(condition.CronJobReadyCondition, keystonev1.ExternalKeystoneAPIServiceMessage) + // Set ready count to 0 since we're not deploying anything + instance.Status.ReadyCount = 0 + + // Verify TLS input (CA cert secret if provided) + ctrlResult, err = r.verifyTLSInput(ctx, instance, helper, configMapVars) + if err != nil { + return ctrlResult, err + } else if (ctrlResult != ctrl.Result{}) { + return ctrlResult, nil + } + instance.Status.Conditions.MarkTrue(condition.TLSInputReadyCondition, condition.InputReadyMessage) + + // TODO: Do we need network annotations if we we dont have Keystone API? + instance.Status.Conditions.MarkTrue(condition.NetworkAttachmentsReadyCondition, keystonev1.ExternalKeystoneAPINetworkAttachmentsReadyMessage) + + // Add endpoints + + // Set API endpoints from externalKeystoneAPI spec + if instance.Status.APIEndpoints == nil { + instance.Status.APIEndpoints = map[string]string{} + } + // Copy endpoints from spec to status + for endpointType, data := range instance.Spec.Override.Service { + if data.EndpointURL != nil { + instance.Status.APIEndpoints[string(endpointType)] = *data.EndpointURL + } else { + instance.Status.Conditions.Set(condition.FalseCondition( + condition.InputReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + condition.InputReadyWaitingMessage)) + return ctrl.Result{}, nil + } + } + + Log.Info("Reconciled External Keystone API successfully") + return ctrl.Result{}, nil +} + +// verifySecret verifies the OpenStack secret exists and adds its hash to configMapVars +func (r *KeystoneAPIReconciler) verifySecret( + ctx context.Context, + instance *keystonev1.KeystoneAPI, + helper *helper.Helper, + configMapVars map[string]env.Setter, +) (ctrl.Result, error) { + // check for required OpenStack secret holding passwords for service/admin user and add hash to the vars map + // NOTE: VerifySecret handles the "not found" error and returns RequeueAfter ctrl.Result if so, so we don't + // need to check the error type here + hash, result, err := oko_secret.VerifySecret(ctx, types.NamespacedName{Name: instance.Spec.Secret, Namespace: instance.Namespace}, []string{"AdminPassword"}, helper.GetClient(), time.Second*10) + if err != nil { + instance.Status.Conditions.Set(condition.FalseCondition( + condition.InputReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + condition.InputReadyErrorMessage, + err.Error())) + return ctrl.Result{}, err + } else if (result != ctrl.Result{}) { + // This case is "secret not found". VerifySecret already logs a message for it. + // We treat this as a warning because it means that the service will not be able to start + // while we are waiting for the secret to be created manually by the user. + instance.Status.Conditions.Set(condition.FalseCondition( + condition.InputReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + condition.InputReadyWaitingMessage)) + return result, nil + } + + // Add hash to configMapVars if provided + if configMapVars != nil { + configMapVars[instance.Spec.Secret] = env.SetValue(hash) + } + + instance.Status.Conditions.MarkTrue(condition.InputReadyCondition, condition.InputReadyMessage) + + return ctrl.Result{}, nil +} +func (r *KeystoneAPIReconciler) verifyTLSInput( + ctx context.Context, + instance *keystonev1.KeystoneAPI, + helper *helper.Helper, + configMapVars map[string]env.Setter, +) (ctrl.Result, error) { + // Validate the CA cert secret if provided + if instance.Spec.TLS.CaBundleSecretName != "" { + hash, err := tls.ValidateCACertSecret( + ctx, + helper.GetClient(), + types.NamespacedName{ + Name: instance.Spec.TLS.CaBundleSecretName, + Namespace: instance.Namespace, + }, + ) + if err != nil { + if k8s_errors.IsNotFound(err) { + // Since the CA cert secret should have been manually created by the user and provided in the spec, + // we treat this as a warning because it means that the service will not be able to start. + instance.Status.Conditions.Set(condition.FalseCondition( + condition.TLSInputReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + condition.TLSInputReadyWaitingMessage, + instance.Spec.TLS.CaBundleSecretName)) + return ctrl.Result{}, nil + } + instance.Status.Conditions.Set(condition.FalseCondition( + condition.TLSInputReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + condition.TLSInputErrorMessage, + err.Error())) + return ctrl.Result{}, err + } + + if hash != "" { + if configMapVars != nil { + configMapVars[tls.CABundleKey] = env.SetValue(hash) + } + } + } + + return ctrl.Result{}, nil +} func (r *KeystoneAPIReconciler) reconcileUpdate(ctx context.Context) (ctrl.Result, error) { Log := r.GetLogger(ctx) Log.Info("Reconciling Service update") @@ -787,32 +978,13 @@ func (r *KeystoneAPIReconciler) reconcileNormal( // // check for required OpenStack secret holding passwords for service/admin user and add hash to the vars map - // NOTE: VerifySecret handles the "not found" error and returns RequeueAfter ctrl.Result if so, so we don't - // need to check the error type here // - hash, result, err := oko_secret.VerifySecret(ctx, types.NamespacedName{Name: instance.Spec.Secret, Namespace: instance.Namespace}, []string{"AdminPassword"}, helper.GetClient(), time.Second*10) + ctrlResult, err := r.verifySecret(ctx, instance, helper, configMapVars) if err != nil { - instance.Status.Conditions.Set(condition.FalseCondition( - condition.InputReadyCondition, - condition.ErrorReason, - condition.SeverityWarning, - condition.InputReadyErrorMessage, - err.Error())) - return ctrl.Result{}, err - } else if (result != ctrl.Result{}) { - // This case is "secret not found". VerifySecret already logs a message for it. - // We treat this as a warning because it means that the service will not be able to start - // while we are waiting for the secret to be created manually by the user. - instance.Status.Conditions.Set(condition.FalseCondition( - condition.InputReadyCondition, - condition.ErrorReason, - condition.SeverityWarning, - condition.InputReadyWaitingMessage)) - return result, nil + return ctrlResult, err + } else if (ctrlResult != ctrl.Result{}) { + return ctrlResult, nil } - configMapVars[instance.Spec.Secret] = env.SetValue(hash) - - instance.Status.Conditions.MarkTrue(condition.InputReadyCondition, condition.InputReadyMessage) // run check OpenStack secret - end @@ -970,40 +1142,12 @@ func (r *KeystoneAPIReconciler) reconcileNormal( // // TLS input validation // - // Validate the CA cert secret if provided - if instance.Spec.TLS.CaBundleSecretName != "" { - hash, err := tls.ValidateCACertSecret( - ctx, - helper.GetClient(), - types.NamespacedName{ - Name: instance.Spec.TLS.CaBundleSecretName, - Namespace: instance.Namespace, - }, - ) - if err != nil { - if k8s_errors.IsNotFound(err) { - // Since the CA cert secret should have been manually created by the user and provided in the spec, - // we treat this as a warning because it means that the service will not be able to start. - instance.Status.Conditions.Set(condition.FalseCondition( - condition.TLSInputReadyCondition, - condition.ErrorReason, - condition.SeverityWarning, - condition.TLSInputReadyWaitingMessage, - instance.Spec.TLS.CaBundleSecretName)) - return ctrl.Result{}, nil - } - instance.Status.Conditions.Set(condition.FalseCondition( - condition.TLSInputReadyCondition, - condition.ErrorReason, - condition.SeverityWarning, - condition.TLSInputErrorMessage, - err.Error())) - return ctrl.Result{}, err - } - - if hash != "" { - configMapVars[tls.CABundleKey] = env.SetValue(hash) - } + // Verify TLS input (CA cert secret if provided) + ctrlResult, err = r.verifyTLSInput(ctx, instance, helper, configMapVars) + if err != nil { + return ctrlResult, err + } else if (ctrlResult != ctrl.Result{}) { + return ctrlResult, nil } // Validate API service certs secrets @@ -1089,7 +1233,7 @@ func (r *KeystoneAPIReconciler) reconcileNormal( } // Handle service init - ctrlResult, err := r.reconcileInit(ctx, instance, helper, serviceLabels, serviceAnnotations, memcached) + ctrlResult, err = r.reconcileInit(ctx, instance, helper, serviceLabels, serviceAnnotations, memcached) if err != nil { return ctrlResult, err } else if (ctrlResult != ctrl.Result{}) { diff --git a/internal/controller/keystoneendpoint_controller.go b/internal/controller/keystoneendpoint_controller.go index 990bda97..9d0b371a 100644 --- a/internal/controller/keystoneendpoint_controller.go +++ b/internal/controller/keystoneendpoint_controller.go @@ -32,6 +32,7 @@ import ( "github.com/go-logr/logr" keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1" condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + "github.com/openstack-k8s-operators/lib-common/modules/common/endpoint" helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" util "github.com/openstack-k8s-operators/lib-common/modules/common/util" openstack "github.com/openstack-k8s-operators/lib-common/modules/openstack" @@ -212,10 +213,15 @@ func (r *KeystoneEndpointReconciler) Reconcile(ctx context.Context, req ctrl.Req // // get admin authentication OpenStack // + adminInterface := endpoint.EndpointInternal + if keystoneAPI.Spec.ExternalKeystoneAPI { + adminInterface = endpoint.EndpointPublic + } os, ctrlResult, err := keystonev1.GetAdminServiceClient( ctx, helper, keystoneAPI, + adminInterface, ) if err != nil { instance.Status.Conditions.Set(condition.FalseCondition( diff --git a/internal/controller/keystoneservice_controller.go b/internal/controller/keystoneservice_controller.go index 8433d397..461cee9e 100644 --- a/internal/controller/keystoneservice_controller.go +++ b/internal/controller/keystoneservice_controller.go @@ -25,6 +25,7 @@ import ( "github.com/go-logr/logr" keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1" condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + endpoint "github.com/openstack-k8s-operators/lib-common/modules/common/endpoint" helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" secret "github.com/openstack-k8s-operators/lib-common/modules/common/secret" @@ -214,10 +215,15 @@ func (r *KeystoneServiceReconciler) Reconcile(ctx context.Context, req ctrl.Requ // // get admin authentication OpenStack // + adminInterface := endpoint.EndpointInternal + if keystoneAPI.Spec.ExternalKeystoneAPI { + adminInterface = endpoint.EndpointPublic + } os, ctrlResult, err := keystonev1.GetAdminServiceClient( ctx, helper, keystoneAPI, + adminInterface, ) if err != nil { instance.Status.Conditions.Set(condition.FalseCondition( @@ -433,7 +439,7 @@ func (r *KeystoneServiceReconciler) reconcileService( instance.Spec.ServiceName, ) // If the service is not found, don't count that as an error here, - // it gets created bellow + // it gets created below if err != nil && !strings.Contains(err.Error(), openstack.ServiceNotFound) { return err } From cf6cdf0233d821dcf5cd82d0dea1f7ffc8707ef1 Mon Sep 17 00:00:00 2001 From: Ade Lee Date: Wed, 17 Dec 2025 16:20:44 -0500 Subject: [PATCH 2/5] Set region in status --- internal/controller/keystoneapi_controller.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/controller/keystoneapi_controller.go b/internal/controller/keystoneapi_controller.go index 50733c3b..a6ff89a7 100644 --- a/internal/controller/keystoneapi_controller.go +++ b/internal/controller/keystoneapi_controller.go @@ -847,6 +847,8 @@ func (r *KeystoneAPIReconciler) reconcileExternalKeystoneAPI( return ctrl.Result{}, nil } } + // Copy region from spec to status + instance.Status.Region = instance.Spec.Region Log.Info("Reconciled External Keystone API successfully") return ctrl.Result{}, nil From d2f5f277ced39cd54e671b426586173477f79d5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Douglas=20Mendiz=C3=A1bal?= Date: Thu, 18 Dec 2025 11:34:42 -0500 Subject: [PATCH 3/5] Generate OpenStackClientConfig for External API Generate the clouds.yaml for the External Keystone API. --- internal/controller/keystoneapi_controller.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/controller/keystoneapi_controller.go b/internal/controller/keystoneapi_controller.go index a6ff89a7..774648dc 100644 --- a/internal/controller/keystoneapi_controller.go +++ b/internal/controller/keystoneapi_controller.go @@ -847,9 +847,17 @@ func (r *KeystoneAPIReconciler) reconcileExternalKeystoneAPI( return ctrl.Result{}, nil } } - // Copy region from spec to status + // Copy region from spec to status instance.Status.Region = instance.Spec.Region + // + // create OpenStackClient config + // + err = r.reconcileCloudConfig(ctx, helper, instance) + if err != nil { + return ctrl.Result{}, err + } + Log.Info("Reconciled External Keystone API successfully") return ctrl.Result{}, nil } From 9b6ca98277faafcaf33e68af744552a629d4e810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Douglas=20Mendiz=C3=A1bal?= Date: Thu, 18 Dec 2025 13:10:30 -0500 Subject: [PATCH 4/5] Remove interface from GetAdminServiceClient Refactor the change added in this branch to pick the right bundle internally based on KeystoneAPI spec instead of making callers of GetAdminServiceClient figure that out. The client will continue to default to the internal interface, but use the public interface when using an external Keytone API. --- api/v1beta1/keystoneapi.go | 7 ++----- internal/controller/keystoneendpoint_controller.go | 6 ------ internal/controller/keystoneservice_controller.go | 6 ------ 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/api/v1beta1/keystoneapi.go b/api/v1beta1/keystoneapi.go index 832200b7..90953350 100644 --- a/api/v1beta1/keystoneapi.go +++ b/api/v1beta1/keystoneapi.go @@ -121,7 +121,6 @@ func GetAdminServiceClient( ctx context.Context, h *helper.Helper, keystoneAPI *KeystoneAPI, - endpointInterface ...endpoint.Endpoint, ) (*openstack.OpenStack, ctrl.Result, error) { os, ctrlResult, err := GetScopedAdminServiceClient( ctx, @@ -130,7 +129,6 @@ func GetAdminServiceClient( &gophercloud.AuthScope{ System: true, }, - endpointInterface..., ) if err != nil { return nil, ctrlResult, err @@ -145,13 +143,12 @@ func GetScopedAdminServiceClient( h *helper.Helper, keystoneAPI *KeystoneAPI, scope *gophercloud.AuthScope, - endpointInterface ...endpoint.Endpoint, ) (*openstack.OpenStack, ctrl.Result, error) { // get endpoint as authurl from keystone instance // default to internal endpoint if not specified epInterface := endpoint.EndpointInternal - if len(endpointInterface) > 0 { - epInterface = endpoint.Endpoint(endpointInterface[0]) + if keystoneAPI.Spec.ExternalKeystoneAPI { + epInterface = endpoint.Endpoint(endpoint.EndpointPublic) } authURL, err := keystoneAPI.GetEndpoint(epInterface) if err != nil { diff --git a/internal/controller/keystoneendpoint_controller.go b/internal/controller/keystoneendpoint_controller.go index 9d0b371a..990bda97 100644 --- a/internal/controller/keystoneendpoint_controller.go +++ b/internal/controller/keystoneendpoint_controller.go @@ -32,7 +32,6 @@ import ( "github.com/go-logr/logr" keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1" condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" - "github.com/openstack-k8s-operators/lib-common/modules/common/endpoint" helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" util "github.com/openstack-k8s-operators/lib-common/modules/common/util" openstack "github.com/openstack-k8s-operators/lib-common/modules/openstack" @@ -213,15 +212,10 @@ func (r *KeystoneEndpointReconciler) Reconcile(ctx context.Context, req ctrl.Req // // get admin authentication OpenStack // - adminInterface := endpoint.EndpointInternal - if keystoneAPI.Spec.ExternalKeystoneAPI { - adminInterface = endpoint.EndpointPublic - } os, ctrlResult, err := keystonev1.GetAdminServiceClient( ctx, helper, keystoneAPI, - adminInterface, ) if err != nil { instance.Status.Conditions.Set(condition.FalseCondition( diff --git a/internal/controller/keystoneservice_controller.go b/internal/controller/keystoneservice_controller.go index 461cee9e..52548456 100644 --- a/internal/controller/keystoneservice_controller.go +++ b/internal/controller/keystoneservice_controller.go @@ -25,7 +25,6 @@ import ( "github.com/go-logr/logr" keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1" condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" - endpoint "github.com/openstack-k8s-operators/lib-common/modules/common/endpoint" helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" secret "github.com/openstack-k8s-operators/lib-common/modules/common/secret" @@ -215,15 +214,10 @@ func (r *KeystoneServiceReconciler) Reconcile(ctx context.Context, req ctrl.Requ // // get admin authentication OpenStack // - adminInterface := endpoint.EndpointInternal - if keystoneAPI.Spec.ExternalKeystoneAPI { - adminInterface = endpoint.EndpointPublic - } os, ctrlResult, err := keystonev1.GetAdminServiceClient( ctx, helper, keystoneAPI, - adminInterface, ) if err != nil { instance.Status.Conditions.Set(condition.FalseCondition( From ebc9e3816c5be7af9eb9798ca8801eaccad10ec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Douglas=20Mendiz=C3=A1bal?= Date: Tue, 6 Jan 2026 13:01:16 -0500 Subject: [PATCH 5/5] Simplify verifyTLSInput function Removed the Result object from the helper fuction return value and refactored to return the error (or nil) to let the calling reconcile function figure out what to do with the error. This restores the original logic that was modified by the initial refactor. Previous to this patch the reconcile functions would continue to execute when the TLS secret was not found. --- internal/controller/keystoneapi_controller.go | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/internal/controller/keystoneapi_controller.go b/internal/controller/keystoneapi_controller.go index 774648dc..9852ea9c 100644 --- a/internal/controller/keystoneapi_controller.go +++ b/internal/controller/keystoneapi_controller.go @@ -817,11 +817,13 @@ func (r *KeystoneAPIReconciler) reconcileExternalKeystoneAPI( instance.Status.ReadyCount = 0 // Verify TLS input (CA cert secret if provided) - ctrlResult, err = r.verifyTLSInput(ctx, instance, helper, configMapVars) + err = r.verifyTLSInput(ctx, instance, helper, configMapVars) if err != nil { - return ctrlResult, err - } else if (ctrlResult != ctrl.Result{}) { - return ctrlResult, nil + if k8s_errors.IsNotFound(err) { + // Don't return NotFound error to the caller + return ctrl.Result{}, nil + } + return ctrl.Result{}, err } instance.Status.Conditions.MarkTrue(condition.TLSInputReadyCondition, condition.InputReadyMessage) @@ -907,7 +909,7 @@ func (r *KeystoneAPIReconciler) verifyTLSInput( instance *keystonev1.KeystoneAPI, helper *helper.Helper, configMapVars map[string]env.Setter, -) (ctrl.Result, error) { +) error { // Validate the CA cert secret if provided if instance.Spec.TLS.CaBundleSecretName != "" { hash, err := tls.ValidateCACertSecret( @@ -928,15 +930,15 @@ func (r *KeystoneAPIReconciler) verifyTLSInput( condition.SeverityWarning, condition.TLSInputReadyWaitingMessage, instance.Spec.TLS.CaBundleSecretName)) - return ctrl.Result{}, nil + } else { + instance.Status.Conditions.Set(condition.FalseCondition( + condition.TLSInputReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + condition.TLSInputErrorMessage, + err.Error())) } - instance.Status.Conditions.Set(condition.FalseCondition( - condition.TLSInputReadyCondition, - condition.ErrorReason, - condition.SeverityWarning, - condition.TLSInputErrorMessage, - err.Error())) - return ctrl.Result{}, err + return err } if hash != "" { @@ -946,7 +948,7 @@ func (r *KeystoneAPIReconciler) verifyTLSInput( } } - return ctrl.Result{}, nil + return nil } func (r *KeystoneAPIReconciler) reconcileUpdate(ctx context.Context) (ctrl.Result, error) { Log := r.GetLogger(ctx) @@ -1153,11 +1155,13 @@ func (r *KeystoneAPIReconciler) reconcileNormal( // TLS input validation // // Verify TLS input (CA cert secret if provided) - ctrlResult, err = r.verifyTLSInput(ctx, instance, helper, configMapVars) + err = r.verifyTLSInput(ctx, instance, helper, configMapVars) if err != nil { - return ctrlResult, err - } else if (ctrlResult != ctrl.Result{}) { - return ctrlResult, nil + if k8s_errors.IsNotFound(err) { + // Don't return NotFound error to the caller + return ctrl.Result{}, nil + } + return ctrl.Result{}, err } // Validate API service certs secrets