This is an automated email from the ASF dual-hosted git repository. ashishtiwari pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git
The following commit(s) were added to refs/heads/master by this push: new cb69f53c feat: add support for CORS httproutefilter (#2548) cb69f53c is described below commit cb69f53cf1e6a79805047a1deadcfa93be67264b Author: Ashish Tiwari <ashishjaitiwari15112...@gmail.com> AuthorDate: Tue Sep 9 16:32:08 2025 +0530 feat: add support for CORS httproutefilter (#2548) --- Makefile | 4 +- api/adc/plugin_types.go | 8 ++- api/adc/types.go | 1 + internal/adc/translator/httproute.go | 46 ++++++++++++++ test/e2e/gatewayapi/httproute.go | 118 +++++++++++++++++++++++++++++++++++ 5 files changed, 172 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 24f34762..478dddf5 100644 --- a/Makefile +++ b/Makefile @@ -262,11 +262,11 @@ endif .PHONY: install-gateway-api install-gateway-api: ## Install Gateway API CRDs into the K8s cluster specified in ~/.kube/config. - kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/$(GATEAY_API_VERSION)/standard-install.yaml + kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/$(GATEAY_API_VERSION)/experimental-install.yaml .PHONY: uninstall-gateway-api uninstall-gateway-api: ## Uninstall Gateway API CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. - kubectl delete -f https://github.com/kubernetes-sigs/gateway-api/releases/download/$(GATEAY_API_VERSION)/standard-install.yaml + kubectl delete -f https://github.com/kubernetes-sigs/gateway-api/releases/download/$(GATEAY_API_VERSION)/experimental-install.yaml .PHONY: install install: manifests kustomize install-gateway-api ## Install CRDs into the K8s cluster specified in ~/.kube/config. diff --git a/api/adc/plugin_types.go b/api/adc/plugin_types.go index 1c2cd888..6d230886 100644 --- a/api/adc/plugin_types.go +++ b/api/adc/plugin_types.go @@ -27,9 +27,11 @@ type IPRestrictConfig struct { // CorsConfig is the rule config for cors plugin. // +k8s:deepcopy-gen=true type CorsConfig struct { - AllowOrigins string `json:"allow_origins,omitempty"` - AllowMethods string `json:"allow_methods,omitempty"` - AllowHeaders string `json:"allow_headers,omitempty"` + AllowOrigins string `json:"allow_origins,omitempty"` + AllowMethods string `json:"allow_methods,omitempty"` + AllowHeaders string `json:"allow_headers,omitempty"` + ExposeHeaders string `json:"expose_headers,omitempty"` + AllowCredential bool `json:"allow_credential,omitempty"` } // CSRfConfig is the rule config for csrf plugin. diff --git a/api/adc/types.go b/api/adc/types.go index db4d2d71..5b162bc3 100644 --- a/api/adc/types.go +++ b/api/adc/types.go @@ -608,6 +608,7 @@ const ( PluginRedirect string = "redirect" PluginResponseRewrite string = "response-rewrite" PluginProxyMirror string = "proxy-mirror" + PluginCORS string = "cors" ) // RewriteConfig is the rule config for proxy-rewrite plugin. diff --git a/internal/adc/translator/httproute.go b/internal/adc/translator/httproute.go index 90816258..bd2b021e 100644 --- a/internal/adc/translator/httproute.go +++ b/internal/adc/translator/httproute.go @@ -60,6 +60,8 @@ func (t *Translator) fillPluginsFromHTTPRouteFilters( t.fillPluginFromHTTPResponseHeaderFilter(plugins, filter.ResponseHeaderModifier) case gatewayv1.HTTPRouteFilterExtensionRef: t.fillPluginFromExtensionRef(plugins, namespace, filter.ExtensionRef, tctx) + case gatewayv1.HTTPRouteFilterCORS: + t.fillPluginFromHTTPCORSFilter(plugins, filter.CORS) } } } @@ -129,6 +131,50 @@ func (t *Translator) fillPluginFromURLRewriteFilter(plugins adctypes.Plugins, ur } } +func (t *Translator) fillPluginFromHTTPCORSFilter(plugins adctypes.Plugins, cors *gatewayv1.HTTPCORSFilter) { + pluginName := adctypes.PluginCORS + obj := plugins[pluginName] + var plugin *adctypes.CorsConfig + if obj == nil { + plugin = &adctypes.CorsConfig{} + plugins[pluginName] = plugin + } else { + plugin = obj.(*adctypes.CorsConfig) + } + + if len(cors.AllowOrigins) > 0 { + origins := make([]string, len(cors.AllowOrigins)) + for i, allowOrigin := range cors.AllowOrigins { + origins[i] = string(allowOrigin) + } + plugin.AllowOrigins = strings.Join(origins, ",") + } + + if len(cors.AllowHeaders) > 0 { + headers := make([]string, len(cors.AllowHeaders)) + for i, allowHeader := range cors.AllowHeaders { + headers[i] = string(allowHeader) + } + plugin.AllowHeaders = strings.Join(headers, ",") + } + + if len(cors.AllowMethods) > 0 { + methods := make([]string, len(cors.AllowMethods)) + for i, allowMethod := range cors.AllowMethods { + methods[i] = string(allowMethod) + } + plugin.AllowMethods = strings.Join(methods, ",") + } + if len(cors.ExposeHeaders) > 0 { + exposeHeaders := make([]string, len(cors.ExposeHeaders)) + for i, exposeHeader := range cors.ExposeHeaders { + exposeHeaders[i] = string(exposeHeader) + } + plugin.ExposeHeaders = strings.Join(exposeHeaders, ",") + } + plugin.AllowCredential = bool(cors.AllowCredentials) +} + func (t *Translator) fillPluginFromHTTPRequestHeaderFilter(plugins adctypes.Plugins, reqHeaderModifier *gatewayv1.HTTPHeaderFilter) { pluginName := adctypes.PluginProxyRewrite obj := plugins[pluginName] diff --git a/test/e2e/gatewayapi/httproute.go b/test/e2e/gatewayapi/httproute.go index 2ee93beb..4dfe6e09 100644 --- a/test/e2e/gatewayapi/httproute.go +++ b/test/e2e/gatewayapi/httproute.go @@ -1702,6 +1702,76 @@ spec: port: 80 ` + var corsTestService = ` +apiVersion: v1 +kind: Service +metadata: + name: cors-test-service +spec: + selector: + app: cors-test + ports: + - port: 80 + targetPort: 5678 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cors-test +spec: + replicas: 1 + selector: + matchLabels: + app: cors-test + template: + metadata: + labels: + app: cors-test + spec: + containers: + - name: cors-test + image: hashicorp/http-echo + args: ["-text=hello", "-listen=:5678"] + ports: + - containerPort: 5678 +` + + var corsFilter = ` +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-route-cors + namespace: %s +spec: + parentRefs: + - name: %s + hostnames: + - cors-test.example + rules: + - matches: + - path: + type: PathPrefix + value: / + filters: + - type: CORS + cors: + allowOrigins: + - http://example.com + allowMethods: + - GET + - POST + - PUT + - DELETE + allowHeaders: + - "Origin" + exposeHeaders: + - "Origin" + allowCredentials: true + backendRefs: + - name: cors-test-service + port: 80 +` + BeforeEach(beforeEachHTTP) It("HTTPRoute RequestHeaderModifier", func() { @@ -1970,6 +2040,54 @@ spec: Interval: time.Second * 2, }) }) + + It("HTTPRoute CORS Filter", func() { + By("create test service and deployment") + Expect(s.CreateResourceFromStringWithNamespace(corsTestService, s.Namespace())). + NotTo(HaveOccurred(), "creating CORS test service") + + By("create HTTPRoute with CORS filter") + s.ResourceApplied("HTTPRoute", "http-route-cors", fmt.Sprintf(corsFilter, s.Namespace(), s.Namespace()), 1) + By("test simple GET request with CORS headers from allowed origin") + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/", + Host: "cors-test.example", + Headers: map[string]string{ + "Origin": "http://example.com", + }, + Checks: []scaffold.ResponseCheckFunc{ + scaffold.WithExpectedStatus(http.StatusOK), + scaffold.WithExpectedBodyContains("hello"), + scaffold.WithExpectedHeaders(map[string]string{ + "Access-Control-Allow-Origin": "http://example.com", + "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE", + "Access-Control-Allow-Headers": "Origin", + "Access-Control-Expose-Headers": "Origin", + "Access-Control-Allow-Credentials": "true", + }), + }, + Timeout: time.Second * 30, + Interval: time.Second * 2, + }) + + By("test simple GET request with CORS headers from disallowed origin") + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/", + Host: "cors-test.example", + Headers: map[string]string{ + "Origin": "http://disallowed.com", + }, + Checks: []scaffold.ResponseCheckFunc{ + scaffold.WithExpectedStatus(http.StatusOK), + scaffold.WithExpectedBodyContains("hello"), + scaffold.WithExpectedNotHeader("Access-Control-Allow-Origin"), + }, + Timeout: time.Second * 30, + Interval: time.Second * 2, + }) + }) }) Context("HTTPRoute Multiple Backend", func() {