add restriction for traffic configuration of partition-style step (#225)
Signed-off-by: yunbo <yunbo10124scut@gmail.com> Co-authored-by: yunbo <yunbo10124scut@gmail.com>
This commit is contained in:
		
							parent
							
								
									16a3f0acc1
								
							
						
					
					
						commit
						78273c2998
					
				| 
						 | 
				
			
			@ -18,6 +18,7 @@ package validating
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"reflect"
 | 
			
		||||
| 
						 | 
				
			
			@ -47,7 +48,27 @@ type RolloutCreateUpdateHandler struct {
 | 
			
		|||
	Decoder *admission.Decoder
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ admission.Handler = &RolloutCreateUpdateHandler{}
 | 
			
		||||
var (
 | 
			
		||||
	_ admission.Handler = &RolloutCreateUpdateHandler{}
 | 
			
		||||
	// PartitionReplicasLimitWithTraffic represents the maximum percentage of replicas
 | 
			
		||||
	// allowed for a step of partition-style release, if traffic/matches specified.
 | 
			
		||||
	// If a step is configured with a number of replicas exceeding this percentage, the traffic strategy for that step
 | 
			
		||||
	// must not be specified. If this rule is violated, the Rollout webhook will block the creation or modification of the Rollout.
 | 
			
		||||
	// The default limit is set to 50%.
 | 
			
		||||
	// Here is why we set this limit for partition style release:
 | 
			
		||||
	// In rollback and continuous scenarios, usually we expect the Rollout to route all traffic to the stable version first.
 | 
			
		||||
	// However, if the stable version's pods are relatively few (less than 1-PartitionReplicasLimitWithTraffic), this might overload the stable version's pods.
 | 
			
		||||
	PartitionReplicasLimitWithTraffic = 50
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	flag.IntVar(&PartitionReplicasLimitWithTraffic, "partition-percent-limit", 50, "represents the maximum percentage of replicas allowed for a step of partition-style release, if traffic/matches specified.")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// record upper level information about the rollout
 | 
			
		||||
type validateContext struct {
 | 
			
		||||
	style string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Handle handles admission requests.
 | 
			
		||||
func (h *RolloutCreateUpdateHandler) Handle(ctx context.Context, req admission.Request) admission.Response {
 | 
			
		||||
| 
						 | 
				
			
			@ -155,7 +176,7 @@ func (h *RolloutCreateUpdateHandler) validateRolloutUpdate(oldObj, newObj *appsv
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func (h *RolloutCreateUpdateHandler) validateRollout(rollout *appsv1beta1.Rollout) field.ErrorList {
 | 
			
		||||
	errList := validateRolloutSpec(rollout, field.NewPath("Spec"))
 | 
			
		||||
	errList := validateRolloutSpec(GetContextFromv1beta1Rollout(rollout), rollout, field.NewPath("Spec"))
 | 
			
		||||
	errList = append(errList, h.validateRolloutConflict(rollout, field.NewPath("Conflict Checker"))...)
 | 
			
		||||
	return errList
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -178,9 +199,9 @@ func (h *RolloutCreateUpdateHandler) validateRolloutConflict(rollout *appsv1beta
 | 
			
		|||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func validateRolloutSpec(rollout *appsv1beta1.Rollout, fldPath *field.Path) field.ErrorList {
 | 
			
		||||
func validateRolloutSpec(c *validateContext, rollout *appsv1beta1.Rollout, fldPath *field.Path) field.ErrorList {
 | 
			
		||||
	errList := validateRolloutSpecObjectRef(&rollout.Spec.WorkloadRef, fldPath.Child("ObjectRef"))
 | 
			
		||||
	errList = append(errList, validateRolloutSpecStrategy(&rollout.Spec.Strategy, fldPath.Child("Strategy"))...)
 | 
			
		||||
	errList = append(errList, validateRolloutSpecStrategy(c, &rollout.Spec.Strategy, fldPath.Child("Strategy"))...)
 | 
			
		||||
	return errList
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -196,7 +217,7 @@ func validateRolloutSpecObjectRef(workloadRef *appsv1beta1.ObjectRef, fldPath *f
 | 
			
		|||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func validateRolloutSpecStrategy(strategy *appsv1beta1.RolloutStrategy, fldPath *field.Path) field.ErrorList {
 | 
			
		||||
func validateRolloutSpecStrategy(c *validateContext, strategy *appsv1beta1.RolloutStrategy, fldPath *field.Path) field.ErrorList {
 | 
			
		||||
	if strategy.Canary == nil && strategy.BlueGreen == nil {
 | 
			
		||||
		return field.ErrorList{field.Invalid(fldPath, nil, "Canary and BlueGreen cannot both be empty")}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -204,25 +225,13 @@ func validateRolloutSpecStrategy(strategy *appsv1beta1.RolloutStrategy, fldPath
 | 
			
		|||
		return field.ErrorList{field.Invalid(fldPath, nil, "Canary and BlueGreen cannot both be set")}
 | 
			
		||||
	}
 | 
			
		||||
	if strategy.BlueGreen != nil {
 | 
			
		||||
		return validateRolloutSpecBlueGreenStrategy(strategy.BlueGreen, fldPath.Child("BlueGreen"))
 | 
			
		||||
		return validateRolloutSpecBlueGreenStrategy(c, strategy.BlueGreen, fldPath.Child("BlueGreen"))
 | 
			
		||||
	}
 | 
			
		||||
	return validateRolloutSpecCanaryStrategy(strategy.Canary, fldPath.Child("Canary"))
 | 
			
		||||
	return validateRolloutSpecCanaryStrategy(c, strategy.Canary, fldPath.Child("Canary"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TrafficRule string
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	TrafficRuleCanary    TrafficRule = "Canary"
 | 
			
		||||
	TrafficRuleBlueGreen TrafficRule = "BlueGreen"
 | 
			
		||||
	NoTraffic            TrafficRule = "NoTraffic"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func validateRolloutSpecCanaryStrategy(canary *appsv1beta1.CanaryStrategy, fldPath *field.Path) field.ErrorList {
 | 
			
		||||
	trafficRule := NoTraffic
 | 
			
		||||
	if len(canary.TrafficRoutings) > 0 {
 | 
			
		||||
		trafficRule = TrafficRuleCanary
 | 
			
		||||
	}
 | 
			
		||||
	errList := validateRolloutSpecCanarySteps(canary.Steps, fldPath.Child("Steps"), trafficRule)
 | 
			
		||||
func validateRolloutSpecCanaryStrategy(c *validateContext, canary *appsv1beta1.CanaryStrategy, fldPath *field.Path) field.ErrorList {
 | 
			
		||||
	errList := validateRolloutSpecCanarySteps(c, canary.Steps, fldPath.Child("Steps"))
 | 
			
		||||
	if len(canary.TrafficRoutings) > 1 {
 | 
			
		||||
		errList = append(errList, field.Invalid(fldPath, canary.TrafficRoutings, "Rollout currently only support single TrafficRouting."))
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -232,12 +241,8 @@ func validateRolloutSpecCanaryStrategy(canary *appsv1beta1.CanaryStrategy, fldPa
 | 
			
		|||
	return errList
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func validateRolloutSpecBlueGreenStrategy(blueGreen *appsv1beta1.BlueGreenStrategy, fldPath *field.Path) field.ErrorList {
 | 
			
		||||
	trafficRule := NoTraffic
 | 
			
		||||
	if len(blueGreen.TrafficRoutings) > 0 {
 | 
			
		||||
		trafficRule = TrafficRuleBlueGreen
 | 
			
		||||
	}
 | 
			
		||||
	errList := validateRolloutSpecCanarySteps(blueGreen.Steps, fldPath.Child("Steps"), trafficRule)
 | 
			
		||||
func validateRolloutSpecBlueGreenStrategy(c *validateContext, blueGreen *appsv1beta1.BlueGreenStrategy, fldPath *field.Path) field.ErrorList {
 | 
			
		||||
	errList := validateRolloutSpecCanarySteps(c, blueGreen.Steps, fldPath.Child("Steps"))
 | 
			
		||||
	if len(blueGreen.TrafficRoutings) > 1 {
 | 
			
		||||
		errList = append(errList, field.Invalid(fldPath, blueGreen.TrafficRoutings, "Rollout currently only support single TrafficRouting."))
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -275,7 +280,7 @@ func validateRolloutSpecCanaryTraffic(traffic appsv1beta1.TrafficRoutingRef, fld
 | 
			
		|||
	return errList
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func validateRolloutSpecCanarySteps(steps []appsv1beta1.CanaryStep, fldPath *field.Path, trafficRule TrafficRule) field.ErrorList {
 | 
			
		||||
func validateRolloutSpecCanarySteps(c *validateContext, steps []appsv1beta1.CanaryStep, fldPath *field.Path) field.ErrorList {
 | 
			
		||||
	stepCount := len(steps)
 | 
			
		||||
	if stepCount == 0 {
 | 
			
		||||
		return field.ErrorList{field.Invalid(fldPath, steps, "The number of Canary.Steps cannot be empty")}
 | 
			
		||||
| 
						 | 
				
			
			@ -293,13 +298,24 @@ func validateRolloutSpecCanarySteps(steps []appsv1beta1.CanaryStep, fldPath *fie
 | 
			
		|||
			return field.ErrorList{field.Invalid(fldPath.Index(i).Child("Replicas"),
 | 
			
		||||
				s.Replicas, `replicas must be positive number, or a percentage with "0%" < canaryReplicas <= "100%"`)}
 | 
			
		||||
		}
 | 
			
		||||
		if trafficRule == NoTraffic || s.Traffic == nil {
 | 
			
		||||
		// no traffic strategy is configured for current step
 | 
			
		||||
		if s.Traffic == nil && len(s.Matches) == 0 {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		// replicas is percentage
 | 
			
		||||
		if c.style == string(appsv1beta1.PartitionRollingStyle) && IsPercentageCanaryReplicasType(s.Replicas) {
 | 
			
		||||
			currCanaryReplicas, _ := intstr.GetScaledValueFromIntOrPercent(s.Replicas, 100, true)
 | 
			
		||||
			if currCanaryReplicas > PartitionReplicasLimitWithTraffic {
 | 
			
		||||
				return field.ErrorList{field.Invalid(fldPath.Index(i).Child("steps"), steps, `For partition style rollout: step[x].replicas must not greater than partition-percent-limit if traffic specified`)}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if s.Traffic == nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		is := intstr.FromString(*s.Traffic)
 | 
			
		||||
		weight, err := intstr.GetScaledValueFromIntOrPercent(&is, 100, true)
 | 
			
		||||
		switch trafficRule {
 | 
			
		||||
		case TrafficRuleBlueGreen:
 | 
			
		||||
		switch c.style {
 | 
			
		||||
		case string(appsv1beta1.BlueGreenRollingStyle):
 | 
			
		||||
			// traffic "0%" is allowed in blueGreen strategy
 | 
			
		||||
			if err != nil || weight < 0 || weight > 100 {
 | 
			
		||||
				return field.ErrorList{field.Invalid(fldPath.Index(i).Child("steps"), steps, `traffic must be percentage with "0%" <= traffic <= "100%" in blueGreen strategy`)}
 | 
			
		||||
| 
						 | 
				
			
			@ -355,3 +371,14 @@ func (h *RolloutCreateUpdateHandler) InjectDecoder(d *admission.Decoder) error {
 | 
			
		|||
	h.Decoder = d
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetContextFromv1beta1Rollout(rollout *appsv1beta1.Rollout) *validateContext {
 | 
			
		||||
	if rollout.Spec.Strategy.Canary == nil && rollout.Spec.Strategy.BlueGreen == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	style := rollout.Spec.Strategy.GetRollingStyle()
 | 
			
		||||
	if appsv1beta1.IsRealPartition(rollout) {
 | 
			
		||||
		style = appsv1beta1.PartitionRollingStyle
 | 
			
		||||
	}
 | 
			
		||||
	return &validateContext{style: string(style)}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,6 +22,7 @@ import (
 | 
			
		|||
	. "github.com/onsi/ginkgo"
 | 
			
		||||
	. "github.com/onsi/gomega"
 | 
			
		||||
	rolloutapi "github.com/openkruise/rollouts/api"
 | 
			
		||||
	appsv1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
 | 
			
		||||
	appsv1beta1 "github.com/openkruise/rollouts/api/v1beta1"
 | 
			
		||||
	apps "k8s.io/api/apps/v1"
 | 
			
		||||
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
			
		||||
| 
						 | 
				
			
			@ -102,6 +103,63 @@ var (
 | 
			
		|||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	rolloutV1alpha1 = appsv1alpha1.Rollout{
 | 
			
		||||
		TypeMeta: metav1.TypeMeta{
 | 
			
		||||
			APIVersion: appsv1alpha1.SchemeGroupVersion.String(),
 | 
			
		||||
			Kind:       "Rollout",
 | 
			
		||||
		},
 | 
			
		||||
		ObjectMeta: metav1.ObjectMeta{
 | 
			
		||||
			Name:        "rollout-demo",
 | 
			
		||||
			Namespace:   "namespace-unit-test",
 | 
			
		||||
			Annotations: map[string]string{},
 | 
			
		||||
		},
 | 
			
		||||
		Spec: appsv1alpha1.RolloutSpec{
 | 
			
		||||
			ObjectRef: appsv1alpha1.ObjectRef{
 | 
			
		||||
				WorkloadRef: &appsv1alpha1.WorkloadRef{
 | 
			
		||||
					APIVersion: apps.SchemeGroupVersion.String(),
 | 
			
		||||
					Kind:       "Deployment",
 | 
			
		||||
					Name:       "deployment-demo",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			Strategy: appsv1alpha1.RolloutStrategy{
 | 
			
		||||
				Canary: &appsv1alpha1.CanaryStrategy{
 | 
			
		||||
					Steps: []appsv1alpha1.CanaryStep{
 | 
			
		||||
						{
 | 
			
		||||
							TrafficRoutingStrategy: appsv1alpha1.TrafficRoutingStrategy{
 | 
			
		||||
								Weight: utilpointer.Int32(10),
 | 
			
		||||
							},
 | 
			
		||||
							Pause: appsv1alpha1.RolloutPause{},
 | 
			
		||||
						},
 | 
			
		||||
						{
 | 
			
		||||
							TrafficRoutingStrategy: appsv1alpha1.TrafficRoutingStrategy{
 | 
			
		||||
								Weight: utilpointer.Int32(30),
 | 
			
		||||
							},
 | 
			
		||||
							Pause: appsv1alpha1.RolloutPause{Duration: utilpointer.Int32(1 * 24 * 60 * 60)},
 | 
			
		||||
						},
 | 
			
		||||
						{
 | 
			
		||||
							TrafficRoutingStrategy: appsv1alpha1.TrafficRoutingStrategy{
 | 
			
		||||
								Weight: utilpointer.Int32(100),
 | 
			
		||||
							},
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
					TrafficRoutings: []appsv1alpha1.TrafficRoutingRef{
 | 
			
		||||
						{
 | 
			
		||||
							Service: "service-demo",
 | 
			
		||||
							Ingress: &appsv1alpha1.IngressTrafficRouting{
 | 
			
		||||
								ClassType: "nginx",
 | 
			
		||||
								Name:      "ingress-nginx-demo",
 | 
			
		||||
							},
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		Status: appsv1alpha1.RolloutStatus{
 | 
			
		||||
			CanaryStatus: &appsv1alpha1.CanaryStatus{
 | 
			
		||||
				CurrentStepState: appsv1alpha1.CanaryStepStateCompleted,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
| 
						 | 
				
			
			@ -262,6 +320,74 @@ func TestRolloutValidateCreate(t *testing.T) {
 | 
			
		|||
				return []client.Object{object}
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Name:    "test with replicasLimitWithTraffic - 1",
 | 
			
		||||
			Succeed: true,
 | 
			
		||||
			GetObject: func() []client.Object {
 | 
			
		||||
				object := rollout.DeepCopy()
 | 
			
		||||
				n := len(object.Spec.Strategy.Canary.Steps)
 | 
			
		||||
				object.Spec.Strategy.Canary.Steps[n-1].Replicas = &intstr.IntOrString{Type: intstr.String, StrVal: "30%"}
 | 
			
		||||
				return []client.Object{object}
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Name:    "test with replicasLimitWithTraffic - 2",
 | 
			
		||||
			Succeed: false,
 | 
			
		||||
			GetObject: func() []client.Object {
 | 
			
		||||
				object := rollout.DeepCopy()
 | 
			
		||||
				n := len(object.Spec.Strategy.Canary.Steps)
 | 
			
		||||
				object.Spec.Strategy.Canary.Steps[n-1].Replicas = &intstr.IntOrString{Type: intstr.String, StrVal: "31%"}
 | 
			
		||||
				return []client.Object{object}
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Name:    "test with replicasLimitWithTraffic - 2 - canary style",
 | 
			
		||||
			Succeed: true,
 | 
			
		||||
			GetObject: func() []client.Object {
 | 
			
		||||
				object := rollout.DeepCopy()
 | 
			
		||||
				object.Spec.Strategy.Canary.EnableExtraWorkloadForCanary = true
 | 
			
		||||
				n := len(object.Spec.Strategy.Canary.Steps)
 | 
			
		||||
				object.Spec.Strategy.Canary.Steps[n-1].Replicas = &intstr.IntOrString{Type: intstr.String, StrVal: "31%"}
 | 
			
		||||
				return []client.Object{object}
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Name:    "test with replicasLimitWithTraffic - 2 - cloneset",
 | 
			
		||||
			Succeed: false,
 | 
			
		||||
			GetObject: func() []client.Object {
 | 
			
		||||
				object := rollout.DeepCopy()
 | 
			
		||||
				object.Spec.WorkloadRef = appsv1beta1.ObjectRef{
 | 
			
		||||
					APIVersion: "apps.kruise.io/v1alpha1",
 | 
			
		||||
					Kind:       "CloneSet",
 | 
			
		||||
					Name:       "whatever",
 | 
			
		||||
				}
 | 
			
		||||
				n := len(object.Spec.Strategy.Canary.Steps)
 | 
			
		||||
				object.Spec.Strategy.Canary.Steps[n-1].Replicas = &intstr.IntOrString{Type: intstr.String, StrVal: "31%"}
 | 
			
		||||
				return []client.Object{object}
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Name:    "test with replicasLimitWithTraffic - 3",
 | 
			
		||||
			Succeed: true,
 | 
			
		||||
			GetObject: func() []client.Object {
 | 
			
		||||
				object := rollout.DeepCopy()
 | 
			
		||||
				PartitionReplicasLimitWithTraffic = 100
 | 
			
		||||
				n := len(object.Spec.Strategy.Canary.Steps)
 | 
			
		||||
				object.Spec.Strategy.Canary.Steps[n-1].Replicas = &intstr.IntOrString{Type: intstr.String, StrVal: "100%"}
 | 
			
		||||
				return []client.Object{object}
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Name:    "test with replicasLimitWithTraffic - 4",
 | 
			
		||||
			Succeed: false,
 | 
			
		||||
			GetObject: func() []client.Object {
 | 
			
		||||
				object := rollout.DeepCopy()
 | 
			
		||||
				PartitionReplicasLimitWithTraffic = 50
 | 
			
		||||
				n := len(object.Spec.Strategy.Canary.Steps)
 | 
			
		||||
				object.Spec.Strategy.Canary.Steps[n-1].Replicas = &intstr.IntOrString{Type: intstr.String, StrVal: "51%"}
 | 
			
		||||
				return []client.Object{object}
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		//{
 | 
			
		||||
		//	Name:    "The last Steps.Traffic is not 100",
 | 
			
		||||
		//	Succeed: false,
 | 
			
		||||
| 
						 | 
				
			
			@ -335,6 +461,133 @@ func TestRolloutValidateCreate(t *testing.T) {
 | 
			
		|||
			errList := handler.validateRollout(objects[0].(*appsv1beta1.Rollout))
 | 
			
		||||
			t.Log(errList)
 | 
			
		||||
			Expect(len(errList) == 0).Should(Equal(cs.Succeed))
 | 
			
		||||
			// restore PartitionReplicasLimitWithTraffic after each case
 | 
			
		||||
			PartitionReplicasLimitWithTraffic = 30
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestRolloutV1alpha1ValidateCreate(t *testing.T) {
 | 
			
		||||
	RegisterFailHandler(Fail)
 | 
			
		||||
 | 
			
		||||
	cases := []struct {
 | 
			
		||||
		Name      string
 | 
			
		||||
		Succeed   bool
 | 
			
		||||
		GetObject func() []client.Object
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			Name:    "Canary style",
 | 
			
		||||
			Succeed: true,
 | 
			
		||||
			GetObject: func() []client.Object {
 | 
			
		||||
				obj := rolloutV1alpha1.DeepCopy()
 | 
			
		||||
				return []client.Object{obj}
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Name:    "Partition style without replicas - 1",
 | 
			
		||||
			Succeed: false,
 | 
			
		||||
			GetObject: func() []client.Object {
 | 
			
		||||
				obj := rolloutV1alpha1.DeepCopy()
 | 
			
		||||
				obj.Annotations[appsv1alpha1.RolloutStyleAnnotation] = string(appsv1alpha1.PartitionRollingStyle)
 | 
			
		||||
				return []client.Object{obj}
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Name:    "Partition style without replicas - 2",
 | 
			
		||||
			Succeed: true,
 | 
			
		||||
			GetObject: func() []client.Object {
 | 
			
		||||
				obj := rolloutV1alpha1.DeepCopy()
 | 
			
		||||
				PartitionReplicasLimitWithTraffic = 100
 | 
			
		||||
				obj.Annotations[appsv1alpha1.RolloutStyleAnnotation] = string(appsv1alpha1.PartitionRollingStyle)
 | 
			
		||||
				return []client.Object{obj}
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Name:    "Partition style without replicas - 3",
 | 
			
		||||
			Succeed: false,
 | 
			
		||||
			GetObject: func() []client.Object {
 | 
			
		||||
				obj := rolloutV1alpha1.DeepCopy()
 | 
			
		||||
				obj.Spec.Strategy.Canary.Steps[len(obj.Spec.Strategy.Canary.Steps)-1].Weight = utilpointer.Int32(32)
 | 
			
		||||
				PartitionReplicasLimitWithTraffic = 31
 | 
			
		||||
				obj.Annotations[appsv1alpha1.RolloutStyleAnnotation] = string(appsv1alpha1.PartitionRollingStyle)
 | 
			
		||||
				return []client.Object{obj}
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Name:    "Partition style without replicas- 3 - canary style",
 | 
			
		||||
			Succeed: true,
 | 
			
		||||
			GetObject: func() []client.Object {
 | 
			
		||||
				obj := rolloutV1alpha1.DeepCopy()
 | 
			
		||||
				obj.Spec.Strategy.Canary.Steps[len(obj.Spec.Strategy.Canary.Steps)-1].Weight = utilpointer.Int32(32)
 | 
			
		||||
				PartitionReplicasLimitWithTraffic = 31
 | 
			
		||||
				return []client.Object{obj}
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Name:    "Partition style without replicas - 3 - cloneset",
 | 
			
		||||
			Succeed: false,
 | 
			
		||||
			GetObject: func() []client.Object {
 | 
			
		||||
				obj := rolloutV1alpha1.DeepCopy()
 | 
			
		||||
				obj.Spec.ObjectRef.WorkloadRef = &appsv1alpha1.WorkloadRef{
 | 
			
		||||
					APIVersion: "apps.kruise.io/v1alpha1",
 | 
			
		||||
					Kind:       "CloneSet",
 | 
			
		||||
					Name:       "whatever",
 | 
			
		||||
				}
 | 
			
		||||
				obj.Spec.Strategy.Canary.Steps[len(obj.Spec.Strategy.Canary.Steps)-1].Weight = utilpointer.Int32(32)
 | 
			
		||||
				PartitionReplicasLimitWithTraffic = 31
 | 
			
		||||
				obj.Annotations[appsv1alpha1.RolloutStyleAnnotation] = string(appsv1alpha1.PartitionRollingStyle)
 | 
			
		||||
				return []client.Object{obj}
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Name:    "Partition style with replicas - 1",
 | 
			
		||||
			Succeed: false,
 | 
			
		||||
			GetObject: func() []client.Object {
 | 
			
		||||
				obj := rolloutV1alpha1.DeepCopy()
 | 
			
		||||
				obj.Spec.Strategy.Canary.Steps[len(obj.Spec.Strategy.Canary.Steps)-1].Weight = utilpointer.Int32(50)
 | 
			
		||||
				obj.Spec.Strategy.Canary.Steps[len(obj.Spec.Strategy.Canary.Steps)-1].Replicas = &intstr.IntOrString{Type: intstr.String, StrVal: "32%"}
 | 
			
		||||
				PartitionReplicasLimitWithTraffic = 31
 | 
			
		||||
				obj.Annotations[appsv1alpha1.RolloutStyleAnnotation] = string(appsv1alpha1.PartitionRollingStyle)
 | 
			
		||||
				return []client.Object{obj}
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Name:    "Partition style with replicas - 2",
 | 
			
		||||
			Succeed: true,
 | 
			
		||||
			GetObject: func() []client.Object {
 | 
			
		||||
				obj := rolloutV1alpha1.DeepCopy()
 | 
			
		||||
				obj.Spec.Strategy.Canary.Steps[len(obj.Spec.Strategy.Canary.Steps)-1].Weight = utilpointer.Int32(50)
 | 
			
		||||
				obj.Spec.Strategy.Canary.Steps[len(obj.Spec.Strategy.Canary.Steps)-1].Replicas = &intstr.IntOrString{Type: intstr.String, StrVal: "31%"}
 | 
			
		||||
				PartitionReplicasLimitWithTraffic = 31
 | 
			
		||||
				obj.Annotations[appsv1alpha1.RolloutStyleAnnotation] = string(appsv1alpha1.PartitionRollingStyle)
 | 
			
		||||
				return []client.Object{obj}
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Name:    "Partition style with replicas - 3",
 | 
			
		||||
			Succeed: true,
 | 
			
		||||
			GetObject: func() []client.Object {
 | 
			
		||||
				obj := rolloutV1alpha1.DeepCopy()
 | 
			
		||||
				obj.Spec.Strategy.Canary.Steps[len(obj.Spec.Strategy.Canary.Steps)-1].Weight = nil
 | 
			
		||||
				obj.Spec.Strategy.Canary.Steps[len(obj.Spec.Strategy.Canary.Steps)-1].Replicas = &intstr.IntOrString{Type: intstr.String, StrVal: "50%"}
 | 
			
		||||
				obj.Annotations[appsv1alpha1.RolloutStyleAnnotation] = string(appsv1alpha1.PartitionRollingStyle)
 | 
			
		||||
				return []client.Object{obj}
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, cs := range cases {
 | 
			
		||||
		t.Run(cs.Name, func(t *testing.T) {
 | 
			
		||||
			objects := cs.GetObject()
 | 
			
		||||
			cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()
 | 
			
		||||
			handler := RolloutCreateUpdateHandler{
 | 
			
		||||
				Client: cli,
 | 
			
		||||
			}
 | 
			
		||||
			errList := handler.validateV1alpha1Rollout(objects[0].(*appsv1alpha1.Rollout))
 | 
			
		||||
			t.Log(errList)
 | 
			
		||||
			Expect(len(errList) == 0).Should(Equal(cs.Succeed))
 | 
			
		||||
			// restore PartitionReplicasLimitWithTraffic after each case
 | 
			
		||||
			PartitionReplicasLimitWithTraffic = 30
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,6 +25,7 @@ import (
 | 
			
		|||
	appsv1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
 | 
			
		||||
	"github.com/openkruise/rollouts/pkg/util"
 | 
			
		||||
	utilclient "github.com/openkruise/rollouts/pkg/util/client"
 | 
			
		||||
	apps "k8s.io/api/apps/v1"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/runtime/schema"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/util/intstr"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/util/validation/field"
 | 
			
		||||
| 
						 | 
				
			
			@ -70,7 +71,7 @@ func (h *RolloutCreateUpdateHandler) validateV1alpha1RolloutUpdate(oldObj, newOb
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func (h *RolloutCreateUpdateHandler) validateV1alpha1Rollout(rollout *appsv1alpha1.Rollout) field.ErrorList {
 | 
			
		||||
	errList := validateV1alpha1RolloutSpec(rollout, field.NewPath("Spec"))
 | 
			
		||||
	errList := validateV1alpha1RolloutSpec(GetContextFromv1alpha1Rollout(rollout), rollout, field.NewPath("Spec"))
 | 
			
		||||
	errList = append(errList, h.validateV1alpha1RolloutConflict(rollout, field.NewPath("Conflict Checker"))...)
 | 
			
		||||
	return errList
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -93,10 +94,10 @@ func (h *RolloutCreateUpdateHandler) validateV1alpha1RolloutConflict(rollout *ap
 | 
			
		|||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func validateV1alpha1RolloutSpec(rollout *appsv1alpha1.Rollout, fldPath *field.Path) field.ErrorList {
 | 
			
		||||
func validateV1alpha1RolloutSpec(c *validateContext, rollout *appsv1alpha1.Rollout, fldPath *field.Path) field.ErrorList {
 | 
			
		||||
	errList := validateV1alpha1RolloutSpecObjectRef(&rollout.Spec.ObjectRef, fldPath.Child("ObjectRef"))
 | 
			
		||||
	errList = append(errList, validateV1alpha1RolloutRollingStyle(rollout, field.NewPath("RollingStyle"))...)
 | 
			
		||||
	errList = append(errList, validateV1alpha1RolloutSpecStrategy(&rollout.Spec.Strategy, fldPath.Child("Strategy"))...)
 | 
			
		||||
	errList = append(errList, validateV1alpha1RolloutSpecStrategy(c, &rollout.Spec.Strategy, fldPath.Child("Strategy"))...)
 | 
			
		||||
	return errList
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -122,16 +123,16 @@ func validateV1alpha1RolloutSpecObjectRef(objectRef *appsv1alpha1.ObjectRef, fld
 | 
			
		|||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func validateV1alpha1RolloutSpecStrategy(strategy *appsv1alpha1.RolloutStrategy, fldPath *field.Path) field.ErrorList {
 | 
			
		||||
	return validateV1alpha1RolloutSpecCanaryStrategy(strategy.Canary, fldPath.Child("Canary"))
 | 
			
		||||
func validateV1alpha1RolloutSpecStrategy(c *validateContext, strategy *appsv1alpha1.RolloutStrategy, fldPath *field.Path) field.ErrorList {
 | 
			
		||||
	return validateV1alpha1RolloutSpecCanaryStrategy(c, strategy.Canary, fldPath.Child("Canary"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func validateV1alpha1RolloutSpecCanaryStrategy(canary *appsv1alpha1.CanaryStrategy, fldPath *field.Path) field.ErrorList {
 | 
			
		||||
func validateV1alpha1RolloutSpecCanaryStrategy(c *validateContext, canary *appsv1alpha1.CanaryStrategy, fldPath *field.Path) field.ErrorList {
 | 
			
		||||
	if canary == nil {
 | 
			
		||||
		return field.ErrorList{field.Invalid(fldPath, nil, "Canary cannot be empty")}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	errList := validateV1alpha1RolloutSpecCanarySteps(canary.Steps, fldPath.Child("Steps"), len(canary.TrafficRoutings) > 0)
 | 
			
		||||
	errList := validateV1alpha1RolloutSpecCanarySteps(c, canary.Steps, fldPath.Child("Steps"), len(canary.TrafficRoutings) > 0)
 | 
			
		||||
	if len(canary.TrafficRoutings) > 1 {
 | 
			
		||||
		errList = append(errList, field.Invalid(fldPath, canary.TrafficRoutings, "Rollout currently only support single TrafficRouting."))
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -169,7 +170,7 @@ func validateV1alpha1RolloutSpecCanaryTraffic(traffic appsv1alpha1.TrafficRoutin
 | 
			
		|||
	return errList
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func validateV1alpha1RolloutSpecCanarySteps(steps []appsv1alpha1.CanaryStep, fldPath *field.Path, isTraffic bool) field.ErrorList {
 | 
			
		||||
func validateV1alpha1RolloutSpecCanarySteps(c *validateContext, steps []appsv1alpha1.CanaryStep, fldPath *field.Path, isTraffic bool) field.ErrorList {
 | 
			
		||||
	stepCount := len(steps)
 | 
			
		||||
	if stepCount == 0 {
 | 
			
		||||
		return field.ErrorList{field.Invalid(fldPath, steps, "The number of Canary.Steps cannot be empty")}
 | 
			
		||||
| 
						 | 
				
			
			@ -188,6 +189,25 @@ func validateV1alpha1RolloutSpecCanarySteps(steps []appsv1alpha1.CanaryStep, fld
 | 
			
		|||
				return field.ErrorList{field.Invalid(fldPath.Index(i).Child("Replicas"),
 | 
			
		||||
					s.Replicas, `replicas must be positive number, or a percentage with "0%" < canaryReplicas <= "100%"`)}
 | 
			
		||||
			}
 | 
			
		||||
			// is partiton-style release
 | 
			
		||||
			// and has traffic strategy for this step
 | 
			
		||||
			// and replicas is in percentage format and greater than replicasLimitWithTraffic
 | 
			
		||||
			if c.style == string(appsv1alpha1.PartitionRollingStyle) &&
 | 
			
		||||
				IsPercentageCanaryReplicasType(s.Replicas) && canaryReplicas > PartitionReplicasLimitWithTraffic &&
 | 
			
		||||
				(s.Matches != nil || s.Weight != nil) {
 | 
			
		||||
				return field.ErrorList{field.Invalid(fldPath.Index(i).Child("steps"), steps,
 | 
			
		||||
					`For patition style rollout: step[x].replicas must not greater than replicasLimitWithTraffic if traffic or matches specified`)}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			// replicas is nil, weight is not nil
 | 
			
		||||
			if c.style == string(appsv1alpha1.PartitionRollingStyle) && *s.Weight > int32(PartitionReplicasLimitWithTraffic) {
 | 
			
		||||
				return field.ErrorList{field.Invalid(fldPath.Index(i).Child("steps"), steps,
 | 
			
		||||
					`For patition style rollout: step[x].weight must not greater than replicasLimitWithTraffic if replicas is not specified`)}
 | 
			
		||||
			}
 | 
			
		||||
			if *s.Weight <= 0 || *s.Weight > 100 {
 | 
			
		||||
				return field.ErrorList{field.Invalid(fldPath.Index(i).Child("Weight"), s.Weight,
 | 
			
		||||
					`weight must be positive number, and less than or equal to replicasLimitWithTraffic (defaults to 30)`)}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -225,3 +245,19 @@ func IsSameV1alpha1WorkloadRefGVKName(a, b *appsv1alpha1.WorkloadRef) bool {
 | 
			
		|||
	}
 | 
			
		||||
	return reflect.DeepEqual(a, b)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetContextFromv1alpha1Rollout(rollout *appsv1alpha1.Rollout) *validateContext {
 | 
			
		||||
	if rollout.Spec.Strategy.Canary == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	style := appsv1alpha1.PartitionRollingStyle
 | 
			
		||||
	switch strings.ToLower(rollout.Annotations[appsv1alpha1.RolloutStyleAnnotation]) {
 | 
			
		||||
	case "", strings.ToLower(string(appsv1alpha1.CanaryRollingStyle)):
 | 
			
		||||
		targetRef := rollout.Spec.ObjectRef.WorkloadRef
 | 
			
		||||
		if targetRef.APIVersion == apps.SchemeGroupVersion.String() && targetRef.Kind == reflect.TypeOf(apps.Deployment{}).Name() {
 | 
			
		||||
			style = appsv1alpha1.CanaryRollingStyle
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &validateContext{style: string(style)}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -1,3 +1,5 @@
 | 
			
		|||
# we recommend that new test cases or modifications to existing test cases should
 | 
			
		||||
# use v1beta1 Rollout, eg. use rollout_v1beta1_canary_base.yaml
 | 
			
		||||
apiVersion: rollouts.kruise.io/v1alpha1
 | 
			
		||||
kind: Rollout
 | 
			
		||||
metadata:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,17 +14,13 @@ spec:
 | 
			
		|||
      - traffic: 20%
 | 
			
		||||
        replicas: 20%
 | 
			
		||||
        pause: {}
 | 
			
		||||
      - traffic: 40%
 | 
			
		||||
        replicas: 40%
 | 
			
		||||
      - replicas: 40%
 | 
			
		||||
        pause: {duration: 10}
 | 
			
		||||
      - traffic: 60%
 | 
			
		||||
        replicas: 60%
 | 
			
		||||
      - replicas: 60%
 | 
			
		||||
        pause: {duration: 10}
 | 
			
		||||
      - traffic: 80%
 | 
			
		||||
        replicas: 80%
 | 
			
		||||
      - replicas: 80%
 | 
			
		||||
        pause: {duration: 10}
 | 
			
		||||
      - traffic: 100%
 | 
			
		||||
        replicas: 100%
 | 
			
		||||
      - replicas: 100%
 | 
			
		||||
        pause: {duration: 0}
 | 
			
		||||
      trafficRoutings:
 | 
			
		||||
      - service: echoserver
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue