@@ -331,6 +331,37 @@ func (r *AxonOpsPlatformReconciler) Reconcile(ctx context.Context, req ctrl.Requ
331331 }
332332 }
333333
334+ // Validate that internal database components are not enabled without the server component.
335+ // External components (with spec.*.external.hosts) are already-running services; the operator
336+ // only writes connection references for them, so they do not require a Server to be present.
337+ // Internal (operator-managed) TimeSeries and Search exist solely to be consumed by the Server;
338+ // enabling them without a Server produces a confusing partial state.
339+ internalTSEnabled := r .isComponentEnabled (server .Spec .TimeSeries ) && ! isTimeSeriesExternal (server )
340+ internalSearchEnabled := r .isComponentEnabled (server .Spec .Search ) && ! isSearchExternal (server )
341+ if ! r .isComponentEnabled (server .Spec .Server ) && (internalTSEnabled || internalSearchEnabled ) {
342+ var components []string
343+ if internalTSEnabled {
344+ components = append (components , "timeSeries" )
345+ }
346+ if internalSearchEnabled {
347+ components = append (components , "search" )
348+ }
349+ msg := fmt .Sprintf ("spec.server must be configured when %s is enabled" , strings .Join (components , " and " ))
350+ log .Info ("Invalid configuration: database components require server" , "components" , components )
351+ meta .SetStatusCondition (& server .Status .Conditions , metav1.Condition {
352+ Type : "Ready" ,
353+ Status : metav1 .ConditionFalse ,
354+ ObservedGeneration : server .Generation ,
355+ Reason : "InvalidConfiguration" ,
356+ Message : msg ,
357+ })
358+ server .Status .ObservedGeneration = server .Generation
359+ if err := r .Status ().Update (ctx , server ); err != nil {
360+ log .Error (err , "Failed to update status for invalid configuration" )
361+ }
362+ return ctrl.Result {}, nil
363+ }
364+
334365 // Verify cert-manager CRDs are available (only needed for internal database/workload resources)
335366 if needsInternalResources (server ) {
336367 if ! r .isCertManagerAvailable () {
@@ -397,10 +428,30 @@ func (r *AxonOpsPlatformReconciler) Reconcile(ctx context.Context, req ctrl.Requ
397428 return ctrl.Result {}, err
398429 }
399430
431+ // External databases require explicit credentials — auto-generating random
432+ // credentials would cause permanent authentication failures since the operator
433+ // cannot know what credentials the existing cluster was provisioned with.
434+ tsAuth := server .Spec .TimeSeries .Authentication
435+ if tsAuth .SecretRef == "" && tsAuth .Username == "" {
436+ msg := "external TimeSeries requires credentials: set spec.timeSeries.authentication.secretRef or spec.timeSeries.authentication.username/password"
437+ log .Info ("Missing external TimeSeries credentials" , "message" , msg )
438+ meta .SetStatusCondition (& server .Status .Conditions , metav1.Condition {
439+ Type : "Ready" ,
440+ Status : metav1 .ConditionFalse ,
441+ ObservedGeneration : server .Generation ,
442+ Reason : "MissingExternalCredentials" ,
443+ Message : msg ,
444+ })
445+ server .Status .ObservedGeneration = server .Generation
446+ if statusErr := r .Status ().Update (ctx , server ); statusErr != nil {
447+ log .Error (statusErr , "Failed to update status" )
448+ }
449+ return ctrl.Result {RequeueAfter : 30 * time .Second }, nil
450+ }
451+
400452 // Ensure authentication secret for external TimeSeries.
401453 // ensureAuthenticationSecret handles all cases: SecretRef (validates it),
402- // inline username/password (creates a managed Secret), and no credentials
403- // (auto-generates into a managed Secret).
454+ // inline username/password (creates a managed Secret).
404455 var err error
405456 timeSeriesSecretName , _ , err = r .ensureAuthenticationSecret (ctx , server , componentTimeseries , server .Spec .TimeSeries .Authentication , server .Spec .TimeSeries .StorageConfig )
406457 if err != nil {
@@ -447,10 +498,30 @@ func (r *AxonOpsPlatformReconciler) Reconcile(ctx context.Context, req ctrl.Requ
447498 return ctrl.Result {}, err
448499 }
449500
501+ // External databases require explicit credentials — auto-generating random
502+ // credentials would cause permanent authentication failures since the operator
503+ // cannot know what credentials the existing cluster was provisioned with.
504+ searchAuth := server .Spec .Search .Authentication
505+ if searchAuth .SecretRef == "" && searchAuth .Username == "" {
506+ msg := "external Search requires credentials: set spec.search.authentication.secretRef or spec.search.authentication.username/password"
507+ log .Info ("Missing external Search credentials" , "message" , msg )
508+ meta .SetStatusCondition (& server .Status .Conditions , metav1.Condition {
509+ Type : "Ready" ,
510+ Status : metav1 .ConditionFalse ,
511+ ObservedGeneration : server .Generation ,
512+ Reason : "MissingExternalCredentials" ,
513+ Message : msg ,
514+ })
515+ server .Status .ObservedGeneration = server .Generation
516+ if statusErr := r .Status ().Update (ctx , server ); statusErr != nil {
517+ log .Error (statusErr , "Failed to update status" )
518+ }
519+ return ctrl.Result {RequeueAfter : 30 * time .Second }, nil
520+ }
521+
450522 // Ensure authentication secret for external Search.
451523 // ensureAuthenticationSecret handles all cases: SecretRef (validates it),
452- // inline username/password (creates a managed Secret), and no credentials
453- // (auto-generates into a managed Secret).
524+ // inline username/password (creates a managed Secret).
454525 var err error
455526 searchSecretName , _ , err = r .ensureAuthenticationSecret (ctx , server , componentSearch , server .Spec .Search .Authentication , server .Spec .Search .StorageConfig )
456527 if err != nil {
0 commit comments