From a4e7ea93c97ff7a2bb8dbc9d4e6bc1559113ed14 Mon Sep 17 00:00:00 2001 From: Vladimir Videlov Date: Thu, 19 Sep 2024 16:51:43 +0200 Subject: [PATCH 01/27] Short host validation --- apis/gateway/v2alpha1/apirule_types.go | 5 +- .../gateway.kyma-project.io_apirules.yaml | 8 +- .../api_controller_integration_test.go | 39 +-- .../v2alpha1/04-10-apirule-custom-resource.md | 2 +- .../v2alpha1/authorizationpolicy/creator.go | 2 +- internal/validation/v2alpha1/hosts.go | 74 ++++- internal/validation/v2alpha1/hosts_test.go | 281 +++++++++++++++++- internal/validation/v2alpha1/v2alpha1.go | 2 +- 8 files changed, 354 insertions(+), 59 deletions(-) diff --git a/apis/gateway/v2alpha1/apirule_types.go b/apis/gateway/v2alpha1/apirule_types.go index 159852380..984e8754e 100644 --- a/apis/gateway/v2alpha1/apirule_types.go +++ b/apis/gateway/v2alpha1/apirule_types.go @@ -55,10 +55,9 @@ type APIRuleSpec struct { Timeout *Timeout `json:"timeout,omitempty"` } -// Host is the URL of the exposed service. -// +kubebuilder:validation:MinLength=3 +// Host is the URL of the exposed service. We support short names. +// +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=255 -// +kubebuilder:validation:XValidation:rule=`self.matches('^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$')`,message="Host is not Fully Qualified Domain Name" type Host string // APIRuleStatus describes the observed state of ApiRule. diff --git a/config/crd/bases/gateway.kyma-project.io_apirules.yaml b/config/crd/bases/gateway.kyma-project.io_apirules.yaml index 48faa4a68..9b47432b4 100644 --- a/config/crd/bases/gateway.kyma-project.io_apirules.yaml +++ b/config/crd/bases/gateway.kyma-project.io_apirules.yaml @@ -363,13 +363,11 @@ spec: hosts: description: Specifies the URLs of the exposed service. items: - description: Host is the URL of the exposed service. + description: Host is the URL of the exposed service. We support + short names. maxLength: 255 - minLength: 3 + minLength: 1 type: string - x-kubernetes-validations: - - message: Host is not Fully Qualified Domain Name - rule: self.matches('^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$') maxItems: 1 minItems: 1 type: array diff --git a/controllers/gateway/api_controller_integration_test.go b/controllers/gateway/api_controller_integration_test.go index 9bb45defe..286322f99 100644 --- a/controllers/gateway/api_controller_integration_test.go +++ b/controllers/gateway/api_controller_integration_test.go @@ -1533,11 +1533,11 @@ var _ = Describe("APIRule Controller", Serial, func() { Expect(err.Error()).To(ContainSubstring("spec.hosts[0]: Too long: may not be longer than 255")) }) - invalidHelper := func(host gatewayv2alpha1.Host) { + It("should not create an APIRule with host name shorter than 1 character", func() { // given apiRuleName := generateTestName(testNameBase, testIDLength) serviceName := testServiceNameBase - serviceHosts := []*gatewayv2alpha1.Host{&host} + serviceHosts := []*gatewayv2alpha1.Host{ptr.To(gatewayv2alpha1.Host(""))} rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) rule.NoAuth = ptr.To(true) @@ -1552,40 +1552,7 @@ var _ = Describe("APIRule Controller", Serial, func() { serviceTeardown(svc) }() Expect(err).Should(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("spec.hosts[0]: Invalid value: \"string\": Host is not Fully Qualified Domain Name")) - } - - It("should not create an APIRule with an empty host", func() { - invalidHelper("") - }) - - It("should not create an APIRule with host name without domain", func() { - invalidHelper("example-com") - }) - - It("should not create an APIRule when host name has uppercase letters", func() { - invalidHelper("Example.Com") - }) - - It("should not create an APIRule with host name segment longer than 63 characters", func() { - serviceHost := gatewayv2alpha1.Host(strings.Repeat("a", 64) + ".com") - invalidHelper(serviceHost) - }) - - It("should not create an APIRule when any domain label is empty", func() { - invalidHelper("host..com") - }) - - It("should not create an APIRule when top level domain is too short", func() { - invalidHelper("host.with.tld.too.short.x") - }) - - It("should not create an APIRule when host contains wrong characters", func() { - invalidHelper("*example.com") - }) - - It("should not create an APIRule when host starts with a dash", func() { - invalidHelper("-example.com") + Expect(err.Error()).To(ContainSubstring("spec.hosts[0] in body should be at least 1 chars long")) }) }) diff --git a/docs/user/custom-resources/apirule/v2alpha1/04-10-apirule-custom-resource.md b/docs/user/custom-resources/apirule/v2alpha1/04-10-apirule-custom-resource.md index 9a7ae1454..93a3ae2f5 100644 --- a/docs/user/custom-resources/apirule/v2alpha1/04-10-apirule-custom-resource.md +++ b/docs/user/custom-resources/apirule/v2alpha1/04-10-apirule-custom-resource.md @@ -23,7 +23,7 @@ This table lists all parameters of APIRule `v2alpha1` CRD together with their de | **corsPolicy.allowCredentials** | **NO** | Specifies whether credentials are allowed in the **Access-Control-Allow-Credentials** CORS header. | None | | **corsPolicy.exposeHeaders** | **NO** | Specifies headers exposed with the **Access-Control-Expose-Headers** CORS header. | None | | **corsPolicy.maxAge** | **NO** | Specifies the maximum age of CORS policy cache. The value is provided in the **Access-Control-Max-Age** CORS header. | None | -| **hosts** | **YES** | Specifies the Service's communication address for inbound external traffic. It must be a fully qualified domain name in proper FQDN format: at least two domain labels with characters, numbers, or dashes. | FQDN format. | +| **hosts** | **YES** | Specifies the Service's communication address for inbound external traffic. It must be a fully qualified domain name (FQDN) in format: at least two domain labels with characters, numbers, or hypens. | FQDN format. | | **service.name** | **NO** | Specifies the name of the exposed Service. | None | | **service.namespace** | **NO** | Specifies the namespace of the exposed Service. | None | | **service.port** | **NO** | Specifies the communication port of the exposed Service. | None | diff --git a/internal/processing/processors/v2alpha1/authorizationpolicy/creator.go b/internal/processing/processors/v2alpha1/authorizationpolicy/creator.go index 1bd62eb37..dd278424f 100644 --- a/internal/processing/processors/v2alpha1/authorizationpolicy/creator.go +++ b/internal/processing/processors/v2alpha1/authorizationpolicy/creator.go @@ -27,7 +27,7 @@ type Creator interface { } type creator struct { - // Controls that requests to Ory Oathkeeper are also permitted when + // Controls that requests to Ory Oathkeeper are also permitted when // migrating from APIRule v1beta1 to v2alpha1. oryPassthrough bool } diff --git a/internal/validation/v2alpha1/hosts.go b/internal/validation/v2alpha1/hosts.go index 7972b3ebc..141f5eb78 100644 --- a/internal/validation/v2alpha1/hosts.go +++ b/internal/validation/v2alpha1/hosts.go @@ -2,13 +2,25 @@ package v2alpha1 import ( "fmt" + "regexp" + "strings" + gatewayv1beta1 "github.com/kyma-project/api-gateway/apis/gateway/v1beta1" gatewayv2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" "github.com/kyma-project/api-gateway/internal/validation" networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" ) -func validateHosts(parentAttributePath string, vsList networkingv1beta1.VirtualServiceList, apiRule *gatewayv2alpha1.APIRule) []validation.Failure { +const ( + fqdnMaxLength = 253 +) + +var ( + regexFqdn = regexp.MustCompile(`^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$`) + regexFqdnLabel = regexp.MustCompile("^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$") +) + +func validateHosts(parentAttributePath string, vsList networkingv1beta1.VirtualServiceList, gwList networkingv1beta1.GatewayList, apiRule *gatewayv2alpha1.APIRule) []validation.Failure { var failures []validation.Failure hostsAttributePath := parentAttributePath + ".hosts" @@ -22,6 +34,31 @@ func validateHosts(parentAttributePath string, vsList networkingv1beta1.VirtualS } for hostIndex, host := range hosts { + if !isFqdn(string(*host)) { + if isFqdnLabel(string(*host)) { // short name + gateway := findGateway(*apiRule.Spec.Gateway, gwList) + if gateway == nil { + hostAttributePath := fmt.Sprintf("%s[%d]", hostsAttributePath, hostIndex) + failures = append(failures, validation.Failure{ + AttributePath: hostAttributePath, + Message: fmt.Sprintf("Unable to find Gateway %s", *apiRule.Spec.Gateway), + }) + } + if hasMultipleHostDefinitions(gateway) { + hostAttributePath := fmt.Sprintf("%s[%d]", hostsAttributePath, hostIndex) + failures = append(failures, validation.Failure{ + AttributePath: hostAttributePath, + Message: "Short host only supported when Gateway has single host definition matching *. format", + }) + } + } else { + hostAttributePath := fmt.Sprintf("%s[%d]", hostsAttributePath, hostIndex) + failures = append(failures, validation.Failure{ + AttributePath: hostAttributePath, + Message: "Host must be a valid FQDN or short name", + }) + } + } for _, vs := range vsList.Items { if occupiesHost(vs, string(*host)) && !ownedBy(vs, apiRule) { hostAttributePath := fmt.Sprintf("%s[%d]", hostsAttributePath, hostIndex) @@ -36,6 +73,41 @@ func validateHosts(parentAttributePath string, vsList networkingv1beta1.VirtualS return failures } +func hasMultipleHostDefinitions(gateway *networkingv1beta1.Gateway) bool { + host := "" + for _, server := range gateway.Spec.Servers { + if len(server.Hosts) > 1 { + return true + } + if !strings.HasPrefix(server.Hosts[0], "*.") { + return true + } + if host == "" { + host = server.Hosts[0] + } else if host != server.Hosts[0] { + return true + } + } + return false +} + +func findGateway(name string, gwList networkingv1beta1.GatewayList) *networkingv1beta1.Gateway { + for _, gateway := range gwList.Items { + if gateway.Name == name { + return gateway + } + } + return nil +} + +func isFqdn(host string) bool { + return len(host) <= fqdnMaxLength && regexFqdn.MatchString(host) +} + +func isFqdnLabel(host string) bool { + return regexFqdnLabel.MatchString(host) +} + func occupiesHost(vs *networkingv1beta1.VirtualService, host string) bool { for _, h := range vs.Spec.Hosts { if h == host { diff --git a/internal/validation/v2alpha1/hosts_test.go b/internal/validation/v2alpha1/hosts_test.go index a90329f48..8dddb02c0 100644 --- a/internal/validation/v2alpha1/hosts_test.go +++ b/internal/validation/v2alpha1/hosts_test.go @@ -1,15 +1,279 @@ package v2alpha1 import ( + "fmt" + "strings" + "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "istio.io/api/networking/v1beta1" networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" ) var _ = Describe("Validate hosts", func() { + It("Should fail if there are no hosts defined", func() { + //given + apiRule := &v2alpha1.APIRule{ + Spec: v2alpha1.APIRuleSpec{}, + } + + //when + problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, networkingv1beta1.GatewayList{}, apiRule) + + //then + Expect(problems).To(HaveLen(1)) + Expect(problems[0].AttributePath).To(Equal(".spec.hosts")) + Expect(problems[0].Message).To(Equal("No hosts defined")) + }) + + It("Should fail if host is empty", func() { + //given + apiRule := &v2alpha1.APIRule{ + Spec: v2alpha1.APIRuleSpec{ + Hosts: []*v2alpha1.Host{ + ptr.To(v2alpha1.Host("")), + }, + }, + } + + //when + problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, networkingv1beta1.GatewayList{}, apiRule) + + //then + Expect(problems).To(HaveLen(1)) + Expect(problems[0].AttributePath).To(Equal(".spec.hosts[0]")) + Expect(problems[0].Message).To(Equal("Host must be a valid FQDN or short name")) + }) + + It("Should succeed if host is FQDN", func() { + //given + apiRule := &v2alpha1.APIRule{ + Spec: v2alpha1.APIRuleSpec{ + Hosts: []*v2alpha1.Host{ + ptr.To(v2alpha1.Host("host.com")), + }, + }, + } + + //when + problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, networkingv1beta1.GatewayList{}, apiRule) + + //then + Expect(problems).To(HaveLen(0)) + }) + + It("Should succeed if host is FQDN label only (short-name)", func() { + //given + apiRule := &v2alpha1.APIRule{ + Spec: v2alpha1.APIRuleSpec{ + Gateway: ptr.To("gateway-name"), + Hosts: []*v2alpha1.Host{ + ptr.To(v2alpha1.Host("short-name-host")), + }, + }, + } + + gwList := networkingv1beta1.GatewayList{ + Items: []*networkingv1beta1.Gateway{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + }, + Spec: v1beta1.Gateway{ + Servers: []*v1beta1.Server{ + { + Hosts: []string{"*.example.com"}, + }, + }, + }, + }, + }, + } + + //when + problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, gwList, apiRule) + + //then + Expect(problems).To(HaveLen(0)) + }) + + It("Should fail if host name has uppercase letters", func() { + //given + apiRule := &v2alpha1.APIRule{ + Spec: v2alpha1.APIRuleSpec{ + Hosts: []*v2alpha1.Host{ + ptr.To(v2alpha1.Host("host.exaMple.com")), + }, + }, + } + + //when + problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, networkingv1beta1.GatewayList{}, apiRule) + + //then + Expect(problems).To(HaveLen(1)) + Expect(problems[0].AttributePath).To(Equal(".spec.hosts[0]")) + Expect(problems[0].Message).To(Equal("Host must be a valid FQDN or short name")) + }) + + It("Should allow lenghty host name with numbers and dashes", func() { + //given + apiRule := &v2alpha1.APIRule{ + Spec: v2alpha1.APIRuleSpec{ + Hosts: []*v2alpha1.Host{ + ptr.To(v2alpha1.Host("host-with-numbers-1234567890-and-dashes----------up-to-63-chars.domain-with-numbers-1234567890-and-dashes--------up-to-63-chars.com")), + }, + }, + } + + //when + problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, networkingv1beta1.GatewayList{}, apiRule) + + //then + Expect(problems).To(HaveLen(0)) + }) + + It("Should fail if the host FQDN is longer than 253 characters", func() { + //given + apiRule := &v2alpha1.APIRule{ + Spec: v2alpha1.APIRuleSpec{ + Hosts: []*v2alpha1.Host{ + ptr.To(v2alpha1.Host("long.%s.com" + strings.Repeat("a", 245))), + }, + }, + } + + //when + problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, networkingv1beta1.GatewayList{}, apiRule) + + //then + Expect(problems).To(HaveLen(1)) + Expect(problems[0].AttributePath).To(Equal(".spec.hosts[0]")) + Expect(problems[0].Message).To(Equal("Host must be a valid FQDN or short name")) + }) + + It("Should fail if any domain label is too long", func() { + //given + apiRule := &v2alpha1.APIRule{ + Spec: v2alpha1.APIRuleSpec{ + Hosts: []*v2alpha1.Host{ + ptr.To(v2alpha1.Host(fmt.Sprintf("%s.com", strings.Repeat("a", 64)))), + ptr.To(v2alpha1.Host(fmt.Sprintf("host.%s.com", strings.Repeat("a", 64)))), + ptr.To(v2alpha1.Host(fmt.Sprintf("host.example.%s", strings.Repeat("a", 64)))), + }, + }, + } + + //when + problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, networkingv1beta1.GatewayList{}, apiRule) + + //then + Expect(problems).To(HaveLen(3)) + for i := 0; i < 3; i++ { + Expect(problems[i].AttributePath).To(Equal(fmt.Sprintf(".spec.hosts[%d]", i))) + Expect(problems[i].Message).To(Equal("Host must be a valid FQDN or short name")) + } + }) + + It("Should fail if any domain label is empty", func() { + //given + apiRule := &v2alpha1.APIRule{ + Spec: v2alpha1.APIRuleSpec{ + Hosts: []*v2alpha1.Host{ + ptr.To(v2alpha1.Host(".domain.com")), + ptr.To(v2alpha1.Host("host..com")), + ptr.To(v2alpha1.Host("host.domain.")), + }, + }, + } + + //when + problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, networkingv1beta1.GatewayList{}, apiRule) + + //then + Expect(problems).To(HaveLen(3)) + for i := 0; i < 3; i++ { + Expect(problems[i].AttributePath).To(Equal(fmt.Sprintf(".spec.hosts[%d]", i))) + Expect(problems[i].Message).To(Equal("Host must be a valid FQDN or short name")) + } + }) + + It("Should fail if top level domain is too short", func() { + //given + apiRule := &v2alpha1.APIRule{ + Spec: v2alpha1.APIRuleSpec{ + Hosts: []*v2alpha1.Host{ + ptr.To(v2alpha1.Host("too.short.x")), + }, + }, + } + + //when + problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, networkingv1beta1.GatewayList{}, apiRule) + + //then + Expect(problems[0].AttributePath).To(Equal(".spec.hosts[0]")) + Expect(problems[0].Message).To(Equal("Host must be a valid FQDN or short name")) + }) + + It("Should fail if any domain label contain wrong characters", func() { + //given + apiRule := &v2alpha1.APIRule{ + Spec: v2alpha1.APIRuleSpec{ + Hosts: []*v2alpha1.Host{ + ptr.To(v2alpha1.Host("wrong-*-inside.example.com")), + ptr.To(v2alpha1.Host("host.wrong-*-inside.com")), + ptr.To(v2alpha1.Host("host.example.wrong-*-inside")), + ptr.To(v2alpha1.Host("*-wrong.example.com")), + ptr.To(v2alpha1.Host("host.*-wrong.com")), + ptr.To(v2alpha1.Host("host.example.*-wrong")), + ptr.To(v2alpha1.Host("wrong-*.example.com")), + ptr.To(v2alpha1.Host("host.wrong-*.com")), + ptr.To(v2alpha1.Host("host.example.wrong-*")), + }, + }, + } + + //when + problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, networkingv1beta1.GatewayList{}, apiRule) + + //then + Expect(problems).To(HaveLen(9)) + for i := 0; i < 9; i++ { + Expect(problems[i].AttributePath).To(Equal(fmt.Sprintf(".spec.hosts[%d]", i))) + Expect(problems[i].Message).To(Equal("Host must be a valid FQDN or short name")) + } + }) + + It("Should fail if any segment in host name starts or ends with dash", func() { + //given + apiRule := &v2alpha1.APIRule{ + Spec: v2alpha1.APIRuleSpec{ + Hosts: []*v2alpha1.Host{ + ptr.To(v2alpha1.Host("-host.example.com")), + ptr.To(v2alpha1.Host("host-.example.com")), + ptr.To(v2alpha1.Host("host.example-.com")), + ptr.To(v2alpha1.Host("host.-example.com")), + ptr.To(v2alpha1.Host("host.example.-com")), + ptr.To(v2alpha1.Host("host.example.com-")), + }, + }, + } + + //when + problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, networkingv1beta1.GatewayList{}, apiRule) + + //then + Expect(problems).To(HaveLen(6)) + for i := 0; i < 6; i++ { + Expect(problems[i].AttributePath).To(Equal(fmt.Sprintf(".spec.hosts[%d]", i))) + Expect(problems[i].Message).To(Equal("Host must be a valid FQDN or short name")) + } + }) + It("Should fail if any host that is occupied by any Virtual Service exposed by another resource", func() { //given apiRule := &v2alpha1.APIRule{ @@ -19,8 +283,8 @@ var _ = Describe("Validate hosts", func() { }, Spec: v2alpha1.APIRuleSpec{ Hosts: []*v2alpha1.Host{ - getHostPtr("host.example.com"), - getHostPtr("occupied.example.com"), + ptr.To(v2alpha1.Host("host.example.com")), + ptr.To(v2alpha1.Host("occupied.example.com")), }, }, } @@ -47,7 +311,7 @@ var _ = Describe("Validate hosts", func() { } //when - problems := validateHosts(".spec", virtualServiceList, apiRule) + problems := validateHosts(".spec", virtualServiceList, networkingv1beta1.GatewayList{}, apiRule) //then Expect(problems).To(HaveLen(1)) @@ -64,8 +328,8 @@ var _ = Describe("Validate hosts", func() { }, Spec: v2alpha1.APIRuleSpec{ Hosts: []*v2alpha1.Host{ - getHostPtr("host.example.com"), - getHostPtr("occupied.example.com"), + ptr.To(v2alpha1.Host("host.example.com")), + ptr.To(v2alpha1.Host("occupied.example.com")), }, }, } @@ -94,18 +358,13 @@ var _ = Describe("Validate hosts", func() { } //when - problems := validateHosts(".spec", virtualServiceList, apiRule) + problems := validateHosts(".spec", virtualServiceList, networkingv1beta1.GatewayList{}, apiRule) //then Expect(problems).To(HaveLen(0)) }) }) -func getHostPtr(hostName string) *v2alpha1.Host { - host := v2alpha1.Host(hostName) - return &host -} - func getMapWithOwnerLabel(apiRule *v2alpha1.APIRule) map[string]string { labelKey, labelValue := getExpectedOwnerLabel(apiRule) return map[string]string{ diff --git a/internal/validation/v2alpha1/v2alpha1.go b/internal/validation/v2alpha1/v2alpha1.go index 24f36d657..a5ed283b4 100644 --- a/internal/validation/v2alpha1/v2alpha1.go +++ b/internal/validation/v2alpha1/v2alpha1.go @@ -35,7 +35,7 @@ func (a *APIRuleValidator) Validate(ctx context.Context, client client.Client, v }) } else { failures = append(failures, validateRules(ctx, client, ".spec", a.ApiRule)...) - failures = append(failures, validateHosts(".spec", vsList, a.ApiRule)...) + failures = append(failures, validateHosts(".spec", vsList, gwList, a.ApiRule)...) failures = append(failures, validateGateway(".spec", gwList, a.ApiRule)...) } From 028a98f06d13995dc3fdef68fde70d9b7ca8152c Mon Sep 17 00:00:00 2001 From: Vladimir Videlov Date: Thu, 19 Sep 2024 17:06:33 +0200 Subject: [PATCH 02/27] More validation --- .../api_controller_integration_test.go | 25 ++++++- internal/validation/v2alpha1/hosts.go | 3 +- internal/validation/v2alpha1/hosts_test.go | 74 +++++++++++++++++-- 3 files changed, 91 insertions(+), 11 deletions(-) diff --git a/controllers/gateway/api_controller_integration_test.go b/controllers/gateway/api_controller_integration_test.go index 286322f99..91b8c2167 100644 --- a/controllers/gateway/api_controller_integration_test.go +++ b/controllers/gateway/api_controller_integration_test.go @@ -1461,8 +1461,8 @@ var _ = Describe("APIRule Controller", Serial, func() { }) }) - Context("when creating APIRule in version v2alpha1 hosts should be FQDN", Ordered, func() { - It("should create an APIRule with a FQDN host", func() { + Context("when creating APIRule in version v2alpha1 hosts should be a valid FQDN or a short name", Ordered, func() { + It("should create an APIRule with a valid FQDN host", func() { // given apiRuleName := generateTestName(testNameBase, testIDLength) serviceName := testServiceNameBase @@ -1483,6 +1483,27 @@ var _ = Describe("APIRule Controller", Serial, func() { }() }) + It("should create an APIRule with a short host name", func() { + // given + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := testServiceNameBase + serviceHost := gatewayv2alpha1.Host("example") + serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} + + rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + rule.NoAuth = ptr.To(true) + apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + + // when + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + }() + }) + It("should create an APIRule with host name that has length of 255 characters", func() { // given apiRuleName := generateTestName(testNameBase, testIDLength) diff --git a/internal/validation/v2alpha1/hosts.go b/internal/validation/v2alpha1/hosts.go index 141f5eb78..2c817d63f 100644 --- a/internal/validation/v2alpha1/hosts.go +++ b/internal/validation/v2alpha1/hosts.go @@ -43,8 +43,7 @@ func validateHosts(parentAttributePath string, vsList networkingv1beta1.VirtualS AttributePath: hostAttributePath, Message: fmt.Sprintf("Unable to find Gateway %s", *apiRule.Spec.Gateway), }) - } - if hasMultipleHostDefinitions(gateway) { + } else if hasMultipleHostDefinitions(gateway) { hostAttributePath := fmt.Sprintf("%s[%d]", hostsAttributePath, hostIndex) failures = append(failures, validation.Failure{ AttributePath: hostAttributePath, diff --git a/internal/validation/v2alpha1/hosts_test.go b/internal/validation/v2alpha1/hosts_test.go index 8dddb02c0..5c4c31945 100644 --- a/internal/validation/v2alpha1/hosts_test.go +++ b/internal/validation/v2alpha1/hosts_test.go @@ -48,7 +48,7 @@ var _ = Describe("Validate hosts", func() { Expect(problems[0].Message).To(Equal("Host must be a valid FQDN or short name")) }) - It("Should succeed if host is FQDN", func() { + It("Should succeed if host is a valid FQDN", func() { //given apiRule := &v2alpha1.APIRule{ Spec: v2alpha1.APIRuleSpec{ @@ -65,7 +65,7 @@ var _ = Describe("Validate hosts", func() { Expect(problems).To(HaveLen(0)) }) - It("Should succeed if host is FQDN label only (short-name)", func() { + It("Should succeed if host is a short name", func() { //given apiRule := &v2alpha1.APIRule{ Spec: v2alpha1.APIRuleSpec{ @@ -100,6 +100,66 @@ var _ = Describe("Validate hosts", func() { Expect(problems).To(HaveLen(0)) }) + It("Should fail if host is a short name and referenced Gateway is missing", func() { + //given + apiRule := &v2alpha1.APIRule{ + Spec: v2alpha1.APIRuleSpec{ + Gateway: ptr.To("gateway-name"), + Hosts: []*v2alpha1.Host{ + ptr.To(v2alpha1.Host("short-name-host")), + }, + }, + } + + //when + problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, networkingv1beta1.GatewayList{}, apiRule) + + //then + Expect(problems).To(HaveLen(1)) + Expect(problems[0].AttributePath).To(Equal(".spec.hosts[0]")) + Expect(problems[0].Message).To(Equal("Unable to find Gateway gateway-name")) + }) + + It("Should fail if host is a short name and referenced Gateway has various hosts definitions", func() { + //given + apiRule := &v2alpha1.APIRule{ + Spec: v2alpha1.APIRuleSpec{ + Gateway: ptr.To("gateway-name"), + Hosts: []*v2alpha1.Host{ + ptr.To(v2alpha1.Host("short-name-host")), + }, + }, + } + + gwList := networkingv1beta1.GatewayList{ + Items: []*networkingv1beta1.Gateway{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + }, + Spec: v1beta1.Gateway{ + Servers: []*v1beta1.Server{ + { + Hosts: []string{"*.example.com"}, + }, + { + Hosts: []string{"*.example2.com"}, + }, + }, + }, + }, + }, + } + + //when + problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, gwList, apiRule) + + //then + Expect(problems).To(HaveLen(1)) + Expect(problems[0].AttributePath).To(Equal(".spec.hosts[0]")) + Expect(problems[0].Message).To(Equal("Short host only supported when Gateway has single host definition matching *. format")) + }) + It("Should fail if host name has uppercase letters", func() { //given apiRule := &v2alpha1.APIRule{ @@ -119,12 +179,12 @@ var _ = Describe("Validate hosts", func() { Expect(problems[0].Message).To(Equal("Host must be a valid FQDN or short name")) }) - It("Should allow lenghty host name with numbers and dashes", func() { + It("Should allow lenghty host name with numbers and hyphens", func() { //given apiRule := &v2alpha1.APIRule{ Spec: v2alpha1.APIRuleSpec{ Hosts: []*v2alpha1.Host{ - ptr.To(v2alpha1.Host("host-with-numbers-1234567890-and-dashes----------up-to-63-chars.domain-with-numbers-1234567890-and-dashes--------up-to-63-chars.com")), + ptr.To(v2alpha1.Host("host-with-numbers-1234567890-and-hyphens---------up-to-63-chars.domain-with-numbers-1234567890-and-hyphens-------up-to-63-chars.com")), }, }, } @@ -136,7 +196,7 @@ var _ = Describe("Validate hosts", func() { Expect(problems).To(HaveLen(0)) }) - It("Should fail if the host FQDN is longer than 253 characters", func() { + It("Should fail if the host is longer than 253 characters", func() { //given apiRule := &v2alpha1.APIRule{ Spec: v2alpha1.APIRuleSpec{ @@ -155,7 +215,7 @@ var _ = Describe("Validate hosts", func() { Expect(problems[0].Message).To(Equal("Host must be a valid FQDN or short name")) }) - It("Should fail if any domain label is too long", func() { + It("Should fail if any domain label is longer than 63 chars", func() { //given apiRule := &v2alpha1.APIRule{ Spec: v2alpha1.APIRuleSpec{ @@ -248,7 +308,7 @@ var _ = Describe("Validate hosts", func() { } }) - It("Should fail if any segment in host name starts or ends with dash", func() { + It("Should fail if any segment in host name starts or ends with hyphen", func() { //given apiRule := &v2alpha1.APIRule{ Spec: v2alpha1.APIRuleSpec{ From 7f98f1bce0638874df30fff67be2216f075f3e15 Mon Sep 17 00:00:00 2001 From: Vladimir Videlov Date: Thu, 19 Sep 2024 18:45:39 +0200 Subject: [PATCH 03/27] Default domain from non-kyma gateway --- controllers/gateway/apirule_controller.go | 22 +-- .../v2alpha1/04-10-apirule-custom-resource.md | 2 +- .../default_domain/default_domain.go | 57 ++++-- .../default_domain/default_domain_test.go | 186 ++++++++++++++++-- .../virtual_service_processor.go | 4 +- 5 files changed, 228 insertions(+), 43 deletions(-) diff --git a/controllers/gateway/apirule_controller.go b/controllers/gateway/apirule_controller.go index 288779382..fa3e1dd42 100644 --- a/controllers/gateway/apirule_controller.go +++ b/controllers/gateway/apirule_controller.go @@ -19,11 +19,12 @@ package gateway import ( "context" "fmt" + "time" + "github.com/kyma-project/api-gateway/internal/dependencies" "github.com/kyma-project/api-gateway/internal/processing/processors/migration" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "time" gatewayv1beta1 "github.com/kyma-project/api-gateway/apis/gateway/v1beta1" gatewayv2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" @@ -77,7 +78,7 @@ func (r *APIRuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct l.Info("Starting reconciliation") ctx = logr.NewContext(ctx, r.Log) - defaultDomainName, err := default_domain.GetDefaultDomainFromKymaGateway(ctx, r.Client) + defaultDomainName, err := default_domain.GetDomainFromKymaGateway(ctx, r.Client) if err != nil && default_domain.HandleDefaultDomainError(l, err) { return doneReconcileErrorRequeue(err, errorReconciliationPeriod) } @@ -117,11 +118,11 @@ func (r *APIRuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct } if r.isApiRuleConvertedFromV2alpha1(apiRule) { - return r.reconcileV2alpha1APIRule(ctx, l, apiRule, defaultDomainName) + return r.reconcileV2Alpha1APIRule(ctx, l, apiRule, defaultDomainName) } l.Info("Reconciling v1beta1 APIRule", "jwtHandler", r.Config.JWTHandler) - cmd := r.getV1beta1Reconciliation(&apiRule, defaultDomainName, &l) + cmd := r.getV1Beta1Reconciliation(&apiRule, defaultDomainName, &l) if name, err := dependencies.APIRule().AreAvailable(ctx, r.Client); err != nil { s, err := handleDependenciesError(name, err).V1beta1Status() if err != nil { @@ -157,7 +158,7 @@ func (r *APIRuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return r.updateStatus(ctx, l, &apiRule) } -func (r *APIRuleReconciler) reconcileV2alpha1APIRule(ctx context.Context, l logr.Logger, apiRule gatewayv1beta1.APIRule, domain string) (ctrl.Result, error) { +func (r *APIRuleReconciler) reconcileV2Alpha1APIRule(ctx context.Context, l logr.Logger, apiRule gatewayv1beta1.APIRule, defaultDomainName string) (ctrl.Result, error) { l.Info("Reconciling v2alpha1 APIRule") toUpdate := apiRule.DeepCopy() migrate, err := apiRuleNeedsMigration(ctx, r.Client, toUpdate) @@ -179,8 +180,7 @@ func (r *APIRuleReconciler) reconcileV2alpha1APIRule(ctx context.Context, l logr if err := rule.ConvertFrom(toUpdate); err != nil { return doneReconcileErrorRequeue(err, r.OnErrorReconcilePeriod) } - cmd := r.getv2alpha1Reconciliation(&apiRule, &rule, - domain, migrate, &l) + cmd := r.getV2Alpha1Reconciliation(&apiRule, &rule, defaultDomainName, migrate, &l) if name, err := dependencies.APIRule().AreAvailable(ctx, r.Client); err != nil { s, err := handleDependenciesError(name, err).V2alpha1Status() @@ -256,9 +256,9 @@ func handleDependenciesError(name string, err error) controllers.Status { } } -func (r *APIRuleReconciler) getV1beta1Reconciliation(apiRule *gatewayv1beta1.APIRule, defaultDomain string, namespacedLogger *logr.Logger) processing.ReconciliationCommand { +func (r *APIRuleReconciler) getV1Beta1Reconciliation(apiRule *gatewayv1beta1.APIRule, defaultDomainName string, namespacedLogger *logr.Logger) processing.ReconciliationCommand { config := r.ReconciliationConfig - config.DefaultDomainName = defaultDomain + config.DefaultDomainName = defaultDomainName switch { case r.Config.JWTHandler == helpers.JWT_HANDLER_ISTIO: return istio.NewIstioReconciliation(apiRule, config, namespacedLogger) @@ -267,9 +267,9 @@ func (r *APIRuleReconciler) getV1beta1Reconciliation(apiRule *gatewayv1beta1.API } } -func (r *APIRuleReconciler) getv2alpha1Reconciliation(apiRulev1beta1 *gatewayv1beta1.APIRule, apiRulev2alpha1 *gatewayv2alpha1.APIRule, defaultDomain string, needsMigration bool, namespacedLogger *logr.Logger) processing.ReconciliationCommand { +func (r *APIRuleReconciler) getV2Alpha1Reconciliation(apiRulev1beta1 *gatewayv1beta1.APIRule, apiRulev2alpha1 *gatewayv2alpha1.APIRule, defaultDomainName string, needsMigration bool, namespacedLogger *logr.Logger) processing.ReconciliationCommand { config := r.ReconciliationConfig - config.DefaultDomainName = defaultDomain + config.DefaultDomainName = defaultDomainName v2alpha1Validator := v2alpha1.NewAPIRuleValidator(apiRulev2alpha1) return v2alpha1Processing.NewReconciliation(apiRulev2alpha1, apiRulev1beta1, v2alpha1Validator, config, namespacedLogger, needsMigration) } diff --git a/docs/user/custom-resources/apirule/v2alpha1/04-10-apirule-custom-resource.md b/docs/user/custom-resources/apirule/v2alpha1/04-10-apirule-custom-resource.md index 93a3ae2f5..553b92d03 100644 --- a/docs/user/custom-resources/apirule/v2alpha1/04-10-apirule-custom-resource.md +++ b/docs/user/custom-resources/apirule/v2alpha1/04-10-apirule-custom-resource.md @@ -23,7 +23,7 @@ This table lists all parameters of APIRule `v2alpha1` CRD together with their de | **corsPolicy.allowCredentials** | **NO** | Specifies whether credentials are allowed in the **Access-Control-Allow-Credentials** CORS header. | None | | **corsPolicy.exposeHeaders** | **NO** | Specifies headers exposed with the **Access-Control-Expose-Headers** CORS header. | None | | **corsPolicy.maxAge** | **NO** | Specifies the maximum age of CORS policy cache. The value is provided in the **Access-Control-Max-Age** CORS header. | None | -| **hosts** | **YES** | Specifies the Service's communication address for inbound external traffic. It must be a fully qualified domain name (FQDN) in format: at least two domain labels with characters, numbers, or hypens. | FQDN format. | +| **hosts** | **YES** | Specifies the Service's communication address for inbound external traffic. It must be a short name or a valid fully qualified domain name (FQDN) in format: at least two domain labels with characters, numbers, or hypens. | Short name or FQDN format. | | **service.name** | **NO** | Specifies the name of the exposed Service. | None | | **service.namespace** | **NO** | Specifies the namespace of the exposed Service. | None | | **service.port** | **NO** | Specifies the communication port of the exposed Service. | None | diff --git a/internal/processing/default_domain/default_domain.go b/internal/processing/default_domain/default_domain.go index f80dfa5ee..f1ba384d8 100644 --- a/internal/processing/default_domain/default_domain.go +++ b/internal/processing/default_domain/default_domain.go @@ -2,23 +2,23 @@ package default_domain import ( "context" + "errors" "fmt" + "strings" + "github.com/go-logr/logr" "github.com/thoas/go-funk" apiv1beta1 "istio.io/api/networking/v1beta1" networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" apierrs "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" - "regexp" "sigs.k8s.io/controller-runtime/pkg/client" - "strings" ) const ( - gatewayNamespace = "kyma-system" - gatewayName = "kyma-gateway" - - protocolHttps = "HTTPS" + kymaGatewayNamespace = "kyma-system" + kymaGatewayName = "kyma-gateway" + kymaGatewayProtocol = "HTTPS" ) func GetHostWithDomain(host, defaultDomainName string) string { @@ -50,34 +50,61 @@ func HandleDefaultDomainError(log logr.Logger, err error) (finishReconciliation } } -func GetDefaultDomainFromKymaGateway(ctx context.Context, k8sClient client.Client) (string, error) { +func GetDomainFromKymaGateway(ctx context.Context, k8sClient client.Client) (string, error) { var gateway networkingv1beta1.Gateway - err := k8sClient.Get(ctx, types.NamespacedName{Namespace: gatewayNamespace, Name: gatewayName}, &gateway) + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: kymaGatewayNamespace, Name: kymaGatewayName}, &gateway) if err != nil { return "", err } httpsServers := funk.Filter(gateway.Spec.GetServers(), func(g *apiv1beta1.Server) bool { - return g.Port != nil && strings.ToUpper(g.Port.Protocol) == protocolHttps + return g.Port != nil && strings.ToUpper(g.Port.Protocol) == kymaGatewayProtocol }).([]*apiv1beta1.Server) if len(httpsServers) != 1 { - return "", fmt.Errorf("could not get default domain, number of https servers was more than 1, num=%d", len(gateway.Spec.GetServers())) + return "", fmt.Errorf("gateway must have a single https server definition, num=%d", len(httpsServers)) } if len(httpsServers[0].Hosts) != 1 { - return "", fmt.Errorf("could not get default domain, number of hosts in HTTPS server was different than default of 1, num=%d", len(gateway.Spec.GetServers()[0].Hosts)) + return "", fmt.Errorf("gateway https server must have a single host definition, num=%d", len(httpsServers[0].Hosts)) + } + + if !strings.HasPrefix(httpsServers[0].Hosts[0], "*.") { + return "", fmt.Errorf(`gateway https server host %s does not start with a prefix "*."`, httpsServers[0].Hosts[0]) } - match, err := regexp.MatchString(`^\*\..+$`, httpsServers[0].Hosts[0]) + return strings.TrimPrefix(httpsServers[0].Hosts[0], "*."), nil +} +func GetDomainFromGateway(ctx context.Context, k8sClient client.Client, gatewayName, gatewayNamespace string) (string, error) { + var gateway networkingv1beta1.Gateway + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: gatewayNamespace, Name: gatewayName}, &gateway) if err != nil { return "", err } - if !match { - return "", fmt.Errorf(`host %s isn't a host with wildcard prefix "*."`, httpsServers[0].Hosts[0]) + if !gatewayServersWithSameSingleHost(&gateway) { + return "", errors.New("gateway must specify server(s) with the same single host") + } + + if !strings.HasPrefix(gateway.Spec.Servers[0].Hosts[0], "*.") { + return "", fmt.Errorf(`gateway https server host %s does not start with a prefix "*."`, gateway.Spec.Servers[0].Hosts[0]) } - return strings.TrimLeft(gateway.Spec.GetServers()[0].Hosts[0], "*."), nil + return strings.TrimPrefix(gateway.Spec.Servers[0].Hosts[0], "*."), nil +} + +func gatewayServersWithSameSingleHost(gateway *networkingv1beta1.Gateway) bool { + host := "" + for _, server := range gateway.Spec.Servers { + if len(server.Hosts) > 1 { + return false + } + if host == "" { + host = server.Hosts[0] + } else if host != server.Hosts[0] { + return false + } + } + return host != "" } diff --git a/internal/processing/default_domain/default_domain_test.go b/internal/processing/default_domain/default_domain_test.go index eab1a3eb9..4840d8cee 100644 --- a/internal/processing/default_domain/default_domain_test.go +++ b/internal/processing/default_domain/default_domain_test.go @@ -3,17 +3,19 @@ package default_domain import ( "context" "fmt" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "os" + "testing" + apinetworkingv1beta1 "istio.io/api/networking/v1beta1" networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "os" - "testing" . "github.com/onsi/ginkgo/v2" "github.com/onsi/ginkgo/v2/reporters" @@ -53,12 +55,11 @@ var _ = ReportAfterSuite("custom reporter", func(report types.Report) { } }) -var _ = Describe("Default APIRule domain", func() { +var _ = Describe("GetDomainFromKymaGateway", func() { It("should get domain from default kyma gateway if it exists", func() { - // given gateway := networkingv1beta1.Gateway{ - ObjectMeta: metav1.ObjectMeta{Name: gatewayName, Namespace: gatewayNamespace}, + ObjectMeta: metav1.ObjectMeta{Name: kymaGatewayName, Namespace: kymaGatewayNamespace}, Spec: apinetworkingv1beta1.Gateway{ Servers: []*apinetworkingv1beta1.Server{ { @@ -80,7 +81,7 @@ var _ = Describe("Default APIRule domain", func() { client := getFakeClient(&gateway) // when - host, err := GetDefaultDomainFromKymaGateway(context.Background(), client) + host, err := GetDomainFromKymaGateway(context.Background(), client) // then Expect(err).ShouldNot(HaveOccurred()) @@ -88,10 +89,9 @@ var _ = Describe("Default APIRule domain", func() { }) It("should return error if gateway does not have an HTTPS server", func() { - // given gateway := networkingv1beta1.Gateway{ - ObjectMeta: metav1.ObjectMeta{Name: gatewayName, Namespace: gatewayNamespace}, + ObjectMeta: metav1.ObjectMeta{Name: kymaGatewayName, Namespace: kymaGatewayNamespace}, Spec: apinetworkingv1beta1.Gateway{ Servers: []*apinetworkingv1beta1.Server{ { @@ -106,51 +106,207 @@ var _ = Describe("Default APIRule domain", func() { client := getFakeClient(&gateway) // when - host, err := GetDefaultDomainFromKymaGateway(context.Background(), client) + host, err := GetDomainFromKymaGateway(context.Background(), client) // then Expect(err).Should(HaveOccurred()) - Expect(errors.IsNotFound(err)).To(BeFalse()) + Expect(err.Error()).To(Equal("gateway must have a single https server definition, num=0")) Expect(host).To(Equal("")) }) It("should return error if gateway does not have an HTTPS server when gateway has multiple servers", func() { + // given + gateway := networkingv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{Name: kymaGatewayName, Namespace: kymaGatewayNamespace}, + Spec: apinetworkingv1beta1.Gateway{ + Servers: []*apinetworkingv1beta1.Server{ + { + Port: &apinetworkingv1beta1.Port{Protocol: "HTTP"}, + }, + { + Port: &apinetworkingv1beta1.Port{Protocol: "HTTP"}, + Hosts: []string{ + "*.local.kyma.dev", + }, + }, + }, + }, + } + client := getFakeClient(&gateway) + + // when + host, err := GetDomainFromKymaGateway(context.Background(), client) + + // then + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(Equal("gateway must have a single https server definition, num=0")) + Expect(host).To(Equal("")) + }) + + It("should return error if gateway has a HTTPS server but host do not start with *. prefix", func() { + // given + gateway := networkingv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{Name: kymaGatewayName, Namespace: kymaGatewayNamespace}, + Spec: apinetworkingv1beta1.Gateway{ + Servers: []*apinetworkingv1beta1.Server{ + { + Port: &apinetworkingv1beta1.Port{Protocol: "HTTPS"}, + Hosts: []string{ + "local.kyma.dev", + }, + }, + }, + }, + } + client := getFakeClient(&gateway) + + // when + host, err := GetDomainFromKymaGateway(context.Background(), client) + + // then + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(Equal(`gateway https server host local.kyma.dev does not start with a prefix "*."`)) + Expect(host).To(Equal("")) + }) + + It("should return empty domain and not found error if gateway does not exist", func() { + // given + client := getFakeClient() + + // when + host, err := GetDomainFromKymaGateway(context.Background(), client) + + // then + Expect(err).Should(HaveOccurred()) + Expect(errors.IsNotFound(err)).To(BeTrue()) + Expect(host).To(Equal("")) + }) +}) + +var _ = Describe("GetDomainFromGateway", func() { + It("should get domain from gateway if it exists", func() { + // given + gateway := networkingv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{Name: "gateway-name", Namespace: "gateway-namespace"}, + Spec: apinetworkingv1beta1.Gateway{ + Servers: []*apinetworkingv1beta1.Server{ + { + Port: &apinetworkingv1beta1.Port{Protocol: "HTTPS"}, + Hosts: []string{ + "*.local.kyma.dev", + }, + }, + { + Port: &apinetworkingv1beta1.Port{Protocol: "HTTP"}, + Hosts: []string{ + "*.local.kyma.dev", + }, + }, + }, + }, + } + + client := getFakeClient(&gateway) + // when + host, err := GetDomainFromGateway(context.Background(), client, "gateway-name", "gateway-namespace") + + // then + Expect(err).ShouldNot(HaveOccurred()) + Expect(host).To(Equal("local.kyma.dev")) + }) + + It("should return error if gateway defines more than a single host", func() { // given gateway := networkingv1beta1.Gateway{ - ObjectMeta: metav1.ObjectMeta{Name: gatewayName, Namespace: gatewayNamespace}, + ObjectMeta: metav1.ObjectMeta{Name: "gateway-name", Namespace: "gateway-namespace"}, Spec: apinetworkingv1beta1.Gateway{ Servers: []*apinetworkingv1beta1.Server{ { Port: &apinetworkingv1beta1.Port{Protocol: "HTTP"}, + Hosts: []string{ + "*.local.kyma.dev", + "*.remote.kyma.dev", + }, }, + }, + }, + } + client := getFakeClient(&gateway) + + // when + host, err := GetDomainFromGateway(context.Background(), client, "gateway-name", "gateway-namespace") + + // then + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(Equal("gateway must specify server(s) with the same single host")) + Expect(host).To(Equal("")) + }) + + It("should return error if gateway defines servers with different hosts", func() { + // given + gateway := networkingv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{Name: "gateway-name", Namespace: "gateway-namespace"}, + Spec: apinetworkingv1beta1.Gateway{ + Servers: []*apinetworkingv1beta1.Server{ { Port: &apinetworkingv1beta1.Port{Protocol: "HTTP"}, Hosts: []string{ "*.local.kyma.dev", }, }, + { + Port: &apinetworkingv1beta1.Port{Protocol: "HTTPS"}, + Hosts: []string{ + "*.remote.kyma.dev", + }, + }, }, }, } client := getFakeClient(&gateway) // when - host, err := GetDefaultDomainFromKymaGateway(context.Background(), client) + host, err := GetDomainFromGateway(context.Background(), client, "gateway-name", "gateway-namespace") // then Expect(err).Should(HaveOccurred()) - Expect(errors.IsNotFound(err)).To(BeFalse()) + Expect(err.Error()).To(Equal("gateway must specify server(s) with the same single host")) Expect(host).To(Equal("")) }) - It("should return \"\" and not found error if gateway does not exists", func() { + It("should return error if gateway has a HTTPS server but host do not start with *. prefix", func() { + // given + gateway := networkingv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{Name: "gateway-name", Namespace: "gateway-namespace"}, + Spec: apinetworkingv1beta1.Gateway{ + Servers: []*apinetworkingv1beta1.Server{ + { + Port: &apinetworkingv1beta1.Port{Protocol: "HTTPS"}, + Hosts: []string{ + "local.kyma.dev", + }, + }, + }, + }, + } + client := getFakeClient(&gateway) + + // when + host, err := GetDomainFromGateway(context.Background(), client, "gateway-name", "gateway-namespace") + + // then + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(Equal(`gateway https server host local.kyma.dev does not start with a prefix "*."`)) + Expect(host).To(Equal("")) + }) + It("should return empty domain and not found error if gateway does not exist", func() { // given client := getFakeClient() // when - host, err := GetDefaultDomainFromKymaGateway(context.Background(), client) + host, err := GetDomainFromGateway(context.Background(), client, "gateway-name", "gateway-namespace") // then Expect(err).Should(HaveOccurred()) diff --git a/internal/processing/processors/v2alpha1/virtualservice/virtual_service_processor.go b/internal/processing/processors/v2alpha1/virtualservice/virtual_service_processor.go index 7dde5f560..b6d97d5cc 100644 --- a/internal/processing/processors/v2alpha1/virtualservice/virtual_service_processor.go +++ b/internal/processing/processors/v2alpha1/virtualservice/virtual_service_processor.go @@ -3,13 +3,14 @@ package virtualservice import ( "context" "fmt" + "time" + gatewayv2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" "github.com/kyma-project/api-gateway/internal/builders" "github.com/kyma-project/api-gateway/internal/processing" "github.com/kyma-project/api-gateway/internal/processing/default_domain" networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" - "time" ) const defaultHttpTimeout uint32 = 180 @@ -89,6 +90,7 @@ func (r virtualServiceCreator) Create(api *gatewayv2alpha1.APIRule) (*networking vsSpecBuilder := builders.VirtualServiceSpec() for _, host := range api.Spec.Hosts { + // TODO: continue here vsSpecBuilder.AddHost(default_domain.GetHostWithDomain(string(*host), r.defaultDomainName)) } From 6ed829053f22c360d4d15d6da5658dda9d3b9028 Mon Sep 17 00:00:00 2001 From: Vladimir Videlov Date: Sun, 22 Sep 2024 11:46:14 +0200 Subject: [PATCH 04/27] State --- apis/gateway/v2alpha1/apirule_types.go | 2 +- .../gateway.kyma-project.io_apirules.yaml | 5 +- .../api_controller_integration_test.go | 79 ++++++-- internal/helpers/hosts.go | 22 +++ internal/helpers/hosts_test.go | 103 ++++++++++ .../default_domain/default_domain.go | 4 +- .../default_domain/default_domain_test.go | 10 +- .../virtual_service_processor.go | 8 +- internal/processing/reconciliation.go | 2 +- internal/validation/v2alpha1/hosts.go | 23 +-- internal/validation/v2alpha1/hosts_test.go | 177 ------------------ internal/validation/validate.go | 1 + 12 files changed, 215 insertions(+), 221 deletions(-) create mode 100644 internal/helpers/hosts.go create mode 100644 internal/helpers/hosts_test.go diff --git a/apis/gateway/v2alpha1/apirule_types.go b/apis/gateway/v2alpha1/apirule_types.go index 984e8754e..859c29b9c 100644 --- a/apis/gateway/v2alpha1/apirule_types.go +++ b/apis/gateway/v2alpha1/apirule_types.go @@ -56,8 +56,8 @@ type APIRuleSpec struct { } // Host is the URL of the exposed service. We support short names. -// +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=255 +// +kubebuilder:validation:XValidation:rule=`self.matches('^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\\.[a-z0-9][a-z0-9-]{0,61}[a-z0-9])*$')`,message="Host must be a short name or a fully qualified domain name" type Host string // APIRuleStatus describes the observed state of ApiRule. diff --git a/config/crd/bases/gateway.kyma-project.io_apirules.yaml b/config/crd/bases/gateway.kyma-project.io_apirules.yaml index 9b47432b4..ef7d17886 100644 --- a/config/crd/bases/gateway.kyma-project.io_apirules.yaml +++ b/config/crd/bases/gateway.kyma-project.io_apirules.yaml @@ -366,8 +366,11 @@ spec: description: Host is the URL of the exposed service. We support short names. maxLength: 255 - minLength: 1 type: string + x-kubernetes-validations: + - message: Host must be a short name or a fully qualified domain + name + rule: self.matches('^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\\.[a-z0-9][a-z0-9-]{0,61}[a-z0-9])*$') maxItems: 1 minItems: 1 type: array diff --git a/controllers/gateway/api_controller_integration_test.go b/controllers/gateway/api_controller_integration_test.go index 91b8c2167..163c0b10b 100644 --- a/controllers/gateway/api_controller_integration_test.go +++ b/controllers/gateway/api_controller_integration_test.go @@ -54,7 +54,6 @@ var _ = Describe("APIRule Controller", Serial, func() { var methodsPost = []gatewayv1beta1.HttpMethod{http.MethodPost} Context("check default domain logic", func() { - It("should have an error when creating an APIRule without a domain in cluster without kyma-gateway", func() { updateJwtHandlerTo(helpers.JWT_HANDLER_ISTIO) @@ -177,7 +176,6 @@ var _ = Describe("APIRule Controller", Serial, func() { }) Context("when creating APIRule in version v2alpha1 respect x-validation rules only", Ordered, func() { - BeforeAll(func() { updateJwtHandlerTo(helpers.JWT_HANDLER_ORY) }) @@ -369,7 +367,6 @@ var _ = Describe("APIRule Controller", Serial, func() { }) Context("when updating the APIRule with multiple paths", func() { - It("should create, update and delete rules depending on patch match", func() { updateJwtHandlerTo(helpers.JWT_HANDLER_ORY) @@ -453,7 +450,6 @@ var _ = Describe("APIRule Controller", Serial, func() { }) Context("when creating an APIRule for exposing service", func() { - Context("on all the paths,", func() { Context("secured with Oauth2 introspection,", func() { Context("in a happy-path scenario", func() { @@ -1466,7 +1462,7 @@ var _ = Describe("APIRule Controller", Serial, func() { // given apiRuleName := generateTestName(testNameBase, testIDLength) serviceName := testServiceNameBase - serviceHost := gatewayv2alpha1.Host("example.com") + serviceHost := gatewayv2alpha1.Host("test.some-example.com") serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) @@ -1483,11 +1479,32 @@ var _ = Describe("APIRule Controller", Serial, func() { }() }) - It("should create an APIRule with a short host name", func() { + It("should create an APIRule with a short name", func() { // given apiRuleName := generateTestName(testNameBase, testIDLength) serviceName := testServiceNameBase - serviceHost := gatewayv2alpha1.Host("example") + serviceHost := gatewayv2alpha1.Host("test--example") + serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} + + rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + rule.NoAuth = ptr.To(true) + apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + + // when + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + }() + }) + + It("should create an APIRule with short name that has length of 1 character", func() { + // given + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := testServiceNameBase + serviceHost := gatewayv2alpha1.Host("a") serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) @@ -1554,17 +1571,16 @@ var _ = Describe("APIRule Controller", Serial, func() { Expect(err.Error()).To(ContainSubstring("spec.hosts[0]: Too long: may not be longer than 255")) }) - It("should not create an APIRule with host name shorter than 1 character", func() { + invalidHelper := func(host gatewayv2alpha1.Host) { // given apiRuleName := generateTestName(testNameBase, testIDLength) serviceName := testServiceNameBase - serviceHosts := []*gatewayv2alpha1.Host{ptr.To(gatewayv2alpha1.Host(""))} + serviceHosts := []*gatewayv2alpha1.Host{ptr.To(gatewayv2alpha1.Host(host))} rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) rule.NoAuth = ptr.To(true) apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) svc := testService(serviceName, testNamespace, testServicePort) - // when Expect(c.Create(context.Background(), svc)).Should(Succeed()) err := c.Create(context.Background(), apiRule) @@ -1573,12 +1589,51 @@ var _ = Describe("APIRule Controller", Serial, func() { serviceTeardown(svc) }() Expect(err).Should(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("spec.hosts[0] in body should be at least 1 chars long")) + Expect(err.Error()).To(ContainSubstring("spec.hosts[0]: Invalid value: \"string\": Host must be a short name or a fully qualified domain name")) + } + + It("should not create an APIRule with an empty host", func() { + invalidHelper("") + }) + + It("should not create an APIRule when host name has uppercase letters", func() { + invalidHelper("eXample.com") + invalidHelper("example.cOm") + }) + + It("should not create an APIRule with host label longer than 63 characters", func() { + invalidHelper(gatewayv2alpha1.Host(strings.Repeat("a", 64) + ".com")) + invalidHelper(gatewayv2alpha1.Host("example." + strings.Repeat("a", 64))) + }) + + It("should not create an APIRule when any domain label is empty", func() { + invalidHelper(".com") + invalidHelper("example..com") + invalidHelper("example.") + }) + + It("should not create an APIRule when top level domain is too short", func() { + invalidHelper("example.c") + }) + + It("should not create an APIRule when host contains wrong characters", func() { + invalidHelper("*example.com") + invalidHelper("exam*ple.com") + invalidHelper("example*.com") + invalidHelper("example.*com") + invalidHelper("example.co*m") + invalidHelper("example.com*") + }) + + It("should not create an APIRule when host starts or ends with a hyphen", func() { + invalidHelper("-example.com") + invalidHelper("example-.com") + invalidHelper("example.-com") + invalidHelper("example.com-") }) }) Context("APIRule version v2alpha1 rule path validation", func() { - It("should fail when path consists of a path and *", func() { // given apiRuleName := generateTestName(testNameBase, testIDLength) diff --git a/internal/helpers/hosts.go b/internal/helpers/hosts.go new file mode 100644 index 000000000..93f75eca0 --- /dev/null +++ b/internal/helpers/hosts.go @@ -0,0 +1,22 @@ +package helpers + +import ( + "regexp" +) + +const ( + fqdnMaxLength = 255 +) + +var ( + regexFqdn = regexp.MustCompile(`^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$`) + regexShortName = regexp.MustCompile("^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$") +) + +func IsHostFqdn(host string) bool { + return len(host) <= fqdnMaxLength && regexFqdn.MatchString(host) +} + +func IsHostShortName(host string) bool { + return regexShortName.MatchString(host) +} diff --git a/internal/helpers/hosts_test.go b/internal/helpers/hosts_test.go new file mode 100644 index 000000000..090aa3fa2 --- /dev/null +++ b/internal/helpers/hosts_test.go @@ -0,0 +1,103 @@ +package helpers + +import ( + "fmt" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("IsHostFqdn", func() { + It("Should be false if host is empty", func() { + Expect(IsHostFqdn("")).To(BeFalse()) + }) + + It("Should be true if host is a valid FQDN", func() { + Expect(IsHostFqdn("host.example.com")).To(BeTrue()) + }) + + It("Should be false if host name has uppercase letters", func() { + Expect(IsHostFqdn("host.exaMple.com")).To(BeFalse()) + }) + + It("Should be true if host name has 255 chars", func() { + Expect(IsHostFqdn(fmt.Sprintf("%s.%s.%s.%s.eee", strings.Repeat("a", 63), strings.Repeat("b", 63), + strings.Repeat("c", 63), strings.Repeat("d", 59)))).To(BeTrue()) + }) + + It("Should be false if host name is longer than 255 chars", func() { + Expect(IsHostFqdn(fmt.Sprintf("%s.%s.%s.%s.eee", strings.Repeat("a", 63), strings.Repeat("b", 63), + strings.Repeat("c", 63), strings.Repeat("d", 60)))).To(BeFalse()) + }) + + It("Should be false if any domain label is longer than 63 chars", func() { + Expect(IsHostFqdn(fmt.Sprintf("%s.com", strings.Repeat("a", 64)))).To(BeFalse()) + Expect(IsHostFqdn(fmt.Sprintf("host.%s.com", strings.Repeat("a", 64)))).To(BeFalse()) + Expect(IsHostFqdn(fmt.Sprintf("host.example.%s", strings.Repeat("a", 64)))).To(BeFalse()) + }) + + It("Should be false if any domain label is empty", func() { + Expect(IsHostFqdn("host.")).To(BeFalse()) + Expect(IsHostFqdn(".com")).To(BeFalse()) + Expect(IsHostFqdn(".example.com")).To(BeFalse()) + Expect(IsHostFqdn("host..com")).To(BeFalse()) + Expect(IsHostFqdn("host.example.")).To(BeFalse()) + }) + + It("Should be false if top level domain is too short", func() { + Expect(IsHostFqdn("host.example.c")).To(BeFalse()) + }) + + It("Should be false if any domain label contain wrong characters", func() { + Expect(IsHostFqdn("*host.example.com")).To(BeFalse()) + Expect(IsHostFqdn("ho*st.example.com")).To(BeFalse()) + Expect(IsHostFqdn("host*.example.com")).To(BeFalse()) + Expect(IsHostFqdn("host.*example.com")).To(BeFalse()) + Expect(IsHostFqdn("host.exam*ple.com")).To(BeFalse()) + Expect(IsHostFqdn("host.example*.com")).To(BeFalse()) + Expect(IsHostFqdn("host.example.*com")).To(BeFalse()) + Expect(IsHostFqdn("host.example.co*m")).To(BeFalse()) + Expect(IsHostFqdn("host.example.com*")).To(BeFalse()) + }) + + It("Should be false if any segment in host name starts or ends with hyphen", func() { + Expect(IsHostFqdn("-host.example.com")).To(BeFalse()) + Expect(IsHostFqdn("host-.example.com")).To(BeFalse()) + Expect(IsHostFqdn("host.-example.com")).To(BeFalse()) + Expect(IsHostFqdn("host.example-.com")).To(BeFalse()) + Expect(IsHostFqdn("host.example.-com")).To(BeFalse()) + Expect(IsHostFqdn("host.example.com-")).To(BeFalse()) + }) +}) + +var _ = Describe("IsHostFqdn", func() { + It("Should be false if host is empty", func() { + Expect(IsHostShortName("")).To(BeFalse()) + }) + + It("Should be true if host is a valid short name", func() { + Expect(IsHostShortName("short-host--name")).To(BeTrue()) + }) + + It("Should be false if short host name has uppercase letters", func() { + Expect(IsHostShortName("sHort")).To(BeFalse()) + }) + + It("Should be true if short host name has 1 char", func() { + Expect(IsHostShortName("a")).To(BeTrue()) + }) + + It("Should be true if short host name has 63 chars", func() { + Expect(IsHostShortName(strings.Repeat("a", 63))).To(BeTrue()) + }) + + It("Should be false if short host name is longer than 63 chars", func() { + Expect(IsHostShortName(strings.Repeat("a", 64))).To(BeFalse()) + }) + + It("Should be false if short host name contains not allowed char", func() { + Expect(IsHostShortName("short-host.")).To(BeFalse()) + Expect(IsHostShortName(".short-host")).To(BeFalse()) + }) +}) diff --git a/internal/processing/default_domain/default_domain.go b/internal/processing/default_domain/default_domain.go index f1ba384d8..eecf9fa2e 100644 --- a/internal/processing/default_domain/default_domain.go +++ b/internal/processing/default_domain/default_domain.go @@ -70,7 +70,7 @@ func GetDomainFromKymaGateway(ctx context.Context, k8sClient client.Client) (str } if !strings.HasPrefix(httpsServers[0].Hosts[0], "*.") { - return "", fmt.Errorf(`gateway https server host %s does not start with a prefix "*."`, httpsServers[0].Hosts[0]) + return "", fmt.Errorf(`gateway https server host %s does not start with the prefix "*."`, httpsServers[0].Hosts[0]) } return strings.TrimPrefix(httpsServers[0].Hosts[0], "*."), nil @@ -88,7 +88,7 @@ func GetDomainFromGateway(ctx context.Context, k8sClient client.Client, gatewayN } if !strings.HasPrefix(gateway.Spec.Servers[0].Hosts[0], "*.") { - return "", fmt.Errorf(`gateway https server host %s does not start with a prefix "*."`, gateway.Spec.Servers[0].Hosts[0]) + return "", fmt.Errorf(`gateway server host %s does not start with the prefix "*."`, gateway.Spec.Servers[0].Hosts[0]) } return strings.TrimPrefix(gateway.Spec.Servers[0].Hosts[0], "*."), nil diff --git a/internal/processing/default_domain/default_domain_test.go b/internal/processing/default_domain/default_domain_test.go index 4840d8cee..2d847558b 100644 --- a/internal/processing/default_domain/default_domain_test.go +++ b/internal/processing/default_domain/default_domain_test.go @@ -143,7 +143,7 @@ var _ = Describe("GetDomainFromKymaGateway", func() { Expect(host).To(Equal("")) }) - It("should return error if gateway has a HTTPS server but host do not start with *. prefix", func() { + It(`should return error if gateway has a HTTPS server but host do not start with "*." prefix`, func() { // given gateway := networkingv1beta1.Gateway{ ObjectMeta: metav1.ObjectMeta{Name: kymaGatewayName, Namespace: kymaGatewayNamespace}, @@ -165,7 +165,7 @@ var _ = Describe("GetDomainFromKymaGateway", func() { // then Expect(err).Should(HaveOccurred()) - Expect(err.Error()).To(Equal(`gateway https server host local.kyma.dev does not start with a prefix "*."`)) + Expect(err.Error()).To(Equal(`gateway https server host local.kyma.dev does not start with the prefix "*."`)) Expect(host).To(Equal("")) }) @@ -191,7 +191,7 @@ var _ = Describe("GetDomainFromGateway", func() { Spec: apinetworkingv1beta1.Gateway{ Servers: []*apinetworkingv1beta1.Server{ { - Port: &apinetworkingv1beta1.Port{Protocol: "HTTPS"}, + Port: &apinetworkingv1beta1.Port{Protocol: "HTTP"}, Hosts: []string{ "*.local.kyma.dev", }, @@ -275,7 +275,7 @@ var _ = Describe("GetDomainFromGateway", func() { Expect(host).To(Equal("")) }) - It("should return error if gateway has a HTTPS server but host do not start with *. prefix", func() { + It(`should return error if gateway has a HTTPS server but host do not start with "*." prefix`, func() { // given gateway := networkingv1beta1.Gateway{ ObjectMeta: metav1.ObjectMeta{Name: "gateway-name", Namespace: "gateway-namespace"}, @@ -297,7 +297,7 @@ var _ = Describe("GetDomainFromGateway", func() { // then Expect(err).Should(HaveOccurred()) - Expect(err.Error()).To(Equal(`gateway https server host local.kyma.dev does not start with a prefix "*."`)) + Expect(err.Error()).To(Equal(`gateway server host local.kyma.dev does not start with the prefix "*."`)) Expect(host).To(Equal("")) }) diff --git a/internal/processing/processors/v2alpha1/virtualservice/virtual_service_processor.go b/internal/processing/processors/v2alpha1/virtualservice/virtual_service_processor.go index b6d97d5cc..fa83f23cd 100644 --- a/internal/processing/processors/v2alpha1/virtualservice/virtual_service_processor.go +++ b/internal/processing/processors/v2alpha1/virtualservice/virtual_service_processor.go @@ -7,6 +7,7 @@ import ( gatewayv2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" "github.com/kyma-project/api-gateway/internal/builders" + "github.com/kyma-project/api-gateway/internal/helpers" "github.com/kyma-project/api-gateway/internal/processing" "github.com/kyma-project/api-gateway/internal/processing/default_domain" networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" @@ -90,8 +91,11 @@ func (r virtualServiceCreator) Create(api *gatewayv2alpha1.APIRule) (*networking vsSpecBuilder := builders.VirtualServiceSpec() for _, host := range api.Spec.Hosts { - // TODO: continue here - vsSpecBuilder.AddHost(default_domain.GetHostWithDomain(string(*host), r.defaultDomainName)) + if helpers.IsHostShortName(string(*host)) { + + } else { + vsSpecBuilder.AddHost(default_domain.GetHostWithDomain(string(*host), r.defaultDomainName)) + } } vsSpecBuilder.Gateway(*api.Spec.Gateway) diff --git a/internal/processing/reconciliation.go b/internal/processing/reconciliation.go index 7d572e734..8e9a17a7d 100644 --- a/internal/processing/reconciliation.go +++ b/internal/processing/reconciliation.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/go-logr/logr" gatewayv1beta1 "github.com/kyma-project/api-gateway/apis/gateway/v1beta1" "github.com/kyma-project/api-gateway/internal/processing/status" @@ -51,7 +52,6 @@ func Reconcile(ctx context.Context, client client.Client, log *logr.Logger, cmd } for _, processor := range cmd.GetProcessors() { - objectChanges, err := processor.EvaluateReconciliation(ctx, client) if err != nil { l.Error(err, "Error during reconciliation") diff --git a/internal/validation/v2alpha1/hosts.go b/internal/validation/v2alpha1/hosts.go index 2c817d63f..41644c3fb 100644 --- a/internal/validation/v2alpha1/hosts.go +++ b/internal/validation/v2alpha1/hosts.go @@ -2,24 +2,15 @@ package v2alpha1 import ( "fmt" - "regexp" "strings" gatewayv1beta1 "github.com/kyma-project/api-gateway/apis/gateway/v1beta1" gatewayv2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" + "github.com/kyma-project/api-gateway/internal/helpers" "github.com/kyma-project/api-gateway/internal/validation" networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" ) -const ( - fqdnMaxLength = 253 -) - -var ( - regexFqdn = regexp.MustCompile(`^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$`) - regexFqdnLabel = regexp.MustCompile("^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$") -) - func validateHosts(parentAttributePath string, vsList networkingv1beta1.VirtualServiceList, gwList networkingv1beta1.GatewayList, apiRule *gatewayv2alpha1.APIRule) []validation.Failure { var failures []validation.Failure hostsAttributePath := parentAttributePath + ".hosts" @@ -34,8 +25,8 @@ func validateHosts(parentAttributePath string, vsList networkingv1beta1.VirtualS } for hostIndex, host := range hosts { - if !isFqdn(string(*host)) { - if isFqdnLabel(string(*host)) { // short name + if !helpers.IsHostFqdn(string(*host)) { + if helpers.IsHostShortName(string(*host)) { // short name gateway := findGateway(*apiRule.Spec.Gateway, gwList) if gateway == nil { hostAttributePath := fmt.Sprintf("%s[%d]", hostsAttributePath, hostIndex) @@ -99,14 +90,6 @@ func findGateway(name string, gwList networkingv1beta1.GatewayList) *networkingv return nil } -func isFqdn(host string) bool { - return len(host) <= fqdnMaxLength && regexFqdn.MatchString(host) -} - -func isFqdnLabel(host string) bool { - return regexFqdnLabel.MatchString(host) -} - func occupiesHost(vs *networkingv1beta1.VirtualService, host string) bool { for _, h := range vs.Spec.Hosts { if h == host { diff --git a/internal/validation/v2alpha1/hosts_test.go b/internal/validation/v2alpha1/hosts_test.go index 5c4c31945..235069859 100644 --- a/internal/validation/v2alpha1/hosts_test.go +++ b/internal/validation/v2alpha1/hosts_test.go @@ -1,9 +1,6 @@ package v2alpha1 import ( - "fmt" - "strings" - "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -160,180 +157,6 @@ var _ = Describe("Validate hosts", func() { Expect(problems[0].Message).To(Equal("Short host only supported when Gateway has single host definition matching *. format")) }) - It("Should fail if host name has uppercase letters", func() { - //given - apiRule := &v2alpha1.APIRule{ - Spec: v2alpha1.APIRuleSpec{ - Hosts: []*v2alpha1.Host{ - ptr.To(v2alpha1.Host("host.exaMple.com")), - }, - }, - } - - //when - problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, networkingv1beta1.GatewayList{}, apiRule) - - //then - Expect(problems).To(HaveLen(1)) - Expect(problems[0].AttributePath).To(Equal(".spec.hosts[0]")) - Expect(problems[0].Message).To(Equal("Host must be a valid FQDN or short name")) - }) - - It("Should allow lenghty host name with numbers and hyphens", func() { - //given - apiRule := &v2alpha1.APIRule{ - Spec: v2alpha1.APIRuleSpec{ - Hosts: []*v2alpha1.Host{ - ptr.To(v2alpha1.Host("host-with-numbers-1234567890-and-hyphens---------up-to-63-chars.domain-with-numbers-1234567890-and-hyphens-------up-to-63-chars.com")), - }, - }, - } - - //when - problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, networkingv1beta1.GatewayList{}, apiRule) - - //then - Expect(problems).To(HaveLen(0)) - }) - - It("Should fail if the host is longer than 253 characters", func() { - //given - apiRule := &v2alpha1.APIRule{ - Spec: v2alpha1.APIRuleSpec{ - Hosts: []*v2alpha1.Host{ - ptr.To(v2alpha1.Host("long.%s.com" + strings.Repeat("a", 245))), - }, - }, - } - - //when - problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, networkingv1beta1.GatewayList{}, apiRule) - - //then - Expect(problems).To(HaveLen(1)) - Expect(problems[0].AttributePath).To(Equal(".spec.hosts[0]")) - Expect(problems[0].Message).To(Equal("Host must be a valid FQDN or short name")) - }) - - It("Should fail if any domain label is longer than 63 chars", func() { - //given - apiRule := &v2alpha1.APIRule{ - Spec: v2alpha1.APIRuleSpec{ - Hosts: []*v2alpha1.Host{ - ptr.To(v2alpha1.Host(fmt.Sprintf("%s.com", strings.Repeat("a", 64)))), - ptr.To(v2alpha1.Host(fmt.Sprintf("host.%s.com", strings.Repeat("a", 64)))), - ptr.To(v2alpha1.Host(fmt.Sprintf("host.example.%s", strings.Repeat("a", 64)))), - }, - }, - } - - //when - problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, networkingv1beta1.GatewayList{}, apiRule) - - //then - Expect(problems).To(HaveLen(3)) - for i := 0; i < 3; i++ { - Expect(problems[i].AttributePath).To(Equal(fmt.Sprintf(".spec.hosts[%d]", i))) - Expect(problems[i].Message).To(Equal("Host must be a valid FQDN or short name")) - } - }) - - It("Should fail if any domain label is empty", func() { - //given - apiRule := &v2alpha1.APIRule{ - Spec: v2alpha1.APIRuleSpec{ - Hosts: []*v2alpha1.Host{ - ptr.To(v2alpha1.Host(".domain.com")), - ptr.To(v2alpha1.Host("host..com")), - ptr.To(v2alpha1.Host("host.domain.")), - }, - }, - } - - //when - problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, networkingv1beta1.GatewayList{}, apiRule) - - //then - Expect(problems).To(HaveLen(3)) - for i := 0; i < 3; i++ { - Expect(problems[i].AttributePath).To(Equal(fmt.Sprintf(".spec.hosts[%d]", i))) - Expect(problems[i].Message).To(Equal("Host must be a valid FQDN or short name")) - } - }) - - It("Should fail if top level domain is too short", func() { - //given - apiRule := &v2alpha1.APIRule{ - Spec: v2alpha1.APIRuleSpec{ - Hosts: []*v2alpha1.Host{ - ptr.To(v2alpha1.Host("too.short.x")), - }, - }, - } - - //when - problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, networkingv1beta1.GatewayList{}, apiRule) - - //then - Expect(problems[0].AttributePath).To(Equal(".spec.hosts[0]")) - Expect(problems[0].Message).To(Equal("Host must be a valid FQDN or short name")) - }) - - It("Should fail if any domain label contain wrong characters", func() { - //given - apiRule := &v2alpha1.APIRule{ - Spec: v2alpha1.APIRuleSpec{ - Hosts: []*v2alpha1.Host{ - ptr.To(v2alpha1.Host("wrong-*-inside.example.com")), - ptr.To(v2alpha1.Host("host.wrong-*-inside.com")), - ptr.To(v2alpha1.Host("host.example.wrong-*-inside")), - ptr.To(v2alpha1.Host("*-wrong.example.com")), - ptr.To(v2alpha1.Host("host.*-wrong.com")), - ptr.To(v2alpha1.Host("host.example.*-wrong")), - ptr.To(v2alpha1.Host("wrong-*.example.com")), - ptr.To(v2alpha1.Host("host.wrong-*.com")), - ptr.To(v2alpha1.Host("host.example.wrong-*")), - }, - }, - } - - //when - problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, networkingv1beta1.GatewayList{}, apiRule) - - //then - Expect(problems).To(HaveLen(9)) - for i := 0; i < 9; i++ { - Expect(problems[i].AttributePath).To(Equal(fmt.Sprintf(".spec.hosts[%d]", i))) - Expect(problems[i].Message).To(Equal("Host must be a valid FQDN or short name")) - } - }) - - It("Should fail if any segment in host name starts or ends with hyphen", func() { - //given - apiRule := &v2alpha1.APIRule{ - Spec: v2alpha1.APIRuleSpec{ - Hosts: []*v2alpha1.Host{ - ptr.To(v2alpha1.Host("-host.example.com")), - ptr.To(v2alpha1.Host("host-.example.com")), - ptr.To(v2alpha1.Host("host.example-.com")), - ptr.To(v2alpha1.Host("host.-example.com")), - ptr.To(v2alpha1.Host("host.example.-com")), - ptr.To(v2alpha1.Host("host.example.com-")), - }, - }, - } - - //when - problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, networkingv1beta1.GatewayList{}, apiRule) - - //then - Expect(problems).To(HaveLen(6)) - for i := 0; i < 6; i++ { - Expect(problems[i].AttributePath).To(Equal(fmt.Sprintf(".spec.hosts[%d]", i))) - Expect(problems[i].Message).To(Equal("Host must be a valid FQDN or short name")) - } - }) - It("Should fail if any host that is occupied by any Virtual Service exposed by another resource", func() { //given apiRule := &v2alpha1.APIRule{ diff --git a/internal/validation/validate.go b/internal/validation/validate.go index beea056af..a2443f230 100644 --- a/internal/validation/validate.go +++ b/internal/validation/validate.go @@ -3,6 +3,7 @@ package validation import ( "context" "fmt" + "github.com/kyma-project/api-gateway/internal/helpers" "golang.org/x/exp/slices" networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" From 3cfb14ccb8445e791926dfd45e06f4dd4a7f1115 Mon Sep 17 00:00:00 2001 From: Vladimir Videlov Date: Mon, 23 Sep 2024 15:54:06 +0200 Subject: [PATCH 05/27] State --- .../api_controller_integration_test.go | 1008 +++++++++-------- controllers/gateway/apirule_controller.go | 20 +- .../processors/migration/processors.go | 2 +- .../processors/v2alpha1/reconciliation.go | 5 +- .../v2alpha1/reconciliation_test.go | 19 +- .../processors/v2alpha1/status_test.go | 2 +- .../v2alpha1/virtualservice/cors_test.go | 10 +- .../v2alpha1/virtualservice/hosts_test.go | 69 +- .../virtualservice/http_matching_test.go | 13 +- .../v2alpha1/virtualservice/request_test.go | 17 +- .../virtual_service_processor.go | 12 +- .../virtual_service_processor_test.go | 24 +- internal/validation/v2alpha1/hosts.go | 12 +- 13 files changed, 680 insertions(+), 533 deletions(-) diff --git a/controllers/gateway/api_controller_integration_test.go b/controllers/gateway/api_controller_integration_test.go index 163c0b10b..c8a6784aa 100644 --- a/controllers/gateway/api_controller_integration_test.go +++ b/controllers/gateway/api_controller_integration_test.go @@ -36,7 +36,6 @@ import ( // Tests needs to be executed serially because of the shared state of the JWT Handler in the API Controller. var _ = Describe("APIRule Controller", Serial, func() { - const ( testNameBase = "test" testIDLength = 5 @@ -154,6 +153,7 @@ var _ = Describe("APIRule Controller", Serial, func() { defer func() { apiRuleTeardown(apiRule) serviceTeardown(svc) + kymaGatewayTeardown(&gateway) }() expectApiRuleStatus(apiRuleName, gatewayv1beta1.StatusOK) @@ -175,197 +175,6 @@ var _ = Describe("APIRule Controller", Serial, func() { }) }) - Context("when creating APIRule in version v2alpha1 respect x-validation rules only", Ordered, func() { - BeforeAll(func() { - updateJwtHandlerTo(helpers.JWT_HANDLER_ORY) - }) - - It("should be able to create an APIRule with noAuth=true", func() { - apiRuleName := generateTestName(testNameBase, testIDLength) - serviceName := testServiceNameBase - serviceHost := gatewayv2alpha1.Host("httpbin-istio-jwt-happy-base.kyma.local") - serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} - - rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) - rule.NoAuth = ptr.To(true) - apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) - svc := testService(serviceName, testNamespace, testServicePort) - - // when - Expect(c.Create(context.Background(), svc)).Should(Succeed()) - Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) - defer func() { - apiRulev2alpha1Teardown(apiRule) - serviceTeardown(svc) - }() - }) - - It("should be able to create an APIRule with jwt", func() { - apiRuleName := generateTestName(testNameBase, testIDLength) - serviceName := testServiceNameBase - serviceHost := gatewayv2alpha1.Host("httpbin-istio-jwt-happy-base.kyma.local") - serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} - - rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) - rule.Jwt = &gatewayv2alpha1.JwtConfig{ - Authentications: []*gatewayv2alpha1.JwtAuthentication{}, - Authorizations: []*gatewayv2alpha1.JwtAuthorization{}, - } - apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) - svc := testService(serviceName, testNamespace, testServicePort) - - // when - Expect(c.Create(context.Background(), svc)).Should(Succeed()) - Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) - defer func() { - apiRulev2alpha1Teardown(apiRule) - serviceTeardown(svc) - }() - }) - - It("should be able to create an APIRule with jwt and noAuth=false", func() { - apiRuleName := generateTestName(testNameBase, testIDLength) - serviceName := testServiceNameBase - serviceHost := gatewayv2alpha1.Host("httpbin-istio-jwt-happy-base.kyma.local") - serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} - - rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) - rule.NoAuth = ptr.To(false) - rule.Jwt = &gatewayv2alpha1.JwtConfig{ - Authentications: []*gatewayv2alpha1.JwtAuthentication{}, - Authorizations: []*gatewayv2alpha1.JwtAuthorization{}, - } - apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) - svc := testService(serviceName, testNamespace, testServicePort) - - // when - Expect(c.Create(context.Background(), svc)).Should(Succeed()) - Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) - defer func() { - apiRulev2alpha1Teardown(apiRule) - serviceTeardown(svc) - }() - }) - - It("should be able to create an APIRule with jwt and mutators", func() { - apiRuleName := generateTestName(testNameBase, testIDLength) - serviceName := testServiceNameBase - serviceHost := gatewayv2alpha1.Host("httpbin-istio-jwt-happy-base.kyma.local") - serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} - - rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) - rule.Jwt = &gatewayv2alpha1.JwtConfig{ - Authentications: []*gatewayv2alpha1.JwtAuthentication{}, - Authorizations: []*gatewayv2alpha1.JwtAuthorization{}, - } - apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) - svc := testService(serviceName, testNamespace, testServicePort) - - // when - Expect(c.Create(context.Background(), svc)).Should(Succeed()) - Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) - defer func() { - apiRulev2alpha1Teardown(apiRule) - serviceTeardown(svc) - }() - }) - - It("should fail to create an APIRule without noAuth and jwt", func() { - apiRuleName := generateTestName(testNameBase, testIDLength) - serviceName := testServiceNameBase - serviceHost := gatewayv2alpha1.Host("httpbin-istio-jwt-happy-base.kyma.local") - serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} - - rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) - apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) - svc := testService(serviceName, testNamespace, testServicePort) - - // when - Expect(c.Create(context.Background(), svc)).Should(Succeed()) - err := c.Create(context.Background(), apiRule) - defer func() { - apiRulev2alpha1Teardown(apiRule) - serviceTeardown(svc) - }() - - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("One of the following fields must be set: noAuth, jwt, extAuth")) - }) - - It("should fail to create an APIRule with noAuth=false", func() { - apiRuleName := generateTestName(testNameBase, testIDLength) - serviceName := testServiceNameBase - serviceHost := gatewayv2alpha1.Host("httpbin-istio-jwt-happy-base.kyma.local") - serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} - - rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) - rule.NoAuth = ptr.To(false) - apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) - svc := testService(serviceName, testNamespace, testServicePort) - - // when - Expect(c.Create(context.Background(), svc)).Should(Succeed()) - err := c.Create(context.Background(), apiRule) - defer func() { - apiRulev2alpha1Teardown(apiRule) - serviceTeardown(svc) - }() - - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("One of the following fields must be set: noAuth, jwt, extAuth")) - }) - - It("should fail to create an APIRule with jwt and noAuth=true", func() { - apiRuleName := generateTestName(testNameBase, testIDLength) - serviceName := testServiceNameBase - serviceHost := gatewayv2alpha1.Host("httpbin-istio-jwt-happy-base.kyma.local") - serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} - - rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) - rule.NoAuth = ptr.To(true) - rule.Jwt = &gatewayv2alpha1.JwtConfig{ - Authentications: []*gatewayv2alpha1.JwtAuthentication{}, - Authorizations: []*gatewayv2alpha1.JwtAuthorization{}, - } - apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) - svc := testService(serviceName, testNamespace, testServicePort) - - // when - Expect(c.Create(context.Background(), svc)).Should(Succeed()) - err := c.Create(context.Background(), apiRule) - defer func() { - apiRulev2alpha1Teardown(apiRule) - serviceTeardown(svc) - }() - - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("One of the following fields must be set: noAuth, jwt, extAuth")) - }) - - It("should fail to create an APIRule with more than one host", func() { - apiRuleName := generateTestName(testNameBase, testIDLength) - serviceName := testServiceNameBase - serviceHost := gatewayv2alpha1.Host("httpbin-istio-jwt-happy-base.kyma.local") - secondServiceHost := gatewayv2alpha1.Host("other-istio-jwt-happy-base.kyma.local") - serviceHosts := []*gatewayv2alpha1.Host{&serviceHost, &secondServiceHost} - - rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) - rule.NoAuth = ptr.To(true) - apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) - svc := testService(serviceName, testNamespace, testServicePort) - - // when - Expect(c.Create(context.Background(), svc)).Should(Succeed()) - err := c.Create(context.Background(), apiRule) - defer func() { - apiRulev2alpha1Teardown(apiRule) - serviceTeardown(svc) - }() - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("spec.hosts: Too many: 2: must have at most 1 items")) - }) - }) - Context("when updating the APIRule with multiple paths", func() { It("should create, update and delete rules depending on patch match", func() { updateJwtHandlerTo(helpers.JWT_HANDLER_ORY) @@ -1294,7 +1103,6 @@ var _ = Describe("APIRule Controller", Serial, func() { }) Context("Handler is istio and ApiRule with JWT handler specific resources exists", func() { - Context("changing jwt handler to ory", func() { It("Should have validation errors for APiRule JWT handler configuration and resources are not deleted", func() { // given @@ -1391,319 +1199,560 @@ var _ = Describe("APIRule Controller", Serial, func() { }) }) - Context("when creating APIRule in version v2alpha1 gateway name should be valid", Ordered, func() { - It("should create an APIRule with a valid gateway", func() { - // given - apiRuleName := generateTestName(testNameBase, testIDLength) - serviceName := testServiceNameBase - - rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) - rule.NoAuth = ptr.To(true) - apiRule := testApiRulev2alpha1Gateway(apiRuleName, testNamespace, serviceName, testNamespace, testGatewayURL, testServicePort, []gatewayv2alpha1.Rule{rule}) - svc := testService(serviceName, testNamespace, testServicePort) - - // when - Expect(c.Create(context.Background(), svc)).Should(Succeed()) - Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) - defer func() { - apiRulev2alpha1Teardown(apiRule) - serviceTeardown(svc) - }() - }) - - invalidHelper := func(gatewayName string) { - // given - apiRuleName := generateTestName(testNameBase, testIDLength) - serviceName := testServiceNameBase - - rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) - rule.NoAuth = ptr.To(true) - apiRule := testApiRulev2alpha1Gateway(apiRuleName, testNamespace, serviceName, testNamespace, gatewayName, testServicePort, []gatewayv2alpha1.Rule{rule}) - svc := testService(serviceName, testNamespace, testServicePort) - - // when - Expect(c.Create(context.Background(), svc)).Should(Succeed()) - err := c.Create(context.Background(), apiRule) - defer func() { - apiRulev2alpha1Teardown(apiRule) - serviceTeardown(svc) - }() - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("spec.gateway: Invalid value: \"string\": Gateway must be in the namespace/name format")) - } - - It("should not create an APIRule with an empty gateway", func() { - invalidHelper("") - }) - - It("should not create an APIRule with too long gateway namespace name", func() { - invalidHelper("insane-very-long-namespace-name-exceeding-sixty-three-characters/validname") - }) - - It("should not create an APIRule with too long gateway name", func() { - invalidHelper("validnamespace/insane-very-long-namespace-name-exceeding-sixty-three-characters") - }) - - It("should not create an APIRule with just the namespace", func() { - invalidHelper("validnamespace/") - }) - - It("should not create an APIRule with just the gateway name", func() { - invalidHelper("/validgateway") - }) + Context("when creating APIRule in version v2alpha1", Ordered, func() { + Context("respect x-validation rules only", Ordered, func() { + BeforeAll(func() { + updateJwtHandlerTo(helpers.JWT_HANDLER_ORY) + }) - It("should not create an APIRule with double slashed gateway name", func() { - invalidHelper("namespace//gateway") - }) - }) + It("should be able to create an APIRule with noAuth=true", func() { + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := testServiceNameBase + serviceHost := gatewayv2alpha1.Host("httpbin-istio-jwt-happy-base.kyma.local") + serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} + + rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + rule.NoAuth = ptr.To(true) + apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + + // when + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + }() + }) - Context("when creating APIRule in version v2alpha1 hosts should be a valid FQDN or a short name", Ordered, func() { - It("should create an APIRule with a valid FQDN host", func() { - // given - apiRuleName := generateTestName(testNameBase, testIDLength) - serviceName := testServiceNameBase - serviceHost := gatewayv2alpha1.Host("test.some-example.com") - serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} + It("should be able to create an APIRule with jwt", func() { + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := testServiceNameBase + serviceHost := gatewayv2alpha1.Host("httpbin-istio-jwt-happy-base.kyma.local") + serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} - rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) - rule.NoAuth = ptr.To(true) - apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) - svc := testService(serviceName, testNamespace, testServicePort) + rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + rule.Jwt = &gatewayv2alpha1.JwtConfig{ + Authentications: []*gatewayv2alpha1.JwtAuthentication{}, + Authorizations: []*gatewayv2alpha1.JwtAuthorization{}, + } + apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + + // when + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + }() + }) - // when - Expect(c.Create(context.Background(), svc)).Should(Succeed()) - Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) - defer func() { - apiRulev2alpha1Teardown(apiRule) - serviceTeardown(svc) - }() - }) + It("should be able to create an APIRule with jwt and noAuth=false", func() { + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := testServiceNameBase + serviceHost := gatewayv2alpha1.Host("httpbin-istio-jwt-happy-base.kyma.local") + serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} + + rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + rule.NoAuth = ptr.To(false) + rule.Jwt = &gatewayv2alpha1.JwtConfig{ + Authentications: []*gatewayv2alpha1.JwtAuthentication{}, + Authorizations: []*gatewayv2alpha1.JwtAuthorization{}, + } + apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + + // when + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + }() + }) - It("should create an APIRule with a short name", func() { - // given - apiRuleName := generateTestName(testNameBase, testIDLength) - serviceName := testServiceNameBase - serviceHost := gatewayv2alpha1.Host("test--example") - serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} + It("should be able to create an APIRule with jwt and mutators", func() { + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := testServiceNameBase + serviceHost := gatewayv2alpha1.Host("httpbin-istio-jwt-happy-base.kyma.local") + serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} - rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) - rule.NoAuth = ptr.To(true) - apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) - svc := testService(serviceName, testNamespace, testServicePort) + rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + rule.Jwt = &gatewayv2alpha1.JwtConfig{ + Authentications: []*gatewayv2alpha1.JwtAuthentication{}, + Authorizations: []*gatewayv2alpha1.JwtAuthorization{}, + } + apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + + // when + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + }() + }) - // when - Expect(c.Create(context.Background(), svc)).Should(Succeed()) - Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) - defer func() { - apiRulev2alpha1Teardown(apiRule) - serviceTeardown(svc) - }() - }) + It("should fail to create an APIRule without noAuth and jwt", func() { + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := testServiceNameBase + serviceHost := gatewayv2alpha1.Host("httpbin-istio-jwt-happy-base.kyma.local") + serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} + + rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + + // when + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + err := c.Create(context.Background(), apiRule) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + }() + + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("One of the following fields must be set: noAuth, jwt, extAuth")) + }) - It("should create an APIRule with short name that has length of 1 character", func() { - // given - apiRuleName := generateTestName(testNameBase, testIDLength) - serviceName := testServiceNameBase - serviceHost := gatewayv2alpha1.Host("a") - serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} + It("should fail to create an APIRule with noAuth=false", func() { + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := testServiceNameBase + serviceHost := gatewayv2alpha1.Host("httpbin-istio-jwt-happy-base.kyma.local") + serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} + + rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + rule.NoAuth = ptr.To(false) + apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + + // when + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + err := c.Create(context.Background(), apiRule) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + }() + + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("One of the following fields must be set: noAuth, jwt, extAuth")) + }) - rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) - rule.NoAuth = ptr.To(true) - apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) - svc := testService(serviceName, testNamespace, testServicePort) + It("should fail to create an APIRule with jwt and noAuth=true", func() { + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := testServiceNameBase + serviceHost := gatewayv2alpha1.Host("httpbin-istio-jwt-happy-base.kyma.local") + serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} + + rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + rule.NoAuth = ptr.To(true) + rule.Jwt = &gatewayv2alpha1.JwtConfig{ + Authentications: []*gatewayv2alpha1.JwtAuthentication{}, + Authorizations: []*gatewayv2alpha1.JwtAuthorization{}, + } + apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + + // when + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + err := c.Create(context.Background(), apiRule) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + }() + + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("One of the following fields must be set: noAuth, jwt, extAuth")) + }) - // when - Expect(c.Create(context.Background(), svc)).Should(Succeed()) - Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) - defer func() { - apiRulev2alpha1Teardown(apiRule) - serviceTeardown(svc) - }() + It("should fail to create an APIRule with more than one host", func() { + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := testServiceNameBase + serviceHost := gatewayv2alpha1.Host("httpbin-istio-jwt-happy-base.kyma.local") + secondServiceHost := gatewayv2alpha1.Host("other-istio-jwt-happy-base.kyma.local") + serviceHosts := []*gatewayv2alpha1.Host{&serviceHost, &secondServiceHost} + + rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + rule.NoAuth = ptr.To(true) + apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + + // when + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + err := c.Create(context.Background(), apiRule) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + }() + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("spec.hosts: Too many: 2: must have at most 1 items")) + }) }) - It("should create an APIRule with host name that has length of 255 characters", func() { - // given - apiRuleName := generateTestName(testNameBase, testIDLength) - serviceName := testServiceNameBase - sixtyThreeA := strings.Repeat("a", 63) - host255 := fmt.Sprintf("%s.%s.%s.%s.com", sixtyThreeA, sixtyThreeA, sixtyThreeA, strings.Repeat("b", 59)) - serviceHost := gatewayv2alpha1.Host(host255) - serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} - - rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) - rule.NoAuth = ptr.To(true) - apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) - svc := testService(serviceName, testNamespace, testServicePort) + Context("gateway name should be valid", Ordered, func() { + It("should create an APIRule with a valid gateway", func() { + // given + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := testServiceNameBase + + rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + rule.NoAuth = ptr.To(true) + apiRule := testApiRulev2alpha1Gateway(apiRuleName, testNamespace, serviceName, testNamespace, testGatewayURL, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + + // when + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + }() + }) - // when - Expect(host255).To(HaveLen(255)) - Expect(c.Create(context.Background(), svc)).Should(Succeed()) - Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) - defer func() { - apiRulev2alpha1Teardown(apiRule) - serviceTeardown(svc) - }() - }) + invalidHelper := func(gatewayName string) { + // given + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := testServiceNameBase + + rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + rule.NoAuth = ptr.To(true) + apiRule := testApiRulev2alpha1Gateway(apiRuleName, testNamespace, serviceName, testNamespace, gatewayName, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + + // when + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + err := c.Create(context.Background(), apiRule) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + }() + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("spec.gateway: Invalid value: \"string\": Gateway must be in the namespace/name format")) + } - It("should not create an APIRule with host name longer than 255 characters", func() { - // given - apiRuleName := generateTestName(testNameBase, testIDLength) - serviceName := testServiceNameBase - sixtyThreeA := strings.Repeat("a", 63) - host256 := fmt.Sprintf("%s.%s.%s.%s.com", sixtyThreeA, sixtyThreeA, sixtyThreeA, strings.Repeat("b", 60)) - serviceHost := gatewayv2alpha1.Host(host256) - serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} - - rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) - rule.NoAuth = ptr.To(true) - apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) - svc := testService(serviceName, testNamespace, testServicePort) + It("should not create an APIRule with an empty gateway", func() { + invalidHelper("") + }) - // when - Expect(host256).To(HaveLen(256)) - Expect(c.Create(context.Background(), svc)).Should(Succeed()) - err := c.Create(context.Background(), apiRule) - defer func() { - apiRulev2alpha1Teardown(apiRule) - serviceTeardown(svc) - }() - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("spec.hosts[0]: Too long: may not be longer than 255")) - }) + It("should not create an APIRule with too long gateway namespace name", func() { + invalidHelper("insane-very-long-namespace-name-exceeding-sixty-three-characters/validname") + }) - invalidHelper := func(host gatewayv2alpha1.Host) { - // given - apiRuleName := generateTestName(testNameBase, testIDLength) - serviceName := testServiceNameBase - serviceHosts := []*gatewayv2alpha1.Host{ptr.To(gatewayv2alpha1.Host(host))} + It("should not create an APIRule with too long gateway name", func() { + invalidHelper("validnamespace/insane-very-long-namespace-name-exceeding-sixty-three-characters") + }) - rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) - rule.NoAuth = ptr.To(true) - apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) - svc := testService(serviceName, testNamespace, testServicePort) - // when - Expect(c.Create(context.Background(), svc)).Should(Succeed()) - err := c.Create(context.Background(), apiRule) - defer func() { - apiRulev2alpha1Teardown(apiRule) - serviceTeardown(svc) - }() - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("spec.hosts[0]: Invalid value: \"string\": Host must be a short name or a fully qualified domain name")) - } + It("should not create an APIRule with just the namespace", func() { + invalidHelper("validnamespace/") + }) - It("should not create an APIRule with an empty host", func() { - invalidHelper("") - }) + It("should not create an APIRule with just the gateway name", func() { + invalidHelper("/validgateway") + }) - It("should not create an APIRule when host name has uppercase letters", func() { - invalidHelper("eXample.com") - invalidHelper("example.cOm") + It("should not create an APIRule with double slashed gateway name", func() { + invalidHelper("namespace//gateway") + }) }) - It("should not create an APIRule with host label longer than 63 characters", func() { - invalidHelper(gatewayv2alpha1.Host(strings.Repeat("a", 64) + ".com")) - invalidHelper(gatewayv2alpha1.Host("example." + strings.Repeat("a", 64))) - }) + Context("hosts should be a valid FQDN or a short name", Ordered, func() { + It("should create an APIRule with a valid FQDN host", func() { + // given + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := testServiceNameBase + serviceHost := gatewayv2alpha1.Host("test.some-example.com") + serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} + + rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + rule.NoAuth = ptr.To(true) + apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + + // when + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + }() + }) - It("should not create an APIRule when any domain label is empty", func() { - invalidHelper(".com") - invalidHelper("example..com") - invalidHelper("example.") - }) + It("should create an APIRule with a short name and apply host domain from gateway", func() { + // given + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := testServiceNameBase + serviceHost := gatewayv2alpha1.Host("test--example") + serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} + + rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + rule.NoAuth = ptr.To(true) + apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + + By("Creating Kyma gateway") + gateway := networkingv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{Name: "kyma-gateway", Namespace: "kyma-system"}, + Spec: apinetworkingv1beta1.Gateway{ + Servers: []*apinetworkingv1beta1.Server{ + { + Port: &apinetworkingv1beta1.Port{ + Protocol: "HTTPS", + }, + Hosts: []string{ + "*.local.kyma.dev", + }, + }, + }, + }, + } - It("should not create an APIRule when top level domain is too short", func() { - invalidHelper("example.c") - }) + // when + Expect(c.Create(context.Background(), &gateway)).Should(Succeed()) + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + kymaGatewayTeardown(&gateway) + }() + + expectApiRuleStatus(apiRuleName, gatewayv1beta1.StatusOK) + + matchingLabels := matchingLabelsFunc(apiRuleName, testNamespace) + + By("Verifying created virtual service") + vsList := networkingv1beta1.VirtualServiceList{} + Eventually(func(g Gomega) { + g.Expect(c.List(context.Background(), &vsList, matchingLabels)).Should(Succeed()) + g.Expect(vsList.Items).To(HaveLen(1)) + + vs := vsList.Items[0] + + //Meta + g.Expect(vs.Name).To(HavePrefix(apiRuleName + "-")) + g.Expect(len(vs.Name) > len(apiRuleName)).To(BeTrue()) + + expectedSpec := builders.VirtualServiceSpec(). + AddHost("test--example.local.kyma.dev"). + Gateway(testGatewayURL). + HTTP(builders.HTTPRoute(). + Match(builders.MatchRequest().Uri().Regex(testPath)). + Route(builders.RouteDestination().Host(testOathkeeperSvcURL).Port(testOathkeeperPort)). + Headers(builders.NewHttpRouteHeadersBuilder().SetHostHeader("test--example.local.kyma.dev").Get()). + CorsPolicy(defaultCorsPolicy). + Timeout(defaultHttpTimeout)) + + gotSpec := *expectedSpec.Get() + g.Expect(*vs.Spec.DeepCopy()).To(Equal(*gotSpec.DeepCopy())) + }, eventuallyTimeout).Should(Succeed()) + }) - It("should not create an APIRule when host contains wrong characters", func() { - invalidHelper("*example.com") - invalidHelper("exam*ple.com") - invalidHelper("example*.com") - invalidHelper("example.*com") - invalidHelper("example.co*m") - invalidHelper("example.com*") - }) + It("should create an APIRule with short name that has length of 1 character", func() { + // given + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := testServiceNameBase + serviceHost := gatewayv2alpha1.Host("a") + serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} + + rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + rule.NoAuth = ptr.To(true) + apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + + // when + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + }() + }) - It("should not create an APIRule when host starts or ends with a hyphen", func() { - invalidHelper("-example.com") - invalidHelper("example-.com") - invalidHelper("example.-com") - invalidHelper("example.com-") - }) - }) + It("should create an APIRule with host name that has length of 255 characters", func() { + // given + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := testServiceNameBase + sixtyThreeA := strings.Repeat("a", 63) + host255 := fmt.Sprintf("%s.%s.%s.%s.com", sixtyThreeA, sixtyThreeA, sixtyThreeA, strings.Repeat("b", 59)) + serviceHost := gatewayv2alpha1.Host(host255) + serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} + + rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + rule.NoAuth = ptr.To(true) + apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + + // when + Expect(host255).To(HaveLen(255)) + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + }() + }) - Context("APIRule version v2alpha1 rule path validation", func() { - It("should fail when path consists of a path and *", func() { - // given - apiRuleName := generateTestName(testNameBase, testIDLength) - serviceName := generateTestName(testServiceNameBase, testIDLength) - serviceHost := gatewayv2alpha1.Host("example.com") - serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} + It("should not create an APIRule with host name longer than 255 characters", func() { + // given + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := testServiceNameBase + sixtyThreeA := strings.Repeat("a", 63) + host256 := fmt.Sprintf("%s.%s.%s.%s.com", sixtyThreeA, sixtyThreeA, sixtyThreeA, strings.Repeat("b", 60)) + serviceHost := gatewayv2alpha1.Host(host256) + serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} + + rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + rule.NoAuth = ptr.To(true) + apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + + // when + Expect(host256).To(HaveLen(256)) + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + err := c.Create(context.Background(), apiRule) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + }() + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("spec.hosts[0]: Too long: may not be longer than 255")) + }) - rule := testRulev2alpha1("/img*", []gatewayv2alpha1.HttpMethod{http.MethodGet}) - rule.NoAuth = ptr.To(true) - apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) - svc := testService(serviceName, testNamespace, testServicePort) + invalidHelper := func(host gatewayv2alpha1.Host) { + // given + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := testServiceNameBase + serviceHosts := []*gatewayv2alpha1.Host{ptr.To(gatewayv2alpha1.Host(host))} + + rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + rule.NoAuth = ptr.To(true) + apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + // when + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + err := c.Create(context.Background(), apiRule) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + }() + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("spec.hosts[0]: Invalid value: \"string\": Host must be a short name or a fully qualified domain name")) + } - Expect(c.Create(context.Background(), svc)).Should(Succeed()) + It("should not create an APIRule with an empty host", func() { + invalidHelper("") + }) - // when - err := c.Create(context.Background(), apiRule) + It("should not create an APIRule when host name has uppercase letters", func() { + invalidHelper("eXample.com") + invalidHelper("example.cOm") + }) - // then - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("spec.rules[0].path: Invalid value: \"/img*\": spec.rules[0].path")) - defer func() { - apiRulev2alpha1Teardown(apiRule) - serviceTeardown(svc) - }() - }) + It("should not create an APIRule with host label longer than 63 characters", func() { + invalidHelper(gatewayv2alpha1.Host(strings.Repeat("a", 64) + ".com")) + invalidHelper(gatewayv2alpha1.Host("example." + strings.Repeat("a", 64))) + }) - It("should apply APIRule when path contains only /*", func() { - // given - apiRuleName := generateTestName(testNameBase, testIDLength) - serviceName := generateTestName(testServiceNameBase, testIDLength) - serviceHost := gatewayv2alpha1.Host("example.com") - serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} + It("should not create an APIRule when any domain label is empty", func() { + invalidHelper(".com") + invalidHelper("example..com") + invalidHelper("example.") + }) - rule := testRulev2alpha1("/*", []gatewayv2alpha1.HttpMethod{http.MethodGet}) - rule.NoAuth = ptr.To(true) - apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) - svc := testService(serviceName, testNamespace, testServicePort) + It("should not create an APIRule when top level domain is too short", func() { + invalidHelper("example.c") + }) - Expect(c.Create(context.Background(), svc)).Should(Succeed()) + It("should not create an APIRule when host contains wrong characters", func() { + invalidHelper("*example.com") + invalidHelper("exam*ple.com") + invalidHelper("example*.com") + invalidHelper("example.*com") + invalidHelper("example.co*m") + invalidHelper("example.com*") + }) - // when then - Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) - defer func() { - apiRulev2alpha1Teardown(apiRule) - serviceTeardown(svc) - }() + It("should not create an APIRule when host starts or ends with a hyphen", func() { + invalidHelper("-example.com") + invalidHelper("example-.com") + invalidHelper("example.-com") + invalidHelper("example.com-") + }) }) - It("should apply APIRule when path contains no *", func() { - // given - apiRuleName := generateTestName(testNameBase, testIDLength) - serviceName := generateTestName(testServiceNameBase, testIDLength) - serviceHost := gatewayv2alpha1.Host("example.com") - serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} - - rule := testRulev2alpha1("/img-new/1", []gatewayv2alpha1.HttpMethod{http.MethodGet}) - rule.NoAuth = ptr.To(true) - apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) - svc := testService(serviceName, testNamespace, testServicePort) + Context("rule path validation respected", func() { + It("should fail when path consists of a path and *", func() { + // given + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := generateTestName(testServiceNameBase, testIDLength) + serviceHost := gatewayv2alpha1.Host("example.com") + serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} + + rule := testRulev2alpha1("/img*", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + rule.NoAuth = ptr.To(true) + apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + + // when + err := c.Create(context.Background(), apiRule) + + // then + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("spec.rules[0].path: Invalid value: \"/img*\": spec.rules[0].path")) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + }() + }) - Expect(c.Create(context.Background(), svc)).Should(Succeed()) + It("should apply APIRule when path contains only /*", func() { + // given + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := generateTestName(testServiceNameBase, testIDLength) + serviceHost := gatewayv2alpha1.Host("example.com") + serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} + + rule := testRulev2alpha1("/*", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + rule.NoAuth = ptr.To(true) + apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + + // when then + Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + }() + }) - // when then - Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) - defer func() { - apiRulev2alpha1Teardown(apiRule) - serviceTeardown(svc) - }() + It("should apply APIRule when path contains no *", func() { + // given + apiRuleName := generateTestName(testNameBase, testIDLength) + serviceName := generateTestName(testServiceNameBase, testIDLength) + serviceHost := gatewayv2alpha1.Host("example.com") + serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} + + rule := testRulev2alpha1("/img-new/1", []gatewayv2alpha1.HttpMethod{http.MethodGet}) + rule.NoAuth = ptr.To(true) + apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) + svc := testService(serviceName, testNamespace, testServicePort) + + Expect(c.Create(context.Background(), svc)).Should(Succeed()) + + // when then + Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) + defer func() { + apiRulev2alpha1Teardown(apiRule) + serviceTeardown(svc) + }() + }) }) - }) It("APIRule in status Error should reconcile to status OK when root cause of error is fixed", func() { @@ -2055,7 +2104,6 @@ func testService(name, namespace string, servicePort uint32) *corev1.Service { } func testOryJWTHandler(issuer string, scopes []string) *gatewayv1beta1.Handler { - configJSON := fmt.Sprintf(`{ "trusted_issuers": ["%s"], "jwks": [], @@ -2071,7 +2119,6 @@ func testOryJWTHandler(issuer string, scopes []string) *gatewayv1beta1.Handler { } func testIstioJWTHandler(issuer string, jwksUri string) *gatewayv1beta1.Handler { - bytes, err := json.Marshal(gatewayv1beta1.JwtConfig{ Authentications: []*gatewayv1beta1.JwtAuthentication{ { @@ -2090,7 +2137,6 @@ func testIstioJWTHandler(issuer string, jwksUri string) *gatewayv1beta1.Handler } func testIstioJWTHandlerWithScopes(issuer string, jwksUri string, authorizationScopes []string) *gatewayv1beta1.Handler { - bytes, err := json.Marshal(gatewayv1beta1.JwtConfig{ Authentications: []*gatewayv1beta1.JwtAuthentication{ { @@ -2114,7 +2160,6 @@ func testIstioJWTHandlerWithScopes(issuer string, jwksUri string, authorizationS } func testIstioJWTHandlerWithAuthorizations(issuer string, jwksUri string, authorizations []*gatewayv1beta1.JwtAuthorization) *gatewayv1beta1.Handler { - bytes, err := json.Marshal(gatewayv1beta1.JwtConfig{ Authentications: []*gatewayv1beta1.JwtAuthentication{ { @@ -2134,7 +2179,6 @@ func testIstioJWTHandlerWithAuthorizations(issuer string, jwksUri string, author } func testOauthHandler(scopes []string) *gatewayv1beta1.Handler { - configJSON := fmt.Sprintf(`{ "required_scope": [%s] }`, toCSVList(scopes)) @@ -2155,7 +2199,6 @@ func noConfigHandler(name string) *gatewayv1beta1.Handler { // Converts a []interface{} to a string slice. Panics if given object is of other type. func asStringSlice(in interface{}) []string { - inSlice := in.([]interface{}) if inSlice == nil { @@ -2172,7 +2215,6 @@ func asStringSlice(in interface{}) []string { } func generateTestName(name string, length int) string { - rand.NewSource(time.Now().UnixNano()) letterRunes := []rune("abcdefghijklmnopqrstuvwxyz") @@ -2191,7 +2233,6 @@ func getRuleList(g Gomega, matchingLabels client.ListOption) []rulev1alpha1.Rule } func verifyRuleList(g Gomega, ruleList []rulev1alpha1.Rule, pathToURLFunc func(string) string, expected ...gatewayv1beta1.Rule) { - g.Expect(ruleList).To(HaveLen(len(expected))) actual := make(map[string]rulev1alpha1.Rule) @@ -2209,6 +2250,7 @@ func verifyRuleList(g Gomega, ruleList []rulev1alpha1.Rule, pathToURLFunc func(s verifyMutators(g, actual[ruleUrl].Spec.Mutators, expected[i].Mutators) } } + func verifyMutators(g Gomega, actual []*rulev1alpha1.Mutator, expected []*gatewayv1beta1.Mutator) { if expected == nil { g.Expect(actual).To(BeNil()) @@ -2218,6 +2260,7 @@ func verifyMutators(g Gomega, actual []*rulev1alpha1.Mutator, expected []*gatewa } } } + func verifyAccessStrategies(g Gomega, actual []*rulev1alpha1.Authenticator, expected []*gatewayv1beta1.Authenticator) { if expected == nil { g.Expect(actual).To(BeNil()) @@ -2344,6 +2387,21 @@ func serviceTeardown(svc *corev1.Service) { }, eventuallyTimeout).Should(Succeed()) } +func kymaGatewayTeardown(gateway *networkingv1beta1.Gateway) { + By(fmt.Sprintf("Deleting Kyma Gateway as part of teardown", gateway.Name)) + err := c.Delete(context.Background(), gateway) + + if err != nil { + Expect(errors.IsNotFound(err)).To(BeTrue()) + } + + Eventually(func(g Gomega) { + a := gatewayv1beta1.APIRule{} + err := c.Get(context.Background(), client.ObjectKey{Name: gateway.Name, Namespace: gateway.Namespace}, &a) + g.Expect(errors.IsNotFound(err)).To(BeTrue()) + }, eventuallyTimeout).Should(Succeed()) +} + func expectApiRuleStatus(apiRuleName string, statusCode gatewayv1beta1.StatusCode) { By(fmt.Sprintf("Verifying that ApiRule %s has status %s", apiRuleName, statusCode)) Eventually(func(g Gomega) { diff --git a/controllers/gateway/apirule_controller.go b/controllers/gateway/apirule_controller.go index fa3e1dd42..c4be79de0 100644 --- a/controllers/gateway/apirule_controller.go +++ b/controllers/gateway/apirule_controller.go @@ -19,6 +19,7 @@ package gateway import ( "context" "fmt" + "strings" "time" "github.com/kyma-project/api-gateway/internal/dependencies" @@ -45,6 +46,7 @@ import ( "github.com/go-logr/logr" "github.com/kyma-project/api-gateway/internal/processing" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" corev1 "k8s.io/api/core/v1" apierrs "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" @@ -180,7 +182,19 @@ func (r *APIRuleReconciler) reconcileV2Alpha1APIRule(ctx context.Context, l logr if err := rule.ConvertFrom(toUpdate); err != nil { return doneReconcileErrorRequeue(err, r.OnErrorReconcilePeriod) } - cmd := r.getV2Alpha1Reconciliation(&apiRule, &rule, defaultDomainName, migrate, &l) + + gatewayName := strings.Split(*rule.Spec.Gateway, "/") + gatewayNN := types.NamespacedName{ + Namespace: gatewayName[0], + Name: gatewayName[1], + } + var gateway networkingv1beta1.Gateway + if err := r.Client.Get(ctx, gatewayNN, &gateway); err != nil { + l.Error(err, "Error while getting Gateway for APIRule", "not found", apierrs.IsNotFound(err)) + return doneReconcileErrorRequeue(err, r.OnErrorReconcilePeriod) + } + + cmd := r.getV2Alpha1Reconciliation(&apiRule, &rule, &gateway, defaultDomainName, migrate, &l) if name, err := dependencies.APIRule().AreAvailable(ctx, r.Client); err != nil { s, err := handleDependenciesError(name, err).V2alpha1Status() @@ -267,11 +281,11 @@ func (r *APIRuleReconciler) getV1Beta1Reconciliation(apiRule *gatewayv1beta1.API } } -func (r *APIRuleReconciler) getV2Alpha1Reconciliation(apiRulev1beta1 *gatewayv1beta1.APIRule, apiRulev2alpha1 *gatewayv2alpha1.APIRule, defaultDomainName string, needsMigration bool, namespacedLogger *logr.Logger) processing.ReconciliationCommand { +func (r *APIRuleReconciler) getV2Alpha1Reconciliation(apiRulev1beta1 *gatewayv1beta1.APIRule, apiRulev2alpha1 *gatewayv2alpha1.APIRule, gateway *networkingv1beta1.Gateway, defaultDomainName string, needsMigration bool, namespacedLogger *logr.Logger) processing.ReconciliationCommand { config := r.ReconciliationConfig config.DefaultDomainName = defaultDomainName v2alpha1Validator := v2alpha1.NewAPIRuleValidator(apiRulev2alpha1) - return v2alpha1Processing.NewReconciliation(apiRulev2alpha1, apiRulev1beta1, v2alpha1Validator, config, namespacedLogger, needsMigration) + return v2alpha1Processing.NewReconciliation(apiRulev2alpha1, apiRulev1beta1, gateway, v2alpha1Validator, config, namespacedLogger, needsMigration) } // SetupWithManager sets up the controller with the Manager. diff --git a/internal/processing/processors/migration/processors.go b/internal/processing/processors/migration/processors.go index c13b86b36..a5babbacb 100644 --- a/internal/processing/processors/migration/processors.go +++ b/internal/processing/processors/migration/processors.go @@ -22,7 +22,7 @@ func NewMigrationProcessors(apiRuleV2alpha1 *gatewayv2alpha1.APIRule, apiRuleV1b processors = append(processors, NewAccessRuleDeletionProcessor(config, apiRuleV1beta1)) fallthrough // We want to also use the processors from the previous steps case switchVsToService: // Step 2 - processors = append(processors, virtualservice.NewVirtualServiceProcessor(config, apiRuleV2alpha1)) + processors = append(processors, virtualservice.NewVirtualServiceProcessor(config, apiRuleV2alpha1, nil)) fallthrough // We want to also use the processors from the previous steps case applyIstioAuthorizationMigrationStep: // Step 1 processors = append(processors, authorizationpolicy.NewMigrationProcessor(log, apiRuleV2alpha1, step != removeOryRule)) diff --git a/internal/processing/processors/v2alpha1/reconciliation.go b/internal/processing/processors/v2alpha1/reconciliation.go index fbcb62f95..c160575dc 100644 --- a/internal/processing/processors/v2alpha1/reconciliation.go +++ b/internal/processing/processors/v2alpha1/reconciliation.go @@ -3,6 +3,7 @@ package v2alpha1 import ( "context" "errors" + "github.com/kyma-project/api-gateway/internal/processing/processors/v2alpha1/authorizationpolicy" "github.com/kyma-project/api-gateway/internal/processing/processors/v2alpha1/requestauthentication" v2alpha1VirtualService "github.com/kyma-project/api-gateway/internal/processing/processors/v2alpha1/virtualservice" @@ -49,13 +50,13 @@ func (r Reconciliation) GetProcessors() []processing.ReconciliationProcessor { return r.processors } -func NewReconciliation(apiRuleV2alpha1 *gatewayv2alpha1.APIRule, apiRuleV1beta1 *gatewayv1beta1.APIRule, validator validation.ApiRuleValidator, config processing.ReconciliationConfig, log *logr.Logger, needsMigration bool) Reconciliation { +func NewReconciliation(apiRuleV2alpha1 *gatewayv2alpha1.APIRule, apiRuleV1beta1 *gatewayv1beta1.APIRule, gateway *networkingv1beta1.Gateway, validator validation.ApiRuleValidator, config processing.ReconciliationConfig, log *logr.Logger, needsMigration bool) Reconciliation { var processors []processing.ReconciliationProcessor if needsMigration { log.Info("APIRule needs migration") processors = append(processors, migration.NewMigrationProcessors(apiRuleV2alpha1, apiRuleV1beta1, config, log)...) } else { - processors = append(processors, v2alpha1VirtualService.NewVirtualServiceProcessor(config, apiRuleV2alpha1)) + processors = append(processors, v2alpha1VirtualService.NewVirtualServiceProcessor(config, apiRuleV2alpha1, gateway)) processors = append(processors, authorizationpolicy.NewProcessor(log, apiRuleV2alpha1)) processors = append(processors, requestauthentication.NewProcessor(apiRuleV2alpha1)) } diff --git a/internal/processing/processors/v2alpha1/reconciliation_test.go b/internal/processing/processors/v2alpha1/reconciliation_test.go index e941b0c85..50698ff9f 100644 --- a/internal/processing/processors/v2alpha1/reconciliation_test.go +++ b/internal/processing/processors/v2alpha1/reconciliation_test.go @@ -3,9 +3,10 @@ package v2alpha1_test import ( "context" "fmt" + "net/http" + oryv1alpha1 "github.com/ory/oathkeeper-maester/api/v1alpha1" v1beta12 "istio.io/api/security/v1beta1" - "net/http" gatewayv1beta1 "github.com/kyma-project/api-gateway/apis/gateway/v1beta1" gatewayv2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" @@ -47,7 +48,7 @@ var _ = Describe("Reconciliation", func() { // when var createdObjects []client.Object - reconciliation := v2alpha1.NewReconciliation(v2alpha1ApiRule, v1beta1ApiRule, nil, GetTestConfig(), &testLogger, false) + reconciliation := v2alpha1.NewReconciliation(v2alpha1ApiRule, v1beta1ApiRule, nil, nil, GetTestConfig(), &testLogger, false) for _, processor := range reconciliation.GetProcessors() { results, err := processor.EvaluateReconciliation(context.Background(), fakeClient) Expect(err).To(BeNil()) @@ -77,7 +78,7 @@ var _ = Describe("Reconciliation", func() { // when var createdObjects []client.Object - reconciliation := v2alpha1.NewReconciliation(v2alpha1ApiRule, v1beta1ApiRule, nil, GetTestConfig(), &testLogger, false) + reconciliation := v2alpha1.NewReconciliation(v2alpha1ApiRule, v1beta1ApiRule, nil, nil, GetTestConfig(), &testLogger, false) for _, processor := range reconciliation.GetProcessors() { results, err := processor.EvaluateReconciliation(context.Background(), fakeClient) Expect(err).To(BeNil()) @@ -169,7 +170,7 @@ var _ = Describe("Reconciliation", func() { // when var createdObjects []client.Object - reconciliation := v2alpha1.NewReconciliation(v2alpha1ApiRule, v1beta1ApiRule, nil, GetTestConfig(), &testLogger, false) + reconciliation := v2alpha1.NewReconciliation(v2alpha1ApiRule, v1beta1ApiRule, nil, nil, GetTestConfig(), &testLogger, false) for _, processor := range reconciliation.GetProcessors() { results, err := processor.EvaluateReconciliation(context.Background(), fakeClient) Expect(err).To(BeNil()) @@ -237,7 +238,7 @@ var _ = Describe("Reconciliation", func() { // when var createdObjects []client.Object - reconciliation := v2alpha1.NewReconciliation(v2alpha1ApiRule, v1beta1ApiRule, nil, GetTestConfig(), &testLogger, false) + reconciliation := v2alpha1.NewReconciliation(v2alpha1ApiRule, v1beta1ApiRule, nil, nil, GetTestConfig(), &testLogger, false) for _, processor := range reconciliation.GetProcessors() { results, err := processor.EvaluateReconciliation(context.Background(), fakeClient) Expect(err).To(BeNil()) @@ -313,7 +314,7 @@ var _ = Describe("Reconciliation", func() { // when var createdObjects []client.Object - reconciliation := v2alpha1.NewReconciliation(v2alpha1ApiRule, v1beta1ApiRule, nil, GetTestConfig(), &testLogger, false) + reconciliation := v2alpha1.NewReconciliation(v2alpha1ApiRule, v1beta1ApiRule, nil, nil, GetTestConfig(), &testLogger, false) for _, processor := range reconciliation.GetProcessors() { results, err := processor.EvaluateReconciliation(context.Background(), fakeClient) Expect(err).To(BeNil()) @@ -397,7 +398,7 @@ var _ = Describe("Reconciliation", func() { // when var createdObjects []client.Object - reconciliation := v2alpha1.NewReconciliation(v2alpha1ApiRule, v1beta1ApiRule, nil, GetTestConfig(), &testLogger, true) + reconciliation := v2alpha1.NewReconciliation(v2alpha1ApiRule, v1beta1ApiRule, nil, nil, GetTestConfig(), &testLogger, true) for _, processor := range reconciliation.GetProcessors() { results, err := processor.EvaluateReconciliation(context.Background(), fakeClient) Expect(err).To(BeNil()) @@ -465,7 +466,7 @@ var _ = Describe("Reconciliation", func() { // when apiRuleValidatorMock := APIRuleValidatorMock{} - reconciliation := v2alpha1.NewReconciliation(v2alpha1ApiRule, v1beta1ApiRule, &apiRuleValidatorMock, GetTestConfig(), &testLogger, false) + reconciliation := v2alpha1.NewReconciliation(v2alpha1ApiRule, v1beta1ApiRule, nil, &apiRuleValidatorMock, GetTestConfig(), &testLogger, false) failures, err := reconciliation.Validate(context.Background(), fakeClient) @@ -487,7 +488,7 @@ var _ = Describe("Reconciliation", func() { fakeClient := GetFakeClient(service) // when - reconciliation := v2alpha1.NewReconciliation(v2alpha1ApiRule, v1beta1ApiRule, nil, GetTestConfig(), &testLogger, false) + reconciliation := v2alpha1.NewReconciliation(v2alpha1ApiRule, v1beta1ApiRule, nil, nil, GetTestConfig(), &testLogger, false) failures, err := reconciliation.Validate(context.Background(), fakeClient) // then diff --git a/internal/processing/processors/v2alpha1/status_test.go b/internal/processing/processors/v2alpha1/status_test.go index ed5b9e189..739b7d3ce 100644 --- a/internal/processing/processors/v2alpha1/status_test.go +++ b/internal/processing/processors/v2alpha1/status_test.go @@ -11,7 +11,7 @@ import ( var _ = Describe("StatusBase", func() { It("should create status from given status code", func() { - r := v2alpha1.NewReconciliation(nil, nil, nil, processing.ReconciliationConfig{}, nil, false) + r := v2alpha1.NewReconciliation(nil, nil, nil, nil, processing.ReconciliationConfig{}, nil, false) // when s, ok := r.GetStatusBase(string(gatewayv2alpha1.Error)).(status.ReconciliationV2alpha1Status) diff --git a/internal/processing/processors/v2alpha1/virtualservice/cors_test.go b/internal/processing/processors/v2alpha1/virtualservice/cors_test.go index 44a09231b..ddd8d1a56 100644 --- a/internal/processing/processors/v2alpha1/virtualservice/cors_test.go +++ b/internal/processing/processors/v2alpha1/virtualservice/cors_test.go @@ -22,9 +22,9 @@ var _ = Describe("CORS", func() { }) DescribeTable("CORS", - func(apiRule *gatewayv2alpha1.APIRule, verifiers []verifier, expectedActions ...string) { - processor = processors.NewVirtualServiceProcessor(GetTestConfig(), apiRule) - checkVirtualServices(client, processor, verifiers, expectedActions...) + func(apiRule *gatewayv2alpha1.APIRule, verifiers []verifier, expectedError error, expectedActions ...string) { + processor = processors.NewVirtualServiceProcessor(GetTestConfig(), apiRule, nil) + checkVirtualServices(client, processor, verifiers, expectedError, expectedActions...) }, Entry("should set default empty values in VirtualService CORSPolicy when no CORS configuration is set in APIRule", @@ -42,7 +42,7 @@ var _ = Describe("CORS", func() { builders.AllowOriginName, })) }, - }, "create"), + }, nil, "create"), Entry("should apply all CORSPolicy headers correctly", NewAPIRuleBuilderWithDummyDataWithNoAuthRule().WithCORSPolicy( @@ -73,6 +73,6 @@ var _ = Describe("CORS", func() { builders.AllowMethodsName, builders.AllowOriginName, })) - }}, "create"), + }}, nil, "create"), ) }) diff --git a/internal/processing/processors/v2alpha1/virtualservice/hosts_test.go b/internal/processing/processors/v2alpha1/virtualservice/hosts_test.go index 10434881c..2e061e287 100644 --- a/internal/processing/processors/v2alpha1/virtualservice/hosts_test.go +++ b/internal/processing/processors/v2alpha1/virtualservice/hosts_test.go @@ -1,9 +1,13 @@ package virtualservice_test import ( + "errors" + gatewayv2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" processors "github.com/kyma-project/api-gateway/internal/processing/processors/v2alpha1/virtualservice" + apinetworkingv1beta1 "istio.io/api/networking/v1beta1" networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" . "github.com/kyma-project/api-gateway/internal/builders/builders_test/v2alpha1_test" @@ -20,25 +24,76 @@ var _ = Describe("Hosts", func() { }) DescribeTable("Hosts", - func(apiRule *gatewayv2alpha1.APIRule, verifiers []verifier, expectedActions ...string) { - processor = processors.NewVirtualServiceProcessor(GetTestConfig(), apiRule) - checkVirtualServices(client, processor, verifiers, expectedActions...) + func(apiRule *gatewayv2alpha1.APIRule, gateway *networkingv1beta1.Gateway, verifiers []verifier, expectedError error, expectedActions ...string) { + processor = processors.NewVirtualServiceProcessor(GetTestConfig(), apiRule, gateway) + checkVirtualServices(client, processor, verifiers, expectedError, expectedActions...) }, Entry("should set the host correctly", - NewAPIRuleBuilder().WithGateway("example/example").WithHost("example.com").Build(), + NewAPIRuleBuilder().WithGateway("gateway-ns/gateway-name").WithHost("example.com").Build(), + nil, []verifier{ func(vs *networkingv1beta1.VirtualService) { Expect(vs.Spec.Hosts).To(ConsistOf("example.com")) }, - }, "create"), + }, nil, "create"), Entry("should set multiple hosts correctly", - NewAPIRuleBuilder().WithGateway("example/example").WithHosts("example.com", "goat.com").Build(), + NewAPIRuleBuilder().WithGateway("gateway-ns/gateway-name").WithHosts("example.com", "goat.com").Build(), + nil, []verifier{ func(vs *networkingv1beta1.VirtualService) { Expect(vs.Spec.Hosts).To(ConsistOf("example.com", "goat.com")) }, - }, "create"), + }, nil, "create"), + + Entry("should set the host correctly when short host is used", + NewAPIRuleBuilder().WithGateway("gateway-ns/gateway-name").WithHost("example").Build(), + &networkingv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{Name: "gateway-name", Namespace: "gateway-ns"}, + Spec: apinetworkingv1beta1.Gateway{ + Servers: []*apinetworkingv1beta1.Server{ + { + Hosts: []string{ + "*.domain.name", + }, + }, + }, + }, + }, + []verifier{ + func(vs *networkingv1beta1.VirtualService) { + Expect(vs.Spec.Hosts).To(ConsistOf("example.domain.name")) + }, + }, nil, "create"), + + Entry("should return error when short host is used but no gateway available", + NewAPIRuleBuilder().WithGateway("gateway-ns/gateway-name").WithHost("example").Build(), + nil, + []verifier{}, errors.New("gateway or host definition is missing")), + + Entry("should return error when short host is used but gateway do not have servers defined", + NewAPIRuleBuilder().WithGateway("gateway-ns/gateway-name").WithHost("example").Build(), + &networkingv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{Name: "gateway-name", Namespace: "gateway-ns"}, + Spec: apinetworkingv1beta1.Gateway{ + Servers: []*apinetworkingv1beta1.Server{}, + }, + }, + []verifier{}, errors.New("gateway or host definition is missing")), + + Entry("should return error when short host is used but gateway do not have hosts defined", + NewAPIRuleBuilder().WithGateway("gateway-ns/gateway-name").WithHost("example").Build(), + &networkingv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{Name: "gateway-name", Namespace: "gateway-ns"}, + Spec: apinetworkingv1beta1.Gateway{ + Servers: []*apinetworkingv1beta1.Server{ + { + Hosts: []string{}, + }, + }, + }, + }, + []verifier{}, errors.New("gateway or host definition is missing")), ) }) diff --git a/internal/processing/processors/v2alpha1/virtualservice/http_matching_test.go b/internal/processing/processors/v2alpha1/virtualservice/http_matching_test.go index 0cc0afad2..0391d3391 100644 --- a/internal/processing/processors/v2alpha1/virtualservice/http_matching_test.go +++ b/internal/processing/processors/v2alpha1/virtualservice/http_matching_test.go @@ -1,10 +1,11 @@ package virtualservice_test import ( + "net/http" + gatewayv2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" processors "github.com/kyma-project/api-gateway/internal/processing/processors/v2alpha1/virtualservice" networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" - "net/http" "sigs.k8s.io/controller-runtime/pkg/client" . "github.com/kyma-project/api-gateway/internal/builders/builders_test/v2alpha1_test" @@ -20,9 +21,9 @@ var _ = Describe("HTTP matching", func() { client = GetFakeClient() }) var _ = DescribeTable("Different methods on same path", - func(apiRule *gatewayv2alpha1.APIRule, verifiers []verifier, expectedActions ...string) { - processor = processors.NewVirtualServiceProcessor(GetTestConfig(), apiRule) - checkVirtualServices(client, processor, verifiers, expectedActions...) + func(apiRule *gatewayv2alpha1.APIRule, verifiers []verifier, expectedError error, expectedActions ...string) { + processor = processors.NewVirtualServiceProcessor(GetTestConfig(), apiRule, nil) + checkVirtualServices(client, processor, verifiers, expectedError, expectedActions...) }, Entry("from two rules with different methods on the same path should create two HTTP routes with different methods", NewAPIRuleBuilderWithDummyData(). @@ -35,7 +36,7 @@ var _ = Describe("HTTP matching", func() { Expect(vs.Spec.Http[0].Match[0].Method.GetRegex()).To(Equal("^(GET)$")) Expect(vs.Spec.Http[1].Match[0].Method.GetRegex()).To(Equal("^(PUT)$")) }, - }, "create"), + }, nil, "create"), Entry("from one rule with two methods on the same path should create one HTTP route with regex matching both methods", NewAPIRuleBuilderWithDummyData(). @@ -47,6 +48,6 @@ var _ = Describe("HTTP matching", func() { Expect(vs.Spec.Http[0].Match[0].Method.GetRegex()).To(Equal("^(GET|PUT)$")) }, - }, "create"), + }, nil, "create"), ) }) diff --git a/internal/processing/processors/v2alpha1/virtualservice/request_test.go b/internal/processing/processors/v2alpha1/virtualservice/request_test.go index 91c501937..67014edb5 100644 --- a/internal/processing/processors/v2alpha1/virtualservice/request_test.go +++ b/internal/processing/processors/v2alpha1/virtualservice/request_test.go @@ -1,10 +1,11 @@ package virtualservice_test import ( + "net/http" + gatewayv2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" processors "github.com/kyma-project/api-gateway/internal/processing/processors/v2alpha1/virtualservice" networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" - "net/http" "sigs.k8s.io/controller-runtime/pkg/client" . "github.com/kyma-project/api-gateway/internal/builders/builders_test/v2alpha1_test" @@ -21,9 +22,9 @@ var _ = Describe("Mutators", func() { }) DescribeTable("Mutators", - func(apiRule *gatewayv2alpha1.APIRule, verifiers []verifier, expectedActions ...string) { - processor = processors.NewVirtualServiceProcessor(GetTestConfig(), apiRule) - checkVirtualServices(client, processor, verifiers, expectedActions...) + func(apiRule *gatewayv2alpha1.APIRule, verifiers []verifier, expectedError error, expectedActions ...string) { + processor = processors.NewVirtualServiceProcessor(GetTestConfig(), apiRule, nil) + checkVirtualServices(client, processor, verifiers, expectedError, expectedActions...) }, Entry("should set only x-forwarded-host header when rule does not use any mutators", @@ -35,7 +36,7 @@ var _ = Describe("Mutators", func() { Expect(vs.Spec.Http[0].Headers.Request.Set).To(HaveLen(1)) Expect(vs.Spec.Http[0].Headers.Request.Set["x-forwarded-host"]).To(Equal("example-host.example.com")) }, - }, "create"), + }, nil, "create"), Entry("should set Headers on request when rule uses HeadersMutator", NewAPIRuleBuilderWithDummyData(). @@ -50,7 +51,7 @@ var _ = Describe("Mutators", func() { Expect(vs.Spec.Http[0].Headers.Request.Set["header1"]).To(Equal("value1")) Expect(vs.Spec.Http[0].Headers.Request.Set["x-forwarded-host"]).To(Equal("example-host.example.com")) }, - }, "create"), + }, nil, "create"), Entry("should set Cookie header on request when rule uses CookieMutator", NewAPIRuleBuilderWithDummyData(). @@ -65,7 +66,7 @@ var _ = Describe("Mutators", func() { Expect(vs.Spec.Http[0].Headers.Request.Set["Cookie"]).To(Equal("header1=value1")) Expect(vs.Spec.Http[0].Headers.Request.Set["x-forwarded-host"]).To(Equal("example-host.example.com")) }, - }, "create"), + }, nil, "create"), Entry("should set Cookie header and custom header on request when rule uses CookieMutator and HeadersMutator", NewAPIRuleBuilderWithDummyData(). @@ -84,6 +85,6 @@ var _ = Describe("Mutators", func() { Expect(vs.Spec.Http[0].Headers.Request.Set["header2"]).To(Equal("value2")) Expect(vs.Spec.Http[0].Headers.Request.Set["x-forwarded-host"]).To(Equal("example-host.example.com")) }, - }, "create"), + }, nil, "create"), ) }) diff --git a/internal/processing/processors/v2alpha1/virtualservice/virtual_service_processor.go b/internal/processing/processors/v2alpha1/virtualservice/virtual_service_processor.go index fa83f23cd..59c1bfb1b 100644 --- a/internal/processing/processors/v2alpha1/virtualservice/virtual_service_processor.go +++ b/internal/processing/processors/v2alpha1/virtualservice/virtual_service_processor.go @@ -2,7 +2,9 @@ package virtualservice import ( "context" + "errors" "fmt" + "strings" "time" gatewayv2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" @@ -16,11 +18,12 @@ import ( const defaultHttpTimeout uint32 = 180 -func NewVirtualServiceProcessor(config processing.ReconciliationConfig, apiRule *gatewayv2alpha1.APIRule) VirtualServiceProcessor { +func NewVirtualServiceProcessor(config processing.ReconciliationConfig, apiRule *gatewayv2alpha1.APIRule, gateway *networkingv1beta1.Gateway) VirtualServiceProcessor { return VirtualServiceProcessor{ ApiRule: apiRule, Creator: virtualServiceCreator{ defaultDomainName: config.DefaultDomainName, + gateway: gateway, }, } } @@ -83,6 +86,7 @@ func (r VirtualServiceProcessor) getObjectChanges(desired *networkingv1beta1.Vir type virtualServiceCreator struct { defaultDomainName string + gateway *networkingv1beta1.Gateway } // Create returns the Virtual Service using the configuration of the APIRule. @@ -92,7 +96,11 @@ func (r virtualServiceCreator) Create(api *gatewayv2alpha1.APIRule) (*networking vsSpecBuilder := builders.VirtualServiceSpec() for _, host := range api.Spec.Hosts { if helpers.IsHostShortName(string(*host)) { - + if r.gateway == nil || len(r.gateway.Spec.Servers) < 1 || len(r.gateway.Spec.Servers[0].Hosts) < 1 { + return nil, errors.New("gateway or host definition is missing") + } + gatewayDomain := strings.TrimPrefix(r.gateway.Spec.Servers[0].Hosts[0], "*.") + vsSpecBuilder.AddHost(default_domain.GetHostWithDomain(string(*host), gatewayDomain)) } else { vsSpecBuilder.AddHost(default_domain.GetHostWithDomain(string(*host), r.defaultDomainName)) } diff --git a/internal/processing/processors/v2alpha1/virtualservice/virtual_service_processor_test.go b/internal/processing/processors/v2alpha1/virtualservice/virtual_service_processor_test.go index f34b539e1..1a46211a8 100644 --- a/internal/processing/processors/v2alpha1/virtualservice/virtual_service_processor_test.go +++ b/internal/processing/processors/v2alpha1/virtualservice/virtual_service_processor_test.go @@ -2,6 +2,7 @@ package virtualservice_test import ( "context" + gatewayv2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" "github.com/kyma-project/api-gateway/internal/builders" . "github.com/kyma-project/api-gateway/internal/builders/builders_test/v2alpha1_test" @@ -34,7 +35,7 @@ var _ = Describe("ObjectChange", func() { It("should return update action when there is a matching VirtualService on cluster", func() { // given apiRuleBuilder := NewAPIRuleBuilderWithDummyData() - processor := processors.NewVirtualServiceProcessor(GetTestConfig(), apiRuleBuilder.Build()) + processor := processors.NewVirtualServiceProcessor(GetTestConfig(), apiRuleBuilder.Build(), nil) result, err := processor.EvaluateReconciliation(context.Background(), GetFakeClient()) Expect(err).To(BeNil()) @@ -42,7 +43,7 @@ var _ = Describe("ObjectChange", func() { Expect(result[0].Action.String()).To(Equal("create")) // when - processor = processors.NewVirtualServiceProcessor(GetTestConfig(), apiRuleBuilder.WithHosts("newHost.com").Build()) + processor = processors.NewVirtualServiceProcessor(GetTestConfig(), apiRuleBuilder.WithHosts("newHost.com").Build(), nil) result, err = processor.EvaluateReconciliation(context.Background(), GetFakeClient(result[0].Obj.(*networkingv1beta1.VirtualService))) // then @@ -87,7 +88,7 @@ var _ = Describe("Fully configured APIRule happy path", func() { Build() client := GetFakeClient() - processor := processors.NewVirtualServiceProcessor(GetTestConfig(), apiRule) + processor := processors.NewVirtualServiceProcessor(GetTestConfig(), apiRule, nil) checkVirtualServices(client, processor, []verifier{ func(vs *networkingv1beta1.VirtualService) { Expect(vs.Spec.Hosts).To(ConsistOf("example.com", "goat.com")) @@ -132,7 +133,7 @@ var _ = Describe("Fully configured APIRule happy path", func() { Expect(vs.Spec.Http[1].Timeout.Seconds).To(Equal(int64(180))) }, - }, "create") + }, nil, "create") }) }) @@ -169,7 +170,7 @@ var _ = Describe("VirtualServiceProcessor", func() { Build() client := GetFakeClient() - processor := processors.NewVirtualServiceProcessor(GetTestConfig(), apiRule) + processor := processors.NewVirtualServiceProcessor(GetTestConfig(), apiRule, nil) checkVirtualServices(client, processor, []verifier{ func(vs *networkingv1beta1.VirtualService) { @@ -180,14 +181,21 @@ var _ = Describe("VirtualServiceProcessor", func() { Expect(vs.Spec.Http[0].Match[0].Method.GetRegex()).To(Equal("^(GET)$")) Expect(vs.Spec.Http[0].Match[0].Uri.GetPrefix()).To(Equal("/")) }, - }, "create") + }, nil, "create") }) }) -func checkVirtualServices(c client.Client, processor processors.VirtualServiceProcessor, verifiers []verifier, expectedActions ...string) { +func checkVirtualServices(c client.Client, processor processors.VirtualServiceProcessor, verifiers []verifier, expectedError error, expectedActions ...string) { result, err := processor.EvaluateReconciliation(context.Background(), c) - Expect(err).To(BeNil()) + if expectedError != nil { + Expect(result).To(HaveLen(len(expectedActions))) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal(expectedError.Error())) + return + } + + Expect(err).ToNot(HaveOccurred()) Expect(result).To(HaveLen(len(expectedActions))) for i, action := range expectedActions { Expect(result[i].Action.String()).To(Equal(action)) diff --git a/internal/validation/v2alpha1/hosts.go b/internal/validation/v2alpha1/hosts.go index 41644c3fb..783a22f8f 100644 --- a/internal/validation/v2alpha1/hosts.go +++ b/internal/validation/v2alpha1/hosts.go @@ -34,7 +34,7 @@ func validateHosts(parentAttributePath string, vsList networkingv1beta1.VirtualS AttributePath: hostAttributePath, Message: fmt.Sprintf("Unable to find Gateway %s", *apiRule.Spec.Gateway), }) - } else if hasMultipleHostDefinitions(gateway) { + } else if !hasSingleHostDefinitionWithCorrectPrefix(gateway) { hostAttributePath := fmt.Sprintf("%s[%d]", hostsAttributePath, hostIndex) failures = append(failures, validation.Failure{ AttributePath: hostAttributePath, @@ -63,22 +63,22 @@ func validateHosts(parentAttributePath string, vsList networkingv1beta1.VirtualS return failures } -func hasMultipleHostDefinitions(gateway *networkingv1beta1.Gateway) bool { +func hasSingleHostDefinitionWithCorrectPrefix(gateway *networkingv1beta1.Gateway) bool { host := "" for _, server := range gateway.Spec.Servers { if len(server.Hosts) > 1 { - return true + return false } if !strings.HasPrefix(server.Hosts[0], "*.") { - return true + return false } if host == "" { host = server.Hosts[0] } else if host != server.Hosts[0] { - return true + return false } } - return false + return true } func findGateway(name string, gwList networkingv1beta1.GatewayList) *networkingv1beta1.Gateway { From 738da6be6c9d0fc52e40eddf08578a4822a2d806 Mon Sep 17 00:00:00 2001 From: Vladimir Videlov Date: Tue, 24 Sep 2024 10:42:01 +0200 Subject: [PATCH 06/27] integration test --- .../api_controller_integration_test.go | 72 +------------------ .../v2alpha1/features/short_host.feature | 7 ++ .../v2alpha1/manifests/short-host.yaml | 16 +++++ .../v2alpha1/scenario_short_host.go | 14 ++++ .../testsuites/v2alpha1/testsuite.go | 4 +- 5 files changed, 41 insertions(+), 72 deletions(-) create mode 100644 tests/integration/testsuites/v2alpha1/features/short_host.feature create mode 100644 tests/integration/testsuites/v2alpha1/manifests/short-host.yaml create mode 100644 tests/integration/testsuites/v2alpha1/scenario_short_host.go diff --git a/controllers/gateway/api_controller_integration_test.go b/controllers/gateway/api_controller_integration_test.go index c8a6784aa..fdfd678ea 100644 --- a/controllers/gateway/api_controller_integration_test.go +++ b/controllers/gateway/api_controller_integration_test.go @@ -1479,76 +1479,6 @@ var _ = Describe("APIRule Controller", Serial, func() { }() }) - It("should create an APIRule with a short name and apply host domain from gateway", func() { - // given - apiRuleName := generateTestName(testNameBase, testIDLength) - serviceName := testServiceNameBase - serviceHost := gatewayv2alpha1.Host("test--example") - serviceHosts := []*gatewayv2alpha1.Host{&serviceHost} - - rule := testRulev2alpha1("/img", []gatewayv2alpha1.HttpMethod{http.MethodGet}) - rule.NoAuth = ptr.To(true) - apiRule := testApiRulev2alpha1(apiRuleName, testNamespace, serviceName, testNamespace, serviceHosts, testServicePort, []gatewayv2alpha1.Rule{rule}) - svc := testService(serviceName, testNamespace, testServicePort) - - By("Creating Kyma gateway") - gateway := networkingv1beta1.Gateway{ - ObjectMeta: metav1.ObjectMeta{Name: "kyma-gateway", Namespace: "kyma-system"}, - Spec: apinetworkingv1beta1.Gateway{ - Servers: []*apinetworkingv1beta1.Server{ - { - Port: &apinetworkingv1beta1.Port{ - Protocol: "HTTPS", - }, - Hosts: []string{ - "*.local.kyma.dev", - }, - }, - }, - }, - } - - // when - Expect(c.Create(context.Background(), &gateway)).Should(Succeed()) - Expect(c.Create(context.Background(), svc)).Should(Succeed()) - Expect(c.Create(context.Background(), apiRule)).Should(Succeed()) - defer func() { - apiRulev2alpha1Teardown(apiRule) - serviceTeardown(svc) - kymaGatewayTeardown(&gateway) - }() - - expectApiRuleStatus(apiRuleName, gatewayv1beta1.StatusOK) - - matchingLabels := matchingLabelsFunc(apiRuleName, testNamespace) - - By("Verifying created virtual service") - vsList := networkingv1beta1.VirtualServiceList{} - Eventually(func(g Gomega) { - g.Expect(c.List(context.Background(), &vsList, matchingLabels)).Should(Succeed()) - g.Expect(vsList.Items).To(HaveLen(1)) - - vs := vsList.Items[0] - - //Meta - g.Expect(vs.Name).To(HavePrefix(apiRuleName + "-")) - g.Expect(len(vs.Name) > len(apiRuleName)).To(BeTrue()) - - expectedSpec := builders.VirtualServiceSpec(). - AddHost("test--example.local.kyma.dev"). - Gateway(testGatewayURL). - HTTP(builders.HTTPRoute(). - Match(builders.MatchRequest().Uri().Regex(testPath)). - Route(builders.RouteDestination().Host(testOathkeeperSvcURL).Port(testOathkeeperPort)). - Headers(builders.NewHttpRouteHeadersBuilder().SetHostHeader("test--example.local.kyma.dev").Get()). - CorsPolicy(defaultCorsPolicy). - Timeout(defaultHttpTimeout)) - - gotSpec := *expectedSpec.Get() - g.Expect(*vs.Spec.DeepCopy()).To(Equal(*gotSpec.DeepCopy())) - }, eventuallyTimeout).Should(Succeed()) - }) - It("should create an APIRule with short name that has length of 1 character", func() { // given apiRuleName := generateTestName(testNameBase, testIDLength) @@ -2388,7 +2318,7 @@ func serviceTeardown(svc *corev1.Service) { } func kymaGatewayTeardown(gateway *networkingv1beta1.Gateway) { - By(fmt.Sprintf("Deleting Kyma Gateway as part of teardown", gateway.Name)) + By(fmt.Sprintf("Deleting Kyma Gateway %s as part of teardown", gateway.Name)) err := c.Delete(context.Background(), gateway) if err != nil { diff --git a/tests/integration/testsuites/v2alpha1/features/short_host.feature b/tests/integration/testsuites/v2alpha1/features/short_host.feature new file mode 100644 index 000000000..313453228 --- /dev/null +++ b/tests/integration/testsuites/v2alpha1/features/short_host.feature @@ -0,0 +1,7 @@ +Feature: Exposing endpoints with NoAuth when specifying short host only in APIRule + + Scenario: Calling a httpbin endpoint unsecured + Given ShortHost: There is a httpbin service + When ShortHost: The APIRule is applied + Then ShortHost: Calling the "/ip" endpoint without a token should result in status between 200 and 200 + And ShortHost: Teardown httpbin service diff --git a/tests/integration/testsuites/v2alpha1/manifests/short-host.yaml b/tests/integration/testsuites/v2alpha1/manifests/short-host.yaml new file mode 100644 index 000000000..a602ffcdc --- /dev/null +++ b/tests/integration/testsuites/v2alpha1/manifests/short-host.yaml @@ -0,0 +1,16 @@ +apiVersion: gateway.kyma-project.io/v2alpha1 +kind: APIRule +metadata: + name: "{{.NamePrefix}}-{{.TestID}}" + namespace: "{{.Namespace}}" +spec: + gateway: "{{.GatewayNamespace}}/{{.GatewayName}}" + hosts: + - "httpbin" + service: + name: httpbin-{{.TestID}} + port: 8000 + rules: + - path: /* + methods: ["GET"] + noAuth: true diff --git a/tests/integration/testsuites/v2alpha1/scenario_short_host.go b/tests/integration/testsuites/v2alpha1/scenario_short_host.go new file mode 100644 index 000000000..278c2e1c3 --- /dev/null +++ b/tests/integration/testsuites/v2alpha1/scenario_short_host.go @@ -0,0 +1,14 @@ +package v2alpha1 + +import ( + "github.com/cucumber/godog" +) + +func initShortHost(ctx *godog.ScenarioContext, ts *testsuite) { + scenario := ts.createScenario("short-host.yaml", "short-host") + + ctx.Step(`^ShortHost: There is a httpbin service$`, scenario.thereIsAHttpbinService) + ctx.Step(`^ShortHost: The APIRule is applied$`, scenario.theAPIRuleIsApplied) + ctx.Step(`^ShortHost: Calling the "([^"]*)" endpoint without a token should result in status between (\d+) and (\d+)$`, scenario.callingTheEndpointWithoutTokenShouldResultInStatusBetween) + ctx.Step(`^ShortHost: Teardown httpbin service$`, scenario.teardownHttpbinService) +} diff --git a/tests/integration/testsuites/v2alpha1/testsuite.go b/tests/integration/testsuites/v2alpha1/testsuite.go index 9e8f4893d..decb7f40f 100644 --- a/tests/integration/testsuites/v2alpha1/testsuite.go +++ b/tests/integration/testsuites/v2alpha1/testsuite.go @@ -5,10 +5,11 @@ import ( _ "embed" "encoding/base64" "fmt" - "github.com/kyma-project/api-gateway/tests/integration/pkg/hooks" "log" "path" + "github.com/kyma-project/api-gateway/tests/integration/pkg/hooks" + "github.com/cucumber/godog" "github.com/kyma-project/api-gateway/tests/integration/pkg/auth" "github.com/kyma-project/api-gateway/tests/integration/pkg/helpers" @@ -89,6 +90,7 @@ func (t *testsuite) InitScenarios(ctx *godog.ScenarioContext) { initExtAuthJwt(ctx, t) initValidationError(ctx, t) initNoAuthWildcard(ctx, t) + initShortHost(ctx, t) } func (t *testsuite) FeaturePath() []string { From 61dd29075c5e25578ed9a5bd86500ab9079a80f9 Mon Sep 17 00:00:00 2001 From: Vladimir Videlov Date: Tue, 24 Sep 2024 11:28:57 +0200 Subject: [PATCH 07/27] Renamings and docs update --- apis/gateway/v2alpha1/apirule_types.go | 4 +- .../gateway.kyma-project.io_apirules.yaml | 4 +- .../api_controller_integration_test.go | 6 +- .../adr/0007-apirule-v2alpha1-api-proposal.md | 2 +- docs/release-notes/2.7.0.md | 5 +- .../v2alpha1/04-10-apirule-custom-resource.md | 27 ++++++- .../tutorials/01-30-set-up-mtls-gateway.md | 10 +-- .../01-40-expose-workload-apigateway.md | 14 ++-- ...42-expose-workloads-multiple-namespaces.md | 4 +- .../01-40-expose-workload-apigateway.md | 10 +-- .../01-41-expose-multiple-workloads.md | 34 ++++---- ...42-expose-workloads-multiple-namespaces.md | 10 +-- ...01-50-expose-and-secure-workload-oauth2.md | 30 ++++--- .../01-53-expose-and-secure-workload-istio.md | 16 ++-- ...se-and-secure-workload-with-certificate.md | 4 +- .../01-52-expose-and-secure-workload-jwt.md | 16 ++-- .../01-60-security/01-62-set-up-idp.md | 2 +- internal/helpers/hosts.go | 4 +- internal/helpers/hosts_test.go | 80 +++++++++---------- .../virtual_service_processor.go | 2 +- internal/validation/v2alpha1/hosts.go | 6 +- internal/validation/v2alpha1/hosts_test.go | 8 +- .../v2alpha1/features/short_host.feature | 2 +- 23 files changed, 163 insertions(+), 137 deletions(-) diff --git a/apis/gateway/v2alpha1/apirule_types.go b/apis/gateway/v2alpha1/apirule_types.go index 859c29b9c..1807de9fa 100644 --- a/apis/gateway/v2alpha1/apirule_types.go +++ b/apis/gateway/v2alpha1/apirule_types.go @@ -55,9 +55,9 @@ type APIRuleSpec struct { Timeout *Timeout `json:"timeout,omitempty"` } -// Host is the URL of the exposed service. We support short names. +// Host is the URL of the exposed service. We support short host names. // +kubebuilder:validation:MaxLength=255 -// +kubebuilder:validation:XValidation:rule=`self.matches('^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\\.[a-z0-9][a-z0-9-]{0,61}[a-z0-9])*$')`,message="Host must be a short name or a fully qualified domain name" +// +kubebuilder:validation:XValidation:rule=`self.matches('^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\\.[a-z0-9][a-z0-9-]{0,61}[a-z0-9])*$')`,message="Host must be a short host name or a fully qualified domain name" type Host string // APIRuleStatus describes the observed state of ApiRule. diff --git a/config/crd/bases/gateway.kyma-project.io_apirules.yaml b/config/crd/bases/gateway.kyma-project.io_apirules.yaml index ef7d17886..4bf997607 100644 --- a/config/crd/bases/gateway.kyma-project.io_apirules.yaml +++ b/config/crd/bases/gateway.kyma-project.io_apirules.yaml @@ -364,11 +364,11 @@ spec: description: Specifies the URLs of the exposed service. items: description: Host is the URL of the exposed service. We support - short names. + short host names. maxLength: 255 type: string x-kubernetes-validations: - - message: Host must be a short name or a fully qualified domain + - message: Host must be a short host name or a fully qualified domain name rule: self.matches('^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\\.[a-z0-9][a-z0-9-]{0,61}[a-z0-9])*$') maxItems: 1 diff --git a/controllers/gateway/api_controller_integration_test.go b/controllers/gateway/api_controller_integration_test.go index fdfd678ea..26e6f47cc 100644 --- a/controllers/gateway/api_controller_integration_test.go +++ b/controllers/gateway/api_controller_integration_test.go @@ -1457,7 +1457,7 @@ var _ = Describe("APIRule Controller", Serial, func() { }) }) - Context("hosts should be a valid FQDN or a short name", Ordered, func() { + Context("hosts should be a valid FQDN or a short host name", Ordered, func() { It("should create an APIRule with a valid FQDN host", func() { // given apiRuleName := generateTestName(testNameBase, testIDLength) @@ -1479,7 +1479,7 @@ var _ = Describe("APIRule Controller", Serial, func() { }() }) - It("should create an APIRule with short name that has length of 1 character", func() { + It("should create an APIRule with short host name that has length of 1 character", func() { // given apiRuleName := generateTestName(testNameBase, testIDLength) serviceName := testServiceNameBase @@ -1568,7 +1568,7 @@ var _ = Describe("APIRule Controller", Serial, func() { serviceTeardown(svc) }() Expect(err).Should(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("spec.hosts[0]: Invalid value: \"string\": Host must be a short name or a fully qualified domain name")) + Expect(err.Error()).To(ContainSubstring("spec.hosts[0]: Invalid value: \"string\": Host must be a short host name or a fully qualified domain name")) } It("should not create an APIRule with an empty host", func() { diff --git a/docs/contributor/adr/0007-apirule-v2alpha1-api-proposal.md b/docs/contributor/adr/0007-apirule-v2alpha1-api-proposal.md index 24feb23d4..8259d8396 100644 --- a/docs/contributor/adr/0007-apirule-v2alpha1-api-proposal.md +++ b/docs/contributor/adr/0007-apirule-v2alpha1-api-proposal.md @@ -25,7 +25,7 @@ Due to the deprecation of Ory and the introduction of new features in API Gatewa | **corsPolicy.allowCredentials** | **NO** | Specifies whether credentials are allowed in the **Access-Control-Allow-Credentials** CORS header. | | | **corsPolicy.exposeHeaders** | **NO** | Specifies headers exposed with the **Access-Control-Expose-Headers** CORS header. | | | **corsPolicy.maxAge** | **NO** | Specifies the maximum age of CORS policy cache. The value is provided in the **Access-Control-Max-Age** CORS header. | | -| **hosts** | **YES** | Specifies the Service's communication address for inbound external traffic. If only the leftmost label is provided, the default domain name is used. | The full domain name or the leftmost label cannot contain the wildcard character `*`. | +| **hosts** | **YES** | Specifies the Service's communication address for inbound external traffic. If only the leftmost label is provided, the domain name from the referenced Gateway is used. | The full domain name or the leftmost label cannot contain the wildcard character `*`. | | **service.name** | **NO** | Specifies the name of the exposed Service. | | | **service.namespace** | **NO** | Specifies the namespace of the exposed Service. | | | **service.port** | **NO** | Specifies the communication port of the exposed Service. | | diff --git a/docs/release-notes/2.7.0.md b/docs/release-notes/2.7.0.md index 4ecc0b456..cf8839eea 100644 --- a/docs/release-notes/2.7.0.md +++ b/docs/release-notes/2.7.0.md @@ -1,4 +1,7 @@ +## New Features + +- Add support for short host name for APIRule in version `v2alpha1` [#1311](https://github.com/kyma-project/api-gateway/pull/1311) ## Bug fixes -- Fix the wildcard path format and the validation of the rule path in an APIRule [#1285](https://github.com/kyma-project/api-gateway/pull/1285) \ No newline at end of file +- Fix the wildcard path format and the validation of the rule path in an APIRule [#1285](https://github.com/kyma-project/api-gateway/pull/1285) diff --git a/docs/user/custom-resources/apirule/v2alpha1/04-10-apirule-custom-resource.md b/docs/user/custom-resources/apirule/v2alpha1/04-10-apirule-custom-resource.md index 553b92d03..f9a426bcc 100644 --- a/docs/user/custom-resources/apirule/v2alpha1/04-10-apirule-custom-resource.md +++ b/docs/user/custom-resources/apirule/v2alpha1/04-10-apirule-custom-resource.md @@ -23,7 +23,7 @@ This table lists all parameters of APIRule `v2alpha1` CRD together with their de | **corsPolicy.allowCredentials** | **NO** | Specifies whether credentials are allowed in the **Access-Control-Allow-Credentials** CORS header. | None | | **corsPolicy.exposeHeaders** | **NO** | Specifies headers exposed with the **Access-Control-Expose-Headers** CORS header. | None | | **corsPolicy.maxAge** | **NO** | Specifies the maximum age of CORS policy cache. The value is provided in the **Access-Control-Max-Age** CORS header. | None | -| **hosts** | **YES** | Specifies the Service's communication address for inbound external traffic. It must be a short name or a valid fully qualified domain name (FQDN) in format: at least two domain labels with characters, numbers, or hypens. | Short name or FQDN format. | +| **hosts** | **YES** | Specifies the Service's communication address for inbound external traffic. It must be a short host name or a valid fully qualified domain name (FQDN) in format: at least two domain labels with characters, numbers, or hypens. | Short host name or FQDN format. | | **service.name** | **NO** | Specifies the name of the exposed Service. | None | | **service.namespace** | **NO** | Specifies the namespace of the exposed Service. | None | | **service.port** | **NO** | Specifies the communication port of the exposed Service. | None | @@ -61,6 +61,9 @@ This table lists all parameters of APIRule `v2alpha1` CRD together with their de > [!WARNING] > If a service is not defined at the **spec.service** level, all defined Access Rules must have it defined at the **spec.rules.service** level. Otherwise, the validation fails. +> [!WARNING] +> If a short host name is defined at the **spec.hosts** level, the referenced Gateway must provide the same single host for all [Server](https://istio.io/latest/docs/reference/config/networking/gateway/#Server) definitions and it must be prefixed with `*.`. Otherwise, the validation fails. + **Status:** The following table lists the fields of the **status** section. @@ -93,3 +96,25 @@ spec: methods: [ "GET" ] noAuth: true ``` + +Usage of short host name. It will use the domain from the referenced Gateway `kyma-system/kyma-gateway`: + +```yaml +apiVersion: gateway.kyma-project.io/v2alpha1 +kind: APIRule +metadata: + name: service-exposed +spec: + gateway: kyma-system/kyma-gateway + hosts: + - foo + service: + name: foo-service + namespace: foo-namespace + port: 8080 + timeout: 360 + rules: + - path: /* + methods: [ "GET" ] + noAuth: true +``` diff --git a/docs/user/tutorials/01-30-set-up-mtls-gateway.md b/docs/user/tutorials/01-30-set-up-mtls-gateway.md index 513483698..bb796c460 100644 --- a/docs/user/tutorials/01-30-set-up-mtls-gateway.md +++ b/docs/user/tutorials/01-30-set-up-mtls-gateway.md @@ -104,7 +104,7 @@ The procedure of setting up a working mTLS Gateway is described in the following Create an Opaque Secret containing the previously generated Root CA certificate in the `istio-system` namespace. Run the following command: - + ```bash kubectl create secret generic -n istio-system kyma-mtls-certs-cacert --from-file=cacert=cacert.crt ``` @@ -119,7 +119,7 @@ To expose a custom workload, create an APIRule. You can either use version `v1be #### **Kyma Dashboard** 1. Go to the `default` namespace. -2. Go to **Discovery and Network > API Rules** and select **Create**. +2. Go to **Discovery and Network > API Rules** and select **Create**. 3. Provide the following configuration details. - **Name**: `httpbin-mtls` - In the Gateway section, select: @@ -129,7 +129,7 @@ To expose a custom workload, create an APIRule. You can either use version `v1be - In the `Rules` section, select: - **Path**: `/.*` - **Handler**: `no_auth` - - **Methods**: `GET` + - **Methods**: `GET` - Add the `httpbin` Service on port `8000`. #### **kubectl** @@ -172,7 +172,7 @@ metadata: name: httpbin-mtls namespace: default spec: - hosts: + hosts: - httpbin.$DOMAIN_TO_EXPOSE_WORKLOADS gateway: default/kyma-mtls-gateway rules: @@ -203,7 +203,7 @@ Now, access the secured workload using the correct JWT: 2. Create a new request and enter the URL `https://httpbin.{DOMAIN_TO_EXPOSE_WORKLOADS}/status/418`. Replace `{DOMAIN_TO_EXPOSE_WORKLOADS}` with the name of your domain. 3. Go to the **Headers** tab and add the header: - **Content-Type**: `application/x-www-form-urlencoded` -4. To call the endpoint, send a `GET` request to the HTTPBin Service. +4. To call the endpoint, send a `GET` request to the HTTPBin Service. If successful, you get the code `418` response. diff --git a/docs/user/tutorials/01-40-expose-workload/01-40-expose-workload-apigateway.md b/docs/user/tutorials/01-40-expose-workload/01-40-expose-workload-apigateway.md index 2f49678a1..8bec329d4 100644 --- a/docs/user/tutorials/01-40-expose-workload/01-40-expose-workload-apigateway.md +++ b/docs/user/tutorials/01-40-expose-workload/01-40-expose-workload-apigateway.md @@ -8,7 +8,7 @@ This tutorial shows how to expose an unsecured instance of the HTTPBin Service a ## Prerequisites * [Deploy a sample HTTPBin Service](../01-00-create-workload.md). -* [Set up your custom domain](../01-10-setup-custom-domain-for-workload.md) or use a Kyma domain instead. +* [Set up your custom domain](../01-10-setup-custom-domain-for-workload.md) or use a Kyma domain instead. ## Steps @@ -17,7 +17,7 @@ This tutorial shows how to expose an unsecured instance of the HTTPBin Service a #### **Kyma Dashboard** -1. Go to **Discovery and Network > API Rules** and select **Create**. +1. Go to **Discovery and Network > API Rules** and select **Create**. 2. Provide the following configuration details. - **Name**: `httpbin` - In the `Service` section, select: @@ -25,7 +25,7 @@ This tutorial shows how to expose an unsecured instance of the HTTPBin Service a - **Port**: `8000` - To fill in the `Gateway` section, use these values: - **Namespace** is the name of the namespace in which you deployed an instance of the HTTPBin Service. If you use a Kyma domain, select the `kyma-system` namespace. - - **Name** is the Gateway's name. If you use a Kyma domain, select `kyma-gateway`. + - **Name** is the Gateway's name. If you use a Kyma domain, select `kyma-gateway`. - In the **Host** field, enter `httpbin.{DOMAIN_TO_EXPORT_WORKLOADS}`. Replace the placeholder with the name of your domain. - In the `Rules` section, add two Rules. Use the following configuration for the first one: - **Path**: `/.*` @@ -35,16 +35,16 @@ This tutorial shows how to expose an unsecured instance of the HTTPBin Service a - **Path**: `/post` - **Handler**: `no_auth` - **Methods**: `POST` - + 3. To create the APIRule, select **Create**. #### **kubectl** 1. Depending on whether you use your custom domain or a Kyma domain, export the necessary values as environment variables: - + #### **Custom Domain** - + ```bash export DOMAIN_TO_EXPOSE_WORKLOADS={DOMAIN_NAME} export GATEWAY=$NAMESPACE/httpbin-gateway @@ -87,7 +87,7 @@ This tutorial shows how to expose an unsecured instance of the HTTPBin Service a > [!NOTE] -> If you are using k3d, add `httpbin.kyma.local` to the entry with k3d IP in your system's `/etc/hosts` file. +> If you are using k3d, add `httpbin.kyma.local` to the entry with k3d IP in your system's `/etc/hosts` file. > [!NOTE] > If you don't specify a namespace for your Service, the default namespace is used. diff --git a/docs/user/tutorials/01-40-expose-workload/01-42-expose-workloads-multiple-namespaces.md b/docs/user/tutorials/01-40-expose-workload/01-42-expose-workloads-multiple-namespaces.md index 4149baf11..155badfdf 100644 --- a/docs/user/tutorials/01-40-expose-workload/01-42-expose-workloads-multiple-namespaces.md +++ b/docs/user/tutorials/01-40-expose-workload/01-42-expose-workloads-multiple-namespaces.md @@ -20,7 +20,7 @@ Create three namespaces. Deploy two instances of the HTTPBin Service, each in a #### **Kyma Dashboard** -1. Go to **Discovery and Network > APIRules** and select **Create**. +1. Go to **Discovery and Network > APIRules** and select **Create**. 2. Switch to the `YAML` tab and paste the following configuration into the editor: ```yaml apiVersion: gateway.kyma-project.io/v1beta1 @@ -140,7 +140,7 @@ To call the endpoints, send `GET` requests to the HTTPBin Services: ```bash curl -ik -X GET https://httpbin-services.$DOMAIN_TO_EXPOSE_WORKLOADS/headers - curl -ik -X GET https://httpbin-services.$DOMAIN_TO_EXPOSE_WORKLOADS/get + curl -ik -X GET https://httpbin-services.$DOMAIN_TO_EXPOSE_WORKLOADS/get ``` If successful, the calls return the `200 OK` response code. diff --git a/docs/user/tutorials/01-40-expose-workload/v2alpha1/01-40-expose-workload-apigateway.md b/docs/user/tutorials/01-40-expose-workload/v2alpha1/01-40-expose-workload-apigateway.md index 6171351c3..50013b5c7 100644 --- a/docs/user/tutorials/01-40-expose-workload/v2alpha1/01-40-expose-workload-apigateway.md +++ b/docs/user/tutorials/01-40-expose-workload/v2alpha1/01-40-expose-workload-apigateway.md @@ -8,7 +8,7 @@ This tutorial shows how to expose an unsecured instance of the HTTPBin Service a ## Prerequisites * [Deploy a sample HTTPBin Service](../../01-00-create-workload.md). -* [Set up your custom domain](../../01-10-setup-custom-domain-for-workload.md) or use a Kyma domain instead. +* [Set up your custom domain](../../01-10-setup-custom-domain-for-workload.md) or use a Kyma domain instead. ## Steps @@ -17,10 +17,10 @@ This tutorial shows how to expose an unsecured instance of the HTTPBin Service a #### **kubectl** 1. Depending on whether you use your custom domain or a Kyma domain, export the necessary values as environment variables: - + #### **Custom Domain** - + ```bash export DOMAIN_TO_EXPOSE_WORKLOADS={DOMAIN_NAME} export GATEWAY=$NAMESPACE/httpbin-gateway @@ -43,7 +43,7 @@ This tutorial shows how to expose an unsecured instance of the HTTPBin Service a name: httpbin namespace: $NAMESPACE spec: - hosts: + hosts: - httpbin.$DOMAIN_TO_EXPOSE_WORKLOADS service: name: $SERVICE_NAME @@ -61,7 +61,7 @@ This tutorial shows how to expose an unsecured instance of the HTTPBin Service a ``` > [!NOTE] -> If you are using k3d, add `httpbin.kyma.local` to the entry with k3d IP in your system's `/etc/hosts` file. +> If you are using k3d, add `httpbin.kyma.local` to the entry with k3d IP in your system's `/etc/hosts` file. > [!NOTE] > If you don't specify a namespace for your Service, the default namespace is used. diff --git a/docs/user/tutorials/01-40-expose-workload/v2alpha1/01-41-expose-multiple-workloads.md b/docs/user/tutorials/01-40-expose-workload/v2alpha1/01-41-expose-multiple-workloads.md index e2b6f7ab5..a83def2da 100644 --- a/docs/user/tutorials/01-40-expose-workload/v2alpha1/01-41-expose-multiple-workloads.md +++ b/docs/user/tutorials/01-40-expose-workload/v2alpha1/01-41-expose-multiple-workloads.md @@ -2,13 +2,13 @@ This tutorial shows how to expose multiple workloads on different paths by defining a Service at the root level and by defining Services on each path separately. -> [!WARNING] +> [!WARNING] > Exposing a workload to the outside world is always a potential security vulnerability, so tread carefully. In a production environment, remember to secure the workload you expose with [JWT](../../01-50-expose-and-secure-a-workload/v2alpha1/01-52-expose-and-secure-workload-jwt.md). ## Prerequisites -* [Deploy two instances of a sample HTTPBin Service](../../01-00-create-workload.md) in one namespace. -* [Set up your custom domain](../../01-10-setup-custom-domain-for-workload.md) or use a Kyma domain instead. +* [Deploy two instances of a sample HTTPBin Service](../../01-00-create-workload.md) in one namespace. +* [Set up your custom domain](../../01-10-setup-custom-domain-for-workload.md) or use a Kyma domain instead. ## Define Multiple Services on Different Paths @@ -16,17 +16,17 @@ Follow the instructions to expose the instances of the HTTPBin Service on differ #### **kubectl** 1. Export the names of two deployed HTTPBin Services as environment variables: - + ```bash export FIRST_SERVICE={SERVICE_NAME} export SECOND_SERVICE={SERVICE_NAME} ``` 2. Depending on whether you use your custom domain or a Kyma domain, export the necessary values as environment variables: - + #### **Custom Domain** - + ```bash export DOMAIN_TO_EXPOSE_WORKLOADS={DOMAIN_NAME} export GATEWAY=$NAMESPACE/httpbin-gateway @@ -37,7 +37,7 @@ Follow the instructions to expose the instances of the HTTPBin Service on differ export DOMAIN_TO_EXPOSE_WORKLOADS={KYMA_DOMAIN_NAME} export GATEWAY=kyma-system/kyma-gateway ``` - + 3. To expose the instances of the HTTPBin Service, create the following APIRule: @@ -52,7 +52,7 @@ Follow the instructions to expose the instances of the HTTPBin Service on differ app: multiple-services example: multiple-services spec: - hosts: + hosts: - multiple-services.$DOMAIN_TO_EXPOSE_WORKLOADS gateway: $GATEWAY rules: @@ -74,24 +74,24 @@ Follow the instructions to expose the instances of the HTTPBin Service on differ ## Define a Service at the Root Level You can also define a Service at the root level. Such a definition is applied to all the paths specified at `spec.rules` that do not have their own Services defined. - -> [!NOTE] + +> [!NOTE] >Services defined at the `spec.rules` level have precedence over Service definition at the `spec.service` level. #### **kubectl** 1. Export the names of the two deployed HTTPBin Services as environment variables: - + ```bash export FIRST_SERVICE={SERVICE_NAME} export SECOND_SERVICE={SERVICE_NAME} ``` 2. Depending on whether you use your custom domain or a Kyma domain, export the necessary values as environment variables: - + #### **Custom Domain** - + ```bash export DOMAIN_TO_EXPOSE_WORKLOADS={DOMAIN_NAME} export GATEWAY=$NAMESPACE/httpbin-gateway @@ -102,7 +102,7 @@ You can also define a Service at the root level. Such a definition is applied to export DOMAIN_TO_EXPOSE_WORKLOADS={KYMA_DOMAIN_NAME} export GATEWAY=kyma-system/kyma-gateway ``` - + 3. To expose the instances of the HTTPBin Service, create the following APIRule: @@ -118,7 +118,7 @@ You can also define a Service at the root level. Such a definition is applied to app: multiple-services example: multiple-services spec: - hosts: + hosts: - multiple-services.$DOMAIN_TO_EXPOSE_WORKLOADS gateway: $GATEWAY service: @@ -151,10 +151,10 @@ To access your HTTPBin Services, use [Postman](https://www.postman.com) or [curl To call the endpoints, send `GET` requests to the HTTPBin Services: - ```bash + ```bash curl -ik -X GET https://multiple-services.$DOMAIN_TO_EXPOSE_WORKLOADS/headers - curl -ik -X GET https://multiple-services.$DOMAIN_TO_EXPOSE_WORKLOADS/get + curl -ik -X GET https://multiple-services.$DOMAIN_TO_EXPOSE_WORKLOADS/get ``` If successful, the calls return the `200 OK` response code. diff --git a/docs/user/tutorials/01-40-expose-workload/v2alpha1/01-42-expose-workloads-multiple-namespaces.md b/docs/user/tutorials/01-40-expose-workload/v2alpha1/01-42-expose-workloads-multiple-namespaces.md index 6e6d2220f..f784a36e6 100644 --- a/docs/user/tutorials/01-40-expose-workload/v2alpha1/01-42-expose-workloads-multiple-namespaces.md +++ b/docs/user/tutorials/01-40-expose-workload/v2alpha1/01-42-expose-workloads-multiple-namespaces.md @@ -28,12 +28,12 @@ Create three namespaces. Deploy two instances of the HTTPBin Service, each in a export NAMESPACE_SECOND_SERVICE={NAMESPACE_NAME} export NAMESPACE_APIRULE={NAMESPACE_NAME} ``` - + 2. Depending on whether you use your custom domain or a Kyma domain, export the necessary values as environment variables: - + #### **Custom Domain** - + ```bash export DOMAIN_TO_EXPOSE_WORKLOADS={DOMAIN_NAME} export GATEWAY=$NAMESPACE/httpbin-gateway @@ -56,7 +56,7 @@ Create three namespaces. Deploy two instances of the HTTPBin Service, each in a name: httpbin-services namespace: $NAMESPACE_APIRULE spec: - hosts: + hosts: - httpbin-services.$DOMAIN_TO_EXPOSE_WORKLOADS gateway: $GATEWAY rules: @@ -97,7 +97,7 @@ To call the endpoints, send `GET` requests to the HTTPBin Services: ```bash curl -ik -X GET https://httpbin-services.$DOMAIN_TO_EXPOSE_WORKLOADS/headers - curl -ik -X GET https://httpbin-services.$DOMAIN_TO_EXPOSE_WORKLOADS/get + curl -ik -X GET https://httpbin-services.$DOMAIN_TO_EXPOSE_WORKLOADS/get ``` If successful, the calls return the `200 OK` response code. diff --git a/docs/user/tutorials/01-50-expose-and-secure-a-workload/01-50-expose-and-secure-workload-oauth2.md b/docs/user/tutorials/01-50-expose-and-secure-a-workload/01-50-expose-and-secure-workload-oauth2.md index 0a27882d9..fb4764aa1 100644 --- a/docs/user/tutorials/01-50-expose-and-secure-a-workload/01-50-expose-and-secure-workload-oauth2.md +++ b/docs/user/tutorials/01-50-expose-and-secure-a-workload/01-50-expose-and-secure-workload-oauth2.md @@ -7,7 +7,6 @@ This tutorial shows how to expose and secure Services using APIGateway Controlle * [Deploy a sample HTTPBin Service](../01-00-create-workload.md). * [Set up your custom domain](../01-10-setup-custom-domain-for-workload.md) or use a Kyma domain instead. * Configure your client ID and client Secret using an OAuth2-compliant provider. -* ## Steps @@ -21,7 +20,7 @@ Follow the steps to get a token with the `read` scope: 2. Go to the `Body` tab and select the `x-www-form-urlencoded` option. Add two key-value pairs to the body: - **grant_type**: `client_credentials&scope=read` - **client_id**: `{CLIENT_ID}` - + Replace `{CLIENT_ID}` with your client ID. 2. Go to the **Headers** tab and add the header: - **Content-Type**: `application/x-www-form-urlencoded` @@ -29,7 +28,7 @@ Follow the steps to get a token with the `read` scope: - **Type**: Basic - **Username**: `{CLIENT_ID}` - **Password**: `{CLIENT_SECRET}` - + Replace `{CLIENT_ID}` and `{CLIENT_SECRET}` with your Client ID and Client Secret. 4. Send a `POST` request and save your token. @@ -39,19 +38,19 @@ To get a token with the `read` scope, go to the `Body` tab and replace the **gra #### **curl** 1. Encode the client's credentials and export them as an environment variable: - + ```bash export ENCODED_CREDENTIALS=$(echo -n "$CLIENT_ID:$CLIENT_SECRET" | base64) export TOKEN_ENDPOINT={YOUR_TOKEN_ENDPOINT} ``` 2. Export your token endpoint as an environment variable: - + ```bash export TOKEN_ENDPOINT={YOUR_TOKEN_ENDPOINT} ``` 3. Get a token with the `read` scope. - + 1. Get the opaque token: ```shell curl --location --request POST "$TOKEN_ENDPOINT?grant_type=client_credentials" -F "scope=read" --header "Content-Type: application/x-www-form-urlencoded" --header "Authorization: Basic $ENCODED_CREDENTIALS" @@ -60,7 +59,7 @@ To get a token with the `read` scope, go to the `Body` tab and replace the **gra ```bash export ACCESS_TOKEN_READ={ISSUED_READ_TOKEN} ``` -4. Get a token with the `write` scope. +4. Get a token with the `write` scope. 1. Get the opaque token: ```shell @@ -77,14 +76,14 @@ To get a token with the `read` scope, go to the `Body` tab and replace the **gra #### **Kyma Dashboard** -1. Go to **Discovery and Network > API Rules** and select **Create**. +1. Go to **Discovery and Network > API Rules** and select **Create**. 2. Provide the following configuration details: - **Name**: `httpbin` - **Service Name**: `httpbin` - **Port**: `8000` - To fill in the `Gateway` section, use these values: - **Namespace** is the name of the namespace in which you deployed an instance of the HTTPBin Service. With a Kyma domain, use the `kyma-system` namespace. - - **Name** is the Gateway's name, for example `httpbin-gateway`. + - **Name** is the Gateway's name, for example `httpbin-gateway`. - In the **Host** field, enter `httpbin.{DOMAIN_TO_EXPORT_WORKLOADS}`. Replace the placeholder with the name of your domain. - Add an access strategy with the following configuration: - **Handler**: `oauth2_introspection` @@ -107,10 +106,10 @@ To get a token with the `read` scope, go to the `Body` tab and replace the **gra #### **kubectl** 1. Depending on whether you use your custom domain or a Kyma domain, export the necessary values as environment variables: - + #### **Custom Domain** - + ```bash export DOMAIN_TO_EXPOSE_WORKLOADS={DOMAIN_NAME} export GATEWAY=$NAMESPACE/httpbin-gateway @@ -124,7 +123,7 @@ To get a token with the `read` scope, go to the `Body` tab and replace the **gra 2. Export your introspection endpoint as an environment variable: - + ```bash export INTROSPECTION_ENDPOINT={INTROSPECTION_URL} ``` @@ -172,7 +171,6 @@ To get a token with the `read` scope, go to the `Body` tab and replace the **gra The exposed Service requires tokens with `read` scope for `GET` requests in the entire Service, and tokens with `write` scope for `POST` requests to the `/post` endpoint of the Service. - > [!WARNING] > When you secure a workload, don't create overlapping Access Rules for paths. Doing so can cause unexpected behavior and reduce the security of your implementation. @@ -186,14 +184,14 @@ Use the token with the `read` scope to access the HTTPBin Service: 1. Create a new request and enter the URL `https://httpbin.{DOMAIN_TO_EXPOSE_WORKLOADS}/status/headers`. Replace `{DOMAIN_TO_EXPOSE_WORKLOADS}` with the name of your domain. 2. Go to the **Headers** tab. Add a new header with the key **Authorization** and the value `Bearer {ACCESS_TOKEN_READ}`. Replace `{ACCESS_TOKEN_READ}` with the Opaque token that has the `read` scope. -4. To call the endpoint, send a `GET` request to the HTTPBin Service. +4. To call the endpoint, send a `GET` request to the HTTPBin Service. Use the token with the `write` scope to access the HTTPBin Service: - + 1. Create a new request and enter the URL `https://httpbin.{DOMAIN_TO_EXPOSE_WORKLOADS}/status/post`. Replace `{DOMAIN_TO_EXPOSE_WORKLOADS}` with the name of your domain. 2. Go to the **Headers** tab. Add a new header with the key **Authorization** and the value `Bearer {ACCESS_TOKEN_WRITE}`. Replace `{ACCESS_TOKEN_WRITE}` with the Opaque token that has the `write` scope. -4. To call the endpoint, send a `POST` request to the HTTPBin Service. +4. To call the endpoint, send a `POST` request to the HTTPBin Service. #### **curl** diff --git a/docs/user/tutorials/01-50-expose-and-secure-a-workload/01-53-expose-and-secure-workload-istio.md b/docs/user/tutorials/01-50-expose-and-secure-a-workload/01-53-expose-and-secure-workload-istio.md index 5292824c5..04c1f17c6 100644 --- a/docs/user/tutorials/01-50-expose-and-secure-a-workload/01-53-expose-and-secure-workload-istio.md +++ b/docs/user/tutorials/01-50-expose-and-secure-a-workload/01-53-expose-and-secure-workload-istio.md @@ -15,7 +15,7 @@ This tutorial shows how to expose and secure a workload using Istio's built-in s #### **Kyma dashboard** - 1. Go to **Istio > Virtual Services** and select **Create**. + 1. Go to **Istio > Virtual Services** and select **Create**. 2. Provide the following configuration details: - **Name**: `httpbin` - Go to **HTTP > Matches > Match** and provide URI of the type **prefix** and value `/`. @@ -27,10 +27,10 @@ This tutorial shows how to expose and secure a workload using Istio's built-in s #### **kubectl** 1. Depending on whether you use your custom domain or a Kyma domain, export the necessary values as environment variables: - + #### **Custom Domain** - + ```bash export DOMAIN_TO_EXPOSE_WORKLOADS={DOMAIN_NAME} export GATEWAY=$NAMESPACE/httpbin-gateway @@ -41,7 +41,7 @@ This tutorial shows how to expose and secure a workload using Istio's built-in s export DOMAIN_TO_EXPOSE_WORKLOADS={KYMA_DOMAIN_NAME} export GATEWAY=kyma-system/kyma-gateway ``` - + 2. To expose your workload, create a VirtualService: @@ -68,7 +68,7 @@ This tutorial shows how to expose and secure a workload using Istio's built-in s host: httpbin.$NAMESPACE.svc.cluster.local EOF ``` - + ### Secure Your Workload @@ -162,15 +162,15 @@ To access your HTTPBin Service, use [Postman](https://www.postman.com) or [curl] 1. Try to access the secured workload without credentials. 1. Enter the URL `https://httpbin.{DOMAIN_TO_EXPOSE_WORKLOADS}/status/200` and replace `{DOMAIN_TO_EXPOSE_WORKLOADS}` with the name of your domain. - 2. To call the endpoint, send a `GET` request to the HTTPBin Service. + 2. To call the endpoint, send a `GET` request to the HTTPBin Service. You get the error `403 Forbidden`. 2. Now, access the secured workload using the correct JWT. 1. Enter the URL `https://httpbin.{DOMAIN_TO_EXPOSE_WORKLOADS}/status/200` and replace `{DOMAIN_TO_EXPOSE_WORKLOADS}` with the name of your domain. - 2. Go to the **Headers** tab. + 2. Go to the **Headers** tab. 3. Add a new header with the key `Authorization` and the value `Bearer {ACCESS_TOKEN}`. Replace `{ACCESS_TOKEN}` with your JWT. - 4. To call the endpoint, send a `GET` request to the HTTPBin Service. + 4. To call the endpoint, send a `GET` request to the HTTPBin Service. If successful, you get the `200 OK` response code. diff --git a/docs/user/tutorials/01-50-expose-and-secure-a-workload/01-54-expose-and-secure-workload-with-certificate.md b/docs/user/tutorials/01-50-expose-and-secure-a-workload/01-54-expose-and-secure-workload-with-certificate.md index 0c217e4e2..d7ebd3f9a 100644 --- a/docs/user/tutorials/01-50-expose-and-secure-a-workload/01-54-expose-and-secure-workload-with-certificate.md +++ b/docs/user/tutorials/01-50-expose-and-secure-a-workload/01-54-expose-and-secure-workload-with-certificate.md @@ -48,7 +48,7 @@ This tutorial shows how to expose and secure a workload with mutual authenticati 2. Create VirtualService that adds the **X-CLIENT-SSL** headers to incoming requests: ```bash - cat < #### **Custom Domain** - + ```bash export DOMAIN_TO_EXPOSE_WORKLOADS={DOMAIN_NAME} export GATEWAY=$NAMESPACE/httpbin-gateway @@ -29,10 +29,10 @@ This tutorial shows how to expose and secure Services using APIGateway Controlle export DOMAIN_TO_EXPOSE_WORKLOADS={KYMA_DOMAIN_NAME} export GATEWAY=kyma-system/kyma-gateway ``` - + 2. To expose and secure the Service, create the following APIRule: - + ```bash cat < Date: Tue, 24 Sep 2024 11:29:43 +0200 Subject: [PATCH 08/27] Update internal/validation/v2alpha1/hosts.go Co-authored-by: Bartosz Chwila <103247439+barchw@users.noreply.github.com> --- internal/validation/v2alpha1/hosts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/validation/v2alpha1/hosts.go b/internal/validation/v2alpha1/hosts.go index 6e6a91081..583c81137 100644 --- a/internal/validation/v2alpha1/hosts.go +++ b/internal/validation/v2alpha1/hosts.go @@ -38,7 +38,7 @@ func validateHosts(parentAttributePath string, vsList networkingv1beta1.VirtualS hostAttributePath := fmt.Sprintf("%s[%d]", hostsAttributePath, hostIndex) failures = append(failures, validation.Failure{ AttributePath: hostAttributePath, - Message: "Short host only supported when Gateway has single host definition matching *. format", + Message: "Lowercase RFC 1123 label is only supported as the APIRule host when selected Gateway has a single host definition matching *. format", }) } } else { From ab44d4e21662ad32e09f3f4e7c6f40f0b7327a32 Mon Sep 17 00:00:00 2001 From: Vladimir Videlov Date: Tue, 24 Sep 2024 11:32:24 +0200 Subject: [PATCH 09/27] Review naming suggestions --- apis/gateway/v2alpha1/apirule_types.go | 4 ++-- .../apirule/v2alpha1/04-10-apirule-custom-resource.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apis/gateway/v2alpha1/apirule_types.go b/apis/gateway/v2alpha1/apirule_types.go index 1807de9fa..74834a7b9 100644 --- a/apis/gateway/v2alpha1/apirule_types.go +++ b/apis/gateway/v2alpha1/apirule_types.go @@ -55,9 +55,9 @@ type APIRuleSpec struct { Timeout *Timeout `json:"timeout,omitempty"` } -// Host is the URL of the exposed service. We support short host names. +// Host is the URL of the exposed service. We support lowercase RFC 1123 labels and FQDN. // +kubebuilder:validation:MaxLength=255 -// +kubebuilder:validation:XValidation:rule=`self.matches('^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\\.[a-z0-9][a-z0-9-]{0,61}[a-z0-9])*$')`,message="Host must be a short host name or a fully qualified domain name" +// +kubebuilder:validation:XValidation:rule=`self.matches('^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\\.[a-z0-9][a-z0-9-]{0,61}[a-z0-9])*$')`,message="Host must be a lowercase RFC 1123 label (must consist of lowercase alphanumeric characters or '-', and must start and end with an lowercase alphanumeric character) or a fully qualified domain name" type Host string // APIRuleStatus describes the observed state of ApiRule. diff --git a/docs/user/custom-resources/apirule/v2alpha1/04-10-apirule-custom-resource.md b/docs/user/custom-resources/apirule/v2alpha1/04-10-apirule-custom-resource.md index f9a426bcc..ea4a464e5 100644 --- a/docs/user/custom-resources/apirule/v2alpha1/04-10-apirule-custom-resource.md +++ b/docs/user/custom-resources/apirule/v2alpha1/04-10-apirule-custom-resource.md @@ -23,7 +23,7 @@ This table lists all parameters of APIRule `v2alpha1` CRD together with their de | **corsPolicy.allowCredentials** | **NO** | Specifies whether credentials are allowed in the **Access-Control-Allow-Credentials** CORS header. | None | | **corsPolicy.exposeHeaders** | **NO** | Specifies headers exposed with the **Access-Control-Expose-Headers** CORS header. | None | | **corsPolicy.maxAge** | **NO** | Specifies the maximum age of CORS policy cache. The value is provided in the **Access-Control-Max-Age** CORS header. | None | -| **hosts** | **YES** | Specifies the Service's communication address for inbound external traffic. It must be a short host name or a valid fully qualified domain name (FQDN) in format: at least two domain labels with characters, numbers, or hypens. | Short host name or FQDN format. | +| **hosts** | **YES** | Specifies the Service's communication address for inbound external traffic. It must be a RFC 1123 label or a valid fully qualified domain name (FQDN) in format: at least two domain labels with characters, numbers, or hyphens. | Lowercase RFC 1123 label or FQDN format. | | **service.name** | **NO** | Specifies the name of the exposed Service. | None | | **service.namespace** | **NO** | Specifies the namespace of the exposed Service. | None | | **service.port** | **NO** | Specifies the communication port of the exposed Service. | None | From 01a51ec945adf21c71c3fec8f7289e2b8ce0514e Mon Sep 17 00:00:00 2001 From: Vladimir Videlov Date: Tue, 24 Sep 2024 11:40:22 +0200 Subject: [PATCH 10/27] Update error message --- config/crd/bases/gateway.kyma-project.io_apirules.yaml | 8 +++++--- controllers/gateway/api_controller_integration_test.go | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/config/crd/bases/gateway.kyma-project.io_apirules.yaml b/config/crd/bases/gateway.kyma-project.io_apirules.yaml index 4bf997607..0465c94f6 100644 --- a/config/crd/bases/gateway.kyma-project.io_apirules.yaml +++ b/config/crd/bases/gateway.kyma-project.io_apirules.yaml @@ -364,12 +364,14 @@ spec: description: Specifies the URLs of the exposed service. items: description: Host is the URL of the exposed service. We support - short host names. + lowercase RFC 1123 labels and FQDN. maxLength: 255 type: string x-kubernetes-validations: - - message: Host must be a short host name or a fully qualified domain - name + - message: Host must be a lowercase RFC 1123 label (must consist + of lowercase alphanumeric characters or '-', and must start + and end with an lowercase alphanumeric character) or a fully + qualified domain name rule: self.matches('^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\\.[a-z0-9][a-z0-9-]{0,61}[a-z0-9])*$') maxItems: 1 minItems: 1 diff --git a/controllers/gateway/api_controller_integration_test.go b/controllers/gateway/api_controller_integration_test.go index 26e6f47cc..553caa111 100644 --- a/controllers/gateway/api_controller_integration_test.go +++ b/controllers/gateway/api_controller_integration_test.go @@ -1568,7 +1568,7 @@ var _ = Describe("APIRule Controller", Serial, func() { serviceTeardown(svc) }() Expect(err).Should(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("spec.hosts[0]: Invalid value: \"string\": Host must be a short host name or a fully qualified domain name")) + Expect(err.Error()).To(ContainSubstring("spec.hosts[0]: Invalid value: \"string\": Host must be a lowercase RFC 1123 label (must consist of lowercase alphanumeric characters or '-', and must start and end with an lowercase alphanumeric character) or a fully qualified domain name")) } It("should not create an APIRule with an empty host", func() { From c13f0125e9899ad542aebbcd4b39a38d87f1c0d2 Mon Sep 17 00:00:00 2001 From: Vladimir Videlov Date: Tue, 24 Sep 2024 11:56:42 +0200 Subject: [PATCH 11/27] Cover case for Istio gateway with host definition "*." --- .../default_domain/default_domain.go | 18 ++++-- .../default_domain/default_domain_test.go | 56 ++++++++++++++++++- internal/validation/v2alpha1/hosts_test.go | 2 +- 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/internal/processing/default_domain/default_domain.go b/internal/processing/default_domain/default_domain.go index eecf9fa2e..e05401eb1 100644 --- a/internal/processing/default_domain/default_domain.go +++ b/internal/processing/default_domain/default_domain.go @@ -70,10 +70,15 @@ func GetDomainFromKymaGateway(ctx context.Context, k8sClient client.Client) (str } if !strings.HasPrefix(httpsServers[0].Hosts[0], "*.") { - return "", fmt.Errorf(`gateway https server host %s does not start with the prefix "*."`, httpsServers[0].Hosts[0]) + return "", fmt.Errorf(`gateway https server host "%s" does not start with the prefix "*."`, httpsServers[0].Hosts[0]) } - return strings.TrimPrefix(httpsServers[0].Hosts[0], "*."), nil + domain := strings.TrimPrefix(httpsServers[0].Hosts[0], "*.") + if domain == "" { + return "", fmt.Errorf(`gateway https server host "%s" does not define domain after the prefix "*."`, httpsServers[0].Hosts[0]) + } + + return domain, nil } func GetDomainFromGateway(ctx context.Context, k8sClient client.Client, gatewayName, gatewayNamespace string) (string, error) { @@ -88,10 +93,15 @@ func GetDomainFromGateway(ctx context.Context, k8sClient client.Client, gatewayN } if !strings.HasPrefix(gateway.Spec.Servers[0].Hosts[0], "*.") { - return "", fmt.Errorf(`gateway server host %s does not start with the prefix "*."`, gateway.Spec.Servers[0].Hosts[0]) + return "", fmt.Errorf(`gateway server host "%s" does not start with the prefix "*."`, gateway.Spec.Servers[0].Hosts[0]) + } + + domain := strings.TrimPrefix(gateway.Spec.Servers[0].Hosts[0], "*.") + if domain == "" { + return "", fmt.Errorf(`gateway server host "%s" does not define domain after the prefix "*."`, gateway.Spec.Servers[0].Hosts[0]) } - return strings.TrimPrefix(gateway.Spec.Servers[0].Hosts[0], "*."), nil + return domain, nil } func gatewayServersWithSameSingleHost(gateway *networkingv1beta1.Gateway) bool { diff --git a/internal/processing/default_domain/default_domain_test.go b/internal/processing/default_domain/default_domain_test.go index 2d847558b..b8ac4fdee 100644 --- a/internal/processing/default_domain/default_domain_test.go +++ b/internal/processing/default_domain/default_domain_test.go @@ -165,7 +165,33 @@ var _ = Describe("GetDomainFromKymaGateway", func() { // then Expect(err).Should(HaveOccurred()) - Expect(err.Error()).To(Equal(`gateway https server host local.kyma.dev does not start with the prefix "*."`)) + Expect(err.Error()).To(Equal(`gateway https server host "local.kyma.dev" does not start with the prefix "*."`)) + Expect(host).To(Equal("")) + }) + + It(`should return error if gateway has a HTTPS server but host do not define domain after "*." prefix`, func() { + // given + gateway := networkingv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{Name: kymaGatewayName, Namespace: kymaGatewayNamespace}, + Spec: apinetworkingv1beta1.Gateway{ + Servers: []*apinetworkingv1beta1.Server{ + { + Port: &apinetworkingv1beta1.Port{Protocol: "HTTPS"}, + Hosts: []string{ + "*.", + }, + }, + }, + }, + } + client := getFakeClient(&gateway) + + // when + host, err := GetDomainFromKymaGateway(context.Background(), client) + + // then + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(Equal(`gateway https server host "*." does not define domain after the prefix "*."`)) Expect(host).To(Equal("")) }) @@ -297,7 +323,33 @@ var _ = Describe("GetDomainFromGateway", func() { // then Expect(err).Should(HaveOccurred()) - Expect(err.Error()).To(Equal(`gateway server host local.kyma.dev does not start with the prefix "*."`)) + Expect(err.Error()).To(Equal(`gateway server host "local.kyma.dev" does not start with the prefix "*."`)) + Expect(host).To(Equal("")) + }) + + It(`should return error if gateway has a HTTPS server but host do not define domain after "*." prefix`, func() { + // given + gateway := networkingv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{Name: "gateway-name", Namespace: "gateway-namespace"}, + Spec: apinetworkingv1beta1.Gateway{ + Servers: []*apinetworkingv1beta1.Server{ + { + Port: &apinetworkingv1beta1.Port{Protocol: "HTTPS"}, + Hosts: []string{ + "*.", + }, + }, + }, + }, + } + client := getFakeClient(&gateway) + + // when + host, err := GetDomainFromGateway(context.Background(), client, "gateway-name", "gateway-namespace") + + // then + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(Equal(`gateway server host "*." does not define domain after the prefix "*."`)) Expect(host).To(Equal("")) }) diff --git a/internal/validation/v2alpha1/hosts_test.go b/internal/validation/v2alpha1/hosts_test.go index c0268d934..733990123 100644 --- a/internal/validation/v2alpha1/hosts_test.go +++ b/internal/validation/v2alpha1/hosts_test.go @@ -154,7 +154,7 @@ var _ = Describe("Validate hosts", func() { //then Expect(problems).To(HaveLen(1)) Expect(problems[0].AttributePath).To(Equal(".spec.hosts[0]")) - Expect(problems[0].Message).To(Equal("Short host only supported when Gateway has single host definition matching *. format")) + Expect(problems[0].Message).To(Equal("Lowercase RFC 1123 label is only supported as the APIRule host when selected Gateway has a single host definition matching *. format")) }) It("Should fail if any host that is occupied by any Virtual Service exposed by another resource", func() { From 8255e4cab5caa826ffa62d8ccd4bf017a468ad85 Mon Sep 17 00:00:00 2001 From: Vladimir Videlov Date: Tue, 24 Sep 2024 13:33:08 +0200 Subject: [PATCH 12/27] Fix --- internal/validation/v2alpha1/hosts.go | 6 +-- internal/validation/v2alpha1/hosts_test.go | 57 +++++++++++++++++++--- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/internal/validation/v2alpha1/hosts.go b/internal/validation/v2alpha1/hosts.go index 583c81137..c471accc4 100644 --- a/internal/validation/v2alpha1/hosts.go +++ b/internal/validation/v2alpha1/hosts.go @@ -32,7 +32,7 @@ func validateHosts(parentAttributePath string, vsList networkingv1beta1.VirtualS hostAttributePath := fmt.Sprintf("%s[%d]", hostsAttributePath, hostIndex) failures = append(failures, validation.Failure{ AttributePath: hostAttributePath, - Message: fmt.Sprintf("Unable to find Gateway %s", *apiRule.Spec.Gateway), + Message: fmt.Sprintf(`Unable to find Gateway "%s"`, *apiRule.Spec.Gateway), }) } else if !hasSingleHostDefinitionWithCorrectPrefix(gateway) { hostAttributePath := fmt.Sprintf("%s[%d]", hostsAttributePath, hostIndex) @@ -81,9 +81,9 @@ func hasSingleHostDefinitionWithCorrectPrefix(gateway *networkingv1beta1.Gateway return true } -func findGateway(name string, gwList networkingv1beta1.GatewayList) *networkingv1beta1.Gateway { +func findGateway(gatewayName string, gwList networkingv1beta1.GatewayList) *networkingv1beta1.Gateway { for _, gateway := range gwList.Items { - if gateway.Name == name { + if gatewayName == strings.Join([]string{gateway.Namespace, gateway.Name}, "/") { return gateway } } diff --git a/internal/validation/v2alpha1/hosts_test.go b/internal/validation/v2alpha1/hosts_test.go index 733990123..7433babd0 100644 --- a/internal/validation/v2alpha1/hosts_test.go +++ b/internal/validation/v2alpha1/hosts_test.go @@ -66,7 +66,7 @@ var _ = Describe("Validate hosts", func() { //given apiRule := &v2alpha1.APIRule{ Spec: v2alpha1.APIRuleSpec{ - Gateway: ptr.To("gateway-name"), + Gateway: ptr.To("gateway-ns/gateway-name"), Hosts: []*v2alpha1.Host{ ptr.To(v2alpha1.Host("short-name-host")), }, @@ -77,7 +77,8 @@ var _ = Describe("Validate hosts", func() { Items: []*networkingv1beta1.Gateway{ { ObjectMeta: metav1.ObjectMeta{ - Name: "gateway-name", + Name: "gateway-name", + Namespace: "gateway-ns", }, Spec: v1beta1.Gateway{ Servers: []*v1beta1.Server{ @@ -97,11 +98,11 @@ var _ = Describe("Validate hosts", func() { Expect(problems).To(HaveLen(0)) }) - It("Should fail if host is a short host name and referenced Gateway is missing", func() { + It("Should fail if host is a short host name and no Gateways available", func() { //given apiRule := &v2alpha1.APIRule{ Spec: v2alpha1.APIRuleSpec{ - Gateway: ptr.To("gateway-name"), + Gateway: ptr.To("gateway-ns/gateway-name"), Hosts: []*v2alpha1.Host{ ptr.To(v2alpha1.Host("short-name-host")), }, @@ -114,14 +115,55 @@ var _ = Describe("Validate hosts", func() { //then Expect(problems).To(HaveLen(1)) Expect(problems[0].AttributePath).To(Equal(".spec.hosts[0]")) - Expect(problems[0].Message).To(Equal("Unable to find Gateway gateway-name")) + Expect(problems[0].Message).To(Equal(`Unable to find Gateway "gateway-ns/gateway-name"`)) + }) + + It("Should fail if host is a short host name and referenced Gateway was not found", func() { + //given + apiRule := &v2alpha1.APIRule{ + Spec: v2alpha1.APIRuleSpec{ + Gateway: ptr.To("gateway-ns/gateway-name"), + Hosts: []*v2alpha1.Host{ + ptr.To(v2alpha1.Host("short-name-host")), + }, + }, + } + + gwList := networkingv1beta1.GatewayList{ + Items: []*networkingv1beta1.Gateway{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: "gateway-other-ns", + }, + Spec: v1beta1.Gateway{ + Servers: []*v1beta1.Server{ + { + Hosts: []string{"*.example.com"}, + }, + { + Hosts: []string{"*.example2.com"}, + }, + }, + }, + }, + }, + } + + //when + problems := validateHosts(".spec", networkingv1beta1.VirtualServiceList{}, gwList, apiRule) + + //then + Expect(problems).To(HaveLen(1)) + Expect(problems[0].AttributePath).To(Equal(".spec.hosts[0]")) + Expect(problems[0].Message).To(Equal(`Unable to find Gateway "gateway-ns/gateway-name"`)) }) It("Should fail if host is a short host name and referenced Gateway has various hosts definitions", func() { //given apiRule := &v2alpha1.APIRule{ Spec: v2alpha1.APIRuleSpec{ - Gateway: ptr.To("gateway-name"), + Gateway: ptr.To("gateway-ns/gateway-name"), Hosts: []*v2alpha1.Host{ ptr.To(v2alpha1.Host("short-name-host")), }, @@ -132,7 +174,8 @@ var _ = Describe("Validate hosts", func() { Items: []*networkingv1beta1.Gateway{ { ObjectMeta: metav1.ObjectMeta{ - Name: "gateway-name", + Name: "gateway-name", + Namespace: "gateway-ns", }, Spec: v1beta1.Gateway{ Servers: []*v1beta1.Server{ From c4348fe05b53a89ffee3489f2fe1dc04ef55eece Mon Sep 17 00:00:00 2001 From: Vladimir Videlov Date: Tue, 24 Sep 2024 14:14:55 +0200 Subject: [PATCH 13/27] Error on APIRule when Gateway not found --- controllers/gateway/apirule_controller.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/controllers/gateway/apirule_controller.go b/controllers/gateway/apirule_controller.go index c4be79de0..0734fddd7 100644 --- a/controllers/gateway/apirule_controller.go +++ b/controllers/gateway/apirule_controller.go @@ -24,6 +24,7 @@ import ( "github.com/kyma-project/api-gateway/internal/dependencies" "github.com/kyma-project/api-gateway/internal/processing/processors/migration" + "github.com/kyma-project/api-gateway/internal/processing/status" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -190,8 +191,22 @@ func (r *APIRuleReconciler) reconcileV2Alpha1APIRule(ctx context.Context, l logr } var gateway networkingv1beta1.Gateway if err := r.Client.Get(ctx, gatewayNN, &gateway); err != nil { - l.Error(err, "Error while getting Gateway for APIRule", "not found", apierrs.IsNotFound(err)) - return doneReconcileErrorRequeue(err, r.OnErrorReconcilePeriod) + v2Alpha1Status := status.ReconciliationV2alpha1Status{ + ApiRuleStatus: &gatewayv2alpha1.APIRuleStatus{ + State: gatewayv2alpha1.State(gatewayv2alpha1.Error), + }, + } + s := v2Alpha1Status.GenerateStatusFromFailures([]validation.Failure{ + { + AttributePath: "spec.gateway", + Message: "Could not get specified Gateway", + }, + }) + if err := s.UpdateStatus(&rule.Status); err != nil { + l.Error(err, "Error updating APIRule status") + return doneReconcileErrorRequeue(err, r.OnErrorReconcilePeriod) + } + return r.convertAndUpdateStatus(ctx, l, rule) } cmd := r.getV2Alpha1Reconciliation(&apiRule, &rule, &gateway, defaultDomainName, migrate, &l) From 15fce55215fcd4d070e9e9a46e639d8a5bb4289d Mon Sep 17 00:00:00 2001 From: Vladimir Videlov Date: Tue, 24 Sep 2024 16:56:44 +0200 Subject: [PATCH 14/27] Fix integration test --- tests/integration/pkg/helpers/api_rule.go | 3 ++- tests/integration/pkg/resource/manager.go | 12 ++++++++- .../v2alpha1/features/short_host.feature | 2 +- .../testsuites/v2alpha1/scenario.go | 25 +++++++++++++++++++ .../v2alpha1/scenario_short_host.go | 2 +- 5 files changed, 40 insertions(+), 4 deletions(-) diff --git a/tests/integration/pkg/helpers/api_rule.go b/tests/integration/pkg/helpers/api_rule.go index 8d5fd4979..4196330fd 100644 --- a/tests/integration/pkg/helpers/api_rule.go +++ b/tests/integration/pkg/helpers/api_rule.go @@ -3,11 +3,12 @@ package helpers import ( "encoding/json" "errors" + "log" + "github.com/avast/retry-go/v4" "github.com/kyma-project/api-gateway/tests/integration/pkg/resource" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/dynamic" - "log" ) type apiRuleStatus struct { diff --git a/tests/integration/pkg/resource/manager.go b/tests/integration/pkg/resource/manager.go index a4fffaff0..c565148d2 100644 --- a/tests/integration/pkg/resource/manager.go +++ b/tests/integration/pkg/resource/manager.go @@ -3,11 +3,12 @@ package resource import ( "context" "fmt" + "log" + "github.com/avast/retry-go/v4" "github.com/kyma-project/api-gateway/tests/integration/pkg/client" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "log" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -59,6 +60,8 @@ func (k godogResourceMapping) String() string { return "RequestAuthentication" case AuthorizationPolicy: return "AuthorizationPolicy" + case Gateway: + return "Gateway" } panic(fmt.Errorf("%#v has unimplemented String() method", k)) } @@ -84,6 +87,7 @@ const ( OryRule RequestAuthentication AuthorizationPolicy + Gateway ) type Manager struct { @@ -621,6 +625,12 @@ func GetResourceGvr(kind string) schema.GroupVersionResource { Version: "v1", Resource: "authorizationpolicies", } + case Gateway.String(): + gvr = schema.GroupVersionResource{ + Group: "networking.istio.io", + Version: "v1", + Resource: "gateways", + } default: panic(fmt.Errorf("cannot get gvr for kind: %s", kind)) } diff --git a/tests/integration/testsuites/v2alpha1/features/short_host.feature b/tests/integration/testsuites/v2alpha1/features/short_host.feature index 89ea4dfb8..9c27e48d1 100644 --- a/tests/integration/testsuites/v2alpha1/features/short_host.feature +++ b/tests/integration/testsuites/v2alpha1/features/short_host.feature @@ -3,5 +3,5 @@ Feature: Exposing endpoints with NoAuth when specifying short host name in APIRu Scenario: Calling a httpbin endpoint unsecured Given ShortHost: There is a httpbin service When ShortHost: The APIRule is applied - Then ShortHost: Calling the "/ip" endpoint without a token should result in status between 200 and 200 + Then ShortHost: Calling short host "httpbin" with path "/ip" without a token should result in status between 200 and 200 And ShortHost: Teardown httpbin service diff --git a/tests/integration/testsuites/v2alpha1/scenario.go b/tests/integration/testsuites/v2alpha1/scenario.go index 890b43523..ef6087fb7 100644 --- a/tests/integration/testsuites/v2alpha1/scenario.go +++ b/tests/integration/testsuites/v2alpha1/scenario.go @@ -124,6 +124,23 @@ func (s *scenario) theAPIRuleHasStatusWithDesc(expectedState, expectedDescriptio }, testcontext.GetRetryOpts()...) } +func (s *scenario) getGatewayHost(name, namespace string) (string, error) { + var host string + err := retry.Do(func() error { + apiRule, err := s.resourceManager.GetResource(s.k8sClient, resource.GetResourceGvr("Gateway"), namespace, name) + if err != nil { + return err + } + host = strings.TrimPrefix(apiRule.Object["spec"].(map[string]interface{})["servers"].([]interface{})[0].(map[string]interface{})["hosts"].([]interface{})[0].(string), "*.") + return nil + }, testcontext.GetRetryOpts()...) + + if err != nil { + return "", err + } + return host, nil +} + func (s *scenario) callingTheEndpointWithMethodWithInvalidTokenShouldResultInStatusBetween(path string, method string, lower, higher int) error { requestHeaders := map[string]string{testcontext.AuthorizationHeaderName: testcontext.AnyToken} return s.httpClient.CallEndpointWithHeadersAndMethod(requestHeaders, fmt.Sprintf("%s%s", s.Url, path), method, &helpers.StatusPredicate{LowerStatusBound: lower, UpperStatusBound: higher}) @@ -138,6 +155,14 @@ func (s *scenario) callingTheEndpointWithoutTokenShouldResultInStatusBetween(pat return s.httpClient.CallEndpointWithRetries(fmt.Sprintf("%s/%s", s.Url, strings.TrimLeft(path, "/")), &helpers.StatusPredicate{LowerStatusBound: lower, UpperStatusBound: higher}) } +func (s *scenario) callingShortHostWithoutTokenShouldResultInStatusBetween(host, path string, lower, higher int) error { + gatewayHost, err := s.getGatewayHost(s.config.GatewayName, s.config.GatewayNamespace) + if err != nil { + return err + } + return s.httpClient.CallEndpointWithRetries(fmt.Sprintf("https://%s.%s/%s", host, gatewayHost, strings.TrimLeft(path, "/")), &helpers.StatusPredicate{LowerStatusBound: lower, UpperStatusBound: higher}) +} + func (s *scenario) callingTheEndpointWithHeader(path, headerName, value string, lower, higher int) error { requestHeaders := map[string]string{headerName: value} return s.httpClient.CallEndpointWithHeadersWithRetries(requestHeaders, fmt.Sprintf("%s/%s", s.Url, strings.TrimLeft(path, "/")), &helpers.StatusPredicate{LowerStatusBound: lower, UpperStatusBound: higher}) diff --git a/tests/integration/testsuites/v2alpha1/scenario_short_host.go b/tests/integration/testsuites/v2alpha1/scenario_short_host.go index 278c2e1c3..1ffa9cfed 100644 --- a/tests/integration/testsuites/v2alpha1/scenario_short_host.go +++ b/tests/integration/testsuites/v2alpha1/scenario_short_host.go @@ -9,6 +9,6 @@ func initShortHost(ctx *godog.ScenarioContext, ts *testsuite) { ctx.Step(`^ShortHost: There is a httpbin service$`, scenario.thereIsAHttpbinService) ctx.Step(`^ShortHost: The APIRule is applied$`, scenario.theAPIRuleIsApplied) - ctx.Step(`^ShortHost: Calling the "([^"]*)" endpoint without a token should result in status between (\d+) and (\d+)$`, scenario.callingTheEndpointWithoutTokenShouldResultInStatusBetween) + ctx.Step(`^ShortHost: Calling short host "([^"]*)" with path "([^"]*)" without a token should result in status between (\d+) and (\d+)$`, scenario.callingShortHostWithoutTokenShouldResultInStatusBetween) ctx.Step(`^ShortHost: Teardown httpbin service$`, scenario.teardownHttpbinService) } From 7fd5421a4f9837cc81fdfebd17526b7642a160c4 Mon Sep 17 00:00:00 2001 From: Vladimir Videlov Date: Wed, 25 Sep 2024 09:35:05 +0200 Subject: [PATCH 15/27] Update internal/validation/v2alpha1/hosts.go Co-authored-by: Bartosz Chwila <103247439+barchw@users.noreply.github.com> --- internal/validation/v2alpha1/hosts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/validation/v2alpha1/hosts.go b/internal/validation/v2alpha1/hosts.go index c471accc4..e0e1ddc82 100644 --- a/internal/validation/v2alpha1/hosts.go +++ b/internal/validation/v2alpha1/hosts.go @@ -83,7 +83,7 @@ func hasSingleHostDefinitionWithCorrectPrefix(gateway *networkingv1beta1.Gateway func findGateway(gatewayName string, gwList networkingv1beta1.GatewayList) *networkingv1beta1.Gateway { for _, gateway := range gwList.Items { - if gatewayName == strings.Join([]string{gateway.Namespace, gateway.Name}, "/") { + if gatewayNamespacedName == strings.Join([]string{gateway.Namespace, gateway.Name}, "/") { return gateway } } From 9cc0e930668ad142e89d9e9aab5b2cb923cdd002 Mon Sep 17 00:00:00 2001 From: Vladimir Videlov Date: Wed, 25 Sep 2024 09:36:41 +0200 Subject: [PATCH 16/27] Update docs/contributor/adr/0007-apirule-v2alpha1-api-proposal.md Co-authored-by: Bartosz Chwila <103247439+barchw@users.noreply.github.com> --- docs/contributor/adr/0007-apirule-v2alpha1-api-proposal.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributor/adr/0007-apirule-v2alpha1-api-proposal.md b/docs/contributor/adr/0007-apirule-v2alpha1-api-proposal.md index 8259d8396..4d73dfbf0 100644 --- a/docs/contributor/adr/0007-apirule-v2alpha1-api-proposal.md +++ b/docs/contributor/adr/0007-apirule-v2alpha1-api-proposal.md @@ -25,7 +25,7 @@ Due to the deprecation of Ory and the introduction of new features in API Gatewa | **corsPolicy.allowCredentials** | **NO** | Specifies whether credentials are allowed in the **Access-Control-Allow-Credentials** CORS header. | | | **corsPolicy.exposeHeaders** | **NO** | Specifies headers exposed with the **Access-Control-Expose-Headers** CORS header. | | | **corsPolicy.maxAge** | **NO** | Specifies the maximum age of CORS policy cache. The value is provided in the **Access-Control-Max-Age** CORS header. | | -| **hosts** | **YES** | Specifies the Service's communication address for inbound external traffic. If only the leftmost label is provided, the domain name from the referenced Gateway is used. | The full domain name or the leftmost label cannot contain the wildcard character `*`. | +| **hosts** | **YES** | Specifies the Service's communication address for inbound external traffic. If only the leftmost label is provided, the domain name from the referenced Gateway is used, expanding the host to `