diff --git a/artifacts/examples/linkerd-canary-steps.yaml b/artifacts/examples/linkerd-canary-steps.yaml index 4a8b7694..1e1a09af 100644 --- a/artifacts/examples/linkerd-canary-steps.yaml +++ b/artifacts/examples/linkerd-canary-steps.yaml @@ -24,7 +24,6 @@ spec: analysis: interval: 15s threshold: 10 - maxWeight: 50 stepWeights: [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55] metrics: - name: request-success-rate diff --git a/pkg/controller/scheduler.go b/pkg/controller/scheduler.go index 069273a0..3008b8c2 100644 --- a/pkg/controller/scheduler.go +++ b/pkg/controller/scheduler.go @@ -14,20 +14,27 @@ import ( "github.com/weaveworks/flagger/pkg/router" ) +func (c *Controller) min(a int, b int) int { + if a < b { + return a + } + return b +} + func (c *Controller) maxWeight(canary *flaggerv1.Canary) int { var stepWeightsLen = len(canary.GetAnalysis().StepWeights) if stepWeightsLen > 0 { - return canary.GetAnalysis().StepWeights[stepWeightsLen-1] + return c.min(c.totalWeight(canary), canary.GetAnalysis().StepWeights[stepWeightsLen-1]) } if canary.GetAnalysis().MaxWeight > 0 { return canary.GetAnalysis().MaxWeight } - // set max weight default value to 100% - return 100 + // set max weight default value to total weight + return c.totalWeight(canary) } func (c *Controller) totalWeight(canary *flaggerv1.Canary) int { - // set max weight default value to 100% + // set total weight default value to 100% return 100 } @@ -37,17 +44,30 @@ func (c *Controller) nextStepWeight(canary *flaggerv1.Canary, canaryWeight int) return canary.GetAnalysis().StepWeight } - if canaryWeight == 0 { - return canary.GetAnalysis().StepWeights[0] + totalWeight := c.totalWeight(canary) + maxStep := totalWeight - canaryWeight + + // If maxStep is zero we need to promote, so any non zero step weight will move the canary to promotion. + // This is the same use case as the last step via StepWeight. + if maxStep == 0 { + return 1 } + // return min of maxStep and the calculated step to avoid going above totalWeight + + // initial step + if canaryWeight == 0 { + return c.min(maxStep, canary.GetAnalysis().StepWeights[0]) + } + + // find the current step and return the difference in weight for i := 0; i < stepWeightsLen-1; i++ { if canary.GetAnalysis().StepWeights[i] == canaryWeight { - return canary.GetAnalysis().StepWeights[i+1] - canaryWeight + return c.min(maxStep, canary.GetAnalysis().StepWeights[i+1]-canaryWeight) } } - return c.totalWeight(canary) - canaryWeight + return maxStep } // scheduleCanaries synchronises the canary map with the jobs map, diff --git a/pkg/controller/scheduler_svc_test.go b/pkg/controller/scheduler_svc_test.go index 77b92ae5..ad1f608a 100644 --- a/pkg/controller/scheduler_svc_test.go +++ b/pkg/controller/scheduler_svc_test.go @@ -11,8 +11,28 @@ import ( flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1" ) +const ( + totalWeight = 100 +) + func TestScheduler_ServicePromotion(t *testing.T) { - mocks := newDeploymentFixture(newTestServiceCanary()) + testServicePromotion(t, newTestServiceCanary(), []int{totalWeight, 80, 60, 40}) +} + +func TestScheduler_ServicePromotionMaxWeight(t *testing.T) { + testServicePromotion(t, newTestServiceCanaryMaxWeight(), []int{totalWeight, 50, 0}) +} + +func TestScheduler_ServicePromotionWithWeightsHappyCase(t *testing.T) { + testServicePromotion(t, newTestServiceCanaryWithWeightsHappyCase(), []int{totalWeight, 99, 98, 90, 20}) +} + +func TestScheduler_ServicePromotionWithWeightsOverflow(t *testing.T) { + testServicePromotion(t, newTestServiceCanaryWithWeightsOverflow(), []int{totalWeight, 99, 98, 90, 0}) +} + +func testServicePromotion(t *testing.T, canary *flaggerv1.Canary, expectedPrimaryWeigths []int) { + mocks := newDeploymentFixture(canary) // init mocks.ctrl.advanceCanary("podinfo", "default") @@ -27,19 +47,15 @@ func TestScheduler_ServicePromotion(t *testing.T) { _, err = mocks.kubeClient.CoreV1().Services("default").Update(context.TODO(), svc2, metav1.UpdateOptions{}) require.NoError(t, err) - // detect service spec changes - mocks.ctrl.advanceCanary("podinfo", "default") - - primaryWeight, canaryWeight, mirrored, err := mocks.router.GetRoutes(mocks.canary) - require.NoError(t, err) - - primaryWeight = 60 - canaryWeight = 40 - err = mocks.router.SetRoutes(mocks.canary, primaryWeight, canaryWeight, mirrored) - require.NoError(t, err) - - // advance - mocks.ctrl.advanceCanary("podinfo", "default") + for _, expectedPrimaryWeigth := range expectedPrimaryWeigths { + mocks.ctrl.advanceCanary("podinfo", "default") + expectedCanaryWeight := totalWeight - expectedPrimaryWeigth + primaryWeight, canaryWeight, mirrored, err := mocks.router.GetRoutes(mocks.canary) + require.NoError(t, err) + assert.Equal(t, expectedPrimaryWeigth, primaryWeight) + assert.Equal(t, expectedCanaryWeight, canaryWeight) + assert.False(t, mirrored) + } // check progressing status c, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{}) @@ -57,9 +73,9 @@ func TestScheduler_ServicePromotion(t *testing.T) { // finalise mocks.ctrl.advanceCanary("podinfo", "default") - primaryWeight, canaryWeight, mirrored, err = mocks.router.GetRoutes(mocks.canary) + primaryWeight, canaryWeight, mirrored, err := mocks.router.GetRoutes(mocks.canary) require.NoError(t, err) - assert.Equal(t, 100, primaryWeight) + assert.Equal(t, totalWeight, primaryWeight) assert.Equal(t, 0, canaryWeight) assert.False(t, mirrored) @@ -101,7 +117,7 @@ func newTestServiceCanary() *flaggerv1.Canary { }, Analysis: &flaggerv1.CanaryAnalysis{ Threshold: 10, - StepWeight: 10, + StepWeight: 20, MaxWeight: 50, Metrics: []flaggerv1.CanaryMetric{ { @@ -120,3 +136,115 @@ func newTestServiceCanary() *flaggerv1.Canary { } return cd } + +func newTestServiceCanaryMaxWeight() *flaggerv1.Canary { + cd := &flaggerv1.Canary{ + TypeMeta: metav1.TypeMeta{APIVersion: flaggerv1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo", + }, + Spec: flaggerv1.CanarySpec{ + TargetRef: flaggerv1.CrossNamespaceObjectReference{ + Name: "podinfo", + APIVersion: "core/v1", + Kind: "Service", + }, + Service: flaggerv1.CanaryService{ + Port: 9898, + }, + Analysis: &flaggerv1.CanaryAnalysis{ + Threshold: 10, + StepWeight: 50, + MaxWeight: totalWeight, + Metrics: []flaggerv1.CanaryMetric{ + { + Name: "request-success-rate", + Threshold: 99, + Interval: "1m", + }, + { + Name: "request-duration", + Threshold: 500000, + Interval: "1m", + }, + }, + }, + }, + } + return cd +} + +func newTestServiceCanaryWithWeightsHappyCase() *flaggerv1.Canary { + cd := &flaggerv1.Canary{ + TypeMeta: metav1.TypeMeta{APIVersion: flaggerv1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo", + }, + Spec: flaggerv1.CanarySpec{ + TargetRef: flaggerv1.CrossNamespaceObjectReference{ + Name: "podinfo", + APIVersion: "core/v1", + Kind: "Service", + }, + Service: flaggerv1.CanaryService{ + Port: 9898, + }, + Analysis: &flaggerv1.CanaryAnalysis{ + Threshold: 10, + StepWeights: []int{1, 2, 10, 80}, + Metrics: []flaggerv1.CanaryMetric{ + { + Name: "request-success-rate", + Threshold: 99, + Interval: "1m", + }, + { + Name: "request-duration", + Threshold: 500000, + Interval: "1m", + }, + }, + }, + }, + } + return cd +} + +func newTestServiceCanaryWithWeightsOverflow() *flaggerv1.Canary { + cd := &flaggerv1.Canary{ + TypeMeta: metav1.TypeMeta{APIVersion: flaggerv1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo", + }, + Spec: flaggerv1.CanarySpec{ + TargetRef: flaggerv1.CrossNamespaceObjectReference{ + Name: "podinfo", + APIVersion: "core/v1", + Kind: "Service", + }, + Service: flaggerv1.CanaryService{ + Port: 9898, + }, + Analysis: &flaggerv1.CanaryAnalysis{ + Threshold: 10, + StepWeights: []int{1, 2, 10, totalWeight + 100}, + Metrics: []flaggerv1.CanaryMetric{ + { + Name: "request-success-rate", + Threshold: 99, + Interval: "1m", + }, + { + Name: "request-duration", + Threshold: 500000, + Interval: "1m", + }, + }, + }, + }, + } + return cd +}