@@ -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+ }
750929func (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