diff --git a/api/v1beta1/gslb_types.go b/api/v1beta1/gslb_types.go index d39132a5c9..88780ad764 100644 --- a/api/v1beta1/gslb_types.go +++ b/api/v1beta1/gslb_types.go @@ -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" ) @@ -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 @@ -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 +} diff --git a/chart/k8gb/templates/crds/k8gb.absa.oss_gslbs.yaml b/chart/k8gb/templates/crds/k8gb.absa.oss_gslbs.yaml index ef901e0d55..36e87785a0 100644 --- a/chart/k8gb/templates/crds/k8gb.absa.oss_gslbs.yaml +++ b/chart/k8gb/templates/crds/k8gb.absa.oss_gslbs.yaml @@ -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: @@ -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 @@ -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 @@ -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 diff --git a/controllers/depresolver/depresolver.go b/controllers/depresolver/depresolver.go index 172fb3579b..fc29efe896 100644 --- a/controllers/depresolver/depresolver.go +++ b/controllers/depresolver/depresolver.go @@ -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 diff --git a/controllers/depresolver/depresolver_spec.go b/controllers/depresolver/depresolver_spec.go index 5fa4acbf0a..688e7f6c59 100644 --- a/controllers/depresolver/depresolver_spec.go +++ b/controllers/depresolver/depresolver_spec.go @@ -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 } diff --git a/controllers/depresolver/depresolver_test.go b/controllers/depresolver/depresolver_test.go index 91ba481b18..08fcc4a3b8 100644 --- a/controllers/depresolver/depresolver_test.go +++ b/controllers/depresolver/depresolver_test.go @@ -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) { @@ -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) { @@ -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") diff --git a/controllers/depresolver/testdata/filled_omitempty.yaml b/controllers/depresolver/testdata/filled_omitempty.yaml index 61f97708ae..36cb242c26 100644 --- a/controllers/depresolver/testdata/filled_omitempty.yaml +++ b/controllers/depresolver/testdata/filled_omitempty.yaml @@ -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% diff --git a/controllers/depresolver/testdata/invalid_omitempty_empty.yaml b/controllers/depresolver/testdata/invalid_omitempty_empty.yaml index 63ed26dfe9..7ee8f9801b 100644 --- a/controllers/depresolver/testdata/invalid_omitempty_empty.yaml +++ b/controllers/depresolver/testdata/invalid_omitempty_empty.yaml @@ -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: + diff --git a/controllers/dnsupdate.go b/controllers/dnsupdate.go index 2bbdeedc97..b1cce4edd3 100644 --- a/controllers/dnsupdate.go +++ b/controllers/dnsupdate.go @@ -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" @@ -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 @@ -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) } } diff --git a/controllers/gslb_controller.go b/controllers/gslb_controller.go index 6b21c83045..ad455ffd15 100644 --- a/controllers/gslb_controller.go +++ b/controllers/gslb_controller.go @@ -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" @@ -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 @@ -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) } } } diff --git a/controllers/gslb_controller_test.go b/controllers/gslb_controller_test.go index 863da15390..a99bf90a99 100644 --- a/controllers/gslb_controller_test.go +++ b/controllers/gslb_controller_test.go @@ -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{ @@ -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") @@ -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{ @@ -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") @@ -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{ @@ -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") diff --git a/terratest/test/k8gb_abstract_full_roundrobin_test.go b/terratest/test/k8gb_abstract_full_roundrobin_test.go index b8530aaa9c..a6970dcb55 100644 --- a/terratest/test/k8gb_abstract_full_roundrobin_test.go +++ b/terratest/test/k8gb_abstract_full_roundrobin_test.go @@ -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) {