Skip to content

Commit 6b70857

Browse files
authored
feat: support cors annotations for ingress (#2618)
Signed-off-by: Ashing Zheng <[email protected]>
1 parent d6cf0ea commit 6b70857

File tree

8 files changed

+185
-97
lines changed

8 files changed

+185
-97
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one or more
2+
// contributor license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright ownership.
4+
// The ASF licenses this file to You under the Apache License, Version 2.0
5+
// (the "License"); you may not use this file except in compliance with
6+
// the License. You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package plugins
17+
18+
import (
19+
adctypes "github.com/apache/apisix-ingress-controller/api/adc"
20+
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
21+
)
22+
23+
type cors struct{}
24+
25+
// NewCorsHandler creates a handler to convert annotations about
26+
// CORS to APISIX cors plugin.
27+
func NewCorsHandler() PluginAnnotationsHandler {
28+
return &cors{}
29+
}
30+
31+
func (c *cors) PluginName() string {
32+
return "cors"
33+
}
34+
35+
func (c *cors) Handle(e annotations.Extractor) (any, error) {
36+
if !e.GetBoolAnnotation(annotations.AnnotationsEnableCors) {
37+
return nil, nil
38+
}
39+
40+
return &adctypes.CorsConfig{
41+
AllowOrigins: e.GetStringAnnotation(annotations.AnnotationsCorsAllowOrigin),
42+
AllowMethods: e.GetStringAnnotation(annotations.AnnotationsCorsAllowMethods),
43+
AllowHeaders: e.GetStringAnnotation(annotations.AnnotationsCorsAllowHeaders),
44+
}, nil
45+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one or more
2+
// contributor license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright ownership.
4+
// The ASF licenses this file to You under the Apache License, Version 2.0
5+
// (the "License"); you may not use this file except in compliance with
6+
// the License. You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package plugins
17+
18+
import (
19+
"testing"
20+
21+
"github.com/stretchr/testify/assert"
22+
23+
adctypes "github.com/apache/apisix-ingress-controller/api/adc"
24+
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
25+
)
26+
27+
func TestCorsHandler(t *testing.T) {
28+
anno := map[string]string{
29+
annotations.AnnotationsEnableCors: "true",
30+
annotations.AnnotationsCorsAllowHeaders: "abc,def",
31+
annotations.AnnotationsCorsAllowOrigin: "https://a.com",
32+
annotations.AnnotationsCorsAllowMethods: "GET,HEAD",
33+
}
34+
p := NewCorsHandler()
35+
out, err := p.Handle(annotations.NewExtractor(anno))
36+
assert.Nil(t, err, "checking given error")
37+
config := out.(*adctypes.CorsConfig)
38+
assert.Equal(t, "abc,def", config.AllowHeaders)
39+
assert.Equal(t, "https://a.com", config.AllowOrigins)
40+
assert.Equal(t, "GET,HEAD", config.AllowMethods)
41+
42+
assert.Equal(t, "cors", p.PluginName())
43+
44+
anno[annotations.AnnotationsEnableCors] = "false"
45+
out, err = p.Handle(annotations.NewExtractor(anno))
46+
assert.Nil(t, err, "checking given error")
47+
assert.Nil(t, out, "checking given output")
48+
}

internal/adc/translator/annotations/plugins/plugins.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ var (
3737

3838
handlers = []PluginAnnotationsHandler{
3939
NewRedirectHandler(),
40+
NewCorsHandler(),
4041
}
4142
)
4243

internal/adc/translator/annotations_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,24 @@ func TestTranslateIngressAnnotations(t *testing.T) {
189189
},
190190
},
191191
},
192+
{
193+
name: "cors plugin",
194+
anno: map[string]string{
195+
annotations.AnnotationsEnableCors: "true",
196+
annotations.AnnotationsCorsAllowOrigin: "https://example.com",
197+
annotations.AnnotationsCorsAllowHeaders: "header-a,header-b",
198+
annotations.AnnotationsCorsAllowMethods: "GET,POST",
199+
},
200+
expected: &IngressConfig{
201+
Plugins: adctypes.Plugins{
202+
"cors": &adctypes.CorsConfig{
203+
AllowOrigins: "https://example.com",
204+
AllowHeaders: "header-a,header-b",
205+
AllowMethods: "GET,POST",
206+
},
207+
},
208+
},
209+
},
192210
}
193211

194212
for _, tt := range tests {

internal/webhook/v1/ingress_webhook.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,6 @@ var unsupportedAnnotations = []string{
4242
"k8s.apisix.apache.org/use-regex",
4343
"k8s.apisix.apache.org/enable-websocket",
4444
"k8s.apisix.apache.org/plugin-config-name",
45-
"k8s.apisix.apache.org/enable-cors",
46-
"k8s.apisix.apache.org/cors-allow-origin",
47-
"k8s.apisix.apache.org/cors-allow-headers",
48-
"k8s.apisix.apache.org/cors-allow-methods",
4945
"k8s.apisix.apache.org/enable-csrf",
5046
"k8s.apisix.apache.org/csrf-key",
5147
"k8s.apisix.apache.org/rewrite-target",

internal/webhook/v1/ingress_webhook_test.go

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -104,30 +104,6 @@ func TestIngressCustomValidator_ValidateCreate_SupportedAnnotations(t *testing.T
104104
assert.Empty(t, warnings)
105105
}
106106

107-
func TestIngressCustomValidator_ValidateUpdate_UnsupportedAnnotations(t *testing.T) {
108-
validator := buildIngressValidator(t)
109-
oldObj := &networkingv1.Ingress{}
110-
obj := &networkingv1.Ingress{
111-
ObjectMeta: metav1.ObjectMeta{
112-
Name: "test-ingress",
113-
Namespace: "default",
114-
Annotations: map[string]string{
115-
"k8s.apisix.apache.org/enable-cors": "true",
116-
"k8s.apisix.apache.org/cors-allow-origin": "*",
117-
},
118-
},
119-
}
120-
121-
warnings, err := validator.ValidateUpdate(context.TODO(), oldObj, obj)
122-
assert.NoError(t, err)
123-
assert.Len(t, warnings, 2)
124-
125-
// Check that warnings contain the expected unsupported annotations
126-
warningsStr := strings.Join(warnings, " ")
127-
assert.Contains(t, warningsStr, "k8s.apisix.apache.org/enable-cors")
128-
assert.Contains(t, warningsStr, "k8s.apisix.apache.org/cors-allow-origin")
129-
}
130-
131107
func TestIngressCustomValidator_ValidateDelete_NoWarnings(t *testing.T) {
132108
validator := buildIngressValidator(t)
133109
obj := &networkingv1.Ingress{

test/e2e/ingress/annotations.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package ingress
1919

2020
import (
2121
"context"
22+
"encoding/json"
2223
"fmt"
2324
"net/http"
2425
"time"
@@ -102,6 +103,31 @@ spec:
102103
port:
103104
number: 443
104105
`
106+
107+
ingressCORS = `
108+
apiVersion: networking.k8s.io/v1
109+
kind: Ingress
110+
metadata:
111+
name: cors
112+
annotations:
113+
k8s.apisix.apache.org/enable-cors: "true"
114+
k8s.apisix.apache.org/cors-allow-origin: "https://allowed.example"
115+
k8s.apisix.apache.org/cors-allow-methods: "GET,POST"
116+
k8s.apisix.apache.org/cors-allow-headers: "Origin,Authorization"
117+
spec:
118+
ingressClassName: %s
119+
rules:
120+
- host: cors.example
121+
http:
122+
paths:
123+
- path: /get
124+
pathType: Exact
125+
backend:
126+
service:
127+
name: nginx
128+
port:
129+
number: 80
130+
`
105131
)
106132
BeforeEach(func() {
107133
s.DeployNginx(framework.NginxOptions{
@@ -167,6 +193,53 @@ spec:
167193
Expect(upstreams[0].Timeout.Send).To(Equal(3), "checking Upstream send timeout")
168194
Expect(upstreams[0].Timeout.Connect).To(Equal(4), "checking Upstream connect timeout")
169195
})
196+
197+
It("cors annotations", func() {
198+
Expect(s.CreateResourceFromString(fmt.Sprintf(ingressCORS, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
199+
200+
s.RequestAssert(&scaffold.RequestAssert{
201+
Method: "GET",
202+
Path: "/get",
203+
Host: "cors.example",
204+
Headers: map[string]string{
205+
"Origin": "https://allowed.example",
206+
},
207+
Checks: []scaffold.ResponseCheckFunc{
208+
scaffold.WithExpectedStatus(http.StatusOK),
209+
scaffold.WithExpectedHeaders(map[string]string{
210+
"Access-Control-Allow-Origin": "https://allowed.example",
211+
"Access-Control-Allow-Methods": "GET,POST",
212+
"Access-Control-Allow-Headers": "Origin,Authorization",
213+
}),
214+
},
215+
})
216+
217+
s.RequestAssert(&scaffold.RequestAssert{
218+
Method: "GET",
219+
Path: "/get",
220+
Host: "cors.example",
221+
Headers: map[string]string{
222+
"Origin": "https://blocked.example",
223+
},
224+
Checks: []scaffold.ResponseCheckFunc{
225+
scaffold.WithExpectedStatus(http.StatusOK),
226+
scaffold.WithExpectedNotHeader("Access-Control-Allow-Origin"),
227+
},
228+
})
229+
230+
routes, err := s.DefaultDataplaneResource().Route().List(context.Background())
231+
Expect(err).NotTo(HaveOccurred(), "listing Service")
232+
Expect(routes).To(HaveLen(1), "checking Route length")
233+
Expect(routes[0].Plugins).To(HaveKey("cors"), "checking Route plugins")
234+
jsonBytes, err := json.Marshal(routes[0].Plugins["cors"])
235+
Expect(err).NotTo(HaveOccurred(), "marshalling cors plugin config")
236+
var corsConfig map[string]any
237+
err = json.Unmarshal(jsonBytes, &corsConfig)
238+
Expect(err).NotTo(HaveOccurred(), "unmarshalling cors plugin config")
239+
Expect(corsConfig["allow_origins"]).To(Equal("https://allowed.example"), "checking cors allow origins")
240+
Expect(corsConfig["allow_methods"]).To(Equal("GET,POST"), "checking cors allow methods")
241+
Expect(corsConfig["allow_headers"]).To(Equal("Origin,Authorization"), "checking cors allow headers")
242+
})
170243
})
171244

172245
Context("Plugins", func() {

test/e2e/webhook/ingress.go

Lines changed: 0 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -87,74 +87,5 @@ spec:
8787
})
8888
})
8989

90-
It("should warn about unsupported annotations on update", func() {
91-
By("creating Ingress without unsupported annotations")
92-
initialIngressYAML := fmt.Sprintf(`
93-
apiVersion: networking.k8s.io/v1
94-
kind: Ingress
95-
metadata:
96-
name: test-webhook-update
97-
namespace: %s
98-
spec:
99-
ingressClassName: %s
100-
rules:
101-
- host: webhook-test-update.example.com
102-
http:
103-
paths:
104-
- path: /
105-
pathType: Prefix
106-
backend:
107-
service:
108-
name: httpbin-service-e2e-test
109-
port:
110-
number: 80
111-
`, s.Namespace(), s.Namespace())
112-
113-
output, err := s.CreateResourceFromStringAndGetOutput(initialIngressYAML)
114-
Expect(err).ShouldNot(HaveOccurred())
115-
Expect(output).ShouldNot(ContainSubstring(`Warning`))
116-
117-
s.RequestAssert(&scaffold.RequestAssert{
118-
Method: "GET",
119-
Path: "/get",
120-
Host: "webhook-test-update.example.com",
121-
Check: scaffold.WithExpectedStatus(http.StatusOK),
122-
})
123-
124-
By("updating Ingress with unsupported annotations")
125-
updatedIngressYAML := fmt.Sprintf(`
126-
apiVersion: networking.k8s.io/v1
127-
kind: Ingress
128-
metadata:
129-
name: test-webhook-update
130-
namespace: %s
131-
annotations:
132-
k8s.apisix.apache.org/enable-cors: "true"
133-
spec:
134-
ingressClassName: %s
135-
rules:
136-
- host: webhook-test-update.example.com
137-
http:
138-
paths:
139-
- path: /
140-
pathType: Prefix
141-
backend:
142-
service:
143-
name: httpbin-service-e2e-test
144-
port:
145-
number: 80
146-
`, s.Namespace(), s.Namespace())
147-
148-
output, err = s.CreateResourceFromStringAndGetOutput(updatedIngressYAML)
149-
Expect(err).ShouldNot(HaveOccurred())
150-
Expect(output).To(ContainSubstring(`Warning: Annotation 'k8s.apisix.apache.org/enable-cors' is not supported`))
151-
152-
s.RequestAssert(&scaffold.RequestAssert{
153-
Method: "GET",
154-
Path: "/get",
155-
Host: "webhook-test-update.example.com",
156-
Check: scaffold.WithExpectedStatus(http.StatusOK),
157-
})
158-
})
15990
})
16091
})

0 commit comments

Comments
 (0)