Skip to content
31 changes: 11 additions & 20 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/kubernetes-sigs/ingress2gateway

go 1.24.0
go 1.24.4

toolchain go1.24.6

Expand All @@ -9,6 +9,7 @@ require (
github.com/getkin/kin-openapi v0.124.0
github.com/google/go-cmp v0.7.0
github.com/kong/kubernetes-ingress-controller/v2 v2.12.3
github.com/nginx/kubernetes-ingress v1.12.1-0.20250715110806-e53439b1cce7
github.com/olekukonko/tablewriter v0.0.5
github.com/samber/lo v1.39.0
github.com/spf13/cobra v1.9.1
Expand All @@ -26,7 +27,6 @@ require (

require (
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/invopop/yaml v0.2.0 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
Expand All @@ -48,46 +48,37 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/oauth2 v0.27.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.30.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/time v0.9.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/protobuf v1.36.5
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/protobuf v1.36.6
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
istio.io/client-go v1.19.0-alpha.1.0.20231130185426-9f1859c8ff42
k8s.io/klog/v2 v2.130.1
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/kustomize/api v0.20.1 // indirect
sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
72 changes: 24 additions & 48 deletions go.sum

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pkg/i2gw/intermediate/intermediate_representation.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type ProviderSpecificGatewayIR struct {
IngressNginx *IngressNginxGatewayIR
Istio *IstioGatewayIR
Kong *KongGatewayIR
Nginx *NginxGatewayIR
Openapi3 *Openapi3GatewayIR
}

Expand All @@ -80,6 +81,7 @@ type ProviderSpecificHTTPRouteIR struct {
IngressNginx *IngressNginxHTTPRouteIR
Istio *IstioHTTPRouteIR
Kong *KongHTTPRouteIR
Nginx *NginxHTTPRouteIR
Openapi3 *Openapi3HTTPRouteIR
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/i2gw/intermediate/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ func MergeIRs(irs ...IR) (IR, field.ErrorList) {
GatewayClasses: make(map[types.NamespacedName]gatewayv1.GatewayClass),
HTTPRoutes: make(map[types.NamespacedName]HTTPRouteContext),
Services: make(map[types.NamespacedName]ProviderSpecificServiceIR),
GRPCRoutes: make(map[types.NamespacedName]gatewayv1.GRPCRoute),
TLSRoutes: make(map[types.NamespacedName]gatewayv1alpha2.TLSRoute),
TCPRoutes: make(map[types.NamespacedName]gatewayv1alpha2.TCPRoute),
UDPRoutes: make(map[types.NamespacedName]gatewayv1alpha2.UDPRoute),
GRPCRoutes: make(map[types.NamespacedName]gatewayv1.GRPCRoute),
BackendTLSPolicies: make(map[types.NamespacedName]gatewayv1alpha3.BackendTLSPolicy),
ReferenceGrants: make(map[types.NamespacedName]gatewayv1beta1.ReferenceGrant),
}
Expand All @@ -60,10 +60,10 @@ func MergeIRs(irs ...IR) (IR, field.ErrorList) {
maps.Copy(mergedIRs.GatewayClasses, gr.GatewayClasses)
maps.Copy(mergedIRs.HTTPRoutes, gr.HTTPRoutes)
maps.Copy(mergedIRs.Services, gr.Services)
maps.Copy(mergedIRs.GRPCRoutes, gr.GRPCRoutes)
maps.Copy(mergedIRs.TLSRoutes, gr.TLSRoutes)
maps.Copy(mergedIRs.TCPRoutes, gr.TCPRoutes)
maps.Copy(mergedIRs.UDPRoutes, gr.UDPRoutes)
maps.Copy(mergedIRs.GRPCRoutes, gr.GRPCRoutes)
maps.Copy(mergedIRs.BackendTLSPolicies, gr.BackendTLSPolicies)
maps.Copy(mergedIRs.ReferenceGrants, gr.ReferenceGrants)
}
Expand Down
15 changes: 13 additions & 2 deletions pkg/i2gw/providers/nginx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,24 @@ This provider converts [NGINX Ingress Controller](https://github.com/nginx/kuber
## Usage

```bash
# Convert NGINX Ingress Controller resources from cluster
# Convert NGINX Ingress Controller resources from cluster (default namespace)
ingress2gateway print --providers=nginx

# Convert from file
# Convert from cluster with specific namespace
ingress2gateway print --providers=nginx --namespace=production

# Convert from file (all namespaces in file)
ingress2gateway print --providers=nginx --input-file=nginx-ingress.yaml
```

## Requirements

* **Ingress Class**: Only Ingress resources with `ingressClassName: nginx` are processed
* **Namespace**:
- When reading from cluster: defaults to `default` namespace
- When reading from file: processes all namespaces in the file
- Use `--namespace` flag to specify a different namespace for cluster reads

## Gateway API Mapping

| NGINX Annotation | Gateway API Resource |
Expand Down
45 changes: 3 additions & 42 deletions pkg/i2gw/providers/nginx/annotations/header_manipulation.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (

"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/intermediate"
"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/common"
nginxcommon "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/nginx/common"
)

// HeaderManipulationFeature converts header manipulation annotations to HTTPRoute filters
Expand Down Expand Up @@ -87,47 +88,13 @@ func addFilterToHTTPRoute(httpRoute *gatewayv1.HTTPRoute, _ networkingv1.Ingress
// createResponseHeaderModifier creates a ResponseHeaderModifier filter from comma-separated header names
func createResponseHeaderModifier(hideHeaders string) *gatewayv1.HTTPRouteFilter {
headersToRemove := parseCommaSeparatedHeaders(hideHeaders)
if len(headersToRemove) == 0 {
return nil
}

return &gatewayv1.HTTPRouteFilter{
Type: gatewayv1.HTTPRouteFilterResponseHeaderModifier,
ResponseHeaderModifier: &gatewayv1.HTTPHeaderFilter{
Remove: headersToRemove,
},
}
return nginxcommon.CreateResponseHeaderModifier(headersToRemove)
}

// createRequestHeaderModifier creates a RequestHeaderModifier filter from proxy-set-headers annotation
func createRequestHeaderModifier(setHeaders string) *gatewayv1.HTTPRouteFilter {
headers := parseSetHeaders(setHeaders)
if len(headers) == 0 {
return nil
}

var headersToSet []gatewayv1.HTTPHeader
for name, value := range headers {
if value != "" && !strings.Contains(value, "$") {
headersToSet = append(headersToSet, gatewayv1.HTTPHeader{
Name: gatewayv1.HTTPHeaderName(name),
Value: value,
})
}
// Note: Headers with NGINX variables cannot be converted to Gateway API
// as Gateway API doesn't support dynamic header values
}

if len(headersToSet) == 0 {
return nil
}

return &gatewayv1.HTTPRouteFilter{
Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier,
RequestHeaderModifier: &gatewayv1.HTTPHeaderFilter{
Set: headersToSet,
},
}
return nginxcommon.CreateRequestHeaderModifier(headers)
}

// parseCommaSeparatedHeaders parses a comma-separated list of header names
Expand All @@ -143,7 +110,6 @@ func parseSetHeaders(setHeaders string) map[string]string {

for _, part := range parts {
if strings.Contains(part, ":") {
// Format: "Header-Name: value"
kv := strings.SplitN(part, ":", 2)
if len(kv) == 2 {
headerName := strings.TrimSpace(kv[0])
Expand All @@ -153,12 +119,7 @@ func parseSetHeaders(setHeaders string) map[string]string {
}
}
}
// Note: Headers without explicit values (format "$Variable-Name") are skipped
// as Gateway API cannot use NGINX variables like $http_* and headers need values
}

return headers
}

// Note: The patchHTTPRouteHeaderMatching function has been removed as it was incomplete.
// Header matching should be implemented separately if needed for specific NGINX features.
30 changes: 7 additions & 23 deletions pkg/i2gw/providers/nginx/annotations/path_rewrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ import (
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/ptr"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"

"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/intermediate"
"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/common"
nginxcommon "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/nginx/common"
)

// RewriteTargetFeature converts nginx.org/rewrites annotation to URLRewrite filter
Expand Down Expand Up @@ -57,20 +57,13 @@ func RewriteTargetFeature(ingresses []networkingv1.Ingress, _ map[types.Namespac
for _, path := range rule.IngressRule.HTTP.Paths {
serviceName := path.Backend.Service.Name
if rewritePath, hasRewrite := rewriteRules[serviceName]; hasRewrite {
filter := gatewayv1.HTTPRouteFilter{
Type: gatewayv1.HTTPRouteFilterURLRewrite,
URLRewrite: &gatewayv1.HTTPURLRewriteFilter{
Path: &gatewayv1.HTTPPathModifier{
Type: gatewayv1.PrefixMatchHTTPPathModifier,
ReplacePrefixMatch: ptr.To(rewritePath),
},
},
filter := nginxcommon.CreateURLRewriteFilter(rewritePath)
if filter != nil {
if httpRouteContext.HTTPRoute.Spec.Rules[i].Filters == nil {
httpRouteContext.HTTPRoute.Spec.Rules[i].Filters = []gatewayv1.HTTPRouteFilter{}
}
httpRouteContext.HTTPRoute.Spec.Rules[i].Filters = append(httpRouteContext.HTTPRoute.Spec.Rules[i].Filters, *filter)
}

if httpRouteContext.HTTPRoute.Spec.Rules[i].Filters == nil {
httpRouteContext.HTTPRoute.Spec.Rules[i].Filters = []gatewayv1.HTTPRouteFilter{}
}
httpRouteContext.HTTPRoute.Spec.Rules[i].Filters = append(httpRouteContext.HTTPRoute.Spec.Rules[i].Filters, filter)
}
}
}
Expand All @@ -87,34 +80,25 @@ func RewriteTargetFeature(ingresses []networkingv1.Ingress, _ map[types.Namespac
// NIC format: "serviceName=service rewrite=path;serviceName2=service2 rewrite=path2"
func parseRewriteRules(rewriteValue string) map[string]string {
rules := make(map[string]string)

if rewriteValue == "" {
return rules
}

// Split by semicolon for each rule
parts := strings.Split(rewriteValue, ";")

for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}

// Expect format: serviceName=service rewrite=rewrite
serviceIdx := strings.Index(part, "=")
rewriteIdx := strings.Index(part, " rewrite=")
if serviceIdx == -1 || rewriteIdx == -1 || rewriteIdx <= serviceIdx {
continue
}

serviceName := strings.TrimSpace(part[serviceIdx+1 : rewriteIdx])
rewritePath := strings.TrimSpace(part[rewriteIdx+9:])

if serviceName != "" && rewritePath != "" {
rules[serviceName] = rewritePath
}
}

return rules
}
77 changes: 77 additions & 0 deletions pkg/i2gw/providers/nginx/common/filters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
Copyright 2025 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

import (
"strings"

gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
)

// CreateResponseHeaderModifier creates a ResponseHeaderModifier filter from comma-separated header names
func CreateResponseHeaderModifier(headersToRemove []string) *gatewayv1.HTTPRouteFilter {
if len(headersToRemove) == 0 {
return nil
}
return &gatewayv1.HTTPRouteFilter{
Type: gatewayv1.HTTPRouteFilterResponseHeaderModifier,
ResponseHeaderModifier: &gatewayv1.HTTPHeaderFilter{
Remove: headersToRemove,
},
}
}

// CreateRequestHeaderModifier creates a RequestHeaderModifier filter from header map
func CreateRequestHeaderModifier(headersToSet map[string]string) *gatewayv1.HTTPRouteFilter {
if len(headersToSet) == 0 {
return nil
}
var headers []gatewayv1.HTTPHeader
for name, value := range headersToSet {
if value != "" && !strings.Contains(value, "$") {
headers = append(headers, gatewayv1.HTTPHeader{
Name: gatewayv1.HTTPHeaderName(name),
Value: value,
})
}
}
if len(headers) == 0 {
return nil
}
return &gatewayv1.HTTPRouteFilter{
Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier,
RequestHeaderModifier: &gatewayv1.HTTPHeaderFilter{
Set: headers,
},
}
}

// CreateURLRewriteFilter creates a URLRewrite filter with ReplacePrefixMatch
func CreateURLRewriteFilter(rewritePath string) *gatewayv1.HTTPRouteFilter {
if rewritePath == "" {
return nil
}
return &gatewayv1.HTTPRouteFilter{
Type: gatewayv1.HTTPRouteFilterURLRewrite,
URLRewrite: &gatewayv1.HTTPURLRewriteFilter{
Path: &gatewayv1.HTTPPathModifier{
Type: gatewayv1.PrefixMatchHTTPPathModifier,
ReplacePrefixMatch: &rewritePath,
},
},
}
}
Loading