Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ type GatewayContext struct {

type HTTPRouteContext struct {
gatewayv1.HTTPRoute
// RequestTimeoutsByRule holds desired HTTPRoute.spec.rules[i].timeouts.request values by rule index.
RequestTimeouts map[int]*gatewayv1.Duration
}

type GatewayClassContext struct {
Expand Down
41 changes: 41 additions & 0 deletions pkg/i2gw/emitters/common_emitter/emitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package common_emitter
import (
emitterir "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/emitter_intermediate"
"k8s.io/apimachinery/pkg/util/validation/field"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
)

type Emitter struct{}
Expand All @@ -31,5 +32,45 @@ func NewEmitter() *Emitter {
// This ALWAYS runs after providers and before provider-specific emitters.
// TODO: Implement common logic such as filtering by maturity status and/or individual features.
func (e *Emitter) Emit(ir emitterir.EmitterIR) (emitterir.EmitterIR, field.ErrorList) {
var errs field.ErrorList

for key, httpRouteContext := range ir.HTTPRoutes {
errs = append(errs, applyHTTPRouteRequestTimeouts(&httpRouteContext)...)
ir.HTTPRoutes[key] = httpRouteContext
}

if len(errs) > 0 {
return ir, errs
}
return ir, nil
}

func applyHTTPRouteRequestTimeouts(httpRouteContext *emitterir.HTTPRouteContext) field.ErrorList {
if httpRouteContext.RequestTimeouts == nil {
return nil
}

var errs field.ErrorList
for ruleIdx, d := range httpRouteContext.RequestTimeouts {
if d == nil {
continue
}
if ruleIdx < 0 || ruleIdx >= len(httpRouteContext.Spec.Rules) {
errs = append(errs, field.Invalid(
field.NewPath("httpRoute", "spec", "rules").Index(ruleIdx),
ruleIdx,
"rule index out of range",
))
continue
}

rule := &httpRouteContext.Spec.Rules[ruleIdx]
if rule.Timeouts == nil {
rule.Timeouts = &gatewayv1.HTTPRouteTimeouts{}
}
rule.Timeouts.Request = d
}

httpRouteContext.RequestTimeouts = nil
return errs
}
93 changes: 93 additions & 0 deletions pkg/i2gw/emitters/common_emitter/emitter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
Copyright 2026 The Kubernetes 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 common_emitter

import (
"testing"

emitterir "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/emitter_intermediate"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
)

func TestApplyHTTPRouteRequestTimeouts(t *testing.T) {
d := gatewayv1.Duration("10s")

testCases := []struct {
name string
ctx emitterir.HTTPRouteContext
wantSet bool
wantErr bool
}{
{
name: "sets request timeout",
ctx: emitterir.HTTPRouteContext{
HTTPRoute: gatewayv1.HTTPRoute{Spec: gatewayv1.HTTPRouteSpec{Rules: []gatewayv1.HTTPRouteRule{{}}}},
RequestTimeouts: map[int]*gatewayv1.Duration{0: &d},
},
wantSet: true,
},
{
name: "nil duration ignored",
ctx: emitterir.HTTPRouteContext{
HTTPRoute: gatewayv1.HTTPRoute{Spec: gatewayv1.HTTPRouteSpec{Rules: []gatewayv1.HTTPRouteRule{{}}}},
RequestTimeouts: map[int]*gatewayv1.Duration{0: nil},
},
wantSet: false,
},
{
name: "out of range rule index",
ctx: emitterir.HTTPRouteContext{
HTTPRoute: gatewayv1.HTTPRoute{Spec: gatewayv1.HTTPRouteSpec{Rules: []gatewayv1.HTTPRouteRule{{}}}},
RequestTimeouts: map[int]*gatewayv1.Duration{1: &d},
},
wantErr: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
errList := applyHTTPRouteRequestTimeouts(&tc.ctx)
if tc.ctx.RequestTimeouts != nil {
t.Fatalf("expected RequestTimeouts to be nil after apply")
}
if tc.wantErr {
if len(errList) == 0 {
t.Fatalf("expected error")
}
return
}
if len(errList) > 0 {
t.Fatalf("expected no errors, got %v", errList)
}

got := tc.ctx.Spec.Rules[0].Timeouts
if tc.wantSet {
if got == nil || got.Request == nil {
t.Fatalf("expected request timeout to be set")
}
if *got.Request != d {
t.Fatalf("expected %v, got %v", d, *got.Request)
}
return
}

if got != nil {
t.Fatalf("expected timeouts to be nil, got %v", got)
}
})
}
}
38 changes: 38 additions & 0 deletions pkg/i2gw/providers/ingressnginx/annotations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
Copyright 2023 The Kubernetes 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 ingressnginx

const (
// Canary annotations
CanaryAnnotation = "nginx.ingress.kubernetes.io/canary"
CanaryWeightAnnotation = "nginx.ingress.kubernetes.io/canary-weight"
CanaryWeightTotalAnnotation = "nginx.ingress.kubernetes.io/canary-weight-total"

// Rewrite annotations
RewriteTargetAnnotation = "nginx.ingress.kubernetes.io/rewrite-target"

// Header annotations
XForwardedPrefixAnnotation = "nginx.ingress.kubernetes.io/x-forwarded-prefix"
UpstreamVhostAnnotation = "nginx.ingress.kubernetes.io/upstream-vhost"
ConnectionProxyHeaderAnnotation = "nginx.ingress.kubernetes.io/connection-proxy-header"
CustomHeadersAnnotation = "nginx.ingress.kubernetes.io/custom-headers"

// Timeout annotations
ProxyConnectTimeoutAnnotation = "nginx.ingress.kubernetes.io/proxy-connect-timeout"
ProxySendTimeoutAnnotation = "nginx.ingress.kubernetes.io/proxy-send-timeout"
ProxyReadTimeoutAnnotation = "nginx.ingress.kubernetes.io/proxy-read-timeout"
)
12 changes: 3 additions & 9 deletions pkg/i2gw/providers/ingressnginx/canary.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,6 @@ import (
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
)

const (
canaryAnnotation = "nginx.ingress.kubernetes.io/canary"
canaryWeightAnnotation = "nginx.ingress.kubernetes.io/canary-weight"
canaryWeightTotalAnnotation = "nginx.ingress.kubernetes.io/canary-weight-total"
)

// canaryConfig holds the parsed canary configuration from a single Ingress
type canaryConfig struct {
weight int32
Expand All @@ -48,7 +42,7 @@ func parseCanaryConfig(ingress *networkingv1.Ingress) (canaryConfig, error) {
weightTotal: 100, // default
}

if weight := ingress.Annotations[canaryWeightAnnotation]; weight != "" {
if weight := ingress.Annotations[CanaryWeightAnnotation]; weight != "" {
w, err := strconv.ParseInt(weight, 10, 32)
if err != nil {
return config, fmt.Errorf("invalid canary-weight annotation %q: %w", weight, err)
Expand All @@ -59,7 +53,7 @@ func parseCanaryConfig(ingress *networkingv1.Ingress) (canaryConfig, error) {
config.weight = int32(w)
}

if total := ingress.Annotations[canaryWeightTotalAnnotation]; total != "" {
if total := ingress.Annotations[CanaryWeightTotalAnnotation]; total != "" {
wt, err := strconv.ParseInt(total, 10, 32)
if err != nil {
return config, fmt.Errorf("invalid canary-weight-total annotation %q: %w", total, err)
Expand Down Expand Up @@ -112,7 +106,7 @@ func canaryFeature(ingresses []networkingv1.Ingress, _ map[types.NamespacedName]

backendRef := &httpRouteContext.HTTPRoute.Spec.Rules[ruleIdx].BackendRefs[backendIdx]

if source.Ingress.Annotations[canaryAnnotation] == "true" {
if source.Ingress.Annotations[CanaryAnnotation] == "true" {
if canaryBackend != nil {
errList = append(errList, field.Invalid(
field.NewPath("httproute", httpRouteContext.HTTPRoute.Name, "spec", "rules").Index(ruleIdx).Child("backendRefs"),
Expand Down
1 change: 1 addition & 0 deletions pkg/i2gw/providers/ingressnginx/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func newResourcesToIRConverter() *resourcesToIRConverter {
return &resourcesToIRConverter{
featureParsers: []i2gw.FeatureParser{
canaryFeature,
headerModifierFeature,
},
}
}
Expand Down
108 changes: 108 additions & 0 deletions pkg/i2gw/providers/ingressnginx/headers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
Copyright 2023 The Kubernetes 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 ingressnginx

import (
"fmt"

"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/notifications"
providerir "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/provider_intermediate"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/validation/field"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
)

func headerModifierFeature(_ []networkingv1.Ingress, _ map[types.NamespacedName]map[string]int32, ir *providerir.ProviderIR) field.ErrorList {
for _, httpRouteContext := range ir.HTTPRoutes {
for i := range httpRouteContext.HTTPRoute.Spec.Rules {
if i >= len(httpRouteContext.RuleBackendSources) {
continue
}
sources := httpRouteContext.RuleBackendSources[i]

ingress := getNonCanaryIngress(sources)
if ingress == nil {
panic("No non-canary ingress found")
}

headersToSet := make(map[string]string)

_, hasRewriteTarget := ingress.Annotations[RewriteTargetAnnotation]

// 1. x-forwarded-prefix
// This annotation only works if rewrite-target is also present.
// TODO: X-Forwarded-Prefix is complex because it depends on rewrite-target.
// Deferring this to a future PR.
if val, ok := ingress.Annotations[XForwardedPrefixAnnotation]; ok && val != "" && hasRewriteTarget {
headersToSet["X-Forwarded-Prefix"] = val
}

// 2. upstream-vhost -> Host header
if val, ok := ingress.Annotations[UpstreamVhostAnnotation]; ok && val != "" {
headersToSet["Host"] = val
}

// 3. connection-proxy-header -> Connection header
if val, ok := ingress.Annotations[ConnectionProxyHeaderAnnotation]; ok && val != "" {
headersToSet["Connection"] = val
}

// 4. custom-headers -> Warn unsupported
// TODO: implement custom-headers annotation.
if _, ok := ingress.Annotations[CustomHeadersAnnotation]; ok {
notify(notifications.WarningNotification, fmt.Sprintf("Ingress %s/%s uses '%s' which is not supported.", ingress.Namespace, ingress.Name, CustomHeadersAnnotation), &httpRouteContext.HTTPRoute)
}

if len(headersToSet) > 0 {
applyHeaderModifiers(&httpRouteContext.HTTPRoute, i, headersToSet)
}
}
}
return nil
}

func applyHeaderModifiers(httpRoute *gatewayv1.HTTPRoute, ruleIndex int, headersToSet map[string]string) {
// Find existing RequestHeaderModifier filter or create new one
var filter *gatewayv1.HTTPRouteFilter
for j, f := range httpRoute.Spec.Rules[ruleIndex].Filters {
if f.Type == gatewayv1.HTTPRouteFilterRequestHeaderModifier && f.RequestHeaderModifier != nil {
filter = &httpRoute.Spec.Rules[ruleIndex].Filters[j]
break
}
}

if filter == nil {
f := gatewayv1.HTTPRouteFilter{
Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier,
RequestHeaderModifier: &gatewayv1.HTTPHeaderFilter{
Set: []gatewayv1.HTTPHeader{},
},
}
httpRoute.Spec.Rules[ruleIndex].Filters = append(httpRoute.Spec.Rules[ruleIndex].Filters, f)
filter = &httpRoute.Spec.Rules[ruleIndex].Filters[len(httpRoute.Spec.Rules[ruleIndex].Filters)-1]
}

for name, value := range headersToSet {
// Used standard append as suggested in PR review
filter.RequestHeaderModifier.Set = append(filter.RequestHeaderModifier.Set, gatewayv1.HTTPHeader{
Name: gatewayv1.HTTPHeaderName(name),
Value: value,
})
notify(notifications.InfoNotification, fmt.Sprintf("Applied header modifier %s: %s to rule %d of route %s/%s", name, value, ruleIndex, httpRoute.Namespace, httpRoute.Name), httpRoute)
}
}
Loading