From 9e519799e26a6054c97b12ff7b291de26e172ad7 Mon Sep 17 00:00:00 2001 From: Chwila Date: Wed, 10 Jul 2024 13:48:23 +0200 Subject: [PATCH 01/12] CORS policy in v2alpha1 --- internal/builders/virtual_service.go | 120 ++++++++----- internal/builders/virtual_service_test.go | 8 +- .../processors/v2alpha1/reconciliation.go | 23 ++- .../v2alpha1/reconciliation_test.go | 22 +-- .../v2alpha1/virtual_service_processor.go | 162 ++++++++++++++++++ .../virtual_service_processor_test.go | 38 ++++ 6 files changed, 312 insertions(+), 61 deletions(-) create mode 100644 internal/processing/processors/v2alpha1/virtual_service_processor.go create mode 100644 internal/processing/processors/v2alpha1/virtual_service_processor_test.go diff --git a/internal/builders/virtual_service.go b/internal/builders/virtual_service.go index a8fc6828d..edce78909 100644 --- a/internal/builders/virtual_service.go +++ b/internal/builders/virtual_service.go @@ -3,30 +3,31 @@ package builders import ( "fmt" apirulev1beta1 "github.com/kyma-project/api-gateway/apis/gateway/v1beta1" + apirulev2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/wrapperspb" - "istio.io/api/networking/v1beta1" - networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + "istio.io/api/networking/v1" + networkingv1 "istio.io/client-go/pkg/apis/networking/v1" "strings" "time" ) -// VirtualService returns builder for istio.io/client-go/pkg/apis/networking/v1beta1/VirtualService type +// VirtualService returns builder for istio.io/client-go/pkg/apis/networking/v1/VirtualService type func VirtualService() *virtualService { return &virtualService{ - value: &networkingv1beta1.VirtualService{}, + value: &networkingv1.VirtualService{}, } } type virtualService struct { - value *networkingv1beta1.VirtualService + value *networkingv1.VirtualService } -func (vs *virtualService) Get() *networkingv1beta1.VirtualService { +func (vs *virtualService) Get() *networkingv1.VirtualService { return vs.value } -func (vs *virtualService) From(val *networkingv1beta1.VirtualService) *virtualService { +func (vs *virtualService) From(val *networkingv1.VirtualService) *virtualService { vs.value = val return vs } @@ -60,22 +61,22 @@ func (vs *virtualService) Spec(val *virtualServiceSpec) *virtualService { return vs } -// VirtualServiceSpec returns builder for istio.io/api/networking/v1beta1/VirtualServiceSpec type +// VirtualServiceSpec returns builder for istio.io/api/networking/v1/VirtualServiceSpec type func VirtualServiceSpec() *virtualServiceSpec { return &virtualServiceSpec{ - value: &v1beta1.VirtualService{}, + value: &v1.VirtualService{}, } } type virtualServiceSpec struct { - value *v1beta1.VirtualService + value *v1.VirtualService } -func (vss *virtualServiceSpec) Get() *v1beta1.VirtualService { +func (vss *virtualServiceSpec) Get() *v1.VirtualService { return vss.value } -func (vss *virtualServiceSpec) From(val *v1beta1.VirtualService) *virtualServiceSpec { +func (vss *virtualServiceSpec) From(val *v1.VirtualService) *virtualServiceSpec { vss.value = val return vss } @@ -95,18 +96,18 @@ func (vss *virtualServiceSpec) HTTP(hr *httpRoute) *virtualServiceSpec { return vss } -// HTTPRoute returns builder for istio.io/api/networking/v1beta1/HTTPRoute type +// HTTPRoute returns builder for istio.io/api/networking/v1/HTTPRoute type func HTTPRoute() *httpRoute { return &httpRoute{ - value: &v1beta1.HTTPRoute{}, + value: &v1.HTTPRoute{}, } } type httpRoute struct { - value *v1beta1.HTTPRoute + value *v1.HTTPRoute } -func (hr *httpRoute) Get() *v1beta1.HTTPRoute { +func (hr *httpRoute) Get() *v1.HTTPRoute { return hr.value } @@ -125,7 +126,7 @@ func (hr *httpRoute) CorsPolicy(cc *corsPolicy) *httpRoute { return hr } -func (hr *httpRoute) Headers(h *v1beta1.Headers) *httpRoute { +func (hr *httpRoute) Headers(h *v1.Headers) *httpRoute { hr.value.Headers = h return hr } @@ -135,23 +136,23 @@ func (hr *httpRoute) Timeout(value time.Duration) *httpRoute { return hr } -// MatchRequest returns builder for istio.io/api/networking/v1beta1/HTTPMatchRequest type +// MatchRequest returns builder for istio.io/api/networking/v1/HTTPMatchRequest type func MatchRequest() *matchRequest { return &matchRequest{ - value: &v1beta1.HTTPMatchRequest{}, + value: &v1.HTTPMatchRequest{}, } } type matchRequest struct { - value *v1beta1.HTTPMatchRequest + value *v1.HTTPMatchRequest } -func (mr *matchRequest) Get() *v1beta1.HTTPMatchRequest { +func (mr *matchRequest) Get() *v1.HTTPMatchRequest { return mr.value } func (mr *matchRequest) Uri() *stringMatch { - mr.value.Uri = &v1beta1.StringMatch{} + mr.value.Uri = &v1.StringMatch{} return &stringMatch{mr.value.Uri, func() *matchRequest { return mr }} } @@ -161,8 +162,8 @@ func (mr *matchRequest) MethodRegEx(httpMethods ...apirulev1beta1.HttpMethod) *m methodStrings := apirulev1beta1.ConvertHttpMethodsToStrings(httpMethods) methodsWithSeparator := strings.Join(methodStrings, "|") - mr.value.Method = &v1beta1.StringMatch{ - MatchType: &v1beta1.StringMatch_Regex{ + mr.value.Method = &v1.StringMatch{ + MatchType: &v1.StringMatch_Regex{ Regex: fmt.Sprintf("^(%s)$", methodsWithSeparator), }, } @@ -170,35 +171,35 @@ func (mr *matchRequest) MethodRegEx(httpMethods ...apirulev1beta1.HttpMethod) *m } type stringMatch struct { - value *v1beta1.StringMatch + value *v1.StringMatch parent func() *matchRequest } func (st *stringMatch) Regex(val string) *matchRequest { - st.value.MatchType = &v1beta1.StringMatch_Regex{Regex: val} + st.value.MatchType = &v1.StringMatch_Regex{Regex: val} return st.parent() } func (st *stringMatch) Prefix(val string) *matchRequest { - st.value.MatchType = &v1beta1.StringMatch_Prefix{Prefix: val} + st.value.MatchType = &v1.StringMatch_Prefix{Prefix: val} return st.parent() } -// RouteDestination returns builder for istio.io/api/networking/v1beta1/HTTPRouteDestination type +// RouteDestination returns builder for istio.io/api/networking/v1/HTTPRouteDestination type func RouteDestination() *routeDestination { - return &routeDestination{&v1beta1.HTTPRouteDestination{ - Destination: &v1beta1.Destination{ - Port: &v1beta1.PortSelector{}, + return &routeDestination{&v1.HTTPRouteDestination{ + Destination: &v1.Destination{ + Port: &v1.PortSelector{}, }, Weight: 100, }} } type routeDestination struct { - value *v1beta1.HTTPRouteDestination + value *v1.HTTPRouteDestination } -func (rd *routeDestination) Get() *v1beta1.HTTPRouteDestination { +func (rd *routeDestination) Get() *v1.HTTPRouteDestination { return rd.value } @@ -212,18 +213,18 @@ func (rd *routeDestination) Port(val uint32) *routeDestination { return rd } -// CorsPolicy returns builder for istio.io/api/networking/v1beta1/CorsPolicy type +// CorsPolicy returns builder for istio.io/api/networking/v1/CorsPolicy type func CorsPolicy() *corsPolicy { return &corsPolicy{ - value: &v1beta1.CorsPolicy{}, + value: &v1.CorsPolicy{}, } } type corsPolicy struct { - value *v1beta1.CorsPolicy + value *v1.CorsPolicy } -func (cp *corsPolicy) Get() *v1beta1.CorsPolicy { +func (cp *corsPolicy) Get() *v1.CorsPolicy { if cp.value.AllowOrigins == nil { return nil } @@ -248,7 +249,7 @@ func (cp *corsPolicy) AllowMethods(val ...string) *corsPolicy { return cp } -func (cp *corsPolicy) AllowOrigins(val ...*v1beta1.StringMatch) *corsPolicy { +func (cp *corsPolicy) AllowOrigins(val ...*v1.StringMatch) *corsPolicy { if len(val) == 0 { cp.value.AllowOrigins = nil } else { @@ -257,6 +258,37 @@ func (cp *corsPolicy) AllowOrigins(val ...*v1beta1.StringMatch) *corsPolicy { return cp } +func (cp *corsPolicy) FromV2Alpha1ApiRuleCorsPolicy(corsPolicy apirulev2alpha1.CorsPolicy) *corsPolicy { + if len(corsPolicy.AllowOrigins) == 0 { + cp.value.AllowOrigins = nil + } else { + matchers := corsPolicy.AllowOrigins.ToIstioStringMatchArray() + cp.value.AllowOrigins = append(cp.value.AllowOrigins, matchers...) + } + + if len(corsPolicy.AllowHeaders) > 0 { + cp.value.AllowHeaders = corsPolicy.AllowHeaders + } + + if len(corsPolicy.AllowMethods) > 0 { + cp.value.AllowMethods = corsPolicy.AllowMethods + } + + if len(corsPolicy.ExposeHeaders) > 0 { + cp.value.ExposeHeaders = corsPolicy.ExposeHeaders + } + + if corsPolicy.AllowCredentials != nil { + cp.value.AllowCredentials = &wrapperspb.BoolValue{Value: *corsPolicy.AllowCredentials} + } + + if corsPolicy.MaxAge != nil { + cp.value.MaxAge = durationpb.New(time.Duration(*corsPolicy.MaxAge) * time.Second) + } + + return cp +} + func (cp *corsPolicy) FromApiRuleCorsPolicy(corsPolicy apirulev1beta1.CorsPolicy) *corsPolicy { if len(corsPolicy.AllowOrigins) == 0 { cp.value.AllowOrigins = nil @@ -288,14 +320,14 @@ func (cp *corsPolicy) FromApiRuleCorsPolicy(corsPolicy apirulev1beta1.CorsPolicy return cp } -// NewHttpRouteHeadersBuilder returns builder for istio.io/api/networking/v1beta1/Headers type +// NewHttpRouteHeadersBuilder returns builder for istio.io/api/networking/v1/Headers type func NewHttpRouteHeadersBuilder() HttpRouteHeadersBuilder { return HttpRouteHeadersBuilder{ - value: &v1beta1.Headers{ - Request: &v1beta1.Headers_HeaderOperations{ + value: &v1.Headers{ + Request: &v1.Headers_HeaderOperations{ Set: make(map[string]string), }, - Response: &v1beta1.Headers_HeaderOperations{ + Response: &v1.Headers_HeaderOperations{ Set: make(map[string]string), }, }, @@ -303,10 +335,10 @@ func NewHttpRouteHeadersBuilder() HttpRouteHeadersBuilder { } type HttpRouteHeadersBuilder struct { - value *v1beta1.Headers + value *v1.Headers } -func (h HttpRouteHeadersBuilder) Get() *v1beta1.Headers { +func (h HttpRouteHeadersBuilder) Get() *v1.Headers { return h.value } diff --git a/internal/builders/virtual_service_test.go b/internal/builders/virtual_service_test.go index 3a4398a77..bb8434d3f 100644 --- a/internal/builders/virtual_service_test.go +++ b/internal/builders/virtual_service_test.go @@ -6,8 +6,8 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "google.golang.org/protobuf/types/known/durationpb" - v1beta12 "istio.io/api/networking/v1beta1" - networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + networkingv1 "istio.io/api/networking/v1" + networkingapiv1 "istio.io/client-go/pkg/apis/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" "regexp" @@ -28,7 +28,7 @@ var _ = Describe("Builder for", func() { name := "testName" namespace := "testNs" - initialVs := networkingv1beta1.VirtualService{} + initialVs := networkingapiv1.VirtualService{} initialVs.Name = "shoudBeOverwritten" initialVs.Spec.Hosts = []string{"a,", "b", "c"} @@ -162,7 +162,7 @@ var _ = Describe("Builder for", func() { })) Expect(result.Http[0].CorsPolicy.AllowOrigins).To(HaveLen(1)) - Expect(result.Http[0].CorsPolicy.AllowOrigins).To(ConsistOf(&v1beta12.StringMatch{MatchType: &v1beta12.StringMatch_Exact{Exact: "localhost"}})) + Expect(result.Http[0].CorsPolicy.AllowOrigins).To(ConsistOf(&networkingv1.StringMatch{MatchType: &networkingv1.StringMatch_Exact{Exact: "localhost"}})) Expect(result.Http[0].CorsPolicy.AllowCredentials).To(Not(BeNil())) Expect(result.Http[0].CorsPolicy.AllowCredentials.Value).To(BeTrue()) Expect(result.Http[0].CorsPolicy.AllowMethods).To(ConsistOf("GET", "POST")) diff --git a/internal/processing/processors/v2alpha1/reconciliation.go b/internal/processing/processors/v2alpha1/reconciliation.go index 4c26bdb43..8bf7fbc08 100644 --- a/internal/processing/processors/v2alpha1/reconciliation.go +++ b/internal/processing/processors/v2alpha1/reconciliation.go @@ -9,7 +9,7 @@ import ( "github.com/kyma-project/api-gateway/internal/processing/processors/istio" "github.com/kyma-project/api-gateway/internal/validation" istioValidation "github.com/kyma-project/api-gateway/internal/validation/v1beta1/istio" - networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + networkingapiv1 "istio.io/client-go/pkg/apis/networking/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -24,7 +24,7 @@ type Reconciliation struct { func (r Reconciliation) Validate(ctx context.Context, client client.Client) ([]validation.Failure, error) { - var vsList networkingv1beta1.VirtualServiceList + var vsList networkingapiv1.VirtualServiceList if err := client.List(ctx, &vsList); err != nil { return make([]validation.Failure, 0), err } @@ -54,3 +54,22 @@ func NewReconciliation(apiRuleV2alpha1 *gatewayv2alpha1.APIRule, apiRuleV1beta1 config: config, } } + +func filterDuplicatePaths(rules []gatewayv2alpha1.Rule) []gatewayv2alpha1.Rule { + uniqueRules := make(map[string]gatewayv2alpha1.Rule) + for _, rule := range rules { + uniqueRules[rule.Path] = rule + } + var uniqueRulesSlice []gatewayv2alpha1.Rule + for _, rule := range uniqueRules { + uniqueRulesSlice = append(uniqueRulesSlice, rule) + } + return uniqueRulesSlice +} + +func findServiceNamespace(api *gatewayv2alpha1.APIRule, rule *gatewayv2alpha1.Rule) string { + if rule.Service != nil && rule.Service.Namespace != nil { + return *rule.Service.Namespace + } + return api.Namespace +} diff --git a/internal/processing/processors/v2alpha1/reconciliation_test.go b/internal/processing/processors/v2alpha1/reconciliation_test.go index 67c347dc9..6cf4b4c25 100644 --- a/internal/processing/processors/v2alpha1/reconciliation_test.go +++ b/internal/processing/processors/v2alpha1/reconciliation_test.go @@ -6,9 +6,9 @@ import ( 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/processing/processors/v2alpha1" - "istio.io/api/networking/v1beta1" + apinetworkingv1 "istio.io/api/networking/v1" securityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/utils/ptr" "net/http" @@ -16,7 +16,7 @@ import ( . "github.com/kyma-project/api-gateway/internal/processing/processing_test" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + networkingv1 "istio.io/client-go/pkg/apis/networking/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -55,7 +55,7 @@ var _ = Describe("Reconciliation", func() { // then Expect(createdObjects).To(HaveLen(1)) - Expect(createdObjects[0]).To(BeAssignableToTypeOf(&networkingv1beta1.VirtualService{})) + Expect(createdObjects[0]).To(BeAssignableToTypeOf(&networkingv1.VirtualService{})) }) It("with v1beta1 JWT and v2alpha1 JWT should provide VirtualService, AuthorizationPolicy and RequestAuthentication", func() { @@ -87,7 +87,7 @@ var _ = Describe("Reconciliation", func() { vsCreated, apCreated, raCreated := false, false, false for _, createdObj := range createdObjects { - vs, vsOk := createdObj.(*networkingv1beta1.VirtualService) + vs, vsOk := createdObj.(*networkingv1.VirtualService) ra, raOk := createdObj.(*securityv1beta1.RequestAuthentication) ap, apOk := createdObj.(*securityv1beta1.AuthorizationPolicy) @@ -179,7 +179,7 @@ var _ = Describe("Reconciliation", func() { vsCreated, raCreated, apCreated := false, false, false for _, createdObj := range createdObjects { - vs, vsOk := createdObj.(*networkingv1beta1.VirtualService) + vs, vsOk := createdObj.(*networkingv1.VirtualService) ra, raOk := createdObj.(*securityv1beta1.RequestAuthentication) ap, apOk := createdObj.(*securityv1beta1.AuthorizationPolicy) @@ -248,7 +248,7 @@ var _ = Describe("Reconciliation", func() { numberOfCreatedRequestAuthentications := 0 for _, createdObj := range createdObjects { - vs, vsOk := createdObj.(*networkingv1beta1.VirtualService) + vs, vsOk := createdObj.(*networkingv1.VirtualService) ra, raOk := createdObj.(*securityv1beta1.RequestAuthentication) ap, apOk := createdObj.(*securityv1beta1.AuthorizationPolicy) @@ -259,7 +259,7 @@ var _ = Describe("Reconciliation", func() { Expect(len(http.Route)).To(Equal(1)) Expect(http.Route[0].Destination.Host).To(Equal("example-service.some-namespace.svc.cluster.local")) - switch http.Match[0].Uri.MatchType.(*v1beta1.StringMatch_Regex).Regex { + switch http.Match[0].Uri.MatchType.(*apinetworkingv1.StringMatch_Regex).Regex { case "/test": break case "/different-path": @@ -323,7 +323,7 @@ var _ = Describe("Reconciliation", func() { vsCreated, raCreated, apCreated := false, false, false for _, createdObj := range createdObjects { - vs, vsOk := createdObj.(*networkingv1beta1.VirtualService) + vs, vsOk := createdObj.(*networkingv1.VirtualService) ra, raOk := createdObj.(*securityv1beta1.RequestAuthentication) ap, apOk := createdObj.(*securityv1beta1.AuthorizationPolicy) @@ -335,7 +335,7 @@ var _ = Describe("Reconciliation", func() { Expect(len(http.Route)).To(Equal(1)) Expect(http.Route[0].Destination.Host).To(Equal("example-service.some-namespace.svc.cluster.local")) - switch http.Match[0].Uri.MatchType.(*v1beta1.StringMatch_Regex).Regex { + switch http.Match[0].Uri.MatchType.(*apinetworkingv1.StringMatch_Regex).Regex { case "/test": break case "/different-path": @@ -368,7 +368,7 @@ func getV2alpha1APIRuleFor(name, namespace string, rules []gatewayv2alpha1.Rule) serviceHost := gatewayv2alpha1.Host("myService.test.com") return &gatewayv2alpha1.APIRule{ - ObjectMeta: v1.ObjectMeta{ + ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, diff --git a/internal/processing/processors/v2alpha1/virtual_service_processor.go b/internal/processing/processors/v2alpha1/virtual_service_processor.go new file mode 100644 index 000000000..88e6a6180 --- /dev/null +++ b/internal/processing/processors/v2alpha1/virtual_service_processor.go @@ -0,0 +1,162 @@ +package v2alpha1 + +import ( + "context" + "fmt" + 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" + networkingv1 "istio.io/client-go/pkg/apis/networking/v1" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "time" +) + +// VirtualServiceProcessor is the generic processor that handles the Virtual Service in the reconciliation of API Rule. +type VirtualServiceProcessor struct { + ApiRule *gatewayv2alpha1.APIRule + Creator VirtualServiceCreator +} + +// VirtualServiceCreator provides the creation of a Virtual Service using the configuration in the given APIRule. +type VirtualServiceCreator interface { + Create(api *gatewayv2alpha1.APIRule) (*networkingv1.VirtualService, error) +} + +// EvaluateReconciliation evaluates the reconciliation of the Virtual Service for the given API Rule. +func (r VirtualServiceProcessor) EvaluateReconciliation(ctx context.Context, client ctrlclient.Client) ([]*processing.ObjectChange, error) { + desired, err := r.getDesiredState(r.ApiRule) + if err != nil { + return make([]*processing.ObjectChange, 0), err + } + + actual, err := r.getActualState(ctx, client, r.ApiRule) + if err != nil { + return make([]*processing.ObjectChange, 0), err + } + + changes := r.getObjectChanges(desired, actual) + + return []*processing.ObjectChange{changes}, nil +} + +func (r VirtualServiceProcessor) getDesiredState(api *gatewayv2alpha1.APIRule) (*networkingv1.VirtualService, error) { + return r.Creator.Create(api) +} + +func (r VirtualServiceProcessor) getActualState(ctx context.Context, client ctrlclient.Client, api *gatewayv2alpha1.APIRule) (*networkingv1.VirtualService, error) { + labels := getOwnerLabels(api) + + var vsList networkingv1.VirtualServiceList + if err := client.List(ctx, &vsList, ctrlclient.MatchingLabels(labels)); err != nil { + return nil, err + } + + if len(vsList.Items) >= 1 { + return vsList.Items[0], nil + } else { + return nil, nil + } +} + +func (r VirtualServiceProcessor) getObjectChanges(desired *networkingv1.VirtualService, actual *networkingv1.VirtualService) *processing.ObjectChange { + if actual != nil { + actual.Spec = *desired.Spec.DeepCopy() + return processing.NewObjectUpdateAction(actual) + } else { + return processing.NewObjectCreateAction(desired) + } +} + +// The owner labels are still set to the old APIRule version. +// Do not switch the owner labels to the new APIRule version unless absolutely necessary! +// This has been done before, and it caused a lot of confusion and bugs. +// If the change for some reason has to be done, please remove the version from the processing.OwnerLabel constant. +func getOwnerLabels(api *gatewayv2alpha1.APIRule) map[string]string { + return map[string]string{ + processing.OwnerLabel: fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace), + } +} + +type virtualServiceCreator struct { + defaultDomainName string +} + +// Create returns the Virtual Service using the configuration of the APIRule. +func (r virtualServiceCreator) Create(api *gatewayv2alpha1.APIRule) (*networkingv1.VirtualService, error) { + virtualServiceNamePrefix := fmt.Sprintf("%s-", api.ObjectMeta.Name) + + vsSpecBuilder := builders.VirtualServiceSpec() + for _, host := range api.Spec.Hosts { + vsSpecBuilder.Host(default_domain.GetHostWithDomain(string(*host), r.defaultDomainName)) + } + + vsSpecBuilder.Gateway(*api.Spec.Gateway) + filteredRules := filterDuplicatePaths(api.Spec.Rules) + + for _, rule := range filteredRules { + httpRouteBuilder := builders.HTTPRoute() + serviceNamespace := findServiceNamespace(api, &rule) + + var host string + var port uint32 + + // Use rule level service if it exists + if rule.Service != nil { + host = default_domain.GetHostLocalDomain(*rule.Service.Name, serviceNamespace) + port = *rule.Service.Port + } else { + // Otherwise use service defined on APIRule spec level + host = default_domain.GetHostLocalDomain(*api.Spec.Service.Name, serviceNamespace) + port = *api.Spec.Service.Port + } + + httpRouteBuilder.Route(builders.RouteDestination().Host(host).Port(port)) + + matchBuilder := builders.MatchRequest() + + if rule.Path == "/*" { + matchBuilder.Uri().Prefix("/") + } else { + matchBuilder.Uri().Regex(rule.Path) + } + + httpRouteBuilder.Match(matchBuilder) + + httpRouteBuilder.Timeout(time.Duration(getVirtualServiceHttpTimeout(api.Spec, rule)) * time.Second) + + headersBuilder := builders.NewHttpRouteHeadersBuilder(). + // For now, the X-Forwarded-Host header is set to the first host in the APIRule hosts list. + // This should be clarified how to resolve in the future. + SetHostHeader(default_domain.GetHostWithDomain(string(*api.Spec.Hosts[0]), r.defaultDomainName)) + + if api.Spec.CorsPolicy != nil { + httpRouteBuilder.CorsPolicy(builders.CorsPolicy().FromV2Alpha1ApiRuleCorsPolicy(*api.Spec.CorsPolicy)) + } + headersBuilder.RemoveUpstreamCORSPolicyHeaders() + + // Mutators go here + + httpRouteBuilder.Headers(headersBuilder.Get()) + + vsSpecBuilder.HTTP(httpRouteBuilder) + + } + + vsBuilder := builders.VirtualService(). + GenerateName(virtualServiceNamePrefix). + Namespace(api.ObjectMeta.Namespace). + Label(processing.OwnerLabel, fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace)) + + vsBuilder.Spec(vsSpecBuilder) + + return vsBuilder.Get(), nil +} + +func getVirtualServiceHttpTimeout(apiRuleSpec gatewayv2alpha1.APIRuleSpec, rule gatewayv2alpha1.Rule) uint32 { + if rule.Timeout != nil { + return uint32(*rule.Timeout) + } + + return uint32(*apiRuleSpec.Timeout) +} diff --git a/internal/processing/processors/v2alpha1/virtual_service_processor_test.go b/internal/processing/processors/v2alpha1/virtual_service_processor_test.go new file mode 100644 index 000000000..7d9af1a3c --- /dev/null +++ b/internal/processing/processors/v2alpha1/virtual_service_processor_test.go @@ -0,0 +1,38 @@ +package v2alpha1_test + +import ( + "context" + gatewayv2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" + "github.com/kyma-project/api-gateway/internal/builders" + processors "github.com/kyma-project/api-gateway/internal/processing/processors/v2alpha1" + networkingv1 "istio.io/client-go/pkg/apis/networking/v1" + + . "github.com/kyma-project/api-gateway/internal/processing/processing_test" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("VirtualServiceProcessor", func() { + It("should create virtual service when no virtual service exists", func() { + // given + processor := processors.VirtualServiceProcessor{ + ApiRule: &gatewayv2alpha1.APIRule{}, + Creator: mockVirtualServiceCreator{}, + } + + // when + result, err := processor.EvaluateReconciliation(context.Background(), GetFakeClient()) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + Expect(result[0].Action.String()).To(Equal("create")) + }) +}) + +type mockVirtualServiceCreator struct{} + +func (r mockVirtualServiceCreator) Create(_ *gatewayv2alpha1.APIRule) (*networkingv1.VirtualService, error) { + return builders.VirtualService().Get(), nil +} From ae13addc5a10ba1ecfc46da9a010d537a04795d1 Mon Sep 17 00:00:00 2001 From: Chwila Date: Wed, 10 Jul 2024 14:14:48 +0200 Subject: [PATCH 02/12] Revert to v1beta1 --- internal/builders/virtual_service.go | 88 +++++++++---------- internal/builders/virtual_service_test.go | 8 +- .../processors/v2alpha1/reconciliation.go | 4 +- .../v2alpha1/reconciliation_test.go | 22 ++--- .../v2alpha1/virtual_service_processor.go | 14 +-- .../virtual_service_processor_test.go | 4 +- 6 files changed, 70 insertions(+), 70 deletions(-) diff --git a/internal/builders/virtual_service.go b/internal/builders/virtual_service.go index edce78909..87bef9784 100644 --- a/internal/builders/virtual_service.go +++ b/internal/builders/virtual_service.go @@ -6,28 +6,28 @@ import ( apirulev2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/wrapperspb" - "istio.io/api/networking/v1" - networkingv1 "istio.io/client-go/pkg/apis/networking/v1" + "istio.io/api/networking/v1beta1" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" "strings" "time" ) -// VirtualService returns builder for istio.io/client-go/pkg/apis/networking/v1/VirtualService type +// VirtualService returns builder for istio.io/client-go/pkg/apis/networking/v1beta1/VirtualService type func VirtualService() *virtualService { return &virtualService{ - value: &networkingv1.VirtualService{}, + value: &networkingv1beta1.VirtualService{}, } } type virtualService struct { - value *networkingv1.VirtualService + value *networkingv1beta1.VirtualService } -func (vs *virtualService) Get() *networkingv1.VirtualService { +func (vs *virtualService) Get() *networkingv1beta1.VirtualService { return vs.value } -func (vs *virtualService) From(val *networkingv1.VirtualService) *virtualService { +func (vs *virtualService) From(val *networkingv1beta1.VirtualService) *virtualService { vs.value = val return vs } @@ -61,22 +61,22 @@ func (vs *virtualService) Spec(val *virtualServiceSpec) *virtualService { return vs } -// VirtualServiceSpec returns builder for istio.io/api/networking/v1/VirtualServiceSpec type +// VirtualServiceSpec returns builder for istio.io/api/networking/v1beta1/VirtualServiceSpec type func VirtualServiceSpec() *virtualServiceSpec { return &virtualServiceSpec{ - value: &v1.VirtualService{}, + value: &v1beta1.VirtualService{}, } } type virtualServiceSpec struct { - value *v1.VirtualService + value *v1beta1.VirtualService } -func (vss *virtualServiceSpec) Get() *v1.VirtualService { +func (vss *virtualServiceSpec) Get() *v1beta1.VirtualService { return vss.value } -func (vss *virtualServiceSpec) From(val *v1.VirtualService) *virtualServiceSpec { +func (vss *virtualServiceSpec) From(val *v1beta1.VirtualService) *virtualServiceSpec { vss.value = val return vss } @@ -96,18 +96,18 @@ func (vss *virtualServiceSpec) HTTP(hr *httpRoute) *virtualServiceSpec { return vss } -// HTTPRoute returns builder for istio.io/api/networking/v1/HTTPRoute type +// HTTPRoute returns builder for istio.io/api/networking/v1beta1/HTTPRoute type func HTTPRoute() *httpRoute { return &httpRoute{ - value: &v1.HTTPRoute{}, + value: &v1beta1.HTTPRoute{}, } } type httpRoute struct { - value *v1.HTTPRoute + value *v1beta1.HTTPRoute } -func (hr *httpRoute) Get() *v1.HTTPRoute { +func (hr *httpRoute) Get() *v1beta1.HTTPRoute { return hr.value } @@ -126,7 +126,7 @@ func (hr *httpRoute) CorsPolicy(cc *corsPolicy) *httpRoute { return hr } -func (hr *httpRoute) Headers(h *v1.Headers) *httpRoute { +func (hr *httpRoute) Headers(h *v1beta1.Headers) *httpRoute { hr.value.Headers = h return hr } @@ -136,23 +136,23 @@ func (hr *httpRoute) Timeout(value time.Duration) *httpRoute { return hr } -// MatchRequest returns builder for istio.io/api/networking/v1/HTTPMatchRequest type +// MatchRequest returns builder for istio.io/api/networking/v1beta1/HTTPMatchRequest type func MatchRequest() *matchRequest { return &matchRequest{ - value: &v1.HTTPMatchRequest{}, + value: &v1beta1.HTTPMatchRequest{}, } } type matchRequest struct { - value *v1.HTTPMatchRequest + value *v1beta1.HTTPMatchRequest } -func (mr *matchRequest) Get() *v1.HTTPMatchRequest { +func (mr *matchRequest) Get() *v1beta1.HTTPMatchRequest { return mr.value } func (mr *matchRequest) Uri() *stringMatch { - mr.value.Uri = &v1.StringMatch{} + mr.value.Uri = &v1beta1.StringMatch{} return &stringMatch{mr.value.Uri, func() *matchRequest { return mr }} } @@ -162,8 +162,8 @@ func (mr *matchRequest) MethodRegEx(httpMethods ...apirulev1beta1.HttpMethod) *m methodStrings := apirulev1beta1.ConvertHttpMethodsToStrings(httpMethods) methodsWithSeparator := strings.Join(methodStrings, "|") - mr.value.Method = &v1.StringMatch{ - MatchType: &v1.StringMatch_Regex{ + mr.value.Method = &v1beta1.StringMatch{ + MatchType: &v1beta1.StringMatch_Regex{ Regex: fmt.Sprintf("^(%s)$", methodsWithSeparator), }, } @@ -171,35 +171,35 @@ func (mr *matchRequest) MethodRegEx(httpMethods ...apirulev1beta1.HttpMethod) *m } type stringMatch struct { - value *v1.StringMatch + value *v1beta1.StringMatch parent func() *matchRequest } func (st *stringMatch) Regex(val string) *matchRequest { - st.value.MatchType = &v1.StringMatch_Regex{Regex: val} + st.value.MatchType = &v1beta1.StringMatch_Regex{Regex: val} return st.parent() } func (st *stringMatch) Prefix(val string) *matchRequest { - st.value.MatchType = &v1.StringMatch_Prefix{Prefix: val} + st.value.MatchType = &v1beta1.StringMatch_Prefix{Prefix: val} return st.parent() } -// RouteDestination returns builder for istio.io/api/networking/v1/HTTPRouteDestination type +// RouteDestination returns builder for istio.io/api/networking/v1beta1/HTTPRouteDestination type func RouteDestination() *routeDestination { - return &routeDestination{&v1.HTTPRouteDestination{ - Destination: &v1.Destination{ - Port: &v1.PortSelector{}, + return &routeDestination{&v1beta1.HTTPRouteDestination{ + Destination: &v1beta1.Destination{ + Port: &v1beta1.PortSelector{}, }, Weight: 100, }} } type routeDestination struct { - value *v1.HTTPRouteDestination + value *v1beta1.HTTPRouteDestination } -func (rd *routeDestination) Get() *v1.HTTPRouteDestination { +func (rd *routeDestination) Get() *v1beta1.HTTPRouteDestination { return rd.value } @@ -213,18 +213,18 @@ func (rd *routeDestination) Port(val uint32) *routeDestination { return rd } -// CorsPolicy returns builder for istio.io/api/networking/v1/CorsPolicy type +// CorsPolicy returns builder for istio.io/api/networking/v1beta1/CorsPolicy type func CorsPolicy() *corsPolicy { return &corsPolicy{ - value: &v1.CorsPolicy{}, + value: &v1beta1.CorsPolicy{}, } } type corsPolicy struct { - value *v1.CorsPolicy + value *v1beta1.CorsPolicy } -func (cp *corsPolicy) Get() *v1.CorsPolicy { +func (cp *corsPolicy) Get() *v1beta1.CorsPolicy { if cp.value.AllowOrigins == nil { return nil } @@ -249,7 +249,7 @@ func (cp *corsPolicy) AllowMethods(val ...string) *corsPolicy { return cp } -func (cp *corsPolicy) AllowOrigins(val ...*v1.StringMatch) *corsPolicy { +func (cp *corsPolicy) AllowOrigins(val ...*v1beta1.StringMatch) *corsPolicy { if len(val) == 0 { cp.value.AllowOrigins = nil } else { @@ -320,14 +320,14 @@ func (cp *corsPolicy) FromApiRuleCorsPolicy(corsPolicy apirulev1beta1.CorsPolicy return cp } -// NewHttpRouteHeadersBuilder returns builder for istio.io/api/networking/v1/Headers type +// NewHttpRouteHeadersBuilder returns builder for istio.io/api/networking/v1beta1/Headers type func NewHttpRouteHeadersBuilder() HttpRouteHeadersBuilder { return HttpRouteHeadersBuilder{ - value: &v1.Headers{ - Request: &v1.Headers_HeaderOperations{ + value: &v1beta1.Headers{ + Request: &v1beta1.Headers_HeaderOperations{ Set: make(map[string]string), }, - Response: &v1.Headers_HeaderOperations{ + Response: &v1beta1.Headers_HeaderOperations{ Set: make(map[string]string), }, }, @@ -335,10 +335,10 @@ func NewHttpRouteHeadersBuilder() HttpRouteHeadersBuilder { } type HttpRouteHeadersBuilder struct { - value *v1.Headers + value *v1beta1.Headers } -func (h HttpRouteHeadersBuilder) Get() *v1.Headers { +func (h HttpRouteHeadersBuilder) Get() *v1beta1.Headers { return h.value } diff --git a/internal/builders/virtual_service_test.go b/internal/builders/virtual_service_test.go index bb8434d3f..3a4398a77 100644 --- a/internal/builders/virtual_service_test.go +++ b/internal/builders/virtual_service_test.go @@ -6,8 +6,8 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "google.golang.org/protobuf/types/known/durationpb" - networkingv1 "istio.io/api/networking/v1" - networkingapiv1 "istio.io/client-go/pkg/apis/networking/v1" + v1beta12 "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" "regexp" @@ -28,7 +28,7 @@ var _ = Describe("Builder for", func() { name := "testName" namespace := "testNs" - initialVs := networkingapiv1.VirtualService{} + initialVs := networkingv1beta1.VirtualService{} initialVs.Name = "shoudBeOverwritten" initialVs.Spec.Hosts = []string{"a,", "b", "c"} @@ -162,7 +162,7 @@ var _ = Describe("Builder for", func() { })) Expect(result.Http[0].CorsPolicy.AllowOrigins).To(HaveLen(1)) - Expect(result.Http[0].CorsPolicy.AllowOrigins).To(ConsistOf(&networkingv1.StringMatch{MatchType: &networkingv1.StringMatch_Exact{Exact: "localhost"}})) + Expect(result.Http[0].CorsPolicy.AllowOrigins).To(ConsistOf(&v1beta12.StringMatch{MatchType: &v1beta12.StringMatch_Exact{Exact: "localhost"}})) Expect(result.Http[0].CorsPolicy.AllowCredentials).To(Not(BeNil())) Expect(result.Http[0].CorsPolicy.AllowCredentials.Value).To(BeTrue()) Expect(result.Http[0].CorsPolicy.AllowMethods).To(ConsistOf("GET", "POST")) diff --git a/internal/processing/processors/v2alpha1/reconciliation.go b/internal/processing/processors/v2alpha1/reconciliation.go index 8bf7fbc08..602080574 100644 --- a/internal/processing/processors/v2alpha1/reconciliation.go +++ b/internal/processing/processors/v2alpha1/reconciliation.go @@ -9,7 +9,7 @@ import ( "github.com/kyma-project/api-gateway/internal/processing/processors/istio" "github.com/kyma-project/api-gateway/internal/validation" istioValidation "github.com/kyma-project/api-gateway/internal/validation/v1beta1/istio" - networkingapiv1 "istio.io/client-go/pkg/apis/networking/v1" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -24,7 +24,7 @@ type Reconciliation struct { func (r Reconciliation) Validate(ctx context.Context, client client.Client) ([]validation.Failure, error) { - var vsList networkingapiv1.VirtualServiceList + var vsList networkingv1beta1.VirtualServiceList if err := client.List(ctx, &vsList); err != nil { return make([]validation.Failure, 0), err } diff --git a/internal/processing/processors/v2alpha1/reconciliation_test.go b/internal/processing/processors/v2alpha1/reconciliation_test.go index 6cf4b4c25..67c347dc9 100644 --- a/internal/processing/processors/v2alpha1/reconciliation_test.go +++ b/internal/processing/processors/v2alpha1/reconciliation_test.go @@ -6,9 +6,9 @@ import ( 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/processing/processors/v2alpha1" - apinetworkingv1 "istio.io/api/networking/v1" + "istio.io/api/networking/v1beta1" securityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/utils/ptr" "net/http" @@ -16,7 +16,7 @@ import ( . "github.com/kyma-project/api-gateway/internal/processing/processing_test" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - networkingv1 "istio.io/client-go/pkg/apis/networking/v1" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -55,7 +55,7 @@ var _ = Describe("Reconciliation", func() { // then Expect(createdObjects).To(HaveLen(1)) - Expect(createdObjects[0]).To(BeAssignableToTypeOf(&networkingv1.VirtualService{})) + Expect(createdObjects[0]).To(BeAssignableToTypeOf(&networkingv1beta1.VirtualService{})) }) It("with v1beta1 JWT and v2alpha1 JWT should provide VirtualService, AuthorizationPolicy and RequestAuthentication", func() { @@ -87,7 +87,7 @@ var _ = Describe("Reconciliation", func() { vsCreated, apCreated, raCreated := false, false, false for _, createdObj := range createdObjects { - vs, vsOk := createdObj.(*networkingv1.VirtualService) + vs, vsOk := createdObj.(*networkingv1beta1.VirtualService) ra, raOk := createdObj.(*securityv1beta1.RequestAuthentication) ap, apOk := createdObj.(*securityv1beta1.AuthorizationPolicy) @@ -179,7 +179,7 @@ var _ = Describe("Reconciliation", func() { vsCreated, raCreated, apCreated := false, false, false for _, createdObj := range createdObjects { - vs, vsOk := createdObj.(*networkingv1.VirtualService) + vs, vsOk := createdObj.(*networkingv1beta1.VirtualService) ra, raOk := createdObj.(*securityv1beta1.RequestAuthentication) ap, apOk := createdObj.(*securityv1beta1.AuthorizationPolicy) @@ -248,7 +248,7 @@ var _ = Describe("Reconciliation", func() { numberOfCreatedRequestAuthentications := 0 for _, createdObj := range createdObjects { - vs, vsOk := createdObj.(*networkingv1.VirtualService) + vs, vsOk := createdObj.(*networkingv1beta1.VirtualService) ra, raOk := createdObj.(*securityv1beta1.RequestAuthentication) ap, apOk := createdObj.(*securityv1beta1.AuthorizationPolicy) @@ -259,7 +259,7 @@ var _ = Describe("Reconciliation", func() { Expect(len(http.Route)).To(Equal(1)) Expect(http.Route[0].Destination.Host).To(Equal("example-service.some-namespace.svc.cluster.local")) - switch http.Match[0].Uri.MatchType.(*apinetworkingv1.StringMatch_Regex).Regex { + switch http.Match[0].Uri.MatchType.(*v1beta1.StringMatch_Regex).Regex { case "/test": break case "/different-path": @@ -323,7 +323,7 @@ var _ = Describe("Reconciliation", func() { vsCreated, raCreated, apCreated := false, false, false for _, createdObj := range createdObjects { - vs, vsOk := createdObj.(*networkingv1.VirtualService) + vs, vsOk := createdObj.(*networkingv1beta1.VirtualService) ra, raOk := createdObj.(*securityv1beta1.RequestAuthentication) ap, apOk := createdObj.(*securityv1beta1.AuthorizationPolicy) @@ -335,7 +335,7 @@ var _ = Describe("Reconciliation", func() { Expect(len(http.Route)).To(Equal(1)) Expect(http.Route[0].Destination.Host).To(Equal("example-service.some-namespace.svc.cluster.local")) - switch http.Match[0].Uri.MatchType.(*apinetworkingv1.StringMatch_Regex).Regex { + switch http.Match[0].Uri.MatchType.(*v1beta1.StringMatch_Regex).Regex { case "/test": break case "/different-path": @@ -368,7 +368,7 @@ func getV2alpha1APIRuleFor(name, namespace string, rules []gatewayv2alpha1.Rule) serviceHost := gatewayv2alpha1.Host("myService.test.com") return &gatewayv2alpha1.APIRule{ - ObjectMeta: metav1.ObjectMeta{ + ObjectMeta: v1.ObjectMeta{ Name: name, Namespace: namespace, }, diff --git a/internal/processing/processors/v2alpha1/virtual_service_processor.go b/internal/processing/processors/v2alpha1/virtual_service_processor.go index 88e6a6180..a8e4b6e63 100644 --- a/internal/processing/processors/v2alpha1/virtual_service_processor.go +++ b/internal/processing/processors/v2alpha1/virtual_service_processor.go @@ -7,7 +7,7 @@ import ( "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" - networkingv1 "istio.io/client-go/pkg/apis/networking/v1" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" "time" ) @@ -20,7 +20,7 @@ type VirtualServiceProcessor struct { // VirtualServiceCreator provides the creation of a Virtual Service using the configuration in the given APIRule. type VirtualServiceCreator interface { - Create(api *gatewayv2alpha1.APIRule) (*networkingv1.VirtualService, error) + Create(api *gatewayv2alpha1.APIRule) (*networkingv1beta1.VirtualService, error) } // EvaluateReconciliation evaluates the reconciliation of the Virtual Service for the given API Rule. @@ -40,14 +40,14 @@ func (r VirtualServiceProcessor) EvaluateReconciliation(ctx context.Context, cli return []*processing.ObjectChange{changes}, nil } -func (r VirtualServiceProcessor) getDesiredState(api *gatewayv2alpha1.APIRule) (*networkingv1.VirtualService, error) { +func (r VirtualServiceProcessor) getDesiredState(api *gatewayv2alpha1.APIRule) (*networkingv1beta1.VirtualService, error) { return r.Creator.Create(api) } -func (r VirtualServiceProcessor) getActualState(ctx context.Context, client ctrlclient.Client, api *gatewayv2alpha1.APIRule) (*networkingv1.VirtualService, error) { +func (r VirtualServiceProcessor) getActualState(ctx context.Context, client ctrlclient.Client, api *gatewayv2alpha1.APIRule) (*networkingv1beta1.VirtualService, error) { labels := getOwnerLabels(api) - var vsList networkingv1.VirtualServiceList + var vsList networkingv1beta1.VirtualServiceList if err := client.List(ctx, &vsList, ctrlclient.MatchingLabels(labels)); err != nil { return nil, err } @@ -59,7 +59,7 @@ func (r VirtualServiceProcessor) getActualState(ctx context.Context, client ctrl } } -func (r VirtualServiceProcessor) getObjectChanges(desired *networkingv1.VirtualService, actual *networkingv1.VirtualService) *processing.ObjectChange { +func (r VirtualServiceProcessor) getObjectChanges(desired *networkingv1beta1.VirtualService, actual *networkingv1beta1.VirtualService) *processing.ObjectChange { if actual != nil { actual.Spec = *desired.Spec.DeepCopy() return processing.NewObjectUpdateAction(actual) @@ -83,7 +83,7 @@ type virtualServiceCreator struct { } // Create returns the Virtual Service using the configuration of the APIRule. -func (r virtualServiceCreator) Create(api *gatewayv2alpha1.APIRule) (*networkingv1.VirtualService, error) { +func (r virtualServiceCreator) Create(api *gatewayv2alpha1.APIRule) (*networkingv1beta1.VirtualService, error) { virtualServiceNamePrefix := fmt.Sprintf("%s-", api.ObjectMeta.Name) vsSpecBuilder := builders.VirtualServiceSpec() diff --git a/internal/processing/processors/v2alpha1/virtual_service_processor_test.go b/internal/processing/processors/v2alpha1/virtual_service_processor_test.go index 7d9af1a3c..0bbf5a4f1 100644 --- a/internal/processing/processors/v2alpha1/virtual_service_processor_test.go +++ b/internal/processing/processors/v2alpha1/virtual_service_processor_test.go @@ -5,7 +5,7 @@ import ( gatewayv2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" "github.com/kyma-project/api-gateway/internal/builders" processors "github.com/kyma-project/api-gateway/internal/processing/processors/v2alpha1" - networkingv1 "istio.io/client-go/pkg/apis/networking/v1" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" . "github.com/kyma-project/api-gateway/internal/processing/processing_test" @@ -33,6 +33,6 @@ var _ = Describe("VirtualServiceProcessor", func() { type mockVirtualServiceCreator struct{} -func (r mockVirtualServiceCreator) Create(_ *gatewayv2alpha1.APIRule) (*networkingv1.VirtualService, error) { +func (r mockVirtualServiceCreator) Create(_ *gatewayv2alpha1.APIRule) (*networkingv1beta1.VirtualService, error) { return builders.VirtualService().Get(), nil } From 16df9385f7d3344e245ae91b609ddc774acaca0b Mon Sep 17 00:00:00 2001 From: Chwila Date: Thu, 11 Jul 2024 10:43:04 +0200 Subject: [PATCH 03/12] Tests --- .../processors/v2alpha1/test_builders_test.go | 191 ++++++++++++++++++ .../v2alpha1/virtual_service_processor.go | 22 +- .../virtual_service_processor_test.go | 135 ++++++++++++- 3 files changed, 342 insertions(+), 6 deletions(-) create mode 100644 internal/processing/processors/v2alpha1/test_builders_test.go diff --git a/internal/processing/processors/v2alpha1/test_builders_test.go b/internal/processing/processors/v2alpha1/test_builders_test.go new file mode 100644 index 000000000..5dde91138 --- /dev/null +++ b/internal/processing/processors/v2alpha1/test_builders_test.go @@ -0,0 +1,191 @@ +package v2alpha1_test + +import ( + gatewayv2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" + "k8s.io/utils/ptr" + "net/http" +) + +type ruleBuilder struct { + rule *gatewayv2alpha1.Rule +} + +func (r *ruleBuilder) WithPath(path string) *ruleBuilder { + r.rule.Path = path + return r +} + +func (r *ruleBuilder) WithTimeout(timeout uint32) *ruleBuilder { + r.rule.Timeout = ptr.To(gatewayv2alpha1.Timeout(timeout)) + return r +} + +func (r *ruleBuilder) WithService(name, namespace string, port uint32) *ruleBuilder { + r.rule.Service = &gatewayv2alpha1.Service{ + Name: &name, + Namespace: &namespace, + Port: &port, + } + return r +} + +func (r *ruleBuilder) WithMethods(methods ...gatewayv2alpha1.HttpMethod) *ruleBuilder { + r.rule.Methods = methods + return r +} + +func (r *ruleBuilder) NoAuth() *ruleBuilder { + r.rule.NoAuth = ptr.To(true) + return r +} + +func (r *ruleBuilder) WithJWTAuthn(issuer, jwksUri string, fromHeaders []*gatewayv2alpha1.JwtHeader, fromParams []string) *ruleBuilder { + if r.rule.Jwt == nil { + r.rule.Jwt = &gatewayv2alpha1.JwtConfig{} + } + r.rule.Jwt.Authentications = append(r.rule.Jwt.Authentications, &gatewayv2alpha1.JwtAuthentication{ + Issuer: issuer, + JwksUri: jwksUri, + FromHeaders: fromHeaders, + FromParams: fromParams, + }) + + return r +} + +func (r *ruleBuilder) WithJWTAuthz(requiredScopes []string, audiences []string) *ruleBuilder { + if r.rule.Jwt == nil { + r.rule.Jwt = &gatewayv2alpha1.JwtConfig{} + } + + r.rule.Jwt.Authorizations = append(r.rule.Jwt.Authorizations, &gatewayv2alpha1.JwtAuthorization{ + RequiredScopes: requiredScopes, + Audiences: audiences, + }) + + return r +} + +func newRuleBuilder() *ruleBuilder { + return &ruleBuilder{ + rule: &gatewayv2alpha1.Rule{}, + } +} + +func (r *ruleBuilder) Build() *gatewayv2alpha1.Rule { + return r.rule +} + +type apiRuleBuilder struct { + apiRule *gatewayv2alpha1.APIRule +} + +func (a *apiRuleBuilder) WithHost(host string) *apiRuleBuilder { + a.apiRule.Spec.Hosts = append(a.apiRule.Spec.Hosts, ptr.To(gatewayv2alpha1.Host(host))) + return a +} + +func (a *apiRuleBuilder) WithHosts(hosts ...string) *apiRuleBuilder { + for _, host := range hosts { + a.WithHost(host) + } + return a +} + +func (a *apiRuleBuilder) WithService(name, namespace string, port uint32) *apiRuleBuilder { + a.apiRule.Spec.Service = &gatewayv2alpha1.Service{ + Name: &name, + Namespace: &namespace, + Port: &port, + } + return a +} + +func (a *apiRuleBuilder) WithGateway(gateway string) *apiRuleBuilder { + a.apiRule.Spec.Gateway = ptr.To(gateway) + return a +} + +func (a *apiRuleBuilder) WithCORSPolicy(policy gatewayv2alpha1.CorsPolicy) *apiRuleBuilder { + a.apiRule.Spec.CorsPolicy = &policy + return a +} + +func (a *apiRuleBuilder) WithTimeout(timeout uint32) *apiRuleBuilder { + a.apiRule.Spec.Timeout = ptr.To(gatewayv2alpha1.Timeout(timeout)) + return a +} + +func (a *apiRuleBuilder) WithRule(rule gatewayv2alpha1.Rule) *apiRuleBuilder { + a.apiRule.Spec.Rules = append(a.apiRule.Spec.Rules, rule) + return a +} + +func (a *apiRuleBuilder) WithRules(rules ...*gatewayv2alpha1.Rule) *apiRuleBuilder { + for _, rule := range rules { + a.WithRule(*rule) + } + return a +} + +func (a *apiRuleBuilder) Build() *gatewayv2alpha1.APIRule { + return a.apiRule +} + +func newAPIRuleBuilder() *apiRuleBuilder { + return &apiRuleBuilder{ + apiRule: &gatewayv2alpha1.APIRule{}, + } +} + +func newAPIRuleBuilderWithDummyData() *apiRuleBuilder { + return newAPIRuleBuilder(). + WithHost("dummy-host.dummy.com"). + WithGateway("dummy-namespace/dummy-gateway"). + WithService("dummy-service", "dummy-namespace", 8080). + WithRule(*newRuleBuilder().WithMethods(http.MethodGet).NoAuth().Build()) +} + +type corsPolicyBuilder struct { + policy gatewayv2alpha1.CorsPolicy +} + +func (c *corsPolicyBuilder) WithAllowOrigins(origins []map[string]string) *corsPolicyBuilder { + c.policy.AllowOrigins = origins + return c +} + +func (c *corsPolicyBuilder) WithAllowMethods(methods []string) *corsPolicyBuilder { + c.policy.AllowMethods = methods + return c +} + +func (c *corsPolicyBuilder) WithAllowHeaders(headers []string) *corsPolicyBuilder { + c.policy.AllowHeaders = headers + return c +} + +func (c *corsPolicyBuilder) WithExposeHeaders(headers []string) *corsPolicyBuilder { + c.policy.ExposeHeaders = headers + return c +} + +func (c *corsPolicyBuilder) WithMaxAge(maxAge uint64) *corsPolicyBuilder { + c.policy.MaxAge = &maxAge + return c +} + +func (c *corsPolicyBuilder) WithAllowCredentials(allow bool) *corsPolicyBuilder { + c.policy.AllowCredentials = &allow + return c +} + +func newCorsPolicyBuilder() *corsPolicyBuilder { + return &corsPolicyBuilder{ + policy: gatewayv2alpha1.CorsPolicy{}, + } +} + +func (c *corsPolicyBuilder) Build() gatewayv2alpha1.CorsPolicy { + return c.policy +} diff --git a/internal/processing/processors/v2alpha1/virtual_service_processor.go b/internal/processing/processors/v2alpha1/virtual_service_processor.go index a8e4b6e63..635422551 100644 --- a/internal/processing/processors/v2alpha1/virtual_service_processor.go +++ b/internal/processing/processors/v2alpha1/virtual_service_processor.go @@ -12,6 +12,17 @@ import ( "time" ) +const defaultHttpTimeout uint32 = 180 + +func NewVirtualServiceProcessor(config processing.ReconciliationConfig, apiRule *gatewayv2alpha1.APIRule) VirtualServiceProcessor { + return VirtualServiceProcessor{ + ApiRule: apiRule, + Creator: virtualServiceCreator{ + defaultDomainName: config.DefaultDomainName, + }, + } +} + // VirtualServiceProcessor is the generic processor that handles the Virtual Service in the reconciliation of API Rule. type VirtualServiceProcessor struct { ApiRule *gatewayv2alpha1.APIRule @@ -106,7 +117,7 @@ func (r virtualServiceCreator) Create(api *gatewayv2alpha1.APIRule) (*networking host = default_domain.GetHostLocalDomain(*rule.Service.Name, serviceNamespace) port = *rule.Service.Port } else { - // Otherwise use service defined on APIRule spec level + // Otherwise, use service defined on APIRule spec level host = default_domain.GetHostLocalDomain(*api.Spec.Service.Name, serviceNamespace) port = *api.Spec.Service.Port } @@ -123,7 +134,7 @@ func (r virtualServiceCreator) Create(api *gatewayv2alpha1.APIRule) (*networking httpRouteBuilder.Match(matchBuilder) - httpRouteBuilder.Timeout(time.Duration(getVirtualServiceHttpTimeout(api.Spec, rule)) * time.Second) + httpRouteBuilder.Timeout(time.Duration(GetVirtualServiceHttpTimeout(api.Spec, rule)) * time.Second) headersBuilder := builders.NewHttpRouteHeadersBuilder(). // For now, the X-Forwarded-Host header is set to the first host in the APIRule hosts list. @@ -153,10 +164,13 @@ func (r virtualServiceCreator) Create(api *gatewayv2alpha1.APIRule) (*networking return vsBuilder.Get(), nil } -func getVirtualServiceHttpTimeout(apiRuleSpec gatewayv2alpha1.APIRuleSpec, rule gatewayv2alpha1.Rule) uint32 { +func GetVirtualServiceHttpTimeout(apiRuleSpec gatewayv2alpha1.APIRuleSpec, rule gatewayv2alpha1.Rule) uint32 { if rule.Timeout != nil { return uint32(*rule.Timeout) } - return uint32(*apiRuleSpec.Timeout) + if apiRuleSpec.Timeout != nil { + return uint32(*apiRuleSpec.Timeout) + } + return defaultHttpTimeout } diff --git a/internal/processing/processors/v2alpha1/virtual_service_processor_test.go b/internal/processing/processors/v2alpha1/virtual_service_processor_test.go index 0bbf5a4f1..e51174a89 100644 --- a/internal/processing/processors/v2alpha1/virtual_service_processor_test.go +++ b/internal/processing/processors/v2alpha1/virtual_service_processor_test.go @@ -4,13 +4,16 @@ 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/processing/processing_test" processors "github.com/kyma-project/api-gateway/internal/processing/processors/v2alpha1" + istioapiv1beta1 "istio.io/api/networking/v1beta1" networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" - - . "github.com/kyma-project/api-gateway/internal/processing/processing_test" + "k8s.io/utils/ptr" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + "sigs.k8s.io/controller-runtime/pkg/client" ) var _ = Describe("VirtualServiceProcessor", func() { @@ -31,6 +34,134 @@ var _ = Describe("VirtualServiceProcessor", func() { }) }) +func checkVirtualServices(c client.Client, processor processors.VirtualServiceProcessor, verifiers []verifier, expectedActions ...string) { + result, err := processor.EvaluateReconciliation(context.Background(), c) + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(len(expectedActions))) + for i, action := range expectedActions { + Expect(result[i].Action.String()).To(Equal(action)) + } + + for i, v := range verifiers { + v(result[i].Obj.(*networkingv1beta1.VirtualService)) + } +} + +type verifier func(*networkingv1beta1.VirtualService) + +var _ = Describe("CORS", func() { + var client client.Client + var processor processors.VirtualServiceProcessor + BeforeEach(func() { + client = GetFakeClient() + }) + + DescribeTable("CORS", + func(apiRule *gatewayv2alpha1.APIRule, verifiers []verifier, expectedActions ...string) { + processor = processors.NewVirtualServiceProcessor(GetTestConfig(), apiRule) + checkVirtualServices(client, processor, verifiers, expectedActions...) + }, + + Entry("should set default empty values in VirtualService CORSPolicy when no CORS configuration is set in APIRule", + newAPIRuleBuilderWithDummyData().Build(), + []verifier{ + func(vs *networkingv1beta1.VirtualService) { + Expect(vs.Spec.Http[0].CorsPolicy).To(BeNil()) + + Expect(vs.Spec.Http[0].Headers.Response.Remove).To(ConsistOf([]string{ + builders.ExposeHeadersName, + builders.MaxAgeName, + builders.AllowHeadersName, + builders.AllowCredentialsName, + builders.AllowMethodsName, + builders.AllowOriginName, + })) + }, + }, "create"), + + Entry("should set CORS configuration in VirtualService CORSPolicy when CORS configuration is set in APIRule", + newAPIRuleBuilderWithDummyData().WithCORSPolicy( + newCorsPolicyBuilder(). + WithAllowOrigins([]map[string]string{{"exact": "dummy.com"}}). + Build()). + Build(), + []verifier{ + func(vs *networkingv1beta1.VirtualService) { + Expect(vs.Spec.Http[0].CorsPolicy).NotTo(BeNil()) + Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(HaveLen(1)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins[0]).To(Equal(&istioapiv1beta1.StringMatch{MatchType: &istioapiv1beta1.StringMatch_Exact{Exact: "dummy.com"}})) + + Expect(vs.Spec.Http[0].Headers.Response.Remove).To(ConsistOf([]string{ + builders.ExposeHeadersName, + builders.MaxAgeName, + builders.AllowHeadersName, + builders.AllowCredentialsName, + builders.AllowMethodsName, + builders.AllowOriginName, + })) + }, + }, "create"), + ) +}) + +var _ = Describe("GetVirtualServiceHttpTimeout", func() { + It("should return default of 180s when no timeout is set", func() { + // given + apiRuleSpec := gatewayv2alpha1.APIRuleSpec{} + rule := gatewayv2alpha1.Rule{} + + // when + timeout := processors.GetVirtualServiceHttpTimeout(apiRuleSpec, rule) + + // then + Expect(timeout).To(Equal(uint32(180))) + }) + + It("should return the timeout set in the rule when it is set and APIRule has different value", func() { + // given + apiRuleSpec := gatewayv2alpha1.APIRuleSpec{ + Timeout: ptr.To(gatewayv2alpha1.Timeout(20)), + } + rule := gatewayv2alpha1.Rule{ + Timeout: ptr.To(gatewayv2alpha1.Timeout(10)), + } + + // when + timeout := processors.GetVirtualServiceHttpTimeout(apiRuleSpec, rule) + + // then + Expect(timeout).To(Equal(uint32(10))) + }) + + It("should return the timeout set in the rule when it is set and APIRule timeout is not", func() { + // given + apiRuleSpec := gatewayv2alpha1.APIRuleSpec{} + rule := gatewayv2alpha1.Rule{ + Timeout: ptr.To(gatewayv2alpha1.Timeout(10)), + } + + // when + timeout := processors.GetVirtualServiceHttpTimeout(apiRuleSpec, rule) + + // then + Expect(timeout).To(Equal(uint32(10))) + }) + + It("should return the timeout set in the APIRule it is set and rule timeout is not", func() { + // given + apiRuleSpec := gatewayv2alpha1.APIRuleSpec{ + Timeout: ptr.To(gatewayv2alpha1.Timeout(20)), + } + rule := gatewayv2alpha1.Rule{} + + // when + timeout := processors.GetVirtualServiceHttpTimeout(apiRuleSpec, rule) + + // then + Expect(timeout).To(Equal(uint32(10))) + }) +}) + type mockVirtualServiceCreator struct{} func (r mockVirtualServiceCreator) Create(_ *gatewayv2alpha1.APIRule) (*networkingv1beta1.VirtualService, error) { From ccbb0ec3d9675ed2170c93b40ade9ccc54e1cdfc Mon Sep 17 00:00:00 2001 From: Chwila Date: Thu, 11 Jul 2024 11:08:46 +0200 Subject: [PATCH 04/12] Tests --- .../processors/v2alpha1/test_builders_test.go | 19 ++++++-- .../v2alpha1/virtual_service_processor.go | 3 +- .../virtual_service_processor_test.go | 44 +++++++++++-------- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/internal/processing/processors/v2alpha1/test_builders_test.go b/internal/processing/processors/v2alpha1/test_builders_test.go index 5dde91138..2ccf3e3a0 100644 --- a/internal/processing/processors/v2alpha1/test_builders_test.go +++ b/internal/processing/processors/v2alpha1/test_builders_test.go @@ -138,12 +138,23 @@ func newAPIRuleBuilder() *apiRuleBuilder { } } +// newAPIRuleBuilderWithDummyData returns an APIRuleBuilder pre-filled with placeholder data: +// +// Host: example-host.example.com +// +// Gateway: example-namespace/example-gateway +// +// Service: example-namespace/example-service:8080 +// +// Rule: GET / +// +// Strategy: NoAuth func newAPIRuleBuilderWithDummyData() *apiRuleBuilder { return newAPIRuleBuilder(). - WithHost("dummy-host.dummy.com"). - WithGateway("dummy-namespace/dummy-gateway"). - WithService("dummy-service", "dummy-namespace", 8080). - WithRule(*newRuleBuilder().WithMethods(http.MethodGet).NoAuth().Build()) + WithHost("example-host.example.com"). + WithGateway("example-namespace/example-gateway"). + WithService("example-service", "example-namespace", 8080). + WithRule(*newRuleBuilder().WithMethods(http.MethodGet).WithPath("/").NoAuth().Build()) } type corsPolicyBuilder struct { diff --git a/internal/processing/processors/v2alpha1/virtual_service_processor.go b/internal/processing/processors/v2alpha1/virtual_service_processor.go index 635422551..a781e5498 100644 --- a/internal/processing/processors/v2alpha1/virtual_service_processor.go +++ b/internal/processing/processors/v2alpha1/virtual_service_processor.go @@ -138,7 +138,8 @@ func (r virtualServiceCreator) Create(api *gatewayv2alpha1.APIRule) (*networking headersBuilder := builders.NewHttpRouteHeadersBuilder(). // For now, the X-Forwarded-Host header is set to the first host in the APIRule hosts list. - // This should be clarified how to resolve in the future. + // The status of this header is still under discussion in the following GitHub issue: + // https://github.com/kyma-project/api-gateway/issues/1159 SetHostHeader(default_domain.GetHostWithDomain(string(*api.Spec.Hosts[0]), r.defaultDomainName)) if api.Spec.CorsPolicy != nil { diff --git a/internal/processing/processors/v2alpha1/virtual_service_processor_test.go b/internal/processing/processors/v2alpha1/virtual_service_processor_test.go index e51174a89..7e55d4543 100644 --- a/internal/processing/processors/v2alpha1/virtual_service_processor_test.go +++ b/internal/processing/processors/v2alpha1/virtual_service_processor_test.go @@ -79,28 +79,36 @@ var _ = Describe("CORS", func() { }, }, "create"), - Entry("should set CORS configuration in VirtualService CORSPolicy when CORS configuration is set in APIRule", + Entry("should apply all CORSPolicy headers correctly", newAPIRuleBuilderWithDummyData().WithCORSPolicy( newCorsPolicyBuilder(). - WithAllowOrigins([]map[string]string{{"exact": "dummy.com"}}). + WithAllowOrigins([]map[string]string{{"exact": "example.com"}}). + WithAllowMethods([]string{"GET", "POST"}). + WithAllowHeaders([]string{"header1", "header2"}). + WithExposeHeaders([]string{"header3", "header4"}). + WithAllowCredentials(true). + WithMaxAge(600). Build()). Build(), - []verifier{ - func(vs *networkingv1beta1.VirtualService) { - Expect(vs.Spec.Http[0].CorsPolicy).NotTo(BeNil()) - Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(HaveLen(1)) - Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins[0]).To(Equal(&istioapiv1beta1.StringMatch{MatchType: &istioapiv1beta1.StringMatch_Exact{Exact: "dummy.com"}})) - - Expect(vs.Spec.Http[0].Headers.Response.Remove).To(ConsistOf([]string{ - builders.ExposeHeadersName, - builders.MaxAgeName, - builders.AllowHeadersName, - builders.AllowCredentialsName, - builders.AllowMethodsName, - builders.AllowOriginName, - })) - }, - }, "create"), + []verifier{func(vs *networkingv1beta1.VirtualService) { + Expect(vs.Spec.Http[0].CorsPolicy).NotTo(BeNil()) + Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(HaveLen(1)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins[0]).To(Equal(&istioapiv1beta1.StringMatch{MatchType: &istioapiv1beta1.StringMatch_Exact{Exact: "example.com"}})) + Expect(vs.Spec.Http[0].CorsPolicy.AllowMethods).To(ConsistOf("GET", "POST")) + Expect(vs.Spec.Http[0].CorsPolicy.AllowHeaders).To(ConsistOf("header1", "header2")) + Expect(vs.Spec.Http[0].CorsPolicy.ExposeHeaders).To(ConsistOf("header3", "header4")) + Expect(vs.Spec.Http[0].CorsPolicy.AllowCredentials.GetValue()).To(BeTrue()) + Expect(vs.Spec.Http[0].CorsPolicy.MaxAge.Seconds).To(Equal(int64(600))) + + Expect(vs.Spec.Http[0].Headers.Response.Remove).To(ConsistOf([]string{ + builders.ExposeHeadersName, + builders.MaxAgeName, + builders.AllowHeadersName, + builders.AllowCredentialsName, + builders.AllowMethodsName, + builders.AllowOriginName, + })) + }}, "create"), ) }) From 9600e31835b6e89d6a46da04c57373c42ed5fb5d Mon Sep 17 00:00:00 2001 From: Chwila Date: Thu, 11 Jul 2024 12:00:13 +0200 Subject: [PATCH 05/12] Add test for multiple hosts --- .../v2alpha1/virtual_service_processor.go | 2 -- .../virtual_service_processor_test.go | 31 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/internal/processing/processors/v2alpha1/virtual_service_processor.go b/internal/processing/processors/v2alpha1/virtual_service_processor.go index a781e5498..0a7ace005 100644 --- a/internal/processing/processors/v2alpha1/virtual_service_processor.go +++ b/internal/processing/processors/v2alpha1/virtual_service_processor.go @@ -147,8 +147,6 @@ func (r virtualServiceCreator) Create(api *gatewayv2alpha1.APIRule) (*networking } headersBuilder.RemoveUpstreamCORSPolicyHeaders() - // Mutators go here - httpRouteBuilder.Headers(headersBuilder.Get()) vsSpecBuilder.HTTP(httpRouteBuilder) diff --git a/internal/processing/processors/v2alpha1/virtual_service_processor_test.go b/internal/processing/processors/v2alpha1/virtual_service_processor_test.go index 7e55d4543..159ffec83 100644 --- a/internal/processing/processors/v2alpha1/virtual_service_processor_test.go +++ b/internal/processing/processors/v2alpha1/virtual_service_processor_test.go @@ -49,6 +49,37 @@ func checkVirtualServices(c client.Client, processor processors.VirtualServicePr type verifier func(*networkingv1beta1.VirtualService) +var _ = Describe("Hosts", func() { + var client client.Client + var processor processors.VirtualServiceProcessor + BeforeEach(func() { + client = GetFakeClient() + }) + + DescribeTable("Hosts", + func(apiRule *gatewayv2alpha1.APIRule, verifiers []verifier, expectedActions ...string) { + processor = processors.NewVirtualServiceProcessor(GetTestConfig(), apiRule) + checkVirtualServices(client, processor, verifiers, expectedActions...) + }, + + Entry("should set the host correctly", + newAPIRuleBuilder().WithGateway("example/example").WithHost("example.com").Build(), + []verifier{ + func(vs *networkingv1beta1.VirtualService) { + Expect(vs.Spec.Hosts).To(ConsistOf("example.com")) + }, + }, "create"), + + Entry("should set multiple hosts correctly", + newAPIRuleBuilder().WithGateway("example/example").WithHosts("example.com", "goat.com").Build(), + []verifier{ + func(vs *networkingv1beta1.VirtualService) { + Expect(vs.Spec.Hosts).To(ConsistOf("example.com", "goat.com")) + }, + }, "create"), + ) +}) + var _ = Describe("CORS", func() { var client client.Client var processor processors.VirtualServiceProcessor From 94e4eb0f1fba8b7154f0ea97ff00ef3b854b19c3 Mon Sep 17 00:00:00 2001 From: Chwila Date: Thu, 11 Jul 2024 13:21:23 +0200 Subject: [PATCH 06/12] Update API --- apis/gateway/v2alpha1/apirule_types.go | 3 +-- config/crd/bases/gateway.kyma-project.io_apirules.yaml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/apis/gateway/v2alpha1/apirule_types.go b/apis/gateway/v2alpha1/apirule_types.go index 4a3e6e12d..2f7f8a682 100644 --- a/apis/gateway/v2alpha1/apirule_types.go +++ b/apis/gateway/v2alpha1/apirule_types.go @@ -36,9 +36,8 @@ const ( type APIRuleSpec struct { // Specifies the URLs of the exposed service. // +kubebuilder:validation:MinItems=1 - // +kubebuilder:validation:MaxItems=1 Hosts []*Host `json:"hosts"` - // Describes the service to expose. + // Describes the service to expose.. // +optional Service *Service `json:"service,omitempty"` // Specifies the Istio Gateway to be used. diff --git a/config/crd/bases/gateway.kyma-project.io_apirules.yaml b/config/crd/bases/gateway.kyma-project.io_apirules.yaml index 94d79f386..b7b826374 100644 --- a/config/crd/bases/gateway.kyma-project.io_apirules.yaml +++ b/config/crd/bases/gateway.kyma-project.io_apirules.yaml @@ -368,7 +368,6 @@ spec: minLength: 3 pattern: ^([a-zA-Z0-9][a-zA-Z0-9-_]*\.)*[a-zA-Z0-9]*[a-zA-Z0-9-_]*[[a-zA-Z0-9]+$ type: string - maxItems: 1 minItems: 1 type: array rules: @@ -501,7 +500,7 @@ spec: minItems: 1 type: array service: - description: Describes the service to expose. + description: Describes the service to expose.. properties: external: description: Specifies if the service is internal (in cluster) From 5e5d4ca9333a41dd5c77683069872c0602b56498 Mon Sep 17 00:00:00 2001 From: Chwila Date: Thu, 11 Jul 2024 13:49:24 +0200 Subject: [PATCH 07/12] Revert "Update API" This reverts commit 94e4eb0f1fba8b7154f0ea97ff00ef3b854b19c3. --- apis/gateway/v2alpha1/apirule_types.go | 3 ++- config/crd/bases/gateway.kyma-project.io_apirules.yaml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apis/gateway/v2alpha1/apirule_types.go b/apis/gateway/v2alpha1/apirule_types.go index 2f7f8a682..4a3e6e12d 100644 --- a/apis/gateway/v2alpha1/apirule_types.go +++ b/apis/gateway/v2alpha1/apirule_types.go @@ -36,8 +36,9 @@ const ( type APIRuleSpec struct { // Specifies the URLs of the exposed service. // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=1 Hosts []*Host `json:"hosts"` - // Describes the service to expose.. + // Describes the service to expose. // +optional Service *Service `json:"service,omitempty"` // Specifies the Istio Gateway to be used. diff --git a/config/crd/bases/gateway.kyma-project.io_apirules.yaml b/config/crd/bases/gateway.kyma-project.io_apirules.yaml index b7b826374..94d79f386 100644 --- a/config/crd/bases/gateway.kyma-project.io_apirules.yaml +++ b/config/crd/bases/gateway.kyma-project.io_apirules.yaml @@ -368,6 +368,7 @@ spec: minLength: 3 pattern: ^([a-zA-Z0-9][a-zA-Z0-9-_]*\.)*[a-zA-Z0-9]*[a-zA-Z0-9-_]*[[a-zA-Z0-9]+$ type: string + maxItems: 1 minItems: 1 type: array rules: @@ -500,7 +501,7 @@ spec: minItems: 1 type: array service: - description: Describes the service to expose.. + description: Describes the service to expose. properties: external: description: Specifies if the service is internal (in cluster) From e61792e3679164b4512ac756cac0f1d89218582f Mon Sep 17 00:00:00 2001 From: Chwila Date: Fri, 12 Jul 2024 12:10:45 +0200 Subject: [PATCH 08/12] review changes --- apis/gateway/v2alpha1/rule.go | 9 ++++++ .../api_controller_integration_test.go | 10 +++---- internal/builders/virtual_service.go | 16 ++++++++++- internal/builders/virtual_service_test.go | 8 +++--- .../istio/virtual_service_processor.go | 2 +- .../ory/virtual_service_processor.go | 2 +- .../processors/v2alpha1/reconciliation.go | 28 +++++++++++++------ .../v2alpha1/virtual_service_processor.go | 4 +-- 8 files changed, 56 insertions(+), 23 deletions(-) diff --git a/apis/gateway/v2alpha1/rule.go b/apis/gateway/v2alpha1/rule.go index 2aff62a43..e73adb815 100644 --- a/apis/gateway/v2alpha1/rule.go +++ b/apis/gateway/v2alpha1/rule.go @@ -7,3 +7,12 @@ func (r *Rule) ContainsAccessStrategyJwt() bool { func (r *Rule) ContainsNoAuth() bool { return r.NoAuth != nil } + +func ConvertHttpMethodsToStrings(methods []HttpMethod) []string { + strings := make([]string, len(methods)) + for i, method := range methods { + strings[i] = string(method) + } + + return strings +} diff --git a/controllers/gateway/api_controller_integration_test.go b/controllers/gateway/api_controller_integration_test.go index f726aaaef..c32a464d4 100644 --- a/controllers/gateway/api_controller_integration_test.go +++ b/controllers/gateway/api_controller_integration_test.go @@ -493,7 +493,7 @@ var _ = Describe("APIRule Controller", Serial, func() { g.Expect(len(vs.Name) > len(apiRuleName)).To(BeTrue()) expectedSpec := builders.VirtualServiceSpec(). - Host(serviceHost). + AddHost(serviceHost). Gateway(testGatewayURL). HTTP(builders.HTTPRoute(). Match(builders.MatchRequest().Uri().Regex(testPath)). @@ -591,7 +591,7 @@ var _ = Describe("APIRule Controller", Serial, func() { vs := vsList.Items[0] expectedSpec := builders.VirtualServiceSpec(). - Host(serviceHost). + AddHost(serviceHost). Gateway(testGatewayURL). HTTP(builders.HTTPRoute(). Match(builders.MatchRequest().Uri().Regex("/img")). @@ -733,7 +733,7 @@ var _ = Describe("APIRule Controller", Serial, func() { vs := vsList.Items[0] expectedSpec := builders.VirtualServiceSpec(). - Host(serviceHost). + AddHost(serviceHost). Gateway(testGatewayURL). HTTP(builders.HTTPRoute(). Match(builders.MatchRequest().Uri().Regex("/img").MethodRegEx(http.MethodGet)). @@ -1042,7 +1042,7 @@ var _ = Describe("APIRule Controller", Serial, func() { vs := vsList.Items[0] expectedSpec := builders.VirtualServiceSpec(). - Host(serviceHost). + AddHost(serviceHost). Gateway(testGatewayURL). HTTP(builders.HTTPRoute(). Match(builders.MatchRequest().Uri().Regex("/img")). @@ -1174,7 +1174,7 @@ var _ = Describe("APIRule Controller", Serial, func() { vs := vsList.Items[0] expectedSpec := builders.VirtualServiceSpec(). - Host(serviceHost). + AddHost(serviceHost). Gateway(testGatewayURL). HTTP(builders.HTTPRoute(). Match(builders.MatchRequest().Uri().Regex("/favicon")). diff --git a/internal/builders/virtual_service.go b/internal/builders/virtual_service.go index 87bef9784..b02b5d44a 100644 --- a/internal/builders/virtual_service.go +++ b/internal/builders/virtual_service.go @@ -81,7 +81,7 @@ func (vss *virtualServiceSpec) From(val *v1beta1.VirtualService) *virtualService return vss } -func (vss *virtualServiceSpec) Host(val string) *virtualServiceSpec { +func (vss *virtualServiceSpec) AddHost(val string) *virtualServiceSpec { vss.value.Hosts = append(vss.value.Hosts, val) return vss } @@ -156,6 +156,20 @@ func (mr *matchRequest) Uri() *stringMatch { return &stringMatch{mr.value.Uri, func() *matchRequest { return mr }} } +// MethodRegExV2Alpha1 sets the HTTP method regex in the HTTPMatchRequest for the given HTTP methods in the format "^(PUT|POST|GET)$". +func (mr *matchRequest) MethodRegExV2Alpha1(httpMethods ...apirulev2alpha1.HttpMethod) *matchRequest { + methodStrings := apirulev2alpha1.ConvertHttpMethodsToStrings(httpMethods) + methodsWithSeparator := strings.Join(methodStrings, "|") + + mr.value.Method = &v1beta1.StringMatch{ + MatchType: &v1beta1.StringMatch_Regex{ + Regex: fmt.Sprintf("^(%s)$", methodsWithSeparator), + }, + } + + return mr +} + // MethodRegEx sets the HTTP method regex in the HTTPMatchRequest for the given HTTP methods in the format "^(PUT|POST|GET)$". func (mr *matchRequest) MethodRegEx(httpMethods ...apirulev1beta1.HttpMethod) *matchRequest { diff --git a/internal/builders/virtual_service_test.go b/internal/builders/virtual_service_test.go index 3a4398a77..69149bffa 100644 --- a/internal/builders/virtual_service_test.go +++ b/internal/builders/virtual_service_test.go @@ -35,7 +35,7 @@ var _ = Describe("Builder for", func() { vs := VirtualService().From(&initialVs).GenerateName(name).Namespace(namespace). Spec( VirtualServiceSpec(). - Host(host). + AddHost(host). Gateway(gateway)). Get() Expect(vs.Name).To(BeEmpty()) @@ -60,8 +60,8 @@ var _ = Describe("Builder for", func() { timeout := time.Second * 60 result := VirtualServiceSpec(). - Host(host). - Host(host2). + AddHost(host). + AddHost(host2). Gateway(gateway). Gateway(gateway2). HTTP(HTTPRoute(). @@ -132,7 +132,7 @@ var _ = Describe("Builder for", func() { } result := VirtualServiceSpec(). - Host(host). + AddHost(host). Gateway(gateway). HTTP(HTTPRoute(). CorsPolicy(CorsPolicy().FromApiRuleCorsPolicy(corsPolicy)). diff --git a/internal/processing/processors/istio/virtual_service_processor.go b/internal/processing/processors/istio/virtual_service_processor.go index 4a82a0f0e..96812346b 100644 --- a/internal/processing/processors/istio/virtual_service_processor.go +++ b/internal/processing/processors/istio/virtual_service_processor.go @@ -36,7 +36,7 @@ func (r virtualServiceCreator) Create(api *gatewayv1beta1.APIRule) (*networkingv virtualServiceNamePrefix := fmt.Sprintf("%s-", api.ObjectMeta.Name) vsSpecBuilder := builders.VirtualServiceSpec() - vsSpecBuilder.Host(default_domain.GetHostWithDomain(*api.Spec.Host, r.defaultDomainName)) + vsSpecBuilder.AddHost(default_domain.GetHostWithDomain(*api.Spec.Host, r.defaultDomainName)) vsSpecBuilder.Gateway(*api.Spec.Gateway) filteredRules := processing.FilterDuplicatePaths(api.Spec.Rules) diff --git a/internal/processing/processors/ory/virtual_service_processor.go b/internal/processing/processors/ory/virtual_service_processor.go index 60cedb671..629f2f570 100644 --- a/internal/processing/processors/ory/virtual_service_processor.go +++ b/internal/processing/processors/ory/virtual_service_processor.go @@ -36,7 +36,7 @@ func (r virtualServiceCreator) Create(api *gatewayv1beta1.APIRule) (*networkingv virtualServiceNamePrefix := fmt.Sprintf("%s-", api.ObjectMeta.Name) vsSpecBuilder := builders.VirtualServiceSpec() - vsSpecBuilder.Host(default_domain.GetHostWithDomain(*api.Spec.Host, r.defaultDomainName)) + vsSpecBuilder.AddHost(default_domain.GetHostWithDomain(*api.Spec.Host, r.defaultDomainName)) vsSpecBuilder.Gateway(*api.Spec.Gateway) filteredRules := processing.FilterDuplicatePaths(api.Spec.Rules) diff --git a/internal/processing/processors/v2alpha1/reconciliation.go b/internal/processing/processors/v2alpha1/reconciliation.go index 602080574..6f26c749c 100644 --- a/internal/processing/processors/v2alpha1/reconciliation.go +++ b/internal/processing/processors/v2alpha1/reconciliation.go @@ -56,20 +56,30 @@ func NewReconciliation(apiRuleV2alpha1 *gatewayv2alpha1.APIRule, apiRuleV1beta1 } func filterDuplicatePaths(rules []gatewayv2alpha1.Rule) []gatewayv2alpha1.Rule { - uniqueRules := make(map[string]gatewayv2alpha1.Rule) + duplicates := make(map[string]bool) + var filteredRules []gatewayv2alpha1.Rule for _, rule := range rules { - uniqueRules[rule.Path] = rule + if _, exists := duplicates[rule.Path]; !exists { + duplicates[rule.Path] = true + filteredRules = append(filteredRules, rule) + } } - var uniqueRulesSlice []gatewayv2alpha1.Rule - for _, rule := range uniqueRules { - uniqueRulesSlice = append(uniqueRulesSlice, rule) - } - return uniqueRulesSlice + + return filteredRules } func findServiceNamespace(api *gatewayv2alpha1.APIRule, rule *gatewayv2alpha1.Rule) string { - if rule.Service != nil && rule.Service.Namespace != nil { + // Fallback direction for the upstream service namespace: Rule.Service > Spec.Service > APIRule + if rule != nil && rule.Service != nil && rule.Service.Namespace != nil { return *rule.Service.Namespace } - return api.Namespace + if api != nil && api.Spec.Service != nil && api.Spec.Service.Namespace != nil { + return *api.Spec.Service.Namespace + } + + if api != nil { + return api.Namespace + } else { + return "" + } } diff --git a/internal/processing/processors/v2alpha1/virtual_service_processor.go b/internal/processing/processors/v2alpha1/virtual_service_processor.go index 0a7ace005..aa660160d 100644 --- a/internal/processing/processors/v2alpha1/virtual_service_processor.go +++ b/internal/processing/processors/v2alpha1/virtual_service_processor.go @@ -99,7 +99,7 @@ func (r virtualServiceCreator) Create(api *gatewayv2alpha1.APIRule) (*networking vsSpecBuilder := builders.VirtualServiceSpec() for _, host := range api.Spec.Hosts { - vsSpecBuilder.Host(default_domain.GetHostWithDomain(string(*host), r.defaultDomainName)) + vsSpecBuilder.AddHost(default_domain.GetHostWithDomain(string(*host), r.defaultDomainName)) } vsSpecBuilder.Gateway(*api.Spec.Gateway) @@ -124,7 +124,7 @@ func (r virtualServiceCreator) Create(api *gatewayv2alpha1.APIRule) (*networking httpRouteBuilder.Route(builders.RouteDestination().Host(host).Port(port)) - matchBuilder := builders.MatchRequest() + matchBuilder := builders.MatchRequest().MethodRegExV2Alpha1(rule.Methods...) if rule.Path == "/*" { matchBuilder.Uri().Prefix("/") From 589fba55fbfbe95e9e8ce334d588f04d020dff95 Mon Sep 17 00:00:00 2001 From: Chwila Date: Fri, 12 Jul 2024 12:55:36 +0200 Subject: [PATCH 09/12] Fix unit test --- .../processors/v2alpha1/virtual_service_processor_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/processing/processors/v2alpha1/virtual_service_processor_test.go b/internal/processing/processors/v2alpha1/virtual_service_processor_test.go index 159ffec83..98d2a49e1 100644 --- a/internal/processing/processors/v2alpha1/virtual_service_processor_test.go +++ b/internal/processing/processors/v2alpha1/virtual_service_processor_test.go @@ -197,7 +197,7 @@ var _ = Describe("GetVirtualServiceHttpTimeout", func() { timeout := processors.GetVirtualServiceHttpTimeout(apiRuleSpec, rule) // then - Expect(timeout).To(Equal(uint32(10))) + Expect(timeout).To(Equal(uint32(20))) }) }) From b5cd1fbd54902e42d108651c3f4204abe547d0a7 Mon Sep 17 00:00:00 2001 From: Chwila Date: Fri, 12 Jul 2024 14:28:49 +0200 Subject: [PATCH 10/12] Add unit tests --- .../processors/v2alpha1/cors_test.go | 77 ++++++ .../processors/v2alpha1/hosts_test.go | 43 ++++ .../processors/v2alpha1/http_matching_test.go | 51 ++++ .../processors/v2alpha1/reconciliation.go | 13 - .../processors/v2alpha1/test_builders_test.go | 11 +- .../processors/v2alpha1/timeout_test.go | 68 +++++ .../v2alpha1/virtual_service_processor.go | 3 +- .../virtual_service_processor_test.go | 242 ++++++++---------- 8 files changed, 349 insertions(+), 159 deletions(-) create mode 100644 internal/processing/processors/v2alpha1/cors_test.go create mode 100644 internal/processing/processors/v2alpha1/hosts_test.go create mode 100644 internal/processing/processors/v2alpha1/http_matching_test.go create mode 100644 internal/processing/processors/v2alpha1/timeout_test.go diff --git a/internal/processing/processors/v2alpha1/cors_test.go b/internal/processing/processors/v2alpha1/cors_test.go new file mode 100644 index 000000000..6cf8bf791 --- /dev/null +++ b/internal/processing/processors/v2alpha1/cors_test.go @@ -0,0 +1,77 @@ +package v2alpha1_test + +import ( + gatewayv2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" + "github.com/kyma-project/api-gateway/internal/builders" + processors "github.com/kyma-project/api-gateway/internal/processing/processors/v2alpha1" + istioapiv1beta1 "istio.io/api/networking/v1beta1" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/kyma-project/api-gateway/internal/processing/processing_test" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("CORS", func() { + var client client.Client + var processor processors.VirtualServiceProcessor + BeforeEach(func() { + client = GetFakeClient() + }) + + DescribeTable("CORS", + func(apiRule *gatewayv2alpha1.APIRule, verifiers []verifier, expectedActions ...string) { + processor = processors.NewVirtualServiceProcessor(GetTestConfig(), apiRule) + checkVirtualServices(client, processor, verifiers, expectedActions...) + }, + + Entry("should set default empty values in VirtualService CORSPolicy when no CORS configuration is set in APIRule", + newAPIRuleBuilderWithDummyDataWithNoAuthRule().Build(), + []verifier{ + func(vs *networkingv1beta1.VirtualService) { + Expect(vs.Spec.Http[0].CorsPolicy).To(BeNil()) + + Expect(vs.Spec.Http[0].Headers.Response.Remove).To(ConsistOf([]string{ + builders.ExposeHeadersName, + builders.MaxAgeName, + builders.AllowHeadersName, + builders.AllowCredentialsName, + builders.AllowMethodsName, + builders.AllowOriginName, + })) + }, + }, "create"), + + Entry("should apply all CORSPolicy headers correctly", + newAPIRuleBuilderWithDummyDataWithNoAuthRule().WithCORSPolicy( + newCorsPolicyBuilder(). + WithAllowOrigins([]map[string]string{{"exact": "example.com"}}). + WithAllowMethods([]string{"GET", "POST"}). + WithAllowHeaders([]string{"header1", "header2"}). + WithExposeHeaders([]string{"header3", "header4"}). + WithAllowCredentials(true). + WithMaxAge(600). + Build()). + Build(), + []verifier{func(vs *networkingv1beta1.VirtualService) { + Expect(vs.Spec.Http[0].CorsPolicy).NotTo(BeNil()) + Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(HaveLen(1)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins[0]).To(Equal(&istioapiv1beta1.StringMatch{MatchType: &istioapiv1beta1.StringMatch_Exact{Exact: "example.com"}})) + Expect(vs.Spec.Http[0].CorsPolicy.AllowMethods).To(ConsistOf("GET", "POST")) + Expect(vs.Spec.Http[0].CorsPolicy.AllowHeaders).To(ConsistOf("header1", "header2")) + Expect(vs.Spec.Http[0].CorsPolicy.ExposeHeaders).To(ConsistOf("header3", "header4")) + Expect(vs.Spec.Http[0].CorsPolicy.AllowCredentials.GetValue()).To(BeTrue()) + Expect(vs.Spec.Http[0].CorsPolicy.MaxAge.Seconds).To(Equal(int64(600))) + + Expect(vs.Spec.Http[0].Headers.Response.Remove).To(ConsistOf([]string{ + builders.ExposeHeadersName, + builders.MaxAgeName, + builders.AllowHeadersName, + builders.AllowCredentialsName, + builders.AllowMethodsName, + builders.AllowOriginName, + })) + }}, "create"), + ) +}) diff --git a/internal/processing/processors/v2alpha1/hosts_test.go b/internal/processing/processors/v2alpha1/hosts_test.go new file mode 100644 index 000000000..7cac36ff5 --- /dev/null +++ b/internal/processing/processors/v2alpha1/hosts_test.go @@ -0,0 +1,43 @@ +package v2alpha1_test + +import ( + gatewayv2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" + processors "github.com/kyma-project/api-gateway/internal/processing/processors/v2alpha1" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/kyma-project/api-gateway/internal/processing/processing_test" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Hosts", func() { + var client client.Client + var processor processors.VirtualServiceProcessor + BeforeEach(func() { + client = GetFakeClient() + }) + + DescribeTable("Hosts", + func(apiRule *gatewayv2alpha1.APIRule, verifiers []verifier, expectedActions ...string) { + processor = processors.NewVirtualServiceProcessor(GetTestConfig(), apiRule) + checkVirtualServices(client, processor, verifiers, expectedActions...) + }, + + Entry("should set the host correctly", + newAPIRuleBuilder().WithGateway("example/example").WithHost("example.com").Build(), + []verifier{ + func(vs *networkingv1beta1.VirtualService) { + Expect(vs.Spec.Hosts).To(ConsistOf("example.com")) + }, + }, "create"), + + Entry("should set multiple hosts correctly", + newAPIRuleBuilder().WithGateway("example/example").WithHosts("example.com", "goat.com").Build(), + []verifier{ + func(vs *networkingv1beta1.VirtualService) { + Expect(vs.Spec.Hosts).To(ConsistOf("example.com", "goat.com")) + }, + }, "create"), + ) +}) diff --git a/internal/processing/processors/v2alpha1/http_matching_test.go b/internal/processing/processors/v2alpha1/http_matching_test.go new file mode 100644 index 000000000..8c2138b9b --- /dev/null +++ b/internal/processing/processors/v2alpha1/http_matching_test.go @@ -0,0 +1,51 @@ +package v2alpha1_test + +import ( + gatewayv2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" + processors "github.com/kyma-project/api-gateway/internal/processing/processors/v2alpha1" + 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/processing/processing_test" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("HTTP matching", func() { + var client client.Client + var processor processors.VirtualServiceProcessor + BeforeEach(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...) + }, + Entry("from two rules with different methods on the same path should create two HTTP routes with different methods", + newAPIRuleBuilderWithDummyData(). + WithRules(newRuleBuilder().WithMethods(http.MethodGet).WithPath("/").NoAuth().Build(), + newRuleBuilder().WithMethods(http.MethodPut).WithPath("/").WithJWTAuthn("example.com", "https://jwks.example.com", nil, nil).Build()).Build(), + []verifier{ + func(vs *networkingv1beta1.VirtualService) { + Expect(vs.Spec.Http).To(HaveLen(2)) + + 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"), + + Entry("from one rule with two methods on the same path should create one HTTP route with regex matching both methods", + newAPIRuleBuilderWithDummyData(). + WithRules(newRuleBuilder().WithMethods(http.MethodGet, http.MethodPut).WithPath("/").NoAuth().Build()). + Build(), + []verifier{ + func(vs *networkingv1beta1.VirtualService) { + Expect(vs.Spec.Http).To(HaveLen(1)) + + Expect(vs.Spec.Http[0].Match[0].Method.GetRegex()).To(Equal("^(GET|PUT)$")) + }, + }, "create"), + ) +}) diff --git a/internal/processing/processors/v2alpha1/reconciliation.go b/internal/processing/processors/v2alpha1/reconciliation.go index 6f26c749c..176a91cab 100644 --- a/internal/processing/processors/v2alpha1/reconciliation.go +++ b/internal/processing/processors/v2alpha1/reconciliation.go @@ -55,19 +55,6 @@ func NewReconciliation(apiRuleV2alpha1 *gatewayv2alpha1.APIRule, apiRuleV1beta1 } } -func filterDuplicatePaths(rules []gatewayv2alpha1.Rule) []gatewayv2alpha1.Rule { - duplicates := make(map[string]bool) - var filteredRules []gatewayv2alpha1.Rule - for _, rule := range rules { - if _, exists := duplicates[rule.Path]; !exists { - duplicates[rule.Path] = true - filteredRules = append(filteredRules, rule) - } - } - - return filteredRules -} - func findServiceNamespace(api *gatewayv2alpha1.APIRule, rule *gatewayv2alpha1.Rule) string { // Fallback direction for the upstream service namespace: Rule.Service > Spec.Service > APIRule if rule != nil && rule.Service != nil && rule.Service.Namespace != nil { diff --git a/internal/processing/processors/v2alpha1/test_builders_test.go b/internal/processing/processors/v2alpha1/test_builders_test.go index 2ccf3e3a0..0b0e49d82 100644 --- a/internal/processing/processors/v2alpha1/test_builders_test.go +++ b/internal/processing/processors/v2alpha1/test_builders_test.go @@ -138,7 +138,7 @@ func newAPIRuleBuilder() *apiRuleBuilder { } } -// newAPIRuleBuilderWithDummyData returns an APIRuleBuilder pre-filled with placeholder data: +// newAPIRuleBuilderWithDummyDataWithNoAuthRule returns an APIRuleBuilder pre-filled with placeholder data: // // Host: example-host.example.com // @@ -149,7 +149,7 @@ func newAPIRuleBuilder() *apiRuleBuilder { // Rule: GET / // // Strategy: NoAuth -func newAPIRuleBuilderWithDummyData() *apiRuleBuilder { +func newAPIRuleBuilderWithDummyDataWithNoAuthRule() *apiRuleBuilder { return newAPIRuleBuilder(). WithHost("example-host.example.com"). WithGateway("example-namespace/example-gateway"). @@ -157,6 +157,13 @@ func newAPIRuleBuilderWithDummyData() *apiRuleBuilder { WithRule(*newRuleBuilder().WithMethods(http.MethodGet).WithPath("/").NoAuth().Build()) } +func newAPIRuleBuilderWithDummyData() *apiRuleBuilder { + return newAPIRuleBuilder(). + WithHost("example-host.example.com"). + WithGateway("example-namespace/example-gateway"). + WithService("example-service", "example-namespace", 8080) +} + type corsPolicyBuilder struct { policy gatewayv2alpha1.CorsPolicy } diff --git a/internal/processing/processors/v2alpha1/timeout_test.go b/internal/processing/processors/v2alpha1/timeout_test.go new file mode 100644 index 000000000..303d88b7f --- /dev/null +++ b/internal/processing/processors/v2alpha1/timeout_test.go @@ -0,0 +1,68 @@ +package v2alpha1_test + +import ( + gatewayv2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" + "k8s.io/utils/ptr" + + processors "github.com/kyma-project/api-gateway/internal/processing/processors/v2alpha1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("GetVirtualServiceHttpTimeout", func() { + It("should return default of 180s when no timeout is set", func() { + // given + apiRuleSpec := gatewayv2alpha1.APIRuleSpec{} + rule := gatewayv2alpha1.Rule{} + + // when + timeout := processors.GetVirtualServiceHttpTimeout(apiRuleSpec, rule) + + // then + Expect(timeout).To(Equal(uint32(180))) + }) + + It("should return the timeout set in the rule when it is set and APIRule has different value", func() { + // given + apiRuleSpec := gatewayv2alpha1.APIRuleSpec{ + Timeout: ptr.To(gatewayv2alpha1.Timeout(20)), + } + rule := gatewayv2alpha1.Rule{ + Timeout: ptr.To(gatewayv2alpha1.Timeout(10)), + } + + // when + timeout := processors.GetVirtualServiceHttpTimeout(apiRuleSpec, rule) + + // then + Expect(timeout).To(Equal(uint32(10))) + }) + + It("should return the timeout set in the rule when it is set and APIRule timeout is not", func() { + // given + apiRuleSpec := gatewayv2alpha1.APIRuleSpec{} + rule := gatewayv2alpha1.Rule{ + Timeout: ptr.To(gatewayv2alpha1.Timeout(10)), + } + + // when + timeout := processors.GetVirtualServiceHttpTimeout(apiRuleSpec, rule) + + // then + Expect(timeout).To(Equal(uint32(10))) + }) + + It("should return the timeout set in the APIRule it is set and rule timeout is not", func() { + // given + apiRuleSpec := gatewayv2alpha1.APIRuleSpec{ + Timeout: ptr.To(gatewayv2alpha1.Timeout(20)), + } + rule := gatewayv2alpha1.Rule{} + + // when + timeout := processors.GetVirtualServiceHttpTimeout(apiRuleSpec, rule) + + // then + Expect(timeout).To(Equal(uint32(20))) + }) +}) diff --git a/internal/processing/processors/v2alpha1/virtual_service_processor.go b/internal/processing/processors/v2alpha1/virtual_service_processor.go index aa660160d..41df5c41f 100644 --- a/internal/processing/processors/v2alpha1/virtual_service_processor.go +++ b/internal/processing/processors/v2alpha1/virtual_service_processor.go @@ -103,9 +103,8 @@ func (r virtualServiceCreator) Create(api *gatewayv2alpha1.APIRule) (*networking } vsSpecBuilder.Gateway(*api.Spec.Gateway) - filteredRules := filterDuplicatePaths(api.Spec.Rules) - for _, rule := range filteredRules { + for _, rule := range api.Spec.Rules { httpRouteBuilder := builders.HTTPRoute() serviceNamespace := findServiceNamespace(api, &rule) diff --git a/internal/processing/processors/v2alpha1/virtual_service_processor_test.go b/internal/processing/processors/v2alpha1/virtual_service_processor_test.go index 98d2a49e1..a288d9d59 100644 --- a/internal/processing/processors/v2alpha1/virtual_service_processor_test.go +++ b/internal/processing/processors/v2alpha1/virtual_service_processor_test.go @@ -6,18 +6,15 @@ import ( "github.com/kyma-project/api-gateway/internal/builders" . "github.com/kyma-project/api-gateway/internal/processing/processing_test" processors "github.com/kyma-project/api-gateway/internal/processing/processors/v2alpha1" - istioapiv1beta1 "istio.io/api/networking/v1beta1" - networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" - "k8s.io/utils/ptr" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - + istioapiv1beta1 "istio.io/api/networking/v1beta1" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" ) -var _ = Describe("VirtualServiceProcessor", func() { - It("should create virtual service when no virtual service exists", func() { +var _ = Describe("ObjectChange", func() { + It("should return create action when there is no VirtualService on cluster", func() { // given processor := processors.VirtualServiceProcessor{ ApiRule: &gatewayv2alpha1.APIRule{}, @@ -32,96 +29,71 @@ var _ = Describe("VirtualServiceProcessor", func() { Expect(result).To(HaveLen(1)) Expect(result[0].Action.String()).To(Equal("create")) }) -}) -func checkVirtualServices(c client.Client, processor processors.VirtualServiceProcessor, verifiers []verifier, expectedActions ...string) { - result, err := processor.EvaluateReconciliation(context.Background(), c) - Expect(err).To(BeNil()) - Expect(result).To(HaveLen(len(expectedActions))) - for i, action := range expectedActions { - Expect(result[i].Action.String()).To(Equal(action)) - } + It("should return update action when there is a matching VirtualService on cluster", func() { + // given + apiRuleBuilder := newAPIRuleBuilderWithDummyData() + processor := processors.NewVirtualServiceProcessor(GetTestConfig(), apiRuleBuilder.Build()) + result, err := processor.EvaluateReconciliation(context.Background(), GetFakeClient()) - for i, v := range verifiers { - v(result[i].Obj.(*networkingv1beta1.VirtualService)) - } -} + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + Expect(result[0].Action.String()).To(Equal("create")) -type verifier func(*networkingv1beta1.VirtualService) + // when + processor = processors.NewVirtualServiceProcessor(GetTestConfig(), apiRuleBuilder.WithHosts("newHost.com").Build()) + result, err = processor.EvaluateReconciliation(context.Background(), GetFakeClient(result[0].Obj.(*networkingv1beta1.VirtualService))) -var _ = Describe("Hosts", func() { - var client client.Client - var processor processors.VirtualServiceProcessor - BeforeEach(func() { - client = GetFakeClient() + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + Expect(result[0].Action.String()).To(Equal("update")) }) - - DescribeTable("Hosts", - func(apiRule *gatewayv2alpha1.APIRule, verifiers []verifier, expectedActions ...string) { - processor = processors.NewVirtualServiceProcessor(GetTestConfig(), apiRule) - checkVirtualServices(client, processor, verifiers, expectedActions...) - }, - - Entry("should set the host correctly", - newAPIRuleBuilder().WithGateway("example/example").WithHost("example.com").Build(), - []verifier{ - func(vs *networkingv1beta1.VirtualService) { - Expect(vs.Spec.Hosts).To(ConsistOf("example.com")) - }, - }, "create"), - - Entry("should set multiple hosts correctly", - newAPIRuleBuilder().WithGateway("example/example").WithHosts("example.com", "goat.com").Build(), - []verifier{ - func(vs *networkingv1beta1.VirtualService) { - Expect(vs.Spec.Hosts).To(ConsistOf("example.com", "goat.com")) - }, - }, "create"), - ) }) -var _ = Describe("CORS", func() { - var client client.Client - var processor processors.VirtualServiceProcessor - BeforeEach(func() { - client = GetFakeClient() - }) +var _ = Describe("Fully configured APIRule happy path", func() { + It("should create a VirtualService with all the configured values", func() { + apiRule := newAPIRuleBuilder(). + WithGateway("example/example"). + WithHosts("example.com", "goat.com"). + WithService("example-service", "example-namespace", 8080). + WithTimeout(180). + WithCORSPolicy(newCorsPolicyBuilder(). + WithAllowOrigins([]map[string]string{{"exact": "example.com"}}). + WithAllowMethods([]string{"GET", "POST"}). + WithAllowHeaders([]string{"header1", "header2"}). + WithExposeHeaders([]string{"header3", "header4"}). + WithAllowCredentials(true). + WithMaxAge(600). + Build()). + WithRules( + newRuleBuilder(). + WithService("another-service", "another-namespace", 9999). + WithMethods("GET", "POST"). + WithPath("/"). + WithJWTAuthn("example.com", "https://jwks.example.com", nil, nil). + WithTimeout(10).Build(), + + newRuleBuilder(). + WithMethods("PUT"). + WithPath("/*"). + NoAuth().Build(), + ). + Build() + + client := GetFakeClient() + processor := processors.NewVirtualServiceProcessor(GetTestConfig(), apiRule) + checkVirtualServices(client, processor, []verifier{ + func(vs *networkingv1beta1.VirtualService) { + Expect(vs.Spec.Hosts).To(ConsistOf("example.com", "goat.com")) + Expect(vs.Spec.Gateways).To(ConsistOf("example/example")) + Expect(vs.Spec.Http).To(HaveLen(2)) + + Expect(vs.Spec.Http[0].Match[0].Method.GetRegex()).To(Equal("^(GET|POST)$")) + Expect(vs.Spec.Http[0].Match[0].Uri.GetRegex()).To(Equal("/")) + Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal("another-service.another-namespace.svc.cluster.local")) + Expect(vs.Spec.Http[0].Route[0].Destination.Port.Number).To(Equal(uint32(9999))) - DescribeTable("CORS", - func(apiRule *gatewayv2alpha1.APIRule, verifiers []verifier, expectedActions ...string) { - processor = processors.NewVirtualServiceProcessor(GetTestConfig(), apiRule) - checkVirtualServices(client, processor, verifiers, expectedActions...) - }, - - Entry("should set default empty values in VirtualService CORSPolicy when no CORS configuration is set in APIRule", - newAPIRuleBuilderWithDummyData().Build(), - []verifier{ - func(vs *networkingv1beta1.VirtualService) { - Expect(vs.Spec.Http[0].CorsPolicy).To(BeNil()) - - Expect(vs.Spec.Http[0].Headers.Response.Remove).To(ConsistOf([]string{ - builders.ExposeHeadersName, - builders.MaxAgeName, - builders.AllowHeadersName, - builders.AllowCredentialsName, - builders.AllowMethodsName, - builders.AllowOriginName, - })) - }, - }, "create"), - - Entry("should apply all CORSPolicy headers correctly", - newAPIRuleBuilderWithDummyData().WithCORSPolicy( - newCorsPolicyBuilder(). - WithAllowOrigins([]map[string]string{{"exact": "example.com"}}). - WithAllowMethods([]string{"GET", "POST"}). - WithAllowHeaders([]string{"header1", "header2"}). - WithExposeHeaders([]string{"header3", "header4"}). - WithAllowCredentials(true). - WithMaxAge(600). - Build()). - Build(), - []verifier{func(vs *networkingv1beta1.VirtualService) { Expect(vs.Spec.Http[0].CorsPolicy).NotTo(BeNil()) Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(HaveLen(1)) Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins[0]).To(Equal(&istioapiv1beta1.StringMatch{MatchType: &istioapiv1beta1.StringMatch_Exact{Exact: "example.com"}})) @@ -131,75 +103,61 @@ var _ = Describe("CORS", func() { Expect(vs.Spec.Http[0].CorsPolicy.AllowCredentials.GetValue()).To(BeTrue()) Expect(vs.Spec.Http[0].CorsPolicy.MaxAge.Seconds).To(Equal(int64(600))) - Expect(vs.Spec.Http[0].Headers.Response.Remove).To(ConsistOf([]string{ - builders.ExposeHeadersName, - builders.MaxAgeName, - builders.AllowHeadersName, - builders.AllowCredentialsName, - builders.AllowMethodsName, - builders.AllowOriginName, - })) - }}, "create"), - ) -}) + Expect(vs.Spec.Http[0].Timeout.Seconds).To(Equal(int64(10))) -var _ = Describe("GetVirtualServiceHttpTimeout", func() { - It("should return default of 180s when no timeout is set", func() { - // given - apiRuleSpec := gatewayv2alpha1.APIRuleSpec{} - rule := gatewayv2alpha1.Rule{} + Expect(vs.Spec.Http[1].Match[0].Method.GetRegex()).To(Equal("^(PUT)$")) + Expect(vs.Spec.Http[1].Match[0].Uri.GetPrefix()).To(Equal("/")) + Expect(vs.Spec.Http[1].Route[0].Destination.Host).To(Equal("example-service.example-namespace.svc.cluster.local")) + Expect(vs.Spec.Http[1].Route[0].Destination.Port.Number).To(Equal(uint32(8080))) - // when - timeout := processors.GetVirtualServiceHttpTimeout(apiRuleSpec, rule) + Expect(vs.Spec.Http[1].CorsPolicy).NotTo(BeNil()) + Expect(vs.Spec.Http[1].CorsPolicy.AllowOrigins).To(HaveLen(1)) + Expect(vs.Spec.Http[1].CorsPolicy.AllowOrigins[0]).To(Equal(&istioapiv1beta1.StringMatch{MatchType: &istioapiv1beta1.StringMatch_Exact{Exact: "example.com"}})) + Expect(vs.Spec.Http[1].CorsPolicy.AllowMethods).To(ConsistOf("GET", "POST")) + Expect(vs.Spec.Http[1].CorsPolicy.AllowHeaders).To(ConsistOf("header1", "header2")) + Expect(vs.Spec.Http[1].CorsPolicy.ExposeHeaders).To(ConsistOf("header3", "header4")) + Expect(vs.Spec.Http[1].CorsPolicy.AllowCredentials.GetValue()).To(BeTrue()) + Expect(vs.Spec.Http[1].CorsPolicy.MaxAge.Seconds).To(Equal(int64(600))) - // then - Expect(timeout).To(Equal(uint32(180))) - }) + Expect(vs.Spec.Http[1].Timeout.Seconds).To(Equal(int64(180))) + }, + }, "create") - It("should return the timeout set in the rule when it is set and APIRule has different value", func() { - // given - apiRuleSpec := gatewayv2alpha1.APIRuleSpec{ - Timeout: ptr.To(gatewayv2alpha1.Timeout(20)), - } - rule := gatewayv2alpha1.Rule{ - Timeout: ptr.To(gatewayv2alpha1.Timeout(10)), - } - - // when - timeout := processors.GetVirtualServiceHttpTimeout(apiRuleSpec, rule) - - // then - Expect(timeout).To(Equal(uint32(10))) }) +}) - It("should return the timeout set in the rule when it is set and APIRule timeout is not", func() { +var _ = Describe("VirtualServiceProcessor", func() { + It("should create virtual service when no virtual service exists", func() { // given - apiRuleSpec := gatewayv2alpha1.APIRuleSpec{} - rule := gatewayv2alpha1.Rule{ - Timeout: ptr.To(gatewayv2alpha1.Timeout(10)), + processor := processors.VirtualServiceProcessor{ + ApiRule: &gatewayv2alpha1.APIRule{}, + Creator: mockVirtualServiceCreator{}, } // when - timeout := processors.GetVirtualServiceHttpTimeout(apiRuleSpec, rule) + result, err := processor.EvaluateReconciliation(context.Background(), GetFakeClient()) // then - Expect(timeout).To(Equal(uint32(10))) + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + Expect(result[0].Action.String()).To(Equal("create")) }) +}) - It("should return the timeout set in the APIRule it is set and rule timeout is not", func() { - // given - apiRuleSpec := gatewayv2alpha1.APIRuleSpec{ - Timeout: ptr.To(gatewayv2alpha1.Timeout(20)), - } - rule := gatewayv2alpha1.Rule{} +func checkVirtualServices(c client.Client, processor processors.VirtualServiceProcessor, verifiers []verifier, expectedActions ...string) { + result, err := processor.EvaluateReconciliation(context.Background(), c) + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(len(expectedActions))) + for i, action := range expectedActions { + Expect(result[i].Action.String()).To(Equal(action)) + } - // when - timeout := processors.GetVirtualServiceHttpTimeout(apiRuleSpec, rule) + for i, v := range verifiers { + v(result[i].Obj.(*networkingv1beta1.VirtualService)) + } +} - // then - Expect(timeout).To(Equal(uint32(20))) - }) -}) +type verifier func(*networkingv1beta1.VirtualService) type mockVirtualServiceCreator struct{} From 3dc06a116e928eba16550ab839b4d8a69d7a78f2 Mon Sep 17 00:00:00 2001 From: Chwila Date: Mon, 15 Jul 2024 14:19:06 +0200 Subject: [PATCH 11/12] Adapt the doc --- .../v2alpha1/04-10-apirule-custom-resource.md | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) 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 fb3faff21..3988ee3f2 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 @@ -13,38 +13,38 @@ This table lists all parameters of APIRule `v2alpha1` CRD together with their de **Spec:** -| Field | Required | Description | Validation | -|:-------------------------------------------------|:---------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------| -| **gateway** | **YES** | Specifies the Istio Gateway. | None | -| **corsPolicy** | **NO** | Allows configuring CORS headers sent with the response. If **corsPolicy** is not defined, the default values are applied. If **corsPolicy** is configured, only the CORS headers defined in the APIRule are sent with the response. For more information, see the [decision record](https://github.com/kyma-project/api-gateway/issues/752). | None | -| **corsPolicy.allowHeaders** | **NO** | Specifies headers allowed with the **Access-Control-Allow-Headers** CORS header. | None | -| **corsPolicy.allowMethods** | **NO** | Specifies methods allowed with the **Access-Control-Allow-Methods** CORS header. | None | -| **corsPolicy.allowOrigins** | **NO** | Specifies origins allowed with the **Access-Control-Allow-Origins** CORS header. | None | -| **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. 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 `*`. | -| **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 | -| **timeout** | **NO** | Specifies the timeout for HTTP requests in seconds for all Access Rules. The value can be overridden for each Access Rule.
If no timeout is specified, the default timeout of 180 seconds applies. | The maximum timeout is limited to 3900 seconds (65 minutes). | -| **rules** | **YES** | Specifies the list of Access Rules. | None | -| **rules.service** | **NO** | Services definitions at this level have higher precedence than the Service definition at the **spec.service** level. | None | -| **rules.path** | **YES** | Specifies the path of the exposed Service. If the path specified in an Access Rule overlaps with the path of another Access Rule, both Access Rules are applied. This happens, for example, if one of the Access Rules' configurations contains `*`. | The value can be either an exact path or a wildcard `*`. | -| **rules.methods** | **NO** | Specifies the list of HTTP request methods available for **spec.rules.path**. The list of supported methods is defined in [RFC 9910: HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110.html) and [RFC 5789: PATCH Method for HTTP](https://www.rfc-editor.org/rfc/rfc5789.html). | None | -| **rules.noAuth** | **NO** | Setting `noAuth` to `true` disables authorization. | When `noAuth` is set to true, it is not allowed to define the `jwt` access strategy on the same path. | -| **rules.jwt** | **NO** | Specifies the Istio JWT access strategy. | None | -| **rules.jwt.authentications** | **YES** | Specifies the list of authentication objects. | None | -| **rules.jwt.authentications.issuer** | **YES** | Identifies the issuer that issued the JWT.
The value must be a URL. Although HTTP is allowed, it is recommended that you use only HTTPS endpoints. | None | -| **rules.jwt.authentications.jwksUri** | **YES** | Contains the URL of the provider’s public key set to validate the signature of the JWT.
The value must be a URL. Although HTTP is allowed, it is recommended that you use only HTTPS endpoints. | None | -| **rules.jwt.authentications.fromHeaders** | **NO** | Specifies the list of headers from which the JWT token is extracted. | None | -| **rules.jwt.authentications.fromHeaders.name** | **YES** | Specifies the name of the header. | None | -| **rules.jwt.authentications.fromHeaders.prefix** | **NO** | Specifies the prefix used before the JWT token. The default is `Bearer`. | None | -| **rules.jwt.authentications.fromParams** | **NO** | Specifies the list of parameters from which the JWT token is extracted. | None | -| **rules.jwt.authorizations** | **NO** | Specifies the list of authorization objects. | None | -| **rules.jwt.authorizations.requiredScopes** | **NO** | Specifies the list of required scope values for the JWT. | None | -| **rules.jwt.authorizations.audiences** | **NO** | Specifies the list of audiences required for the JWT. | None | -| **rules.timeout** | **NO** | Specifies the timeout, in seconds, for HTTP requests made to **spec.rules.path**. Timeout definitions set at this level take precedence over any timeout defined at the **spec.timeout** level. | The maximum timeout is limited to 3900 seconds (65 minutes). | +| Field | Required | Description | Validation | +|:-------------------------------------------------|:---------:|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------| +| **gateway** | **YES** | Specifies the Istio Gateway. | None | +| **corsPolicy** | **NO** | Allows configuring CORS headers sent with the response. If **corsPolicy** is not defined, the CORS headers will be enforced to be empty. | None | +| **corsPolicy.allowHeaders** | **NO** | Specifies headers allowed with the **Access-Control-Allow-Headers** CORS header. | None | +| **corsPolicy.allowMethods** | **NO** | Specifies methods allowed with the **Access-Control-Allow-Methods** CORS header. | None | +| **corsPolicy.allowOrigins** | **NO** | Specifies origins allowed with the **Access-Control-Allow-Origins** CORS header. | None | +| **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. 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 `*`. | +| **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 | +| **timeout** | **NO** | Specifies the timeout for HTTP requests in seconds for all Access Rules. The value can be overridden for each Access Rule.
If no timeout is specified, the default timeout of 180 seconds applies. | The maximum timeout is limited to 3900 seconds (65 minutes). | +| **rules** | **YES** | Specifies the list of Access Rules. | None | +| **rules.service** | **NO** | Services definitions at this level have higher precedence than the Service definition at the **spec.service** level. | None | +| **rules.path** | **YES** | Specifies the path of the exposed Service. If the path specified in an Access Rule overlaps with the path of another Access Rule, both Access Rules are applied. This happens, for example, if one of the Access Rules' configurations contains `*`. | The value can be either an exact path or a wildcard `*`. | +| **rules.methods** | **NO** | Specifies the list of HTTP request methods available for **spec.rules.path**. The list of supported methods is defined in [RFC 9910: HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110.html) and [RFC 5789: PATCH Method for HTTP](https://www.rfc-editor.org/rfc/rfc5789.html). | None | +| **rules.noAuth** | **NO** | Setting `noAuth` to `true` disables authorization. | When `noAuth` is set to true, it is not allowed to define the `jwt` access strategy on the same path. | +| **rules.jwt** | **NO** | Specifies the Istio JWT access strategy. | None | +| **rules.jwt.authentications** | **YES** | Specifies the list of authentication objects. | None | +| **rules.jwt.authentications.issuer** | **YES** | Identifies the issuer that issued the JWT.
The value must be a URL. Although HTTP is allowed, it is recommended that you use only HTTPS endpoints. | None | +| **rules.jwt.authentications.jwksUri** | **YES** | Contains the URL of the provider’s public key set to validate the signature of the JWT.
The value must be a URL. Although HTTP is allowed, it is recommended that you use only HTTPS endpoints. | None | +| **rules.jwt.authentications.fromHeaders** | **NO** | Specifies the list of headers from which the JWT token is extracted. | None | +| **rules.jwt.authentications.fromHeaders.name** | **YES** | Specifies the name of the header. | None | +| **rules.jwt.authentications.fromHeaders.prefix** | **NO** | Specifies the prefix used before the JWT token. The default is `Bearer`. | None | +| **rules.jwt.authentications.fromParams** | **NO** | Specifies the list of parameters from which the JWT token is extracted. | None | +| **rules.jwt.authorizations** | **NO** | Specifies the list of authorization objects. | None | +| **rules.jwt.authorizations.requiredScopes** | **NO** | Specifies the list of required scope values for the JWT. | None | +| **rules.jwt.authorizations.audiences** | **NO** | Specifies the list of audiences required for the JWT. | None | +| **rules.timeout** | **NO** | Specifies the timeout, in seconds, for HTTP requests made to **spec.rules.path**. Timeout definitions set at this level take precedence over any timeout defined at the **spec.timeout** level. | The maximum timeout is limited to 3900 seconds (65 minutes). | > [!WARNING] > When you use an unsupported `v1beta1` configuration in version `v2alpha1` of the APIRule CR, you get an empty **spec**. See [supported access strategies](04-15-api-rule-access-strategies.md). From 7c23e7b4bf39bf94b954a68c71421313502424b9 Mon Sep 17 00:00:00 2001 From: Bartosz Chwila <103247439+barchw@users.noreply.github.com> Date: Mon, 15 Jul 2024 15:24:25 +0200 Subject: [PATCH 12/12] Update docs/user/custom-resources/apirule/v2alpha1/04-10-apirule-custom-resource.md Co-authored-by: Natalia Sitko <80401180+nataliasitko@users.noreply.github.com> --- .../apirule/v2alpha1/04-10-apirule-custom-resource.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3988ee3f2..6f87d74bc 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 @@ -16,7 +16,7 @@ This table lists all parameters of APIRule `v2alpha1` CRD together with their de | Field | Required | Description | Validation | |:-------------------------------------------------|:---------:|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------| | **gateway** | **YES** | Specifies the Istio Gateway. | None | -| **corsPolicy** | **NO** | Allows configuring CORS headers sent with the response. If **corsPolicy** is not defined, the CORS headers will be enforced to be empty. | None | +| **corsPolicy** | **NO** | Allows configuring CORS headers sent with the response. If **corsPolicy** is not defined, the CORS headers are enforced to be empty. | None | | **corsPolicy.allowHeaders** | **NO** | Specifies headers allowed with the **Access-Control-Allow-Headers** CORS header. | None | | **corsPolicy.allowMethods** | **NO** | Specifies methods allowed with the **Access-Control-Allow-Methods** CORS header. | None | | **corsPolicy.allowOrigins** | **NO** | Specifies origins allowed with the **Access-Control-Allow-Origins** CORS header. | None |