Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RLP Defaults #456

Merged
merged 12 commits into from
Apr 10, 2024
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ jobs:
- name: Create k8s Kind Cluster
uses: helm/[email protected]
with:
version: v0.20.0
version: v0.22.0
config: utils/kind-cluster.yaml
cluster_name: ${{ env.KIND_CLUSTER_NAME }}
wait: 120s
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ $(OPM):
opm: $(OPM) ## Download opm locally if necessary.

KIND = $(PROJECT_PATH)/bin/kind
KIND_VERSION = v0.20.0
KIND_VERSION = v0.22.0
$(KIND):
$(call go-install-tool,$(KIND),sigs.k8s.io/kind@$(KIND_VERSION))

Expand Down Expand Up @@ -393,7 +393,7 @@ run: generate fmt vet ## Run a controller from your host.
go run ./main.go

docker-build: ## Build docker image with the manager.
docker build -t $(IMG) .
docker build -t $(IMG) . --load
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If using podman runtime with docker cli also, there was the following warning locally for me:

 => CACHED [stage-1 2/3] COPY --from=builder /workspace/manager .                                                                                  0.0s
WARNING: No output specified with docker-container driver. Build result will only remain in the build cache. To push result image into registry use --push or to load image into docker use --load

This can cause the cluster to use an old build image as the newly built image is kept in cache only


docker-push: ## Push docker image with the manager.
docker push $(IMG)
Expand Down
27 changes: 26 additions & 1 deletion api/v1beta2/ratelimitpolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,25 @@ func (l Limit) CountersAsStringList() []string {

// RateLimitPolicySpec defines the desired state of RateLimitPolicy
// +kubebuilder:validation:XValidation:rule="self.targetRef.kind != 'Gateway' || !has(self.limits) || !self.limits.exists(x, has(self.limits[x].routeSelectors))",message="route selectors not supported when targeting a Gateway"
// +kubebuilder:validation:XValidation:rule="!(has(self.defaults) && has(self.limits))",message="Implicit and explicit defaults are mutually exclusive"
type RateLimitPolicySpec struct {
// TargetRef identifies an API object to apply policy to.
// +kubebuilder:validation:XValidation:rule="self.group == 'gateway.networking.k8s.io'",message="Invalid targetRef.group. The only supported value is 'gateway.networking.k8s.io'"
// +kubebuilder:validation:XValidation:rule="self.kind == 'HTTPRoute' || self.kind == 'Gateway'",message="Invalid targetRef.kind. The only supported values are 'HTTPRoute' and 'Gateway'"
TargetRef gatewayapiv1alpha2.PolicyTargetReference `json:"targetRef"`

// Defaults define explicit default values for this policy and for policies inheriting this policy.
// Defaults are mutually exclusive with implicit defaults defined by RateLimitPolicyCommonSpec.
// +optional
Defaults *RateLimitPolicyCommonSpec `json:"defaults,omitempty"`

// RateLimitPolicyCommonSpec defines implicit default values for this policy and for policies inheriting this policy.
// RateLimitPolicyCommonSpec is mutually exclusive with explicit defaults defined by Defaults.
RateLimitPolicyCommonSpec `json:""`
}

// RateLimitPolicyCommonSpec contains common shared fields.
type RateLimitPolicyCommonSpec struct {
// Limits holds the struct of limits indexed by a unique name
// +optional
// +kubebuilder:validation:MaxProperties=14
Expand Down Expand Up @@ -237,7 +250,7 @@ func (r *RateLimitPolicy) GetWrappedNamespace() gatewayapiv1.Namespace {

func (r *RateLimitPolicy) GetRulesHostnames() (ruleHosts []string) {
ruleHosts = make([]string, 0)
for _, limit := range r.Spec.Limits {
for _, limit := range r.Spec.CommonSpec().Limits {
for _, routeSelector := range limit.RouteSelectors {
convertHostnamesToString := func(gwHostnames []gatewayapiv1.Hostname) []string {
hostnames := make([]string, 0, len(gwHostnames))
Expand All @@ -264,6 +277,18 @@ func (r *RateLimitPolicy) DirectReferenceAnnotationName() string {
return RateLimitPolicyDirectReferenceAnnotationName
}

// CommonSpec returns the Default RateLimitPolicyCommonSpec if it is defined.
// Otherwise, it returns the RateLimitPolicyCommonSpec from the spec.
// This function should be used instead of accessing the fields directly, so that either the explicit or implicit default
// is returned.
func (r *RateLimitPolicySpec) CommonSpec() *RateLimitPolicyCommonSpec {
if r.Defaults != nil {
return r.Defaults
}

return &r.RateLimitPolicyCommonSpec
}

func init() {
SchemeBuilder.Register(&RateLimitPolicy{}, &RateLimitPolicyList{})
}
85 changes: 67 additions & 18 deletions api/v1beta2/ratelimitpolicy_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import (
"strings"
"testing"

"gotest.tools/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1"
gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"

"github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant"
)

func testBuildBasicRLP(name string, kind gatewayapiv1.Kind) *RateLimitPolicy {
return &RateLimitPolicy{
func testBuildBasicRLP(name string, kind gatewayapiv1.Kind, mutateFn func(*RateLimitPolicy)) *RateLimitPolicy {
p := &RateLimitPolicy{
TypeMeta: metav1.TypeMeta{
Kind: "RateLimitPolicy",
APIVersion: GroupVersion.String(),
Expand All @@ -31,32 +32,36 @@ func testBuildBasicRLP(name string, kind gatewayapiv1.Kind) *RateLimitPolicy {
},
},
}
}

func testBuildBasicGatewayRLP(name string) *RateLimitPolicy {
return testBuildBasicRLP(name, gatewayapiv1.Kind("Gateway"))
if mutateFn != nil {
mutateFn(p)
}

return p
}

func testBuildBasicHTTPRouteRLP(name string) *RateLimitPolicy {
return testBuildBasicRLP(name, gatewayapiv1.Kind("HTTPRoute"))
func testBuildBasicHTTPRouteRLP(name string, mutateFn func(*RateLimitPolicy)) *RateLimitPolicy {
return testBuildBasicRLP(name, "HTTPRoute", mutateFn)
}

// TestRateLimitPolicyValidation calls rlp.Validate()
// for a valid return value.
func TestRateLimitPolicyValidation(t *testing.T) {
name := "httproute-a"

// Different namespace
rlp := testBuildBasicHTTPRouteRLP(name)
otherNS := gatewayapiv1.Namespace(rlp.GetNamespace() + "other")
rlp.Spec.TargetRef.Namespace = &otherNS
err := rlp.Validate()
if err == nil {
t.Fatal(`rlp.Validate() did not return error and should`)
}
if !strings.Contains(err.Error(), "invalid targetRef.Namespace") {
t.Fatalf(`rlp.Validate() did not return expected error. Instead: %v`, err)
}
t.Run("Invalid - Different namespace", func(subT *testing.T) {
rlp := testBuildBasicHTTPRouteRLP(name, func(policy *RateLimitPolicy) {
otherNS := gatewayapiv1.Namespace(policy.GetNamespace() + "other")
policy.Spec.TargetRef.Namespace = &otherNS
})
err := rlp.Validate()
if err == nil {
subT.Fatal(`rlp.Validate() did not return error and should`)
}
if !strings.Contains(err.Error(), "invalid targetRef.Namespace") {
subT.Fatalf(`rlp.Validate() did not return expected error. Instead: %v`, err)
}
})
}

func TestRateLimitPolicyListGetItems(t *testing.T) {
Expand All @@ -75,3 +80,47 @@ func TestRateLimitPolicyListGetItems(t *testing.T) {
t.Errorf("Expected item to be a Policy")
}
}

func TestRateLimitPolicy_GetLimits(t *testing.T) {
const name = "policy"
var (
defaultLimits = map[string]Limit{
"default": {
Rates: []Rate{{Limit: 10, Duration: 1, Unit: "seconds"}},
},
}
implicitLimits = map[string]Limit{
"implicit": {
Rates: []Rate{{Limit: 20, Duration: 2, Unit: "minutes"}},
},
}
)

t.Run("No limits defined", func(subT *testing.T) {
r := testBuildBasicHTTPRouteRLP(name, nil)
assert.DeepEqual(subT, r.Spec.CommonSpec().Limits, map[string]Limit(nil))
})
t.Run("Defaults defined", func(subT *testing.T) {
r := testBuildBasicHTTPRouteRLP(name, func(policy *RateLimitPolicy) {
policy.Spec.Defaults = &RateLimitPolicyCommonSpec{
Limits: defaultLimits,
}
})
assert.DeepEqual(subT, r.Spec.CommonSpec().Limits, defaultLimits)
})
t.Run("Implicit rules defined", func(subT *testing.T) {
r := testBuildBasicHTTPRouteRLP(name, func(policy *RateLimitPolicy) {
policy.Spec.Limits = implicitLimits
})
assert.DeepEqual(subT, r.Spec.CommonSpec().Limits, implicitLimits)
})
t.Run("Default rules takes precedence over implicit rules if validation is somehow bypassed", func(subT *testing.T) {
r := testBuildBasicHTTPRouteRLP(name, func(policy *RateLimitPolicy) {
policy.Spec.Defaults = &RateLimitPolicyCommonSpec{
Limits: defaultLimits,
}
policy.Spec.Limits = implicitLimits
})
assert.DeepEqual(subT, r.Spec.CommonSpec().Limits, defaultLimits)
})
}
33 changes: 27 additions & 6 deletions api/v1beta2/zz_generated.deepcopy.go

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

Loading
Loading