Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
Expand Down
28 changes: 28 additions & 0 deletions pkg/apis/flagger/v1beta1/canary.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ const (
MetricInterval = "1m"
)

// Deployment strategies
const (
DeploymentStrategyCanary = "canary"
DeploymentStrategyBlueGreen = "blue-green"
DeploymentStrategyABTesting = "ab-testing"
)

// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

Expand Down Expand Up @@ -640,3 +647,24 @@ func (c *Canary) SkipAnalysis() bool {
}
return c.Spec.SkipAnalysis
}

// DeploymentStrategy returns the deployment strategy based on canary analysis configuration
func (c *Canary) DeploymentStrategy() string {
analysis := c.GetAnalysis()
if analysis == nil {
return DeploymentStrategyCanary
}

// A/B Testing: has match conditions and iterations
if len(analysis.Match) > 0 && analysis.Iterations > 0 {
return DeploymentStrategyABTesting
}

// Blue/Green: has iterations but no match conditions
if analysis.Iterations > 0 {
return DeploymentStrategyBlueGreen
}

// Canary Release: default (has maxWeight, stepWeight, or stepWeights)
return DeploymentStrategyCanary
}
94 changes: 94 additions & 0 deletions pkg/apis/flagger/v1beta1/canary_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
Copyright 2025 The Flux authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1beta1

import (
"testing"

istiov1alpha1 "github.com/fluxcd/flagger/pkg/apis/istio/common/v1alpha1"
istiov1beta1 "github.com/fluxcd/flagger/pkg/apis/istio/v1beta1"
"github.com/stretchr/testify/assert"
)

func TestCanary_GetDeploymentStrategy(t *testing.T) {
tests := []struct {
name string
analysis *CanaryAnalysis
expected string
}{
{
name: "canary strategy with maxWeight",
analysis: &CanaryAnalysis{
MaxWeight: 30,
StepWeight: 10,
},
expected: DeploymentStrategyCanary,
},
{
name: "canary strategy with stepWeights",
analysis: &CanaryAnalysis{
StepWeights: []int{10, 20, 30},
},
expected: DeploymentStrategyCanary,
},
{
name: "blue-green strategy with iterations",
analysis: &CanaryAnalysis{
Iterations: 5,
},
expected: DeploymentStrategyBlueGreen,
},
{
name: "ab-testing strategy with iterations and match",
analysis: &CanaryAnalysis{
Iterations: 10,
Match: []istiov1beta1.HTTPMatchRequest{
{
Headers: map[string]istiov1alpha1.StringMatch{
"x-canary": {
Exact: "insider",
},
},
},
},
},
expected: DeploymentStrategyABTesting,
},
{
name: "default to canary when analysis is nil",
analysis: nil,
expected: DeploymentStrategyCanary,
},
{
name: "default to canary when analysis is empty",
analysis: &CanaryAnalysis{},
expected: DeploymentStrategyCanary,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
canary := &Canary{
Spec: CanarySpec{
Analysis: tt.analysis,
},
}
result := canary.DeploymentStrategy()
assert.Equal(t, tt.expected, result)
})
}
}
37 changes: 21 additions & 16 deletions pkg/controller/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,30 +209,35 @@ func alertMetadata(canary *flaggerv1.Canary) []notifier.Field {
},
)

if canary.GetAnalysis().StepWeight > 0 {
fields = append(fields, notifier.Field{
Name: "Traffic routing",
Value: fmt.Sprintf("Weight step: %v max: %v",
canary.GetAnalysis().StepWeight,
canary.GetAnalysis().MaxWeight),
})
} else if len(canary.GetAnalysis().StepWeights) > 0 {
fields = append(fields, notifier.Field{
Name: "Traffic routing",
Value: fmt.Sprintf("Weight steps: %s max: %v",
strings.Trim(strings.Join(strings.Fields(fmt.Sprint(canary.GetAnalysis().StepWeights)), ","), "[]"),
canary.GetAnalysis().MaxWeight),
})
} else if len(canary.GetAnalysis().Match) > 0 {
strategy := canary.DeploymentStrategy()
switch strategy {
case flaggerv1.DeploymentStrategyABTesting:
fields = append(fields, notifier.Field{
Name: "Traffic routing",
Value: "A/B Testing",
})
} else if canary.GetAnalysis().Iterations > 0 {
case flaggerv1.DeploymentStrategyBlueGreen:
fields = append(fields, notifier.Field{
Name: "Traffic routing",
Value: "Blue/Green",
})
default:
// Canary strategy
if canary.GetAnalysis().StepWeight > 0 {
fields = append(fields, notifier.Field{
Name: "Traffic routing",
Value: fmt.Sprintf("Weight step: %v max: %v",
canary.GetAnalysis().StepWeight,
canary.GetAnalysis().MaxWeight),
})
} else if len(canary.GetAnalysis().StepWeights) > 0 {
fields = append(fields, notifier.Field{
Name: "Traffic routing",
Value: fmt.Sprintf("Weight steps: %s max: %v",
strings.Trim(strings.Join(strings.Fields(fmt.Sprint(canary.GetAnalysis().StepWeights)), ","), "[]"),
canary.GetAnalysis().MaxWeight),
})
}
}
return fields
}
29 changes: 23 additions & 6 deletions pkg/controller/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (

flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
"github.com/fluxcd/flagger/pkg/canary"
"github.com/fluxcd/flagger/pkg/metrics"
"github.com/fluxcd/flagger/pkg/router"
)

Expand Down Expand Up @@ -400,6 +401,12 @@ func (c *Controller) advanceCanary(name string, namespace string) {
return
}
c.recorder.SetStatus(cd, flaggerv1.CanaryPhaseSucceeded)
c.recorder.IncSuccesses(metrics.CanaryMetricLabels{
Name: cd.Spec.TargetRef.Name,
Namespace: cd.Namespace,
DeploymentStrategy: cd.DeploymentStrategy(),
AnalysisStatus: metrics.AnalysisStatusCompleted,
})
c.runPostRolloutHooks(cd, flaggerv1.CanaryPhaseSucceeded)
c.recordEventInfof(cd, "Promotion completed! Scaling down %s.%s", cd.Spec.TargetRef.Name, cd.Namespace)
c.alert(cd, "Canary analysis completed successfully, promotion finished.",
Expand Down Expand Up @@ -460,14 +467,12 @@ func (c *Controller) advanceCanary(name string, namespace string) {
}
}

// strategy: A/B testing
if len(cd.GetAnalysis().Match) > 0 && cd.GetAnalysis().Iterations > 0 {
strategy := cd.DeploymentStrategy()
switch strategy {
case flaggerv1.DeploymentStrategyABTesting:
c.runAB(cd, canaryController, meshRouter)
return
}

// strategy: Blue/Green
if cd.GetAnalysis().Iterations > 0 {
case flaggerv1.DeploymentStrategyBlueGreen:
c.runBlueGreen(cd, canaryController, meshRouter, provider, mirrored)
return
}
Expand Down Expand Up @@ -814,6 +819,12 @@ func (c *Controller) shouldSkipAnalysis(canary *flaggerv1.Canary, canaryControll

// notify
c.recorder.SetStatus(canary, flaggerv1.CanaryPhaseSucceeded)
c.recorder.IncSuccesses(metrics.CanaryMetricLabels{
Name: canary.Spec.TargetRef.Name,
Namespace: canary.Namespace,
DeploymentStrategy: canary.DeploymentStrategy(),
AnalysisStatus: metrics.AnalysisStatusSkipped,
})
c.recordEventInfof(canary, "Promotion completed! Canary analysis was skipped for %s.%s",
canary.Spec.TargetRef.Name, canary.Namespace)
c.alert(canary, "Canary analysis was skipped, promotion finished.",
Expand Down Expand Up @@ -961,6 +972,12 @@ func (c *Controller) rollback(canary *flaggerv1.Canary, canaryController canary.
}

c.recorder.SetStatus(canary, flaggerv1.CanaryPhaseFailed)
c.recorder.IncFailures(metrics.CanaryMetricLabels{
Name: canary.Spec.TargetRef.Name,
Namespace: canary.Namespace,
DeploymentStrategy: canary.DeploymentStrategy(),
AnalysisStatus: metrics.AnalysisStatusCompleted,
})
c.runPostRolloutHooks(canary, flaggerv1.CanaryPhaseFailed)
}

Expand Down
Loading