Skip to content

Commit

Permalink
Add optional namespace for service in APIRule (#29)
Browse files Browse the repository at this point in the history
* Initial impl

* Refactor and unit tests

* Update apirule_types.go

* gofmt -s -w .

* Update internal/processing/processing_test.go

Co-authored-by: Bartosz Chwila <103247439+barchw@users.noreply.github.com>

* Update internal/processing/processing_test.go

Co-authored-by: Bartosz Chwila <103247439+barchw@users.noreply.github.com>

* Update internal/processing/processing_test.go

Co-authored-by: Bartosz Chwila <103247439+barchw@users.noreply.github.com>

* Better safe than sorry

Co-authored-by: Bartosz Chwila <103247439+barchw@users.noreply.github.com>
  • Loading branch information
videlov and barchw authored Sep 28, 2022
1 parent 953625e commit 064be15
Show file tree
Hide file tree
Showing 19 changed files with 284 additions and 38 deletions.
10 changes: 5 additions & 5 deletions api/v1alpha1/apiRule_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
runtime "k8s.io/apimachinery/pkg/runtime"
)

//StatusCode .
// StatusCode .
type StatusCode string

const (
Expand Down Expand Up @@ -53,7 +53,7 @@ type APIRuleStatus struct {
AccessRuleStatus *APIRuleResourceStatus `json:"accessRuleStatus,omitempty"`
}

//APIRule is the Schema for the apis ApiRule
// APIRule is the Schema for the apis ApiRule
// +kubebuilder:deprecatedversion:warning=v1alpha1 is deprecated as of Kyma 2.5.X
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
Expand All @@ -74,7 +74,7 @@ type APIRuleList struct {
Items []APIRule `json:"items"`
}

//Service .
// Service .
type Service struct {
// Name of the service
Name *string `json:"name"`
Expand All @@ -92,7 +92,7 @@ type Service struct {
IsExternal *bool `json:"external,omitempty"`
}

//Rule .
// Rule .
type Rule struct {
// Path to be exposed
// +kubebuilder:validation:Pattern=^([0-9a-zA-Z./*()?!\\_-]+)
Expand All @@ -108,7 +108,7 @@ type Rule struct {
Mutators []*Mutator `json:"mutators,omitempty"`
}

//APIRuleResourceStatus .
// APIRuleResourceStatus .
type APIRuleResourceStatus struct {
Code StatusCode `json:"code,omitempty"`
Description string `json:"desc,omitempty"`
Expand Down
4 changes: 2 additions & 2 deletions api/v1alpha1/groupversion_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ limitations under the License.
*/

// Package v1alpha1 contains API Schema definitions for the gateway v1alpha1 API group
//+kubebuilder:object:generate=true
//+groupName=gateway.kyma-project.io
// +kubebuilder:object:generate=true
// +groupName=gateway.kyma-project.io
package v1alpha1

import (
Expand Down
2 changes: 1 addition & 1 deletion api/v1alpha1/internal_types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package v1alpha1

//JWTAccStrConfig is used to deserialize jwt accessStrategy configuration for the validation purposes
// JWTAccStrConfig is used to deserialize jwt accessStrategy configuration for the validation purposes
type JWTAccStrConfig struct {
TrustedIssuers []string `json:"trusted_issuers,omitempty"`
JWKSUrls []string `json:"jwks_urls,omitempty"`
Expand Down
14 changes: 9 additions & 5 deletions api/v1beta1/apirule_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
runtime "k8s.io/apimachinery/pkg/runtime"
)

//StatusCode .
// StatusCode .
type StatusCode string

const (
Expand Down Expand Up @@ -59,7 +59,7 @@ type APIRuleStatus struct {
AccessRuleStatus *APIRuleResourceStatus `json:"accessRuleStatus,omitempty"`
}

//APIRule is the Schema for the apis ApiRule
// APIRule is the Schema for the apis ApiRule
// +kubebuilder:storageversion
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
Expand All @@ -80,10 +80,14 @@ type APIRuleList struct {
Items []APIRule `json:"items"`
}

//Service .
// Service .
type Service struct {
// Name of the service
Name *string `json:"name"`
// Namespace of the service, if omitted will default to the APIRule namespace
// +kubebuilder:validation:Pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
// +optional
Namespace *string `json:"namespace,omitempty"`
// Port of the service to expose
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=65535
Expand All @@ -93,7 +97,7 @@ type Service struct {
IsExternal *bool `json:"external,omitempty"`
}

//Rule .
// Rule .
type Rule struct {
// Path to be exposed
// +kubebuilder:validation:Pattern=^([0-9a-zA-Z./*()?!\\_-]+)
Expand All @@ -112,7 +116,7 @@ type Rule struct {
Mutators []*Mutator `json:"mutators,omitempty"`
}

//APIRuleResourceStatus .
// APIRuleResourceStatus .
type APIRuleResourceStatus struct {
Code StatusCode `json:"code,omitempty"`
Description string `json:"desc,omitempty"`
Expand Down
4 changes: 2 additions & 2 deletions api/v1beta1/groupversion_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ limitations under the License.
*/

// Package v1beta1 contains API Schema definitions for the gateway v1beta1 API group
//+kubebuilder:object:generate=true
//+groupName=gateway.kyma-project.io
// +kubebuilder:object:generate=true
// +groupName=gateway.kyma-project.io
package v1beta1

import (
Expand Down
2 changes: 1 addition & 1 deletion api/v1beta1/internal_types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package v1beta1

//JWTAccStrConfig is used to deserialize jwt accessStrategy configuration for the validation purposes
// JWTAccStrConfig is used to deserialize jwt accessStrategy configuration for the validation purposes
type JWTAccStrConfig struct {
TrustedIssuers []string `json:"trusted_issuers,omitempty"`
JWKSUrls []string `json:"jwks_urls,omitempty"`
Expand Down
5 changes: 5 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions config/crd/bases/gateway.kyma-project.io_apirules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,11 @@ spec:
name:
description: Name of the service
type: string
namespace:
description: Namespace of the service, if omitted will default
to the APIRule namespace
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
port:
description: Port of the service to expose
format: int32
Expand Down Expand Up @@ -320,6 +325,11 @@ spec:
name:
description: Name of the service
type: string
namespace:
description: Namespace of the service, if omitted will default
to the APIRule namespace
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
port:
description: Port of the service to expose
format: int32
Expand Down
6 changes: 3 additions & 3 deletions controllers/apirule_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func (r *APIRuleReconciler) SetupWithManager(mgr ctrl.Manager) error {
Complete(r)
}

//Sets status of APIRule. Accepts an auxilary status code that is used to report VirtualService and AccessRule status.
// Sets status of APIRule. Accepts an auxilary status code that is used to report VirtualService and AccessRule status.
func (r *APIRuleReconciler) setStatus(ctx context.Context, api *gatewayv1beta1.APIRule, apiStatus *gatewayv1beta1.APIRuleResourceStatus, auxStatusCode gatewayv1beta1.StatusCode) (ctrl.Result, error) {
virtualServiceStatus := &gatewayv1beta1.APIRuleResourceStatus{
Code: auxStatusCode,
Expand All @@ -151,7 +151,7 @@ func (r *APIRuleReconciler) setStatus(ctx context.Context, api *gatewayv1beta1.A
return r.updateStatusOrRetry(ctx, api, apiStatus, virtualServiceStatus, accessRuleStatus)
}

//Sets status of APIRule in error condition. Accepts an auxilary status code that is used to report VirtualService and AccessRule status.
// Sets status of APIRule in error condition. Accepts an auxilary status code that is used to report VirtualService and AccessRule status.
func (r *APIRuleReconciler) setStatusForError(ctx context.Context, api *gatewayv1beta1.APIRule, err error, auxStatusCode gatewayv1beta1.StatusCode) (ctrl.Result, error) {
r.Log.Error(err, "Error during reconciliation")

Expand All @@ -165,7 +165,7 @@ func (r *APIRuleReconciler) setStatusForError(ctx context.Context, api *gatewayv
return r.updateStatusOrRetry(ctx, api, generateErrorStatus(err), virtualServiceStatus, accessRuleStatus)
}

//Updates api status. If there was an error during update, returns the error so that entire reconcile loop is retried. If there is no error, returns a "reconcile success" value.
// Updates api status. If there was an error during update, returns the error so that entire reconcile loop is retried. If there is no error, returns a "reconcile success" value.
func (r *APIRuleReconciler) updateStatusOrRetry(ctx context.Context, api *gatewayv1beta1.APIRule, apiStatus, virtualServiceStatus, accessRuleStatus *gatewayv1beta1.APIRuleResourceStatus) (ctrl.Result, error) {
_, updateStatusErr := r.updateStatus(ctx, api, apiStatus, virtualServiceStatus, accessRuleStatus)
if updateStatusErr != nil {
Expand Down
16 changes: 16 additions & 0 deletions internal/helpers/namespace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package helpers

import (
gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1"
)

func FindServiceNamespace(api *gatewayv1beta1.APIRule, rule *gatewayv1beta1.Rule) *string {
// 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
}
if api.Spec.Service != nil && api.Spec.Service.Namespace != nil {
return api.Spec.Service.Namespace
}
return &api.ObjectMeta.Namespace
}
6 changes: 4 additions & 2 deletions internal/processing/access_rule_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,14 @@ func generateAccessRuleSpec(api *gatewayv1beta1.APIRule, rule gatewayv1beta1.Rul
Authenticators(builders.Authenticators().From(accessStrategies)).
Mutators(builders.Mutators().From(rule.Mutators))

serviceNamespace := helpers.FindServiceNamespace(api, &rule)

// Use rule level service if it exists
if rule.Service != nil {
return accessRuleSpec.Upstream(builders.Upstream().
URL(fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", *rule.Service.Name, api.ObjectMeta.Namespace, int(*rule.Service.Port)))).Get()
URL(fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", *rule.Service.Name, *serviceNamespace, int(*rule.Service.Port)))).Get()
}
// Otherwise use service defined on APIRule spec level
return accessRuleSpec.Upstream(builders.Upstream().
URL(fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", *api.Spec.Service.Name, api.ObjectMeta.Namespace, int(*api.Spec.Service.Port)))).Get()
URL(fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", *api.Spec.Service.Name, *serviceNamespace, int(*api.Spec.Service.Port)))).Get()
}
121 changes: 119 additions & 2 deletions internal/processing/processing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,44 @@ var _ = Describe("Factory", func() {
Expect(len(accessRules)).To(Equal(0))
})

It("should override VS destination host for specified spec level service namespace", func() {
strategies := []*gatewayv1beta1.Authenticator{
{
Handler: &gatewayv1beta1.Handler{
Name: "allow",
},
},
}

allowRule := getRuleFor(apiPath, apiMethods, []*gatewayv1beta1.Mutator{}, strategies)
rules := []gatewayv1beta1.Rule{allowRule}

apiRule := getAPIRuleFor(rules)

overrideServiceName := "testName"
overrideServiceNamespace := "testName-namespace"
overrideServicePort := uint32(8080)

apiRule.Spec.Service = &gatewayv1beta1.Service{
Name: &overrideServiceName,
Namespace: &overrideServiceNamespace,
Port: &overrideServicePort,
}

f := NewFactory(nil, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain)

desiredState := f.CalculateRequiredState(apiRule)
vs := desiredState.virtualService
accessRules := desiredState.accessRules

//verify VS has rule level destination host
Expect(len(vs.Spec.Http[0].Route)).To(Equal(1))
Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(overrideServiceName + "." + overrideServiceNamespace + ".svc.cluster.local"))

//Verify AR does not exist
Expect(len(accessRules)).To(Equal(0))
})

It("noop: should override access rule upstream with rule level service", func() {
strategies := []*gatewayv1beta1.Authenticator{
{
Expand Down Expand Up @@ -164,7 +202,49 @@ var _ = Describe("Factory", func() {
Expect(accessRules[expectedNoopRuleMatchURL].Spec.Upstream.URL).To(Equal(expectedRuleUpstreamURL))
})

It("allow: should override VS destination host", func() {
It("noop: should override access rule upstream with rule level service for specified namespace", func() {
strategies := []*gatewayv1beta1.Authenticator{
{
Handler: &gatewayv1beta1.Handler{
Name: "noop",
},
},
}

overrideServiceName := "testName"
overrideServiceNamespace := "testName-namespace"
overrideServicePort := uint32(8080)

service := &gatewayv1beta1.Service{
Name: &overrideServiceName,
Namespace: &overrideServiceNamespace,
Port: &overrideServicePort,
}

allowRule := getRuleWithServiceFor(apiPath, apiMethods, []*gatewayv1beta1.Mutator{}, strategies, service)
rules := []gatewayv1beta1.Rule{allowRule}

apiRule := getAPIRuleFor(rules)

f := NewFactory(nil, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain)

desiredState := f.CalculateRequiredState(apiRule)
vs := desiredState.virtualService
accessRules := desiredState.accessRules

expectedNoopRuleMatchURL := fmt.Sprintf("<http|https>://%s<%s>", serviceHost, apiPath)
expectedRuleUpstreamURL := fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", overrideServiceName, overrideServiceNamespace, overrideServicePort)

//Verify VS
Expect(len(vs.Spec.Http[0].Route)).To(Equal(1))
Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(oathkeeperSvc))

//Verify AR has rule level upstream
Expect(len(accessRules)).To(Equal(1))
Expect(accessRules[expectedNoopRuleMatchURL].Spec.Upstream.URL).To(Equal(expectedRuleUpstreamURL))
})

It("allow: should override VS destination host with rule level service", func() {
strategies := []*gatewayv1beta1.Authenticator{
{
Handler: &gatewayv1beta1.Handler{
Expand Down Expand Up @@ -200,6 +280,44 @@ var _ = Describe("Factory", func() {
Expect(len(accessRules)).To(Equal(0))
})

It("allow: should override VS destination host with rule level service for specified namespace", func() {
strategies := []*gatewayv1beta1.Authenticator{
{
Handler: &gatewayv1beta1.Handler{
Name: "allow",
},
},
}

overrideServiceName := "testName"
overrideServiceNamespace := "testName-namespace"
overrideServicePort := uint32(8080)

service := &gatewayv1beta1.Service{
Name: &overrideServiceName,
Namespace: &overrideServiceNamespace,
Port: &overrideServicePort,
}

allowRule := getRuleWithServiceFor(apiPath, apiMethods, []*gatewayv1beta1.Mutator{}, strategies, service)
rules := []gatewayv1beta1.Rule{allowRule}

apiRule := getAPIRuleFor(rules)

f := NewFactory(nil, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain)

desiredState := f.CalculateRequiredState(apiRule)
vs := desiredState.virtualService
accessRules := desiredState.accessRules

//verify VS has rule level destination host
Expect(len(vs.Spec.Http[0].Route)).To(Equal(1))
Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(overrideServiceName + "." + overrideServiceNamespace + ".svc.cluster.local"))

//Verify AR does not exist
Expect(len(accessRules)).To(Equal(0))
})

It("should produce VS and ARs for given paths", func() {
noop := []*gatewayv1beta1.Authenticator{
{
Expand Down Expand Up @@ -889,7 +1007,6 @@ var _ = Describe("Factory", func() {

apiRule := getAPIRuleFor(rules)


rule := rulev1alpha1.Rule{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
Expand Down
6 changes: 4 additions & 2 deletions internal/processing/virtual_service_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@ func (f *Factory) generateVirtualService(api *gatewayv1beta1.APIRule) *networkin
for _, rule := range filteredRules {
httpRouteBuilder := builders.HTTPRoute()
host, port := f.oathkeeperSvc, f.oathkeeperSvcPort
serviceNamespace := helpers.FindServiceNamespace(api, &rule)

if !isSecured(rule) {
// Use rule level service if it exists
if rule.Service != nil {
host = fmt.Sprintf("%s.%s.svc.cluster.local", *rule.Service.Name, api.ObjectMeta.Namespace)
host = fmt.Sprintf("%s.%s.svc.cluster.local", *rule.Service.Name, *serviceNamespace)
port = *rule.Service.Port
} else {
// Otherwise use service defined on APIRule spec level
host = fmt.Sprintf("%s.%s.svc.cluster.local", *api.Spec.Service.Name, api.ObjectMeta.Namespace)
host = fmt.Sprintf("%s.%s.svc.cluster.local", *api.Spec.Service.Name, *serviceNamespace)
port = *api.Spec.Service.Port
}
}
Expand Down
Loading

0 comments on commit 064be15

Please sign in to comment.