This is an automated email from the ASF dual-hosted git repository.

ronething pushed a commit to branch feat/cors
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git

commit d66bf8728416fb858b123c40b685eed8f2ab35cb
Author: Ashing Zheng <[email protected]>
AuthorDate: Fri Oct 24 16:45:36 2025 +0800

    feat: support cors annotations
    
    Signed-off-by: Ashing Zheng <[email protected]>
---
 examples/httpbin/ingress-cors.yaml               |  23 +++
 internal/adc/translator/annotations.go           |   3 +
 internal/adc/translator/annotations/cors.go      |  48 +++++
 internal/adc/translator/annotations/cors_test.go | 113 ++++++++++++
 internal/adc/translator/ingress.go               |  19 ++
 internal/adc/translator/ingress_test.go          | 219 +++++++++++++++++++++++
 6 files changed, 425 insertions(+)

diff --git a/examples/httpbin/ingress-cors.yaml 
b/examples/httpbin/ingress-cors.yaml
new file mode 100644
index 00000000..e8f73e0c
--- /dev/null
+++ b/examples/httpbin/ingress-cors.yaml
@@ -0,0 +1,23 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: httpbin-ingress-cors
+  annotations:
+    k8s.apisix.apache.org/enable-cors: "true"
+    k8s.apisix.apache.org/cors-allow-origin: 
"https://example.com,https://app.example.com";
+    k8s.apisix.apache.org/cors-allow-methods: "GET,POST,PUT,DELETE,OPTIONS"
+    k8s.apisix.apache.org/cors-allow-headers: 
"Content-Type,Authorization,X-Requested-With"
+spec:
+  ingressClassName: apisix
+  rules:
+  - host: httpbin.local
+    http:
+      paths:
+      - path: /
+        pathType: Prefix
+        backend:
+          service:
+            name: httpbin
+            port:
+              number: 80
+
diff --git a/internal/adc/translator/annotations.go 
b/internal/adc/translator/annotations.go
index 28319b65..eca9a005 100644
--- a/internal/adc/translator/annotations.go
+++ b/internal/adc/translator/annotations.go
@@ -21,6 +21,7 @@ import (
 
        "github.com/imdario/mergo"
 
+       adctypes "github.com/apache/apisix-ingress-controller/api/adc"
        
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
        
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/upstream"
 )
@@ -28,11 +29,13 @@ import (
 // Structure extracted by Ingress Resource
 type IngressConfig struct {
        Upstream upstream.Upstream
+       Cors     *adctypes.CorsConfig `json:"cors,omitempty"`
 }
 
 // parsers registered for ingress annotations
 var ingressAnnotationParsers = map[string]annotations.IngressAnnotationsParser{
        "upstream": upstream.NewParser(),
+       "cors":     annotations.NewCorsParser(),
 }
 
 func (t *Translator) TranslateIngressAnnotations(anno map[string]string) 
*IngressConfig {
diff --git a/internal/adc/translator/annotations/cors.go 
b/internal/adc/translator/annotations/cors.go
new file mode 100644
index 00000000..44d5bfbc
--- /dev/null
+++ b/internal/adc/translator/annotations/cors.go
@@ -0,0 +1,48 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 annotations
+
+import (
+       adctypes "github.com/apache/apisix-ingress-controller/api/adc"
+)
+
+type corsParser struct{}
+
+// NewCorsParser creates a parser to convert CORS annotations
+// to APISIX cors plugin configuration.
+func NewCorsParser() IngressAnnotationsParser {
+       return &corsParser{}
+}
+
+func (c *corsParser) Parse(e Extractor) (any, error) {
+       if !e.GetBoolAnnotation(AnnotationsEnableCors) {
+               return nil, nil
+       }
+
+       corsConfig := &adctypes.CorsConfig{
+               AllowOrigins: e.GetStringAnnotation(AnnotationsCorsAllowOrigin),
+               AllowMethods: 
e.GetStringAnnotation(AnnotationsCorsAllowMethods),
+               AllowHeaders: 
e.GetStringAnnotation(AnnotationsCorsAllowHeaders),
+       }
+
+       // Return nil if all fields are empty (only enable-cors is set)
+       if corsConfig.AllowOrigins == "" && corsConfig.AllowMethods == "" && 
corsConfig.AllowHeaders == "" {
+               // Use default CORS config with just enable flag
+               return corsConfig, nil
+       }
+
+       return corsConfig, nil
+}
diff --git a/internal/adc/translator/annotations/cors_test.go 
b/internal/adc/translator/annotations/cors_test.go
new file mode 100644
index 00000000..ddcc5ba3
--- /dev/null
+++ b/internal/adc/translator/annotations/cors_test.go
@@ -0,0 +1,113 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 annotations
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+
+       adctypes "github.com/apache/apisix-ingress-controller/api/adc"
+)
+
+func TestCorsParser_Parse(t *testing.T) {
+       tests := []struct {
+               name        string
+               annotations map[string]string
+               expected    *adctypes.CorsConfig
+               expectNil   bool
+       }{
+               {
+                       name: "cors disabled",
+                       annotations: map[string]string{
+                               AnnotationsEnableCors: "false",
+                       },
+                       expectNil: true,
+               },
+               {
+                       name: "cors enabled with all fields",
+                       annotations: map[string]string{
+                               AnnotationsEnableCors:       "true",
+                               AnnotationsCorsAllowOrigin:  
"https://example.com";,
+                               AnnotationsCorsAllowMethods: "GET,POST,PUT",
+                               AnnotationsCorsAllowHeaders: 
"Content-Type,Authorization",
+                       },
+                       expected: &adctypes.CorsConfig{
+                               AllowOrigins: "https://example.com";,
+                               AllowMethods: "GET,POST,PUT",
+                               AllowHeaders: "Content-Type,Authorization",
+                       },
+               },
+               {
+                       name: "cors enabled with partial fields",
+                       annotations: map[string]string{
+                               AnnotationsEnableCors:      "true",
+                               AnnotationsCorsAllowOrigin: 
"https://example.com";,
+                       },
+                       expected: &adctypes.CorsConfig{
+                               AllowOrigins: "https://example.com";,
+                       },
+               },
+               {
+                       name: "cors enabled without any config",
+                       annotations: map[string]string{
+                               AnnotationsEnableCors: "true",
+                       },
+                       expected: &adctypes.CorsConfig{},
+               },
+               {
+                       name:        "no cors annotation",
+                       annotations: map[string]string{},
+                       expectNil:   true,
+               },
+               {
+                       name: "cors enabled with wildcard origin",
+                       annotations: map[string]string{
+                               AnnotationsEnableCors:       "true",
+                               AnnotationsCorsAllowOrigin:  "*",
+                               AnnotationsCorsAllowMethods: "*",
+                               AnnotationsCorsAllowHeaders: "*",
+                       },
+                       expected: &adctypes.CorsConfig{
+                               AllowOrigins: "*",
+                               AllowMethods: "*",
+                               AllowHeaders: "*",
+                       },
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       parser := NewCorsParser()
+                       extractor := NewExtractor(tt.annotations)
+
+                       result, err := parser.Parse(extractor)
+
+                       assert.NoError(t, err)
+
+                       if tt.expectNil {
+                               assert.Nil(t, result)
+                       } else {
+                               assert.NotNil(t, result)
+                               corsConfig, ok := result.(*adctypes.CorsConfig)
+                               assert.True(t, ok)
+                               assert.Equal(t, tt.expected.AllowOrigins, 
corsConfig.AllowOrigins)
+                               assert.Equal(t, tt.expected.AllowMethods, 
corsConfig.AllowMethods)
+                               assert.Equal(t, tt.expected.AllowHeaders, 
corsConfig.AllowHeaders)
+                       }
+               })
+       }
+}
diff --git a/internal/adc/translator/ingress.go 
b/internal/adc/translator/ingress.go
index da3c5d0e..675aee4a 100644
--- a/internal/adc/translator/ingress.go
+++ b/internal/adc/translator/ingress.go
@@ -167,10 +167,29 @@ func (t *Translator) buildServiceFromIngressPath(
        }
        service.Routes = []*adctypes.Route{route}
 
+       // Parse and apply annotations to service plugins
+       t.applyIngressAnnotations(obj, service)
+
        t.fillHTTPRoutePoliciesForIngress(tctx, service.Routes)
        return service
 }
 
+func (t *Translator) applyIngressAnnotations(obj *networkingv1.Ingress, 
service *adctypes.Service) {
+       if len(obj.Annotations) == 0 {
+               return
+       }
+
+       ingress := t.TranslateIngressAnnotations(obj.Annotations)
+       if ingress == nil {
+               return
+       }
+
+       // Apply CORS plugin if configured
+       if ingress.Cors != nil {
+               service.Plugins[adctypes.PluginCORS] = ingress.Cors
+       }
+}
+
 func (t *Translator) resolveIngressUpstream(
        tctx *provider.TranslateContext,
        obj *networkingv1.Ingress,
diff --git a/internal/adc/translator/ingress_test.go 
b/internal/adc/translator/ingress_test.go
new file mode 100644
index 00000000..4eca5303
--- /dev/null
+++ b/internal/adc/translator/ingress_test.go
@@ -0,0 +1,219 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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 translator
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+       corev1 "k8s.io/api/core/v1"
+       discoveryv1 "k8s.io/api/discovery/v1"
+       networkingv1 "k8s.io/api/networking/v1"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/apimachinery/pkg/types"
+
+       adctypes "github.com/apache/apisix-ingress-controller/api/adc"
+       
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
+       "github.com/apache/apisix-ingress-controller/internal/provider"
+)
+
+func TestTranslateIngress_WithCORS(t *testing.T) {
+       translator := &Translator{}
+
+       ingress := &networkingv1.Ingress{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "test-ingress",
+                       Namespace: "default",
+                       Annotations: map[string]string{
+                               annotations.AnnotationsEnableCors:       "true",
+                               annotations.AnnotationsCorsAllowOrigin:  
"https://example.com";,
+                               annotations.AnnotationsCorsAllowMethods: 
"GET,POST,PUT",
+                               annotations.AnnotationsCorsAllowHeaders: 
"Content-Type,Authorization",
+                       },
+               },
+               Spec: networkingv1.IngressSpec{
+                       Rules: []networkingv1.IngressRule{
+                               {
+                                       Host: "test.example.com",
+                                       IngressRuleValue: 
networkingv1.IngressRuleValue{
+                                               HTTP: 
&networkingv1.HTTPIngressRuleValue{
+                                                       Paths: 
[]networkingv1.HTTPIngressPath{
+                                                               {
+                                                                       Path:   
  "/api",
+                                                                       
PathType: func() *networkingv1.PathType { pt := networkingv1.PathTypePrefix; 
return &pt }(),
+                                                                       
Backend: networkingv1.IngressBackend{
+                                                                               
Service: &networkingv1.IngressServiceBackend{
+                                                                               
        Name: "test-service",
+                                                                               
        Port: networkingv1.ServiceBackendPort{
+                                                                               
                Number: 80,
+                                                                               
        },
+                                                                               
},
+                                                                       },
+                                                               },
+                                                       },
+                                               },
+                                       },
+                               },
+                       },
+               },
+       }
+
+       svc := &corev1.Service{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "test-service",
+                       Namespace: "default",
+               },
+               Spec: corev1.ServiceSpec{
+                       Type: corev1.ServiceTypeClusterIP,
+                       Ports: []corev1.ServicePort{
+                               {
+                                       Port: 80,
+                                       Name: "http",
+                               },
+                       },
+               },
+       }
+
+       endpointSlice := discoveryv1.EndpointSlice{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "test-service-abc",
+                       Namespace: "default",
+                       Labels: map[string]string{
+                               discoveryv1.LabelServiceName: "test-service",
+                       },
+               },
+               Ports: []discoveryv1.EndpointPort{
+                       {
+                               Name: func() *string { s := "http"; return &s 
}(),
+                               Port: func() *int32 { p := int32(80); return &p 
}(),
+                       },
+               },
+               Endpoints: []discoveryv1.Endpoint{
+                       {
+                               Addresses: []string{"10.0.0.1"},
+                               Conditions: discoveryv1.EndpointConditions{
+                                       Ready: func() *bool { b := true; return 
&b }(),
+                               },
+                       },
+               },
+       }
+
+       tctx := &provider.TranslateContext{
+               Services: map[types.NamespacedName]*corev1.Service{
+                       {Namespace: "default", Name: "test-service"}: svc,
+               },
+               EndpointSlices: 
map[types.NamespacedName][]discoveryv1.EndpointSlice{
+                       {Namespace: "default", Name: "test-service"}: 
{endpointSlice},
+               },
+       }
+
+       result, err := translator.TranslateIngress(tctx, ingress)
+
+       assert.NoError(t, err)
+       assert.NotNil(t, result)
+       assert.Len(t, result.Services, 1)
+
+       service := result.Services[0]
+       assert.NotNil(t, service)
+       assert.NotNil(t, service.Plugins)
+
+       // Verify CORS plugin is configured
+       corsPlugin, exists := service.Plugins[adctypes.PluginCORS]
+       assert.True(t, exists, "CORS plugin should be present")
+       assert.NotNil(t, corsPlugin)
+
+       corsConfig, ok := corsPlugin.(*adctypes.CorsConfig)
+       assert.True(t, ok, "CORS plugin should be of type *adctypes.CorsConfig")
+       assert.Equal(t, "https://example.com";, corsConfig.AllowOrigins)
+       assert.Equal(t, "GET,POST,PUT", corsConfig.AllowMethods)
+       assert.Equal(t, "Content-Type,Authorization", corsConfig.AllowHeaders)
+}
+
+func TestTranslateIngress_WithoutCORS(t *testing.T) {
+       translator := &Translator{}
+
+       ingress := &networkingv1.Ingress{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "test-ingress",
+                       Namespace: "default",
+                       // No CORS annotations
+               },
+               Spec: networkingv1.IngressSpec{
+                       Rules: []networkingv1.IngressRule{
+                               {
+                                       Host: "test.example.com",
+                                       IngressRuleValue: 
networkingv1.IngressRuleValue{
+                                               HTTP: 
&networkingv1.HTTPIngressRuleValue{
+                                                       Paths: 
[]networkingv1.HTTPIngressPath{
+                                                               {
+                                                                       Path:   
  "/api",
+                                                                       
PathType: func() *networkingv1.PathType { pt := networkingv1.PathTypePrefix; 
return &pt }(),
+                                                                       
Backend: networkingv1.IngressBackend{
+                                                                               
Service: &networkingv1.IngressServiceBackend{
+                                                                               
        Name: "test-service",
+                                                                               
        Port: networkingv1.ServiceBackendPort{
+                                                                               
                Number: 80,
+                                                                               
        },
+                                                                               
},
+                                                                       },
+                                                               },
+                                                       },
+                                               },
+                                       },
+                               },
+                       },
+               },
+       }
+
+       svc := &corev1.Service{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "test-service",
+                       Namespace: "default",
+               },
+               Spec: corev1.ServiceSpec{
+                       Type: corev1.ServiceTypeClusterIP,
+                       Ports: []corev1.ServicePort{
+                               {
+                                       Port: 80,
+                                       Name: "http",
+                               },
+                       },
+               },
+       }
+
+       tctx := &provider.TranslateContext{
+               Services: map[types.NamespacedName]*corev1.Service{
+                       {Namespace: "default", Name: "test-service"}: svc,
+               },
+               EndpointSlices: 
map[types.NamespacedName][]discoveryv1.EndpointSlice{},
+       }
+
+       result, err := translator.TranslateIngress(tctx, ingress)
+
+       assert.NoError(t, err)
+       assert.NotNil(t, result)
+       assert.Len(t, result.Services, 1)
+
+       service := result.Services[0]
+       assert.NotNil(t, service)
+       assert.NotNil(t, service.Plugins)
+
+       // Verify CORS plugin is NOT configured
+       _, exists := service.Plugins[adctypes.PluginCORS]
+       assert.False(t, exists, "CORS plugin should not be present")
+}

Reply via email to