diff --git a/go.mod b/go.mod index bb47eed6..b800ced7 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/kubernetes-sigs/ingress2gateway -go 1.24.0 +go 1.24.4 toolchain go1.24.6 @@ -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 @@ -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 @@ -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 ) diff --git a/go.sum b/go.sum index 1b9abe35..c83abaec 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,6 @@ github.com/GoogleCloudPlatform/gke-gateway-api v1.3.0 h1:4WjH6dFtnezCFiYlbmq0SBF github.com/GoogleCloudPlatform/gke-gateway-api v1.3.0/go.mod h1:IFDp1XhE20jjqWG3o2ocYoz33nCH6HC4rJ6Hdag4y1M= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -23,10 +21,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M= github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= -github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= -github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= @@ -43,8 +39,6 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= -github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -54,8 +48,6 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgY github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= @@ -74,8 +66,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -89,10 +81,10 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= -github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= -github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nginx/kubernetes-ingress v1.12.1-0.20250715110806-e53439b1cce7 h1:IRZ/IlegsumYSoxvsy2EH4ePRzxZ5SY9EQziRclysq8= +github.com/nginx/kubernetes-ingress v1.12.1-0.20250715110806-e53439b1cce7/go.mod h1:PGfIkpxlBpQW5l4AdQlBPLuaWPvP9QRN5Py6Jva1a8g= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= @@ -102,8 +94,6 @@ github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= -github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -113,8 +103,8 @@ github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/ github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= @@ -124,8 +114,6 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= @@ -134,19 +122,14 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= -github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= @@ -166,43 +149,41 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -210,7 +191,6 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -242,10 +222,6 @@ sigs.k8s.io/gateway-api v1.1.0 h1:DsLDXCi6jR+Xz8/xd0Z1PYl2Pn0TyaFMOPPZIj4inDM= sigs.k8s.io/gateway-api v1.1.0/go.mod h1:ZH4lHrL2sDi0FHZ9jjneb8kKnGzFWyrTya35sWUTrRs= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= -sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= -sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= -sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= diff --git a/pkg/i2gw/intermediate/intermediate_representation.go b/pkg/i2gw/intermediate/intermediate_representation.go index 9928ec13..2c856668 100644 --- a/pkg/i2gw/intermediate/intermediate_representation.go +++ b/pkg/i2gw/intermediate/intermediate_representation.go @@ -60,6 +60,7 @@ type ProviderSpecificGatewayIR struct { IngressNginx *IngressNginxGatewayIR Istio *IstioGatewayIR Kong *KongGatewayIR + Nginx *NginxGatewayIR Openapi3 *Openapi3GatewayIR } @@ -80,6 +81,7 @@ type ProviderSpecificHTTPRouteIR struct { IngressNginx *IngressNginxHTTPRouteIR Istio *IstioHTTPRouteIR Kong *KongHTTPRouteIR + Nginx *NginxHTTPRouteIR Openapi3 *Openapi3HTTPRouteIR } diff --git a/pkg/i2gw/intermediate/utils.go b/pkg/i2gw/intermediate/utils.go index 1c1e5961..c6f6eddd 100644 --- a/pkg/i2gw/intermediate/utils.go +++ b/pkg/i2gw/intermediate/utils.go @@ -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), } @@ -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) } diff --git a/pkg/i2gw/providers/nginx/README.md b/pkg/i2gw/providers/nginx/README.md index 21e9a51c..1098b192 100644 --- a/pkg/i2gw/providers/nginx/README.md +++ b/pkg/i2gw/providers/nginx/README.md @@ -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 | diff --git a/pkg/i2gw/providers/nginx/annotations/header_manipulation.go b/pkg/i2gw/providers/nginx/annotations/header_manipulation.go index 97367563..d6a74021 100644 --- a/pkg/i2gw/providers/nginx/annotations/header_manipulation.go +++ b/pkg/i2gw/providers/nginx/annotations/header_manipulation.go @@ -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 @@ -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 @@ -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]) @@ -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. diff --git a/pkg/i2gw/providers/nginx/annotations/path_rewrite.go b/pkg/i2gw/providers/nginx/annotations/path_rewrite.go index 48902ee0..bf9aa64a 100644 --- a/pkg/i2gw/providers/nginx/annotations/path_rewrite.go +++ b/pkg/i2gw/providers/nginx/annotations/path_rewrite.go @@ -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 @@ -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) } } } @@ -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 } diff --git a/pkg/i2gw/providers/nginx/common/filters.go b/pkg/i2gw/providers/nginx/common/filters.go new file mode 100644 index 00000000..2a678978 --- /dev/null +++ b/pkg/i2gw/providers/nginx/common/filters.go @@ -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, + }, + }, + } +} diff --git a/pkg/i2gw/providers/nginx/common/types.go b/pkg/i2gw/providers/nginx/common/types.go new file mode 100644 index 00000000..65cb4e06 --- /dev/null +++ b/pkg/i2gw/providers/nginx/common/types.go @@ -0,0 +1,23 @@ +/* +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 + +const ( + CoreGroup = "" + SecretKind = "Secret" + ServiceKind = "Service" +) diff --git a/pkg/i2gw/providers/nginx/converter.go b/pkg/i2gw/providers/nginx/converter.go index 33b91f42..59c536c2 100644 --- a/pkg/i2gw/providers/nginx/converter.go +++ b/pkg/i2gw/providers/nginx/converter.go @@ -24,6 +24,7 @@ import ( "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/intermediate" "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/common" "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/nginx/annotations" + "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/nginx/crds" ) type resourcesToIRConverter struct { @@ -61,6 +62,32 @@ func (c *resourcesToIRConverter) convert(storage *storage) (intermediate.IR, fie return intermediate.IR{}, errorList } + // Convert all NGINX CRDs (VirtualServer, VirtualServerRoute, TransportServer) to IR + crdIR, crdNotifications, errs := crds.ToGatewayIR( + storage.VirtualServers, + storage.VirtualServerRoutes, + storage.TransportServers, + storage.GlobalConfiguration, + ) + if len(errs) > 0 { + errorList = append(errorList, errs...) + } + + // Log CRD conversion notifications + for _, notification := range crdNotifications { + notify(notification.Type, notification.Message) + } + + if len(errorList) > 0 { + return intermediate.IR{}, errorList + } + + // Merge CRD IR with Ingress IR + ir, errs = intermediate.MergeIRs(ir, crdIR) + if len(errs) > 0 { + return intermediate.IR{}, errs + } + for _, parseFeatureFunc := range c.featureParsers { errs := parseFeatureFunc(ingressList, storage.ServicePorts, &ir) errorList = append(errorList, errs...) diff --git a/pkg/i2gw/providers/nginx/crd_reader.go b/pkg/i2gw/providers/nginx/crd_reader.go new file mode 100644 index 00000000..ca4d1637 --- /dev/null +++ b/pkg/i2gw/providers/nginx/crd_reader.go @@ -0,0 +1,83 @@ +/* +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 nginx + +import ( + "bytes" + "context" + "fmt" + "os" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/common" +) + +// genericReadFromCluster reads CRDs of type T from the cluster using the given GVK. +func genericReadFromCluster[T any](ctx context.Context, c client.Client, namespace string, gvk schema.GroupVersionKind, newObj func() *T) ([]T, error) { + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(gvk) + + if err := c.List(ctx, list); err != nil { + return nil, fmt.Errorf("failed to list %s: %w", gvk.GroupKind().String(), err) + } + + var items []T + for _, u := range list.Items { + if namespace != "" && u.GetNamespace() != namespace { + continue + } + obj := newObj() + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), obj); err != nil { + return nil, fmt.Errorf("failed to parse %s object %s/%s: %w", gvk.Kind, u.GetNamespace(), u.GetName(), err) + } + items = append(items, *obj) + } + return items, nil +} + +// genericReadFromFile reads CRDs of type T from a YAML file using the given GVK. +func genericReadFromFile[T any](filename string, namespace string, gvk schema.GroupVersionKind, newObj func() *T) ([]T, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to read file %v: %w", filename, err) + } + + reader := bytes.NewReader(data) + objs, err := common.ExtractObjectsFromReader(reader, namespace) + if err != nil { + return nil, fmt.Errorf("failed to extract objects: %w", err) + } + + var items []T + for _, u := range objs { + if namespace != "" && u.GetNamespace() != namespace { + continue + } + if !u.GroupVersionKind().Empty() && u.GroupVersionKind() == gvk { + obj := newObj() + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), obj); err != nil { + return nil, fmt.Errorf("failed to parse %s object %s/%s: %w", gvk.Kind, u.GetNamespace(), u.GetName(), err) + } + items = append(items, *obj) + } + } + return items, nil +} diff --git a/pkg/i2gw/providers/nginx/crds/conversion_main.go b/pkg/i2gw/providers/nginx/crds/conversion_main.go new file mode 100644 index 00000000..041c4f86 --- /dev/null +++ b/pkg/i2gw/providers/nginx/crds/conversion_main.go @@ -0,0 +1,124 @@ +/* +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 crds + +import ( + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation/field" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gatewayv1alpha3 "sigs.k8s.io/gateway-api/apis/v1alpha3" + + "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/intermediate" + "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/notifications" + nginxv1 "github.com/nginx/kubernetes-ingress/pkg/apis/configuration/v1" +) + +// ToGatewayIR converts nginx VirtualServer, VirtualServerRoute, and TransportServer CRDs to Gateway API resources +// This function creates one shared Gateway per namespace that handles both Layer 7 and Layer 4 traffic +func ToGatewayIR( + virtualServers []nginxv1.VirtualServer, + _ []nginxv1.VirtualServerRoute, + transportServers []nginxv1.TransportServer, + globalConfiguration *nginxv1.GlobalConfiguration) ( + partial intermediate.IR, + notificationList []notifications.Notification, + errs field.ErrorList, +) { + notificationList = make([]notifications.Notification, 0) + + var validVirtualServers []nginxv1.VirtualServer + for _, vs := range virtualServers { + if vs.Spec.Host == "" { + addNotification(¬ificationList, notifications.WarningNotification, + "VirtualServer has no host specified, skipping", &vs) + continue + } + validVirtualServers = append(validVirtualServers, vs) + } + + // Check if we have any resources to process + if len(validVirtualServers) == 0 && len(transportServers) == 0 { + return intermediate.IR{}, notificationList, errs + } + + // Group resources by namespace + namespaceVSMap := make(map[string][]nginxv1.VirtualServer) + for _, vs := range validVirtualServers { + namespaceVSMap[vs.Namespace] = append(namespaceVSMap[vs.Namespace], vs) + } + + namespaceTSMap := make(map[string][]nginxv1.TransportServer) + for _, ts := range transportServers { + namespaceTSMap[ts.Namespace] = append(namespaceTSMap[ts.Namespace], ts) + } + + // Initialize result maps + gatewayMap := make(map[types.NamespacedName]intermediate.GatewayContext) + httpRouteMap := make(map[types.NamespacedName]intermediate.HTTPRouteContext) + backendTLSPoliciesMap := make(map[types.NamespacedName]gatewayv1alpha3.BackendTLSPolicy) + grpcRouteMap := make(map[types.NamespacedName]gatewayv1.GRPCRoute) + tcpRouteMap := make(map[types.NamespacedName]gatewayv1alpha2.TCPRoute) + tlsRouteMap := make(map[types.NamespacedName]gatewayv1alpha2.TLSRoute) + udpRouteMap := make(map[types.NamespacedName]gatewayv1alpha2.UDPRoute) + + // Build a listener map + listenerMap := make(map[string]gatewayv1.Listener) + if globalConfiguration != nil { + for _, l := range globalConfiguration.Spec.Listeners { + listenerMap[l.Name] = gatewayv1.Listener{ + Name: gatewayv1.SectionName(l.Name), + Port: gatewayv1.PortNumber(l.Port), + Protocol: gatewayv1.ProtocolType(l.Protocol), + } + } + } + + // Get all namespaces that have either VirtualServers or TransportServers + allNamespaces := make(map[string]bool) + for namespace := range namespaceVSMap { + allNamespaces[namespace] = true + } + for namespace := range namespaceTSMap { + allNamespaces[namespace] = true + } + + for namespace := range allNamespaces { + vsListForNamespace := namespaceVSMap[namespace] // May be empty slice + tsListForNamespace := namespaceTSMap[namespace] // May be empty slice + + // Create shared gateway for both VirtualServers and TransportServers + gatewayFactory := NewNamespaceGatewayFactory(namespace, vsListForNamespace, tsListForNamespace, ¬ificationList, listenerMap) + gateways, _ := gatewayFactory.CreateNamespaceGateway() + + for gatewayKey, gateway := range gateways { + gatewayMap[gatewayKey] = gateway + } + + // TODO: VirtualServer and TransportServer route conversion will be added in subsequent PRs + } + + return intermediate.IR{ + Gateways: gatewayMap, + HTTPRoutes: httpRouteMap, + BackendTLSPolicies: backendTLSPoliciesMap, + GRPCRoutes: grpcRouteMap, + TCPRoutes: tcpRouteMap, + TLSRoutes: tlsRouteMap, + UDPRoutes: udpRouteMap, + }, notificationList, errs +} diff --git a/pkg/i2gw/providers/nginx/crds/gateway_builder.go b/pkg/i2gw/providers/nginx/crds/gateway_builder.go new file mode 100644 index 00000000..f6547099 --- /dev/null +++ b/pkg/i2gw/providers/nginx/crds/gateway_builder.go @@ -0,0 +1,391 @@ +/* +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 crds + +import ( + "fmt" + "sort" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/intermediate" + "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/notifications" + "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/nginx/common" + nginxv1 "github.com/nginx/kubernetes-ingress/pkg/apis/configuration/v1" +) + +const ( + // DefaultGatewayName is the default name for gateways created from VirtualServers and TransportServers + DefaultGatewayName = "nginx" + + namespaceGatewayHTTPPort = 80 + namespaceGatewayHTTPSPort = 443 +) + +type listenerKey struct { + Port int + Protocol gatewayv1.ProtocolType + Hostname string +} + +type GatewayListenerKey struct { + gatewayName string + listenerName string +} + +// NamespaceGatewayFactory creates shared Gateway resources for VirtualServers and TransportServers in a namespace +type NamespaceGatewayFactory struct { + namespace string + virtualServers []nginxv1.VirtualServer + transportServers []nginxv1.TransportServer + notificationList *[]notifications.Notification + listenerMap map[string]gatewayv1.Listener +} + +// NewNamespaceGatewayFactory creates a new factory for namespace-scoped Gateway creation +func NewNamespaceGatewayFactory(namespace string, virtualServers []nginxv1.VirtualServer, transportServers []nginxv1.TransportServer, notifs *[]notifications.Notification, listenerMap map[string]gatewayv1.Listener) *NamespaceGatewayFactory { + return &NamespaceGatewayFactory{ + namespace: namespace, + virtualServers: virtualServers, + transportServers: transportServers, + notificationList: notifs, + listenerMap: listenerMap, + } +} + +// CreateNamespaceGateway creates a single Gateway for all VirtualServers and TransportServers in the namespace +func (f *NamespaceGatewayFactory) CreateNamespaceGateway() (map[types.NamespacedName]intermediate.GatewayContext, map[string][]GatewayListenerKey) { + gatewayName := DefaultGatewayName + gatewayKey := types.NamespacedName{ + Namespace: f.namespace, + Name: gatewayName, + } + + gateways := make(map[types.NamespacedName]intermediate.GatewayContext) + + // Create all listeners for the single gateway + listeners, virtualServerMap := f.createListeners(gatewayName) + + gateways[gatewayKey] = intermediate.GatewayContext{ + Gateway: gatewayv1.Gateway{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gatewayv1.GroupVersion.String(), + Kind: "Gateway", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: gatewayName, + Namespace: f.namespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "ingress2gateway", + "ingress2gateway.io/source": "nginx-virtualserver", + }, + }, + Spec: gatewayv1.GatewaySpec{ + GatewayClassName: "nginx", + Listeners: listeners, + }, + }, + ProviderSpecificIR: intermediate.ProviderSpecificGatewayIR{ + Nginx: &intermediate.NginxGatewayIR{}, + }, + } + + return gateways, virtualServerMap +} + +// createListeners creates HTTP and HTTPS listeners for the Gateway +func (f *NamespaceGatewayFactory) createListeners(gatewayName string) ([]gatewayv1.Listener, map[string][]GatewayListenerKey) { + uniqueListeners := make(map[listenerKey]gatewayv1.Listener) + virtualServerMap := make(map[string][]GatewayListenerKey) + + for _, vs := range f.virtualServers { + httpPort, httpsPort := f.getListenerPorts(vs) + hostPtr := &vs.Spec.Host + if vs.Spec.Host == "" { + hostPtr = nil + } + + // HTTP + if httpPort > 0 { + key := listenerKey{ + Port: httpPort, + Protocol: gatewayv1.HTTPProtocolType, + Hostname: vs.Spec.Host, + } + + redirect := false + if vs.Spec.TLS != nil && vs.Spec.TLS.Redirect != nil && vs.Spec.TLS.Redirect.Enable { + redirect = true + } + + if _, exists := uniqueListeners[key]; !exists { + listenerName := fmt.Sprintf("http-%d-%s", httpPort, sanitizeHostname(vs.Spec.Host)) + uniqueListeners[key] = gatewayv1.Listener{ + Name: gatewayv1.SectionName(listenerName), + Port: gatewayv1.PortNumber(httpPort), + Protocol: gatewayv1.HTTPProtocolType, + Hostname: (*gatewayv1.Hostname)(hostPtr), + } + } + + if !redirect { + virtualServerMap[vs.Name] = append(virtualServerMap[vs.Name], GatewayListenerKey{ + gatewayName: gatewayName, + listenerName: string(uniqueListeners[key].Name), + }) + } + } + + // HTTPS + if httpsPort > 0 { + secret := "" + if vs.Spec.TLS != nil && vs.Spec.TLS.Secret != "" { + secret = vs.Spec.TLS.Secret + } + + key := listenerKey{ + Port: httpsPort, + Protocol: gatewayv1.HTTPSProtocolType, + Hostname: vs.Spec.Host, + } + + if _, exists := uniqueListeners[key]; !exists { + listenerName := fmt.Sprintf("https-%d-%s-%s", httpsPort, sanitizeHostname(vs.Spec.Host), sanitizeSecret(secret)) + uniqueListeners[key] = gatewayv1.Listener{ + Name: gatewayv1.SectionName(listenerName), + Port: gatewayv1.PortNumber(httpsPort), + Protocol: gatewayv1.HTTPSProtocolType, + Hostname: (*gatewayv1.Hostname)(hostPtr), + TLS: &gatewayv1.GatewayTLSConfig{ + Mode: Ptr(gatewayv1.TLSModeTerminate), + CertificateRefs: []gatewayv1.SecretObjectReference{ + { + Group: Ptr(gatewayv1.Group(common.CoreGroup)), + Kind: Ptr(gatewayv1.Kind(common.SecretKind)), + Name: gatewayv1.ObjectName(secret), + }, + }, + }, + } + } + + virtualServerMap[vs.Name] = append(virtualServerMap[vs.Name], GatewayListenerKey{ + gatewayName: gatewayName, + listenerName: string(uniqueListeners[key].Name), + }) + } + } + + // Process TransportServers to create TCP/TLS/UDP listeners + transportServerMap := make(map[string][]GatewayListenerKey) + for _, ts := range f.transportServers { + port := f.getTransportServerPort(ts) + if port == nil { + continue + } + protocol := f.getTransportServerProtocol(ts) + if protocol == nil { + continue + } + + key := listenerKey{ + Port: *port, + Protocol: *protocol, + Hostname: ts.Spec.Host, // May be empty for non-SNI protocols + } + + listenerName := f.generateTransportListenerName(ts, *port, *protocol) + + if _, exists := uniqueListeners[key]; !exists { + listener := gatewayv1.Listener{ + Name: gatewayv1.SectionName(listenerName), + Port: gatewayv1.PortNumber(*port), + Protocol: *protocol, + } + + // Add hostname for TLS routes that use SNI + if ts.Spec.Host != "" && *protocol == gatewayv1.TLSProtocolType { + hostname := gatewayv1.Hostname(ts.Spec.Host) + listener.Hostname = &hostname + } + + // Add TLS configuration for TLS listeners + if *protocol == gatewayv1.TLSProtocolType { + if ts.Spec.TLS != nil && ts.Spec.TLS.Secret != "" { + // TLS termination + listener.TLS = &gatewayv1.GatewayTLSConfig{ + Mode: Ptr(gatewayv1.TLSModeTerminate), + CertificateRefs: []gatewayv1.SecretObjectReference{ + { + Group: Ptr(gatewayv1.Group(common.CoreGroup)), + Kind: Ptr(gatewayv1.Kind(common.SecretKind)), + Name: gatewayv1.ObjectName(ts.Spec.TLS.Secret), + }, + }, + } + } else { + // TLS passthrough (default for TLS_PASSTHROUGH protocol) + listener.TLS = &gatewayv1.GatewayTLSConfig{ + Mode: Ptr(gatewayv1.TLSModePassthrough), + } + } + } + + uniqueListeners[key] = listener + } + + // Map TransportServer to listener + transportServerMap[ts.Name] = append(transportServerMap[ts.Name], GatewayListenerKey{ + gatewayName: gatewayName, + listenerName: listenerName, + }) + } + + // Merge virtualServerMap and transportServerMap + for tsName, listeners := range transportServerMap { + virtualServerMap[tsName] = listeners + } + + // Sort virtualServerMap entries for deterministic parent references + for vsName, listenerKeys := range virtualServerMap { + sort.Slice(listenerKeys, func(i, j int) bool { + if listenerKeys[i].gatewayName != listenerKeys[j].gatewayName { + return listenerKeys[i].gatewayName < listenerKeys[j].gatewayName + } + return listenerKeys[i].listenerName < listenerKeys[j].listenerName + }) + virtualServerMap[vsName] = listenerKeys + } + + // Convert map to slice and sort for deterministic order + var listeners []gatewayv1.Listener + for _, listener := range uniqueListeners { + listeners = append(listeners, listener) + } + + // Sort listeners by name for deterministic ordering + sort.Slice(listeners, func(i, j int) bool { + return string(listeners[i].Name) < string(listeners[j].Name) + }) + + return listeners, virtualServerMap +} + +// getListenerPorts determines which ports/protocols a VirtualServer needs +func (f *NamespaceGatewayFactory) getListenerPorts(vs nginxv1.VirtualServer) (httpPort, httpsPort int) { + // Check if VirtualServer specifies custom listeners + if vs.Spec.Listener != nil { + // Use custom listeners from GlobalConfiguration + if vs.Spec.Listener.HTTP != "" { + if listener, found := f.listenerMap[vs.Spec.Listener.HTTP]; found { + if listener.Protocol == gatewayv1.HTTPProtocolType { + httpPort = int(listener.Port) + } + } + } + if vs.Spec.Listener.HTTPS != "" { + if listener, found := f.listenerMap[vs.Spec.Listener.HTTPS]; found { + if listener.Protocol == gatewayv1.HTTPSProtocolType { + httpsPort = int(listener.Port) + } + } + } + } else { + // Use default ports + httpPort = namespaceGatewayHTTPPort + if vs.Spec.TLS != nil { + httpsPort = namespaceGatewayHTTPSPort + } + } + return httpPort, httpsPort +} + +// getTransportServerPort determines the port for a TransportServer from GlobalConfiguration or defaults +func (f *NamespaceGatewayFactory) getTransportServerPort(ts nginxv1.TransportServer) *int { + listenerName := ts.Spec.Listener.Name + if listener, exists := f.listenerMap[listenerName]; exists { + return Ptr(int(listener.Port)) + } + return nil +} + +// getTransportServerProtocol maps TransportServer protocol to Gateway API protocol +func (f *NamespaceGatewayFactory) getTransportServerProtocol(ts nginxv1.TransportServer) *gatewayv1.ProtocolType { + switch ts.Spec.Listener.Protocol { + case "TCP": + return Ptr(gatewayv1.TCPProtocolType) + case "UDP": + return Ptr(gatewayv1.UDPProtocolType) + case "TLS_PASSTHROUGH": + return Ptr(gatewayv1.TLSProtocolType) + default: + return nil + } +} + +// generateTransportListenerName creates a listener name for TransportServer +func (f *NamespaceGatewayFactory) generateTransportListenerName(ts nginxv1.TransportServer, port int, protocol gatewayv1.ProtocolType) string { + protocolStr := strings.ToLower(string(protocol)) + + if ts.Spec.Host != "" { + hostname := sanitizeHostname(ts.Spec.Host) + return fmt.Sprintf("%s-%d-%s", protocolStr, port, hostname) + } + + return fmt.Sprintf("%s-%d", protocolStr, port) +} + +// sanitizeHostname replaces special characters for use in sectionName +func sanitizeHostname(host string) string { + if host == "" { + return "catchall" + } + host = strings.ToLower(host) + host = strings.ReplaceAll(host, ".", "-") + host = strings.ReplaceAll(host, "*", "wildcard") + host = strings.ReplaceAll(host, ":", "-") + host = strings.ReplaceAll(host, "_", "-") + host = strings.Trim(host, "-") + if host == "" { + return "catchall" + } + if len(host) > 30 { // Keep it shorter for section names + host = host[:30] + } + return host +} + +// sanitizeSecret replaces special characters in secret names for use in sectionName +func sanitizeSecret(secret string) string { + if secret == "" { + return "nosecret" + } + secret = strings.ToLower(secret) + secret = strings.ReplaceAll(secret, ".", "-") + secret = strings.ReplaceAll(secret, "_", "-") + secret = strings.Trim(secret, "-") + if secret == "" { + return "nosecret" + } + if len(secret) > 20 { // Keep it shorter for section names + secret = secret[:20] + } + return secret +} diff --git a/pkg/i2gw/providers/nginx/crds/utils.go b/pkg/i2gw/providers/nginx/crds/utils.go new file mode 100644 index 00000000..6c539681 --- /dev/null +++ b/pkg/i2gw/providers/nginx/crds/utils.go @@ -0,0 +1,53 @@ +/* +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 crds + +import ( + "strings" + + "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/notifications" + nginxv1 "github.com/nginx/kubernetes-ingress/pkg/apis/configuration/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Utility functions + +// Ptr is Generic pointer conversion utility +func Ptr[T any](t T) *T { + return &t +} + +// findUpstream finds an upstream by name in the upstreams slice +func findUpstream(upstreams []nginxv1.Upstream, name string) *nginxv1.Upstream { + for _, upstream := range upstreams { + if upstream.Name == name { + return &upstream + } + } + return nil +} + +// containsRegexPatterns checks if a value contains regex special characters +func containsRegexPatterns(s string) bool { + return strings.ContainsAny(s, `\.+*?^$()[]{}|`) +} + +// addNotification adds a notification to the notification list +func addNotification(notificationList *[]notifications.Notification, messageType notifications.MessageType, message string, obj client.Object) { + n := notifications.NewNotification(messageType, message, obj) + *notificationList = append(*notificationList, n) +} diff --git a/pkg/i2gw/providers/nginx/crds/utils_test.go b/pkg/i2gw/providers/nginx/crds/utils_test.go new file mode 100644 index 00000000..ff97eddb --- /dev/null +++ b/pkg/i2gw/providers/nginx/crds/utils_test.go @@ -0,0 +1,81 @@ +/* +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 crds + +import ( + "testing" + + nginxv1 "github.com/nginx/kubernetes-ingress/pkg/apis/configuration/v1" +) + +func TestContainsRegexPatterns(t *testing.T) { + t.Run("regex pattern detection", func(t *testing.T) { + tests := []struct { + input string + expected bool + }{ + {"simple-path", false}, + {"/api/v1", false}, + {"/api/.*", true}, + {"/app/[0-9]+", true}, + {"/test?param=value", true}, + {"regular-string", false}, + {"/path/with/dots.extension", true}, + {"/path^start", true}, + {"/path$end", true}, + {"/path(group)", true}, + {"/path{1,3}", true}, + {"/path|alternative", true}, + } + + for _, test := range tests { + result := containsRegexPatterns(test.input) + if result != test.expected { + t.Errorf("containsRegexPatterns(%q) = %v, expected %v", test.input, result, test.expected) + } + } + }) +} + +func TestFindUpstream(t *testing.T) { + upstreams := []nginxv1.Upstream{ + {Name: "upstream1", Service: "service1", Port: 80}, + {Name: "upstream2", Service: "service2", Port: 8080}, + {Name: "upstream3", Service: "service3", Port: 3000}, + } + + t.Run("find existing upstream", func(t *testing.T) { + result := findUpstream(upstreams, "upstream2") + if result == nil { + t.Error("Expected to find upstream2, got nil") + return + } + if result.Service != "service2" { + t.Errorf("Expected service2, got %s", result.Service) + } + if result.Port != 8080 { + t.Errorf("Expected port 8080, got %d", result.Port) + } + }) + + t.Run("upstream not found", func(t *testing.T) { + result := findUpstream(upstreams, "nonexistent") + if result != nil { + t.Error("Expected nil for nonexistent upstream, got result") + } + }) +} diff --git a/pkg/i2gw/providers/nginx/nginx.go b/pkg/i2gw/providers/nginx/nginx.go index a7a1e349..b526c88f 100644 --- a/pkg/i2gw/providers/nginx/nginx.go +++ b/pkg/i2gw/providers/nginx/nginx.go @@ -25,10 +25,19 @@ import ( "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/intermediate" ) -const Name = "nginx" +const ( + Name = "nginx" + + GlobalConfigurationFlag = "global-configuration" +) func init() { i2gw.ProviderConstructorByName[Name] = NewProvider + + i2gw.RegisterProviderSpecificFlag(Name, i2gw.ProviderSpecificFlag{ + Name: GlobalConfigurationFlag, + Description: "Namespace and Name of NIC GlobalConfiguration resource.", + }) } type Provider struct { diff --git a/pkg/i2gw/providers/nginx/notification.go b/pkg/i2gw/providers/nginx/notification.go new file mode 100644 index 00000000..84edd0d7 --- /dev/null +++ b/pkg/i2gw/providers/nginx/notification.go @@ -0,0 +1,28 @@ +/* +Copyright 2024 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 nginx + +import ( + "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/notifications" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// notify dispatches a notification with the nginx provider name +func notify(mType notifications.MessageType, message string, callingObject ...client.Object) { + newNotification := notifications.NewNotification(mType, message, callingObject...) + notifications.NotificationAggr.DispatchNotification(newNotification, string(Name)) +} diff --git a/pkg/i2gw/providers/nginx/resource_reader.go b/pkg/i2gw/providers/nginx/resource_reader.go index 31fcbcfe..af7926ac 100644 --- a/pkg/i2gw/providers/nginx/resource_reader.go +++ b/pkg/i2gw/providers/nginx/resource_reader.go @@ -18,11 +18,17 @@ package nginx import ( "context" + "fmt" + "strings" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw" + "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/notifications" "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/common" + nginxv1 "github.com/nginx/kubernetes-ingress/pkg/apis/configuration/v1" ) // NginxIngressClasses contains NGINX IngressClass names @@ -30,6 +36,14 @@ var NginxIngressClasses = sets.New( "nginx", ) +// NGINX CRD GroupVersionKind constants +var ( + VirtualServerGVK = schema.GroupVersionKind{Group: "k8s.nginx.org", Version: "v1", Kind: "VirtualServer"} + VirtualServerRouteGVK = schema.GroupVersionKind{Group: "k8s.nginx.org", Version: "v1", Kind: "VirtualServerRoute"} + TransportServerGVK = schema.GroupVersionKind{Group: "k8s.nginx.org", Version: "v1", Kind: "TransportServer"} + GlobalConfigurationGVK = schema.GroupVersionKind{Group: "k8s.nginx.org", Version: "v1", Kind: "GlobalConfiguration"} +) + type resourceReader struct { conf *i2gw.ProviderConf } @@ -41,6 +55,31 @@ func newResourceReader(conf *i2gw.ProviderConf) *resourceReader { } } +// helper constructors for CRD types +func newVirtualServer() *nginxv1.VirtualServer { return &nginxv1.VirtualServer{} } +func newVirtualServerRoute() *nginxv1.VirtualServerRoute { + return &nginxv1.VirtualServerRoute{} +} +func newTransportServer() *nginxv1.TransportServer { + return &nginxv1.TransportServer{} +} +func newGlobalConfiguration() *nginxv1.GlobalConfiguration { + return &nginxv1.GlobalConfiguration{} +} + +// parseNamespacedName parses a string in the format "namespace/name" or just "name" +// Returns namespace and name. If no namespace is specified, uses the default namespace. +func parseNamespacedName(namespacedName, defaultNamespace string) (namespace, name string) { + if strings.Contains(namespacedName, "/") { + parts := strings.SplitN(namespacedName, "/", 2) + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1] + } + } + // If no namespace specified or invalid format, use default namespace + return defaultNamespace, namespacedName +} + // readResourcesFromCluster reads nginx resources from the Kubernetes cluster func (r *resourceReader) readResourcesFromCluster(ctx context.Context) (*storage, error) { storage := newResourceStorage() @@ -51,11 +90,58 @@ func (r *resourceReader) readResourcesFromCluster(ctx context.Context) (*storage } storage.Ingresses = ingresses - services, err := common.ReadServicesFromCluster(ctx, r.conf.Client) + // Read VirtualServer CRDs + virtualServers, err := genericReadFromCluster( + ctx, + r.conf.Client, + r.conf.Namespace, + VirtualServerGVK, + newVirtualServer, + ) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to read VirtualServers: %w", err) + } + storage.VirtualServers = virtualServers + + // Read VirtualServerRoute CRDs + virtualServerRoutes, err := genericReadFromCluster( + ctx, + r.conf.Client, + r.conf.Namespace, + VirtualServerRouteGVK, + newVirtualServerRoute, + ) + if err != nil { + return nil, fmt.Errorf("failed to read VirtualServerRoutes: %w", err) + } + storage.VirtualServerRoutes = virtualServerRoutes + + // Read TransportServer CRDs + transportServers, err := genericReadFromCluster( + ctx, + r.conf.Client, + r.conf.Namespace, + TransportServerGVK, + newTransportServer, + ) + if err != nil { + return nil, fmt.Errorf("failed to read TransportServers: %w", err) + } + storage.TransportServers = transportServers + + // Read single GlobalConfiguration specified by flag + if flags, ok := r.conf.ProviderSpecificFlags[Name]; ok { + if gcName := flags[GlobalConfigurationFlag]; gcName != "" { + var gc nginxv1.GlobalConfiguration + // Parse namespace/name format, fallback to provider config namespace + namespace, name := parseNamespacedName(gcName, r.conf.Namespace) + key := types.NamespacedName{Namespace: namespace, Name: name} + if err := r.conf.Client.Get(ctx, key, &gc); err != nil { + return nil, fmt.Errorf("failed to get GlobalConfiguration %s/%s: %w", key.Namespace, key.Name, err) + } + storage.GlobalConfiguration = &gc + } } - storage.ServicePorts = common.GroupServicePortsByPortName(services) return storage, nil } @@ -70,6 +156,67 @@ func (r *resourceReader) readResourcesFromFile(filename string) (*storage, error } storage.Ingresses = ingresses + // Read VirtualServer CRDs + virtualServers, err := genericReadFromFile( + filename, + r.conf.Namespace, + VirtualServerGVK, + newVirtualServer, + ) + if err != nil { + return nil, fmt.Errorf("failed to read VirtualServers: %w", err) + } + storage.VirtualServers = virtualServers + + // Read VirtualServerRoute CRDs + virtualServerRoutes, err := genericReadFromFile( + filename, + r.conf.Namespace, + VirtualServerRouteGVK, + newVirtualServerRoute, + ) + if err != nil { + return nil, fmt.Errorf("failed to read VirtualServerRoutes: %w", err) + } + storage.VirtualServerRoutes = virtualServerRoutes + + // Read TransportServer CRDs + transportServers, err := genericReadFromFile( + filename, + r.conf.Namespace, + TransportServerGVK, + newTransportServer, + ) + if err != nil { + return nil, fmt.Errorf("failed to read TransportServers: %w", err) + } + storage.TransportServers = transportServers + + // Read single GlobalConfiguration from file specified by flag + if flags, ok := r.conf.ProviderSpecificFlags[Name]; ok { + if gcName := flags[GlobalConfigurationFlag]; gcName != "" { + // Parse namespace/name format, fallback to provider config namespace + namespace, name := parseNamespacedName(gcName, r.conf.Namespace) + + globalConfs, err := genericReadFromFile( + filename, + namespace, + GlobalConfigurationGVK, + newGlobalConfiguration, + ) + if err != nil { + return nil, fmt.Errorf("failed to read GlobalConfigurations from file: %w", err) + } + for _, gc := range globalConfs { + notify(notifications.WarningNotification, fmt.Sprintf("GlobalConfiguration name: %s", gc.Name), &gc) + if gc.Name == name { + storage.GlobalConfiguration = &gc + break + } + } + } + } + services, err := common.ReadServicesFromFile(filename, r.conf.Namespace) if err != nil { return nil, err diff --git a/pkg/i2gw/providers/nginx/storage.go b/pkg/i2gw/providers/nginx/storage.go index 655572f9..18a5bb1d 100644 --- a/pkg/i2gw/providers/nginx/storage.go +++ b/pkg/i2gw/providers/nginx/storage.go @@ -19,17 +19,27 @@ package nginx import ( networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/types" + + nginxv1 "github.com/nginx/kubernetes-ingress/pkg/apis/configuration/v1" ) type storage struct { - Ingresses map[types.NamespacedName]*networkingv1.Ingress - ServicePorts map[types.NamespacedName]map[string]int32 + Ingresses map[types.NamespacedName]*networkingv1.Ingress + VirtualServers []nginxv1.VirtualServer + VirtualServerRoutes []nginxv1.VirtualServerRoute + TransportServers []nginxv1.TransportServer + GlobalConfiguration *nginxv1.GlobalConfiguration + ServicePorts map[types.NamespacedName]map[string]int32 } // newResourceStorage creates a new storage instance func newResourceStorage() *storage { return &storage{ - Ingresses: map[types.NamespacedName]*networkingv1.Ingress{}, - ServicePorts: map[types.NamespacedName]map[string]int32{}, + Ingresses: map[types.NamespacedName]*networkingv1.Ingress{}, + VirtualServers: []nginxv1.VirtualServer{}, + VirtualServerRoutes: []nginxv1.VirtualServerRoute{}, + TransportServers: []nginxv1.TransportServer{}, + GlobalConfiguration: nil, + ServicePorts: map[types.NamespacedName]map[string]int32{}, } }