/* 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 controller import ( "encoding/json" "net/http" "net/http/httptest" "sync" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1" ) 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) }) } }