Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion pkg/router/gateway_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package router

import (
"context"
"encoding/json"
"fmt"
"reflect"
"slices"
Expand Down Expand Up @@ -241,6 +242,24 @@ func (gwr *GatewayAPIRouter) Reconcile(canary *flaggerv1.Canary) error {
hrClone.Spec = httpRouteSpec
hrClone.ObjectMeta.Annotations = mergedAnnotations
hrClone.ObjectMeta.Labels = newMetadata.Labels

// If the kubectl annotation is present it will be used to restore the HTTPRoute on Finalize,
// so there is no need to duplicate the original spec. Otherwise, store it so that it can be
// restored once the canary is deleted with revertOnDeletion enabled.
if _, ok := hrClone.Annotations[kubectlAnnotation]; !ok && specDiff != "" {
if _, ok := hrClone.Annotations[configAnnotation]; !ok {
b, err := json.Marshal(httpRoute.Spec)
if err != nil {
gwr.logger.Warnf("Unable to marshal HTTPRoute %s for orig-configuration annotation", httpRoute.Name)
} else {
if hrClone.ObjectMeta.Annotations == nil {
hrClone.ObjectMeta.Annotations = make(map[string]string)
}
hrClone.ObjectMeta.Annotations[configAnnotation] = string(b)
}
}
}

_, err := gwr.gatewayAPIClient.GatewayapiV1().HTTPRoutes(hrNamespace).
Update(context.TODO(), hrClone, metav1.UpdateOptions{})
if err != nil {
Expand Down Expand Up @@ -443,7 +462,44 @@ func (gwr *GatewayAPIRouter) SetRoutes(
return nil
}

func (gwr *GatewayAPIRouter) Finalize(_ *flaggerv1.Canary) error {
// Finalize restores the HTTPRoute to the state it was in before Flagger took it over,
// so that traffic keeps flowing to the target service after the canary is deleted.
func (gwr *GatewayAPIRouter) Finalize(canary *flaggerv1.Canary) error {
apexSvcName, _, _ := canary.GetServiceNames()
hrNamespace := canary.Namespace

httpRoute, err := gwr.gatewayAPIClient.GatewayapiV1().HTTPRoutes(hrNamespace).Get(
context.TODO(), apexSvcName, metav1.GetOptions{},
)
if err != nil {
return fmt.Errorf("HTTPRoute %s.%s get query error: %w", apexSvcName, hrNamespace, err)
}

var storedSpec v1.HTTPRouteSpec
if a, ok := httpRoute.Annotations[kubectlAnnotation]; ok {
var storedHTTPRoute v1.HTTPRoute
if err := json.Unmarshal([]byte(a), &storedHTTPRoute); err != nil {
return fmt.Errorf("HTTPRoute %s.%s failed to unmarshal annotation %s",
apexSvcName, hrNamespace, kubectlAnnotation)
}
storedSpec = storedHTTPRoute.Spec
} else if a, ok := httpRoute.Annotations[configAnnotation]; ok {
if err := json.Unmarshal([]byte(a), &storedSpec); err != nil {
return fmt.Errorf("HTTPRoute %s.%s failed to unmarshal annotation %s",
apexSvcName, hrNamespace, configAnnotation)
}
} else {
gwr.logger.Warnf("HTTPRoute %s.%s original configuration not found, unable to revert", apexSvcName, hrNamespace)
return nil
}

clone := httpRoute.DeepCopy()
clone.Spec = storedSpec

_, err = gwr.gatewayAPIClient.GatewayapiV1().HTTPRoutes(hrNamespace).Update(context.TODO(), clone, metav1.UpdateOptions{})
if err != nil {
return fmt.Errorf("HTTPRoute %s.%s update error: %w", apexSvcName, hrNamespace, err)
}
return nil
}

Expand Down
103 changes: 103 additions & 0 deletions pkg/router/gateway_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package router

import (
"context"
"encoding/json"
"fmt"
"strings"
"testing"
Expand Down Expand Up @@ -708,3 +709,105 @@ func TestGatewayAPIRouter_GetRoutes(t *testing.T) {
assert.False(t, mirrored)
})
}

func TestGatewayAPIRouter_Finalize(t *testing.T) {
canary := newTestGatewayAPICanary()

originalSpec := v1.HTTPRouteSpec{
Hostnames: []v1.Hostname{"podinfo.example.com"},
Rules: []v1.HTTPRouteRule{
{
BackendRefs: []v1.HTTPBackendRef{
{
BackendRef: v1.BackendRef{
BackendObjectReference: v1.BackendObjectReference{
Name: "podinfo",
},
},
},
},
},
},
}

t.Run("errors when the HTTPRoute is not found", func(t *testing.T) {
mocks := newFixture(canary)
router := &GatewayAPIRouter{
gatewayAPIClient: mocks.meshClient,
kubeClient: mocks.kubeClient,
logger: mocks.logger,
}

err := router.Finalize(canary)
require.Error(t, err)
})

t.Run("does not error when no original configuration is stored", func(t *testing.T) {
mocks := newFixture(canary)
router := &GatewayAPIRouter{
gatewayAPIClient: mocks.meshClient,
kubeClient: mocks.kubeClient,
logger: mocks.logger,
}
require.NoError(t, router.Reconcile(canary))

require.NoError(t, router.Finalize(canary))
})

t.Run("restores the spec stored in the flagger config annotation", func(t *testing.T) {
mocks := newFixture(canary)
router := &GatewayAPIRouter{
gatewayAPIClient: mocks.meshClient,
kubeClient: mocks.kubeClient,
logger: mocks.logger,
}
require.NoError(t, router.Reconcile(canary))

httpRoute, err := router.gatewayAPIClient.GatewayapiV1().HTTPRoutes(canary.Namespace).
Get(context.TODO(), canary.Name, metav1.GetOptions{})
require.NoError(t, err)

b, err := json.Marshal(originalSpec)
require.NoError(t, err)
httpRoute.Annotations = map[string]string{configAnnotation: string(b)}
_, err = router.gatewayAPIClient.GatewayapiV1().HTTPRoutes(canary.Namespace).
Update(context.TODO(), httpRoute, metav1.UpdateOptions{})
require.NoError(t, err)

require.NoError(t, router.Finalize(canary))

httpRoute, err = router.gatewayAPIClient.GatewayapiV1().HTTPRoutes(canary.Namespace).
Get(context.TODO(), canary.Name, metav1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, originalSpec, httpRoute.Spec)
})

t.Run("restores the spec embedded in the kubectl annotation", func(t *testing.T) {
mocks := newFixture(canary)
router := &GatewayAPIRouter{
gatewayAPIClient: mocks.meshClient,
kubeClient: mocks.kubeClient,
logger: mocks.logger,
}
require.NoError(t, router.Reconcile(canary))

httpRoute, err := router.gatewayAPIClient.GatewayapiV1().HTTPRoutes(canary.Namespace).
Get(context.TODO(), canary.Name, metav1.GetOptions{})
require.NoError(t, err)

stored := v1.HTTPRoute{Spec: originalSpec}
b, err := json.Marshal(stored)
require.NoError(t, err)
httpRoute.Annotations = map[string]string{kubectlAnnotation: string(b)}
_, err = router.gatewayAPIClient.GatewayapiV1().HTTPRoutes(canary.Namespace).
Update(context.TODO(), httpRoute, metav1.UpdateOptions{})
require.NoError(t, err)

require.NoError(t, router.Finalize(canary))

httpRoute, err = router.gatewayAPIClient.GatewayapiV1().HTTPRoutes(canary.Namespace).
Get(context.TODO(), canary.Name, metav1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, originalSpec, httpRoute.Spec)
})
}