diff --git a/internal/adc/translator/annotations.go b/internal/adc/translator/annotations.go new file mode 100644 index 00000000..28319b65 --- /dev/null +++ b/internal/adc/translator/annotations.go @@ -0,0 +1,69 @@ +// 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" + "fmt" + + "github.com/imdario/mergo" + + "github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations" + "github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/upstream" +) + +// Structure extracted by Ingress Resource +type IngressConfig struct { + Upstream upstream.Upstream +} + +// parsers registered for ingress annotations +var ingressAnnotationParsers = map[string]annotations.IngressAnnotationsParser{ + "upstream": upstream.NewParser(), +} + +func (t *Translator) TranslateIngressAnnotations(anno map[string]string) *IngressConfig { + if len(anno) == 0 { + return nil + } + ing := &IngressConfig{} + if err := translateAnnotations(anno, ing); err != nil { + t.Log.Error(err, "failed to translate ingress annotations", "annotations", anno) + } + return ing +} + +func translateAnnotations(anno map[string]string, dst any) error { + extractor := annotations.NewExtractor(anno) + data := make(map[string]any) + var errs []error + + for name, parser := range ingressAnnotationParsers { + out, err := parser.Parse(extractor) + if err != nil { + errs = append(errs, fmt.Errorf("parse %s: %w", name, err)) + continue + } + if out != nil { + data[name] = out + } + } + + if err := mergo.MapWithOverwrite(dst, data); err != nil { + errs = append(errs, fmt.Errorf("merge: %w", err)) + } + return errors.Join(errs...) +} diff --git a/internal/adc/translator/annotations/upstream/upstream.go b/internal/adc/translator/annotations/upstream/upstream.go new file mode 100644 index 00000000..881e02b9 --- /dev/null +++ b/internal/adc/translator/annotations/upstream/upstream.go @@ -0,0 +1,88 @@ +// 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 upstream + +import ( + "fmt" + "strconv" + "strings" + + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" + "github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations" +) + +func NewParser() annotations.IngressAnnotationsParser { + return &Upstream{} +} + +type Upstream struct { + Scheme string + Retries int + TimeoutRead int + TimeoutConnect int + TimeoutSend int +} + +var validSchemes = map[string]struct{}{ + apiv2.SchemeHTTP: {}, + apiv2.SchemeHTTPS: {}, + apiv2.SchemeGRPC: {}, + apiv2.SchemeGRPCS: {}, +} + +func (u Upstream) Parse(e annotations.Extractor) (any, error) { + if scheme := strings.ToLower(e.GetStringAnnotation(annotations.AnnotationsUpstreamScheme)); scheme != "" { + if _, ok := validSchemes[scheme]; ok { + u.Scheme = scheme + } else { + return nil, fmt.Errorf("invalid upstream scheme: %s", scheme) + } + } + + if retry := e.GetStringAnnotation(annotations.AnnotationsUpstreamRetry); retry != "" { + t, err := strconv.Atoi(retry) + if err != nil { + return nil, fmt.Errorf("could not parse retry as an integer: %s", err.Error()) + } + u.Retries = t + } + + if timeoutConnect := strings.TrimSuffix(e.GetStringAnnotation(annotations.AnnotationsUpstreamTimeoutConnect), "s"); timeoutConnect != "" { + t, err := strconv.Atoi(timeoutConnect) + if err != nil { + return nil, fmt.Errorf("could not parse timeout as an integer: %s", err.Error()) + } + u.TimeoutConnect = t + } + + if timeoutRead := strings.TrimSuffix(e.GetStringAnnotation(annotations.AnnotationsUpstreamTimeoutRead), "s"); timeoutRead != "" { + t, err := strconv.Atoi(timeoutRead) + if err != nil { + return nil, fmt.Errorf("could not parse timeout as an integer: %s", err.Error()) + } + u.TimeoutRead = t + } + + if timeoutSend := strings.TrimSuffix(e.GetStringAnnotation(annotations.AnnotationsUpstreamTimeoutSend), "s"); timeoutSend != "" { + t, err := strconv.Atoi(timeoutSend) + if err != nil { + return nil, fmt.Errorf("could not parse timeout as an integer: %s", err.Error()) + } + u.TimeoutSend = t + } + + return u, nil +} diff --git a/internal/adc/translator/annotations/upstream/upstream_test.go b/internal/adc/translator/annotations/upstream/upstream_test.go new file mode 100644 index 00000000..55d7459d --- /dev/null +++ b/internal/adc/translator/annotations/upstream/upstream_test.go @@ -0,0 +1,98 @@ +// 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 upstream + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations" +) + +func TestIPRestrictionHandler(t *testing.T) { + anno := map[string]string{ + annotations.AnnotationsUpstreamScheme: "grpcs", + } + u := NewParser() + + out, err := u.Parse(annotations.NewExtractor(anno)) + assert.Nil(t, err, "checking given error") + + ups, ok := out.(Upstream) + if !ok { + t.Fatalf("could not parse upstream") + } + assert.Equal(t, "grpcs", ups.Scheme) + + anno[annotations.AnnotationsUpstreamScheme] = "gRPC" + out, err = u.Parse(annotations.NewExtractor(anno)) + ups, ok = out.(Upstream) + if !ok { + t.Fatalf("could not parse upstream") + } + assert.Nil(t, err, "checking given error") + assert.Equal(t, "grpc", ups.Scheme) + + anno[annotations.AnnotationsUpstreamScheme] = "nothing" + out, err = u.Parse(annotations.NewExtractor(anno)) + assert.NotNil(t, err, "checking given error") + assert.Nil(t, out, "checking given output") +} + +func TestRetryParsing(t *testing.T) { + anno := map[string]string{ + annotations.AnnotationsUpstreamRetry: "2", + } + u := NewParser() + out, err := u.Parse(annotations.NewExtractor(anno)) + assert.Nil(t, err, "checking given error") + ups, ok := out.(Upstream) + if !ok { + t.Fatalf("could not parse upstream") + } + assert.Nil(t, err, "checking given error") + assert.Equal(t, 2, ups.Retries) + + anno[annotations.AnnotationsUpstreamRetry] = "asdf" + out, err = u.Parse(annotations.NewExtractor(anno)) + assert.NotNil(t, err, "checking given error") + assert.Nil(t, out, "checking given output") +} + +func TestTimeoutParsing(t *testing.T) { + anno := map[string]string{ + annotations.AnnotationsUpstreamTimeoutConnect: "2s", + annotations.AnnotationsUpstreamTimeoutRead: "3s", + annotations.AnnotationsUpstreamTimeoutSend: "4s", + } + u := NewParser() + out, err := u.Parse(annotations.NewExtractor(anno)) + assert.Nil(t, err, "checking given error") + + ups, ok := out.(Upstream) + if !ok { + t.Fatalf("could not parse upstream") + } + assert.Nil(t, err, "checking given error") + assert.Equal(t, 2, ups.TimeoutConnect) + assert.Equal(t, 3, ups.TimeoutRead) + assert.Equal(t, 4, ups.TimeoutSend) + anno[annotations.AnnotationsUpstreamRetry] = "asdf" + out, err = u.Parse(annotations.NewExtractor(anno)) + assert.NotNil(t, err, "checking given error") + assert.Nil(t, out, "checking given output") +} diff --git a/internal/adc/translator/annotations_test.go b/internal/adc/translator/annotations_test.go new file mode 100644 index 00000000..8216be3f --- /dev/null +++ b/internal/adc/translator/annotations_test.go @@ -0,0 +1,174 @@ +// 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" + + "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, + }, + }, + }, + } + + 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/adc/translator/ingress.go b/internal/adc/translator/ingress.go index f17b159f..64ef6b26 100644 --- a/internal/adc/translator/ingress.go +++ b/internal/adc/translator/ingress.go @@ -18,6 +18,7 @@ package translator import ( + "cmp" "fmt" "strings" @@ -74,8 +75,48 @@ func (t *Translator) TranslateIngress(tctx *provider.TranslateContext, obj *netw labels := label.GenLabel(obj) + config := t.TranslateIngressAnnotations(obj.Annotations) + + t.Log.V(1).Info("translating Ingress Annotations", "config", config) + // handle TLS configuration, convert to SSL objects +<<<<<<< HEAD for _, tls := range obj.Spec.TLS { +======= + if err := t.translateIngressTLSSection(tctx, obj, result, labels); err != nil { + return nil, err + } + + // process Ingress rules, convert to Service and Route objects + for i, rule := range obj.Spec.Rules { + if rule.HTTP == nil { + continue + } + + hosts := []string{} + if rule.Host != "" { + hosts = append(hosts, rule.Host) + } + + for j, path := range rule.HTTP.Paths { + index := fmt.Sprintf("%d-%d", i, j) + if svc := t.buildServiceFromIngressPath(tctx, obj, config, &path, index, hosts, labels); svc != nil { + result.Services = append(result.Services, svc) + } + } + } + + return result, nil +} + +func (t *Translator) translateIngressTLSSection( + tctx *provider.TranslateContext, + obj *networkingv1.Ingress, + result *TranslateResult, + labels map[string]string, +) error { + for tlsIndex, tls := range obj.Spec.TLS { +>>>>>>> 2dc7ae60 (feat: support upstream scheme/retries/timeouts via ingress annotations (#2614)) if tls.SecretName == "" { continue } @@ -94,6 +135,7 @@ func (t *Translator) TranslateIngress(tctx *provider.TranslateContext, obj *netw result.SSL = append(result.SSL, ssl) } +<<<<<<< HEAD // process Ingress rules, convert to Service and Route objects for i, rule := range obj.Spec.Rules { // extract hostnames @@ -120,6 +162,36 @@ func (t *Translator) TranslateIngress(tctx *provider.TranslateContext, obj *netw // create an upstream upstream := adctypes.NewDefaultUpstream() +======= +func (t *Translator) buildServiceFromIngressPath( + tctx *provider.TranslateContext, + obj *networkingv1.Ingress, + config *IngressConfig, + path *networkingv1.HTTPIngressPath, + index string, + hosts []string, + labels map[string]string, +) *adctypes.Service { + if path.Backend.Service == nil { + return nil + } + + service := adctypes.NewDefaultService() + service.Labels = labels + service.Name = adctypes.ComposeServiceNameWithRule(obj.Namespace, obj.Name, index) + service.ID = id.GenID(service.Name) + service.Hosts = hosts + + upstream := adctypes.NewDefaultUpstream() + protocol := t.resolveIngressUpstream(tctx, obj, config, path.Backend.Service, upstream) + service.Upstream = upstream + + route := buildRouteFromIngressPath(obj, path, index, labels) + if protocol == internaltypes.AppProtocolWS || protocol == internaltypes.AppProtocolWSS { + route.EnableWebsocket = ptr.To(true) + } + service.Routes = []*adctypes.Route{route} +>>>>>>> 2dc7ae60 (feat: support upstream scheme/retries/timeouts via ingress annotations (#2614)) // get the EndpointSlice of the backend service backendService := path.Backend.Service @@ -128,6 +200,7 @@ func (t *Translator) TranslateIngress(tctx *provider.TranslateContext, obj *netw t.AttachBackendTrafficPolicyToUpstream(backendRef, tctx.BackendTrafficPolicies, upstream) } +<<<<<<< HEAD // get the service port configuration var servicePort int32 = 0 var servicePortName string @@ -136,6 +209,41 @@ func (t *Translator) TranslateIngress(tctx *provider.TranslateContext, obj *netw } else if backendService.Port.Name != "" { servicePortName = backendService.Port.Name } +======= +func (t *Translator) resolveIngressUpstream( + tctx *provider.TranslateContext, + obj *networkingv1.Ingress, + config *IngressConfig, + backendService *networkingv1.IngressServiceBackend, + upstream *adctypes.Upstream, +) string { + backendRef := convertBackendRef(obj.Namespace, backendService.Name, internaltypes.KindService) + t.AttachBackendTrafficPolicyToUpstream(backendRef, tctx.BackendTrafficPolicies, upstream) + if config != nil { + upConfig := config.Upstream + if upConfig.Scheme != "" { + upstream.Scheme = upConfig.Scheme + } + if upConfig.Retries > 0 { + upstream.Retries = ptr.To(int64(upConfig.Retries)) + } + if upConfig.TimeoutConnect > 0 || upConfig.TimeoutRead > 0 || upConfig.TimeoutSend > 0 { + upstream.Timeout = &adctypes.Timeout{ + Connect: cmp.Or(upConfig.TimeoutConnect, 60), + Read: cmp.Or(upConfig.TimeoutRead, 60), + Send: cmp.Or(upConfig.TimeoutSend, 60), + } + } + } + // determine service port/port name + var protocol string + var port intstr.IntOrString + if backendService.Port.Number != 0 { + port = intstr.FromInt32(backendService.Port.Number) + } else if backendService.Port.Name != "" { + port = intstr.FromString(backendService.Port.Name) + } +>>>>>>> 2dc7ae60 (feat: support upstream scheme/retries/timeouts via ingress annotations (#2614)) getService := tctx.Services[types.NamespacedName{ Namespace: obj.Namespace, @@ -218,7 +326,53 @@ func (t *Translator) TranslateIngress(tctx *provider.TranslateContext, obj *netw } } +<<<<<<< HEAD return result, nil +======= + endpointSlices := tctx.EndpointSlices[types.NamespacedName{ + Namespace: obj.Namespace, + Name: backendService.Name, + }] + if len(endpointSlices) > 0 { + upstream.Nodes = t.translateEndpointSliceForIngress(1, endpointSlices, getServicePort) + } + + return protocol +} + +func buildRouteFromIngressPath( + obj *networkingv1.Ingress, + path *networkingv1.HTTPIngressPath, + index string, + labels map[string]string, +) *adctypes.Route { + route := adctypes.NewDefaultRoute() + route.Name = adctypes.ComposeRouteName(obj.Namespace, obj.Name, index) + route.ID = id.GenID(route.Name) + route.Labels = labels + + uris := []string{path.Path} + if path.PathType != nil { + switch *path.PathType { + case networkingv1.PathTypePrefix: + // As per the specification of Ingress path matching rule: + // if the last element of the path is a substring of the + // last element in request path, it is not a match, e.g. /foo/bar + // matches /foo/bar/baz, but does not match /foo/barbaz. + // While in APISIX, /foo/bar matches both /foo/bar/baz and + // /foo/barbaz. + // In order to be conformant with Ingress specification, here + // we create two paths here, the first is the path itself + // (exact match), the other is path + "/*" (prefix match). + prefix := strings.TrimSuffix(path.Path, "/") + "/*" + uris = append(uris, prefix) + case networkingv1.PathTypeImplementationSpecific: + uris = []string{"/*"} + } + } + route.Uris = uris + return route +>>>>>>> 2dc7ae60 (feat: support upstream scheme/retries/timeouts via ingress annotations (#2614)) } // translateEndpointSliceForIngress create upstream nodes from EndpointSlice diff --git a/internal/webhook/v1/ingress_webhook.go b/internal/webhook/v1/ingress_webhook.go index 777d1e1b..2b8188c8 100644 --- a/internal/webhook/v1/ingress_webhook.go +++ b/internal/webhook/v1/ingress_webhook.go @@ -36,11 +36,6 @@ var unsupportedAnnotations = []string{ "k8s.apisix.apache.org/use-regex", "k8s.apisix.apache.org/enable-websocket", "k8s.apisix.apache.org/plugin-config-name", - "k8s.apisix.apache.org/upstream-scheme", - "k8s.apisix.apache.org/upstream-retries", - "k8s.apisix.apache.org/upstream-connect-timeout", - "k8s.apisix.apache.org/upstream-read-timeout", - "k8s.apisix.apache.org/upstream-send-timeout", "k8s.apisix.apache.org/enable-cors", "k8s.apisix.apache.org/cors-allow-origin", "k8s.apisix.apache.org/cors-allow-headers", diff --git a/test/e2e/framework/manifests/nginx.yaml b/test/e2e/framework/manifests/nginx.yaml index 053e383f..356f60f7 100644 --- a/test/e2e/framework/manifests/nginx.yaml +++ b/test/e2e/framework/manifests/nginx.yaml @@ -42,6 +42,50 @@ data: location / { return 200 'Hello, World!'; } +<<<<<<< HEAD +======= + + location /delay { + content_by_lua_block { + local delay = tonumber(ngx.var.arg_delay) or 0 + ngx.sleep(delay) + ngx.say("Slept for ", delay, " seconds") + } + } + + location /ws { + content_by_lua_block { + local server = require "resty.websocket.server" + local wb, err = server:new { + timeout = 5000, -- 5 seconds timeout + max_payload_len = 65535 -- max message length + } + if not wb then + ngx.log(ngx.ERR, "failed to create websocket: ", err) + return ngx.exit(444) + end + + while true do + local data, typ, err = wb:recv_frame() + if wb.fatal then + ngx.log(ngx.ERR, "failed to receive frame: ", err) + break + end + + if typ == "close" then + wb:send_close() + break + elseif typ == "text" then + wb:send_text(data) -- echo text + elseif typ == "binary" then + wb:send_binary(data) -- echo binary + elseif typ == "ping" then + wb:send_pong() + end + end + } + } +>>>>>>> 2dc7ae60 (feat: support upstream scheme/retries/timeouts via ingress annotations (#2614)) } } @@ -110,6 +154,24 @@ spec: port: 443 protocol: TCP targetPort: 443 +<<<<<<< HEAD +======= + appProtocol: https + - name: https-v2 + port: 7443 + protocol: TCP + targetPort: 443 + - name: ws + port: 8080 + protocol: TCP + targetPort: 80 + appProtocol: kubernetes.io/ws + - name: wss + port: 8443 + protocol: TCP + targetPort: 443 + appProtocol: kubernetes.io/wss +>>>>>>> 2dc7ae60 (feat: support upstream scheme/retries/timeouts via ingress annotations (#2614)) type: ClusterIP --- apiVersion: v1 diff --git a/test/e2e/ingress/annotations.go b/test/e2e/ingress/annotations.go new file mode 100644 index 00000000..3043d5b1 --- /dev/null +++ b/test/e2e/ingress/annotations.go @@ -0,0 +1,171 @@ +// 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" + "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 +` + ) + 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") + }) + }) +})