@@ -814,6 +814,94 @@ func GetRedisReplicationRealMaster(ctx context.Context, client kubernetes.Interf
814814 return ""
815815}
816816
817+ // ConfigureExternalMasterReplication configures every pod in the replication StatefulSet
818+ // as a read-replica (slave) of an external Redis master. It is used exclusively in
819+ // slave-only mode (ExternalMaster.Enabled = true).
820+ //
821+ // For each pod the function:
822+ // 1. Checks the current INFO replication output; if the pod is already a slave of the
823+ // correct external master it is skipped to avoid unnecessary resync disruption.
824+ // 2. Issues CONFIG SET masterauth before REPLICAOF so authentication to the external
825+ // master succeeds on the first attempt.
826+ // 3. Issues REPLICAOF <host> <port>.
827+ //
828+ // All operations are idempotent and safe to call on every reconcile loop.
829+ func ConfigureExternalMasterReplication (ctx context.Context , client kubernetes.Interface , cr * rrvb2.RedisReplication ) error {
830+ logger := log .FromContext (ctx )
831+
832+ externalHost := cr .Spec .ExternalMaster .Host
833+ externalPort := cr .GetExternalMasterPort ()
834+ externalPortStr := strconv .Itoa (int (externalPort ))
835+
836+ var pass string
837+ if cr .Spec .KubernetesConfig .ExistingPasswordSecret != nil {
838+ var err error
839+ pass , err = getRedisPassword (
840+ ctx , client , cr .Namespace ,
841+ * cr .Spec .KubernetesConfig .ExistingPasswordSecret .Name ,
842+ * cr .Spec .KubernetesConfig .ExistingPasswordSecret .Key ,
843+ )
844+ if err != nil {
845+ logger .Error (err , "Failed to get Redis password for external master replication" )
846+ return err
847+ }
848+ }
849+
850+ replicas := cr .Spec .GetReplicationCounts ("replication" )
851+ for i := 0 ; i < int (replicas ); i ++ {
852+ podName := cr .Name + "-" + strconv .Itoa (i )
853+ redisClient := configureRedisReplicationClient (ctx , client , cr , podName )
854+ defer redisClient .Close ()
855+
856+ // Check current replication state; skip pod if already correctly configured
857+ // to avoid unnecessary resync disruption every 30 s.
858+ info , err := redisClient .Info (ctx , "Replication" ).Result ()
859+ if err != nil {
860+ logger .Error (err , "Failed to get replication info, skipping pod" , "pod" , podName )
861+ continue
862+ }
863+ if isAlreadySlaveOf (info , externalHost , externalPortStr ) {
864+ logger .V (1 ).Info ("Pod already replicating from correct external master, skipping" ,
865+ "pod" , podName , "host" , externalHost , "port" , externalPort )
866+ continue
867+ }
868+
869+ // Set masterauth BEFORE issuing REPLICAOF so the first handshake authenticates.
870+ if pass != "" {
871+ if err := redisClient .ConfigSet (ctx , "masterauth" , pass ).Err (); err != nil {
872+ logger .Error (err , "Failed to set masterauth on pod" , "pod" , podName )
873+ return err
874+ }
875+ }
876+
877+ logger .V (1 ).Info ("Configuring pod as slave of external master" ,
878+ "pod" , podName , "host" , externalHost , "port" , externalPort )
879+ if err := redisClient .SlaveOf (ctx , externalHost , externalPortStr ).Err (); err != nil {
880+ logger .Error (err , "Failed to issue REPLICAOF to external master" , "pod" , podName )
881+ return err
882+ }
883+ }
884+
885+ return nil
886+ }
887+
888+ // isAlreadySlaveOf reports whether an INFO replication output indicates the instance
889+ // is already a slave of the given host:port. Both host and port must match exactly.
890+ func isAlreadySlaveOf (info , host , port string ) bool {
891+ var isSlave , correctHost , correctPort bool
892+ for _ , line := range strings .Split (info , "\r \n " ) {
893+ switch {
894+ case strings .HasPrefix (line , "role:" ):
895+ isSlave = strings .TrimPrefix (line , "role:" ) == "slave"
896+ case strings .HasPrefix (line , "master_host:" ):
897+ correctHost = strings .TrimPrefix (line , "master_host:" ) == host
898+ case strings .HasPrefix (line , "master_port:" ):
899+ correctPort = strings .TrimPrefix (line , "master_port:" ) == port
900+ }
901+ }
902+ return isSlave && correctHost && correctPort
903+ }
904+
817905// SetRedisClusterDynamicConfig applies dynamic configuration to each Redis instance in the cluster
818906func SetRedisClusterDynamicConfig (ctx context.Context , client kubernetes.Interface , cr * rcvb2.RedisCluster ) error {
819907 // Get dynamic configuration
0 commit comments