Repository: weaveworks/flagger
Pull request #463 (2020-02-28 09:52:50)

Rename spec.canaryAnalysis to spec.analysis
Changes:
- add `analysis` field to Canary API
- deprecate `canaryAnalysis` filed (to be removed in the next API version)
- maintain backwards compatibility with v1alpha3 by using `spec.canaryAnalysis` if `spec.analysis` is nil
- set analysis threshold default value to 1
- crd: rename `spec.canaryAnalysis` to `spec.analysis`
- crd: required fields `spec.analysis.interval` and `spec.analysis.threshold`
- e2e: update tests to v1beta1 and rename `spec.canaryAnalysis` to `spec.analysis`

Upgrade from v1alpha3 to v1beta1:
- apply v1beta1 CRDs
- upgrade Flagger 
- replace `apiVersion: flagger.app/v1alpha3` with `apiVersion: flagger.app/v1beta1`
- replace `spec.canaryAnalysis` with `spec.analysis`
- apply all canary definitions on the cluster

Followup #457
- update docs and tutorials to v1beta1
- add upgrade guide to docs


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 8e0b7de1..1f7ca709 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -14,8 +14,8 @@ jobs:
           name: Run go mod download
           command: go mod download
       - run:
-          name: Run go fmt
-          command: make test-fmt
+          name: Check code formatting
+          command: go install golang.org/x/tools/cmd/goimports && make test-fmt
       - run:
           name: Build Flagger
           command: |
diff --git a/Makefile b/Makefile
index dd9c42fa..4bbef9de 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,6 @@ TAG?=latest
 VERSION?=$(shell grep 'VERSION' pkg/version/version.go | awk '{ print $$4 }' | tr -d '"')
 VERSION_MINOR:=$(shell grep 'VERSION' pkg/version/version.go | awk '{ print $$4 }' | tr -d '"' | rev | cut -d'.' -f2- | rev)
 PATCH:=$(shell grep 'VERSION' pkg/version/version.go | awk '{ print $$4 }' | tr -d '"' | awk -F. '{print $$NF}')
-SOURCE_DIRS = cmd pkg/apis pkg/controller pkg/server pkg/canary pkg/metrics pkg/router pkg/notifier
 LT_VERSION?=$(shell grep 'VERSION' cmd/loadtester/main.go | awk '{ print $$4 }' | tr -d '"' | head -n1)
 TS=$(shell date +%Y-%m-%d_%H-%M-%S)
 
@@ -43,10 +42,12 @@ push:
 	docker push weaveworks/flagger:$(VERSION)
 
 fmt:
-	gofmt -l -s -w $(SOURCE_DIRS)
+	gofmt -l -s -w ./
+	goimports -l -w ./
 
 test-fmt:
-	gofmt -l -s $(SOURCE_DIRS) | grep ".*\.go"; if [ "$$?" = "0" ]; then exit 1; fi
+	gofmt -l -s ./ | grep ".*\.go"; if [ "$$?" = "0" ]; then exit 1; fi
+	goimports -l ./ | grep ".*\.go"; if [ "$$?" = "0" ]; then exit 1; fi
 
 test-codegen:
 	./hack/verify-codegen.sh
diff --git a/artifacts/flagger/crd.yaml b/artifacts/flagger/crd.yaml
index 17fcc3f5..957b3f79 100644
--- a/artifacts/flagger/crd.yaml
+++ b/artifacts/flagger/crd.yaml
@@ -42,19 +42,19 @@ spec:
       priority: 1
     - name: Interval
       type: string
-      JSONPath: .spec.canaryAnalysis.interval
+      JSONPath: .spec.analysis.interval
       priority: 1
     - name: Mirror
       type: boolean
-      JSONPath: .spec.canaryAnalysis.mirror
+      JSONPath: .spec.analysis.mirror
       priority: 1
     - name: StepWeight
       type: string
-      JSONPath: .spec.canaryAnalysis.stepWeight
+      JSONPath: .spec.analysis.stepWeight
       priority: 1
     - name: MaxWeight
       type: string
-      JSONPath: .spec.canaryAnalysis.maxWeight
+      JSONPath: .spec.analysis.maxWeight
       priority: 1
     - name: LastTransitionTime
       type: string
@@ -66,7 +66,7 @@ spec:
           required:
             - targetRef
             - service
-            - canaryAnalysis
+            - analysis
           properties:
             provider:
               description: Traffic managent provider
@@ -502,9 +502,12 @@ spec:
             skipAnalysis:
               description: Skip analysis and promote canary
               type: boolean
-            canaryAnalysis:
+            analysis:
               description: Canary analysis for this canary
               type: object
+              oneOf:
+                - required: ["interval", "threshold", "iterations"]
+                - required: ["interval", "threshold", "stepWeight"]
               properties:
                 interval:
                   description: Schedule interval for this canary
@@ -523,7 +526,7 @@ spec:
                   description: Incremental traffic percentage step
                   type: number
                 mirror:
-                  description: Mirror traffic to canary before shifting
+                  description: Mirror traffic to canary
                   type: boolean
                 match:
                   description: A/B testing match conditions
diff --git a/charts/flagger/crds/crd.yaml b/charts/flagger/crds/crd.yaml
index 17fcc3f5..957b3f79 100644
--- a/charts/flagger/crds/crd.yaml
+++ b/charts/flagger/crds/crd.yaml
@@ -42,19 +42,19 @@ spec:
       priority: 1
     - name: Interval
       type: string
-      JSONPath: .spec.canaryAnalysis.interval
+      JSONPath: .spec.analysis.interval
       priority: 1
     - name: Mirror
       type: boolean
-      JSONPath: .spec.canaryAnalysis.mirror
+      JSONPath: .spec.analysis.mirror
       priority: 1
     - name: StepWeight
       type: string
-      JSONPath: .spec.canaryAnalysis.stepWeight
+      JSONPath: .spec.analysis.stepWeight
       priority: 1
     - name: MaxWeight
       type: string
-      JSONPath: .spec.canaryAnalysis.maxWeight
+      JSONPath: .spec.analysis.maxWeight
       priority: 1
     - name: LastTransitionTime
       type: string
@@ -66,7 +66,7 @@ spec:
           required:
             - targetRef
             - service
-            - canaryAnalysis
+            - analysis
           properties:
             provider:
               description: Traffic managent provider
@@ -502,9 +502,12 @@ spec:
             skipAnalysis:
               description: Skip analysis and promote canary
               type: boolean
-            canaryAnalysis:
+            analysis:
               description: Canary analysis for this canary
               type: object
+              oneOf:
+                - required: ["interval", "threshold", "iterations"]
+                - required: ["interval", "threshold", "stepWeight"]
               properties:
                 interval:
                   description: Schedule interval for this canary
@@ -523,7 +526,7 @@ spec:
                   description: Incremental traffic percentage step
                   type: number
                 mirror:
-                  description: Mirror traffic to canary before shifting
+                  description: Mirror traffic to canary
                   type: boolean
                 match:
                   description: A/B testing match conditions
diff --git a/kustomize/base/flagger/crd.yaml b/kustomize/base/flagger/crd.yaml
index 17fcc3f5..957b3f79 100644
--- a/kustomize/base/flagger/crd.yaml
+++ b/kustomize/base/flagger/crd.yaml
@@ -42,19 +42,19 @@ spec:
       priority: 1
     - name: Interval
       type: string
-      JSONPath: .spec.canaryAnalysis.interval
+      JSONPath: .spec.analysis.interval
       priority: 1
     - name: Mirror
       type: boolean
-      JSONPath: .spec.canaryAnalysis.mirror
+      JSONPath: .spec.analysis.mirror
       priority: 1
     - name: StepWeight
       type: string
-      JSONPath: .spec.canaryAnalysis.stepWeight
+      JSONPath: .spec.analysis.stepWeight
       priority: 1
     - name: MaxWeight
       type: string
-      JSONPath: .spec.canaryAnalysis.maxWeight
+      JSONPath: .spec.analysis.maxWeight
       priority: 1
     - name: LastTransitionTime
       type: string
@@ -66,7 +66,7 @@ spec:
           required:
             - targetRef
             - service
-            - canaryAnalysis
+            - analysis
           properties:
             provider:
               description: Traffic managent provider
@@ -502,9 +502,12 @@ spec:
             skipAnalysis:
               description: Skip analysis and promote canary
               type: boolean
-            canaryAnalysis:
+            analysis:
               description: Canary analysis for this canary
               type: object
+              oneOf:
+                - required: ["interval", "threshold", "iterations"]
+                - required: ["interval", "threshold", "stepWeight"]
               properties:
                 interval:
                   description: Schedule interval for this canary
@@ -523,7 +526,7 @@ spec:
                   description: Incremental traffic percentage step
                   type: number
                 mirror:
-                  description: Mirror traffic to canary before shifting
+                  description: Mirror traffic to canary
                   type: boolean
                 match:
                   description: A/B testing match conditions
diff --git a/pkg/apis/flagger/v1beta1/canary.go b/pkg/apis/flagger/v1beta1/canary.go
index f370f55e..f13bd908 100644
--- a/pkg/apis/flagger/v1beta1/canary.go
+++ b/pkg/apis/flagger/v1beta1/canary.go
@@ -80,8 +80,11 @@ type CanarySpec struct {
 	// Service defines how ClusterIP services, service mesh or ingress routing objects are generated
 	Service CanaryService `json:"service"`
 
-	// CanaryAnalysis defines how the analysis should be performed
-	CanaryAnalysis CanaryAnalysis `json:"canaryAnalysis"`
+	// Analysis defines the validation process of a release
+	Analysis *CanaryAnalysis `json:"analysis,omitempty"`
+
+	// Deprecated: replaced by Analysis
+	CanaryAnalysis *CanaryAnalysis `json:"canaryAnalysis,omitempty"`
 
 	// ProgressDeadlineSeconds represents the maximum time in seconds for a
 	// canary deployment to make progress before it is considered to be failed
@@ -352,13 +355,22 @@ func (c *Canary) GetProgressDeadlineSeconds() int {
 	return ProgressDeadlineSeconds
 }
 
+// GetAnalysis returns the analysis v1beta1 or v1alpha3
+// to be removed along with spec.canaryAnalysis in v1
+func (c *Canary) GetAnalysis() *CanaryAnalysis {
+	if c.Spec.Analysis != nil {
+		return c.Spec.Analysis
+	}
+	return c.Spec.CanaryAnalysis
+}
+
 // GetAnalysisInterval returns the canary analysis interval (default 60s)
 func (c *Canary) GetAnalysisInterval() time.Duration {
-	if c.Spec.CanaryAnalysis.Interval == "" {
+	if c.GetAnalysis().Interval == "" {
 		return AnalysisInterval
 	}
 
-	interval, err := time.ParseDuration(c.Spec.CanaryAnalysis.Interval)
+	interval, err := time.ParseDuration(c.GetAnalysis().Interval)
 	if err != nil {
 		return AnalysisInterval
 	}
@@ -370,7 +382,24 @@ func (c *Canary) GetAnalysisInterval() time.Duration {
 	return interval
 }
 
+// GetAnalysisThreshold returns the canary threshold (default 1)
+func (c *Canary) GetAnalysisThreshold() int {
+	if c.GetAnalysis().Threshold > 0 {
+		return c.GetAnalysis().Threshold
+	}
+	return 1
+}
+
 // GetMetricInterval returns the metric interval default value (1m)
 func (c *Canary) GetMetricInterval() string {
 	return MetricInterval
 }
+
+// SkipAnalysis returns true if the analysis is nil
+// or if spec.SkipAnalysis is true
+func (c *Canary) SkipAnalysis() bool {
+	if c.Spec.Analysis == nil && c.Spec.CanaryAnalysis == nil {
+		return true
+	}
+	return c.Spec.SkipAnalysis
+}
diff --git a/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go b/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go
index c825b121..86fb45e7 100644
--- a/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go
+++ b/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go
@@ -392,7 +392,16 @@ func (in *CanarySpec) DeepCopyInto(out *CanarySpec) {
 		**out = **in
 	}
 	in.Service.DeepCopyInto(&out.Service)
-	in.CanaryAnalysis.DeepCopyInto(&out.CanaryAnalysis)
+	if in.Analysis != nil {
+		in, out := &in.Analysis, &out.Analysis
+		*out = new(CanaryAnalysis)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.CanaryAnalysis != nil {
+		in, out := &in.CanaryAnalysis, &out.CanaryAnalysis
+		*out = new(CanaryAnalysis)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.ProgressDeadlineSeconds != nil {
 		in, out := &in.ProgressDeadlineSeconds, &out.ProgressDeadlineSeconds
 		*out = new(int32)
diff --git a/pkg/canary/daemonset_controller.go b/pkg/canary/daemonset_controller.go
index 1c07c7fc..0041247b 100644
--- a/pkg/canary/daemonset_controller.go
+++ b/pkg/canary/daemonset_controller.go
@@ -91,7 +91,7 @@ func (c *DaemonSetController) Initialize(cd *flaggerv1.Canary, skipLivenessCheck
 	}
 
 	if cd.Status.Phase == "" || cd.Status.Phase == flaggerv1.CanaryPhaseInitializing {
-		if !skipLivenessChecks && !cd.Spec.SkipAnalysis {
+		if !skipLivenessChecks && !cd.SkipAnalysis() {
 			_, readyErr := c.IsPrimaryReady(cd)
 			if readyErr != nil {
 				return readyErr
diff --git a/pkg/canary/deployment_controller.go b/pkg/canary/deployment_controller.go
index e27dbc63..96b6b77f 100644
--- a/pkg/canary/deployment_controller.go
+++ b/pkg/canary/deployment_controller.go
@@ -37,7 +37,7 @@ func (c *DeploymentController) Initialize(cd *flaggerv1.Canary, skipLivenessChec
 	}
 
 	if cd.Status.Phase == "" || cd.Status.Phase == flaggerv1.CanaryPhaseInitializing {
-		if !skipLivenessChecks && !cd.Spec.SkipAnalysis {
+		if !skipLivenessChecks && !cd.SkipAnalysis() {
 			_, readyErr := c.IsPrimaryReady(cd)
 			if readyErr != nil {
 				return readyErr
diff --git a/pkg/controller/events.go b/pkg/controller/events.go
index 00fe49a1..fb29915a 100644
--- a/pkg/controller/events.go
+++ b/pkg/controller/events.go
@@ -2,6 +2,7 @@ package controller
 
 import (
 	"fmt"
+
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	corev1 "k8s.io/api/core/v1"
@@ -30,8 +31,8 @@ func (c *Controller) recordEventWarningf(r *flaggerv1.Canary, template string, a
 
 func (c *Controller) sendEventToWebhook(r *flaggerv1.Canary, eventType, template string, args []interface{}) {
 	webhookOverride := false
-	if len(r.Spec.CanaryAnalysis.Webhooks) > 0 {
-		for _, canaryWebhook := range r.Spec.CanaryAnalysis.Webhooks {
+	if len(r.GetAnalysis().Webhooks) > 0 {
+		for _, canaryWebhook := range r.GetAnalysis().Webhooks {
 			if canaryWebhook.Type == flaggerv1.EventHook {
 				webhookOverride = true
 				err := CallEventWebhook(r, canaryWebhook.URL, fmt.Sprintf(template, args...), eventType)
@@ -51,7 +52,7 @@ func (c *Controller) sendEventToWebhook(r *flaggerv1.Canary, eventType, template
 }
 
 func (c *Controller) alert(canary *flaggerv1.Canary, message string, metadata bool, severity flaggerv1.AlertSeverity) {
-	if c.notifier == nil && len(canary.Spec.CanaryAnalysis.Alerts) == 0 {
+	if c.notifier == nil && len(canary.GetAnalysis().Alerts) == 0 {
 		return
 	}
 
@@ -61,7 +62,7 @@ func (c *Controller) alert(canary *flaggerv1.Canary, message string, metadata bo
 	}
 
 	// send alert with the global notifier
-	if len(canary.Spec.CanaryAnalysis.Alerts) == 0 {
+	if len(canary.GetAnalysis().Alerts) == 0 {
 		err := c.notifier.Post(canary.Name, canary.Namespace, message, fields, string(severity))
 		if err != nil {
 			c.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
@@ -72,7 +73,7 @@ func (c *Controller) alert(canary *flaggerv1.Canary, message string, metadata bo
 	}
 
 	// send canary alerts
-	for _, alert := range canary.Spec.CanaryAnalysis.Alerts {
+	for _, alert := range canary.GetAnalysis().Alerts {
 		// determine if alert should be sent based on severity level
 		shouldAlert := false
 		if alert.Severity == flaggerv1.SeverityInfo {
@@ -161,7 +162,7 @@ func alertMetadata(canary *flaggerv1.Canary) []notifier.Field {
 		},
 		notifier.Field{
 			Name:  "Failed checks threshold",
-			Value: fmt.Sprintf("%v", canary.Spec.CanaryAnalysis.Threshold),
+			Value: fmt.Sprintf("%v", canary.GetAnalysisThreshold()),
 		},
 		notifier.Field{
 			Name:  "Progress deadline",
@@ -169,19 +170,19 @@ func alertMetadata(canary *flaggerv1.Canary) []notifier.Field {
 		},
 	)
 
-	if canary.Spec.CanaryAnalysis.StepWeight > 0 {
+	if canary.GetAnalysis().StepWeight > 0 {
 		fields = append(fields, notifier.Field{
 			Name: "Traffic routing",
 			Value: fmt.Sprintf("Weight step: %v max: %v",
-				canary.Spec.CanaryAnalysis.StepWeight,
-				canary.Spec.CanaryAnalysis.MaxWeight),
+				canary.GetAnalysis().StepWeight,
+				canary.GetAnalysis().MaxWeight),
 		})
-	} else if len(canary.Spec.CanaryAnalysis.Match) > 0 {
+	} else if len(canary.GetAnalysis().Match) > 0 {
 		fields = append(fields, notifier.Field{
 			Name:  "Traffic routing",
 			Value: "A/B Testing",
 		})
-	} else if canary.Spec.CanaryAnalysis.Iterations > 0 {
+	} else if canary.GetAnalysis().Iterations > 0 {
 		fields = append(fields, notifier.Field{
 			Name:  "Traffic routing",
 			Value: "Blue/Green",
diff --git a/pkg/controller/scheduler.go b/pkg/controller/scheduler.go
index f930f0e1..6d393b06 100644
--- a/pkg/controller/scheduler.go
+++ b/pkg/controller/scheduler.go
@@ -157,12 +157,12 @@ func (c *Controller) advanceCanary(name string, namespace string, skipLivenessCh
 
 	// set max weight default value to 100%
 	maxWeight := 100
-	if cd.Spec.CanaryAnalysis.MaxWeight > 0 {
-		maxWeight = cd.Spec.CanaryAnalysis.MaxWeight
+	if cd.GetAnalysis().MaxWeight > 0 {
+		maxWeight = cd.GetAnalysis().MaxWeight
 	}
 
 	// check primary status
-	if !skipLivenessChecks && !cd.Spec.SkipAnalysis {
+	if !skipLivenessChecks && !cd.SkipAnalysis() {
 		if _, err := canaryController.IsPrimaryReady(cd); err != nil {
 			c.recordEventWarningf(cd, "%v", err)
 			return
@@ -278,7 +278,7 @@ func (c *Controller) advanceCanary(name string, namespace string, skipLivenessCh
 
 	// check if the number of failed checks reached the threshold
 	if cd.Status.Phase == flaggerv1.CanaryPhaseProgressing &&
-		(!retriable || cd.Status.FailedChecks >= cd.Spec.CanaryAnalysis.Threshold) {
+		(!retriable || cd.Status.FailedChecks >= cd.GetAnalysisThreshold()) {
 		if !retriable {
 			c.recordEventWarningf(cd, "Rolling back %s.%s progress deadline exceeded %v",
 				cd.Name, cd.Namespace, err)
@@ -297,7 +297,7 @@ func (c *Controller) advanceCanary(name string, namespace string, skipLivenessCh
 	// check if the canary success rate is above the threshold
 	// skip check if no traffic is routed or mirrored to canary
 	if canaryWeight == 0 && cd.Status.Iterations == 0 &&
-		(cd.Spec.CanaryAnalysis.Mirror == false || mirrored == false) {
+		(cd.GetAnalysis().Mirror == false || mirrored == false) {
 		c.recordEventInfof(cd, "Starting canary analysis for %s.%s", cd.Spec.TargetRef.Name, cd.Namespace)
 
 		// run pre-rollout web hooks
@@ -320,31 +320,31 @@ func (c *Controller) advanceCanary(name string, namespace string, skipLivenessCh
 
 	// use blue/green strategy for kubernetes provider
 	if provider == "kubernetes" {
-		if len(cd.Spec.CanaryAnalysis.Match) > 0 {
+		if len(cd.GetAnalysis().Match) > 0 {
 			c.recordEventWarningf(cd, "A/B testing is not supported when using the kubernetes provider")
-			cd.Spec.CanaryAnalysis.Match = nil
+			cd.GetAnalysis().Match = nil
 		}
-		if cd.Spec.CanaryAnalysis.Iterations < 1 {
+		if cd.GetAnalysis().Iterations < 1 {
 			c.recordEventWarningf(cd, "Progressive traffic is not supported when using the kubernetes provider")
 			c.recordEventWarningf(cd, "Setting canaryAnalysis.iterations: 10")
-			cd.Spec.CanaryAnalysis.Iterations = 10
+			cd.GetAnalysis().Iterations = 10
 		}
 	}
 
 	// strategy: A/B testing
-	if len(cd.Spec.CanaryAnalysis.Match) > 0 && cd.Spec.CanaryAnalysis.Iterations > 0 {
+	if len(cd.GetAnalysis().Match) > 0 && cd.GetAnalysis().Iterations > 0 {
 		c.runAB(cd, canaryController, meshRouter, provider)
 		return
 	}
 
 	// strategy: Blue/Green
-	if cd.Spec.CanaryAnalysis.Iterations > 0 {
+	if cd.GetAnalysis().Iterations > 0 {
 		c.runBlueGreen(cd, canaryController, meshRouter, provider, mirrored)
 		return
 	}
 
 	// strategy: Canary progressive traffic increase
-	if cd.Spec.CanaryAnalysis.StepWeight > 0 {
+	if cd.GetAnalysis().StepWeight > 0 {
 		c.runCanary(cd, canaryController, meshRouter, provider, mirrored, canaryWeight, primaryWeight, maxWeight)
 	}
 
@@ -358,25 +358,25 @@ func (c *Controller) runCanary(canary *flaggerv1.Canary, canaryController canary
 		// If in "mirror" mode, do one step of mirroring before shifting traffic to canary.
 		// When mirroring, all requests go to primary and canary, but only responses from
 		// primary go back to the user.
-		if canary.Spec.CanaryAnalysis.Mirror && canaryWeight == 0 {
+		if canary.GetAnalysis().Mirror && canaryWeight == 0 {
 			if mirrored == false {
 				mirrored = true
 				primaryWeight = 100
 				canaryWeight = 0
 			} else {
 				mirrored = false
-				primaryWeight = 100 - canary.Spec.CanaryAnalysis.StepWeight
-				canaryWeight = canary.Spec.CanaryAnalysis.StepWeight
+				primaryWeight = 100 - canary.GetAnalysis().StepWeight
+				canaryWeight = canary.GetAnalysis().StepWeight
 			}
 			c.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
 				Infof("Running mirror step %d/%d/%t", primaryWeight, canaryWeight, mirrored)
 		} else {
 
-			primaryWeight -= canary.Spec.CanaryAnalysis.StepWeight
+			primaryWeight -= canary.GetAnalysis().StepWeight
 			if primaryWeight < 0 {
 				primaryWeight = 0
 			}
-			canaryWeight += canary.Spec.CanaryAnalysis.StepWeight
+			canaryWeight += canary.GetAnalysis().StepWeight
 			if canaryWeight > 100 {
 				canaryWeight = 100
 			}
@@ -424,7 +424,7 @@ func (c *Controller) runAB(canary *flaggerv1.Canary, canaryController canary.Con
 	primaryName := fmt.Sprintf("%s-primary", canary.Spec.TargetRef.Name)
 
 	// route traffic to canary and increment iterations
-	if canary.Spec.CanaryAnalysis.Iterations > canary.Status.Iterations {
+	if canary.GetAnalysis().Iterations > canary.Status.Iterations {
 		if err := meshRouter.SetRoutes(canary, 0, 100, false); err != nil {
 			c.recordEventWarningf(canary, "%v", err)
 			return
@@ -436,7 +436,7 @@ func (c *Controller) runAB(canary *flaggerv1.Canary, canaryController canary.Con
 			return
 		}
 		c.recordEventInfof(canary, "Advance %s.%s canary iteration %v/%v",
-			canary.Name, canary.Namespace, canary.Status.Iterations+1, canary.Spec.CanaryAnalysis.Iterations)
+			canary.Name, canary.Namespace, canary.Status.Iterations+1, canary.GetAnalysis().Iterations)
 		return
 	}
 
@@ -446,7 +446,7 @@ func (c *Controller) runAB(canary *flaggerv1.Canary, canaryController canary.Con
 	}
 
 	// promote canary - max iterations reached
-	if canary.Spec.CanaryAnalysis.Iterations == canary.Status.Iterations {
+	if canary.GetAnalysis().Iterations == canary.Status.Iterations {
 		c.recordEventInfof(canary, "Copying %s.%s template spec to %s.%s",
 			canary.Spec.TargetRef.Name, canary.Namespace, primaryName, canary.Namespace)
 		if err := canaryController.Promote(canary); err != nil {
@@ -466,10 +466,10 @@ func (c *Controller) runBlueGreen(canary *flaggerv1.Canary, canaryController can
 	primaryName := fmt.Sprintf("%s-primary", canary.Spec.TargetRef.Name)
 
 	// increment iterations
-	if canary.Spec.CanaryAnalysis.Iterations > canary.Status.Iterations {
+	if canary.GetAnalysis().Iterations > canary.Status.Iterations {
 		// If in "mirror" mode, mirror requests during the entire B/G canary test
 		if provider != "kubernetes" &&
-			canary.Spec.CanaryAnalysis.Mirror == true && mirrored == false {
+			canary.GetAnalysis().Mirror == true && mirrored == false {
 			if err := meshRouter.SetRoutes(canary, 100, 0, true); err != nil {
 				c.recordEventWarningf(canary, "%v", err)
 			}
@@ -481,7 +481,7 @@ func (c *Controller) runBlueGreen(canary *flaggerv1.Canary, canaryController can
 			return
 		}
 		c.recordEventInfof(canary, "Advance %s.%s canary iteration %v/%v",
-			canary.Name, canary.Namespace, canary.Status.Iterations+1, canary.Spec.CanaryAnalysis.Iterations)
+			canary.Name, canary.Namespace, canary.Status.Iterations+1, canary.GetAnalysis().Iterations)
 		return
 	}
 
@@ -491,9 +491,9 @@ func (c *Controller) runBlueGreen(canary *flaggerv1.Canary, canaryController can
 	}
 
 	// route all traffic to canary - max iterations reached
-	if canary.Spec.CanaryAnalysis.Iterations == canary.Status.Iterations {
+	if canary.GetAnalysis().Iterations == canary.Status.Iterations {
 		if provider != "kubernetes" {
-			if canary.Spec.CanaryAnalysis.Mirror {
+			if canary.GetAnalysis().Mirror {
 				c.recordEventInfof(canary, "Stop traffic mirroring and route all traffic to canary")
 			} else {
 				c.recordEventInfof(canary, "Routing all traffic to canary")
@@ -514,7 +514,7 @@ func (c *Controller) runBlueGreen(canary *flaggerv1.Canary, canaryController can
 	}
 
 	// promote canary - max iterations reached
-	if canary.Spec.CanaryAnalysis.Iterations < canary.Status.Iterations {
+	if canary.GetAnalysis().Iterations < canary.Status.Iterations {
 		c.recordEventInfof(canary, "Copying %s.%s template spec to %s.%s",
 			canary.Spec.TargetRef.Name, canary.Namespace, primaryName, canary.Namespace)
 		if err := canaryController.Promote(canary); err != nil {
@@ -532,7 +532,7 @@ func (c *Controller) runBlueGreen(canary *flaggerv1.Canary, canaryController can
 }
 
 func (c *Controller) shouldSkipAnalysis(canary *flaggerv1.Canary, canaryController canary.Controller, meshRouter router.Interface, primaryWeight int, canaryWeight int) bool {
-	if !canary.Spec.SkipAnalysis {
+	if !canary.SkipAnalysis() {
 		return false
 	}
 
@@ -656,7 +656,7 @@ func (c *Controller) hasCanaryRevisionChanged(canary *flaggerv1.Canary, canaryCo
 }
 
 func (c *Controller) runConfirmRolloutHooks(canary *flaggerv1.Canary, canaryController canary.Controller) bool {
-	for _, webhook := range canary.Spec.CanaryAnalysis.Webhooks {
+	for _, webhook := range canary.GetAnalysis().Webhooks {
 		if webhook.Type == flaggerv1.ConfirmRolloutHook {
 			err := CallWebhook(canary.Name, canary.Namespace, flaggerv1.CanaryPhaseProgressing, webhook)
 			if err != nil {
@@ -685,7 +685,7 @@ func (c *Controller) runConfirmRolloutHooks(canary *flaggerv1.Canary, canaryCont
 }
 
 func (c *Controller) runConfirmPromotionHooks(canary *flaggerv1.Canary) bool {
-	for _, webhook := range canary.Spec.CanaryAnalysis.Webhooks {
+	for _, webhook := range canary.GetAnalysis().Webhooks {
 		if webhook.Type == flaggerv1.ConfirmPromotionHook {
 			err := CallWebhook(canary.Name, canary.Namespace, flaggerv1.CanaryPhaseProgressing, webhook)
 			if err != nil {
@@ -702,7 +702,7 @@ func (c *Controller) runConfirmPromotionHooks(canary *flaggerv1.Canary) bool {
 }
 
 func (c *Controller) runPreRolloutHooks(canary *flaggerv1.Canary) bool {
-	for _, webhook := range canary.Spec.CanaryAnalysis.Webhooks {
+	for _, webhook := range canary.GetAnalysis().Webhooks {
 		if webhook.Type == flaggerv1.PreRolloutHook {
 			err := CallWebhook(canary.Name, canary.Namespace, flaggerv1.CanaryPhaseProgressing, webhook)
 			if err != nil {
@@ -718,7 +718,7 @@ func (c *Controller) runPreRolloutHooks(canary *flaggerv1.Canary) bool {
 }
 
 func (c *Controller) runPostRolloutHooks(canary *flaggerv1.Canary, phase flaggerv1.CanaryPhase) bool {
-	for _, webhook := range canary.Spec.CanaryAnalysis.Webhooks {
+	for _, webhook := range canary.GetAnalysis().Webhooks {
 		if webhook.Type == flaggerv1.PostRolloutHook {
 			err := CallWebhook(canary.Name, canary.Namespace, phase, webhook)
 			if err != nil {
@@ -733,7 +733,7 @@ func (c *Controller) runPostRolloutHooks(canary *flaggerv1.Canary, phase flagger
 }
 
 func (c *Controller) runRollbackHooks(canary *flaggerv1.Canary, phase flaggerv1.CanaryPhase) bool {
-	for _, webhook := range canary.Spec.CanaryAnalysis.Webhooks {
+	for _, webhook := range canary.GetAnalysis().Webhooks {
 		if webhook.Type == flaggerv1.RollbackHook {
 			err := CallWebhook(canary.Name, canary.Namespace, phase, webhook)
 			if err != nil {
@@ -749,7 +749,7 @@ func (c *Controller) runRollbackHooks(canary *flaggerv1.Canary, phase flaggerv1.
 
 func (c *Controller) runAnalysis(canary *flaggerv1.Canary) bool {
 	// run external checks
-	for _, webhook := range canary.Spec.CanaryAnalysis.Webhooks {
+	for _, webhook := range canary.GetAnalysis().Webhooks {
 		if webhook.Type == "" || webhook.Type == flaggerv1.RolloutHook {
 			err := CallWebhook(canary.Name, canary.Namespace, flaggerv1.CanaryPhaseProgressing, webhook)
 			if err != nil {
@@ -812,7 +812,7 @@ func (c *Controller) runBuiltinMetricChecks(canary *flaggerv1.Canary) bool {
 	observer := observerFactory.Observer(metricsProvider)
 
 	// run metrics checks
-	for _, metric := range canary.Spec.CanaryAnalysis.Metrics {
+	for _, metric := range canary.GetAnalysis().Metrics {
 		if metric.Interval == "" {
 			metric.Interval = canary.GetMetricInterval()
 		}
@@ -914,7 +914,7 @@ func (c *Controller) runBuiltinMetricChecks(canary *flaggerv1.Canary) bool {
 }
 
 func (c *Controller) runMetricChecks(canary *flaggerv1.Canary) bool {
-	for _, metric := range canary.Spec.CanaryAnalysis.Metrics {
+	for _, metric := range canary.GetAnalysis().Metrics {
 		if metric.TemplateRef != nil {
 			namespace := canary.Namespace
 			if metric.TemplateRef.Namespace != "" {
@@ -1007,7 +1007,7 @@ func toMetricModel(r *flaggerv1.Canary, interval string) flaggerv1.MetricTemplat
 }
 
 func (c *Controller) rollback(canary *flaggerv1.Canary, canaryController canary.Controller, meshRouter router.Interface) {
-	if canary.Status.FailedChecks >= canary.Spec.CanaryAnalysis.Threshold {
+	if canary.Status.FailedChecks >= canary.GetAnalysisThreshold() {
 		c.recordEventWarningf(canary, "Rolling back %s.%s failed checks threshold reached %v",
 			canary.Name, canary.Namespace, canary.Status.FailedChecks)
 		c.alert(canary, fmt.Sprintf("Failed checks threshold reached %v", canary.Status.FailedChecks),
diff --git a/pkg/controller/webhook.go b/pkg/controller/webhook.go
index ed5316c7..c07bd657 100644
--- a/pkg/controller/webhook.go
+++ b/pkg/controller/webhook.go
@@ -7,12 +7,13 @@ import (
 	"errors"
 	"fmt"
 	"io/ioutil"
-	"k8s.io/utils/clock"
 	"net/http"
 	"net/url"
 	"strconv"
 	"time"
 
+	"k8s.io/utils/clock"
+
 	flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
 )
 
diff --git a/pkg/metrics/observers/observer.go b/pkg/metrics/observers/observer.go
index a2e90658..496f2dd4 100644
--- a/pkg/metrics/observers/observer.go
+++ b/pkg/metrics/observers/observer.go
@@ -1,8 +1,9 @@
 package observers
 
 import (
-	flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
 	"time"
+
+	flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
 )
 
 type Interface interface {
diff --git a/pkg/router/appmesh.go b/pkg/router/appmesh.go
index 1d13fff8..2f096417 100644
--- a/pkg/router/appmesh.go
+++ b/pkg/router/appmesh.go
@@ -203,7 +203,7 @@ func (ar *AppMeshRouter) reconcileVirtualService(canary *flaggerv1.Canary, name
 	}
 
 	// A/B testing - header based routing
-	if len(canary.Spec.CanaryAnalysis.Match) > 0 && canaryWeight == 0 {
+	if len(canary.GetAnalysis().Match) > 0 && canaryWeight == 0 {
 		routes = []appmeshv1.Route{
 			{
 				Name:     fmt.Sprintf("%s-a", apexName),
@@ -451,7 +451,7 @@ func makeRetryPolicy(canary *flaggerv1.Canary) *appmeshv1.HttpRetryPolicy {
 func (ar *AppMeshRouter) makeHeaders(canary *flaggerv1.Canary) []appmeshv1.HttpRouteHeader {
 	headers := []appmeshv1.HttpRouteHeader{}
 
-	for _, m := range canary.Spec.CanaryAnalysis.Match {
+	for _, m := range canary.GetAnalysis().Match {
 		for key, value := range m.Headers {
 			header := appmeshv1.HttpRouteHeader{
 				Name: key,
diff --git a/pkg/router/contour.go b/pkg/router/contour.go
index 50bbee2a..8ad846ab 100644
--- a/pkg/router/contour.go
+++ b/pkg/router/contour.go
@@ -64,7 +64,7 @@ func (cr *ContourRouter) Reconcile(canary *flaggerv1.Canary) error {
 		},
 	}
 
-	if len(canary.Spec.CanaryAnalysis.Match) > 0 {
+	if len(canary.GetAnalysis().Match) > 0 {
 		newSpec = contourv1.HTTPProxySpec{
 			Routes: []contourv1.Route{
 				{
@@ -277,7 +277,7 @@ func (cr *ContourRouter) SetRoutes(
 		},
 	}
 
-	if len(canary.Spec.CanaryAnalysis.Match) > 0 {
+	if len(canary.GetAnalysis().Match) > 0 {
 		proxy.Spec = contourv1.HTTPProxySpec{
 			Routes: []contourv1.Route{
 				{
@@ -364,8 +364,8 @@ func (cr *ContourRouter) makePrefix(canary *flaggerv1.Canary) string {
 func (cr *ContourRouter) makeConditions(canary *flaggerv1.Canary) []contourv1.Condition {
 	list := []contourv1.Condition{}
 
-	if len(canary.Spec.CanaryAnalysis.Match) > 0 {
-		for _, match := range canary.Spec.CanaryAnalysis.Match {
+	if len(canary.GetAnalysis().Match) > 0 {
+		for _, match := range canary.GetAnalysis().Match {
 			for s, stringMatch := range match.Headers {
 				h := &contourv1.HeaderCondition{
 					Name:  s,
diff --git a/pkg/router/ingress.go b/pkg/router/ingress.go
index 22e8b9b8..e97d68c4 100644
--- a/pkg/router/ingress.go
+++ b/pkg/router/ingress.go
@@ -117,7 +117,7 @@ func (i *IngressRouter) GetRoutes(canary *flaggerv1.Canary) (
 	}
 
 	// A/B testing
-	if len(canary.Spec.CanaryAnalysis.Match) > 0 {
+	if len(canary.GetAnalysis().Match) > 0 {
 		for k := range canaryIngress.Annotations {
 			if k == i.GetAnnotationWithPrefix("canary-by-cookie") || k == i.GetAnnotationWithPrefix("canary-by-header") {
 				return 0, 100, false, nil
@@ -158,11 +158,11 @@ func (i *IngressRouter) SetRoutes(
 	iClone := canaryIngress.DeepCopy()
 
 	// A/B testing
-	if len(canary.Spec.CanaryAnalysis.Match) > 0 {
+	if len(canary.GetAnalysis().Match) > 0 {
 		cookie := ""
 		header := ""
 		headerValue := ""
-		for _, m := range canary.Spec.CanaryAnalysis.Match {
+		for _, m := range canary.GetAnalysis().Match {
 			for k, v := range m.Headers {
 				if k == "cookie" {
 					cookie = v.Exact
diff --git a/pkg/router/istio.go b/pkg/router/istio.go
index 015698cf..df809313 100644
--- a/pkg/router/istio.go
+++ b/pkg/router/istio.go
@@ -152,8 +152,8 @@ func (ir *IstioRouter) reconcileVirtualService(canary *flaggerv1.Canary) error {
 		},
 	}
 
-	if len(canary.Spec.CanaryAnalysis.Match) > 0 {
-		canaryMatch := mergeMatchConditions(canary.Spec.CanaryAnalysis.Match, canary.Spec.Service.Match)
+	if len(canary.GetAnalysis().Match) > 0 {
+		canaryMatch := mergeMatchConditions(canary.GetAnalysis().Match, canary.Spec.Service.Match)
 		newSpec.Http = []istiov1alpha3.HTTPRoute{
 			{
 				Match:      canaryMatch,
@@ -323,9 +323,9 @@ func (ir *IstioRouter) SetRoutes(
 	}
 
 	// fix routing (A/B testing)
-	if len(canary.Spec.CanaryAnalysis.Match) > 0 {
+	if len(canary.GetAnalysis().Match) > 0 {
 		// merge the common routes with the canary ones
-		canaryMatch := mergeMatchConditions(canary.Spec.CanaryAnalysis.Match, canary.Spec.Service.Match)
+		canaryMatch := mergeMatchConditions(canary.GetAnalysis().Match, canary.Spec.Service.Match)
 		vsCopy.Spec.Http = []istiov1alpha3.HTTPRoute{
 			{
 				Match:      canaryMatch,


Test patch:
diff --git a/cmd/loadtester/main.go b/cmd/loadtester/main.go
index a7b6a923..db01eef8 100644
--- a/cmd/loadtester/main.go
+++ b/cmd/loadtester/main.go
@@ -2,12 +2,13 @@ package main
 
 import (
 	"flag"
+	"log"
+	"time"
+
 	"github.com/weaveworks/flagger/pkg/loadtester"
 	"github.com/weaveworks/flagger/pkg/logger"
 	"github.com/weaveworks/flagger/pkg/signals"
 	"go.uber.org/zap"
-	"log"
-	"time"
 )
 
 var VERSION = "0.12.1"
diff --git a/pkg/canary/deployment_fixture_test.go b/pkg/canary/deployment_fixture_test.go
index 62e2a2b9..486b3472 100644
--- a/pkg/canary/deployment_fixture_test.go
+++ b/pkg/canary/deployment_fixture_test.go
@@ -207,7 +207,7 @@ func newDeploymentControllerTestCanary() *flaggerv1.Canary {
 				Kind:       "HorizontalPodAutoscaler",
 			}, Service: flaggerv1.CanaryService{
 				Port: 9898,
-			}, CanaryAnalysis: flaggerv1.CanaryAnalysis{
+			}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
 				Threshold:  10,
 				StepWeight: 10,
 				MaxWeight:  50,
diff --git a/pkg/controller/scheduler_common_test.go b/pkg/controller/scheduler_common_test.go
index 512a0f4c..01f06865 100644
--- a/pkg/controller/scheduler_common_test.go
+++ b/pkg/controller/scheduler_common_test.go
@@ -1,5 +1,26 @@
 package controller
 
+import (
+	"fmt"
+
+	flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
+	clientset "github.com/weaveworks/flagger/pkg/client/clientset/versioned"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func assertPhase(flaggerClient clientset.Interface, canary string, phase flaggerv1.CanaryPhase) error {
+	c, err := flaggerClient.FlaggerV1beta1().Canaries("default").Get(canary, metav1.GetOptions{})
+	if err != nil {
+		return err
+	}
+
+	if c.Status.Phase != phase {
+		return fmt.Errorf("Got canary state %v wanted %v", c.Status.Phase, phase)
+	}
+
+	return nil
+}
+
 func alwaysReady() bool {
 	return true
 }
diff --git a/pkg/controller/scheduler_daemonset_fixture_test.go b/pkg/controller/scheduler_daemonset_fixture_test.go
index 8e0b2478..8d2cdd6d 100644
--- a/pkg/controller/scheduler_daemonset_fixture_test.go
+++ b/pkg/controller/scheduler_daemonset_fixture_test.go
@@ -250,7 +250,7 @@ func newDaemonSetTestCanary() *flaggerv1.Canary {
 				Kind:       "DaemonSet",
 			}, Service: flaggerv1.CanaryService{
 				Port: 9898,
-			}, CanaryAnalysis: flaggerv1.CanaryAnalysis{
+			}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
 				Threshold:  10,
 				StepWeight: 10,
 				MaxWeight:  50,
@@ -307,7 +307,7 @@ func newDaemonSetTestCanaryAB() *flaggerv1.Canary {
 				Kind:       "DaemonSet",
 			}, Service: flaggerv1.CanaryService{
 				Port: 9898,
-			}, CanaryAnalysis: flaggerv1.CanaryAnalysis{
+			}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
 				Threshold:  10,
 				Iterations: 10,
 				Match: []istiov1alpha3.HTTPMatchRequest{
diff --git a/pkg/controller/scheduler_deployment_fixture_test.go b/pkg/controller/scheduler_deployment_fixture_test.go
index a3f2db50..74a7321e 100644
--- a/pkg/controller/scheduler_deployment_fixture_test.go
+++ b/pkg/controller/scheduler_deployment_fixture_test.go
@@ -257,7 +257,7 @@ func newDeploymentTestCanary() *flaggerv1.Canary {
 				Kind:       "HorizontalPodAutoscaler",
 			}, Service: flaggerv1.CanaryService{
 				Port: 9898,
-			}, CanaryAnalysis: flaggerv1.CanaryAnalysis{
+			}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
 				Threshold:  10,
 				StepWeight: 10,
 				MaxWeight:  50,
@@ -319,7 +319,7 @@ func newDeploymentTestCanaryAB() *flaggerv1.Canary {
 				Kind:       "HorizontalPodAutoscaler",
 			}, Service: flaggerv1.CanaryService{
 				Port: 9898,
-			}, CanaryAnalysis: flaggerv1.CanaryAnalysis{
+			}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
 				Threshold:  10,
 				Iterations: 10,
 				Match: []istiov1alpha3.HTTPMatchRequest{
diff --git a/pkg/controller/scheduler_deployment_test.go b/pkg/controller/scheduler_deployment_test.go
index da72f375..b1171942 100644
--- a/pkg/controller/scheduler_deployment_test.go
+++ b/pkg/controller/scheduler_deployment_test.go
@@ -144,6 +144,116 @@ func TestScheduler_DeploymentSkipAnalysis(t *testing.T) {
 	}
 }
 
+func TestScheduler_DeploymentAnalysisPhases(t *testing.T) {
+	cd := newDeploymentTestCanary()
+	cd.Spec.Analysis = &flaggerv1.CanaryAnalysis{
+		Interval:   "1m",
+		StepWeight: 100,
+	}
+	mocks := newDeploymentFixture(cd)
+
+	// init
+	mocks.ctrl.advanceCanary("podinfo", "default", true)
+	if err := assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseInitialized); err != nil {
+		t.Fatal(err.Error())
+	}
+
+	// update
+	dep2 := newDeploymentTestDeploymentV2()
+	_, err := mocks.kubeClient.AppsV1().Deployments("default").Update(dep2)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+
+	// detect changes
+	mocks.ctrl.advanceCanary("podinfo", "default", true)
+	if err := assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseProgressing); err != nil {
+		t.Fatal(err.Error())
+	}
+
+	// progressing
+	mocks.ctrl.advanceCanary("podinfo", "default", true)
+	if err := assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseProgressing); err != nil {
+		t.Fatal(err.Error())
+	}
+
+	// promoting
+	mocks.ctrl.advanceCanary("podinfo", "default", true)
+	if err := assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhasePromoting); err != nil {
+		t.Fatal(err.Error())
+	}
+
+	// finalising
+	mocks.ctrl.advanceCanary("podinfo", "default", true)
+	if err := assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseFinalising); err != nil {
+		t.Fatal(err.Error())
+	}
+
+	// succeeded
+	mocks.ctrl.advanceCanary("podinfo", "default", true)
+	if err := assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseSucceeded); err != nil {
+		t.Fatal(err.Error())
+	}
+}
+
+func TestScheduler_DeploymentBlueGreenAnalysisPhases(t *testing.T) {
+	cd := newDeploymentTestCanary()
+	cd.Spec.Analysis = &flaggerv1.CanaryAnalysis{
+		Interval:   "1m",
+		Iterations: 1,
+	}
+	mocks := newDeploymentFixture(cd)
+
+	// init
+	mocks.ctrl.advanceCanary("podinfo", "default", true)
+	if err := assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseInitialized); err != nil {
+		t.Fatal(err.Error())
+	}
+
+	// update
+	dep2 := newDeploymentTestDeploymentV2()
+	_, err := mocks.kubeClient.AppsV1().Deployments("default").Update(dep2)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+
+	// detect changes (progressing)
+	mocks.ctrl.advanceCanary("podinfo", "default", true)
+	if err := assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseProgressing); err != nil {
+		t.Fatal(err.Error())
+	}
+
+	// advance (progressing)
+	mocks.ctrl.advanceCanary("podinfo", "default", true)
+	if err := assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseProgressing); err != nil {
+		t.Fatal(err.Error())
+	}
+
+	// route traffic to primary (progressing)
+	mocks.ctrl.advanceCanary("podinfo", "default", true)
+	if err := assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseProgressing); err != nil {
+		t.Fatal(err.Error())
+	}
+
+	// promoting
+	mocks.ctrl.advanceCanary("podinfo", "default", true)
+	if err := assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhasePromoting); err != nil {
+		t.Fatal(err.Error())
+	}
+
+	// finalising
+	mocks.ctrl.advanceCanary("podinfo", "default", true)
+	if err := assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseFinalising); err != nil {
+		t.Fatal(err.Error())
+	}
+
+	// succeeded
+	mocks.ctrl.advanceCanary("podinfo", "default", true)
+	if err := assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseSucceeded); err != nil {
+		t.Fatal(err.Error())
+	}
+}
+
 func TestScheduler_DeploymentNewRevisionReset(t *testing.T) {
 	mocks := newDeploymentFixture(nil)
 	// init
diff --git a/pkg/controller/scheduler_svc_test.go b/pkg/controller/scheduler_svc_test.go
index 5e11d9fc..0db42439 100644
--- a/pkg/controller/scheduler_svc_test.go
+++ b/pkg/controller/scheduler_svc_test.go
@@ -142,7 +142,7 @@ func newTestServiceCanary() *flaggerv1.Canary {
 			Service: flaggerv1.CanaryService{
 				Port: 9898,
 			},
-			CanaryAnalysis: flaggerv1.CanaryAnalysis{
+			CanaryAnalysis: &flaggerv1.CanaryAnalysis{
 				Threshold:  10,
 				StepWeight: 10,
 				MaxWeight:  50,
diff --git a/pkg/loadtester/runner.go b/pkg/loadtester/runner.go
index 2cba9050..0a9ea90a 100644
--- a/pkg/loadtester/runner.go
+++ b/pkg/loadtester/runner.go
@@ -2,10 +2,11 @@ package loadtester
 
 import (
 	"context"
-	"go.uber.org/zap"
 	"sync"
 	"sync/atomic"
 	"time"
+
+	"go.uber.org/zap"
 )
 
 type TaskRunner struct {
diff --git a/pkg/loadtester/runner_test.go b/pkg/loadtester/runner_test.go
index 1c7ab9c7..1ab72487 100644
--- a/pkg/loadtester/runner_test.go
+++ b/pkg/loadtester/runner_test.go
@@ -1,9 +1,10 @@
 package loadtester
 
 import (
-	"github.com/weaveworks/flagger/pkg/logger"
 	"testing"
 	"time"
+
+	"github.com/weaveworks/flagger/pkg/logger"
 )
 
 func TestTaskRunner_Start(t *testing.T) {
diff --git a/pkg/loadtester/server.go b/pkg/loadtester/server.go
index a8817a4b..f6dc79da 100644
--- a/pkg/loadtester/server.go
+++ b/pkg/loadtester/server.go
@@ -179,7 +179,6 @@ func ListenAndServe(port string, timeout time.Duration, logger *zap.SugaredLogge
 			return
 		}
 
-
 		canaryName := fmt.Sprintf("rollback.%s.%s", canary.Name, canary.Namespace)
 		gate.close(canaryName)
 
diff --git a/pkg/loadtester/task.go b/pkg/loadtester/task.go
index 395b34a9..1ad8056a 100644
--- a/pkg/loadtester/task.go
+++ b/pkg/loadtester/task.go
@@ -3,9 +3,10 @@ package loadtester
 import (
 	"context"
 	"encoding/hex"
-	"go.uber.org/zap"
 	"hash/fnv"
 	"sync"
+
+	"go.uber.org/zap"
 )
 
 // Modeling a loadtester task
diff --git a/pkg/loadtester/task_ngrinder.go b/pkg/loadtester/task_ngrinder.go
index 4a4dca15..9d7e8b86 100644
--- a/pkg/loadtester/task_ngrinder.go
+++ b/pkg/loadtester/task_ngrinder.go
@@ -6,12 +6,13 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"go.uber.org/zap"
 	"io/ioutil"
 	"net/http"
 	"net/url"
 	"strconv"
 	"time"
+
+	"go.uber.org/zap"
 )
 
 const TaskTypeNGrinder = "ngrinder"
diff --git a/pkg/loadtester/task_ngrinder_test.go b/pkg/loadtester/task_ngrinder_test.go
index 699aea72..b7917763 100644
--- a/pkg/loadtester/task_ngrinder_test.go
+++ b/pkg/loadtester/task_ngrinder_test.go
@@ -3,10 +3,11 @@ package loadtester
 import (
 	"context"
 	"fmt"
-	"github.com/weaveworks/flagger/pkg/logger"
-	"gopkg.in/h2non/gock.v1"
 	"testing"
 	"time"
+
+	"github.com/weaveworks/flagger/pkg/logger"
+	"gopkg.in/h2non/gock.v1"
 )
 
 func TestTaskNGrinder(t *testing.T) {
diff --git a/pkg/loadtester/task_shell.go b/pkg/loadtester/task_shell.go
index 4f79b505..c8fcf496 100644
--- a/pkg/loadtester/task_shell.go
+++ b/pkg/loadtester/task_shell.go
@@ -4,9 +4,10 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"go.uber.org/zap"
 	"os/exec"
 	"strconv"
+
+	"go.uber.org/zap"
 )
 
 const TaskTypeShell = "cmd"
diff --git a/pkg/router/router_test.go b/pkg/router/router_test.go
index 9e4ad3ef..205a8ab3 100644
--- a/pkg/router/router_test.go
+++ b/pkg/router/router_test.go
@@ -112,7 +112,7 @@ func newTestCanary() *flaggerv1.Canary {
 					"public-gateway.istio",
 					"mesh",
 				},
-			}, CanaryAnalysis: flaggerv1.CanaryAnalysis{
+			}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
 				Threshold:  10,
 				StepWeight: 10,
 				MaxWeight:  50,
@@ -158,7 +158,7 @@ func newTestCanaryAppMesh() *flaggerv1.Canary {
 					PerTryTimeout: "gateway-error",
 					RetryOn:       "5s",
 				},
-			}, CanaryAnalysis: flaggerv1.CanaryAnalysis{
+			}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
 				Threshold:  10,
 				StepWeight: 10,
 				MaxWeight:  50,
@@ -203,7 +203,7 @@ func newTestSMICanary() *flaggerv1.Canary {
 				},
 				PortDiscovery: true,
 			},
-			CanaryAnalysis: flaggerv1.CanaryAnalysis{
+			CanaryAnalysis: &flaggerv1.CanaryAnalysis{
 				Threshold:  10,
 				StepWeight: 10,
 				MaxWeight:  50,
@@ -227,7 +227,7 @@ func newTestSMICanary() *flaggerv1.Canary {
 
 func newTestMirror() *flaggerv1.Canary {
 	cd := newTestCanary()
-	cd.Spec.CanaryAnalysis.Mirror = true
+	cd.GetAnalysis().Mirror = true
 	return cd
 }
 
@@ -247,7 +247,7 @@ func newTestABTest() *flaggerv1.Canary {
 			Service: flaggerv1.CanaryService{
 				Port:     9898,
 				MeshName: "global",
-			}, CanaryAnalysis: flaggerv1.CanaryAnalysis{
+			}, CanaryAnalysis: &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{
+			}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
 				Threshold:  10,
 				StepWeight: 10,
 				MaxWeight:  50,
diff --git a/test/e2e-contour-tests.sh b/test/e2e-contour-tests.sh
index a27eb398..70d18372 100755
--- a/test/e2e-contour-tests.sh
+++ b/test/e2e-contour-tests.sh
@@ -34,7 +34,7 @@ spec:
 EOF
 
 cat <<EOF | kubectl apply -f -
-apiVersion: flagger.app/v1alpha3
+apiVersion: flagger.app/v1beta1
 kind: Canary
 metadata:
   name: podinfo
@@ -49,7 +49,7 @@ spec:
   service:
     port: 80
     targetPort: 9898
-  canaryAnalysis:
+  analysis:
     interval: 15s
     threshold: 15
     maxWeight: 50
@@ -145,7 +145,7 @@ echo '✔ Canary promotion test passed'
 
 
 cat <<EOF | kubectl apply -f -
-apiVersion: flagger.app/v1alpha3
+apiVersion: flagger.app/v1beta1
 kind: Canary
 metadata:
   name: podinfo
@@ -160,7 +160,7 @@ spec:
   service:
     port: 80
     targetPort: 9898
-  canaryAnalysis:
+  analysis:
     interval: 15s
     threshold: 15
     maxWeight: 50
@@ -218,7 +218,7 @@ done
 echo '✔ Canary rollback test passed'
 
 cat <<EOF | kubectl apply -f -
-apiVersion: flagger.app/v1alpha3
+apiVersion: flagger.app/v1beta1
 kind: Canary
 metadata:
   name: podinfo
@@ -233,7 +233,7 @@ spec:
   service:
     port: 80
     targetPort: 9898
-  canaryAnalysis:
+  analysis:
     interval: 10s
     threshold: 5
     iterations: 5
diff --git a/test/e2e-gloo-tests.sh b/test/e2e-gloo-tests.sh
index f686a0b1..f4525c72 100755
--- a/test/e2e-gloo-tests.sh
+++ b/test/e2e-gloo-tests.sh
@@ -37,7 +37,7 @@ spec:
 EOF
 
 cat <<EOF | kubectl apply -f -
-apiVersion: flagger.app/v1alpha3
+apiVersion: flagger.app/v1beta1
 kind: Canary
 metadata:
   name: podinfo
@@ -52,7 +52,7 @@ spec:
   service:
     port: 80
     targetPort: 9898
-  canaryAnalysis:
+  analysis:
     interval: 15s
     threshold: 15
     maxWeight: 50
diff --git a/test/e2e-istio.sh b/test/e2e-istio.sh
index b655af2b..1cb04e95 100755
--- a/test/e2e-istio.sh
+++ b/test/e2e-istio.sh
@@ -2,7 +2,7 @@
 
 set -o errexit
 
-ISTIO_VER="1.4.4"
+ISTIO_VER="1.4.5"
 REPO_ROOT=$(git rev-parse --show-toplevel)
 
 echo ">>> Installing Istio ${ISTIO_VER}"
diff --git a/test/e2e-kubernetes-tests-daemonset.sh b/test/e2e-kubernetes-tests-daemonset.sh
index 2b35c3bc..443f4dfb 100755
--- a/test/e2e-kubernetes-tests-daemonset.sh
+++ b/test/e2e-kubernetes-tests-daemonset.sh
@@ -18,7 +18,7 @@ echo '>>> Initialising canary'
 kubectl apply -f ${REPO_ROOT}/test/e2e-daemonset.yaml
 
 cat <<EOF | kubectl apply -f -
-apiVersion: flagger.app/v1alpha3
+apiVersion: flagger.app/v1beta1
 kind: Canary
 metadata:
   name: podinfo
@@ -35,7 +35,7 @@ spec:
     targetPort: 9898
     name: podinfo-svc
     portDiscovery: true
-  canaryAnalysis:
+  analysis:
     interval: 15s
     threshold: 10
     iterations: 5
diff --git a/test/e2e-kubernetes-tests-deployment.sh b/test/e2e-kubernetes-tests-deployment.sh
index 65bde77b..2cd3bed7 100755
--- a/test/e2e-kubernetes-tests-deployment.sh
+++ b/test/e2e-kubernetes-tests-deployment.sh
@@ -18,7 +18,7 @@ echo '>>> Initialising canary'
 kubectl apply -f ${REPO_ROOT}/test/e2e-workload.yaml
 
 cat <<EOF | kubectl apply -f -
-apiVersion: flagger.app/v1alpha3
+apiVersion: flagger.app/v1beta1
 kind: Canary
 metadata:
   name: podinfo
@@ -35,7 +35,7 @@ spec:
     targetPort: 9898
     name: podinfo-svc
     portDiscovery: true
-  canaryAnalysis:
+  analysis:
     interval: 15s
     threshold: 10
     iterations: 5
diff --git a/test/e2e-kubernetes-tests-svc.sh b/test/e2e-kubernetes-tests-svc.sh
index 8a5f8a87..9cc2eb39 100755
--- a/test/e2e-kubernetes-tests-svc.sh
+++ b/test/e2e-kubernetes-tests-svc.sh
@@ -34,7 +34,7 @@ spec:
 EOS
 
 cat <<EOF | kubectl apply -f -
-apiVersion: flagger.app/v1alpha3
+apiVersion: flagger.app/v1beta1
 kind: Canary
 metadata:
   name: podinfo
@@ -48,7 +48,7 @@ spec:
   progressDeadlineSeconds: 60
   service:
     port: 9898
-  canaryAnalysis:
+  analysis:
     interval: 15s
     threshold: 10
     iterations: 5
diff --git a/test/e2e-linkerd-tests.sh b/test/e2e-linkerd-tests.sh
index 1f86dbd3..519f9f58 100755
--- a/test/e2e-linkerd-tests.sh
+++ b/test/e2e-linkerd-tests.sh
@@ -43,7 +43,7 @@ spec:
 EOF
 
 cat <<EOF | kubectl apply -f -
-apiVersion: flagger.app/v1alpha3
+apiVersion: flagger.app/v1beta1
 kind: Canary
 metadata:
   name: podinfo
@@ -58,7 +58,7 @@ spec:
     port: 80
     targetPort: http
     portDiscovery: true
-  canaryAnalysis:
+  analysis:
     interval: 15s
     threshold: 15
     maxWeight: 50
@@ -154,7 +154,7 @@ done
 echo '✔ Canary promotion test passed'
 
 cat <<EOF | kubectl apply -f -
-apiVersion: flagger.app/v1alpha3
+apiVersion: flagger.app/v1beta1
 kind: Canary
 metadata:
   name: podinfo
@@ -168,7 +168,7 @@ spec:
   service:
     port: 80
     targetPort: 9898
-  canaryAnalysis:
+  analysis:
     interval: 15s
     threshold: 3
     maxWeight: 50
diff --git a/test/e2e-nginx-tests.sh b/test/e2e-nginx-tests.sh
index 50129786..3cf04fe9 100755
--- a/test/e2e-nginx-tests.sh
+++ b/test/e2e-nginx-tests.sh
@@ -19,7 +19,7 @@ kubectl apply -f ${REPO_ROOT}/test/e2e-workload.yaml
 kubectl apply -f ${REPO_ROOT}/test/e2e-ingress.yaml
 
 cat <<EOF | kubectl apply -f -
-apiVersion: flagger.app/v1alpha3
+apiVersion: flagger.app/v1beta1
 kind: Canary
 metadata:
   name: podinfo
@@ -37,7 +37,7 @@ spec:
   service:
     port: 80
     targetPort: http
-  canaryAnalysis:
+  analysis:
     interval: 15s
     threshold: 15
     maxWeight: 30
@@ -156,7 +156,7 @@ done
 echo '✔ Canary promotion test passed'
 
 cat <<EOF | kubectl apply -f -
-apiVersion: flagger.app/v1alpha3
+apiVersion: flagger.app/v1beta1
 kind: Canary
 metadata:
   name: podinfo
@@ -174,7 +174,7 @@ spec:
   service:
     port: 80
     targetPort: 9898
-  canaryAnalysis:
+  analysis:
     interval: 10s
     threshold: 5
     iterations: 5
diff --git a/test/e2e-nginx.sh b/test/e2e-nginx.sh
index 29bf1b84..5c33c194 100755
--- a/test/e2e-nginx.sh
+++ b/test/e2e-nginx.sh
@@ -3,7 +3,7 @@
 set -o errexit
 
 REPO_ROOT=$(git rev-parse --show-toplevel)
-NGINX_VERSION=1.26.0
+NGINX_VERSION=1.33.0
 
 echo '>>> Installing NGINX Ingress'
 kubectl create ns ingress-nginx
diff --git a/test/e2e-tests.sh b/test/e2e-tests.sh
index 599a8689..a19ef5ed 100755
--- a/test/e2e-tests.sh
+++ b/test/e2e-tests.sh
@@ -19,7 +19,7 @@ echo '>>> Initialising canary'
 kubectl apply -f ${REPO_ROOT}/test/e2e-workload.yaml
 
 cat <<EOF | kubectl apply -f -
-apiVersion: flagger.app/v1alpha3
+apiVersion: flagger.app/v1beta1
 kind: Canary
 metadata:
   name: podinfo
@@ -39,7 +39,7 @@ spec:
           x-envoy-upstream-rq-timeout-ms: "15000"
           x-envoy-max-retries: "10"
           x-envoy-retry-on: "gateway-error,connect-failure,refused-stream"
-  canaryAnalysis:
+  analysis:
     interval: 15s
     threshold: 15
     maxWeight: 30
@@ -145,7 +145,7 @@ if [[ "$1" = "canary" ]]; then
 fi
 
 cat <<EOF | kubectl apply -f -
-apiVersion: flagger.app/v1alpha3
+apiVersion: flagger.app/v1beta1
 kind: Canary
 metadata:
   name: podinfo
@@ -161,7 +161,7 @@ spec:
     port: 80
     targetPort: 9898
     portName: http-podinfo
-  canaryAnalysis:
+  analysis:
     interval: 10s
     threshold: 5
     iterations: 5
@@ -234,7 +234,7 @@ done
 echo '✔ B/G promotion test passed'
 
 cat <<EOF | kubectl apply -f -
-apiVersion: flagger.app/v1alpha3
+apiVersion: flagger.app/v1beta1
 kind: Canary
 metadata:
   name: podinfo
@@ -250,7 +250,7 @@ spec:
     port: 80
     portName: http-podinfo
     targetPort: http
-  canaryAnalysis:
+  analysis:
     interval: 10s
     threshold: 5
     iterations: 5