Skip to content

Commit

Permalink
Configuration weight (1/4)
Browse files Browse the repository at this point in the history
Related to #50
The weight configuration goes into the GSLB strategy. This PR only aims
to load and validate the weight configuration, which is specified in percentages.

```yaml
  strategy:
    type: roundRobin # Use a round robin load balancing strategy, when deciding which downstream clusters to route clients too
    weight: 20%
```
gslb.spec.strategy.weight is not allowed for failover and geoip strategies. If it is not specified in roundRobin,
then it is not a weight RoundRobin but classic one.

Signed-off-by: kuritka <kuritka@gmail.com>

move RoundRobinStrategy, FailoverStrategy, GeoStrategy constants into depresolver package
  • Loading branch information
kuritka committed Jun 7, 2022
1 parent 0c7da00 commit 6e6e3ae
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 55 deletions.
27 changes: 27 additions & 0 deletions api/v1beta1/gslb_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ Generated by GoLic, for more details see: https://github.com/AbsaOSS/golic
*/

import (
"strconv"
"strings"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand All @@ -30,6 +33,8 @@ import (
type Strategy struct {
// Load balancing strategy type:(roundRobin|failover)
Type string `json:"type"`
// roundrobin and in the future consistent may (but also may not) contain a weight
Weight Percentage `json:"weight,omitempty"`
// Primary Geo Tag. Valid for failover strategy only
PrimaryGeoTag string `json:"primaryGeoTag,omitempty"`
// Defines DNS record TTL in seconds
Expand Down Expand Up @@ -95,3 +100,25 @@ func (h HealthStatus) String() string {
func init() {
SchemeBuilder.Register(&Gslb{}, &GslbList{})
}

type Percentage string

func (p Percentage) String() string {
return string(p)
}

func (p Percentage) IsEmpty() bool {
return string(p) == ""
}

func (p Percentage) TryParse() (v int, err error) {
if p.IsEmpty() {
return 0, nil
}
return strconv.Atoi(strings.TrimSuffix(strings.ReplaceAll(p.String(), " ", ""), "%"))
}

func (p Percentage) Int() int {
v, _ := p.TryParse()
return v
}
72 changes: 37 additions & 35 deletions chart/k8gb/templates/crds/k8gb.absa.oss_gslbs.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
{{- if .Values.k8gb.deployCrds }}
---
{{- if .Values.k8gb.deployCrds }}---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.7.0
controller-gen.kubebuilder.io/version: v0.8.0
creationTimestamp: null
name: gslbs.k8gb.absa.oss
spec:
Expand Down Expand Up @@ -129,27 +128,26 @@ spec:
a network host, as defined by RFC 3986. Note the following
deviations from the \"host\" part of the URI as defined
in RFC 3986: 1. IPs are not allowed. Currently an IngressRuleValue
can only apply to the IP in the Spec of the parent
Ingress. 2. The `:` delimiter is not respected because
ports are not allowed. \t Currently the port of an Ingress
is implicitly :80 for http and \t :443 for https. Both
these may change in the future. Incoming requests are
matched against the host before the IngressRuleValue.
If the host is unspecified, the Ingress routes all traffic
based on the specified IngressRuleValue. \n Host can be
\"precise\" which is a domain name without the terminating
dot of a network host (e.g. \"foo.bar.com\") or \"wildcard\",
which is a domain name prefixed with a single wildcard
label (e.g. \"*.foo.com\"). The wildcard character '*'
must appear by itself as the first DNS label and matches
only a single label. You cannot have a wildcard label
by itself (e.g. Host == \"*\"). Requests will be matched
against the Host field in the following way: 1. If Host
is precise, the request matches this rule if the http
host header is equal to Host. 2. If Host is a wildcard,
then the request matches this rule if the http host header
is to equal to the suffix (removing the first label) of
the wildcard rule."
can only apply to the IP in the Spec of the parent Ingress.
2. The `:` delimiter is not respected because ports are
not allowed. Currently the port of an Ingress is implicitly
:80 for http and :443 for https. Both these may change
in the future. Incoming requests are matched against the
host before the IngressRuleValue. If the host is unspecified,
the Ingress routes all traffic based on the specified
IngressRuleValue. \n Host can be \"precise\" which is
a domain name without the terminating dot of a network
host (e.g. \"foo.bar.com\") or \"wildcard\", which is
a domain name prefixed with a single wildcard label (e.g.
\"*.foo.com\"). The wildcard character '*' must appear
by itself as the first DNS label and matches only a single
label. You cannot have a wildcard label by itself (e.g.
Host == \"*\"). Requests will be matched against the Host
field in the following way: 1. If Host is precise, the
request matches this rule if the http host header is equal
to Host. 2. If Host is a wildcard, then the request matches
this rule if the http host header is to equal to the suffix
(removing the first label) of the wildcard rule."
type: string
http:
description: 'HTTPIngressRuleValue is a list of http selectors
Expand Down Expand Up @@ -245,19 +243,19 @@ spec:
of the Path matching. PathType can be one of
the following values: * Exact: Matches the URL
path exactly. * Prefix: Matches based on a URL
path prefix split by ''/''. Matching is done
path prefix split by ''/''. Matching is done
on a path element by element basis. A path element
refers is the list of labels in the path split
by the ''/'' separator. A request is a match
refers is the list of labels in the path split
by the ''/'' separator. A request is a match
for path p if every p is an element-wise prefix
of p of the request path. Note that if the
last element of the path is a substring of
the last element in request path, it is not
a match (e.g. /foo/bar matches /foo/bar/baz,
but does not match /foo/barbaz). * ImplementationSpecific:
Interpretation of the Path matching is up to the
IngressClass. Implementations can treat this
as a separate PathType or treat it identically
of p of the request path. Note that if the last
element of the path is a substring of the last
element in request path, it is not a match (e.g.
/foo/bar matches /foo/bar/baz, but does not
match /foo/barbaz). * ImplementationSpecific:
Interpretation of the Path matching is up to
the IngressClass. Implementations can treat
this as a separate PathType or treat it identically
to Prefix or Exact path types. Implementations
are required to support all path types.'
type: string
Expand Down Expand Up @@ -320,6 +318,10 @@ spec:
type:
description: Load balancing strategy type:(roundRobin|failover)
type: string
weight:
description: roundrobin and in the future consistent may (but
also may not) contain a weight
type: string
required:
- type
type: object
Expand Down
9 changes: 9 additions & 0 deletions controllers/depresolver/depresolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ const (
DNSTypeMultipleProviders EdgeDNSType = "MultipleProviders"
)

const (
// GeoIP Strategy
GeoStrategy = "geoip"
// RoundRobin Strategy
RoundRobinStrategy = "roundRobin"
// Failover Strategy
FailoverStrategy = "failover"
)

// Log configuration
type Log struct {
// Level [panic, fatal, error,warn,info,debug,trace], defines level of logger, default: info
Expand Down
13 changes: 13 additions & 0 deletions controllers/depresolver/depresolver_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,18 @@ func (dr *DependencyResolver) validateSpec(strategy k8gbv1beta1.Strategy) (err e
if err != nil {
return
}
w, err := strategy.Weight.TryParse()
if err != nil {
return
}
if !strategy.Weight.IsEmpty() {
if strategy.Type != RoundRobinStrategy {
return fmt.Errorf(`"spec.strategy.weight" is allowed only for roundRobin strategy`)
}
err = field("Weight", w).isHigherOrEqualToZero().isLessOrEqualTo(100).err
if err != nil {
return
}
}
return
}
45 changes: 45 additions & 0 deletions controllers/depresolver/depresolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ func TestResolveSpecWithFilledFields(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, 35, gslb.Spec.Strategy.DNSTtlSeconds)
assert.Equal(t, 305, gslb.Spec.Strategy.SplitBrainThresholdSeconds)
assert.False(t, gslb.Spec.Strategy.Weight.IsEmpty())
assert.Equal(t, 20, gslb.Spec.Strategy.Weight.Int())
}

func TestResolveSpecWithoutFields(t *testing.T) {
Expand All @@ -110,6 +112,9 @@ func TestResolveSpecWithoutFields(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, predefinedStrategy.DNSTtlSeconds, gslb.Spec.Strategy.DNSTtlSeconds)
assert.Equal(t, predefinedStrategy.SplitBrainThresholdSeconds, gslb.Spec.Strategy.SplitBrainThresholdSeconds)
assert.Equal(t, 0, gslb.Spec.Strategy.Weight.Int())
assert.Equal(t, "", gslb.Spec.Strategy.Weight.String())
assert.True(t, gslb.Spec.Strategy.Weight.IsEmpty())
}

func TestResolveSpecWithZeroSplitBrain(t *testing.T) {
Expand Down Expand Up @@ -146,6 +151,46 @@ func TestResolveSpecWithNegativeFields(t *testing.T) {
assert.Error(t, err)
}

func TestResolveSpecWithWeightCornerCases(t *testing.T) {
type test struct {
strategy string
weight k8gbv1beta1.Percentage
expectederr bool
empty bool
}
tests := []test{
{RoundRobinStrategy, "-20%", true, false},
{RoundRobinStrategy, "-20", true, false},
{RoundRobinStrategy, "+20", false, false},
{RoundRobinStrategy, " 20% ", false, false},
{RoundRobinStrategy, "20%", false, false},
{RoundRobinStrategy, "20", false, false},
{RoundRobinStrategy, "100%", false, false},
{RoundRobinStrategy, "101", true, false},
{RoundRobinStrategy, "101%", true, false},
{RoundRobinStrategy, "0", false, false},
{RoundRobinStrategy, "0%", false, false},
{RoundRobinStrategy, "", false, true},
{RoundRobinStrategy, "blah", true, false},
{FailoverStrategy, "10%", true, false},
{GeoStrategy, "0%", true, false},
}
// arrange
cl, gslb := getTestContext("./testdata/filled_omitempty.yaml")
resolver := NewDependencyResolver()
// act
for _, tt := range tests {
t.Run(fmt.Sprintf("weight_%s_%s", tt.strategy, tt.weight), func(t *testing.T) {
gslb.Spec.Strategy.Weight = tt.weight
gslb.Spec.Strategy.Type = tt.strategy
err := resolver.ResolveGslbSpec(context.TODO(), gslb, cl)
// assert
assert.Equal(t, tt.empty, gslb.Spec.Strategy.Weight.IsEmpty())
assert.True(t, (err != nil) == tt.expectederr)
})
}
}

func TestSpecRunWhenChanged(t *testing.T) {
// arrange
cl, gslb := getTestContext("./testdata/filled_omitempty.yaml")
Expand Down
1 change: 1 addition & 0 deletions controllers/depresolver/testdata/filled_omitempty.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ spec:
type: roundRobin # Use a round robin load balancing strategy, when deciding which downstream clusters to route clients too
splitBrainThresholdSeconds: 305
dnsTtlSeconds: 35
weight: 20%

2 changes: 2 additions & 0 deletions controllers/depresolver/testdata/invalid_omitempty_empty.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,6 @@ spec:
strategy:
type: roundRobin # Use a round robin load balancing strategy, when deciding which downstream clusters to route clients too
splitBrainThresholdSeconds:
weight:


12 changes: 7 additions & 5 deletions controllers/dnsupdate.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"sort"
"strings"

"github.com/k8gb-io/k8gb/controllers/depresolver"

k8gbv1beta1 "github.com/k8gb-io/k8gb/api/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
Expand Down Expand Up @@ -79,9 +81,9 @@ func (r *GslbReconciler) gslbDNSEndpoint(gslb *k8gbv1beta1.Gslb) (*externaldns.D

if len(externalTargets) > 0 {
switch gslb.Spec.Strategy.Type {
case roundRobinStrategy, geoStrategy:
case depresolver.RoundRobinStrategy, depresolver.GeoStrategy:
finalTargets = append(finalTargets, externalTargets...)
case failoverStrategy:
case depresolver.FailoverStrategy:
// If cluster is Primary
if isPrimary {
// If cluster is Primary and Healthy return only own targets
Expand Down Expand Up @@ -156,11 +158,11 @@ func (r *GslbReconciler) gslbDNSEndpoint(gslb *k8gbv1beta1.Gslb) (*externaldns.D

func (r *GslbReconciler) updateRuntimeStatus(gslb *k8gbv1beta1.Gslb, isPrimary bool, isHealthy k8gbv1beta1.HealthStatus, finalTargets []string) {
switch gslb.Spec.Strategy.Type {
case roundRobinStrategy:
case depresolver.RoundRobinStrategy:
m.UpdateRoundrobinStatus(gslb, isHealthy, finalTargets)
case geoStrategy:
case depresolver.GeoStrategy:
m.UpdateGeoIPStatus(gslb, isHealthy, finalTargets)
case failoverStrategy:
case depresolver.FailoverStrategy:
m.UpdateFailoverStatus(gslb, isPrimary, isHealthy, finalTargets)
}
}
13 changes: 5 additions & 8 deletions controllers/gslb_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,6 @@ type GslbReconciler struct {

const (
gslbFinalizer = "k8gb.absa.oss/finalizer"
geoStrategy = "geoip"
roundRobinStrategy = "roundRobin"
failoverStrategy = "failover"
primaryGeoTagAnnotation = "k8gb.io/primary-geotag"
strategyAnnotation = "k8gb.io/strategy"
dnsTTLSecondsAnnotation = "k8gb.io/dns-ttl-seconds"
Expand Down Expand Up @@ -284,7 +281,7 @@ func (r *GslbReconciler) SetupWithManager(mgr ctrl.Manager) error {
}
}

if strategy == failoverStrategy {
if strategy == depresolver.FailoverStrategy {
for annotationKey, annotationValue := range a.GetAnnotations() {
if annotationKey == primaryGeoTagAnnotation {
gslb.Spec.Strategy.PrimaryGeoTag = annotationValue
Expand Down Expand Up @@ -321,10 +318,10 @@ func (r *GslbReconciler) SetupWithManager(mgr ctrl.Manager) error {
for annotationKey, annotationValue := range a.GetAnnotations() {
if annotationKey == strategyAnnotation {
switch annotationValue {
case roundRobinStrategy:
createGslbFromIngress(annotationKey, annotationValue, a, roundRobinStrategy)
case failoverStrategy:
createGslbFromIngress(annotationKey, annotationValue, a, failoverStrategy)
case depresolver.RoundRobinStrategy:
createGslbFromIngress(annotationKey, annotationValue, a, depresolver.RoundRobinStrategy)
case depresolver.FailoverStrategy:
createGslbFromIngress(annotationKey, annotationValue, a, depresolver.FailoverStrategy)
}
}
}
Expand Down
12 changes: 6 additions & 6 deletions controllers/gslb_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ func TestReturnsOwnRecordsUsingFailoverStrategyWhenPrimary(t *testing.T) {
RecordTTL: 30,
RecordType: "A",
Targets: externaldns.Targets{"10.0.0.1", "10.0.0.2", "10.0.0.3"},
Labels: externaldns.Labels{"strategy": failoverStrategy},
Labels: externaldns.Labels{"strategy": depresolver.FailoverStrategy},
},
}
ingressIPs := []corev1.LoadBalancerIngress{
Expand All @@ -553,7 +553,7 @@ func TestReturnsOwnRecordsUsingFailoverStrategyWhenPrimary(t *testing.T) {
require.NoError(t, err, "Failed to update gslb Ingress Address")

// enable failover strategy
settings.gslb.Spec.Strategy.Type = failoverStrategy
settings.gslb.Spec.Strategy.Type = depresolver.FailoverStrategy
settings.gslb.Spec.Strategy.PrimaryGeoTag = "eu"
err = settings.client.Update(context.TODO(), settings.gslb)
require.NoError(t, err, "Can't update gslb")
Expand Down Expand Up @@ -587,7 +587,7 @@ func TestReturnsExternalRecordsUsingFailoverStrategy(t *testing.T) {
RecordTTL: 30,
RecordType: "A",
Targets: externaldns.Targets{"10.1.0.1", "10.1.0.2", "10.1.0.3"},
Labels: externaldns.Labels{"strategy": failoverStrategy},
Labels: externaldns.Labels{"strategy": depresolver.FailoverStrategy},
},
}
ingressIPs := []corev1.LoadBalancerIngress{
Expand Down Expand Up @@ -620,7 +620,7 @@ func TestReturnsExternalRecordsUsingFailoverStrategy(t *testing.T) {
require.NoError(t, err, "Failed to update gslb Ingress Address")

// enable failover strategy
settings.gslb.Spec.Strategy.Type = failoverStrategy
settings.gslb.Spec.Strategy.Type = depresolver.FailoverStrategy
settings.gslb.Spec.Strategy.PrimaryGeoTag = "eu"
err = settings.client.Update(context.TODO(), settings.gslb)
require.NoError(t, err, "Can't update gslb")
Expand Down Expand Up @@ -655,7 +655,7 @@ func TestReturnsExternalRecordsUsingFailoverStrategyAndFallbackDNSserver(t *test
RecordTTL: 30,
RecordType: "A",
Targets: externaldns.Targets{"10.1.0.1", "10.1.0.2"},
Labels: externaldns.Labels{"strategy": failoverStrategy},
Labels: externaldns.Labels{"strategy": depresolver.FailoverStrategy},
},
}
ingressIPs := []corev1.LoadBalancerIngress{
Expand Down Expand Up @@ -694,7 +694,7 @@ func TestReturnsExternalRecordsUsingFailoverStrategyAndFallbackDNSserver(t *test
require.NoError(t, err, "Failed to update gslb Ingress Address")

// enable failover strategy
settings.gslb.Spec.Strategy.Type = failoverStrategy
settings.gslb.Spec.Strategy.Type = depresolver.FailoverStrategy
settings.gslb.Spec.Strategy.PrimaryGeoTag = "eu"
err = settings.client.Update(context.TODO(), settings.gslb)
require.NoError(t, err, "Can't update gslb")
Expand Down
3 changes: 2 additions & 1 deletion terratest/test/k8gb_abstract_full_roundrobin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ Generated by GoLic, for more details see: https://github.com/AbsaOSS/golic

import (
"fmt"
"github.com/stretchr/testify/require"
"k8gbterratest/utils"
"testing"

"github.com/stretchr/testify/require"
)

func abstractTestFullRoundRobin(t *testing.T, n int) {
Expand Down

0 comments on commit 6e6e3ae

Please # to comment.