Repository: apecloud/kubeblocks

Issue:
[BUG]HscaleOfflineInstances is always in running status when scaleIn component that only has 1 replicas
**Describe the env**
Kubernetes: v1.31.1-aliyun.1
KubeBlocks: 1.0.0-beta.21
kbcli: 1.0.0-beta.8

**To Reproduce**
Steps to reproduce the behavior:
1. Create pulsar cluster with 1 broker 
```
- name: broker
      componentDef: pulsar-broker
      replicas: 1
```
2. scaleIn the component broker that only has 1 replicas
```
apiVersion: operations.kubeblocks.io/v1alpha1
kind: OpsRequest
metadata:
  generateName: pulsar-vzrjek-hscaleoffinstance-
  labels:
    app.kubernetes.io/instance: pulsar-vzrjek
    app.kubernetes.io/managed-by: kubeblocks
  namespace: default
spec:
  type: HorizontalScaling
  clusterName: pulsar-vzrjek
  force: true
  horizontalScaling:
  - componentName: broker
    scaleIn:
      onlineInstancesToOffline:
      - pulsar-vzrjek-broker-0  

      `kubectl create -f test_ops_cluster_pulsar-vzrjek.yaml`
```
3. check the ops status
```
kbcli cluster list-ops pulsar-vzrjek --status all  --namespace default
NAME                                    NAMESPACE   TYPE                CLUSTER         COMPONENT   STATUS    PROGRESS   CREATED-TIME                 
pulsar-vzrjek-hscaleoffinstance-ftvw4   default     HorizontalScaling   pulsar-vzrjek   broker      Running   0/1        Jan 03,2025 14:21 UTC+0800
```
4. describe ops
```
k describe opsrequest pulsar-vzrjek-hscaleoffinstance-ftvw4
Events:
  Type    Reason                    Age   From                    Message
  ----    ------                    ----  ----                    -------
  Normal  WaitForProgressing        12m   ops-request-controller  wait for the controller to process the OpsRequest: pulsar-vzrjek-hscaleoffinstance-ftvw4 in Cluster: pulsar-vzrjek
  Normal  ValidateOpsRequestPassed  12m   ops-request-controller  OpsRequest: pulsar-vzrjek-hscaleoffinstance-ftvw4 is validated
  Normal  HorizontalScalingStarted  12m   ops-request-controller  Start to horizontal scale replicas in Cluster: pulsar-vzrjek
```
5. further testing
HscaleOfflineInstances works well when scaleIn component that has more than 1 replicas

**Expected behavior**
turn the ops to failed if this ops is invalid

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**
 - OS: [e.g. iOS]
 - Browser [e.g. chrome, safari]
 - Version [e.g. 22]

**Additional context**
Add any other context about the problem here.


Discussion:
@shanshanying: It is illegal to offline the ONLY instance left in cluster. we should reject such operations. 
@yipeng1030 could pls help raise the constraint in Ops.  /cc @wangyelei 

Solution patch:
diff --git a/apis/apps/v1/shardingdefinition_types.go b/apis/apps/v1/shardingdefinition_types.go
index 756df6fd4..bae85bb03 100644
--- a/apis/apps/v1/shardingdefinition_types.go
+++ b/apis/apps/v1/shardingdefinition_types.go
@@ -150,7 +150,7 @@ type ShardsLimit struct {
 	// +kubebuilder:validation:Required
 	MinShards int32 `json:"minShards"`
 
-	// The maximum limit of replicas.
+	// The maximum limit of shards.
 	//
 	// +kubebuilder:validation:Required
 	MaxShards int32 `json:"maxShards"`
diff --git a/apis/operations/v1alpha1/opsrequest_validation.go b/apis/operations/v1alpha1/opsrequest_validation.go
index 764f7c040..e530e6278 100644
--- a/apis/operations/v1alpha1/opsrequest_validation.go
+++ b/apis/operations/v1alpha1/opsrequest_validation.go
@@ -266,7 +266,7 @@ func compareQuantity(requestQuantity, limitQuantity *resource.Quantity) bool {
 }
 
 // validateHorizontalScaling validates api when spec.type is HorizontalScaling
-func (r *OpsRequest) validateHorizontalScaling(_ context.Context, _ client.Client, cluster *appsv1.Cluster) error {
+func (r *OpsRequest) validateHorizontalScaling(ctx context.Context, cli client.Client, cluster *appsv1.Cluster) error {
 	horizontalScalingList := r.Spec.HorizontalScalingList
 	if len(horizontalScalingList) == 0 {
 		return notEmptyError("spec.horizontalScaling")
@@ -282,14 +282,39 @@ func (r *OpsRequest) validateHorizontalScaling(_ context.Context, _ client.Clien
 	}
 	for _, comSpec := range cluster.Spec.ComponentSpecs {
 		if hScale, ok := hScaleMap[comSpec.Name]; ok {
-			if err := r.validateHorizontalScalingSpec(hScale, comSpec, cluster.Name, false); err != nil {
+			// Default values if no limit is found
+			minNum, maxNum := 1, 16384
+			if comSpec.ComponentDef != "" {
+				compDef := &appsv1.ComponentDefinition{}
+				if err := cli.Get(ctx, client.ObjectKey{Name: comSpec.ComponentDef, Namespace: r.Namespace}, compDef); err != nil {
+					return err
+				}
+				if compDef.Spec.ReplicasLimit != nil {
+					minNum = int(compDef.Spec.ReplicasLimit.MinReplicas)
+					maxNum = int(compDef.Spec.ReplicasLimit.MaxReplicas)
+				}
+			}
+			if err := r.validateHorizontalScalingSpec(hScale, comSpec, cluster.Name, false, maxNum, minNum); err != nil {
 				return err
 			}
 		}
+
 	}
 	for _, spec := range cluster.Spec.Shardings {
 		if hScale, ok := hScaleMap[spec.Name]; ok {
-			if err := r.validateHorizontalScalingSpec(hScale, spec.Template, cluster.Name, true); err != nil {
+			// Default values if no limit is found
+			minNum, maxNum := 1, 2048
+			if spec.ShardingDef != "" {
+				shardingDef := &appsv1.ShardingDefinition{}
+				if err := cli.Get(ctx, types.NamespacedName{Name: spec.ShardingDef, Namespace: r.Namespace}, shardingDef); err != nil {
+					return err
+				}
+				if shardingDef.Spec.ShardsLimit != nil {
+					minNum = int(shardingDef.Spec.ShardsLimit.MinShards)
+					maxNum = int(shardingDef.Spec.ShardsLimit.MaxShards)
+				}
+			}
+			if err := r.validateHorizontalScalingSpec(hScale, spec.Template, cluster.Name, true, maxNum, minNum); err != nil {
 				return err
 			}
 		}
@@ -307,82 +332,102 @@ func (r *OpsRequest) CountOfflineOrOnlineInstances(clusterName, componentName st
 	return offlineOrOnlineInsCountMap
 }
 
-func (r *OpsRequest) validateHorizontalScalingSpec(hScale HorizontalScaling, compSpec appsv1.ClusterComponentSpec, clusterName string, isSharding bool) error {
+func (r *OpsRequest) validateHorizontalScalingSpec(hScale HorizontalScaling, compSpec appsv1.ClusterComponentSpec, clusterName string, isSharding bool, maxReplicasOrShards int, minReplicasOrShards int) error {
 	scaleIn := hScale.ScaleIn
 	scaleOut := hScale.ScaleOut
+	// Validate Shards if present
 	if hScale.Shards != nil {
-		if !isSharding {
-			return fmt.Errorf(`shards field cannot be used for the component "%s"`, hScale.ComponentName)
-		}
-		if scaleOut != nil || scaleIn != nil {
-			return fmt.Errorf(`shards field cannot be used together with scaleOut or scaleIn for the component "%s"`, hScale.ComponentName)
-		}
-		return nil
+		return r.validateShards(hScale, isSharding, minReplicasOrShards, maxReplicasOrShards)
 	}
-	if lastCompConfiguration, ok := r.Status.LastConfiguration.Components[hScale.ComponentName]; ok {
-		// use last component configuration snapshot
-		compSpec.Instances = lastCompConfiguration.Instances
-		compSpec.Replicas = *lastCompConfiguration.Replicas
-		compSpec.OfflineInstances = lastCompConfiguration.OfflineInstances
+	// Use last configuration if available
+	if err := r.applyLastConfiguration(hScale.ComponentName, &compSpec); err != nil {
+		return err
+	}
+
+	compInstanceTplReplicas := map[string]int32{}
+	for _, instance := range compSpec.Instances {
+		compInstanceTplReplicas[instance.Name] = instance.GetReplicas()
 	}
-	compInsTplMap := map[string]int32{}
-	for _, v := range compSpec.Instances {
-		compInsTplMap[v.Name] = v.GetReplicas()
-	}
-	// Rules:
-	// 1. length of offlineInstancesToOnline or onlineInstancesToOffline can't greater than the configured replicaChanges for the component.
-	// 2. replicaChanges for component must greater than or equal to the sum of replicaChanges configured in instance templates.
-	validateHScaleOperation := func(replicaChanger ReplicaChanger, newInstances []appsv1.InstanceTemplate, offlineOrOnlineInsNames []string, isScaleIn bool) error {
-		msgPrefix := "ScaleIn:"
-		hScaleInstanceFieldName := "onlineInstancesToOffline"
-		if !isScaleIn {
-			msgPrefix = "ScaleOut:"
-			hScaleInstanceFieldName = "offlineInstancesToOnline"
-		}
-		if isSharding && len(offlineOrOnlineInsNames) > 0 {
-			return fmt.Errorf(`cannot specify %s for a sharding component "%s"`, hScaleInstanceFieldName, hScale.ComponentName)
-		}
-		if replicaChanger.ReplicaChanges != nil && len(offlineOrOnlineInsNames) > int(*replicaChanger.ReplicaChanges) {
-			return fmt.Errorf(`the length of %s can't be greater than the "replicaChanges" for the component`, hScaleInstanceFieldName)
-		}
-		offlineOrOnlineInsCountMap := r.CountOfflineOrOnlineInstances(clusterName, hScale.ComponentName, offlineOrOnlineInsNames)
-		insTplChangeMap := map[string]int32{}
-		allReplicaChanges := int32(0)
-		for _, v := range replicaChanger.Instances {
-			compInsReplicas, ok := compInsTplMap[v.Name]
-			if !ok {
-				return fmt.Errorf(`%s cannot find the instance template "%s" in component "%s"`,
-					msgPrefix, v.Name, hScale.ComponentName)
+
+	// Define the scaling operation validation function
+	validateHScaleOperation := func(replicaChanger ReplicaChanger, newInstances []appsv1.InstanceTemplate, instanceNames []string, isScaleIn bool) error {
+		var operationPrefix, instanceField string
+		if isScaleIn {
+			operationPrefix = "ScaleIn:"
+			instanceField = "onlineInstancesToOffline"
+		} else {
+			operationPrefix = "ScaleOut:"
+			instanceField = "offlineInstancesToOnline"
+		}
+
+		if isSharding && len(instanceNames) > 0 {
+			return fmt.Errorf(`cannot specify %s for a sharding component "%s"`, instanceField, hScale.ComponentName)
+		}
+
+		// Rule 1: Check if the length of the instance names is greater than the configured replicaChanges
+		if replicaChanger.ReplicaChanges != nil && len(instanceNames) > int(*replicaChanger.ReplicaChanges) {
+			return fmt.Errorf(`the length of %s can't be greater than the "replicaChanges" for the component`, instanceField)
+		}
+
+		// Track the count of offline/online instances
+		offlineOrOnlineInsCountMap := r.CountOfflineOrOnlineInstances(clusterName, hScale.ComponentName, instanceNames)
+		insTplChangeMap := make(map[string]int32)
+		totalReplicaChanges := int32(0)
+
+		// Rule 2: Validate each instance template and ensure replicaChanges are valid
+		for _, instance := range replicaChanger.Instances {
+			instanceReplicas, exists := compInstanceTplReplicas[instance.Name]
+			if !exists {
+				return fmt.Errorf(`%s cannot find the instance template "%s" in component "%s"`, operationPrefix, instance.Name, hScale.ComponentName)
 			}
-			if isScaleIn && v.ReplicaChanges > compInsReplicas {
-				return fmt.Errorf(`%s "replicaChanges" of instanceTemplate "%s" can't be greater than %d`,
-					msgPrefix, v.Name, compInsReplicas)
+			if isScaleIn && instance.ReplicaChanges > instanceReplicas {
+				return fmt.Errorf(`%s "replicaChanges" of instanceTemplate "%s" can't be greater than %d`, operationPrefix, instance.Name, instanceReplicas)
 			}
-			allReplicaChanges += v.ReplicaChanges
-			insTplChangeMap[v.Name] = v.ReplicaChanges
+			totalReplicaChanges += instance.ReplicaChanges
+			insTplChangeMap[instance.Name] = instance.ReplicaChanges
 		}
+
+		// Rule 3: Ensure replicaChanges are not less than the replicaCount for each instance template
 		for insTplName, replicaCount := range offlineOrOnlineInsCountMap {
-			replicaChanges, ok := insTplChangeMap[insTplName]
-			if !ok {
-				allReplicaChanges += replicaCount
+			replicaChangesForOneInsTpl, exists := insTplChangeMap[insTplName]
+			if !exists {
+				totalReplicaChanges += replicaCount
 				continue
 			}
-			if replicaChanges < replicaCount {
+			if replicaChangesForOneInsTpl < replicaCount {
 				return fmt.Errorf(`"replicaChanges" can't be less than %d when %d instances of the instance template "%s" are configured in %s`,
-					replicaCount, replicaCount, insTplName, hScaleInstanceFieldName)
+					replicaCount, replicaCount, insTplName, instanceField)
 			}
 		}
+
+		// Validate new instance templates
 		for _, insTpl := range newInstances {
-			if _, ok := compInsTplMap[insTpl.Name]; ok {
+			if _, exists := compInstanceTplReplicas[insTpl.Name]; exists {
 				return fmt.Errorf(`new instance template "%s" already exists in component "%s"`, insTpl.Name, hScale.ComponentName)
 			}
-			allReplicaChanges += insTpl.GetReplicas()
+			totalReplicaChanges += insTpl.GetReplicas()
 		}
-		if replicaChanger.ReplicaChanges != nil && allReplicaChanges > *replicaChanger.ReplicaChanges {
-			return fmt.Errorf(`%s "replicaChanges" can't be less than the sum of "replicaChanges" for specified instance templates`, msgPrefix)
+
+		// Validate if replicaChanges exceed the allowed replicaChanges limit
+		if replicaChanger.ReplicaChanges != nil && totalReplicaChanges > *replicaChanger.ReplicaChanges {
+			return fmt.Errorf(`%s "replicaChanges" can't be less than the sum of "replicaChanges" for specified instance templates`, operationPrefix)
+		}
+
+		// Check if the final replica count is within the limits
+		actualReplicaChange := totalReplicaChanges
+		if replicaChanger.ReplicaChanges != nil {
+			actualReplicaChange = *replicaChanger.ReplicaChanges
+		}
+		if isScaleIn && int(compSpec.Replicas)-int(actualReplicaChange) < minReplicasOrShards {
+			return fmt.Errorf(`the number of replicas after scaling down violates the replica limit for component "%s"`, hScale.ComponentName)
+		}
+		if !isScaleIn && int(compSpec.Replicas)+int(actualReplicaChange) > maxReplicasOrShards {
+			return fmt.Errorf(`the number of replicas after scaling up violates the replica limit for component "%s"`, hScale.ComponentName)
 		}
 		return nil
 	}
+
+	// Validate scaleIn and scaleOut separately
 	if scaleIn != nil {
 		if err := validateHScaleOperation(scaleIn.ReplicaChanger, nil, scaleIn.OnlineInstancesToOffline, true); err != nil {
 			return err
@@ -396,13 +441,51 @@ func (r *OpsRequest) validateHorizontalScalingSpec(hScale HorizontalScaling, com
 			return err
 		}
 	}
-	// instance cannot be both in OfflineInstancesToOnline and OnlineInstancesToOffline
-	if scaleIn != nil && scaleOut != nil {
+
+	// Check for conflicting scaling operations
+	if err := r.checkConflictingScalingOperations(hScale); err != nil {
+		return err
+	}
+	return nil
+}
+
+// validateShards validates the shards field if it is present
+func (r *OpsRequest) validateShards(hScale HorizontalScaling, isSharding bool, minReplicasOrShards int, maxReplicasOrShards int) error {
+	if hScale.Shards != nil {
+		if !isSharding {
+			return fmt.Errorf(`shards field cannot be used for the component "%s"`, hScale.ComponentName)
+		}
+		if hScale.ScaleOut != nil || hScale.ScaleIn != nil {
+			return fmt.Errorf(`shards field cannot be used together with scaleOut or scaleIn for the component "%s"`, hScale.ComponentName)
+		}
+		shards := int(*hScale.Shards)
+		if shards < minReplicasOrShards || shards > maxReplicasOrShards {
+			return fmt.Errorf(`the number of shards after horizontal scale violates the shards limit "%s"`, hScale.ComponentName)
+		}
+	}
+	return nil
+}
+
+// applyLastConfiguration applies the last known configuration to the component spec
+func (r *OpsRequest) applyLastConfiguration(componentName string, compSpec *appsv1.ClusterComponentSpec) error {
+	if lastCompConfiguration, ok := r.Status.LastConfiguration.Components[componentName]; ok {
+		compSpec.Instances = lastCompConfiguration.Instances
+		if lastCompConfiguration.Replicas != nil {
+			compSpec.Replicas = *lastCompConfiguration.Replicas
+		}
+		compSpec.OfflineInstances = lastCompConfiguration.OfflineInstances
+	}
+	return nil
+}
+
+// checkConflictingScalingOperations checks for any conflicting scaling operations
+func (r *OpsRequest) checkConflictingScalingOperations(hScale HorizontalScaling) error {
+	if hScale.ScaleIn != nil && hScale.ScaleOut != nil {
 		offlineToOnlineSet := make(map[string]struct{})
-		for _, instance := range scaleIn.OnlineInstancesToOffline {
+		for _, instance := range hScale.ScaleIn.OnlineInstancesToOffline {
 			offlineToOnlineSet[instance] = struct{}{}
 		}
-		for _, instance := range scaleOut.OfflineInstancesToOnline {
+		for _, instance := range hScale.ScaleOut.OfflineInstancesToOnline {
 			if _, exists := offlineToOnlineSet[instance]; exists {
 				return fmt.Errorf(`instance "%s" cannot be both in "OfflineInstancesToOnline" and "OnlineInstancesToOffline"`, instance)
 			}
diff --git a/config/crd/bases/apps.kubeblocks.io_shardingdefinitions.yaml b/config/crd/bases/apps.kubeblocks.io_shardingdefinitions.yaml
index a5ad8a526..0fa5e0834 100644
--- a/config/crd/bases/apps.kubeblocks.io_shardingdefinitions.yaml
+++ b/config/crd/bases/apps.kubeblocks.io_shardingdefinitions.yaml
@@ -1192,7 +1192,7 @@ spec:
                   This field is immutable.
                 properties:
                   maxShards:
-                    description: The maximum limit of replicas.
+                    description: The maximum limit of shards.
                     format: int32
                     type: integer
                   minShards:
diff --git a/deploy/helm/crds/apps.kubeblocks.io_shardingdefinitions.yaml b/deploy/helm/crds/apps.kubeblocks.io_shardingdefinitions.yaml
index a5ad8a526..0fa5e0834 100644
--- a/deploy/helm/crds/apps.kubeblocks.io_shardingdefinitions.yaml
+++ b/deploy/helm/crds/apps.kubeblocks.io_shardingdefinitions.yaml
@@ -1192,7 +1192,7 @@ spec:
                   This field is immutable.
                 properties:
                   maxShards:
-                    description: The maximum limit of replicas.
+                    description: The maximum limit of shards.
                     format: int32
                     type: integer
                   minShards:



Test patch:
diff --git a/pkg/operations/horizontal_scaling_test.go b/pkg/operations/horizontal_scaling_test.go
index ee9d41207..ecd44485c 100644
--- a/pkg/operations/horizontal_scaling_test.go
+++ b/pkg/operations/horizontal_scaling_test.go
@@ -46,10 +46,11 @@ import (
 var _ = Describe("HorizontalScaling OpsRequest", func() {
 
 	var (
-		randomStr   = testCtx.GetRandomStr()
-		compDefName = "test-compdef-" + randomStr
-		clusterName = "test-cluster-" + randomStr
-		insTplName  = "foo"
+		randomStr       = testCtx.GetRandomStr()
+		compDefName     = "test-compdef-" + randomStr
+		shardingDefName = "test-shardingdef-" + randomStr
+		clusterName     = "test-cluster-" + randomStr
+		insTplName      = "foo"
 	)
 
 	cleanEnv := func() {
@@ -89,7 +90,13 @@ var _ = Describe("HorizontalScaling OpsRequest", func() {
 			horizontalScaling opsv1alpha1.HorizontalScaling,
 			ignoreHscaleStrictValidate bool) (*OpsResource, []*corev1.Pod) {
 			By("init operations resources with CLusterDefinition/Hybrid components Cluster/consensus Pods")
-			opsRes, _, _ := initOperationsResources(compDefName, clusterName)
+			opsRes, compDef, _ := initOperationsResources(compDefName, clusterName)
+			Expect(testapps.ChangeObj(&testCtx, compDef, func(compDef *appsv1.ComponentDefinition) {
+				compDef.Spec.ReplicasLimit = &appsv1.ReplicasLimit{
+					MinReplicas: 0,
+					MaxReplicas: 10,
+				}
+			})).Should(Succeed())
 			its := testapps.MockInstanceSetComponent(&testCtx, clusterName, defaultCompName)
 			if changeClusterSpec != nil {
 				Expect(testapps.ChangeObj(&testCtx, opsRes.Cluster, func(cluster *appsv1.Cluster) {
@@ -733,6 +740,141 @@ var _ = Describe("HorizontalScaling OpsRequest", func() {
 				g.Expect(pobj.Status.Phase).Should(Equal(opsv1alpha1.OpsSucceedPhase))
 			})).Should(Succeed())
 		})
+		It("should fail when scaling out beyond the max replicas", func() {
+			By("Setting up the component with a replica limit of 1 to 3")
+			opsRes, compDef, _ := initOperationsResources(compDefName, clusterName)
+			Expect(testapps.ChangeObj(&testCtx, compDef, func(compDef *appsv1.ComponentDefinition) {
+				compDef.Spec.ReplicasLimit = &appsv1.ReplicasLimit{
+					MinReplicas: 1,
+					MaxReplicas: 3,
+				}
+			})).Should(Succeed())
+
+			By("Creating a horizontal scaling operation to scale out beyond the limit")
+			ops := createHorizontalScaling(clusterName, opsv1alpha1.HorizontalScaling{
+				ComponentOps: opsv1alpha1.ComponentOps{
+					ComponentName: defaultCompName,
+				},
+				ScaleOut: &opsv1alpha1.ScaleOut{
+					OfflineInstancesToOnline: []string{clusterName + defaultCompName + "-3", clusterName + defaultCompName + "-4"},
+				},
+			}, false)
+
+			ops.Namespace = testCtx.DefaultNamespace
+			initClusterAnnotationAndPhaseForOps(opsRes)
+
+			By("Validating the operation")
+			err := ops.Validate(ctx, k8sClient, opsRes.Cluster, true)
+			Expect(err).To(HaveOccurred()) // Expect an error due to exceeding the maximum replica limit
+		})
+
+		It("should fail when scaling in below the min replicas", func() {
+			By("Setting up the component with a replica limit of 1 to 3")
+			opsRes, compDef, _ := initOperationsResources(compDefName, clusterName)
+			Expect(testapps.ChangeObj(&testCtx, compDef, func(compDef *appsv1.ComponentDefinition) {
+				compDef.Spec.ReplicasLimit = &appsv1.ReplicasLimit{
+					MinReplicas: 1,
+					MaxReplicas: 3,
+				}
+			})).Should(Succeed())
+
+			By("Creating a horizontal scaling operation to scale in below the limit")
+			ops := createHorizontalScaling(clusterName, opsv1alpha1.HorizontalScaling{
+				ComponentOps: opsv1alpha1.ComponentOps{
+					ComponentName: defaultCompName,
+				},
+				ScaleIn: &opsv1alpha1.ScaleIn{
+					OnlineInstancesToOffline: []string{clusterName + defaultCompName + "-0", clusterName + defaultCompName + "-1", clusterName + defaultCompName + "-2"},
+				},
+			}, false)
+
+			ops.Namespace = testCtx.DefaultNamespace
+			initClusterAnnotationAndPhaseForOps(opsRes)
+
+			By("Validating the operation")
+			err := ops.Validate(ctx, k8sClient, opsRes.Cluster, true)
+			Expect(err).To(HaveOccurred()) // Expect an error due to scaling in below the minimum replica limit
+		})
+
+		It("should succeed when scaling within the replicas limit", func() {
+			By("Setting up the component with a replica limit of 1 to 3")
+			opsRes, compDef, _ := initOperationsResources(compDefName, clusterName)
+			Expect(testapps.ChangeObj(&testCtx, compDef, func(compDef *appsv1.ComponentDefinition) {
+				compDef.Spec.ReplicasLimit = &appsv1.ReplicasLimit{
+					MinReplicas: 1,
+					MaxReplicas: 4,
+				}
+			})).Should(Succeed())
+
+			By("Creating a horizontal scaling operation to scale within the limit")
+			ops := createHorizontalScaling(clusterName, opsv1alpha1.HorizontalScaling{
+				ComponentOps: opsv1alpha1.ComponentOps{
+					ComponentName: defaultCompName,
+				},
+				ScaleOut: &opsv1alpha1.ScaleOut{
+					OfflineInstancesToOnline: []string{clusterName + defaultCompName + "-3"},
+				},
+			}, false)
+
+			ops.Namespace = testCtx.DefaultNamespace
+			initClusterAnnotationAndPhaseForOps(opsRes)
+
+			By("Validating the operation")
+			err := ops.Validate(ctx, k8sClient, opsRes.Cluster, true)
+			Expect(err).ToNot(HaveOccurred()) // Should pass as it's within the specified replica limit
+		})
+
+		It("should succeed when scaling within the replicas limit with shards", func() {
+			By("Setting up the sharding with a shard limit of 1 to 3")
+			opsRes, _, _ := initOperationsResources(compDefName, clusterName)
+			shardingDef := testapps.NewShardingDefinitionFactory(shardingDefName, compDefName).Create(&testCtx).GetObject()
+			// add a sharding component
+			Expect(testapps.ChangeObj(&testCtx, opsRes.Cluster, func(cluster *appsv1.Cluster) {
+				cluster.Spec.Shardings = []appsv1.ClusterSharding{
+					{
+						Name:        defaultCompName,
+						Shards:      int32(3),
+						Template:    cluster.Spec.ComponentSpecs[0],
+						ShardingDef: shardingDefName,
+					},
+				}
+				cluster.Spec.ComponentSpecs = []appsv1.ClusterComponentSpec{}
+			})).Should(Succeed())
+
+			Expect(testapps.ChangeObj(&testCtx, shardingDef, func(shardingDef *appsv1.ShardingDefinition) {
+				shardingDef.Spec.ShardsLimit = &appsv1.ShardsLimit{
+					MinShards: 1,
+					MaxShards: 3,
+				}
+			})).Should(Succeed())
+
+			By("Testing horizontal scaling operation with different shard values")
+			shardValues := []int32{0, 2, 4}               // Shard values to test
+			expectedResults := []bool{false, true, false} // Expected results for each shard value: fail, pass, fail
+
+			for i, shards := range shardValues {
+				ops := createHorizontalScaling(clusterName, opsv1alpha1.HorizontalScaling{
+					Shards: &shards,
+					ComponentOps: opsv1alpha1.ComponentOps{
+						ComponentName: defaultCompName,
+					},
+				}, false)
+
+				ops.Namespace = testCtx.DefaultNamespace
+				initClusterAnnotationAndPhaseForOps(opsRes)
+
+				By("Validating the operation")
+				err := ops.Validate(ctx, k8sClient, opsRes.Cluster, true)
+
+				// Check if the result matches the expected result
+				if expectedResults[i] {
+					Expect(err).ToNot(HaveOccurred()) // Should pass when shards is 2 (within limit)
+				} else {
+					Expect(err).To(HaveOccurred()) // Should fail when shards is 4 (beyond limit) and 0 (below limit)
+				}
+			}
+
+		})
 	})
 })