Skip to content

Commit 3ccee68

Browse files
committed
WIP: 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)
1 parent 593df0a commit 3ccee68

File tree

3 files changed

+216
-24
lines changed

3 files changed

+216
-24
lines changed

api/v1beta1/conditions.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,25 @@ const (
111111

112112
// KeystoneServiceOSUserReadyErrorMessage
113113
KeystoneServiceOSUserReadyErrorMessage = "Keystone Service user error occured %s"
114+
115+
//
116+
// External Keystone API condition messages
117+
//
118+
// ExternalKeystoneAPIDBMessage
119+
ExternalKeystoneAPIDBMessage = "External Keystone API configured - database is not managed by this operator"
120+
121+
// ExternalKeystoneAPIDBAccountMessage
122+
ExternalKeystoneAPIDBAccountMessage = "External Keystone API configured - database account is not managed by this operator"
123+
124+
// ExternalKeystoneAPIRabbitMQTransportURLMessage
125+
ExternalKeystoneAPIRabbitMQTransportURLMessage = "External Keystone API configured - RabbitMQ is not managed by this operator"
126+
127+
// ExternalKeystoneAPIMemcachedReadyMessage
128+
ExternalKeystoneAPIMemcachedReadyMessage = "External Keystone API configured - memcached is not managed by this operator"
129+
130+
// ExternalKeystoneAPIServiceConfigReadyMessage
131+
ExternalKeystoneAPIServiceMessage = "External Keystone API configured - service is not managed by this operator"
132+
133+
// ExternalKeystoneAPINetworkAttachmentsReadyMessage
134+
ExternalKeystoneAPINetworkAttachmentsReadyMessage = "External Keystone API configured - network attachments are not managed by this operator"
114135
)

api/v1beta1/keystoneapi_types.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,17 @@ type KeystoneAPISpecCore struct {
213213
// This is only needed when multiple realms are federated.
214214
// Config files mount path is set to /var/lib/httpd/metadata/
215215
FederatedRealmConfig string `json:"federatedRealmConfig"`
216+
217+
// +kubebuilder:validation:Optional
218+
// ExternalKeystoneAPI - Configuration for external Keystone API endpoints
219+
ExternalKeystoneAPI *ExternalKeystoneAPI `json:"externalKeystoneAPI,omitempty"`
220+
}
221+
222+
// ExternalKeystoneAPI defines the configuration for an external Keystone API
223+
type ExternalKeystoneAPI struct {
224+
// +kubebuilder:validation:Optional
225+
// Endpoints - Endpoint URLs for the external Keystone API
226+
Endpoints map[string]string `json:"endpoints,omitempty"`
216227
}
217228

218229
// APIOverrideSpec to override the generated manifest of several child resources.

internal/controller/keystoneapi_controller.go

Lines changed: 184 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,11 @@ func (r *KeystoneAPIReconciler) Reconcile(ctx context.Context, req ctrl.Request)
244244
return r.reconcileDelete(ctx, instance, helper)
245245
}
246246

247+
// Check if external Keystone API is configured
248+
if instance.Spec.ExternalKeystoneAPI != nil {
249+
return r.reconcileExternalKeystoneAPI(ctx, instance, helper)
250+
}
251+
247252
// Handle non-deleted clusters
248253
return r.reconcileNormal(ctx, instance, helper)
249254
}
@@ -451,6 +456,14 @@ func (r *KeystoneAPIReconciler) reconcileDelete(ctx context.Context, instance *k
451456
Log := r.GetLogger(ctx)
452457
Log.Info("Reconciling Service delete")
453458

459+
// If using external Keystone API, we don't have any resources to clean up
460+
if instance.Spec.ExternalKeystoneAPI != nil {
461+
// Just remove the finalizer
462+
controllerutil.RemoveFinalizer(instance, helper.GetFinalizer())
463+
Log.Info("Reconciled External Keystone API delete successfully")
464+
return ctrl.Result{}, nil
465+
}
466+
454467
// We need to allow all KeystoneEndpoint and KeystoneService processing to finish
455468
// in the case of a delete before we remove the finalizers. For instance, in the
456469
// case of the Memcached dependency, if Memcached is deleted before all Keystone
@@ -747,6 +760,172 @@ func (r *KeystoneAPIReconciler) reconcileInit(
747760
return ctrl.Result{}, nil
748761
}
749762

763+
func (r *KeystoneAPIReconciler) reconcileExternalKeystoneAPI(
764+
ctx context.Context,
765+
instance *keystonev1.KeystoneAPI,
766+
helper *helper.Helper,
767+
) (ctrl.Result, error) {
768+
Log := r.GetLogger(ctx)
769+
Log.Info("Reconciling External Keystone API")
770+
771+
// When using external Keystone API, we skip all deployment logic
772+
// and just use the endpoints from the spec
773+
774+
// serviceLabels?
775+
776+
configMapVars := make(map[string]env.Setter)
777+
778+
// Verify secret is available (needed for admin client operations)
779+
ctrlResult, err := r.verifySecret(ctx, instance, helper, configMapVars)
780+
if err != nil {
781+
return ctrlResult, err
782+
} else if (ctrlResult != ctrl.Result{}) {
783+
return ctrlResult, nil
784+
}
785+
786+
// service DB is not needed
787+
instance.Status.Conditions.MarkTrue(mariadbv1.MariaDBAccountReadyCondition, keystonev1.ExternalKeystoneAPIDBAccountMessage)
788+
instance.Status.Conditions.MarkTrue(condition.DBReadyCondition, keystonev1.ExternalKeystoneAPIDBMessage)
789+
instance.Status.Conditions.MarkTrue(condition.DBSyncReadyCondition, keystonev1.ExternalKeystoneAPIDBMessage)
790+
791+
// RabbitMQ transportURL is not needed
792+
instance.Status.Conditions.MarkTrue(condition.RabbitMqTransportURLReadyCondition, keystonev1.ExternalKeystoneAPIRabbitMQTransportURLMessage)
793+
794+
// memcached is not needed
795+
instance.Status.Conditions.MarkTrue(condition.MemcachedReadyCondition, keystonev1.ExternalKeystoneAPIMemcachedReadyMessage)
796+
797+
// Mark service conditions as ready since they're being managed externally
798+
instance.Status.Conditions.MarkTrue(condition.ServiceAccountReadyCondition, "External Keystone API - no service account needed")
799+
instance.Status.Conditions.MarkTrue(condition.RoleReadyCondition, "External Keystone API - no role needed")
800+
instance.Status.Conditions.MarkTrue(condition.RoleBindingReadyCondition, "External Keystone API - no role binding needed")
801+
instance.Status.Conditions.MarkTrue(condition.ServiceConfigReadyCondition, keystonev1.ExternalKeystoneAPIServiceMessage)
802+
instance.Status.Conditions.MarkTrue(condition.BootstrapReadyCondition, keystonev1.ExternalKeystoneAPIServiceMessage)
803+
instance.Status.Conditions.MarkTrue(condition.CreateServiceReadyCondition, keystonev1.ExternalKeystoneAPIServiceMessage)
804+
instance.Status.Conditions.MarkTrue(condition.DeploymentReadyCondition, keystonev1.ExternalKeystoneAPIServiceMessage)
805+
instance.Status.Conditions.MarkTrue(condition.CronJobReadyCondition, keystonev1.ExternalKeystoneAPIServiceMessage)
806+
// Set ready count to 0 since we're not deploying anything
807+
instance.Status.ReadyCount = 0
808+
809+
// Verify TLS input (CA cert secret if provided)
810+
ctrlResult, err = r.verifyTLSInput(ctx, instance, helper, configMapVars)
811+
if err != nil {
812+
return ctrlResult, err
813+
} else if (ctrlResult != ctrl.Result{}) {
814+
return ctrlResult, nil
815+
}
816+
instance.Status.Conditions.MarkTrue(condition.TLSInputReadyCondition, condition.InputReadyMessage)
817+
818+
// TODO: Do we need network annotations if we we dont have Keystone API?
819+
instance.Status.Conditions.MarkTrue(condition.NetworkAttachmentsReadyCondition, keystonev1.ExternalKeystoneAPINetworkAttachmentsReadyMessage)
820+
821+
// Add endpoints
822+
823+
// Set API endpoints from externalKeystoneAPI spec
824+
if instance.Status.APIEndpoints == nil {
825+
instance.Status.APIEndpoints = map[string]string{}
826+
}
827+
828+
// Copy endpoints from spec to status
829+
// TODO: check instance.Spec.Override.Service?
830+
for key, value := range instance.Spec.ExternalKeystoneAPI.Endpoints {
831+
instance.Status.APIEndpoints[key] = value
832+
}
833+
834+
// Set region if specified
835+
if instance.Spec.Region != "" {
836+
instance.Status.Region = instance.Spec.Region
837+
}
838+
839+
Log.Info("Reconciled External Keystone API successfully")
840+
return ctrl.Result{}, nil
841+
}
842+
843+
// verifySecret verifies the OpenStack secret exists and adds its hash to configMapVars
844+
func (r *KeystoneAPIReconciler) verifySecret(
845+
ctx context.Context,
846+
instance *keystonev1.KeystoneAPI,
847+
helper *helper.Helper,
848+
configMapVars map[string]env.Setter,
849+
) (ctrl.Result, error) {
850+
// check for required OpenStack secret holding passwords for service/admin user and add hash to the vars map
851+
// NOTE: VerifySecret handles the "not found" error and returns RequeueAfter ctrl.Result if so, so we don't
852+
// need to check the error type here
853+
hash, result, err := oko_secret.VerifySecret(ctx, types.NamespacedName{Name: instance.Spec.Secret, Namespace: instance.Namespace}, []string{"AdminPassword"}, helper.GetClient(), time.Second*10)
854+
if err != nil {
855+
instance.Status.Conditions.Set(condition.FalseCondition(
856+
condition.InputReadyCondition,
857+
condition.ErrorReason,
858+
condition.SeverityWarning,
859+
condition.InputReadyErrorMessage,
860+
err.Error()))
861+
return ctrl.Result{}, err
862+
} else if (result != ctrl.Result{}) {
863+
// This case is "secret not found". VerifySecret already logs a message for it.
864+
// We treat this as a warning because it means that the service will not be able to start
865+
// while we are waiting for the secret to be created manually by the user.
866+
instance.Status.Conditions.Set(condition.FalseCondition(
867+
condition.InputReadyCondition,
868+
condition.ErrorReason,
869+
condition.SeverityWarning,
870+
condition.InputReadyWaitingMessage))
871+
return result, nil
872+
}
873+
874+
// Add hash to configMapVars if provided
875+
if configMapVars != nil {
876+
configMapVars[instance.Spec.Secret] = env.SetValue(hash)
877+
}
878+
879+
instance.Status.Conditions.MarkTrue(condition.InputReadyCondition, condition.InputReadyMessage)
880+
881+
return ctrl.Result{}, nil
882+
}
883+
func (r *KeystoneAPIReconciler) verifyTLSInput(
884+
ctx context.Context,
885+
instance *keystonev1.KeystoneAPI,
886+
helper *helper.Helper,
887+
configMapVars map[string]env.Setter,
888+
) (ctrl.Result, error) {
889+
// Validate the CA cert secret if provided
890+
if instance.Spec.TLS.CaBundleSecretName != "" {
891+
hash, err := tls.ValidateCACertSecret(
892+
ctx,
893+
helper.GetClient(),
894+
types.NamespacedName{
895+
Name: instance.Spec.TLS.CaBundleSecretName,
896+
Namespace: instance.Namespace,
897+
},
898+
)
899+
if err != nil {
900+
if k8s_errors.IsNotFound(err) {
901+
// Since the CA cert secret should have been manually created by the user and provided in the spec,
902+
// we treat this as a warning because it means that the service will not be able to start.
903+
instance.Status.Conditions.Set(condition.FalseCondition(
904+
condition.TLSInputReadyCondition,
905+
condition.ErrorReason,
906+
condition.SeverityWarning,
907+
condition.TLSInputReadyWaitingMessage,
908+
instance.Spec.TLS.CaBundleSecretName))
909+
return ctrl.Result{}, nil
910+
}
911+
instance.Status.Conditions.Set(condition.FalseCondition(
912+
condition.TLSInputReadyCondition,
913+
condition.ErrorReason,
914+
condition.SeverityWarning,
915+
condition.TLSInputErrorMessage,
916+
err.Error()))
917+
return ctrl.Result{}, err
918+
}
919+
920+
if hash != "" {
921+
if configMapVars != nil {
922+
configMapVars[tls.CABundleKey] = env.SetValue(hash)
923+
}
924+
}
925+
}
926+
927+
return ctrl.Result{}, nil
928+
}
750929
func (r *KeystoneAPIReconciler) reconcileUpdate(ctx context.Context) (ctrl.Result, error) {
751930
Log := r.GetLogger(ctx)
752931
Log.Info("Reconciling Service update")
@@ -787,32 +966,13 @@ func (r *KeystoneAPIReconciler) reconcileNormal(
787966

788967
//
789968
// check for required OpenStack secret holding passwords for service/admin user and add hash to the vars map
790-
// NOTE: VerifySecret handles the "not found" error and returns RequeueAfter ctrl.Result if so, so we don't
791-
// need to check the error type here
792969
//
793-
hash, result, err := oko_secret.VerifySecret(ctx, types.NamespacedName{Name: instance.Spec.Secret, Namespace: instance.Namespace}, []string{"AdminPassword"}, helper.GetClient(), time.Second*10)
970+
ctrlResult, err := r.verifySecret(ctx, instance, helper, configMapVars)
794971
if err != nil {
795-
instance.Status.Conditions.Set(condition.FalseCondition(
796-
condition.InputReadyCondition,
797-
condition.ErrorReason,
798-
condition.SeverityWarning,
799-
condition.InputReadyErrorMessage,
800-
err.Error()))
801-
return ctrl.Result{}, err
802-
} else if (result != ctrl.Result{}) {
803-
// This case is "secret not found". VerifySecret already logs a message for it.
804-
// We treat this as a warning because it means that the service will not be able to start
805-
// while we are waiting for the secret to be created manually by the user.
806-
instance.Status.Conditions.Set(condition.FalseCondition(
807-
condition.InputReadyCondition,
808-
condition.ErrorReason,
809-
condition.SeverityWarning,
810-
condition.InputReadyWaitingMessage))
811-
return result, nil
972+
return ctrlResult, err
973+
} else if (ctrlResult != ctrl.Result{}) {
974+
return ctrlResult, nil
812975
}
813-
configMapVars[instance.Spec.Secret] = env.SetValue(hash)
814-
815-
instance.Status.Conditions.MarkTrue(condition.InputReadyCondition, condition.InputReadyMessage)
816976

817977
// run check OpenStack secret - end
818978

@@ -1089,7 +1249,7 @@ func (r *KeystoneAPIReconciler) reconcileNormal(
10891249
}
10901250

10911251
// Handle service init
1092-
ctrlResult, err := r.reconcileInit(ctx, instance, helper, serviceLabels, serviceAnnotations, memcached)
1252+
ctrlResult, err = r.reconcileInit(ctx, instance, helper, serviceLabels, serviceAnnotations, memcached)
10931253
if err != nil {
10941254
return ctrlResult, err
10951255
} else if (ctrlResult != ctrl.Result{}) {

0 commit comments

Comments
 (0)