mirror of https://github.com/fluxcd/flagger.git
Merge pull request #492 from weaveworks/mirror-percentage
istio router: make mirrorPercentage configurable for traffic mirroring
This commit is contained in:
commit
9a5328b507
|
|
@ -528,6 +528,9 @@ spec:
|
|||
mirror:
|
||||
description: Mirror traffic to canary
|
||||
type: boolean
|
||||
mirrorWeight:
|
||||
description: Percentage of traffic to be mirrored
|
||||
type: number
|
||||
match:
|
||||
description: A/B testing match conditions
|
||||
type: array
|
||||
|
|
|
|||
|
|
@ -528,6 +528,9 @@ spec:
|
|||
mirror:
|
||||
description: Mirror traffic to canary
|
||||
type: boolean
|
||||
mirrorWeight:
|
||||
description: Percentage of traffic to be mirrored
|
||||
type: number
|
||||
match:
|
||||
description: A/B testing match conditions
|
||||
type: array
|
||||
|
|
|
|||
|
|
@ -324,6 +324,8 @@ spec:
|
|||
iterations: 10
|
||||
# enable traffic shadowing
|
||||
mirror: true
|
||||
# weight of the traffic mirrored to your canary (defaults to 100%)
|
||||
mirrorWeight: 100
|
||||
metrics:
|
||||
- name: request-success-rate
|
||||
thresholdRange:
|
||||
|
|
@ -357,7 +359,7 @@ With the above configuration, Flagger will run a canary release with the followi
|
|||
* run the acceptance tests
|
||||
* abort the canary release if tests fail
|
||||
* start the load tests
|
||||
* mirror traffic from primary to canary
|
||||
* mirror 100% of the traffic from primary to canary
|
||||
* check request success rate and request duration every minute
|
||||
* abort the canary release if the metrics check failure threshold is reached
|
||||
* stop traffic mirroring after the number of iterations is reached
|
||||
|
|
|
|||
|
|
@ -277,6 +277,8 @@ Istio example:
|
|||
threshold: 2
|
||||
# Traffic shadowing (compatible with Istio only)
|
||||
mirror: true
|
||||
# Weight of the traffic mirrored to your canary (defaults to 100%)
|
||||
mirrorWeight: 100
|
||||
```
|
||||
|
||||
Mirroring rollout steps for service mesh:
|
||||
|
|
@ -287,7 +289,7 @@ Mirroring rollout steps for service mesh:
|
|||
* run the acceptance tests
|
||||
* abort the canary release if tests fail
|
||||
* start the load tests
|
||||
* mirror traffic from primary to canary
|
||||
* mirror 100% of the traffic from primary to canary
|
||||
* check request success rate and request duration every minute
|
||||
* abort the canary release if the failure threshold is reached
|
||||
* stop traffic mirroring after the number of iterations is reached
|
||||
|
|
|
|||
|
|
@ -528,6 +528,9 @@ spec:
|
|||
mirror:
|
||||
description: Mirror traffic to canary
|
||||
type: boolean
|
||||
mirrorWeight:
|
||||
description: Percentage of traffic to be mirrored
|
||||
type: number
|
||||
match:
|
||||
description: A/B testing match conditions
|
||||
type: array
|
||||
|
|
|
|||
|
|
@ -175,10 +175,14 @@ type CanaryAnalysis struct {
|
|||
// +optional
|
||||
Iterations int `json:"iterations,omitempty"`
|
||||
|
||||
//Enable traffic mirroring for Blue/Green
|
||||
// Enable traffic mirroring for Blue/Green
|
||||
// +optional
|
||||
Mirror bool `json:"mirror,omitempty"`
|
||||
|
||||
// Percentage of the traffic to be mirrored in the range of [0, 100].
|
||||
// +optional
|
||||
MirrorWeight int `json:"mirrorWeight,omitempty"`
|
||||
|
||||
// Max traffic percentage routed to canary
|
||||
// +optional
|
||||
MaxWeight int `json:"maxWeight,omitempty"`
|
||||
|
|
|
|||
|
|
@ -325,6 +325,11 @@ type HTTPRoute struct {
|
|||
// destination.
|
||||
Mirror *Destination `json:"mirror,omitempty"`
|
||||
|
||||
// Percentage of the traffic to be mirrored by the `mirror` field.
|
||||
// If this field is absent, all the traffic (100%) will be mirrored.
|
||||
// Max value is 100.
|
||||
MirrorPercentage *Percent `json:"mirrorPercentage,omitempty"`
|
||||
|
||||
// Cross-Origin Resource Sharing policy (CORS). Refer to
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
||||
// for further details about cross origin resource sharing.
|
||||
|
|
@ -334,6 +339,11 @@ type HTTPRoute struct {
|
|||
Headers *Headers `json:"headers,omitempty"`
|
||||
}
|
||||
|
||||
// Percent specifies a percentage in the range of [0.0, 100.0].
|
||||
type Percent struct {
|
||||
Value float64 `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// Header manipulation rules
|
||||
type Headers struct {
|
||||
// Header manipulation rules to apply before forwarding a request
|
||||
|
|
|
|||
|
|
@ -421,6 +421,11 @@ func (in *HTTPRoute) DeepCopyInto(out *HTTPRoute) {
|
|||
*out = new(Destination)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.MirrorPercentage != nil {
|
||||
in, out := &in.MirrorPercentage, &out.MirrorPercentage
|
||||
*out = new(Percent)
|
||||
**out = **in
|
||||
}
|
||||
if in.CorsPolicy != nil {
|
||||
in, out := &in.CorsPolicy, &out.CorsPolicy
|
||||
*out = new(CorsPolicy)
|
||||
|
|
@ -628,6 +633,22 @@ func (in *OutlierDetection) DeepCopy() *OutlierDetection {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Percent) DeepCopyInto(out *Percent) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Percent.
|
||||
func (in *Percent) DeepCopy() *Percent {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Percent)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PortSelector) DeepCopyInto(out *PortSelector) {
|
||||
*out = *in
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ func newDeploymentControllerTestCanary() *flaggerv1.Canary {
|
|||
Kind: "HorizontalPodAutoscaler",
|
||||
}, Service: flaggerv1.CanaryService{
|
||||
Port: 9898,
|
||||
}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
|
||||
}, Analysis: &flaggerv1.CanaryAnalysis{
|
||||
Threshold: 10,
|
||||
StepWeight: 10,
|
||||
MaxWeight: 50,
|
||||
|
|
|
|||
|
|
@ -249,7 +249,7 @@ func newDaemonSetTestCanary() *flaggerv1.Canary {
|
|||
Kind: "DaemonSet",
|
||||
}, Service: flaggerv1.CanaryService{
|
||||
Port: 9898,
|
||||
}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
|
||||
}, Analysis: &flaggerv1.CanaryAnalysis{
|
||||
Threshold: 10,
|
||||
StepWeight: 10,
|
||||
MaxWeight: 50,
|
||||
|
|
@ -288,7 +288,7 @@ func newDaemonSetTestCanary() *flaggerv1.Canary {
|
|||
|
||||
func newDaemonSetTestCanaryMirror() *flaggerv1.Canary {
|
||||
cd := newDaemonSetTestCanary()
|
||||
cd.Spec.CanaryAnalysis.Mirror = true
|
||||
cd.Spec.Analysis.Mirror = true
|
||||
return cd
|
||||
}
|
||||
|
||||
|
|
@ -306,7 +306,7 @@ func newDaemonSetTestCanaryAB() *flaggerv1.Canary {
|
|||
Kind: "DaemonSet",
|
||||
}, Service: flaggerv1.CanaryService{
|
||||
Port: 9898,
|
||||
}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
|
||||
}, Analysis: &flaggerv1.CanaryAnalysis{
|
||||
Threshold: 10,
|
||||
Iterations: 10,
|
||||
Match: []istiov1alpha3.HTTPMatchRequest{
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ func TestScheduler_DaemonSetRollback(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
cd := c.DeepCopy()
|
||||
cd.Spec.CanaryAnalysis.Metrics = append(c.Spec.CanaryAnalysis.Metrics, flaggerv1.CanaryMetric{
|
||||
cd.Spec.Analysis.Metrics = append(c.Spec.Analysis.Metrics, flaggerv1.CanaryMetric{
|
||||
Name: "fail",
|
||||
Interval: "1m",
|
||||
ThresholdRange: &flaggerv1.CanaryThresholdRange{
|
||||
|
|
@ -437,7 +437,7 @@ func TestScheduler_DaemonSetAlerts(t *testing.T) {
|
|||
defer ts.Close()
|
||||
|
||||
canary := newDaemonSetTestCanary()
|
||||
canary.Spec.CanaryAnalysis.Alerts = []flaggerv1.CanaryAlert{
|
||||
canary.Spec.Analysis.Alerts = []flaggerv1.CanaryAlert{
|
||||
{
|
||||
Name: "slack-dev",
|
||||
Severity: "info",
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ func newDeploymentTestCanary() *flaggerv1.Canary {
|
|||
Kind: "HorizontalPodAutoscaler",
|
||||
}, Service: flaggerv1.CanaryService{
|
||||
Port: 9898,
|
||||
}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
|
||||
}, Analysis: &flaggerv1.CanaryAnalysis{
|
||||
Threshold: 10,
|
||||
StepWeight: 10,
|
||||
MaxWeight: 50,
|
||||
|
|
@ -295,7 +295,7 @@ func newDeploymentTestCanary() *flaggerv1.Canary {
|
|||
|
||||
func newDeploymentTestCanaryMirror() *flaggerv1.Canary {
|
||||
cd := newDeploymentTestCanary()
|
||||
cd.Spec.CanaryAnalysis.Mirror = true
|
||||
cd.Spec.Analysis.Mirror = true
|
||||
return cd
|
||||
}
|
||||
|
||||
|
|
@ -318,7 +318,7 @@ func newDeploymentTestCanaryAB() *flaggerv1.Canary {
|
|||
Kind: "HorizontalPodAutoscaler",
|
||||
}, Service: flaggerv1.CanaryService{
|
||||
Port: 9898,
|
||||
}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
|
||||
}, Analysis: &flaggerv1.CanaryAnalysis{
|
||||
Threshold: 10,
|
||||
Iterations: 10,
|
||||
Match: []istiov1alpha3.HTTPMatchRequest{
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ func TestScheduler_DeploymentRollback(t *testing.T) {
|
|||
c, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get("podinfo", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
cd := c.DeepCopy()
|
||||
cd.Spec.CanaryAnalysis.Metrics = append(c.Spec.CanaryAnalysis.Metrics, flaggerv1.CanaryMetric{
|
||||
cd.Spec.Analysis.Metrics = append(c.Spec.Analysis.Metrics, flaggerv1.CanaryMetric{
|
||||
Name: "fail",
|
||||
Interval: "1m",
|
||||
ThresholdRange: &flaggerv1.CanaryThresholdRange{
|
||||
|
|
@ -519,7 +519,7 @@ func TestScheduler_DeploymentAlerts(t *testing.T) {
|
|||
defer ts.Close()
|
||||
|
||||
canary := newDeploymentTestCanary()
|
||||
canary.Spec.CanaryAnalysis.Alerts = []flaggerv1.CanaryAlert{
|
||||
canary.Spec.Analysis.Alerts = []flaggerv1.CanaryAlert{
|
||||
{
|
||||
Name: "slack-dev",
|
||||
Severity: "info",
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ func newTestServiceCanary() *flaggerv1.Canary {
|
|||
Service: flaggerv1.CanaryService{
|
||||
Port: 9898,
|
||||
},
|
||||
CanaryAnalysis: &flaggerv1.CanaryAnalysis{
|
||||
Analysis: &flaggerv1.CanaryAnalysis{
|
||||
Threshold: 10,
|
||||
StepWeight: 10,
|
||||
MaxWeight: 50,
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ func TestContourRouter_Reconcile(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
cdClone = cd.DeepCopy()
|
||||
cdClone.Spec.CanaryAnalysis.Iterations = 5
|
||||
cdClone.Spec.CanaryAnalysis.Match = newTestABTest().Spec.CanaryAnalysis.Match
|
||||
cdClone.Spec.Analysis.Iterations = 5
|
||||
cdClone.Spec.Analysis.Match = newTestABTest().Spec.Analysis.Match
|
||||
canary, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Update(cdClone)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -104,8 +104,8 @@ func TestContourRouter_Routes(t *testing.T) {
|
|||
|
||||
// test update to A/B
|
||||
cdClone := cd.DeepCopy()
|
||||
cdClone.Spec.CanaryAnalysis.Iterations = 5
|
||||
cdClone.Spec.CanaryAnalysis.Match = newTestABTest().Spec.CanaryAnalysis.Match
|
||||
cdClone.Spec.Analysis.Iterations = 5
|
||||
cdClone.Spec.Analysis.Match = newTestABTest().Spec.Analysis.Match
|
||||
canary, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Update(cdClone)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ func (ir *IstioRouter) reconcileVirtualService(canary *flaggerv1.Canary) error {
|
|||
newSpec,
|
||||
virtualService.Spec,
|
||||
cmpopts.IgnoreFields(istiov1alpha3.DestinationWeight{}, "Weight"),
|
||||
cmpopts.IgnoreFields(istiov1alpha3.HTTPRoute{}, "Mirror"),
|
||||
cmpopts.IgnoreFields(istiov1alpha3.HTTPRoute{}, "Mirror", "MirrorPercentage"),
|
||||
); diff != "" {
|
||||
vtClone := virtualService.DeepCopy()
|
||||
vtClone.Spec = newSpec
|
||||
|
|
@ -304,6 +304,10 @@ func (ir *IstioRouter) SetRoutes(
|
|||
vsCopy.Spec.Http[0].Mirror = &istiov1alpha3.Destination{
|
||||
Host: canaryName,
|
||||
}
|
||||
|
||||
if mw := canary.GetAnalysis().MirrorWeight; mw > 0 {
|
||||
vsCopy.Spec.Http[0].MirrorPercentage = &istiov1alpha3.Percent{Value: float64(mw)}
|
||||
}
|
||||
}
|
||||
|
||||
// fix routing (A/B testing)
|
||||
|
|
|
|||
|
|
@ -91,69 +91,78 @@ func TestIstioRouter_SetRoutes(t *testing.T) {
|
|||
err := router.Reconcile(mocks.canary)
|
||||
require.NoError(t, err)
|
||||
|
||||
p, c, m, err := router.GetRoutes(mocks.canary)
|
||||
require.NoError(t, err)
|
||||
|
||||
p = 60
|
||||
c = 40
|
||||
m = false
|
||||
|
||||
err = router.SetRoutes(mocks.canary, p, c, m)
|
||||
require.NoError(t, err)
|
||||
|
||||
vs, err := mocks.meshClient.NetworkingV1alpha3().VirtualServices("default").Get("podinfo", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
pHost := fmt.Sprintf("%s-primary", mocks.canary.Spec.TargetRef.Name)
|
||||
cHost := fmt.Sprintf("%s-canary", mocks.canary.Spec.TargetRef.Name)
|
||||
pRoute := istiov1alpha3.DestinationWeight{}
|
||||
cRoute := istiov1alpha3.DestinationWeight{}
|
||||
var mirror *istiov1alpha3.Destination
|
||||
|
||||
for _, http := range vs.Spec.Http {
|
||||
for _, route := range http.Route {
|
||||
if route.Destination.Host == pHost {
|
||||
pRoute = route
|
||||
}
|
||||
if route.Destination.Host == cHost {
|
||||
cRoute = route
|
||||
mirror = http.Mirror
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
p, c := 60, 40
|
||||
err := router.SetRoutes(mocks.canary, p, c, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
vs, err := mocks.meshClient.NetworkingV1alpha3().VirtualServices("default").Get("podinfo", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
var pRoute, cRoute istiov1alpha3.DestinationWeight
|
||||
var mirror *istiov1alpha3.Destination
|
||||
for _, http := range vs.Spec.Http {
|
||||
for _, route := range http.Route {
|
||||
if route.Destination.Host == pHost {
|
||||
pRoute = route
|
||||
}
|
||||
if route.Destination.Host == cHost {
|
||||
cRoute = route
|
||||
mirror = http.Mirror
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, p, pRoute.Weight)
|
||||
assert.Equal(t, c, cRoute.Weight)
|
||||
assert.Nil(t, mirror)
|
||||
assert.Equal(t, p, pRoute.Weight)
|
||||
assert.Equal(t, c, cRoute.Weight)
|
||||
assert.Nil(t, mirror)
|
||||
|
||||
mirror = nil
|
||||
p = 100
|
||||
c = 0
|
||||
m = true
|
||||
})
|
||||
|
||||
err = router.SetRoutes(mocks.canary, p, c, m)
|
||||
require.NoError(t, err)
|
||||
t.Run("mirror", func(t *testing.T) {
|
||||
for _, w := range []int{0, 10, 50} {
|
||||
p, c := 100, 0
|
||||
|
||||
vs, err = mocks.meshClient.NetworkingV1alpha3().VirtualServices("default").Get("podinfo", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
// set mirror weight
|
||||
mocks.canary.Spec.Analysis.MirrorWeight = w
|
||||
err := router.SetRoutes(mocks.canary, p, c, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, http := range vs.Spec.Http {
|
||||
for _, route := range http.Route {
|
||||
if route.Destination.Host == pHost {
|
||||
pRoute = route
|
||||
vs, err := mocks.meshClient.NetworkingV1alpha3().VirtualServices("default").Get("podinfo", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
var pRoute, cRoute istiov1alpha3.DestinationWeight
|
||||
var mirror *istiov1alpha3.Destination
|
||||
var mirrorWeight *istiov1alpha3.Percent
|
||||
for _, http := range vs.Spec.Http {
|
||||
for _, route := range http.Route {
|
||||
if route.Destination.Host == pHost {
|
||||
pRoute = route
|
||||
}
|
||||
if route.Destination.Host == cHost {
|
||||
cRoute = route
|
||||
mirror = http.Mirror
|
||||
mirrorWeight = http.MirrorPercentage
|
||||
}
|
||||
}
|
||||
}
|
||||
if route.Destination.Host == cHost {
|
||||
cRoute = route
|
||||
mirror = http.Mirror
|
||||
|
||||
assert.Equal(t, p, pRoute.Weight)
|
||||
assert.Equal(t, c, cRoute.Weight)
|
||||
if assert.NotNil(t, mirror) {
|
||||
assert.Equal(t, cHost, mirror.Host)
|
||||
}
|
||||
|
||||
if w > 0 && assert.NotNil(t, mirrorWeight) {
|
||||
assert.Equal(t, w, int(mirrorWeight.Value))
|
||||
} else {
|
||||
assert.Nil(t, mirrorWeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, p, pRoute.Weight)
|
||||
assert.Equal(t, c, cRoute.Weight)
|
||||
if assert.NotNil(t, mirror) {
|
||||
assert.Equal(t, cHost, mirror.Host)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestIstioRouter_GetRoutes(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ func newTestCanary() *flaggerv1.Canary {
|
|||
"public-gateway.istio",
|
||||
"mesh",
|
||||
},
|
||||
}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
|
||||
}, Analysis: &flaggerv1.CanaryAnalysis{
|
||||
Threshold: 10,
|
||||
StepWeight: 10,
|
||||
MaxWeight: 50,
|
||||
|
|
@ -158,7 +158,7 @@ func newTestCanaryAppMesh() *flaggerv1.Canary {
|
|||
PerTryTimeout: "gateway-error",
|
||||
RetryOn: "5s",
|
||||
},
|
||||
}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
|
||||
}, Analysis: &flaggerv1.CanaryAnalysis{
|
||||
Threshold: 10,
|
||||
StepWeight: 10,
|
||||
MaxWeight: 50,
|
||||
|
|
@ -203,7 +203,7 @@ func newTestSMICanary() *flaggerv1.Canary {
|
|||
},
|
||||
PortDiscovery: true,
|
||||
},
|
||||
CanaryAnalysis: &flaggerv1.CanaryAnalysis{
|
||||
Analysis: &flaggerv1.CanaryAnalysis{
|
||||
Threshold: 10,
|
||||
StepWeight: 10,
|
||||
MaxWeight: 50,
|
||||
|
|
@ -247,7 +247,7 @@ func newTestABTest() *flaggerv1.Canary {
|
|||
Service: flaggerv1.CanaryService{
|
||||
Port: 9898,
|
||||
MeshName: "global",
|
||||
}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
|
||||
}, Analysis: &flaggerv1.CanaryAnalysis{
|
||||
Threshold: 10,
|
||||
Iterations: 2,
|
||||
Match: []istiov1alpha3.HTTPMatchRequest{
|
||||
|
|
@ -397,7 +397,7 @@ func newTestCanaryIngress() *flaggerv1.Canary {
|
|||
},
|
||||
Service: flaggerv1.CanaryService{
|
||||
Port: 9898,
|
||||
}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
|
||||
}, Analysis: &flaggerv1.CanaryAnalysis{
|
||||
Threshold: 10,
|
||||
StepWeight: 10,
|
||||
MaxWeight: 50,
|
||||
|
|
|
|||
Loading…
Reference in New Issue