mirror of
https://github.com/fluxcd/flagger.git
synced 2026-02-14 18:10:00 +00:00
Add Canary Webhook checksum.
This adds a new Checksum field to the canary webhook body, which is a hash of the LastAppliedSpec and TrackedConfigs. This can be used to identify the rollout of a specific configuration, and differentiate between webhooks being sent for different configuration and deployment versions. Signed-off-by: Kevin McDermott <kevin@weave.works>
This commit is contained in:
committed by
Kevin McDermott
parent
788e692e90
commit
56b6339f8c
@@ -83,16 +83,19 @@ Webhook payload (HTTP POST):
|
||||
|
||||
```javascript
|
||||
{
|
||||
"name": "podinfo",
|
||||
"namespace": "test",
|
||||
"phase": "Progressing",
|
||||
"metadata": {
|
||||
"test": "all",
|
||||
"token": "16688eb5e9f289f1991c"
|
||||
}
|
||||
"name": "podinfo",
|
||||
"namespace": "test",
|
||||
"phase": "Progressing",
|
||||
"checksum": "85d557f47b",
|
||||
"metadata": {
|
||||
"test": "all",
|
||||
"token": "16688eb5e9f289f1991c"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The checksum field is hashed from the TrackedConfigs and LastAppliedSpec of the Canary, it can be used to identify a Canary for a specific configuration of the deployed resources.
|
||||
|
||||
Response status codes:
|
||||
|
||||
* 200-202 - advance canary by increasing the traffic weight
|
||||
@@ -107,6 +110,7 @@ Event payload (HTTP POST):
|
||||
"name": "string (canary name)",
|
||||
"namespace": "string (canary namespace)",
|
||||
"phase": "string (canary phase)",
|
||||
"checksum": "string (canary checksum"),
|
||||
"metadata": {
|
||||
"eventMessage": "string (canary event message)",
|
||||
"eventType": "string (canary event type)",
|
||||
|
||||
@@ -403,6 +403,11 @@ type CanaryWebhookPayload struct {
|
||||
// Phase of the canary analysis
|
||||
Phase CanaryPhase `json:"phase"`
|
||||
|
||||
// Hash from the TrackedConfigs and LastAppliedSpec of the Canary.
|
||||
// Can be used to identify a Canary for a specific configuration of the
|
||||
// deployed resources.
|
||||
Checksum string `json:"checksum"`
|
||||
|
||||
// Metadata (key-value pairs) for this webhook
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func hasSpecChanged(cd *flaggerv1.Canary, spec interface{}) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
newHash := computeHash(spec)
|
||||
newHash := ComputeHash(spec)
|
||||
|
||||
// do not trigger a canary deployment on manual rollback
|
||||
if cd.Status.LastPromotedSpec == newHash {
|
||||
@@ -48,10 +48,10 @@ func hasSpecChanged(cd *flaggerv1.Canary, spec interface{}) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// computeHash returns a hash value calculated from a spec using the spew library
|
||||
// ComputeHash returns a hash value calculated from a spec using the spew library
|
||||
// which follows pointers and prints actual values of the nested objects
|
||||
// ensuring the hash does not change when a pointer changes.
|
||||
func computeHash(spec interface{}) string {
|
||||
func ComputeHash(spec interface{}) string {
|
||||
hasher := fnv.New32a()
|
||||
printer := spew.ConfigState{
|
||||
Indent: " ",
|
||||
|
||||
@@ -30,7 +30,7 @@ import (
|
||||
)
|
||||
|
||||
func syncCanaryStatus(flaggerClient clientset.Interface, cd *flaggerv1.Canary, status flaggerv1.CanaryStatus, canaryResource interface{}, setAll func(cdCopy *flaggerv1.Canary)) error {
|
||||
hash := computeHash(canaryResource)
|
||||
hash := ComputeHash(canaryResource)
|
||||
|
||||
firstTry := true
|
||||
name, ns := cd.GetName(), cd.GetNamespace()
|
||||
|
||||
@@ -727,7 +727,7 @@ func (c *Controller) runAnalysis(canary *flaggerv1.Canary) bool {
|
||||
// run external checks
|
||||
for _, webhook := range canary.GetAnalysis().Webhooks {
|
||||
if webhook.Type == "" || webhook.Type == flaggerv1.RolloutHook {
|
||||
err := CallWebhook(canary.Name, canary.Namespace, flaggerv1.CanaryPhaseProgressing, webhook)
|
||||
err := CallWebhook(*canary, flaggerv1.CanaryPhaseProgressing, webhook)
|
||||
if err != nil {
|
||||
c.recordEventWarningf(canary, "Halt %s.%s advancement external check %s failed %v",
|
||||
canary.Name, canary.Namespace, webhook.Name, err)
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
func (c *Controller) runConfirmTrafficIncreaseHooks(canary *flaggerv1.Canary) bool {
|
||||
for _, webhook := range canary.GetAnalysis().Webhooks {
|
||||
if webhook.Type == flaggerv1.ConfirmTrafficIncreaseHook {
|
||||
err := CallWebhook(canary.Name, canary.Namespace, flaggerv1.CanaryPhaseProgressing, webhook)
|
||||
err := CallWebhook(*canary, flaggerv1.CanaryPhaseProgressing, webhook)
|
||||
if err != nil {
|
||||
c.recordEventWarningf(canary, "Halt %s.%s advancement waiting for traffic increase approval %s",
|
||||
canary.Name, canary.Namespace, webhook.Name)
|
||||
@@ -44,7 +44,7 @@ func (c *Controller) runConfirmTrafficIncreaseHooks(canary *flaggerv1.Canary) bo
|
||||
func (c *Controller) runConfirmRolloutHooks(canary *flaggerv1.Canary, canaryController canary.Controller) bool {
|
||||
for _, webhook := range canary.GetAnalysis().Webhooks {
|
||||
if webhook.Type == flaggerv1.ConfirmRolloutHook {
|
||||
err := CallWebhook(canary.Name, canary.Namespace, canary.Status.Phase, webhook)
|
||||
err := CallWebhook(*canary, canary.Status.Phase, webhook)
|
||||
if err != nil {
|
||||
if canary.Status.Phase != flaggerv1.CanaryPhaseWaiting {
|
||||
if err := canaryController.SetStatusPhase(canary, flaggerv1.CanaryPhaseWaiting); err != nil {
|
||||
@@ -67,7 +67,7 @@ func (c *Controller) runConfirmRolloutHooks(canary *flaggerv1.Canary, canaryCont
|
||||
func (c *Controller) runConfirmPromotionHooks(canary *flaggerv1.Canary, canaryController canary.Controller) bool {
|
||||
for _, webhook := range canary.GetAnalysis().Webhooks {
|
||||
if webhook.Type == flaggerv1.ConfirmPromotionHook {
|
||||
err := CallWebhook(canary.Name, canary.Namespace, flaggerv1.CanaryPhaseProgressing, webhook)
|
||||
err := CallWebhook(*canary, flaggerv1.CanaryPhaseProgressing, webhook)
|
||||
if err != nil {
|
||||
if canary.Status.Phase != flaggerv1.CanaryPhaseWaitingPromotion {
|
||||
if err := canaryController.SetStatusPhase(canary, flaggerv1.CanaryPhaseWaitingPromotion); err != nil {
|
||||
@@ -95,7 +95,7 @@ func (c *Controller) runConfirmPromotionHooks(canary *flaggerv1.Canary, canaryCo
|
||||
func (c *Controller) runPreRolloutHooks(canary *flaggerv1.Canary) bool {
|
||||
for _, webhook := range canary.GetAnalysis().Webhooks {
|
||||
if webhook.Type == flaggerv1.PreRolloutHook {
|
||||
err := CallWebhook(canary.Name, canary.Namespace, flaggerv1.CanaryPhaseProgressing, webhook)
|
||||
err := CallWebhook(*canary, flaggerv1.CanaryPhaseProgressing, webhook)
|
||||
if err != nil {
|
||||
c.recordEventWarningf(canary, "Halt %s.%s advancement pre-rollout check %s failed %v",
|
||||
canary.Name, canary.Namespace, webhook.Name, err)
|
||||
@@ -111,7 +111,7 @@ func (c *Controller) runPreRolloutHooks(canary *flaggerv1.Canary) bool {
|
||||
func (c *Controller) runPostRolloutHooks(canary *flaggerv1.Canary, phase flaggerv1.CanaryPhase) bool {
|
||||
for _, webhook := range canary.GetAnalysis().Webhooks {
|
||||
if webhook.Type == flaggerv1.PostRolloutHook {
|
||||
err := CallWebhook(canary.Name, canary.Namespace, phase, webhook)
|
||||
err := CallWebhook(*canary, phase, webhook)
|
||||
if err != nil {
|
||||
c.recordEventWarningf(canary, "Post-rollout hook %s failed %v", webhook.Name, err)
|
||||
return false
|
||||
@@ -126,7 +126,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.GetAnalysis().Webhooks {
|
||||
if webhook.Type == flaggerv1.RollbackHook {
|
||||
err := CallWebhook(canary.Name, canary.Namespace, phase, webhook)
|
||||
err := CallWebhook(*canary, phase, webhook)
|
||||
if err != nil {
|
||||
c.recordEventInfof(canary, "Rollback hook %s not signaling a rollback", webhook.Name)
|
||||
} else {
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"time"
|
||||
|
||||
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
|
||||
"github.com/fluxcd/flagger/pkg/canary"
|
||||
)
|
||||
|
||||
func callWebhook(webhook string, payload interface{}, timeout string) error {
|
||||
@@ -81,11 +82,12 @@ func callWebhook(webhook string, payload interface{}, timeout string) error {
|
||||
|
||||
// CallWebhook does a HTTP POST to an external service and
|
||||
// returns an error if the response status code is non-2xx
|
||||
func CallWebhook(name string, namespace string, phase flaggerv1.CanaryPhase, w flaggerv1.CanaryWebhook) error {
|
||||
func CallWebhook(canary flaggerv1.Canary, phase flaggerv1.CanaryPhase, w flaggerv1.CanaryWebhook) error {
|
||||
payload := flaggerv1.CanaryWebhookPayload{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Name: canary.Name,
|
||||
Namespace: canary.Namespace,
|
||||
Phase: phase,
|
||||
Checksum: canaryChecksum(canary),
|
||||
}
|
||||
|
||||
if w.Metadata != nil {
|
||||
@@ -106,6 +108,7 @@ func CallEventWebhook(r *flaggerv1.Canary, w flaggerv1.CanaryWebhook, message, e
|
||||
Name: r.Name,
|
||||
Namespace: r.Namespace,
|
||||
Phase: r.Status.Phase,
|
||||
Checksum: canaryChecksum(*r),
|
||||
Metadata: map[string]string{
|
||||
"eventMessage": message,
|
||||
"eventType": eventtype,
|
||||
@@ -123,3 +126,15 @@ func CallEventWebhook(r *flaggerv1.Canary, w flaggerv1.CanaryWebhook, message, e
|
||||
}
|
||||
return callWebhook(w.URL, payload, "5s")
|
||||
}
|
||||
|
||||
func canaryChecksum(c flaggerv1.Canary) string {
|
||||
canaryFields := struct {
|
||||
TrackedConfigs *map[string]string
|
||||
LastAppliedSpec string
|
||||
}{
|
||||
c.Status.TrackedConfigs,
|
||||
c.Status.LastAppliedSpec,
|
||||
}
|
||||
|
||||
return canary.ComputeHash(canaryFields)
|
||||
}
|
||||
|
||||
@@ -26,25 +26,71 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
path string
|
||||
body map[string]any
|
||||
header http.Header
|
||||
}
|
||||
|
||||
func TestCallWebhook(t *testing.T) {
|
||||
requests := []testRequest{}
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
|
||||
var body map[string]any
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
|
||||
|
||||
requests = append(requests, testRequest{
|
||||
path: r.URL.Path,
|
||||
body: body,
|
||||
})
|
||||
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
hook := flaggerv1.CanaryWebhook{
|
||||
Name: "validation",
|
||||
URL: ts.URL,
|
||||
URL: ts.URL + "/testing",
|
||||
Timeout: "10s",
|
||||
Metadata: &map[string]string{"key1": "val1"},
|
||||
}
|
||||
|
||||
err := CallWebhook("podinfo", v1.NamespaceDefault, flaggerv1.CanaryPhaseProgressing, hook)
|
||||
canary := flaggerv1.Canary{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "podinfo", Namespace: corev1.NamespaceDefault,
|
||||
},
|
||||
Status: flaggerv1.CanaryStatus{
|
||||
TrackedConfigs: &map[string]string{
|
||||
"test-config-map": "484637c76acaa7c6",
|
||||
},
|
||||
LastAppliedSpec: "4cb74184589",
|
||||
},
|
||||
}
|
||||
err := CallWebhook(canary,
|
||||
flaggerv1.CanaryPhaseProgressing, hook)
|
||||
require.NoError(t, err)
|
||||
|
||||
want := []testRequest{
|
||||
{
|
||||
path: "/testing",
|
||||
body: map[string]any{
|
||||
"name": "podinfo",
|
||||
"namespace": "default",
|
||||
"phase": "Progressing",
|
||||
"checksum": canaryChecksum(canary),
|
||||
"metadata": map[string]any{
|
||||
"key1": "val1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.EqualValues(t, want, requests)
|
||||
}
|
||||
|
||||
func TestCallWebhook_StatusCode(t *testing.T) {
|
||||
@@ -57,16 +103,30 @@ func TestCallWebhook_StatusCode(t *testing.T) {
|
||||
URL: ts.URL,
|
||||
}
|
||||
|
||||
err := CallWebhook("podinfo", v1.NamespaceDefault, flaggerv1.CanaryPhaseProgressing, hook)
|
||||
err := CallWebhook(
|
||||
flaggerv1.Canary{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "podinfo", Namespace: corev1.NamespaceDefault}},
|
||||
flaggerv1.CanaryPhaseProgressing, hook)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCallEventWebhook(t *testing.T) {
|
||||
canaryName := "podinfo"
|
||||
canaryNamespace := v1.NamespaceDefault
|
||||
canaryNamespace := corev1.NamespaceDefault
|
||||
canaryMessage := fmt.Sprintf("Starting canary analysis for %s.%s", canaryName, canaryNamespace)
|
||||
canaryEventType := corev1.EventTypeNormal
|
||||
|
||||
canary := &flaggerv1.Canary{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: canaryName,
|
||||
Namespace: canaryNamespace,
|
||||
},
|
||||
Status: flaggerv1.CanaryStatus{
|
||||
Phase: flaggerv1.CanaryPhaseProgressing,
|
||||
},
|
||||
}
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
d := json.NewDecoder(r.Body)
|
||||
|
||||
@@ -98,22 +158,19 @@ func TestCallEventWebhook(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
if payload.Checksum != canaryChecksum(*canary) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
hook := flaggerv1.CanaryWebhook{
|
||||
Name: "event",
|
||||
URL: ts.URL,
|
||||
}
|
||||
canary := &flaggerv1.Canary{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: canaryName,
|
||||
Namespace: canaryNamespace,
|
||||
},
|
||||
Status: flaggerv1.CanaryStatus{
|
||||
Phase: flaggerv1.CanaryPhaseProgressing,
|
||||
},
|
||||
}
|
||||
|
||||
err := CallEventWebhook(canary, hook, canaryMessage, canaryEventType)
|
||||
require.NoError(t, err)
|
||||
@@ -121,7 +178,7 @@ func TestCallEventWebhook(t *testing.T) {
|
||||
|
||||
func TestCallEventWebhookStatusCode(t *testing.T) {
|
||||
canaryName := "podinfo"
|
||||
canaryNamespace := v1.NamespaceDefault
|
||||
canaryNamespace := corev1.NamespaceDefault
|
||||
canaryMessage := fmt.Sprintf("Starting canary analysis for %s.%s", canaryName, canaryNamespace)
|
||||
canaryEventType := corev1.EventTypeNormal
|
||||
|
||||
@@ -134,7 +191,7 @@ func TestCallEventWebhookStatusCode(t *testing.T) {
|
||||
URL: ts.URL,
|
||||
}
|
||||
canary := &flaggerv1.Canary{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: canaryName,
|
||||
Namespace: canaryNamespace,
|
||||
},
|
||||
@@ -146,3 +203,63 @@ func TestCallEventWebhookStatusCode(t *testing.T) {
|
||||
err := CallEventWebhook(canary, hook, canaryMessage, canaryEventType)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCanaryChecksum(t *testing.T) {
|
||||
canary1 := flaggerv1.Canary{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "podinfo", Namespace: corev1.NamespaceDefault},
|
||||
|
||||
Status: flaggerv1.CanaryStatus{
|
||||
TrackedConfigs: &map[string]string{
|
||||
"test-config-map": "484637c76acaa7c6",
|
||||
},
|
||||
LastAppliedSpec: "5f56684589",
|
||||
},
|
||||
}
|
||||
canary1sum := canaryChecksum(canary1)
|
||||
|
||||
canary2 := flaggerv1.Canary{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "podinfo", Namespace: corev1.NamespaceDefault},
|
||||
|
||||
Status: flaggerv1.CanaryStatus{
|
||||
TrackedConfigs: &map[string]string{
|
||||
"test-config-map": "9fc3a7c76acaa7c6",
|
||||
},
|
||||
LastAppliedSpec: "5f56684589",
|
||||
},
|
||||
}
|
||||
canary2sum := canaryChecksum(canary2)
|
||||
|
||||
canary3 := flaggerv1.Canary{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "podinfo",
|
||||
Namespace: corev1.NamespaceDefault,
|
||||
},
|
||||
Status: flaggerv1.CanaryStatus{
|
||||
TrackedConfigs: &map[string]string{
|
||||
"test-config-map": "484637c76acaa7c6",
|
||||
},
|
||||
LastAppliedSpec: "4cb74184589",
|
||||
},
|
||||
}
|
||||
canary3sum := canaryChecksum(canary3)
|
||||
|
||||
canary4 := flaggerv1.Canary{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "podinfo",
|
||||
Namespace: corev1.NamespaceDefault,
|
||||
},
|
||||
Status: flaggerv1.CanaryStatus{
|
||||
TrackedConfigs: nil,
|
||||
LastAppliedSpec: "4cb74184589",
|
||||
},
|
||||
}
|
||||
canary4sum := canaryChecksum(canary4)
|
||||
|
||||
require.Equal(t, canary1sum, canaryChecksum(canary1))
|
||||
require.NotEqual(t, canary1sum, canary2sum)
|
||||
require.NotEqual(t, canary2sum, canary3sum)
|
||||
require.NotEqual(t, canary3sum, canary1sum)
|
||||
require.NotEqual(t, canary4sum, canary1sum)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user