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
3 changes: 3 additions & 0 deletions artifacts/flagger/crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,9 @@ spec:
format: string
type: string
type: array
httpRouteName:
description: The name of the HTTPRoute generated by Flagger for the Gateway API provider. Defaults to the apex service name.
type: string
apex:
description: Metadata to add to the apex service
type: object
Expand Down
3 changes: 3 additions & 0 deletions charts/flagger/crds/crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,9 @@ spec:
format: string
type: string
type: array
httpRouteName:
description: The name of the HTTPRoute generated by Flagger for the Gateway API provider. Defaults to the apex service name.
type: string
apex:
description: Metadata to add to the apex service
type: object
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ require (
k8s.io/client-go v0.34.1
k8s.io/code-generator v0.34.1
k8s.io/klog/v2 v2.130.1
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
knative.dev/serving v0.46.6
)

Expand Down Expand Up @@ -100,6 +99,7 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f // indirect
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
knative.dev/networking v0.0.0-20250902160145-7dad473f6351 // indirect
knative.dev/pkg v0.0.0-20250909011231-077dcf0d00e8 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
Expand Down
3 changes: 3 additions & 0 deletions kustomize/base/flagger/crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,9 @@ spec:
format: string
type: string
type: array
httpRouteName:
description: The name of the HTTPRoute generated by Flagger for the Gateway API provider. Defaults to the apex service name.
type: string
apex:
description: Metadata to add to the apex service
type: object
Expand Down
15 changes: 15 additions & 0 deletions pkg/apis/flagger/v1beta1/canary.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,11 @@ type CanaryService struct {
// +optional
Backends []string `json:"backends,omitempty"`

// HTTPRouteName is the name of the HTTPRoute generated by Flagger
// for the Gateway API provider. Defaults to the apex service name.
// +optional
HTTPRouteName string `json:"httpRouteName,omitempty"`

// Apex is metadata to add to the apex service
// +optional
Apex *CustomMetadata `json:"apex,omitempty"`
Expand Down Expand Up @@ -610,6 +615,16 @@ func (c *Canary) GetServiceNames() (apexName, primaryName, canaryName string) {
return
}

// GetHTTPRouteName returns the name to use for the HTTPRoute resource.
// Defaults to the apex service name if not explicitly set.
func (c *Canary) GetHTTPRouteName() string {
if c.Spec.Service.HTTPRouteName != "" {
return c.Spec.Service.HTTPRouteName
}
apexName, _, _ := c.GetServiceNames()
return apexName
}

// GetProgressDeadlineSeconds returns the progress deadline (default 600s)
func (c *Canary) GetProgressDeadlineSeconds() int {
if c.Spec.ProgressDeadlineSeconds != nil {
Expand Down
45 changes: 14 additions & 31 deletions pkg/router/gateway_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ func (gwr *GatewayAPIRouter) Reconcile(canary *flaggerv1.Canary) error {
return fmt.Errorf("GatewayRefs must be specified when using Gateway API as a provider.")
}

apexSvcName, primarySvcName, canarySvcName := canary.GetServiceNames()
_, primarySvcName, canarySvcName := canary.GetServiceNames()
routeName := canary.GetHTTPRouteName()

hrNamespace := canary.Namespace

Expand Down Expand Up @@ -138,7 +139,7 @@ func (gwr *GatewayAPIRouter) Reconcile(canary *flaggerv1.Canary) error {
}

httpRoute, err := gwr.gatewayAPIClient.GatewayapiV1().HTTPRoutes(hrNamespace).Get(
context.TODO(), apexSvcName, metav1.GetOptions{},
context.TODO(), routeName, metav1.GetOptions{},
)

newMetadata := canary.Spec.Service.Apex
Expand All @@ -156,7 +157,7 @@ func (gwr *GatewayAPIRouter) Reconcile(canary *flaggerv1.Canary) error {
if errors.IsNotFound(err) {
route := &v1.HTTPRoute{
ObjectMeta: metav1.ObjectMeta{
Name: apexSvcName,
Name: routeName,
Namespace: hrNamespace,
Labels: newMetadata.Labels,
Annotations: newMetadata.Annotations,
Expand All @@ -178,13 +179,13 @@ func (gwr *GatewayAPIRouter) Reconcile(canary *flaggerv1.Canary) error {
Create(context.TODO(), route, metav1.CreateOptions{})

if err != nil {
return fmt.Errorf("HTTPRoute %s.%s create error: %w", apexSvcName, hrNamespace, err)
return fmt.Errorf("HTTPRoute %s.%s create error: %w", routeName, hrNamespace, err)
}
gwr.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
Infof("HTTPRoute %s.%s created", route.GetName(), hrNamespace)
return nil
} else if err != nil {
return fmt.Errorf("HTTPRoute %s.%s get error: %w", apexSvcName, hrNamespace, err)
return fmt.Errorf("HTTPRoute %s.%s get error: %w", routeName, hrNamespace, err)
}

ignoreCmpOptions := []cmp.Option{
Expand Down Expand Up @@ -266,33 +267,14 @@ func (gwr *GatewayAPIRouter) GetRoutes(canary *flaggerv1.Canary) (
mirrored bool,
err error,
) {
apexSvcName, primarySvcName, canarySvcName := canary.GetServiceNames()
_, primarySvcName, canarySvcName := canary.GetServiceNames()
routeName := canary.GetHTTPRouteName()
hrNamespace := canary.Namespace
httpRoute, err := gwr.gatewayAPIClient.GatewayapiV1().HTTPRoutes(hrNamespace).Get(context.TODO(), apexSvcName, metav1.GetOptions{})
httpRoute, err := gwr.gatewayAPIClient.GatewayapiV1().HTTPRoutes(hrNamespace).Get(context.TODO(), routeName, metav1.GetOptions{})
if err != nil {
err = fmt.Errorf("HTTPRoute %s.%s get error: %w", apexSvcName, hrNamespace, err)
err = fmt.Errorf("HTTPRoute %s.%s get error: %w", routeName, hrNamespace, err)
return
}

currentGeneration := httpRoute.GetGeneration()
for _, parentRef := range httpRoute.Spec.CommonRouteSpec.ParentRefs {
for _, parentStatus := range httpRoute.Status.Parents {
if !reflect.DeepEqual(parentStatus.ParentRef, parentRef) {
continue
}

for _, condition := range parentStatus.Conditions {
if condition.Type == string(v1.RouteConditionAccepted) && (condition.Status != metav1.ConditionTrue || condition.ObservedGeneration < currentGeneration) {
err = fmt.Errorf(
"HTTPRoute %s.%s parent %s is not ready (status: %s, observed generation: %d, current generation: %d)",
apexSvcName, hrNamespace, parentRef.Name, string(condition.Status), condition.ObservedGeneration, currentGeneration,
)
return 0, 0, false, err
}
}
}
}

var weightedRule *v1.HTTPRouteRule
for _, rule := range httpRoute.Spec.Rules {
// If session affinity is enabled, then we are only interested in the rule
Expand Down Expand Up @@ -346,11 +328,12 @@ func (gwr *GatewayAPIRouter) SetRoutes(
) error {
pWeight := int32(primaryWeight)
cWeight := int32(canaryWeight)
apexSvcName, primarySvcName, canarySvcName := canary.GetServiceNames()
_, primarySvcName, canarySvcName := canary.GetServiceNames()
routeName := canary.GetHTTPRouteName()
hrNamespace := canary.Namespace
httpRoute, err := gwr.gatewayAPIClient.GatewayapiV1().HTTPRoutes(hrNamespace).Get(context.TODO(), apexSvcName, metav1.GetOptions{})
httpRoute, err := gwr.gatewayAPIClient.GatewayapiV1().HTTPRoutes(hrNamespace).Get(context.TODO(), routeName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("HTTPRoute %s.%s get error: %w", apexSvcName, hrNamespace, err)
return fmt.Errorf("HTTPRoute %s.%s get error: %w", routeName, hrNamespace, err)
}
hrClone := httpRoute.DeepCopy()
hostNames := []v1.Hostname{}
Expand Down
143 changes: 39 additions & 104 deletions pkg/router/gateway_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"

flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
v1 "github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1"
Expand Down Expand Up @@ -74,6 +73,45 @@ func TestGatewayAPIRouter_Reconcile(t *testing.T) {
assert.Equal(t, httpRoute.Annotations["foo"], "bar")
}

func TestGatewayAPIRouter_ReconcileCustomHTTPRouteName(t *testing.T) {
canary := newTestGatewayAPICanary()
canary.Spec.Service.HTTPRouteName = "my-custom-route"
mocks := newFixture(canary)
router := &GatewayAPIRouter{
gatewayAPIClient: mocks.meshClient,
kubeClient: mocks.kubeClient,
logger: mocks.logger,
}

err := router.Reconcile(canary)
require.NoError(t, err)

// HTTPRoute should be created with the custom name
httpRoute, err := router.gatewayAPIClient.GatewayapiV1().HTTPRoutes("default").Get(context.TODO(), "my-custom-route", metav1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, "my-custom-route", httpRoute.Name)

// BackendRefs should still point at the primary and canary services
backendRefs := httpRoute.Spec.Rules[0].BackendRefs
require.Equal(t, 2, len(backendRefs))
assert.Equal(t, v1.ObjectName("podinfo-primary"), backendRefs[0].Name)
assert.Equal(t, v1.ObjectName("podinfo-canary"), backendRefs[1].Name)

// GetRoutes and SetRoutes should work with the custom name
primaryWeight, canaryWeight, _, err := router.GetRoutes(canary)
require.NoError(t, err)
assert.Equal(t, 100, primaryWeight)
assert.Equal(t, 0, canaryWeight)

err = router.SetRoutes(canary, 50, 50, false)
require.NoError(t, err)

primaryWeight, canaryWeight, _, err = router.GetRoutes(canary)
require.NoError(t, err)
assert.Equal(t, 50, primaryWeight)
assert.Equal(t, 50, canaryWeight)
}

func TestGatewayAPIRouter_Routes(t *testing.T) {
canary := newTestGatewayAPICanary()
mocks := newFixture(canary)
Expand Down Expand Up @@ -604,106 +642,3 @@ func TestGatewayAPIRouter_makeFilters_CORS(t *testing.T) {
// Assert MaxAge (24h = 86400 seconds)
assert.Equal(t, int32(86400), corsFilter.CORS.MaxAge)
}

func TestGatewayAPIRouter_GetRoutes(t *testing.T) {
canary := newTestGatewayAPICanary()
mocks := newFixture(canary)
router := &GatewayAPIRouter{
gatewayAPIClient: mocks.meshClient,
kubeClient: mocks.kubeClient,
logger: mocks.logger,
}

httpRoute := &v1.HTTPRoute{
ObjectMeta: metav1.ObjectMeta{
Name: "podinfo",
Generation: 1,
},
Spec: v1.HTTPRouteSpec{
Rules: []v1.HTTPRouteRule{
{
BackendRefs: []v1.HTTPBackendRef{
{
BackendRef: v1.BackendRef{
BackendObjectReference: v1.BackendObjectReference{
Name: "podinfo-canary",
},
Weight: ptr.To(int32(10)),
},
},
{
BackendRef: v1.BackendRef{
BackendObjectReference: v1.BackendObjectReference{
Name: "podinfo-primary",
},
Weight: ptr.To(int32(90)),
},
},
},
},
},
CommonRouteSpec: v1.CommonRouteSpec{
ParentRefs: []v1.ParentReference{
{
Name: "podinfo",
},
},
},
},
}
httpRoute, err := router.gatewayAPIClient.GatewayapiV1().HTTPRoutes("default").Create(context.TODO(), httpRoute, metav1.CreateOptions{})
require.NoError(t, err)

t.Run("httproute generation", func(t *testing.T) {
httpRoute.ObjectMeta.Generation = 5
httpRoute.Status.Parents = []v1.RouteParentStatus{
{
ParentRef: v1.ParentReference{
Name: "podinfo",
SectionName: ptr.To(v1.SectionName("https")),
},
Conditions: []metav1.Condition{
{
Type: string(v1.RouteConditionAccepted),
Status: metav1.ConditionTrue,
ObservedGeneration: 1,
},
},
},
{
ParentRef: v1.ParentReference{
Name: "podinfo",
},
Conditions: []metav1.Condition{
{
Type: string(v1.RouteConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: 4,
},
},
},
}
httpRoute, err := router.gatewayAPIClient.GatewayapiV1().HTTPRoutes("default").Update(context.TODO(), httpRoute, metav1.UpdateOptions{})
require.NoError(t, err)

_, _, _, err = router.GetRoutes(canary)
require.Error(t, err)

httpRoute.Status.Parents[1].Conditions[0].ObservedGeneration = 5
_, err = router.gatewayAPIClient.GatewayapiV1().HTTPRoutes("default").Update(context.TODO(), httpRoute, metav1.UpdateOptions{})
require.NoError(t, err)

_, _, _, err = router.GetRoutes(canary)
require.Error(t, err)

httpRoute.Status.Parents[1].Conditions[0].Status = metav1.ConditionTrue
_, err = router.gatewayAPIClient.GatewayapiV1().HTTPRoutes("default").Update(context.TODO(), httpRoute, metav1.UpdateOptions{})
require.NoError(t, err)

primaryWeight, canaryWeight, mirrored, err := router.GetRoutes(canary)
require.NoError(t, err)
assert.Equal(t, 90, primaryWeight)
assert.Equal(t, 10, canaryWeight)
assert.False(t, mirrored)
})
}
Loading