diff --git a/pkg/controller/events_test.go b/pkg/controller/events_test.go new file mode 100644 index 00000000..10b3c783 --- /dev/null +++ b/pkg/controller/events_test.go @@ -0,0 +1,239 @@ +package controller + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "sync" + "testing" + + flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestWebhooks(t *testing.T) { + notReady := flaggerv1.CanaryWebhookPayload{ + Metadata: map[string]string{ + "eventMessage": "podinfo-primary.default not ready: waiting for rollout to finish: 0 out of 1 new replicas have been updated", + }, + } + metricsInitialized := flaggerv1.CanaryWebhookPayload{ + Metadata: map[string]string{ + "eventMessage": "all the metrics providers are available!", + }, + } + + tc := []struct { + name string + expectedEvents map[string][]flaggerv1.CanaryWebhookPayload + test func(t *testing.T, mock fixture) + }{ + { + name: "skip-analysis", + expectedEvents: map[string][]flaggerv1.CanaryWebhookPayload{ + "event": { + metricsInitialized, + notReady, + metricsInitialized, + {Phase: flaggerv1.CanaryPhaseInitialized, Metadata: map[string]string{"eventMessage": "Initialization done! podinfo.default"}}, + {Phase: flaggerv1.CanaryPhaseInitialized, Metadata: map[string]string{"eventMessage": "Confirm-rollout check confirm-rollout-webhook passed"}}, + {Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": "New revision detected! Scaling up podinfo.default"}}, + {Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": "Copying podinfo.default template spec to podinfo-primary.default"}}, + {Phase: flaggerv1.CanaryPhaseSucceeded, Metadata: map[string]string{"eventMessage": "Promotion completed! Canary analysis was skipped for podinfo.default"}}, + }, + "confirm-rollout": { + {Phase: flaggerv1.CanaryPhaseInitialized, Metadata: map[string]string{"eventMessage": ""}}, + }, + }, + test: func(t *testing.T, mocks fixture) { + mocks.ctrl.advanceCanary("podinfo", "default") + mocks.makePrimaryReady(t) + mocks.ctrl.advanceCanary("podinfo", "default") + + // enable skip + cd, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(t.Context(), "podinfo", metav1.GetOptions{}) + require.NoError(t, err) + cd.Spec.SkipAnalysis = true + _, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Update(t.Context(), cd, metav1.UpdateOptions{}) + require.NoError(t, err) + + // update + dep2 := newDeploymentTestDeploymentV2() + _, err = mocks.kubeClient.AppsV1().Deployments("default").Update(t.Context(), dep2, metav1.UpdateOptions{}) + require.NoError(t, err) + + // detect changes + mocks.ctrl.advanceCanary("podinfo", "default") + mocks.makeCanaryReady(t) + + // advance + mocks.ctrl.advanceCanary("podinfo", "default") + }, + }, + { + name: "canary", + expectedEvents: map[string][]flaggerv1.CanaryWebhookPayload{ + "event": { + metricsInitialized, + notReady, + metricsInitialized, + {Phase: flaggerv1.CanaryPhaseInitialized, Metadata: map[string]string{"eventMessage": "Initialization done! podinfo.default"}}, + {Phase: flaggerv1.CanaryPhaseInitialized, Metadata: map[string]string{"eventMessage": "Confirm-rollout check confirm-rollout-webhook passed"}}, + {Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": "New revision detected! Scaling up podinfo.default"}}, + {Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": "Starting canary analysis for podinfo.default"}}, + {Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": "Pre-rollout check pre-rollout-webhook passed"}}, + {Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": "Confirm-traffic-increase check confirm-traffic-increase-webhook passed"}}, + {Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": "Advance podinfo.default canary weight 100"}}, + {Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": "Confirm-traffic-increase check confirm-traffic-increase-webhook passed"}}, + {Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": "Confirm-promotion check confirm-promotion-webhook passed"}}, + {Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": "Copying podinfo.default template spec to podinfo-primary.default"}}, + {Phase: flaggerv1.CanaryPhasePromoting, Metadata: map[string]string{"eventMessage": "Advance podinfo.default primary weight 50"}}, + {Phase: flaggerv1.CanaryPhasePromoting, Metadata: map[string]string{"eventMessage": "Advance podinfo.default primary weight 100"}}, + {Phase: flaggerv1.CanaryPhaseSucceeded, Metadata: map[string]string{"eventMessage": "Post-rollout check post-rollout-webhook passed"}}, + {Phase: flaggerv1.CanaryPhaseSucceeded, Metadata: map[string]string{"eventMessage": "Promotion completed! Scaling down podinfo.default"}}, + }, + "confirm-rollout": { + {Phase: flaggerv1.CanaryPhaseInitialized, Metadata: map[string]string{"eventMessage": ""}}, + }, + "confirm-promotion": { + {Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": ""}}, + }, + "confirm-traffic-increase": { + {Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": ""}}, + {Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": ""}}, + }, + "pre-rollout": { + {Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": ""}}, + }, + "post-rollout": { + {Phase: flaggerv1.CanaryPhaseSucceeded, Metadata: map[string]string{"eventMessage": ""}}, + }, + "rollout": { + {Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": ""}}, + }, + }, + test: func(t *testing.T, mocks fixture) { + mocks.canary.Spec.Analysis.Interval = "1m" + mocks.canary.Spec.Analysis.StepWeight = 100 + mocks.canary.Spec.Analysis.StepWeightPromotion = 50 + _, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Update(t.Context(), mocks.canary, metav1.UpdateOptions{}) + require.NoError(t, err) + + // initializing + mocks.ctrl.advanceCanary("podinfo", "default") + + // make primary ready + mocks.makePrimaryReady(t) + + // initialized + mocks.ctrl.advanceCanary("podinfo", "default") + require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseInitialized)) + + // update + dep2 := newDeploymentTestDeploymentV2() + _, err = mocks.kubeClient.AppsV1().Deployments("default").Update(t.Context(), dep2, metav1.UpdateOptions{}) + require.NoError(t, err) + + // detect changes + mocks.ctrl.advanceCanary("podinfo", "default") + require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseProgressing)) + mocks.makeCanaryReady(t) + + // progressing + mocks.ctrl.advanceCanary("podinfo", "default") + require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseProgressing)) + + // start promotion + mocks.ctrl.advanceCanary("podinfo", "default") + require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhasePromoting)) + + // end promotion + mocks.ctrl.advanceCanary("podinfo", "default") + require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhasePromoting)) + + // finalising + mocks.ctrl.advanceCanary("podinfo", "default") + require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseFinalising)) + + // succeeded + mocks.ctrl.advanceCanary("podinfo", "default") + require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseSucceeded)) + }, + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + var mu sync.Mutex + events := map[string][]flaggerv1.CanaryWebhookPayload{} + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mu.Lock() + defer mu.Unlock() + + var payload flaggerv1.CanaryWebhookPayload + err := json.NewDecoder(r.Body).Decode(&payload) + require.NoError(t, err) + + // only record the event attributes we're comparing + eventType := r.URL.Path[1:] + events[eventType] = append(events[eventType], flaggerv1.CanaryWebhookPayload{ + Phase: payload.Phase, + Metadata: map[string]string{ + "eventMessage": payload.Metadata["eventMessage"], + }, + }) + + w.WriteHeader(http.StatusOK) + })) + + defer server.Close() + + c := newDeploymentTestCanary() + c.Spec.Analysis.Webhooks = []flaggerv1.CanaryWebhook{ + { + Type: flaggerv1.EventHook, + Name: "event-webhook", + URL: server.URL + "/event", + }, + { + Type: flaggerv1.PreRolloutHook, + Name: "pre-rollout-webhook", + URL: server.URL + "/pre-rollout", + }, + { + Type: flaggerv1.RolloutHook, + Name: "rollout-webhook", + URL: server.URL + "/rollout", + }, + { + Type: flaggerv1.ConfirmPromotionHook, + Name: "confirm-promotion-webhook", + URL: server.URL + "/confirm-promotion", + }, + { + Type: flaggerv1.ConfirmRolloutHook, + Name: "confirm-rollout-webhook", + URL: server.URL + "/confirm-rollout", + }, + { + Type: flaggerv1.PostRolloutHook, + Name: "post-rollout-webhook", + URL: server.URL + "/post-rollout", + }, + { + Type: flaggerv1.ConfirmTrafficIncreaseHook, + Name: "confirm-traffic-increase-webhook", + URL: server.URL + "/confirm-traffic-increase", + }, + } + + mocks := newDeploymentFixture(c) + tt.test(t, mocks) + + assert.Equal(t, tt.expectedEvents, events) + }) + } +} diff --git a/pkg/controller/scheduler.go b/pkg/controller/scheduler.go index 957e3508..12a94bef 100644 --- a/pkg/controller/scheduler.go +++ b/pkg/controller/scheduler.go @@ -400,6 +400,7 @@ func (c *Controller) advanceCanary(name string, namespace string) { c.recordEventWarningf(cd, "%v", err) return } + c.recorder.SetStatus(cd, flaggerv1.CanaryPhaseSucceeded) c.recorder.IncSuccesses(metrics.CanaryMetricLabels{ Name: cd.Spec.TargetRef.Name, @@ -407,10 +408,14 @@ func (c *Controller) advanceCanary(name string, namespace string) { 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.", + + canarySucceeded := cd.DeepCopy() + canarySucceeded.Status.Phase = flaggerv1.CanaryPhaseSucceeded + c.runPostRolloutHooks(canarySucceeded, flaggerv1.CanaryPhaseSucceeded) + c.recordEventInfof(canarySucceeded, "Promotion completed! Scaling down %s.%s", cd.Spec.TargetRef.Name, cd.Namespace) + c.alert(canarySucceeded, "Canary analysis completed successfully, promotion finished.", false, flaggerv1.SeverityInfo) + return } @@ -825,9 +830,12 @@ func (c *Controller) shouldSkipAnalysis(canary *flaggerv1.Canary, canaryControll DeploymentStrategy: canary.DeploymentStrategy(), AnalysisStatus: metrics.AnalysisStatusSkipped, }) - c.recordEventInfof(canary, "Promotion completed! Canary analysis was skipped for %s.%s", + + canarySucceeded := canary.DeepCopy() + canarySucceeded.Status.Phase = flaggerv1.CanaryPhaseSucceeded + c.recordEventInfof(canarySucceeded, "Promotion completed! Canary analysis was skipped for %s.%s", canary.Spec.TargetRef.Name, canary.Namespace) - c.alert(canary, "Canary analysis was skipped, promotion finished.", + c.alert(canarySucceeded, "Canary analysis was skipped, promotion finished.", false, flaggerv1.SeverityInfo) return true