diff --git a/internal/adc/translator/annotations/plugins/cors.go b/internal/adc/translator/annotations/plugins/cors.go new file mode 100644 index 00000000..2b4cd3d6 --- /dev/null +++ b/internal/adc/translator/annotations/plugins/cors.go @@ -0,0 +1,45 @@ +// 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 plugins + +import ( + adctypes "github.com/apache/apisix-ingress-controller/api/adc" + "github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations" +) + +type cors struct{} + +// NewCorsHandler creates a handler to convert annotations about +// CORS to APISIX cors plugin. +func NewCorsHandler() PluginAnnotationsHandler { + return &cors{} +} + +func (c *cors) PluginName() string { + return "cors" +} + +func (c *cors) Handle(e annotations.Extractor) (any, error) { + if !e.GetBoolAnnotation(annotations.AnnotationsEnableCors) { + return nil, nil + } + + return &adctypes.CorsConfig{ + AllowOrigins: e.GetStringAnnotation(annotations.AnnotationsCorsAllowOrigin), + AllowMethods: e.GetStringAnnotation(annotations.AnnotationsCorsAllowMethods), + AllowHeaders: e.GetStringAnnotation(annotations.AnnotationsCorsAllowHeaders), + }, nil +} diff --git a/internal/adc/translator/annotations/plugins/cors_test.go b/internal/adc/translator/annotations/plugins/cors_test.go new file mode 100644 index 00000000..9b22f918 --- /dev/null +++ b/internal/adc/translator/annotations/plugins/cors_test.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 plugins + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + adctypes "github.com/apache/apisix-ingress-controller/api/adc" + "github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations" +) + +func TestCorsHandler(t *testing.T) { + anno := map[string]string{ + annotations.AnnotationsEnableCors: "true", + annotations.AnnotationsCorsAllowHeaders: "abc,def", + annotations.AnnotationsCorsAllowOrigin: "https://a.com", + annotations.AnnotationsCorsAllowMethods: "GET,HEAD", + } + p := NewCorsHandler() + out, err := p.Handle(annotations.NewExtractor(anno)) + assert.Nil(t, err, "checking given error") + config := out.(*adctypes.CorsConfig) + assert.Equal(t, "abc,def", config.AllowHeaders) + assert.Equal(t, "https://a.com", config.AllowOrigins) + assert.Equal(t, "GET,HEAD", config.AllowMethods) + + assert.Equal(t, "cors", p.PluginName()) + + anno[annotations.AnnotationsEnableCors] = "false" + out, err = p.Handle(annotations.NewExtractor(anno)) + assert.Nil(t, err, "checking given error") + assert.Nil(t, out, "checking given output") +} diff --git a/internal/adc/translator/annotations/plugins/plugins.go b/internal/adc/translator/annotations/plugins/plugins.go new file mode 100644 index 00000000..ee1e6206 --- /dev/null +++ b/internal/adc/translator/annotations/plugins/plugins.go @@ -0,0 +1,66 @@ +// 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 plugins + +import ( + logf "sigs.k8s.io/controller-runtime/pkg/log" + + adctypes "github.com/apache/apisix-ingress-controller/api/adc" + "github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations" +) + +// Handler abstracts the behavior so that the apisix-ingress-controller knows +// how to parse some annotations and convert them to APISIX plugins. +type PluginAnnotationsHandler interface { + // Handle parses the target annotation and converts it to the type-agnostic structure. + // The return value might be nil since some features have an explicit switch, users should + // judge whether Handle is failed by the second error value. + Handle(annotations.Extractor) (any, error) + // PluginName returns a string which indicates the target plugin name in APISIX. + PluginName() string +} + +var ( + log = logf.Log.WithName("annotations").WithName("plugins").WithName("parser") + + handlers = []PluginAnnotationsHandler{ + NewRedirectHandler(), + NewCorsHandler(), + } +) + +type plugins struct{} + +func NewParser() annotations.IngressAnnotationsParser { + return &plugins{} +} + +func (p *plugins) Parse(e annotations.Extractor) (any, error) { + plugins := make(adctypes.Plugins) + for _, handler := range handlers { + out, err := handler.Handle(e) + if err != nil { + log.Error(err, "Failed to handle annotation", "handler", handler.PluginName()) + continue + } + if out != nil { + plugins[handler.PluginName()] = out + } + } + if len(plugins) > 0 { + return plugins, nil + } + return nil, nil +} diff --git a/internal/adc/translator/annotations_test.go b/internal/adc/translator/annotations_test.go new file mode 100644 index 00000000..279d7beb --- /dev/null +++ b/internal/adc/translator/annotations_test.go @@ -0,0 +1,221 @@ +// 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 ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + + 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" +) + +type mockParser struct { + output any + err error +} + +func (m *mockParser) Parse(extractor annotations.Extractor) (any, error) { + return m.output, m.err +} + +func TestTranslateAnnotations(t *testing.T) { + tests := []struct { + name string + anno map[string]string + parsers map[string]annotations.IngressAnnotationsParser + expected any + expectErr bool + }{ + { + name: "successful parsing", + anno: map[string]string{"key1": "value1"}, + parsers: map[string]annotations.IngressAnnotationsParser{ + "key1": &mockParser{output: "parsedValue1", err: nil}, + }, + expected: map[string]any{"key1": "parsedValue1"}, + expectErr: false, + }, + { + name: "parsing with error", + anno: map[string]string{"key1": "value1"}, + parsers: map[string]annotations.IngressAnnotationsParser{ + "key1": &mockParser{output: nil, err: errors.New("parse error")}, + }, + expected: map[string]any{}, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + orig := ingressAnnotationParsers + defer func() { ingressAnnotationParsers = orig }() + + ingressAnnotationParsers = make(map[string]annotations.IngressAnnotationsParser) + for key, parser := range tt.parsers { + ingressAnnotationParsers[key] = parser + } + + dst := make(map[string]any) + err := translateAnnotations(tt.anno, &dst) + + if tt.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.expected, dst) + }) + } +} + +func TestTranslateIngressAnnotations(t *testing.T) { + tests := []struct { + name string + anno map[string]string + expected *IngressConfig + }{ + { + name: "no matching annotations", + anno: map[string]string{"upstream": "value1"}, + expected: &IngressConfig{}, + }, + { + name: "invalid scheme", + anno: map[string]string{annotations.AnnotationsUpstreamScheme: "invalid"}, + expected: &IngressConfig{}, + }, + { + name: "http scheme", + anno: map[string]string{annotations.AnnotationsUpstreamScheme: "https"}, + expected: &IngressConfig{ + Upstream: upstream.Upstream{ + Scheme: "https", + }, + }, + }, + { + name: "retries", + anno: map[string]string{annotations.AnnotationsUpstreamRetry: "3"}, + expected: &IngressConfig{ + Upstream: upstream.Upstream{ + Retries: 3, + }, + }, + }, + { + name: "read timeout", + anno: map[string]string{ + annotations.AnnotationsUpstreamTimeoutRead: "5s", + }, + expected: &IngressConfig{ + Upstream: upstream.Upstream{ + TimeoutRead: 5, + }, + }, + }, + { + name: "timeouts", + anno: map[string]string{ + annotations.AnnotationsUpstreamTimeoutRead: "5s", + annotations.AnnotationsUpstreamTimeoutSend: "6s", + annotations.AnnotationsUpstreamTimeoutConnect: "7s", + }, + expected: &IngressConfig{ + Upstream: upstream.Upstream{ + TimeoutRead: 5, + TimeoutSend: 6, + TimeoutConnect: 7, + }, + }, + }, + { + name: "timeout/scheme/retries", + anno: map[string]string{ + annotations.AnnotationsUpstreamTimeoutRead: "5s", + annotations.AnnotationsUpstreamScheme: "http", + annotations.AnnotationsUpstreamRetry: "2", + }, + expected: &IngressConfig{ + Upstream: upstream.Upstream{ + TimeoutRead: 5, + Scheme: "http", + Retries: 2, + }, + }, + }, + { + name: "redirect to https", + anno: map[string]string{ + annotations.AnnotationsHttpToHttps: "true", + }, + expected: &IngressConfig{ + Plugins: adctypes.Plugins{ + "redirect": &adctypes.RedirectConfig{ + HttpToHttps: true, + }, + }, + }, + }, + { + name: "redirect to specific uri", + anno: map[string]string{ + annotations.AnnotationsHttpRedirect: "/newpath", + annotations.AnnotationsHttpRedirectCode: "301", + }, + expected: &IngressConfig{ + Plugins: adctypes.Plugins{ + "redirect": &adctypes.RedirectConfig{ + URI: "/newpath", + RetCode: 301, + }, + }, + }, + }, + { + name: "cors plugin", + anno: map[string]string{ + annotations.AnnotationsEnableCors: "true", + annotations.AnnotationsCorsAllowOrigin: "https://example.com", + annotations.AnnotationsCorsAllowHeaders: "header-a,header-b", + annotations.AnnotationsCorsAllowMethods: "GET,POST", + }, + expected: &IngressConfig{ + Plugins: adctypes.Plugins{ + "cors": &adctypes.CorsConfig{ + AllowOrigins: "https://example.com", + AllowHeaders: "header-a,header-b", + AllowMethods: "GET,POST", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + translator := &Translator{} + result := translator.TranslateIngressAnnotations(tt.anno) + + assert.NotNil(t, result) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/internal/webhook/v1/ingress_webhook.go b/internal/webhook/v1/ingress_webhook.go index a2e5a44e..db7f0a81 100644 --- a/internal/webhook/v1/ingress_webhook.go +++ b/internal/webhook/v1/ingress_webhook.go @@ -41,6 +41,7 @@ var unsupportedAnnotations = []string{ "k8s.apisix.apache.org/use-regex", "k8s.apisix.apache.org/enable-websocket", "k8s.apisix.apache.org/plugin-config-name", +<<<<<<< HEAD "k8s.apisix.apache.org/upstream-scheme", "k8s.apisix.apache.org/upstream-retries", "k8s.apisix.apache.org/upstream-connect-timeout", @@ -50,6 +51,8 @@ var unsupportedAnnotations = []string{ "k8s.apisix.apache.org/cors-allow-origin", "k8s.apisix.apache.org/cors-allow-headers", "k8s.apisix.apache.org/cors-allow-methods", +======= +>>>>>>> 6b708576 (feat: support cors annotations for ingress (#2618)) "k8s.apisix.apache.org/enable-csrf", "k8s.apisix.apache.org/csrf-key", "k8s.apisix.apache.org/http-to-https", diff --git a/internal/webhook/v1/ingress_webhook_test.go b/internal/webhook/v1/ingress_webhook_test.go index 89f3fa6d..b775a819 100644 --- a/internal/webhook/v1/ingress_webhook_test.go +++ b/internal/webhook/v1/ingress_webhook_test.go @@ -104,30 +104,6 @@ func TestIngressCustomValidator_ValidateCreate_SupportedAnnotations(t *testing.T assert.Empty(t, warnings) } -func TestIngressCustomValidator_ValidateUpdate_UnsupportedAnnotations(t *testing.T) { - validator := buildIngressValidator(t) - oldObj := &networkingv1.Ingress{} - obj := &networkingv1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-ingress", - Namespace: "default", - Annotations: map[string]string{ - "k8s.apisix.apache.org/enable-cors": "true", - "k8s.apisix.apache.org/cors-allow-origin": "*", - }, - }, - } - - warnings, err := validator.ValidateUpdate(context.TODO(), oldObj, obj) - assert.NoError(t, err) - assert.Len(t, warnings, 2) - - // Check that warnings contain the expected unsupported annotations - warningsStr := strings.Join(warnings, " ") - assert.Contains(t, warningsStr, "k8s.apisix.apache.org/enable-cors") - assert.Contains(t, warningsStr, "k8s.apisix.apache.org/cors-allow-origin") -} - func TestIngressCustomValidator_ValidateDelete_NoWarnings(t *testing.T) { validator := buildIngressValidator(t) obj := &networkingv1.Ingress{ diff --git a/test/e2e/ingress/annotations.go b/test/e2e/ingress/annotations.go new file mode 100644 index 00000000..57e9b8ad --- /dev/null +++ b/test/e2e/ingress/annotations.go @@ -0,0 +1,332 @@ +// 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 ingress + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/utils/ptr" + + "github.com/apache/apisix-ingress-controller/test/e2e/framework" + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +var _ = Describe("Test Ingress With Annotations", Label("networking.k8s.io", "ingress"), func() { + s := scaffold.NewDefaultScaffold() + + Context("Upstream", func() { + var ( + ingressRetries = ` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: retries + annotations: + k8s.apisix.apache.org/upstream-retries: "3" +spec: + ingressClassName: %s + rules: + - host: nginx.example + http: + paths: + - path: /get + pathType: Exact + backend: + service: + name: nginx + port: + number: 80 +` + ingressSchemeHTTPS = ` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: https-backend + annotations: + k8s.apisix.apache.org/upstream-scheme: https +spec: + ingressClassName: %s + rules: + - host: nginx.example + http: + paths: + - path: /get + pathType: Exact + backend: + service: + name: nginx + port: + number: 7443 +` + + ingressTimeouts = ` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: timeouts + annotations: + k8s.apisix.apache.org/upstream-read-timeout: "2s" + k8s.apisix.apache.org/upstream-send-timeout: "3s" + k8s.apisix.apache.org/upstream-connect-timeout: "4s" +spec: + ingressClassName: %s + rules: + - host: nginx.example + http: + paths: + - path: /delay + pathType: Exact + backend: + service: + name: nginx + port: + number: 443 +` + + ingressCORS = ` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: cors + annotations: + k8s.apisix.apache.org/enable-cors: "true" + k8s.apisix.apache.org/cors-allow-origin: "https://allowed.example" + k8s.apisix.apache.org/cors-allow-methods: "GET,POST" + k8s.apisix.apache.org/cors-allow-headers: "Origin,Authorization" +spec: + ingressClassName: %s + rules: + - host: cors.example + http: + paths: + - path: /get + pathType: Exact + backend: + service: + name: nginx + port: + number: 80 +` + ) + BeforeEach(func() { + s.DeployNginx(framework.NginxOptions{ + Namespace: s.Namespace(), + Replicas: ptr.To(int32(1)), + }) + By("create GatewayProxy") + Expect(s.CreateResourceFromString(s.GetGatewayProxySpec())).NotTo(HaveOccurred(), "creating GatewayProxy") + + By("create IngressClass") + err := s.CreateResourceFromStringWithNamespace(s.GetIngressClassYaml(), "") + Expect(err).NotTo(HaveOccurred(), "creating IngressClass") + time.Sleep(5 * time.Second) + }) + It("retries", func() { + Expect(s.CreateResourceFromString(fmt.Sprintf(ingressRetries, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress") + + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/get", + Host: "nginx.example", + Check: scaffold.WithExpectedStatus(http.StatusOK), + }) + upstreams, err := s.DefaultDataplaneResource().Upstream().List(context.Background()) + Expect(err).NotTo(HaveOccurred(), "listing Upstream") + Expect(upstreams).To(HaveLen(1), "checking Upstream length") + Expect(upstreams[0].Retries).To(Equal(ptr.To(int64(3))), "checking Upstream retries") + }) + It("scheme", func() { + Expect(s.CreateResourceFromString(fmt.Sprintf(ingressSchemeHTTPS, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress") + + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/get", + Host: "nginx.example", + Check: scaffold.WithExpectedStatus(http.StatusOK), + }) + upstreams, err := s.DefaultDataplaneResource().Upstream().List(context.Background()) + Expect(err).NotTo(HaveOccurred(), "listing Upstream") + Expect(upstreams).To(HaveLen(1), "checking Upstream length") + Expect(upstreams[0].Scheme).To(Equal("https"), "checking Upstream scheme") + }) + It("timeouts", func() { + Expect(s.CreateResourceFromString(fmt.Sprintf(ingressTimeouts, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress") + + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/delay", + Host: "nginx.example", + Check: scaffold.WithExpectedStatus(http.StatusOK), + }) + + _ = s.NewAPISIXClient().GET("/delay").WithQuery("delay", "10"). + WithHost("nginx.example").Expect().Status(http.StatusGatewayTimeout) + + _ = s.NewAPISIXClient().GET("/delay").WithHost("nginx.example").Expect().Status(http.StatusOK) + + upstreams, err := s.DefaultDataplaneResource().Upstream().List(context.Background()) + Expect(err).NotTo(HaveOccurred(), "listing Upstream") + Expect(upstreams).To(HaveLen(1), "checking Upstream length") + Expect(upstreams[0].Timeout).ToNot(BeNil(), "checking Upstream timeout") + Expect(upstreams[0].Timeout.Read).To(Equal(2), "checking Upstream read timeout") + Expect(upstreams[0].Timeout.Send).To(Equal(3), "checking Upstream send timeout") + Expect(upstreams[0].Timeout.Connect).To(Equal(4), "checking Upstream connect timeout") + }) + + It("cors annotations", func() { + Expect(s.CreateResourceFromString(fmt.Sprintf(ingressCORS, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress") + + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/get", + Host: "cors.example", + Headers: map[string]string{ + "Origin": "https://allowed.example", + }, + Checks: []scaffold.ResponseCheckFunc{ + scaffold.WithExpectedStatus(http.StatusOK), + scaffold.WithExpectedHeaders(map[string]string{ + "Access-Control-Allow-Origin": "https://allowed.example", + "Access-Control-Allow-Methods": "GET,POST", + "Access-Control-Allow-Headers": "Origin,Authorization", + }), + }, + }) + + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/get", + Host: "cors.example", + Headers: map[string]string{ + "Origin": "https://blocked.example", + }, + Checks: []scaffold.ResponseCheckFunc{ + scaffold.WithExpectedStatus(http.StatusOK), + scaffold.WithExpectedNotHeader("Access-Control-Allow-Origin"), + }, + }) + + routes, err := s.DefaultDataplaneResource().Route().List(context.Background()) + Expect(err).NotTo(HaveOccurred(), "listing Service") + Expect(routes).To(HaveLen(1), "checking Route length") + Expect(routes[0].Plugins).To(HaveKey("cors"), "checking Route plugins") + jsonBytes, err := json.Marshal(routes[0].Plugins["cors"]) + Expect(err).NotTo(HaveOccurred(), "marshalling cors plugin config") + var corsConfig map[string]any + err = json.Unmarshal(jsonBytes, &corsConfig) + Expect(err).NotTo(HaveOccurred(), "unmarshalling cors plugin config") + Expect(corsConfig["allow_origins"]).To(Equal("https://allowed.example"), "checking cors allow origins") + Expect(corsConfig["allow_methods"]).To(Equal("GET,POST"), "checking cors allow methods") + Expect(corsConfig["allow_headers"]).To(Equal("Origin,Authorization"), "checking cors allow headers") + }) + }) + + Context("Plugins", func() { + var ( + tohttps = ` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: tohttps + annotations: + k8s.apisix.apache.org/http-to-https: "true" +spec: + ingressClassName: %s + rules: + - host: httpbin.example + http: + paths: + - path: /get + pathType: Exact + backend: + service: + name: httpbin-service-e2e-test + port: + number: 80 +` + redirect = ` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: redirect + annotations: + k8s.apisix.apache.org/http-redirect: "/anything$uri" + k8s.apisix.apache.org/http-redirect-code: "308" +spec: + ingressClassName: %s + rules: + - host: httpbin.example + http: + paths: + - path: /ip + pathType: Exact + backend: + service: + name: httpbin-service-e2e-test + port: + number: 80 +` + ) + BeforeEach(func() { + By("create GatewayProxy") + Expect(s.CreateResourceFromString(s.GetGatewayProxySpec())).NotTo(HaveOccurred(), "creating GatewayProxy") + + By("create IngressClass") + err := s.CreateResourceFromStringWithNamespace(s.GetIngressClassYaml(), "") + Expect(err).NotTo(HaveOccurred(), "creating IngressClass") + time.Sleep(5 * time.Second) + }) + It("redirect", func() { + Expect(s.CreateResourceFromString(fmt.Sprintf(tohttps, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress") + Expect(s.CreateResourceFromString(fmt.Sprintf(redirect, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress") + + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/get", + Host: "httpbin.example", + Check: scaffold.WithExpectedStatus(http.StatusMovedPermanently), + }) + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/ip", + Host: "httpbin.example", + Check: scaffold.WithExpectedStatus(http.StatusPermanentRedirect), + }) + + _ = s.NewAPISIXClient(). + GET("/get"). + WithHost("httpbin.example"). + Expect(). + Status(http.StatusMovedPermanently). + Header("Location").IsEqual("https://httpbin.example:9443/get") + + _ = s.NewAPISIXClient(). + GET("/ip"). + WithHost("httpbin.example"). + Expect(). + Status(http.StatusPermanentRedirect). + Header("Location").IsEqual("/anything/ip") + }) + }) +}) diff --git a/test/e2e/webhook/ingress.go b/test/e2e/webhook/ingress.go index 37608fb2..69a7f3ef 100644 --- a/test/e2e/webhook/ingress.go +++ b/test/e2e/webhook/ingress.go @@ -87,74 +87,5 @@ spec: }) }) - It("should warn about unsupported annotations on update", func() { - By("creating Ingress without unsupported annotations") - initialIngressYAML := fmt.Sprintf(` -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: test-webhook-update - namespace: %s -spec: - ingressClassName: %s - rules: - - host: webhook-test-update.example.com - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: httpbin-service-e2e-test - port: - number: 80 -`, s.Namespace(), s.Namespace()) - - output, err := s.CreateResourceFromStringAndGetOutput(initialIngressYAML) - Expect(err).ShouldNot(HaveOccurred()) - Expect(output).ShouldNot(ContainSubstring(`Warning`)) - - s.RequestAssert(&scaffold.RequestAssert{ - Method: "GET", - Path: "/get", - Host: "webhook-test-update.example.com", - Check: scaffold.WithExpectedStatus(http.StatusOK), - }) - - By("updating Ingress with unsupported annotations") - updatedIngressYAML := fmt.Sprintf(` -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: test-webhook-update - namespace: %s - annotations: - k8s.apisix.apache.org/enable-cors: "true" -spec: - ingressClassName: %s - rules: - - host: webhook-test-update.example.com - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: httpbin-service-e2e-test - port: - number: 80 -`, s.Namespace(), s.Namespace()) - - output, err = s.CreateResourceFromStringAndGetOutput(updatedIngressYAML) - Expect(err).ShouldNot(HaveOccurred()) - Expect(output).To(ContainSubstring(`Warning: Annotation 'k8s.apisix.apache.org/enable-cors' is not supported`)) - - s.RequestAssert(&scaffold.RequestAssert{ - Method: "GET", - Path: "/get", - Host: "webhook-test-update.example.com", - Check: scaffold.WithExpectedStatus(http.StatusOK), - }) - }) }) })